From 86db36bf8a67714309e87c6372a5b2d213628724 Mon Sep 17 00:00:00 2001 From: 0xnullifier Date: Fri, 6 Mar 2026 02:18:42 +0530 Subject: [PATCH 01/71] feat: use multisig sdk instead of the base http client --- package.json | 2 + src/lib/miden-chain/constants.ts | 2 + src/lib/miden/activity/transactions.ts | 75 ++++++++++ src/lib/miden/front/psm-manager.ts | 60 ++++++++ src/lib/miden/psm/account.ts | 68 ++++++++++ src/lib/miden/psm/index.ts | 181 +++++++++++++++++++++++++ src/lib/miden/psm/signer.ts | 43 ++++++ yarn.lock | 49 +++++-- 8 files changed, 465 insertions(+), 15 deletions(-) create mode 100644 src/lib/miden/front/psm-manager.ts create mode 100644 src/lib/miden/psm/account.ts create mode 100644 src/lib/miden/psm/index.ts create mode 100644 src/lib/miden/psm/signer.ts diff --git a/package.json b/package.json index 37963547a..4e8826741 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,8 @@ "@miden-sdk/miden-sdk": "^0.13.0", "@newhighsco/storybook-addon-svgr": "^2.0.7", "@noble/hashes": "^1.4.0", + "@openzeppelin/miden-multisig-client": "^0.13.0", + "@openzeppelin/psm-client": "^0.13.0", "@peculiar/webcrypto": "1.1.6", "@radix-ui/react-slot": "^1.2.3", "@segment/analytics-node": "^2.3.0", diff --git a/src/lib/miden-chain/constants.ts b/src/lib/miden-chain/constants.ts index 7bfb00e26..850feab48 100644 --- a/src/lib/miden-chain/constants.ts +++ b/src/lib/miden-chain/constants.ts @@ -61,3 +61,5 @@ export enum MidenTokens { export const TOKEN_MAPPING = { [MidenTokens.Miden]: { faucetId: 'mtst1aqmat9m63ctdsgz6xcyzpuprpulwk9vg_qruqqypuyph' } }; + +export const DEFAULT_PSM_ENDPOINT = 'https://psm-stg.openzeppelin.com'; diff --git a/src/lib/miden/activity/transactions.ts b/src/lib/miden/activity/transactions.ts index 53f735af8..b15b7379b 100644 --- a/src/lib/miden/activity/transactions.ts +++ b/src/lib/miden/activity/transactions.ts @@ -5,6 +5,7 @@ import { NoteFilter, NoteFilterTypes, NoteId, + TransactionRequest, TransactionResult } from '@miden-sdk/miden-sdk'; import { liveQuery } from 'dexie'; @@ -12,6 +13,7 @@ import { liveQuery } from 'dexie'; import { consumeNoteId } from 'lib/miden-worker/consumeNoteId'; import { sendTransaction } from 'lib/miden-worker/sendTransaction'; import { submitTransaction } from 'lib/miden-worker/submitTransaction'; +import { getOrCreateMultisigService, isPsmAccount } from 'lib/miden/front/psm-manager'; import * as Repo from 'lib/miden/repo'; import { isExtension, isMobile } from 'lib/platform'; import { u8ToB64 } from 'lib/shared/helpers'; @@ -598,6 +600,18 @@ export const generateTransaction = async ( await updateTransactionStatus(transaction.id, ITransactionStatus.GeneratingTransaction, { processingStartedAt: Math.floor(Date.now() / 1000) // seconds }); +<<<<<<< HEAD +======= + console.log('Generating transaction', { + txId: transaction.id, + type: transaction.type, + accountId: transaction.accountId + }); + // Route PSM accounts through PSM service + if (isPsmAccount(transaction.accountId)) { + await generatePsmTransaction(transaction, signCallback); + return; + } // Process transaction let result: TransactionResult; @@ -667,6 +681,67 @@ export const generateTransaction = async ( } }; +/** + * Generate a transaction for a PSM account using the MultisigService. + * Routes the transaction through MultisigService proposal methods. + */ +const generatePsmTransaction = async ( + transaction: ITransaction, + signCallback: (publicKey: string, signingInputs: string) => Promise +): Promise => { + const multisigService = await getOrCreateMultisigService(transaction.accountId, signCallback); + + let proposalResult; + + switch (transaction.type) { + case 'send': { + const sendTx = transaction as SendTransaction; + proposalResult = await multisigService.createSendProposal( + sendTx.secondaryAccountId, + sendTx.faucetId, + BigInt(sendTx.amount) + ); + break; + } + case 'consume': { + const consumeTx = transaction as ConsumeTransaction; + proposalResult = await multisigService.createConsumeNotesProposal([consumeTx.noteId]); + break; + } + case 'execute': + default: { + // For custom transactions, get TransactionSummary and create a custom proposal + const summaryBytes = await withWasmClientLock(async () => { + const midenClient = await getMidenClient(); + const txRequest = TransactionRequest.deserialize(transaction.requestBytes!); + return ( + await midenClient.webClient.executeForSummary(accountIdStringToSdk(transaction.accountId), txRequest) + ).serialize(); + }); + proposalResult = await multisigService.createCustomProposal(summaryBytes); + break; + } + } + + // Get the proposal commitment for signing and execution + const proposalCommitment = proposalResult.proposal.commitment; + console.log('Transaction proposal created with commitment:', proposalCommitment); + + // Sign and execute the proposal + await multisigService.signAndExecuteProposal(proposalCommitment); + + await multisigService.sync(); + // Determine display message based on transaction type + const displayMessage = + transaction.type === 'send' ? 'Sent' : transaction.type === 'consume' ? 'Received' : 'Completed'; + + // Update transaction as completed + await updateTransactionStatus(transaction.id, ITransactionStatus.Completed, { + displayMessage, + completedAt: Math.floor(Date.now() / 1000) + }); +}; + export const cancelTransaction = async (transaction: Transaction, error: any) => { // Cancel the transaction await Repo.transactions.where({ id: transaction.id }).modify(dbTx => { diff --git a/src/lib/miden/front/psm-manager.ts b/src/lib/miden/front/psm-manager.ts new file mode 100644 index 000000000..3ef47b129 --- /dev/null +++ b/src/lib/miden/front/psm-manager.ts @@ -0,0 +1,60 @@ +import { MultisigService } from 'lib/miden/psm'; +import { useWalletStore } from 'lib/store'; +import { WalletType } from 'screens/onboarding/types'; + +import { MULTISIG_SLOT_NAMES } from '../psm/account'; +import { getMidenClient, withWasmClientLock } from '../sdk/miden-client'; +/** + * Get or create a MultisigService for the given account (lazy initialization). + * Services are cached in memory and reused for subsequent calls. + */ +export async function getOrCreateMultisigService( + accountPublicKey: string, + signCallback: (publicKey: string, signingInputs: string) => Promise +): Promise { + // Verify this is a PSM account using Zustand store + const accounts = useWalletStore.getState().accounts; + const account = accounts.find(acc => acc.publicKey === accountPublicKey); + if (!account || account.type !== WalletType.Psm) { + throw new Error('Account is not a PSM account'); + } + + // Get the Account object and WebClient from Miden client + const { sdkAccount } = await withWasmClientLock(async () => { + const midenClient = await getMidenClient(); + const sdkAccount = await midenClient.getAccount(accountPublicKey); + return { sdkAccount }; + }); + + if (!sdkAccount) { + throw new Error('Account not found in local storage'); + } + + const mapEntries = sdkAccount.storage().getMapEntries(MULTISIG_SLOT_NAMES.SIGNER_PUBLIC_KEYS); + if (!mapEntries) { + throw new Error('No signer public keys found in account storage'); + } + + const commitment = mapEntries[0].value.slice(2); + if (!commitment) { + throw new Error('Commitment not found in account storage'); + } + + // Get the actual public key from the public key commitment + const publicKey = await useWalletStore.getState().getPublicKeyForCommitment(commitment); + + // Initialize MultisigService with the account, public key, commitment, sign function, and webClient + const service = await MultisigService.init(sdkAccount, `0x${publicKey}`, `0x${commitment}`, signCallback); + + return service; +} + +/** + * Check if an account is a PSM account and has completed at least one transaction. + * PSM routing only applies after the first transaction. + */ +export function isPsmAccount(accountPublicKey: string): boolean { + const accounts = useWalletStore.getState().accounts; + const account = accounts.find(acc => acc.publicKey === accountPublicKey); + return account?.type === WalletType.Psm; +} diff --git a/src/lib/miden/psm/account.ts b/src/lib/miden/psm/account.ts new file mode 100644 index 000000000..268ac6515 --- /dev/null +++ b/src/lib/miden/psm/account.ts @@ -0,0 +1,68 @@ +import { Account, AuthSecretKey, WebClient } from '@miden-sdk/miden-sdk'; +import { createMultisigAccount, MultisigClient } from '@openzeppelin/miden-multisig-client'; + +import { DEFAULT_PSM_ENDPOINT } from 'lib/miden-chain/constants'; +import { PSM_URL_STORAGE_KEY } from 'lib/settings/constants'; + +import { fetchFromStorage } from '../front'; + +// Re-export the slot names from the package for reading account state +export const MULTISIG_SLOT_NAMES = { + THRESHOLD_CONFIG: 'openzeppelin::multisig::threshold_config', + SIGNER_PUBLIC_KEYS: 'openzeppelin::multisig::signer_public_keys', + EXECUTED_TRANSACTIONS: 'openzeppelin::multisig::executed_transactions', + PROCEDURE_THRESHOLDS: 'openzeppelin::multisig::procedure_thresholds' +} as const; + +/** + * Create a PSM (Private State Manager) account using the MultisigClient. + * + * This creates a 1-of-1 multisig account with PSM signature verification enabled. + * The account is registered with the PSM backend and the secret key is stored locally. + * + * @param webClient - The Miden WebClient instance + * @param seed - Optional seed for key derivation (random if not provided) + * @returns The created Account + */ +export async function createPsmAccount(webClient: WebClient, seed?: Uint8Array): Promise { + if (!seed) { + seed = crypto.getRandomValues(new Uint8Array(32)); + } + + try { + // Generate the signer secret key from seed + const sk = AuthSecretKey.rpoFalconWithRNG(seed); + const signerCommitment = sk.publicKey().toCommitment(); + + // Get PSM endpoint and initialize client + const psmEndpoint = (await fetchFromStorage(PSM_URL_STORAGE_KEY)) || DEFAULT_PSM_ENDPOINT; + const client = new MultisigClient(webClient, { psmEndpoint }); + const { psmCommitment, psmPublicKey } = await client.initialize('falcon'); + + console.log('Creating PSM account with PSM commitment:', psmCommitment); + + // Create the multisig account using the package utility + const { account } = await createMultisigAccount(webClient, { + threshold: 1, + signerCommitments: [signerCommitment.toHex()], + psmCommitment, + psmPublicKey, + psmEnabled: true, + storageMode: 'public', + signatureScheme: 'falcon' + }); + + // Sync state with the node + await webClient.syncState(); + + // Store the secret key in WebStore for signing + await webClient.addAccountSecretKeyToWebStore(account.id(), sk); + + console.log('PSM account created:', account.id().toString()); + + return account; + } catch (e) { + console.error('Error creating PSM account:', e); + throw new Error('Failed to create PSM account'); + } +} diff --git a/src/lib/miden/psm/index.ts b/src/lib/miden/psm/index.ts new file mode 100644 index 000000000..7639f84e0 --- /dev/null +++ b/src/lib/miden/psm/index.ts @@ -0,0 +1,181 @@ +import { Account, WebClient } from '@miden-sdk/miden-sdk'; +import { + AccountInspector, + Multisig, + MultisigClient, + MultisigConfig, + PsmHttpClient, + type ProposalMetadata, + type TransactionProposal, + type TransactionProposalResult +} from '@openzeppelin/miden-multisig-client'; + +import { DEFAULT_PSM_ENDPOINT } from 'lib/miden-chain/constants'; +import { PSM_URL_STORAGE_KEY } from 'lib/settings/constants'; +import { u8ToB64 } from 'lib/shared/helpers'; +import { useWalletStore } from 'lib/store'; + +import { fetchFromStorage } from '../front'; +import { accountIdStringToSdk } from '../sdk/helpers'; +import { MidenClientInterface } from '../sdk/miden-client-interface'; +import { WalletSigner, type SignWordFunction } from './signer'; + +const MAX_SYNC_RETRIES = 20; + +/** + * MultisigService wraps the MultisigClient and Multisig classes from + * @openzeppelin/miden-multisig-client to provide a simplified interface + * for PSM account operations. + */ +export class MultisigService { + multisig: Multisig; + client: MultisigClient; + syncRetryCount: number = 0; + + constructor(multisig: Multisig, client: MultisigClient) { + this.multisig = multisig; + this.client = client; + } + + /** + * Initialize a MultisigService for an existing PSM account. + */ + static async init( + account: Account, + publicKey: string, + signerCommitment: string, + signCallback?: (publicKey: string, signingInputs: string) => Promise + ): Promise { + try { + const signer = new WalletSigner(publicKey, signerCommitment, useWalletStore.getState().signWord); + const psmEndpoint = (await fetchFromStorage(PSM_URL_STORAGE_KEY)) || DEFAULT_PSM_ENDPOINT; + + const webClient = ( + await MidenClientInterface.create( + signCallback + ? { + signCallback: async (publicKey: Uint8Array, signingInputs: Uint8Array) => { + const keyString = Buffer.from(publicKey).toString('hex'); + const signingInputsString = Buffer.from(signingInputs).toString('hex'); + return await signCallback(keyString, signingInputsString); + } + } + : {} + ) + ).webClient; + + const client = new MultisigClient(webClient, { psmEndpoint }); + const { psmCommitment } = await client.initialize('falcon'); + + // Load the existing multisig account + let multisig: Multisig; + if (account.isNew()) { + console.log('Creating new Multisig for account:', account.id().toString()); + const config: MultisigConfig = { + threshold: 1, + signerCommitments: [signerCommitment], + psmCommitment: psmCommitment, + psmEnabled: true + }; + const psmClient = new PsmHttpClient(psmEndpoint); + psmClient.setSigner(signer); + multisig = new Multisig(account, config, psmClient, signer, webClient); + await multisig.registerOnPsm(); + } else { + multisig = await client.load(account.id().toString(), signer); + } + return new MultisigService(multisig, client); + } catch (error) { + console.log('Error initializing MultisigService:', error); + throw error; + } + } + + /** + * Get the account ID for this multisig. + */ + get accountId(): string { + return this.multisig.accountId; + } + + /** + * Create a send (P2ID) transaction proposal. + */ + async createSendProposal(recipientId: string, faucetId: string, amount: bigint): Promise { + return this.multisig.createSendProposal( + accountIdStringToSdk(recipientId).toString(), + accountIdStringToSdk(faucetId).toString(), + amount + ); + } + + /** + * Create a consume notes transaction proposal. + */ + async createConsumeNotesProposal(noteIds: string[]): Promise { + return this.multisig.createConsumeNotesProposal(noteIds); + } + + /** + * Create a custom transaction proposal from a TransactionSummary. + * This is used for 'execute' type transactions. + */ + async createCustomProposal(summaryBytes: Uint8Array): Promise { + const txSummaryBase64 = u8ToB64(summaryBytes); + + // Sync state to ensure we have the latest nonce + await this.multisig.syncState(); + const account = this.multisig.account; + if (!account) { + throw new Error('Account not found in MultisigService'); + } + const nonce = Number(account.nonce().asInt()) + 2; + + // Create metadata for unknown/custom proposal type + const metadata: ProposalMetadata = { + proposalType: 'unknown', + description: 'Custom transaction' + }; + + const proposal = await this.multisig.createProposal(nonce, txSummaryBase64, metadata); + const proposals = await this.multisig.syncTransactionProposals(); + + return { proposal, proposals }; + } + + async signAndExecuteProposal(commitment: string): Promise { + await this.multisig.signTransactionProposal(commitment); + await this.multisig.executeTransactionProposal(commitment); + } + + async sync(): Promise { + try { + await this.multisig.syncAll(); + this.syncRetryCount = 0; // Reset retry count on successful sync + } catch (error) { + const isNonceTooLow = + error instanceof Error && error.message.includes('nonce') && error.message.includes('too low'); + + if (isNonceTooLow) { + console.warn('Nonce is too low, local state is ahead of on chain state, retrying sync...', this.syncRetryCount); + + if (this.syncRetryCount < MAX_SYNC_RETRIES) { + this.syncRetryCount++; + await new Promise(resolve => setTimeout(resolve, 3000)); // Wait before retrying + await this.sync(); + } else { + console.error('Max sync retries reached. Please check your account state and try again.'); + } + } else { + throw error; // Rethrow if it's a different error + } + } + } + + async getConsumableNotes() { + return this.multisig.getConsumableNotes(); + } +} + +// Re-export types that may be needed by consumers +export type { TransactionProposal, TransactionProposalResult, ProposalMetadata }; diff --git a/src/lib/miden/psm/signer.ts b/src/lib/miden/psm/signer.ts new file mode 100644 index 000000000..bcd69f218 --- /dev/null +++ b/src/lib/miden/psm/signer.ts @@ -0,0 +1,43 @@ +import { AccountId, Felt, FeltArray, Rpo256, Word } from '@miden-sdk/miden-sdk'; +import { SignatureScheme, Signer } from '@openzeppelin/psm-client'; + +export type SignWordFunction = (publicKey: string, wordHex: string) => Promise; + +export class WalletSigner implements Signer { + readonly commitment: string; + readonly publicKey: string; + readonly scheme: SignatureScheme = 'falcon'; + private signWordFn: SignWordFunction; + readonly commitmentForStorageRetrieval: string; + + constructor(publicKey: string, commitment: string, signWordFn: SignWordFunction) { + this.publicKey = publicKey; + this.commitment = commitment; + this.commitmentForStorageRetrieval = commitment.slice(2); + this.signWordFn = signWordFn; + } + + async signAccountIdWithTimestamp(accountId: string, timestamp: number): Promise { + const digest = WalletSigner.computeAccountDigest(accountId, timestamp); + console.log('Signing account digest for storage retrieval', accountId); + const sig = await this.signWordFn(this.commitmentForStorageRetrieval, digest.toHex()); + return sig; + } + + async signCommitment(commitmentHex: string): Promise { + const paddedHex = commitmentHex.startsWith('0x') ? commitmentHex : `0x${commitmentHex}`; + const sig = await this.signWordFn(this.commitmentForStorageRetrieval, paddedHex); + return sig; + } + + static computeAccountDigest(accountId: string, timestamp: number): Word { + const paddedHex = accountId.startsWith('0x') ? accountId : `0x${accountId}`; + const parsedAccountId = AccountId.fromHex(paddedHex); + const prefix = parsedAccountId.prefix(); + const suffix = parsedAccountId.suffix(); + + const feltArray = new FeltArray([prefix, suffix, new Felt(BigInt(timestamp)), new Felt(BigInt(0))]); + + return Rpo256.hashElements(feltArray); + } +} diff --git a/yarn.lock b/yarn.lock index ced4d6a26..5aac77837 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2195,6 +2195,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== +"@noble/hashes@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.0.1.tgz#fc1a928061d1232b0a52bb754393c37a5216c89e" + integrity sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2216,6 +2221,20 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@openzeppelin/miden-multisig-client@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/miden-multisig-client/-/miden-multisig-client-0.13.0.tgz#e33ab86a774f976efa0a576dcd806bdac60e548d" + integrity sha512-b0wxXCM6iSWlKLKb0QzG15z4+UeqKGv4920Gvk6jn1ofeth1uy8Q8pTdDICe/L1UvwUsgqLhicEADUKkB+KOiw== + dependencies: + "@miden-sdk/miden-sdk" "^0.13.0" + "@noble/hashes" "^2.0.1" + "@openzeppelin/psm-client" "^0.13.0" + +"@openzeppelin/psm-client@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/psm-client/-/psm-client-0.13.0.tgz#fe4df2282ddede6af451fdfe728e5733bf85f4c0" + integrity sha512-pf/b5CpWfVDbYBSXCDxyuRgbgrKNCB8Y/Bw9U2vW3DteUmfGdsCeCX+PprwVbTvNo1thfja1NzbEfkAvO9XLIw== + "@peculiar/asn1-schema@^2.0.27", "@peculiar/asn1-schema@^2.3.13": version "2.6.0" resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz#0dca1601d5b0fed2a72fed7a5f1d0d7dbe3a6f82" @@ -7066,13 +7085,13 @@ fraction.js@^5.3.4: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a" integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ== -framer-motion@^12.35.2: - version "12.35.2" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.35.2.tgz#add40fe8cdb8eb553feb9dd17525c93b51e6f67c" - integrity sha512-dhfuEMaNo0hc+AEqyHiIfiJRNb9U9UQutE9FoKm5pjf7CMitp9xPEF1iWZihR1q86LBmo6EJ7S8cN8QXEy49AA== +framer-motion@^12.9.2: + version "12.29.0" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.29.0.tgz#f031407c6025f96e9425de3da039b73c368b4153" + integrity sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg== dependencies: - motion-dom "^12.35.2" - motion-utils "^12.29.2" + motion-dom "^12.29.0" + motion-utils "^12.27.2" tslib "^2.4.0" front-matter@^4.0.2: @@ -9473,17 +9492,17 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -motion-dom@^12.35.2: - version "12.35.2" - resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.35.2.tgz#489b9eb7206dbac3d6087aae584f08e78c16cfbd" - integrity sha512-pWXFMTwvGDbx1Fe9YL5HZebv2NhvGBzRtiNUv58aoK7+XrsuaydQ0JGRKK2r+bTKlwgSWwWxHbP5249Qr/BNpg== +motion-dom@^12.29.0: + version "12.29.0" + resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.29.0.tgz#93ba293d15df5edc9f3fd43225a5b59c26997a5c" + integrity sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA== dependencies: - motion-utils "^12.29.2" + motion-utils "^12.27.2" -motion-utils@^12.29.2: - version "12.29.2" - resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.29.2.tgz#8fdd28babe042c2456b078ab33b32daa3bf5938b" - integrity sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A== +motion-utils@^12.27.2: + version "12.27.2" + resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.27.2.tgz#cd3038236ae2dc3d643bdb272ed831fd6d8ab616" + integrity sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q== ms@2.0.0: version "2.0.0" From b8a46a28d5e7b673cb705c37460efa4b3166d307 Mon Sep 17 00:00:00 2001 From: Wiktor Starczewski Date: Wed, 11 Mar 2026 15:18:49 +0100 Subject: [PATCH 02/71] fix: address PSM integration review findings (#155) - Remove secret key logging from vault.getPublicKeyForCommitment - Fix broken conditional in autoSync (dangling no-op if) - Add error handling to generatePsmTransaction (cancel on failure) - Throw on max sync retries instead of silently succeeding - Remove unused imports (PsmHttpClient, ProposalMetadata, AccountInspector, WebClient) - Fix misleading comments in psm-manager - Document nonce +2 offset in createCustomProposal --- src/lib/miden/activity/transactions.ts | 6 +++++- src/lib/miden/back/vault.ts | 25 +++++++++++++++++++++++++ src/lib/miden/front/psm-manager.ts | 6 ++---- src/lib/miden/psm/index.ts | 6 +++--- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/lib/miden/activity/transactions.ts b/src/lib/miden/activity/transactions.ts index b15b7379b..40fe554f3 100644 --- a/src/lib/miden/activity/transactions.ts +++ b/src/lib/miden/activity/transactions.ts @@ -609,7 +609,11 @@ export const generateTransaction = async ( }); // Route PSM accounts through PSM service if (isPsmAccount(transaction.accountId)) { - await generatePsmTransaction(transaction, signCallback); + try { + await generatePsmTransaction(transaction, signCallback); + } catch (error) { + await cancelTransaction(transaction, error); + } return; } diff --git a/src/lib/miden/back/vault.ts b/src/lib/miden/back/vault.ts index b0b0b9b51..fd36b0ee7 100644 --- a/src/lib/miden/back/vault.ts +++ b/src/lib/miden/back/vault.ts @@ -503,6 +503,31 @@ export class Vault { return Buffer.from(signature.serialize()).toString('hex'); } + async signWord(publicKey: string, wordHex: string): Promise { + const word = Word.fromHex(wordHex); + const secretKey = await fetchAndDecryptOneWithLegacyFallBack( + accAuthSecretKeyStrgKey(publicKey), + this.vaultKey + ); + let secretKeyBytes = new Uint8Array(Buffer.from(secretKey, 'hex')); + const wasmSecretKey = AuthSecretKey.deserialize(secretKeyBytes); + const signature = wasmSecretKey.sign(word); + return `0x${Buffer.from(signature.serialize().slice(1)).toString('hex')}`; + } + + async getPublicKeyForCommitment(pkc: string): Promise { + try { + const sk = await fetchAndDecryptOneWithLegacyFallBack(accAuthSecretKeyStrgKey(pkc), this.vaultKey); + let secretKeyBytes = new Uint8Array(Buffer.from(sk, 'hex')); + const wasmSecretKey = AuthSecretKey.deserialize(secretKeyBytes); + // Skip first byte (type prefix) from serialized public key + return Buffer.from(wasmSecretKey.publicKey().serialize().slice(1)).toString('hex'); + } catch (e) { + console.error('Error in getPublicKeyForCommitment', e); + throw new PublicError('Failed to get public key for commitment'); + } + } + async getAuthSecretKey(key: string) { const secretKey = await fetchAndDecryptOneWithLegacyFallBack(accAuthSecretKeyStrgKey(key), this.vaultKey); return secretKey; diff --git a/src/lib/miden/front/psm-manager.ts b/src/lib/miden/front/psm-manager.ts index 3ef47b129..f77ecfa23 100644 --- a/src/lib/miden/front/psm-manager.ts +++ b/src/lib/miden/front/psm-manager.ts @@ -5,8 +5,7 @@ import { WalletType } from 'screens/onboarding/types'; import { MULTISIG_SLOT_NAMES } from '../psm/account'; import { getMidenClient, withWasmClientLock } from '../sdk/miden-client'; /** - * Get or create a MultisigService for the given account (lazy initialization). - * Services are cached in memory and reused for subsequent calls. + * Create a MultisigService for the given PSM account. */ export async function getOrCreateMultisigService( accountPublicKey: string, @@ -50,8 +49,7 @@ export async function getOrCreateMultisigService( } /** - * Check if an account is a PSM account and has completed at least one transaction. - * PSM routing only applies after the first transaction. + * Check if an account is a PSM account. */ export function isPsmAccount(accountPublicKey: string): boolean { const accounts = useWalletStore.getState().accounts; diff --git a/src/lib/miden/psm/index.ts b/src/lib/miden/psm/index.ts index 7639f84e0..d058b222f 100644 --- a/src/lib/miden/psm/index.ts +++ b/src/lib/miden/psm/index.ts @@ -1,6 +1,5 @@ -import { Account, WebClient } from '@miden-sdk/miden-sdk'; +import { Account } from '@miden-sdk/miden-sdk'; import { - AccountInspector, Multisig, MultisigClient, MultisigConfig, @@ -129,6 +128,7 @@ export class MultisigService { if (!account) { throw new Error('Account not found in MultisigService'); } + // +2 accounts for the current nonce plus the proposal execution incrementing nonce const nonce = Number(account.nonce().asInt()) + 2; // Create metadata for unknown/custom proposal type @@ -164,7 +164,7 @@ export class MultisigService { await new Promise(resolve => setTimeout(resolve, 3000)); // Wait before retrying await this.sync(); } else { - console.error('Max sync retries reached. Please check your account state and try again.'); + throw new Error('Max sync retries reached: local state is ahead of on-chain state'); } } else { throw error; // Rethrow if it's a different error From 08ab29e8e07ca3a19d7d2ef1f999c9692aabb8c6 Mon Sep 17 00:00:00 2001 From: 0xnullifier Date: Thu, 12 Mar 2026 22:48:03 +0800 Subject: [PATCH 03/71] feat: use the new `createTransactionProposalRequest` method for creating and TransactionRequest object and proceeding normally --- package.json | 2 +- src/lib/miden-chain/constants.ts | 2 +- src/lib/miden/activity/transactions.ts | 44 ++++++++++++++++++++------ src/lib/miden/psm/index.ts | 7 +++- yarn.lock | 15 ++++++--- 5 files changed, 52 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 4e8826741..1d2cf1a44 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@miden-sdk/miden-sdk": "^0.13.0", "@newhighsco/storybook-addon-svgr": "^2.0.7", "@noble/hashes": "^1.4.0", - "@openzeppelin/miden-multisig-client": "^0.13.0", + "@openzeppelin/miden-multisig-client": "^0.13.1", "@openzeppelin/psm-client": "^0.13.0", "@peculiar/webcrypto": "1.1.6", "@radix-ui/react-slot": "^1.2.3", diff --git a/src/lib/miden-chain/constants.ts b/src/lib/miden-chain/constants.ts index 850feab48..c5cd74e70 100644 --- a/src/lib/miden-chain/constants.ts +++ b/src/lib/miden-chain/constants.ts @@ -62,4 +62,4 @@ export const TOKEN_MAPPING = { [MidenTokens.Miden]: { faucetId: 'mtst1aqmat9m63ctdsgz6xcyzpuprpulwk9vg_qruqqypuyph' } }; -export const DEFAULT_PSM_ENDPOINT = 'https://psm-stg.openzeppelin.com'; +export const DEFAULT_PSM_ENDPOINT = 'http://localhost:3000'; diff --git a/src/lib/miden/activity/transactions.ts b/src/lib/miden/activity/transactions.ts index 40fe554f3..a9e0f1453 100644 --- a/src/lib/miden/activity/transactions.ts +++ b/src/lib/miden/activity/transactions.ts @@ -729,21 +729,45 @@ const generatePsmTransaction = async ( // Get the proposal commitment for signing and execution const proposalCommitment = proposalResult.proposal.commitment; - console.log('Transaction proposal created with commitment:', proposalCommitment); // Sign and execute the proposal - await multisigService.signAndExecuteProposal(proposalCommitment); + const tr = await multisigService.signAndCreateTransactionRequest(proposalCommitment); - await multisigService.sync(); - // Determine display message based on transaction type - const displayMessage = - transaction.type === 'send' ? 'Sent' : transaction.type === 'consume' ? 'Received' : 'Completed'; + const options: MidenClientCreateOptions = { + signCallback: async (publicKey: Uint8Array, signingInputs: Uint8Array) => { + const keyString = Buffer.from(publicKey).toString('hex'); + const signingInputsString = Buffer.from(signingInputs).toString('hex'); + return await signCallback(keyString, signingInputsString); + } + }; - // Update transaction as completed - await updateTransactionStatus(transaction.id, ITransactionStatus.Completed, { - displayMessage, - completedAt: Math.floor(Date.now() / 1000) + // Wrap WASM client operations in a lock to prevent concurrent access + const transactionResultBytes = await withWasmClientLock(async () => { + const midenClient = await getMidenClient(options); + return await midenClient.newTransaction(transaction.accountId, tr.serialize()); }); + + const transactionResult = TransactionResult.deserialize(transactionResultBytes); + + await withWasmClientLock(async () => { + const midenClient = await getMidenClient(); + await midenClient.submitTransaction(transactionResultBytes, transaction.delegateTransaction); + }); + + switch (transaction.type) { + case 'send': + await completeSendTransaction(transaction as SendTransaction, transactionResult); + break; + case 'consume': + await completeConsumeTransaction(transaction.id, transactionResult); + break; + case 'execute': + default: + await completeCustomTransaction(transaction, transactionResult); + break; + } + + await multisigService.sync(); }; export const cancelTransaction = async (transaction: Transaction, error: any) => { diff --git a/src/lib/miden/psm/index.ts b/src/lib/miden/psm/index.ts index d058b222f..677420803 100644 --- a/src/lib/miden/psm/index.ts +++ b/src/lib/miden/psm/index.ts @@ -1,4 +1,4 @@ -import { Account } from '@miden-sdk/miden-sdk'; +import { Account, TransactionRequest } from '@miden-sdk/miden-sdk'; import { Multisig, MultisigClient, @@ -148,6 +148,11 @@ export class MultisigService { await this.multisig.executeTransactionProposal(commitment); } + async signAndCreateTransactionRequest(commitment: string): Promise { + await this.multisig.signTransactionProposal(commitment); + return await this.multisig.createTransactionProposalRequest(commitment); + } + async sync(): Promise { try { await this.multisig.syncAll(); diff --git a/yarn.lock b/yarn.lock index 5aac77837..1028ff611 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2221,20 +2221,25 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@openzeppelin/miden-multisig-client@^0.13.0": - version "0.13.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/miden-multisig-client/-/miden-multisig-client-0.13.0.tgz#e33ab86a774f976efa0a576dcd806bdac60e548d" - integrity sha512-b0wxXCM6iSWlKLKb0QzG15z4+UeqKGv4920Gvk6jn1ofeth1uy8Q8pTdDICe/L1UvwUsgqLhicEADUKkB+KOiw== +"@openzeppelin/miden-multisig-client@^0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@openzeppelin/miden-multisig-client/-/miden-multisig-client-0.13.1.tgz#da78637e0fb1397a5ba264a4a849ec9bfb094708" + integrity sha512-+potgpQgl4VE5CsXwqPyDuEHM0UY6ZvSATY8LBn0+3ZW8t4udIdLmsr7DzKDoUly6GSQWy8zNveCss/Buj/T8w== dependencies: "@miden-sdk/miden-sdk" "^0.13.0" "@noble/hashes" "^2.0.1" - "@openzeppelin/psm-client" "^0.13.0" + "@openzeppelin/psm-client" "^0.13.1" "@openzeppelin/psm-client@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@openzeppelin/psm-client/-/psm-client-0.13.0.tgz#fe4df2282ddede6af451fdfe728e5733bf85f4c0" integrity sha512-pf/b5CpWfVDbYBSXCDxyuRgbgrKNCB8Y/Bw9U2vW3DteUmfGdsCeCX+PprwVbTvNo1thfja1NzbEfkAvO9XLIw== +"@openzeppelin/psm-client@^0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@openzeppelin/psm-client/-/psm-client-0.13.1.tgz#c7131e3eb9286edb3e156fc6847e17140d0a63f6" + integrity sha512-uUVw7qI7MtW3SUemX0a2JActTjCXojRPtIYPF7OQC7eIu01H9fpY/yqo8DBgQvkUS1wOssPueVVqVBqZEy6PHA== + "@peculiar/asn1-schema@^2.0.27", "@peculiar/asn1-schema@^2.3.13": version "2.6.0" resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz#0dca1601d5b0fed2a72fed7a5f1d0d7dbe3a6f82" From 0009fb73175df0469d9dfc71a3d21ecddf0493f4 Mon Sep 17 00:00:00 2001 From: 0xnullifier Date: Sat, 14 Mar 2026 00:11:54 +0530 Subject: [PATCH 04/71] chore: use deployed psm url --- src/lib/miden-chain/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/miden-chain/constants.ts b/src/lib/miden-chain/constants.ts index c5cd74e70..850feab48 100644 --- a/src/lib/miden-chain/constants.ts +++ b/src/lib/miden-chain/constants.ts @@ -62,4 +62,4 @@ export const TOKEN_MAPPING = { [MidenTokens.Miden]: { faucetId: 'mtst1aqmat9m63ctdsgz6xcyzpuprpulwk9vg_qruqqypuyph' } }; -export const DEFAULT_PSM_ENDPOINT = 'http://localhost:3000'; +export const DEFAULT_PSM_ENDPOINT = 'https://psm-stg.openzeppelin.com'; From 08346ab68b4bd84fccac8c0ba2a3a5dbeab7bbe4 Mon Sep 17 00:00:00 2001 From: 0xnullifier Date: Mon, 16 Mar 2026 21:04:03 +0530 Subject: [PATCH 05/71] feat: add a option to choose recovery type during onboarding --- public/_locales/en/en.json | 7 ++ src/app/pages/Welcome.tsx | 20 ++++- src/lib/miden/back/actions.ts | 4 +- src/lib/miden/back/main.ts | 2 +- src/lib/miden/back/vault.ts | 15 ++-- src/lib/miden/front/client.ts | 4 +- src/lib/shared/types.ts | 1 + src/lib/store/index.ts | 3 +- src/lib/store/types.ts | 8 +- .../SelectRecoveryMethod.tsx | 73 +++++++++++++++++++ src/screens/onboarding/navigator.tsx | 10 ++- src/screens/onboarding/types.ts | 8 ++ 12 files changed, 139 insertions(+), 16 deletions(-) create mode 100644 src/screens/onboarding/create-wallet-flow/SelectRecoveryMethod.tsx diff --git a/public/_locales/en/en.json b/public/_locales/en/en.json index 86520a47d..61a3d9c93 100644 --- a/public/_locales/en/en.json +++ b/public/_locales/en/en.json @@ -410,6 +410,13 @@ "chooseYourAccountType": "Choose Your Account Type", "chooseAccountTypeDescription": "Select how you want to interact with the blockchain -- on-chain or off-chain.", "canAddMultipleAccountsLater": "You can add and manage multiple account types later within the wallet.", + "chooseRecoveryMethod": "Set Up Account Recovery", + "chooseRecoveryMethodDescription": "Choose how you want to recover your account if you lose access.", + "guardianRecovery": "Guardian", + "guardianRecoveryDescription": "Recommended. Guardian-based recovery for your account.", + "fullyPrivateRecovery": "Fully Private", + "fullyPrivateRecoveryDescription": "Local only. No recovery — losing your device can permanently lose funds.", + "default": "Default", "transactionFile": "Transaction file", "verificationFailed": "Verification Failed", "transactionVerifiedSuccessfully": "The transaction has been successfully verified. You can now claim your tokens.", diff --git a/src/app/pages/Welcome.tsx b/src/app/pages/Welcome.tsx index a562f5de0..f21a1fcb8 100644 --- a/src/app/pages/Welcome.tsx +++ b/src/app/pages/Welcome.tsx @@ -13,7 +13,7 @@ import { useWalletStore } from 'lib/store'; import { fetchStateFromBackend } from 'lib/store/hooks/useIntercomSync'; import { navigate, useLocation } from 'lib/woozie'; import { OnboardingFlow } from 'screens/onboarding/navigator'; -import { ImportType, OnboardingAction, OnboardingStep, OnboardingType } from 'screens/onboarding/types'; +import { ImportType, OnboardingAction, OnboardingStep, OnboardingType, WalletType } from 'screens/onboarding/types'; /** * Check if hardware security is available for vault key protection. @@ -71,6 +71,7 @@ const Welcome: FC = () => { const [onboardingType, setOnboardingType] = useState(null); const [importType, setImportType] = useState(null); const [password, setPassword] = useState(null); + const [walletType, setWalletType] = useState(WalletType.Psm); const [importedWithFile, setImportedWithFile] = useState(false); const [isLoading, setIsLoading] = useState(false); const [useBiometric, setUseBiometric] = useState(true); @@ -94,14 +95,14 @@ const Welcome: FC = () => { // For hardware-only wallets, pass undefined as password const actualPassword = password === '__HARDWARE_ONLY__' ? undefined : password; if (!importedWithFile) { - await registerWallet(actualPassword, seedPhraseFormatted, onboardingType === OnboardingType.Import); + await registerWallet(walletType, actualPassword, seedPhraseFormatted, onboardingType === OnboardingType.Import); } else { await importWalletFromClient(actualPassword, seedPhraseFormatted); } } else { throw new Error('Missing password or seed phrase'); } - }, [password, seedPhrase, importedWithFile, registerWallet, onboardingType, importWalletFromClient]); + }, [password, seedPhrase, importedWithFile, registerWallet, onboardingType, importWalletFromClient, walletType]); const onAction = async (action: OnboardingAction) => { let eventCategory = AnalyticsEventCategory.ButtonPress; @@ -180,6 +181,14 @@ const Welcome: FC = () => { setPassword(action.payload.password); eventCategory = AnalyticsEventCategory.FormSubmit; // Hardware protection is automatically set up in Vault.spawn() when available + if (onboardingType === OnboardingType.Create) { + navigate('/#select-recovery-method'); + } else { + navigate('/#confirmation'); + } + break; + case 'select-recovery-method': + setWalletType(action.payload); navigate('/#confirmation'); break; case 'confirmation': @@ -231,6 +240,8 @@ const Welcome: FC = () => { navigate('/#import-from-seed'); } } + } else if (step === OnboardingStep.SelectRecoveryMethod) { + navigate('/#create-password'); } else if (step === OnboardingStep.ImportFromFile || step === OnboardingStep.ImportFromSeed) { navigate('/#select-import-type'); } @@ -271,6 +282,9 @@ const Welcome: FC = () => { case '#create-password': setStep(OnboardingStep.CreatePassword); break; + case '#select-recovery-method': + setStep(OnboardingStep.SelectRecoveryMethod); + break; case '#confirmation': if (!password) { navigate('/'); diff --git a/src/lib/miden/back/actions.ts b/src/lib/miden/back/actions.ts index 7e182ecd3..a21605136 100644 --- a/src/lib/miden/back/actions.ts +++ b/src/lib/miden/back/actions.ts @@ -73,13 +73,13 @@ export async function isDAppEnabled() { return bools.every(Boolean); } -export function registerNewWallet(password?: string, mnemonic?: string, ownMnemonic?: boolean) { +export function registerNewWallet(walletType: WalletType, password?: string, mnemonic?: string, ownMnemonic?: boolean) { return withInited(async () => { console.log('[Actions.registerNewWallet] Starting...'); // Password may be undefined for hardware-only wallets (mobile/desktop with Secure Enclave) // Vault.spawn() will handle this by using hardware protection instead // spawn() returns the vault directly, avoiding a second biometric prompt from unlock() - const vault = await Vault.spawn(password ?? '', mnemonic, ownMnemonic); + const vault = await Vault.spawn(walletType, password ?? '', mnemonic, ownMnemonic); console.log('[Actions.registerNewWallet] Vault.spawn completed, initializing state...'); const accounts = await vault.fetchAccounts(); const settings = await vault.fetchSettings(); diff --git a/src/lib/miden/back/main.ts b/src/lib/miden/back/main.ts index 4c5f798b7..998d4ab4b 100644 --- a/src/lib/miden/back/main.ts +++ b/src/lib/miden/back/main.ts @@ -102,7 +102,7 @@ async function processRequest(req: WalletRequest, port: Runtime.Port): Promise { + static async spawn( + walletType: WalletType, + password: string, + mnemonic?: string, + ownMnemonic?: boolean + ): Promise { return withError('Failed to create wallet', async (): Promise => { // Generate random vault key (256-bit) const vaultKeyBytes = Passworder.generateVaultKey(); @@ -233,7 +238,7 @@ export class Vault { insertKeyCallback }; const hdAccIndex = 0; - const walletSeed = deriveClientSeed(WalletType.OnChain, mnemonic, 0); + const walletSeed = deriveClientSeed(walletType, mnemonic, 0); // Wrap WASM client operations in a lock to prevent concurrent access const accPublicKey = await withWasmClientLock(async () => { @@ -243,12 +248,12 @@ export class Vault { return await midenClient.importPublicMidenWalletFromSeed(walletSeed); } catch (e) { console.error('Failed to import wallet from seed in spawn, creating new wallet instead', e); - return await midenClient.createMidenWallet(WalletType.OnChain, walletSeed); + return await midenClient.createMidenWallet(walletType, walletSeed); } } else { // Sync to chain tip BEFORE creating first account (no accounts = no tags = fast sync) await midenClient.syncState(); - return await midenClient.createMidenWallet(WalletType.OnChain, walletSeed); + return await midenClient.createMidenWallet(walletType, walletSeed); } }); @@ -256,7 +261,7 @@ export class Vault { publicKey: accPublicKey, name: 'Miden Account 1', isPublic: true, - type: WalletType.OnChain, + type: walletType, hdIndex: hdAccIndex }; const newAccounts = [initialAccount]; diff --git a/src/lib/miden/front/client.ts b/src/lib/miden/front/client.ts index 3339a7f9e..6c1b0fb5c 100644 --- a/src/lib/miden/front/client.ts +++ b/src/lib/miden/front/client.ts @@ -96,8 +96,8 @@ export const [MidenContextProvider, useMidenContext] = constate(() => { // Wrap store actions in useCallback for stable references const registerWallet = useCallback( - async (password: string | undefined, mnemonic?: string, ownMnemonic?: boolean) => { - await storeRegisterWallet(password, mnemonic, ownMnemonic); + async (walletType: WalletType, password: string | undefined, mnemonic: string, ownMnemonic: boolean) => { + await storeRegisterWallet(walletType, password, mnemonic, ownMnemonic); }, [storeRegisterWallet] ); diff --git a/src/lib/shared/types.ts b/src/lib/shared/types.ts index 11fa530f8..f38996447 100644 --- a/src/lib/shared/types.ts +++ b/src/lib/shared/types.ts @@ -288,6 +288,7 @@ export interface NewWalletRequest extends WalletMessageBase { password?: string; // Optional for hardware-only wallets (mobile/desktop with Secure Enclave) mnemonic?: string; ownMnemonic?: boolean; + walletType: WalletType; } export interface NewWalletResponse extends WalletMessageBase { diff --git a/src/lib/store/index.ts b/src/lib/store/index.ts index 0e5155184..a9d2de601 100644 --- a/src/lib/store/index.ts +++ b/src/lib/store/index.ts @@ -122,9 +122,10 @@ export const useWalletStore = create()( }, // Auth actions - registerWallet: async (password, mnemonic, ownMnemonic) => { + registerWallet: async (walletType, password, mnemonic, ownMnemonic) => { const res = await request({ type: WalletMessageType.NewWalletRequest, + walletType, password, mnemonic, ownMnemonic diff --git a/src/lib/store/types.ts b/src/lib/store/types.ts index def6bee5d..1b8d9454d 100644 --- a/src/lib/store/types.ts +++ b/src/lib/store/types.ts @@ -98,7 +98,12 @@ export interface WalletActions { syncFromBackend: (state: MidenState) => void; // Auth actions - registerWallet: (password: string | undefined, mnemonic?: string, ownMnemonic?: boolean) => Promise; + registerWallet: ( + walletType: WalletType, + password: string | undefined, + mnemonic: string, + ownMnemonic: boolean + ) => Promise; importWalletFromClient: (password: string | undefined, mnemonic: string) => Promise; unlock: (password?: string) => Promise; @@ -115,6 +120,7 @@ export interface WalletActions { signData: (publicKey: string, signingInputs: string) => Promise; signTransaction: (publicKey: string, signingInputs: string) => Promise; getAuthSecretKey: (key: string) => Promise; + getPublicKeyForCommitment: (publicKeyCommitment: string) => Promise; // DApp actions getDAppPayload: (id: string) => Promise; diff --git a/src/screens/onboarding/create-wallet-flow/SelectRecoveryMethod.tsx b/src/screens/onboarding/create-wallet-flow/SelectRecoveryMethod.tsx new file mode 100644 index 000000000..32cb5cc94 --- /dev/null +++ b/src/screens/onboarding/create-wallet-flow/SelectRecoveryMethod.tsx @@ -0,0 +1,73 @@ +import React, { useMemo } from 'react'; + +import classNames from 'clsx'; +import { useTranslation } from 'react-i18next'; + +import { ReactComponent as ArrowRightIcon } from 'app/icons/arrow-right.svg'; + +import { WalletType } from '../types'; + +export interface SelectRecoveryMethodScreenProps extends Omit, 'onSubmit'> { + onSubmit?: (payload: WalletType) => void; +} + +type RecoveryOption = { + id: WalletType; + title: string; + description: string; + isDefault?: boolean; + isLast?: boolean; +}; + +export const SelectRecoveryMethodScreen = ({ onSubmit, ...props }: SelectRecoveryMethodScreenProps) => { + const { t } = useTranslation(); + const options: RecoveryOption[] = useMemo( + () => [ + { + id: WalletType.Psm, + title: t('guardianRecovery'), + description: t('guardianRecoveryDescription'), + isDefault: true + }, + { + id: WalletType.OffChain, + title: t('fullyPrivateRecovery'), + description: t('fullyPrivateRecoveryDescription'), + isLast: true + } + ], + [t] + ); + + return ( +
+
+

{t('chooseRecoveryMethod')}

+

{t('chooseRecoveryMethodDescription')}

+
+ {options.map(option => ( +
onSubmit?.(option.id)} + > +
+
+

{option.title}

+ {option.isDefault && ( + + {t('default')} + + )} +
+ +
+

{option.description}

+
+ ))} +
+ ); +}; diff --git a/src/screens/onboarding/navigator.tsx b/src/screens/onboarding/navigator.tsx index ab2f86ca9..6c95410d1 100644 --- a/src/screens/onboarding/navigator.tsx +++ b/src/screens/onboarding/navigator.tsx @@ -15,6 +15,7 @@ import { ConfirmationScreen } from './common/Confirmation'; import { CreatePasswordScreen } from './common/CreatePassword'; import { WelcomeScreen } from './common/Welcome'; import { BackUpSeedPhraseScreen } from './create-wallet-flow/BackUpSeedPhrase'; +import { SelectRecoveryMethodScreen } from './create-wallet-flow/SelectRecoveryMethod'; import { SelectTransactionTypeScreen } from './create-wallet-flow/SelectTransactionType'; import { VerifySeedPhraseScreen } from './create-wallet-flow/VerifySeedPhrase'; import { ImportSeedPhraseScreen } from './import-wallet-flow/ImportSeedPhrase'; @@ -54,6 +55,8 @@ const Header: React.FC<{ currentStep = 3; } else if (step === OnboardingStep.ImportFromSeed || step === OnboardingStep.ImportFromFile) { currentStep = 2; + } else if (step === OnboardingStep.SelectRecoveryMethod) { + currentStep = 4; } else if (step === OnboardingStep.Confirmation) { currentStep = 4; } @@ -61,7 +64,7 @@ const Header: React.FC<{ return (
- +
); @@ -141,6 +144,9 @@ export const OnboardingFlow: FC = ({ const onCreatePasswordSubmit = (password: string) => onForwardAction?.({ id: 'create-password-submit', payload: { password, enableBiometric: false } }); + const onSelectRecoveryMethodSubmit = (walletType: WalletType) => + onForwardAction?.({ id: 'select-recovery-method', payload: walletType }); + const onSelectTransactionTypeSubmit = () => onForwardAction?.({ id: 'select-transaction-type', payload: 'private' }); @@ -178,6 +184,8 @@ export const OnboardingFlow: FC = ({ return ; case OnboardingStep.CreatePassword: return ; + case OnboardingStep.SelectRecoveryMethod: + return ; case OnboardingStep.SelectTransactionType: return ; case OnboardingStep.Confirmation: diff --git a/src/screens/onboarding/types.ts b/src/screens/onboarding/types.ts index cf94367a0..fa66b5ce5 100644 --- a/src/screens/onboarding/types.ts +++ b/src/screens/onboarding/types.ts @@ -24,6 +24,7 @@ export enum OnboardingStep { CreatePassword = 'create-password', BiometricSetup = 'biometric-setup', SelectTransactionType = 'select-transaction-type', + SelectRecoveryMethod = 'select-recovery-method', Confirmation = 'confirmation' } export type OnboardingActionId = @@ -37,6 +38,7 @@ export type OnboardingActionId = | 'create-password-submit' | 'biometric-setup-submit' | 'select-transaction-type' + | 'select-recovery-method' | 'confirmation' | 'import-from-file' | 'import-from-seed'; @@ -80,6 +82,11 @@ export type SelectTransactionTypeAction = { payload: string; }; +export type SelectRecoveryMethodAction = { + id: 'select-recovery-method'; + payload: WalletType; +}; + export type ConfirmationAction = { id: 'confirmation'; }; @@ -116,6 +123,7 @@ export type OnboardingAction = | CreatePasswordSubmitAction | BiometricSetupSubmitAction | SelectTransactionTypeAction + | SelectRecoveryMethodAction | ConfirmationAction | ImportSeedPhraseSubmitAction | BackAction From 71d821299fd723305f85edb8b5eb4945c456d40c Mon Sep 17 00:00:00 2001 From: 0xnullifier Date: Thu, 19 Mar 2026 13:00:17 +0530 Subject: [PATCH 06/71] chore: merge conflicts --- src/lib/miden/activity/transactions.ts | 4 +--- yarn.lock | 30 +++++++++++++------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/lib/miden/activity/transactions.ts b/src/lib/miden/activity/transactions.ts index a9e0f1453..7a5fad37e 100644 --- a/src/lib/miden/activity/transactions.ts +++ b/src/lib/miden/activity/transactions.ts @@ -30,7 +30,7 @@ import { TransactionOutput } from '../db/types'; import { toNoteTypeString } from '../helpers'; -import { getBech32AddressFromAccountId } from '../sdk/helpers'; +import { accountIdStringToSdk, getBech32AddressFromAccountId } from '../sdk/helpers'; import { getMidenClient, withWasmClientLock } from '../sdk/miden-client'; import { MidenClientCreateOptions } from '../sdk/miden-client-interface'; import { ConsumableNote, NoteTypeEnum, NoteType as NoteTypeString } from '../types'; @@ -600,8 +600,6 @@ export const generateTransaction = async ( await updateTransactionStatus(transaction.id, ITransactionStatus.GeneratingTransaction, { processingStartedAt: Math.floor(Date.now() / 1000) // seconds }); -<<<<<<< HEAD -======= console.log('Generating transaction', { txId: transaction.id, type: transaction.type, diff --git a/yarn.lock b/yarn.lock index 1028ff611..0a908ce1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7090,13 +7090,13 @@ fraction.js@^5.3.4: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a" integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ== -framer-motion@^12.9.2: - version "12.29.0" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.29.0.tgz#f031407c6025f96e9425de3da039b73c368b4153" - integrity sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg== +framer-motion@^12.35.2: + version "12.38.0" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.38.0.tgz#cf28e072a95942881ca4e33fd33be41192fd146b" + integrity sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g== dependencies: - motion-dom "^12.29.0" - motion-utils "^12.27.2" + motion-dom "^12.38.0" + motion-utils "^12.36.0" tslib "^2.4.0" front-matter@^4.0.2: @@ -9497,17 +9497,17 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -motion-dom@^12.29.0: - version "12.29.0" - resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.29.0.tgz#93ba293d15df5edc9f3fd43225a5b59c26997a5c" - integrity sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA== +motion-dom@^12.38.0: + version "12.38.0" + resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.38.0.tgz#9ef3253ea0fb28b6757588327073848d940e9aab" + integrity sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA== dependencies: - motion-utils "^12.27.2" + motion-utils "^12.36.0" -motion-utils@^12.27.2: - version "12.27.2" - resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.27.2.tgz#cd3038236ae2dc3d643bdb272ed831fd6d8ab616" - integrity sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q== +motion-utils@^12.36.0: + version "12.36.0" + resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.36.0.tgz#cff2df2a28c3fe53a3de7e0103ba7f73ff7d77a7" + integrity sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg== ms@2.0.0: version "2.0.0" From 2b00a3e93049d28b632560615bd62d62ef8203fb Mon Sep 17 00:00:00 2001 From: 0xnullifier Date: Thu, 19 Mar 2026 14:33:04 +0530 Subject: [PATCH 07/71] feat: make onboarding screen for gaurdian aligned with the rest of the app --- public/_locales/en/en.json | 4 +- src/lib/miden/back/actions.ts | 8 +++ src/lib/miden/back/main.ts | 1 + src/lib/miden/back/vault.ts | 4 ++ src/lib/miden/sdk/miden-client-interface.ts | 6 ++ src/lib/settings/constants.ts | 2 + src/lib/store/index.ts | 1 + src/lib/store/types.ts | 1 - .../SelectRecoveryMethod.tsx | 64 +++++++++++-------- src/screens/onboarding/types.ts | 1 + 10 files changed, 64 insertions(+), 28 deletions(-) diff --git a/public/_locales/en/en.json b/public/_locales/en/en.json index 61a3d9c93..bd5fadbc0 100644 --- a/public/_locales/en/en.json +++ b/public/_locales/en/en.json @@ -413,9 +413,9 @@ "chooseRecoveryMethod": "Set Up Account Recovery", "chooseRecoveryMethodDescription": "Choose how you want to recover your account if you lose access.", "guardianRecovery": "Guardian", - "guardianRecoveryDescription": "Recommended. Guardian-based recovery for your account.", + "guardianRecoveryDescription": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state.", "fullyPrivateRecovery": "Fully Private", - "fullyPrivateRecoveryDescription": "Local only. No recovery — losing your device can permanently lose funds.", + "fullyPrivateRecoveryDescription": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds.", "default": "Default", "transactionFile": "Transaction file", "verificationFailed": "Verification Failed", diff --git a/src/lib/miden/back/actions.ts b/src/lib/miden/back/actions.ts index a21605136..af9a8c713 100644 --- a/src/lib/miden/back/actions.ts +++ b/src/lib/miden/back/actions.ts @@ -74,6 +74,14 @@ export async function isDAppEnabled() { } export function registerNewWallet(walletType: WalletType, password?: string, mnemonic?: string, ownMnemonic?: boolean) { + console.log( + '[Actions.registerNewWallet] Called with walletType:', + walletType, + 'mnemonic provided:', + Boolean(mnemonic), + 'ownMnemonic flag:', + ownMnemonic + ); return withInited(async () => { console.log('[Actions.registerNewWallet] Starting...'); // Password may be undefined for hardware-only wallets (mobile/desktop with Secure Enclave) diff --git a/src/lib/miden/back/main.ts b/src/lib/miden/back/main.ts index 998d4ab4b..7b1db767e 100644 --- a/src/lib/miden/back/main.ts +++ b/src/lib/miden/back/main.ts @@ -102,6 +102,7 @@ async function processRequest(req: WalletRequest, port: Runtime.Port): Promise { + console.log('Spawning new vault with wallet type', walletType); return withError('Failed to create wallet', async (): Promise => { // Generate random vault key (256-bit) const vaultKeyBytes = Passworder.generateVaultKey(); @@ -633,6 +634,8 @@ function getMainDerivationPath(walletType: WalletType, accIndex: number) { walletTypeIndex = 0; } else if (walletType === WalletType.OffChain) { walletTypeIndex = 1; + } else if (walletType === WalletType.Psm) { + walletTypeIndex = 2; } else { throw new Error('Invalid wallet type'); } @@ -665,6 +668,7 @@ async function withError(errMessage: string, factory: (doThrow: () => void) = throw new Error(''); }); } catch (err: any) { + console.log('Error in vault operation', err); throw err instanceof PublicError ? err : new PublicError(errMessage); } } diff --git a/src/lib/miden/sdk/miden-client-interface.ts b/src/lib/miden/sdk/miden-client-interface.ts index 48e25f423..71cbf8d3b 100644 --- a/src/lib/miden/sdk/miden-client-interface.ts +++ b/src/lib/miden/sdk/miden-client-interface.ts @@ -28,6 +28,7 @@ import { WalletType } from 'screens/onboarding/types'; import { ConsumeTransaction, SendTransaction } from '../db/types'; import { toNoteType } from '../helpers'; +import { createPsmAccount } from '../psm/account'; import { NoteExportType } from './constants'; import { accountIdStringToSdk, getBech32AddressFromAccountId } from './helpers'; @@ -108,6 +109,11 @@ export class MidenClientInterface { } async createMidenWallet(walletType: WalletType, seed?: Uint8Array): Promise { + if (walletType === WalletType.Psm) { + const account = await createPsmAccount(this.webClient, seed); + return getBech32AddressFromAccountId(account.id()); + } + // Create a new wallet const accountStorageMode = walletType === WalletType.OnChain ? AccountStorageMode.public() : AccountStorageMode.private(); diff --git a/src/lib/settings/constants.ts b/src/lib/settings/constants.ts index 47a2dd9fb..a289693f0 100644 --- a/src/lib/settings/constants.ts +++ b/src/lib/settings/constants.ts @@ -12,3 +12,5 @@ export const DEFAULT_HAPTIC_FEEDBACK = true; export const THEME_STORAGE_KEY = 'theme_setting'; export const DEFAULT_THEME: 'light' | 'dark' = 'light'; + +export const PSM_URL_STORAGE_KEY = 'psm_url_setting'; diff --git a/src/lib/store/index.ts b/src/lib/store/index.ts index a9d2de601..1e4e05fb6 100644 --- a/src/lib/store/index.ts +++ b/src/lib/store/index.ts @@ -123,6 +123,7 @@ export const useWalletStore = create()( // Auth actions registerWallet: async (walletType, password, mnemonic, ownMnemonic) => { + console.log('[WalletStore] registerWallet called with walletType:', walletType); const res = await request({ type: WalletMessageType.NewWalletRequest, walletType, diff --git a/src/lib/store/types.ts b/src/lib/store/types.ts index 1b8d9454d..1ad471191 100644 --- a/src/lib/store/types.ts +++ b/src/lib/store/types.ts @@ -120,7 +120,6 @@ export interface WalletActions { signData: (publicKey: string, signingInputs: string) => Promise; signTransaction: (publicKey: string, signingInputs: string) => Promise; getAuthSecretKey: (key: string) => Promise; - getPublicKeyForCommitment: (publicKeyCommitment: string) => Promise; // DApp actions getDAppPayload: (id: string) => Promise; diff --git a/src/screens/onboarding/create-wallet-flow/SelectRecoveryMethod.tsx b/src/screens/onboarding/create-wallet-flow/SelectRecoveryMethod.tsx index 32cb5cc94..0d667ce4a 100644 --- a/src/screens/onboarding/create-wallet-flow/SelectRecoveryMethod.tsx +++ b/src/screens/onboarding/create-wallet-flow/SelectRecoveryMethod.tsx @@ -3,7 +3,8 @@ import React, { useMemo } from 'react'; import classNames from 'clsx'; import { useTranslation } from 'react-i18next'; -import { ReactComponent as ArrowRightIcon } from 'app/icons/arrow-right.svg'; +import { Button } from 'components/Button'; +import { Badge } from 'lib/ui/badge'; import { WalletType } from '../types'; @@ -38,36 +39,49 @@ export const SelectRecoveryMethodScreen = ({ onSubmit, ...props }: SelectRecover ], [t] ); + const [selected, setSelected] = React.useState(options.find(o => o.isDefault)?.id || options[0].id); + + const handleContinue = () => { + onSubmit?.(selected); + }; return ( -
-
+
+

{t('chooseRecoveryMethod')}

-

{t('chooseRecoveryMethodDescription')}

+

{t('chooseRecoveryMethodDescription')}

- {options.map(option => ( -
onSubmit?.(option.id)} - > -
-
-

{option.title}

- {option.isDefault && ( - - {t('default')} - - )} +
+ {options.map(option => ( +
setSelected(option.id)} + > +
+
+

{option.title}

+ {option.isDefault && ( + + {t('default')} + + )} +
- +

{option.description}

-

{option.description}

-
- ))} + ))} +
+
+
); }; diff --git a/src/screens/onboarding/types.ts b/src/screens/onboarding/types.ts index fa66b5ce5..6c37dad5e 100644 --- a/src/screens/onboarding/types.ts +++ b/src/screens/onboarding/types.ts @@ -5,6 +5,7 @@ export enum OnboardingType { export enum WalletType { OffChain = 'off-chain', + Psm = 'psm', OnChain = 'on-chain' } From c015b4492505c6edbb8609602746676d3d8798c7 Mon Sep 17 00:00:00 2001 From: 0xnullifier Date: Thu, 19 Mar 2026 14:34:19 +0530 Subject: [PATCH 08/71] chore: changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e92df717a..251f0fbb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [FIX][all] Removed stale "Download Generated Files" button and output notes storage. The `useExportNotes` hook, `registerOutputNote`, and related storage key were unused dead code. Simplifies the transaction completion screen and its auto-close logic. (#160) * [FIX][all] Removed the "Upload File" button and drag-and-drop note import from the Receive page. The freed space is now used by the notes list, making it taller. (#161) * [FEATURE][all] Complete UI revamp across the wallet. +* [FEATURE][all] Integrate gaurdian for private state management for the wallet. --- From 405bfd032537a0ee54a7462462c9f2382b31e693 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 19 Mar 2026 09:06:31 +0000 Subject: [PATCH 09/71] chore: update translation files --- public/_locales/de/messages.json | 28 ++++++++++++++++++++++++++++ public/_locales/en/messages.json | 28 ++++++++++++++++++++++++++++ public/_locales/en_GB/messages.json | 28 ++++++++++++++++++++++++++++ public/_locales/es/messages.json | 28 ++++++++++++++++++++++++++++ public/_locales/fr/messages.json | 28 ++++++++++++++++++++++++++++ public/_locales/ja/messages.json | 28 ++++++++++++++++++++++++++++ public/_locales/ko/messages.json | 28 ++++++++++++++++++++++++++++ public/_locales/pl/messages.json | 28 ++++++++++++++++++++++++++++ public/_locales/pt/messages.json | 28 ++++++++++++++++++++++++++++ public/_locales/ru/messages.json | 28 ++++++++++++++++++++++++++++ public/_locales/tr/messages.json | 28 ++++++++++++++++++++++++++++ public/_locales/uk/messages.json | 28 ++++++++++++++++++++++++++++ public/_locales/zh_CN/messages.json | 28 ++++++++++++++++++++++++++++ public/_locales/zh_TW/messages.json | 28 ++++++++++++++++++++++++++++ 14 files changed, 392 insertions(+) diff --git a/public/_locales/de/messages.json b/public/_locales/de/messages.json index a1f3a6b92..245af893e 100644 --- a/public/_locales/de/messages.json +++ b/public/_locales/de/messages.json @@ -1779,6 +1779,34 @@ "message": "Sie können später im Wallet mehrere Kontotypen hinzufügen und verwalten.", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "Richten Sie die Kontowiederherstellung ein", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "Wählen Sie aus, wie Sie Ihr Konto wiederherstellen möchten, wenn Sie den Zugriff verlieren.", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "Wächter", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "Empfohlen. Wächterbasierte Wiederherstellung für Ihr Konto, aber der Wächterbetreiber kann Ihren Status sehen.", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "Völlig privat", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "Nur lokal, niemand außer Ihnen sieht Ihren Status, aber keine Wiederherstellung. Der Verlust Ihres Geräts führt zum dauerhaften Verlust Ihres Geldes.", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "Standard", + "englishSource": "Default" + }, "transactionFile": { "message": "Transaktionsdatei", "englishSource": "Transaction file" diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 284d8c44b..ba2324ca5 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -1743,6 +1743,34 @@ "message": "You can add and manage multiple account types later within the wallet.", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "Set Up Account Recovery", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "Choose how you want to recover your account if you lose access.", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "Guardian", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state.", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "Fully Private", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds.", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "Default", + "englishSource": "Default" + }, "transactionFile": { "message": "Transaction file", "englishSource": "Transaction file" diff --git a/public/_locales/en_GB/messages.json b/public/_locales/en_GB/messages.json index 050ae19fd..792610bd5 100644 --- a/public/_locales/en_GB/messages.json +++ b/public/_locales/en_GB/messages.json @@ -1807,6 +1807,34 @@ "message": "You can add and manage multiple account types later within the wallet.", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "Set Up Account Recovery", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "Choose how you want to recover your account if you lose access.", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "Guardian", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state.", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "Fully Private", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds.", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "Default", + "englishSource": "Default" + }, "transactionFile": { "message": "Transaction file", "englishSource": "Transaction file" diff --git a/public/_locales/es/messages.json b/public/_locales/es/messages.json index dbec36472..0762a10ad 100644 --- a/public/_locales/es/messages.json +++ b/public/_locales/es/messages.json @@ -1716,6 +1716,34 @@ "message": "Puede agregar y administrar varios tipos de cuentas más adelante dentro de la billetera.", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "Configurar la recuperación de la cuenta", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "Elija cómo desea recuperar su cuenta si pierde el acceso.", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "Guardián", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "Recomendado. Recuperación basada en Guardian para su cuenta, pero el operador Gaurdian puede ver su estado.", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "Totalmente privado", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "Solo local, nadie ve su estado excepto usted, pero no hay recuperación; perder su dispositivo resultará en la pérdida permanente de sus fondos.", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "Por defecto", + "englishSource": "Default" + }, "transactionFile": { "message": "archivo de transacción", "englishSource": "Transaction file" diff --git a/public/_locales/fr/messages.json b/public/_locales/fr/messages.json index 22f231f66..4a817a5cd 100644 --- a/public/_locales/fr/messages.json +++ b/public/_locales/fr/messages.json @@ -1778,6 +1778,34 @@ "message": "Vous pouvez ajouter et gérer plusieurs types de comptes ultérieurement dans le portefeuille.", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "Configurer la récupération de compte", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "Choisissez comment vous souhaitez récupérer votre compte si vous perdez l'accès.", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "Tuteur", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "Recommandé. Récupération basée sur le tuteur pour votre compte, mais l'opérateur gardien peut voir votre état.", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "Entièrement privé", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "Local uniquement, personne ne voit votre état sauf vous mais pas de récupération, la perte de votre appareil entraînera une perte permanente de vos fonds.", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "Défaut", + "englishSource": "Default" + }, "transactionFile": { "message": "Fichier de transactions", "englishSource": "Transaction file" diff --git a/public/_locales/ja/messages.json b/public/_locales/ja/messages.json index 61086cda5..e3c8c7afe 100644 --- a/public/_locales/ja/messages.json +++ b/public/_locales/ja/messages.json @@ -1779,6 +1779,34 @@ "message": "後からウォレット内で複数のアカウント タイプを追加および管理できます。", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "アカウント回復のセットアップ", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "アクセスできなくなった場合にアカウントを回復する方法を選択します。", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "ガーディアン", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "推奨。アカウントは Guardian ベースで回復されますが、Guurdian オペレーターはお客様の状態を確認できます。", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "完全プライベート", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "ローカルのみ、あなた以外の誰もあなたの状態を見ることはできませんが、回復はできません。デバイスを失うと資金が永久に失われます。", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "デフォルト", + "englishSource": "Default" + }, "transactionFile": { "message": "トランザクションファイル", "englishSource": "Transaction file" diff --git a/public/_locales/ko/messages.json b/public/_locales/ko/messages.json index 8e9335dd0..7742924ad 100644 --- a/public/_locales/ko/messages.json +++ b/public/_locales/ko/messages.json @@ -1779,6 +1779,34 @@ "message": "나중에 지갑 내에서 여러 계정 유형을 추가하고 관리할 수 있습니다.", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "계정 복구 설정", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "액세스 권한을 상실한 경우 계정을 복구할 방법을 선택하세요.", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "보호자", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "추천합니다. 귀하의 계정에 대한 보호자 기반 복구가 가능하지만 보호자 운영자는 귀하의 상태를 볼 수 있습니다.", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "완전 비공개", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "로컬에서만 가능하며 귀하 외에는 누구도 귀하의 상태를 볼 수 없지만 복구할 수는 없습니다. 장치를 분실하면 자금이 영구적으로 손실됩니다.", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "기본", + "englishSource": "Default" + }, "transactionFile": { "message": "거래 파일", "englishSource": "Transaction file" diff --git a/public/_locales/pl/messages.json b/public/_locales/pl/messages.json index 398de2bbf..2766db6ae 100644 --- a/public/_locales/pl/messages.json +++ b/public/_locales/pl/messages.json @@ -1716,6 +1716,34 @@ "message": "Możesz później dodać wiele typów kont i zarządzać nimi w portfelu.", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "Skonfiguruj odzyskiwanie konta", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "Wybierz sposób odzyskania konta w przypadku utraty dostępu.", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "Opiekun", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "Zalecony. Odzyskiwanie konta na podstawie opiekuna, ale operator strażnika może zobaczyć Twój stan.", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "Całkowicie prywatny", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "Tylko lokalnie, nikt poza Tobą nie widzi Twojego stanu, ale nie ma możliwości odzyskania urządzenia. Utrata urządzenia spowoduje trwałą utratę środków.", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "Domyślny", + "englishSource": "Default" + }, "transactionFile": { "message": "Plik transakcyjny", "englishSource": "Transaction file" diff --git a/public/_locales/pt/messages.json b/public/_locales/pt/messages.json index 66b7ea903..3fbd65334 100644 --- a/public/_locales/pt/messages.json +++ b/public/_locales/pt/messages.json @@ -1777,6 +1777,34 @@ "message": "Você pode adicionar e gerenciar vários tipos de contas posteriormente na carteira.", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "Configurar recuperação de conta", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "Escolha como deseja recuperar sua conta caso perca o acesso.", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "Guardião", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "Recomendado. Recuperação baseada no guardião para sua conta, mas o operador gaurdiano pode ver seu estado.", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "Totalmente privado", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "Somente local, ninguém vê seu estado, exceto você, mas não há recuperação. A perda do seu dispositivo resultará na perda permanente de seus fundos.", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "Padrão", + "englishSource": "Default" + }, "transactionFile": { "message": "Arquivo de transação", "englishSource": "Transaction file" diff --git a/public/_locales/ru/messages.json b/public/_locales/ru/messages.json index 9181f65af..7c827c3f2 100644 --- a/public/_locales/ru/messages.json +++ b/public/_locales/ru/messages.json @@ -1780,6 +1780,34 @@ "message": "Позже вы сможете добавлять и управлять несколькими типами учетных записей в кошельке.", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "Настройка восстановления учетной записи", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "Выберите, как вы хотите восстановить свою учетную запись, если вы потеряете доступ.", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "Хранитель", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "Рекомендуется. Восстановление вашей учетной записи на основе Guardian, но оператор Guardian может видеть ваше состояние.", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "Полностью приватный", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "Только локально, никто, кроме вас, не видит ваше состояние, но восстановления нет, потеря устройства приведет к безвозвратной потере ваших средств.", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "По умолчанию", + "englishSource": "Default" + }, "transactionFile": { "message": "Файл транзакции", "englishSource": "Transaction file" diff --git a/public/_locales/tr/messages.json b/public/_locales/tr/messages.json index 34ec8776f..fb2a71940 100644 --- a/public/_locales/tr/messages.json +++ b/public/_locales/tr/messages.json @@ -1779,6 +1779,34 @@ "message": "Daha sonra cüzdanın içine birden fazla hesap türü ekleyebilir ve yönetebilirsiniz.", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "Hesap Kurtarmayı Ayarlayın", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "Erişiminizi kaybederseniz hesabınızı nasıl kurtarmak istediğinizi seçin.", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "Gardiyan", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "Tavsiye edilen. Hesabınız için koruyucu tabanlı kurtarma ancak gaurdian operatörü durumunuzu görebilir.", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "Tamamen Özel", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "Yalnızca yerel, durumunuzu sizden başka kimse göremez ancak iyileşme olmaz, cihazınızı kaybetmek, paranızın kalıcı olarak kaybolmasına neden olur.", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "Varsayılan", + "englishSource": "Default" + }, "transactionFile": { "message": "İşlem dosyası", "englishSource": "Transaction file" diff --git a/public/_locales/uk/messages.json b/public/_locales/uk/messages.json index 54363d53e..edd4f4332 100644 --- a/public/_locales/uk/messages.json +++ b/public/_locales/uk/messages.json @@ -1780,6 +1780,34 @@ "message": "Пізніше в гаманці можна додати кілька типів облікових записів і керувати ними.", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "Налаштувати відновлення облікового запису", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "Виберіть спосіб відновлення облікового запису, якщо ви втратите доступ.", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "Опікун", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "Рекомендовано. Відновлення вашого облікового запису на основі опікуна, але оператор-охоронець може бачити ваш стан.", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "Повністю приватний", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "Лише локально, ніхто не бачить ваш стан, крім вас, але відновлення не відбувається, втрата пристрою призведе до остаточної втрати ваших коштів.", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "За замовчуванням", + "englishSource": "Default" + }, "transactionFile": { "message": "Файл транзакцій", "englishSource": "Transaction file" diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json index df6f9bc14..2310f8da4 100644 --- a/public/_locales/zh_CN/messages.json +++ b/public/_locales/zh_CN/messages.json @@ -1779,6 +1779,34 @@ "message": "您可以稍后在钱包中添加和管理多个帐户类型。", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "设置帐户恢复", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "选择在失去访问权限时恢复帐户的方式。", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "监护人", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "受到推崇的。基于监护人的帐户恢复,但监护人操作员可以看到您的状态。", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "完全私人", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "仅限本地,除了您之外没有人看到您的状态,但无法恢复,丢失您的设备将导致您的资金永久丢失。", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "默认", + "englishSource": "Default" + }, "transactionFile": { "message": "交易文件", "englishSource": "Transaction file" diff --git a/public/_locales/zh_TW/messages.json b/public/_locales/zh_TW/messages.json index bce09bb6f..1ea7b633e 100644 --- a/public/_locales/zh_TW/messages.json +++ b/public/_locales/zh_TW/messages.json @@ -1779,6 +1779,34 @@ "message": "您可以稍后在钱包中添加和管理多个帐户类型。", "englishSource": "You can add and manage multiple account types later within the wallet." }, + "chooseRecoveryMethod": { + "message": "设置帐户恢复", + "englishSource": "Set Up Account Recovery" + }, + "chooseRecoveryMethodDescription": { + "message": "选择在失去访问权限时恢复帐户的方式。", + "englishSource": "Choose how you want to recover your account if you lose access." + }, + "guardianRecovery": { + "message": "监护人", + "englishSource": "Guardian" + }, + "guardianRecoveryDescription": { + "message": "受到推崇的。基于监护人的帐户恢复,但监护人操作员可以看到您的状态。", + "englishSource": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state." + }, + "fullyPrivateRecovery": { + "message": "完全私人", + "englishSource": "Fully Private" + }, + "fullyPrivateRecoveryDescription": { + "message": "仅限本地,除了您之外没有人看到您的状态,但无法恢复,丢失您的设备将导致您的资金永久丢失。", + "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." + }, + "default": { + "message": "默认", + "englishSource": "Default" + }, "transactionFile": { "message": "交易文件", "englishSource": "Transaction file" From ed288d9a75c33a5f4cf7570f0f7afa33fedb38f8 Mon Sep 17 00:00:00 2001 From: 0xnullifier Date: Thu, 19 Mar 2026 21:38:31 +0530 Subject: [PATCH 10/71] feat: add psm account provider to properly handle service worker transaction processing --- src/lib/miden/activity/transactions.ts | 30 ++++--- src/lib/miden/back/actions.ts | 12 +++ src/lib/miden/back/main.ts | 12 +++ src/lib/miden/back/transaction-processor.ts | 25 +++++- src/lib/miden/front/autoSync.ts | 10 +++ src/lib/miden/front/psm-manager.ts | 87 ++++++++++++++++++--- src/lib/miden/psm/index.ts | 19 +---- src/lib/shared/types.ts | 29 +++++++ src/lib/store/index.ts | 19 +++++ src/lib/store/types.ts | 2 + 10 files changed, 206 insertions(+), 39 deletions(-) diff --git a/src/lib/miden/activity/transactions.ts b/src/lib/miden/activity/transactions.ts index 7a5fad37e..5c755db13 100644 --- a/src/lib/miden/activity/transactions.ts +++ b/src/lib/miden/activity/transactions.ts @@ -13,7 +13,7 @@ import { liveQuery } from 'dexie'; import { consumeNoteId } from 'lib/miden-worker/consumeNoteId'; import { sendTransaction } from 'lib/miden-worker/sendTransaction'; import { submitTransaction } from 'lib/miden-worker/submitTransaction'; -import { getOrCreateMultisigService, isPsmAccount } from 'lib/miden/front/psm-manager'; +import { getOrCreateMultisigService, isPsmAccount, type PsmAccountProvider } from 'lib/miden/front/psm-manager'; import * as Repo from 'lib/miden/repo'; import { isExtension, isMobile } from 'lib/platform'; import { u8ToB64 } from 'lib/shared/helpers'; @@ -584,7 +584,8 @@ export const verifyStuckTransactionsFromNode = async (): Promise => { export const generateTransaction = async ( transaction: Transaction, signCallback: (publicKey: string, signingInputs: string) => Promise, - useWorker: boolean = true + useWorker: boolean = true, + psmProvider?: PsmAccountProvider ) => { // Sync state first to ensure we have latest account state // Separate lock acquisition to avoid holding lock during network call @@ -606,9 +607,9 @@ export const generateTransaction = async ( accountId: transaction.accountId }); // Route PSM accounts through PSM service - if (isPsmAccount(transaction.accountId)) { + if (await isPsmAccount(transaction.accountId, psmProvider)) { try { - await generatePsmTransaction(transaction, signCallback); + await generatePsmTransaction(transaction, signCallback, psmProvider); } catch (error) { await cancelTransaction(transaction, error); } @@ -689,9 +690,11 @@ export const generateTransaction = async ( */ const generatePsmTransaction = async ( transaction: ITransaction, - signCallback: (publicKey: string, signingInputs: string) => Promise + signCallback: (publicKey: string, signingInputs: string) => Promise, + psmProvider?: PsmAccountProvider ): Promise => { - const multisigService = await getOrCreateMultisigService(transaction.accountId, signCallback); + console.log('Generating PSM transaction'); + const multisigService = await getOrCreateMultisigService(transaction.accountId, psmProvider); let proposalResult; @@ -792,7 +795,8 @@ export const getTransactionById = async (id: string) => { export const generateTransactionsLoop = async ( signCallback: (publicKey: string, signingInputs: string) => Promise, - useWorker: boolean = true + useWorker: boolean = true, + psmProvider?: PsmAccountProvider ): Promise => { await cancelStuckTransactions(); await cancelStaleQueuedTransactions(); @@ -818,7 +822,7 @@ export const generateTransactionsLoop = async ( // Call safely to cancel transaction and unlock records if something goes wrong try { - await generateTransaction(nextTransaction, signCallback, useWorker); + await generateTransaction(nextTransaction, signCallback, useWorker, psmProvider); return true; } catch (e) { logger.warning('Failed to generate transaction', e); @@ -831,13 +835,14 @@ export const generateTransactionsLoop = async ( export const safeGenerateTransactionsLoop = async ( signCallback: (publicKey: string, signingInputs: string) => Promise, - useWorker: boolean = true + useWorker: boolean = true, + psmProvider?: PsmAccountProvider ) => { return navigator.locks .request(`generate-transactions-loop`, { ifAvailable: true }, async lock => { if (!lock) return; - const result = await generateTransactionsLoop(signCallback, useWorker); + const result = await generateTransactionsLoop(signCallback, useWorker, psmProvider); if (result === false) { return false; } @@ -859,7 +864,8 @@ export const safeGenerateTransactionsLoop = async ( */ export const startBackgroundTransactionProcessing = ( signCallback: (publicKey: string, signingInputs: string) => Promise, - useWorker: boolean = false + useWorker: boolean = false, + psmProvider?: PsmAccountProvider ) => { // Process transactions in a loop until none are left const processLoop = async () => { @@ -869,7 +875,7 @@ export const startBackgroundTransactionProcessing = ( while (hasMore && attempts < maxAttempts) { attempts++; - await safeGenerateTransactionsLoop(signCallback, useWorker); + await safeGenerateTransactionsLoop(signCallback, useWorker, psmProvider); // Check if there are more transactions to process const remaining = await getAllUncompletedTransactions(); diff --git a/src/lib/miden/back/actions.ts b/src/lib/miden/back/actions.ts index af9a8c713..6148ea2ee 100644 --- a/src/lib/miden/back/actions.ts +++ b/src/lib/miden/back/actions.ts @@ -208,6 +208,18 @@ export function signTransaction(publicKey: string, signingInputs: string) { }); } +export function signWord(publicKey: string, wordHex: string) { + return withUnlocked(async ({ vault }) => { + return await vault.signWord(publicKey, wordHex); + }); +} + +export function getPublicKeyForCommitment(commitment: string) { + return withUnlocked(async ({ vault }) => { + return await vault.getPublicKeyForCommitment(commitment); + }); +} + export function getAuthSecretKey(key: string) { return withUnlocked(async ({ vault }) => { return await vault.getAuthSecretKey(key); diff --git a/src/lib/miden/back/main.ts b/src/lib/miden/back/main.ts index 7b1db767e..c4a631e37 100644 --- a/src/lib/miden/back/main.ts +++ b/src/lib/miden/back/main.ts @@ -183,6 +183,18 @@ async function processRequest(req: WalletRequest, port: Runtime.Port): Promise { + return withUnlocked(async ({ vault }) => { + return await vault.fetchAccounts(); + }); + }, + getPublicKeyForCommitment: async (commitment: string) => { + return withUnlocked(async ({ vault }) => { + return await vault.getPublicKeyForCommitment(commitment); + }); + }, + signWord: async (publicKey: string, wordHex: string) => { + return withUnlocked(async ({ vault }) => { + return await vault.signWord(publicKey, wordHex); + }); + } +}; + /** * Start processing queued transactions in the service worker. * Deduplicates via isProcessing flag + navigator.locks in safeGenerateTransactionsLoop. @@ -38,7 +61,7 @@ export async function startTransactionProcessing(): Promise { while (attempts < maxAttempts) { attempts++; console.log('[TransactionProcessor] Loop attempt', attempts); - const result = await safeGenerateTransactionsLoop(swSignCallback, false); + const result = await safeGenerateTransactionsLoop(swSignCallback, false, vaultPsmProvider); console.log('[TransactionProcessor] Loop result:', result); // Broadcast progress so popup UI can update diff --git a/src/lib/miden/front/autoSync.ts b/src/lib/miden/front/autoSync.ts index 54f659598..0af2d55d8 100644 --- a/src/lib/miden/front/autoSync.ts +++ b/src/lib/miden/front/autoSync.ts @@ -3,6 +3,7 @@ import { WalletState, WalletStatus } from 'lib/shared/types'; import { useWalletStore } from 'lib/store'; import { getMidenClient, withWasmClientLock } from '../sdk/miden-client'; +import { syncPsmAccounts } from './psm-manager'; const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); @@ -98,6 +99,7 @@ export class Sync { syncDebugInfo.lastError = 'getMidenClient returned null'; return null; } + console.log('[AutoSync] Starting sync with Miden client...'); const syncSummary = await client.syncState(); return syncSummary.blockNum(); }); @@ -109,6 +111,14 @@ export class Sync { } syncDebugInfo.syncCount++; syncDebugInfo.lastSyncTime = new Date().toLocaleTimeString(); + + // Sync PSM state after chain sync (runs outside WASM lock — HTTP calls only) + try { + console.log('[AutoSync] Syncing PSM accounts...'); + await syncPsmAccounts(); + } catch (psmError) { + console.error('[AutoSync] PSM sync error:', psmError); + } } catch (error) { console.error('[AutoSync] Error during sync:', error); syncDebugInfo.lastError = String(error); diff --git a/src/lib/miden/front/psm-manager.ts b/src/lib/miden/front/psm-manager.ts index f77ecfa23..81e66aa0f 100644 --- a/src/lib/miden/front/psm-manager.ts +++ b/src/lib/miden/front/psm-manager.ts @@ -1,24 +1,56 @@ import { MultisigService } from 'lib/miden/psm'; +import { WalletAccount } from 'lib/shared/types'; import { useWalletStore } from 'lib/store'; import { WalletType } from 'screens/onboarding/types'; import { MULTISIG_SLOT_NAMES } from '../psm/account'; import { getMidenClient, withWasmClientLock } from '../sdk/miden-client'; + +// Cache MultisigService instances to avoid re-initialization on every sync cycle +const psmServiceCache = new Map(); + +/** + * Callbacks for resolving account data. + * Allows psm-manager to work in both frontend (Zustand) and service worker (Vault) contexts. + */ +export interface PsmAccountProvider { + getAccounts: () => Promise; + getPublicKeyForCommitment: (commitment: string) => Promise; + signWord: (publicKey: string, wordHex: string) => Promise; +} + +/** + * Default provider that uses the Zustand store (frontend context). + */ +const zustandProvider: PsmAccountProvider = { + getAccounts: async () => useWalletStore.getState().accounts, + getPublicKeyForCommitment: (commitment: string) => useWalletStore.getState().getPublicKeyForCommitment(commitment), + signWord: (publicKey: string, wordHex: string) => useWalletStore.getState().signWord(publicKey, wordHex) +}; + /** * Create a MultisigService for the given PSM account. + * Returns a cached instance if available. */ export async function getOrCreateMultisigService( accountPublicKey: string, - signCallback: (publicKey: string, signingInputs: string) => Promise + provider: PsmAccountProvider = zustandProvider ): Promise { - // Verify this is a PSM account using Zustand store - const accounts = useWalletStore.getState().accounts; + console.log(`[PSM Manager] Getting/creating MultisigService for account: ${accountPublicKey}`); + // Return cached instance if available + const cached = psmServiceCache.get(accountPublicKey); + if (cached) { + return cached; + } + + // Verify this is a PSM account + const accounts = await provider.getAccounts(); const account = accounts.find(acc => acc.publicKey === accountPublicKey); if (!account || account.type !== WalletType.Psm) { throw new Error('Account is not a PSM account'); } - // Get the Account object and WebClient from Miden client + // Get the Account object from Miden client const { sdkAccount } = await withWasmClientLock(async () => { const midenClient = await getMidenClient(); const sdkAccount = await midenClient.getAccount(accountPublicKey); @@ -38,12 +70,15 @@ export async function getOrCreateMultisigService( if (!commitment) { throw new Error('Commitment not found in account storage'); } - + console.log('[PSM Manager] Retrieved commitment from account storage:', commitment); // Get the actual public key from the public key commitment - const publicKey = await useWalletStore.getState().getPublicKeyForCommitment(commitment); + const publicKey = await provider.getPublicKeyForCommitment(commitment); + console.log('[PSM Manager] Retrieved public key for commitment:', publicKey); + // Initialize MultisigService with the account, public key, commitment, and signWord function + const service = await MultisigService.init(sdkAccount, `0x${publicKey}`, `0x${commitment}`, provider.signWord); - // Initialize MultisigService with the account, public key, commitment, sign function, and webClient - const service = await MultisigService.init(sdkAccount, `0x${publicKey}`, `0x${commitment}`, signCallback); + // Cache for future use + psmServiceCache.set(accountPublicKey, service); return service; } @@ -51,8 +86,40 @@ export async function getOrCreateMultisigService( /** * Check if an account is a PSM account. */ -export function isPsmAccount(accountPublicKey: string): boolean { - const accounts = useWalletStore.getState().accounts; +export async function isPsmAccount( + accountPublicKey: string, + provider: PsmAccountProvider = zustandProvider +): Promise { + console.log(`[PSM Manager] Checking if account is PSM: ${accountPublicKey}`); + const accounts = await provider.getAccounts(); + console.log('[PSM Manager] Retrieved accounts from provider:', accounts); const account = accounts.find(acc => acc.publicKey === accountPublicKey); return account?.type === WalletType.Psm; } + +/** + * Sync PSM state for all PSM accounts. + * Called from AutoSync after chain state sync (frontend context only). + */ +export async function syncPsmAccounts(): Promise { + const accounts = await zustandProvider.getAccounts(); + const psmAccounts = accounts.filter(acc => acc.type === WalletType.Psm); + + if (psmAccounts.length === 0) return; + + for (const account of psmAccounts) { + try { + const service = await getOrCreateMultisigService(account.publicKey); + await service.sync(); + } catch (error) { + console.error(`[PSM Sync] Error syncing PSM account ${account.publicKey}:`, error); + } + } +} + +/** + * Clear the PSM service cache. Call on logout/lock. + */ +export function clearPsmCache(): void { + psmServiceCache.clear(); +} diff --git a/src/lib/miden/psm/index.ts b/src/lib/miden/psm/index.ts index 677420803..165e9f0fd 100644 --- a/src/lib/miden/psm/index.ts +++ b/src/lib/miden/psm/index.ts @@ -12,7 +12,6 @@ import { import { DEFAULT_PSM_ENDPOINT } from 'lib/miden-chain/constants'; import { PSM_URL_STORAGE_KEY } from 'lib/settings/constants'; import { u8ToB64 } from 'lib/shared/helpers'; -import { useWalletStore } from 'lib/store'; import { fetchFromStorage } from '../front'; import { accountIdStringToSdk } from '../sdk/helpers'; @@ -43,25 +42,13 @@ export class MultisigService { account: Account, publicKey: string, signerCommitment: string, - signCallback?: (publicKey: string, signingInputs: string) => Promise + signWordFn: SignWordFunction ): Promise { try { - const signer = new WalletSigner(publicKey, signerCommitment, useWalletStore.getState().signWord); + const signer = new WalletSigner(publicKey, signerCommitment, signWordFn); const psmEndpoint = (await fetchFromStorage(PSM_URL_STORAGE_KEY)) || DEFAULT_PSM_ENDPOINT; - const webClient = ( - await MidenClientInterface.create( - signCallback - ? { - signCallback: async (publicKey: Uint8Array, signingInputs: Uint8Array) => { - const keyString = Buffer.from(publicKey).toString('hex'); - const signingInputsString = Buffer.from(signingInputs).toString('hex'); - return await signCallback(keyString, signingInputsString); - } - } - : {} - ) - ).webClient; + const webClient = (await MidenClientInterface.create({})).webClient; const client = new MultisigClient(webClient, { psmEndpoint }); const { psmCommitment } = await client.initialize('falcon'); diff --git a/src/lib/shared/types.ts b/src/lib/shared/types.ts index f38996447..3da432b7b 100644 --- a/src/lib/shared/types.ts +++ b/src/lib/shared/types.ts @@ -56,6 +56,10 @@ export enum WalletMessageType { SignDataResponse = 'SIGN_DATA_RESPONSE', SignTransactionRequest = 'SIGN_TRANSACTION_REQUEST', SignTransactionResponse = 'SIGN_TRANSACTION_RESPONSE', + SignWordRequest = 'SIGN_WORD_REQUEST', + SignWordResponse = 'SIGN_WORD_RESPONSE', + GetPublicKeyForCommitmentRequest = 'GET_PUBLIC_KEY_FOR_COMMITMENT_REQUEST', + GetPublicKeyForCommitmentResponse = 'GET_PUBLIC_KEY_FOR_COMMITMENT_RESPONSE', GetAuthSecretKeyRequest = 'GET_AUTH_SECRET_KEY_REQUEST', GetAuthSecretKeyResponse = 'GET_AUTH_SECRET_KEY_RESPONSE', SubmitTransactionRequest = 'SUBMIT_TRANSACTION_REQUEST', @@ -468,6 +472,27 @@ export interface SignTransactionResponse extends WalletMessageBase { signature: string; } +export interface SignWordRequest extends WalletMessageBase { + type: WalletMessageType.SignWordRequest; + publicKey: string; + wordHex: string; +} + +export interface SignWordResponse extends WalletMessageBase { + type: WalletMessageType.SignWordResponse; + signature: string; +} + +export interface GetPublicKeyForCommitmentRequest extends WalletMessageBase { + type: WalletMessageType.GetPublicKeyForCommitmentRequest; + commitment: string; +} + +export interface GetPublicKeyForCommitmentResponse extends WalletMessageBase { + type: WalletMessageType.GetPublicKeyForCommitmentResponse; + publicKey: string; +} + export interface GetAuthSecretKeyRequest extends WalletMessageBase { type: WalletMessageType.GetAuthSecretKeyRequest; key: string; @@ -666,6 +691,8 @@ export type WalletRequest = | UpdateSettingsRequest | SignDataRequest | SignTransactionRequest + | SignWordRequest + | GetPublicKeyForCommitmentRequest | GetAuthSecretKeyRequest | PageRequest | DAppGetPayloadRequest @@ -714,6 +741,8 @@ export type WalletResponse = | UpdateSettingsResponse | SignDataResponse | SignTransactionResponse + | SignWordResponse + | GetPublicKeyForCommitmentResponse | GetAuthSecretKeyResponse | PageResponse // | DAppGetPayloadResponse diff --git a/src/lib/store/index.ts b/src/lib/store/index.ts index 1e4e05fb6..6efd1eee5 100644 --- a/src/lib/store/index.ts +++ b/src/lib/store/index.ts @@ -265,6 +265,25 @@ export const useWalletStore = create()( return new Uint8Array(Buffer.from(signatureAsHex, 'hex')); }, + signWord: async (publicKey, wordHex) => { + const res = await request({ + type: WalletMessageType.SignWordRequest, + publicKey, + wordHex + }); + assertResponse(res.type === WalletMessageType.SignWordResponse); + return res.signature; + }, + + getPublicKeyForCommitment: async commitment => { + const res = await request({ + type: WalletMessageType.GetPublicKeyForCommitmentRequest, + commitment + }); + assertResponse(res.type === WalletMessageType.GetPublicKeyForCommitmentResponse); + return res.publicKey; + }, + getAuthSecretKey: async key => { const res = await request({ type: WalletMessageType.GetAuthSecretKeyRequest, diff --git a/src/lib/store/types.ts b/src/lib/store/types.ts index 1ad471191..b2caa0c5b 100644 --- a/src/lib/store/types.ts +++ b/src/lib/store/types.ts @@ -119,6 +119,8 @@ export interface WalletActions { // Signing actions signData: (publicKey: string, signingInputs: string) => Promise; signTransaction: (publicKey: string, signingInputs: string) => Promise; + signWord: (publicKey: string, wordHex: string) => Promise; + getPublicKeyForCommitment: (commitment: string) => Promise; getAuthSecretKey: (key: string) => Promise; // DApp actions From bca5958ac1b24106899b926ab321daa6cafab369 Mon Sep 17 00:00:00 2001 From: 0xnullifier Date: Fri, 20 Mar 2026 19:10:24 +0530 Subject: [PATCH 11/71] feat: add account recovery flow for gaurdian backed accounts --- public/_locales/en/en.json | 2 ++ src/app/pages/Welcome.tsx | 16 +++++----- src/lib/miden/back/vault.ts | 20 ++++++++++-- src/lib/miden/front/psm-manager.ts | 17 ++-------- src/lib/miden/psm/account.ts | 23 +++++++++++++- src/lib/miden/psm/index.ts | 28 ++++++++++++++++- src/lib/miden/sdk/miden-client-interface.ts | 28 ++++++++++++++++- .../SelectRecoveryMethod.tsx | 20 +++++++----- src/screens/onboarding/navigator.tsx | 31 ++++++++++++++++--- 9 files changed, 146 insertions(+), 39 deletions(-) diff --git a/public/_locales/en/en.json b/public/_locales/en/en.json index bd5fadbc0..1695e394b 100644 --- a/public/_locales/en/en.json +++ b/public/_locales/en/en.json @@ -416,6 +416,8 @@ "guardianRecoveryDescription": "Recommended. Guardian-based recovery for your account but gaurdian operator can see your state.", "fullyPrivateRecovery": "Fully Private", "fullyPrivateRecoveryDescription": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds.", + "publicAccountRecovery": "Public", + "publicAccountRecoveryDescription": "Recover a public on-chain account.", "default": "Default", "transactionFile": "Transaction file", "verificationFailed": "Verification Failed", diff --git a/src/app/pages/Welcome.tsx b/src/app/pages/Welcome.tsx index f21a1fcb8..5fb0bdabb 100644 --- a/src/app/pages/Welcome.tsx +++ b/src/app/pages/Welcome.tsx @@ -148,9 +148,9 @@ const Welcome: FC = () => { { const hardwareAvailable = await checkHardwareSecurityAvailable(); if (hardwareAvailable) { - // Hardware-only mode: skip password, go directly to confirmation + // Hardware-only mode: skip password, go to recovery method selection setPassword('__HARDWARE_ONLY__'); - navigate('/#confirmation'); + navigate('/#select-recovery-method'); } else { navigate('/#create-password'); } @@ -181,11 +181,7 @@ const Welcome: FC = () => { setPassword(action.payload.password); eventCategory = AnalyticsEventCategory.FormSubmit; // Hardware protection is automatically set up in Vault.spawn() when available - if (onboardingType === OnboardingType.Create) { - navigate('/#select-recovery-method'); - } else { - navigate('/#confirmation'); - } + navigate('/#select-recovery-method'); break; case 'select-recovery-method': setWalletType(action.payload); @@ -241,7 +237,11 @@ const Welcome: FC = () => { } } } else if (step === OnboardingStep.SelectRecoveryMethod) { - navigate('/#create-password'); + if (onboardingType === OnboardingType.Import && password === '__HARDWARE_ONLY__') { + navigate('/#import-from-seed'); + } else { + navigate('/#create-password'); + } } else if (step === OnboardingStep.ImportFromFile || step === OnboardingStep.ImportFromSeed) { navigate('/#select-import-type'); } diff --git a/src/lib/miden/back/vault.ts b/src/lib/miden/back/vault.ts index c8c9eff81..d5e33e7a4 100644 --- a/src/lib/miden/back/vault.ts +++ b/src/lib/miden/back/vault.ts @@ -241,12 +241,28 @@ export class Vault { const hdAccIndex = 0; const walletSeed = deriveClientSeed(walletType, mnemonic, 0); + // Helper to sign words using the vault key (needed for PSM import) + const signWordFn = async (pk: string, wordHex: string) => { + const word = Word.fromHex(wordHex); + const secretKey = await fetchAndDecryptOneWithLegacyFallBack(accAuthSecretKeyStrgKey(pk), vaultKey); + const wasmSecretKey = AuthSecretKey.deserialize(new Uint8Array(Buffer.from(secretKey, 'hex'))); + const signature = wasmSecretKey.sign(word); + return `0x${Buffer.from(signature.serialize().slice(1)).toString('hex')}`; + }; + + // Helper to get public key from commitment (needed for PSM import) + const getPublicKeyForCommitment = async (pkc: string) => { + const sk = await fetchAndDecryptOneWithLegacyFallBack(accAuthSecretKeyStrgKey(pkc), vaultKey); + const wasmSecretKey = AuthSecretKey.deserialize(new Uint8Array(Buffer.from(sk, 'hex'))); + return Buffer.from(wasmSecretKey.publicKey().serialize().slice(1)).toString('hex'); + }; + // Wrap WASM client operations in a lock to prevent concurrent access const accPublicKey = await withWasmClientLock(async () => { const midenClient = await getMidenClient(options); if (ownMnemonic && midenClient.network !== 'mock') { try { - return await midenClient.importPublicMidenWalletFromSeed(walletSeed); + return await midenClient.importAccountBySeed(walletType, walletSeed, signWordFn, getPublicKeyForCommitment); } catch (e) { console.error('Failed to import wallet from seed in spawn, creating new wallet instead', e); return await midenClient.createMidenWallet(walletType, walletSeed); @@ -261,7 +277,7 @@ export class Vault { const initialAccount: WalletAccount = { publicKey: accPublicKey, name: 'Miden Account 1', - isPublic: true, + isPublic: walletType === WalletType.OnChain, type: walletType, hdIndex: hdAccIndex }; diff --git a/src/lib/miden/front/psm-manager.ts b/src/lib/miden/front/psm-manager.ts index 81e66aa0f..37eb13b59 100644 --- a/src/lib/miden/front/psm-manager.ts +++ b/src/lib/miden/front/psm-manager.ts @@ -3,7 +3,7 @@ import { WalletAccount } from 'lib/shared/types'; import { useWalletStore } from 'lib/store'; import { WalletType } from 'screens/onboarding/types'; -import { MULTISIG_SLOT_NAMES } from '../psm/account'; +import { getSignerDetailsFromAccount } from '../psm/account'; import { getMidenClient, withWasmClientLock } from '../sdk/miden-client'; // Cache MultisigService instances to avoid re-initialization on every sync cycle @@ -61,19 +61,8 @@ export async function getOrCreateMultisigService( throw new Error('Account not found in local storage'); } - const mapEntries = sdkAccount.storage().getMapEntries(MULTISIG_SLOT_NAMES.SIGNER_PUBLIC_KEYS); - if (!mapEntries) { - throw new Error('No signer public keys found in account storage'); - } - - const commitment = mapEntries[0].value.slice(2); - if (!commitment) { - throw new Error('Commitment not found in account storage'); - } - console.log('[PSM Manager] Retrieved commitment from account storage:', commitment); - // Get the actual public key from the public key commitment - const publicKey = await provider.getPublicKeyForCommitment(commitment); - console.log('[PSM Manager] Retrieved public key for commitment:', publicKey); + const { commitment, publicKey } = await getSignerDetailsFromAccount(sdkAccount, provider.getPublicKeyForCommitment); + console.log('[PSM Manager] Retrieved signer details - commitment:', commitment, 'publicKey:', publicKey); // Initialize MultisigService with the account, public key, commitment, and signWord function const service = await MultisigService.init(sdkAccount, `0x${publicKey}`, `0x${commitment}`, provider.signWord); diff --git a/src/lib/miden/psm/account.ts b/src/lib/miden/psm/account.ts index 268ac6515..0250e7fa1 100644 --- a/src/lib/miden/psm/account.ts +++ b/src/lib/miden/psm/account.ts @@ -14,6 +14,27 @@ export const MULTISIG_SLOT_NAMES = { PROCEDURE_THRESHOLDS: 'openzeppelin::multisig::procedure_thresholds' } as const; +/** + * Extract signer commitment and public key from a PSM account's storage. + */ +export async function getSignerDetailsFromAccount( + account: Account, + getPublicKeyForCommitment: (commitment: string) => Promise +): Promise<{ commitment: string; publicKey: string }> { + const mapEntries = account.storage().getMapEntries(MULTISIG_SLOT_NAMES.SIGNER_PUBLIC_KEYS); + if (!mapEntries) { + throw new Error('No signer public keys found in account storage'); + } + + const commitment = mapEntries[0].value.slice(2); + if (!commitment) { + throw new Error('Commitment not found in account storage'); + } + + const publicKey = await getPublicKeyForCommitment(commitment); + return { commitment, publicKey }; +} + /** * Create a PSM (Private State Manager) account using the MultisigClient. * @@ -48,7 +69,7 @@ export async function createPsmAccount(webClient: WebClient, seed?: Uint8Array): psmCommitment, psmPublicKey, psmEnabled: true, - storageMode: 'public', + storageMode: 'private', signatureScheme: 'falcon' }); diff --git a/src/lib/miden/psm/index.ts b/src/lib/miden/psm/index.ts index 165e9f0fd..57021ed59 100644 --- a/src/lib/miden/psm/index.ts +++ b/src/lib/miden/psm/index.ts @@ -1,4 +1,4 @@ -import { Account, TransactionRequest } from '@miden-sdk/miden-sdk'; +import { Account, TransactionRequest, WebClient } from '@miden-sdk/miden-sdk'; import { Multisig, MultisigClient, @@ -77,6 +77,32 @@ export class MultisigService { } } + static async importAccountFromPsm( + publicKey: string, + signerCommitment: string, + signWordFn: SignWordFunction, + accountId: string, + webClient: WebClient + ) { + const psmEndpoint = (await fetchFromStorage(PSM_URL_STORAGE_KEY)) || DEFAULT_PSM_ENDPOINT; + const psm = new PsmHttpClient(psmEndpoint); + const signer = new WalletSigner(publicKey, signerCommitment, signWordFn); + psm.setSigner(signer); + try { + const { stateJson } = await psm.getState(accountId); + const accountBase64 = stateJson.data; + const binaryString = atob(accountBase64); + const accountBytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + accountBytes[i] = binaryString.charCodeAt(i); + } + const account = Account.deserialize(accountBytes); + await webClient.newAccount(account, true); + } catch (error) { + console.log('Error fetching account state from PSM:', error); + } + } + /** * Get the account ID for this multisig. */ diff --git a/src/lib/miden/sdk/miden-client-interface.ts b/src/lib/miden/sdk/miden-client-interface.ts index 71cbf8d3b..37858067b 100644 --- a/src/lib/miden/sdk/miden-client-interface.ts +++ b/src/lib/miden/sdk/miden-client-interface.ts @@ -28,7 +28,9 @@ import { WalletType } from 'screens/onboarding/types'; import { ConsumeTransaction, SendTransaction } from '../db/types'; import { toNoteType } from '../helpers'; -import { createPsmAccount } from '../psm/account'; +import { createPsmAccount, getSignerDetailsFromAccount } from '../psm/account'; +import { MultisigService } from '../psm/index'; +import { SignWordFunction } from '../psm/signer'; import { NoteExportType } from './constants'; import { accountIdStringToSdk, getBech32AddressFromAccountId } from './helpers'; @@ -138,6 +140,30 @@ export class MidenClientInterface { return getBech32AddressFromAccountId(account.id()); } + async importAccountBySeed( + walletType: WalletType, + seed: Uint8Array, + signWordFn: SignWordFunction, + getPublicKeyForCommitment: (commitment: string) => Promise + ): Promise { + if (walletType === WalletType.Psm) { + const account = await createPsmAccount(this.webClient, seed); + console.log('[MidenClientInterface] Imported PSM account from seed with ID:', account.id().toString()); + const accountId = account.id().toString(); + const { commitment, publicKey } = await getSignerDetailsFromAccount(account, getPublicKeyForCommitment); + await MultisigService.importAccountFromPsm( + `0x${publicKey}`, + `0x${commitment}`, + signWordFn, + accountId, + this.webClient + ); + return getBech32AddressFromAccountId(account.id()); + } + + return await this.importPublicMidenWalletFromSeed(seed); + } + // TODO: is this method even used? async consumeTransaction(accountId: string, listOfNoteIds: string[], delegateTransaction?: boolean) { const notes = await this.getNotesByIds(listOfNoteIds); diff --git a/src/screens/onboarding/create-wallet-flow/SelectRecoveryMethod.tsx b/src/screens/onboarding/create-wallet-flow/SelectRecoveryMethod.tsx index 0d667ce4a..0a137b1d2 100644 --- a/src/screens/onboarding/create-wallet-flow/SelectRecoveryMethod.tsx +++ b/src/screens/onboarding/create-wallet-flow/SelectRecoveryMethod.tsx @@ -8,11 +8,7 @@ import { Badge } from 'lib/ui/badge'; import { WalletType } from '../types'; -export interface SelectRecoveryMethodScreenProps extends Omit, 'onSubmit'> { - onSubmit?: (payload: WalletType) => void; -} - -type RecoveryOption = { +export type RecoveryOption = { id: WalletType; title: string; description: string; @@ -20,9 +16,18 @@ type RecoveryOption = { isLast?: boolean; }; -export const SelectRecoveryMethodScreen = ({ onSubmit, ...props }: SelectRecoveryMethodScreenProps) => { +export interface SelectRecoveryMethodScreenProps extends Omit, 'onSubmit'> { + onSubmit?: (payload: WalletType) => void; + options?: RecoveryOption[]; +} + +export const SelectRecoveryMethodScreen = ({ + onSubmit, + options: optionsProp, + ...props +}: SelectRecoveryMethodScreenProps) => { const { t } = useTranslation(); - const options: RecoveryOption[] = useMemo( + const defaultOptions: RecoveryOption[] = useMemo( () => [ { id: WalletType.Psm, @@ -39,6 +44,7 @@ export const SelectRecoveryMethodScreen = ({ onSubmit, ...props }: SelectRecover ], [t] ); + const options = optionsProp || defaultOptions; const [selected, setSelected] = React.useState(options.find(o => o.isDefault)?.id || options[0].id); const handleContinue = () => { diff --git a/src/screens/onboarding/navigator.tsx b/src/screens/onboarding/navigator.tsx index 6c95410d1..fb259e0a2 100644 --- a/src/screens/onboarding/navigator.tsx +++ b/src/screens/onboarding/navigator.tsx @@ -2,7 +2,6 @@ import React, { FC, useCallback, useState } from 'react'; import classNames from 'clsx'; import { AnimatePresence, motion } from 'framer-motion'; - import { useTranslation } from 'react-i18next'; import { IconName } from 'app/icons/v2'; @@ -15,7 +14,7 @@ import { ConfirmationScreen } from './common/Confirmation'; import { CreatePasswordScreen } from './common/CreatePassword'; import { WelcomeScreen } from './common/Welcome'; import { BackUpSeedPhraseScreen } from './create-wallet-flow/BackUpSeedPhrase'; -import { SelectRecoveryMethodScreen } from './create-wallet-flow/SelectRecoveryMethod'; +import { RecoveryOption, SelectRecoveryMethodScreen } from './create-wallet-flow/SelectRecoveryMethod'; import { SelectTransactionTypeScreen } from './create-wallet-flow/SelectTransactionType'; import { VerifySeedPhraseScreen } from './create-wallet-flow/VerifySeedPhrase'; import { ImportSeedPhraseScreen } from './import-wallet-flow/ImportSeedPhrase'; @@ -184,8 +183,28 @@ export const OnboardingFlow: FC = ({ return ; case OnboardingStep.CreatePassword: return ; - case OnboardingStep.SelectRecoveryMethod: - return ; + case OnboardingStep.SelectRecoveryMethod: { + const importRecoveryOptions: RecoveryOption[] = [ + { + id: WalletType.Psm, + title: t('guardianRecovery'), + description: t('guardianRecoveryDescription'), + isDefault: true + }, + { + id: WalletType.OnChain, + title: t('publicAccountRecovery'), + description: t('publicAccountRecoveryDescription'), + isLast: true + } + ]; + return ( + + ); + } case OnboardingStep.SelectTransactionType: return ; case OnboardingStep.Confirmation: @@ -212,7 +231,9 @@ export const OnboardingFlow: FC = ({ isHardwareSecurityAvailable, onBiometricChange, biometricAttempts, - biometricError + biometricError, + onboardingType, + t ]); const onBack = () => { From d7a5a4ffbd86a875d2c3f7728747c6320399ef39 Mon Sep 17 00:00:00 2001 From: 0xnullifier Date: Mon, 23 Mar 2026 20:50:45 +0530 Subject: [PATCH 12/71] feat: wip --- package.json | 4 +- src/lib/miden-chain/constants.ts | 4 +- src/lib/miden/activity/transactions.ts | 75 +++++++++++++------------- src/lib/miden/psm/account.ts | 41 +++++++------- src/lib/miden/psm/digest.ts | 61 +++++++++++++++++++++ src/lib/miden/psm/index.ts | 60 ++++++++------------- src/lib/miden/psm/signer.ts | 34 +++++------- test/state-helpers.ts | 8 +-- yarn.lock | 39 +++++++------- 9 files changed, 184 insertions(+), 142 deletions(-) create mode 100644 src/lib/miden/psm/digest.ts diff --git a/package.json b/package.json index 1d2cf1a44..76562166e 100644 --- a/package.json +++ b/package.json @@ -83,8 +83,8 @@ "@miden-sdk/miden-sdk": "^0.13.0", "@newhighsco/storybook-addon-svgr": "^2.0.7", "@noble/hashes": "^1.4.0", - "@openzeppelin/miden-multisig-client": "^0.13.1", - "@openzeppelin/psm-client": "^0.13.0", + "@openzeppelin/guardian-client": "^0.13.3", + "@openzeppelin/miden-multisig-client": "^0.13.3", "@peculiar/webcrypto": "1.1.6", "@radix-ui/react-slot": "^1.2.3", "@segment/analytics-node": "^2.3.0", diff --git a/src/lib/miden-chain/constants.ts b/src/lib/miden-chain/constants.ts index 850feab48..332674384 100644 --- a/src/lib/miden-chain/constants.ts +++ b/src/lib/miden-chain/constants.ts @@ -34,7 +34,7 @@ export const MIDEN_FAUCET_ENDPOINTS = new Map([ ]); export const MIDEN_NOTE_TRANSPORT_LAYER_ENDPOINTS = new Map([ - [MIDEN_NETWORK_NAME.TESTNET, 'https://transport.miden.io'], + [MIDEN_NETWORK_NAME.TESTNET, 'http://127.0.0.1:57292'], [MIDEN_NETWORK_NAME.LOCALNET, 'http://127.0.0.1:57292'] ]); @@ -62,4 +62,4 @@ export const TOKEN_MAPPING = { [MidenTokens.Miden]: { faucetId: 'mtst1aqmat9m63ctdsgz6xcyzpuprpulwk9vg_qruqqypuyph' } }; -export const DEFAULT_PSM_ENDPOINT = 'https://psm-stg.openzeppelin.com'; +export const DEFAULT_PSM_ENDPOINT = 'http://localhost:3000'; diff --git a/src/lib/miden/activity/transactions.ts b/src/lib/miden/activity/transactions.ts index 5c755db13..b5166c723 100644 --- a/src/lib/miden/activity/transactions.ts +++ b/src/lib/miden/activity/transactions.ts @@ -8,6 +8,7 @@ import { TransactionRequest, TransactionResult } from '@miden-sdk/miden-sdk'; +import { type Proposal } from '@openzeppelin/miden-multisig-client'; import { liveQuery } from 'dexie'; import { consumeNoteId } from 'lib/miden-worker/consumeNoteId'; @@ -696,7 +697,7 @@ const generatePsmTransaction = async ( console.log('Generating PSM transaction'); const multisigService = await getOrCreateMultisigService(transaction.accountId, psmProvider); - let proposalResult; + let proposalResult: Proposal; switch (transaction.type) { case 'send': { @@ -729,44 +730,44 @@ const generatePsmTransaction = async ( } // Get the proposal commitment for signing and execution - const proposalCommitment = proposalResult.proposal.commitment; + await multisigService.signAndExecuteProposal(proposalResult.id); // Sign and execute the proposal - const tr = await multisigService.signAndCreateTransactionRequest(proposalCommitment); - - const options: MidenClientCreateOptions = { - signCallback: async (publicKey: Uint8Array, signingInputs: Uint8Array) => { - const keyString = Buffer.from(publicKey).toString('hex'); - const signingInputsString = Buffer.from(signingInputs).toString('hex'); - return await signCallback(keyString, signingInputsString); - } - }; - - // Wrap WASM client operations in a lock to prevent concurrent access - const transactionResultBytes = await withWasmClientLock(async () => { - const midenClient = await getMidenClient(options); - return await midenClient.newTransaction(transaction.accountId, tr.serialize()); - }); - - const transactionResult = TransactionResult.deserialize(transactionResultBytes); - - await withWasmClientLock(async () => { - const midenClient = await getMidenClient(); - await midenClient.submitTransaction(transactionResultBytes, transaction.delegateTransaction); - }); - - switch (transaction.type) { - case 'send': - await completeSendTransaction(transaction as SendTransaction, transactionResult); - break; - case 'consume': - await completeConsumeTransaction(transaction.id, transactionResult); - break; - case 'execute': - default: - await completeCustomTransaction(transaction, transactionResult); - break; - } + // const tr = await multisigService.signAndCreateTransactionRequest(proposalCommitment); + + // const options: MidenClientCreateOptions = { + // signCallback: async (publicKey: Uint8Array, signingInputs: Uint8Array) => { + // const keyString = Buffer.from(publicKey).toString('hex'); + // const signingInputsString = Buffer.from(signingInputs).toString('hex'); + // return await signCallback(keyString, signingInputsString); + // } + // }; + + // // Wrap WASM client operations in a lock to prevent concurrent access + // const transactionResultBytes = await withWasmClientLock(async () => { + // const midenClient = await getMidenClient(options); + // return await midenClient.newTransaction(transaction.accountId, tr.serialize()); + // }); + + // const transactionResult = TransactionResult.deserialize(transactionResultBytes); + + // await withWasmClientLock(async () => { + // const midenClient = await getMidenClient(); + // await midenClient.submitTransaction(transactionResultBytes, transaction.delegateTransaction); + // }); + + // switch (transaction.type) { + // case 'send': + // await completeSendTransaction(transaction as SendTransaction, transactionResult); + // break; + // case 'consume': + // await completeConsumeTransaction(transaction.id, transactionResult); + // break; + // case 'execute': + // default: + // await completeCustomTransaction(transaction, transactionResult); + // break; + // } await multisigService.sync(); }; diff --git a/src/lib/miden/psm/account.ts b/src/lib/miden/psm/account.ts index 0250e7fa1..51ee9e8d7 100644 --- a/src/lib/miden/psm/account.ts +++ b/src/lib/miden/psm/account.ts @@ -1,5 +1,5 @@ import { Account, AuthSecretKey, WebClient } from '@miden-sdk/miden-sdk'; -import { createMultisigAccount, MultisigClient } from '@openzeppelin/miden-multisig-client'; +import { FalconSigner, MultisigClient } from '@openzeppelin/miden-multisig-client'; import { DEFAULT_PSM_ENDPOINT } from 'lib/miden-chain/constants'; import { PSM_URL_STORAGE_KEY } from 'lib/settings/constants'; @@ -56,32 +56,33 @@ export async function createPsmAccount(webClient: WebClient, seed?: Uint8Array): const signerCommitment = sk.publicKey().toCommitment(); // Get PSM endpoint and initialize client - const psmEndpoint = (await fetchFromStorage(PSM_URL_STORAGE_KEY)) || DEFAULT_PSM_ENDPOINT; - const client = new MultisigClient(webClient, { psmEndpoint }); - const { psmCommitment, psmPublicKey } = await client.initialize('falcon'); - - console.log('Creating PSM account with PSM commitment:', psmCommitment); - + const guardianEndpoint = (await fetchFromStorage(PSM_URL_STORAGE_KEY)) || DEFAULT_PSM_ENDPOINT; + const client = new MultisigClient(webClient, { guardianEndpoint }); + const { commitment, pubkey } = await client.guardianClient.getPubkey(); // Create the multisig account using the package utility - const { account } = await createMultisigAccount(webClient, { - threshold: 1, - signerCommitments: [signerCommitment.toHex()], - psmCommitment, - psmPublicKey, - psmEnabled: true, - storageMode: 'private', - signatureScheme: 'falcon' - }); - + const multisig = await client.create( + { + threshold: 1, + signerCommitments: [signerCommitment.toHex()], + guardianCommitment: commitment, + guardianPublicKey: pubkey, + guardianEnabled: true, + storageMode: 'private', + signatureScheme: 'falcon', + seed + }, + new FalconSigner(sk) + ); + await multisig.registerOnGuardian(); // Sync state with the node await webClient.syncState(); // Store the secret key in WebStore for signing - await webClient.addAccountSecretKeyToWebStore(account.id(), sk); + await webClient.addAccountSecretKeyToWebStore(multisig.account.id(), sk); - console.log('PSM account created:', account.id().toString()); + console.log('PSM account created:', multisig.account.id().toString()); - return account; + return multisig.account; } catch (e) { console.error('Error creating PSM account:', e); throw new Error('Failed to create PSM account'); diff --git a/src/lib/miden/psm/digest.ts b/src/lib/miden/psm/digest.ts new file mode 100644 index 000000000..9a8fab4e9 --- /dev/null +++ b/src/lib/miden/psm/digest.ts @@ -0,0 +1,61 @@ +import { AccountId, Felt, FeltArray, Rpo256, Word } from '@miden-sdk/miden-sdk'; +import type { RequestAuthPayload } from '@openzeppelin/guardian-client'; + +export class AuthDigest { + static fromAccountIdWithTimestamp(accountId: string, timestamp: number): Word { + const paddedHex = accountId.startsWith('0x') ? accountId : `0x${accountId}`; + const parsedAccountId = AccountId.fromHex(paddedHex); + const prefix = parsedAccountId.prefix(); + const suffix = parsedAccountId.suffix(); + + const feltArray = new FeltArray([prefix, suffix, new Felt(BigInt(timestamp)), new Felt(0n)]); + + return Rpo256.hashElements(feltArray); + } + + static fromRequest(accountId: string, timestamp: number, requestPayload: RequestAuthPayload): Word { + return AuthDigest.fromAccountIdTimestampAndPayloadWord( + accountId, + timestamp, + AuthDigest.payloadWordFromBytes(requestPayload.toBytes()) + ); + } + + private static fromAccountIdTimestampAndPayloadWord(accountId: string, timestamp: number, payloadWord: Word): Word { + const paddedHex = accountId.startsWith('0x') ? accountId : `0x${accountId}`; + const parsedAccountId = AccountId.fromHex(paddedHex); + const prefix = parsedAccountId.prefix(); + const suffix = parsedAccountId.suffix(); + + const feltArray = new FeltArray([prefix, suffix, new Felt(BigInt(timestamp)), ...payloadWord.toFelts()]); + + return Rpo256.hashElements(feltArray); + } + + static fromCommitmentHex(commitmentHex: string): Word { + const paddedHex = commitmentHex.startsWith('0x') ? commitmentHex : `0x${commitmentHex}`; + const cleanHex = paddedHex.slice(2).padStart(64, '0'); + return Word.fromHex(`0x${cleanHex}`); + } + + private static emptyPayloadWord(): Word { + return Word.fromHex(`0x${'0'.repeat(64)}`); + } + + private static payloadWordFromBytes(bytes: Uint8Array): Word { + if (bytes.length === 0) { + return AuthDigest.emptyPayloadWord(); + } + + const payloadElements: Felt[] = []; + for (let i = 0; i < bytes.length; i += 8) { + let packed = 0n; + for (let j = 0; j < 8 && i + j < bytes.length; j += 1) { + packed |= BigInt(bytes[i + j]) << (8n * BigInt(j)); + } + payloadElements.push(new Felt(packed)); + } + + return Rpo256.hashElements(new FeltArray(payloadElements)); + } +} diff --git a/src/lib/miden/psm/index.ts b/src/lib/miden/psm/index.ts index 57021ed59..c49f49ffb 100644 --- a/src/lib/miden/psm/index.ts +++ b/src/lib/miden/psm/index.ts @@ -3,10 +3,10 @@ import { Multisig, MultisigClient, MultisigConfig, - PsmHttpClient, + GuardianHttpClient, type ProposalMetadata, type TransactionProposal, - type TransactionProposalResult + type Proposal } from '@openzeppelin/miden-multisig-client'; import { DEFAULT_PSM_ENDPOINT } from 'lib/miden-chain/constants'; @@ -46,30 +46,13 @@ export class MultisigService { ): Promise { try { const signer = new WalletSigner(publicKey, signerCommitment, signWordFn); - const psmEndpoint = (await fetchFromStorage(PSM_URL_STORAGE_KEY)) || DEFAULT_PSM_ENDPOINT; + const guardianEndpoint = (await fetchFromStorage(PSM_URL_STORAGE_KEY)) || DEFAULT_PSM_ENDPOINT; const webClient = (await MidenClientInterface.create({})).webClient; - const client = new MultisigClient(webClient, { psmEndpoint }); - const { psmCommitment } = await client.initialize('falcon'); - - // Load the existing multisig account - let multisig: Multisig; - if (account.isNew()) { - console.log('Creating new Multisig for account:', account.id().toString()); - const config: MultisigConfig = { - threshold: 1, - signerCommitments: [signerCommitment], - psmCommitment: psmCommitment, - psmEnabled: true - }; - const psmClient = new PsmHttpClient(psmEndpoint); - psmClient.setSigner(signer); - multisig = new Multisig(account, config, psmClient, signer, webClient); - await multisig.registerOnPsm(); - } else { - multisig = await client.load(account.id().toString(), signer); - } + const client = new MultisigClient(webClient, { guardianEndpoint }); + const multisig = await client.load(account.id().toString(), signer); + return new MultisigService(multisig, client); } catch (error) { console.log('Error initializing MultisigService:', error); @@ -85,7 +68,7 @@ export class MultisigService { webClient: WebClient ) { const psmEndpoint = (await fetchFromStorage(PSM_URL_STORAGE_KEY)) || DEFAULT_PSM_ENDPOINT; - const psm = new PsmHttpClient(psmEndpoint); + const psm = new GuardianHttpClient(psmEndpoint); const signer = new WalletSigner(publicKey, signerCommitment, signWordFn); psm.setSigner(signer); try { @@ -113,8 +96,8 @@ export class MultisigService { /** * Create a send (P2ID) transaction proposal. */ - async createSendProposal(recipientId: string, faucetId: string, amount: bigint): Promise { - return this.multisig.createSendProposal( + async createSendProposal(recipientId: string, faucetId: string, amount: bigint): Promise { + return this.multisig.createP2idProposal( accountIdStringToSdk(recipientId).toString(), accountIdStringToSdk(faucetId).toString(), amount @@ -124,7 +107,7 @@ export class MultisigService { /** * Create a consume notes transaction proposal. */ - async createConsumeNotesProposal(noteIds: string[]): Promise { + async createConsumeNotesProposal(noteIds: string[]): Promise { return this.multisig.createConsumeNotesProposal(noteIds); } @@ -132,7 +115,7 @@ export class MultisigService { * Create a custom transaction proposal from a TransactionSummary. * This is used for 'execute' type transactions. */ - async createCustomProposal(summaryBytes: Uint8Array): Promise { + async createCustomProposal(summaryBytes: Uint8Array): Promise { const txSummaryBase64 = u8ToB64(summaryBytes); // Sync state to ensure we have the latest nonce @@ -151,24 +134,23 @@ export class MultisigService { }; const proposal = await this.multisig.createProposal(nonce, txSummaryBase64, metadata); - const proposals = await this.multisig.syncTransactionProposals(); - return { proposal, proposals }; + return proposal; } - async signAndExecuteProposal(commitment: string): Promise { - await this.multisig.signTransactionProposal(commitment); - await this.multisig.executeTransactionProposal(commitment); + async signAndExecuteProposal(id: string): Promise { + await this.multisig.signProposal(id); + await this.multisig.executeProposal(id); } - async signAndCreateTransactionRequest(commitment: string): Promise { - await this.multisig.signTransactionProposal(commitment); - return await this.multisig.createTransactionProposalRequest(commitment); - } + // async signAndCreateTransactionRequest(id: string): Promise { + // await this.multisig.signProposal(id); + // return await this.multisig. + // } async sync(): Promise { try { - await this.multisig.syncAll(); + await this.multisig.syncState(); this.syncRetryCount = 0; // Reset retry count on successful sync } catch (error) { const isNonceTooLow = @@ -196,4 +178,4 @@ export class MultisigService { } // Re-export types that may be needed by consumers -export type { TransactionProposal, TransactionProposalResult, ProposalMetadata }; +export type { TransactionProposal, ProposalMetadata }; diff --git a/src/lib/miden/psm/signer.ts b/src/lib/miden/psm/signer.ts index bcd69f218..807f18faa 100644 --- a/src/lib/miden/psm/signer.ts +++ b/src/lib/miden/psm/signer.ts @@ -1,43 +1,35 @@ -import { AccountId, Felt, FeltArray, Rpo256, Word } from '@miden-sdk/miden-sdk'; -import { SignatureScheme, Signer } from '@openzeppelin/psm-client'; +import { RequestAuthPayload, SignatureScheme, Signer } from '@openzeppelin/guardian-client'; +import { AuthDigest } from './digest'; export type SignWordFunction = (publicKey: string, wordHex: string) => Promise; export class WalletSigner implements Signer { readonly commitment: string; readonly publicKey: string; readonly scheme: SignatureScheme = 'falcon'; - private signWordFn: SignWordFunction; - readonly commitmentForStorageRetrieval: string; + private signWordFn: (wordHex: string) => Promise; constructor(publicKey: string, commitment: string, signWordFn: SignWordFunction) { this.publicKey = publicKey; this.commitment = commitment; - this.commitmentForStorageRetrieval = commitment.slice(2); - this.signWordFn = signWordFn; + this.signWordFn = (wordHex: string) => signWordFn(commitment.slice(2), wordHex); } async signAccountIdWithTimestamp(accountId: string, timestamp: number): Promise { - const digest = WalletSigner.computeAccountDigest(accountId, timestamp); - console.log('Signing account digest for storage retrieval', accountId); - const sig = await this.signWordFn(this.commitmentForStorageRetrieval, digest.toHex()); + const digest = AuthDigest.fromAccountIdWithTimestamp(accountId, timestamp); + const sig = await this.signWordFn(digest.toHex()); + console.log('Signature for accountId and timestamp:', sig); return sig; } + async signRequest(accountId: string, timestamp: number, requestPayload: RequestAuthPayload): Promise { + const digest = AuthDigest.fromRequest(accountId, timestamp, requestPayload); + return this.signWordFn(digest.toHex()); + } + async signCommitment(commitmentHex: string): Promise { const paddedHex = commitmentHex.startsWith('0x') ? commitmentHex : `0x${commitmentHex}`; - const sig = await this.signWordFn(this.commitmentForStorageRetrieval, paddedHex); + const sig = await this.signWordFn(paddedHex); return sig; } - - static computeAccountDigest(accountId: string, timestamp: number): Word { - const paddedHex = accountId.startsWith('0x') ? accountId : `0x${accountId}`; - const parsedAccountId = AccountId.fromHex(paddedHex); - const prefix = parsedAccountId.prefix(); - const suffix = parsedAccountId.suffix(); - - const feltArray = new FeltArray([prefix, suffix, new Felt(BigInt(timestamp)), new Felt(BigInt(0))]); - - return Rpo256.hashElements(feltArray); - } } diff --git a/test/state-helpers.ts b/test/state-helpers.ts index b0d790cbe..093a49c97 100644 --- a/test/state-helpers.ts +++ b/test/state-helpers.ts @@ -1,6 +1,7 @@ -import { WalletMessageType, WalletStatus, GetStateResponse } from 'lib/shared/types'; -import { request } from 'lib/miden/front/client'; import { IntercomClient } from 'lib/intercom'; +import { request } from 'lib/miden/front/client'; +import { WalletMessageType, WalletStatus, GetStateResponse } from 'lib/shared/types'; +import { WalletType } from 'screens/onboarding/types'; export const PASSWORD = 'pw'; export const MNEMONIC = 'test test test test test test test test test test test test'; @@ -50,7 +51,8 @@ export async function ensureWalletReady() { type: WalletMessageType.NewWalletRequest, password: PASSWORD, mnemonic: MNEMONIC, - ownMnemonic: true + ownMnemonic: true, + walletType: WalletType.OffChain }) ); return getState(); diff --git a/yarn.lock b/yarn.lock index 0a908ce1a..11db2b9a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2155,7 +2155,7 @@ dependencies: "@types/mdx" "^2.0.0" -"@miden-sdk/miden-sdk@^0.13.0": +"@miden-sdk/miden-sdk@0.13.0", "@miden-sdk/miden-sdk@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@miden-sdk/miden-sdk/-/miden-sdk-0.13.0.tgz#df7f639f90931d279761a62bb513374366d755e4" integrity sha512-N0qUCZW9Dvk3Oqj37IrGmm0b0v3Nq5qHsX3BtQIzZIwDXKXKPBxy/0lO40oCwDtwI8AfriZQyMLbJR81Fo4Vpg== @@ -2190,7 +2190,14 @@ "@svgr/webpack" "8.1.0" new-url-loader "0.1.1" -"@noble/hashes@^1.4.0": +"@noble/curves@^1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.7.tgz#79d04b4758a43e4bca2cbdc62e7771352fa6b951" + integrity sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw== + dependencies: + "@noble/hashes" "1.8.0" + +"@noble/hashes@1.8.0", "@noble/hashes@^1.4.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== @@ -2221,24 +2228,20 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@openzeppelin/miden-multisig-client@^0.13.1": - version "0.13.1" - resolved "https://registry.yarnpkg.com/@openzeppelin/miden-multisig-client/-/miden-multisig-client-0.13.1.tgz#da78637e0fb1397a5ba264a4a849ec9bfb094708" - integrity sha512-+potgpQgl4VE5CsXwqPyDuEHM0UY6ZvSATY8LBn0+3ZW8t4udIdLmsr7DzKDoUly6GSQWy8zNveCss/Buj/T8w== +"@openzeppelin/guardian-client@^0.13.3": + version "0.13.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/guardian-client/-/guardian-client-0.13.3.tgz#49f56d59c1aa20593cc201992ddbeaadd45b010c" + integrity sha512-YypaFbTh6WR5xjhn4hFRDUqvDDCHoMhf+cIsbpBF/t1AD3rpw1FkpkLaEYXSF7YQG42yYMKxtv+voX2XjihKTg== + +"@openzeppelin/miden-multisig-client@^0.13.3": + version "0.13.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/miden-multisig-client/-/miden-multisig-client-0.13.3.tgz#1adf757a3d6dcc16ec1b1ffb5b7dacb8715d9fd6" + integrity sha512-Jolr9VREpv6xxLm639Cx8relQu6Bf2WipSLGRpw85M11IgbYZ0E0aSpLkEJ2YwUfvW6QZ+3ItVVIVdS2nz/0Fg== dependencies: - "@miden-sdk/miden-sdk" "^0.13.0" + "@miden-sdk/miden-sdk" "0.13.0" + "@noble/curves" "^1.9.7" "@noble/hashes" "^2.0.1" - "@openzeppelin/psm-client" "^0.13.1" - -"@openzeppelin/psm-client@^0.13.0": - version "0.13.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/psm-client/-/psm-client-0.13.0.tgz#fe4df2282ddede6af451fdfe728e5733bf85f4c0" - integrity sha512-pf/b5CpWfVDbYBSXCDxyuRgbgrKNCB8Y/Bw9U2vW3DteUmfGdsCeCX+PprwVbTvNo1thfja1NzbEfkAvO9XLIw== - -"@openzeppelin/psm-client@^0.13.1": - version "0.13.1" - resolved "https://registry.yarnpkg.com/@openzeppelin/psm-client/-/psm-client-0.13.1.tgz#c7131e3eb9286edb3e156fc6847e17140d0a63f6" - integrity sha512-uUVw7qI7MtW3SUemX0a2JActTjCXojRPtIYPF7OQC7eIu01H9fpY/yqo8DBgQvkUS1wOssPueVVqVBqZEy6PHA== + "@openzeppelin/guardian-client" "^0.13.3" "@peculiar/asn1-schema@^2.0.27", "@peculiar/asn1-schema@^2.3.13": version "2.6.0" From 39eba6bda1ce53b3673fe96202a5f496aad3301a Mon Sep 17 00:00:00 2001 From: 0xnullifier Date: Tue, 24 Mar 2026 22:35:36 +0530 Subject: [PATCH 13/71] chore: bump up versions --- package.json | 4 +- src/lib/miden-chain/constants.ts | 4 +- src/lib/miden/activity/transactions.ts | 73 ++++++++++++-------------- src/lib/miden/psm/index.ts | 8 +-- yarn.lock | 18 +++---- 5 files changed, 52 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 76562166e..586a1d3e4 100644 --- a/package.json +++ b/package.json @@ -83,8 +83,8 @@ "@miden-sdk/miden-sdk": "^0.13.0", "@newhighsco/storybook-addon-svgr": "^2.0.7", "@noble/hashes": "^1.4.0", - "@openzeppelin/guardian-client": "^0.13.3", - "@openzeppelin/miden-multisig-client": "^0.13.3", + "@openzeppelin/guardian-client": "^0.13.4", + "@openzeppelin/miden-multisig-client": "^0.13.4", "@peculiar/webcrypto": "1.1.6", "@radix-ui/react-slot": "^1.2.3", "@segment/analytics-node": "^2.3.0", diff --git a/src/lib/miden-chain/constants.ts b/src/lib/miden-chain/constants.ts index 332674384..850feab48 100644 --- a/src/lib/miden-chain/constants.ts +++ b/src/lib/miden-chain/constants.ts @@ -34,7 +34,7 @@ export const MIDEN_FAUCET_ENDPOINTS = new Map([ ]); export const MIDEN_NOTE_TRANSPORT_LAYER_ENDPOINTS = new Map([ - [MIDEN_NETWORK_NAME.TESTNET, 'http://127.0.0.1:57292'], + [MIDEN_NETWORK_NAME.TESTNET, 'https://transport.miden.io'], [MIDEN_NETWORK_NAME.LOCALNET, 'http://127.0.0.1:57292'] ]); @@ -62,4 +62,4 @@ export const TOKEN_MAPPING = { [MidenTokens.Miden]: { faucetId: 'mtst1aqmat9m63ctdsgz6xcyzpuprpulwk9vg_qruqqypuyph' } }; -export const DEFAULT_PSM_ENDPOINT = 'http://localhost:3000'; +export const DEFAULT_PSM_ENDPOINT = 'https://psm-stg.openzeppelin.com'; diff --git a/src/lib/miden/activity/transactions.ts b/src/lib/miden/activity/transactions.ts index b5166c723..ec1e8dda8 100644 --- a/src/lib/miden/activity/transactions.ts +++ b/src/lib/miden/activity/transactions.ts @@ -729,45 +729,42 @@ const generatePsmTransaction = async ( } } - // Get the proposal commitment for signing and execution - await multisigService.signAndExecuteProposal(proposalResult.id); - // Sign and execute the proposal - // const tr = await multisigService.signAndCreateTransactionRequest(proposalCommitment); - - // const options: MidenClientCreateOptions = { - // signCallback: async (publicKey: Uint8Array, signingInputs: Uint8Array) => { - // const keyString = Buffer.from(publicKey).toString('hex'); - // const signingInputsString = Buffer.from(signingInputs).toString('hex'); - // return await signCallback(keyString, signingInputsString); - // } - // }; - - // // Wrap WASM client operations in a lock to prevent concurrent access - // const transactionResultBytes = await withWasmClientLock(async () => { - // const midenClient = await getMidenClient(options); - // return await midenClient.newTransaction(transaction.accountId, tr.serialize()); - // }); - - // const transactionResult = TransactionResult.deserialize(transactionResultBytes); - - // await withWasmClientLock(async () => { - // const midenClient = await getMidenClient(); - // await midenClient.submitTransaction(transactionResultBytes, transaction.delegateTransaction); - // }); - - // switch (transaction.type) { - // case 'send': - // await completeSendTransaction(transaction as SendTransaction, transactionResult); - // break; - // case 'consume': - // await completeConsumeTransaction(transaction.id, transactionResult); - // break; - // case 'execute': - // default: - // await completeCustomTransaction(transaction, transactionResult); - // break; - // } + const tr = await multisigService.signAndCreateTransactionRequest(proposalResult.id); + + const options: MidenClientCreateOptions = { + signCallback: async (publicKey: Uint8Array, signingInputs: Uint8Array) => { + const keyString = Buffer.from(publicKey).toString('hex'); + const signingInputsString = Buffer.from(signingInputs).toString('hex'); + return await signCallback(keyString, signingInputsString); + } + }; + + // Wrap WASM client operations in a lock to prevent concurrent access + const transactionResultBytes = await withWasmClientLock(async () => { + const midenClient = await getMidenClient(options); + return await midenClient.newTransaction(transaction.accountId, tr.serialize()); + }); + + const transactionResult = TransactionResult.deserialize(transactionResultBytes); + + await withWasmClientLock(async () => { + const midenClient = await getMidenClient(); + await midenClient.submitTransaction(transactionResultBytes, transaction.delegateTransaction); + }); + + switch (transaction.type) { + case 'send': + await completeSendTransaction(transaction as SendTransaction, transactionResult); + break; + case 'consume': + await completeConsumeTransaction(transaction.id, transactionResult); + break; + case 'execute': + default: + await completeCustomTransaction(transaction, transactionResult); + break; + } await multisigService.sync(); }; diff --git a/src/lib/miden/psm/index.ts b/src/lib/miden/psm/index.ts index c49f49ffb..3f2a7161c 100644 --- a/src/lib/miden/psm/index.ts +++ b/src/lib/miden/psm/index.ts @@ -143,10 +143,10 @@ export class MultisigService { await this.multisig.executeProposal(id); } - // async signAndCreateTransactionRequest(id: string): Promise { - // await this.multisig.signProposal(id); - // return await this.multisig. - // } + async signAndCreateTransactionRequest(id: string): Promise { + await this.multisig.signProposal(id); + return await this.multisig.createTransactionProposalRequest(id); + } async sync(): Promise { try { diff --git a/yarn.lock b/yarn.lock index 11db2b9a8..36fa35281 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2228,20 +2228,20 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@openzeppelin/guardian-client@^0.13.3": - version "0.13.3" - resolved "https://registry.yarnpkg.com/@openzeppelin/guardian-client/-/guardian-client-0.13.3.tgz#49f56d59c1aa20593cc201992ddbeaadd45b010c" - integrity sha512-YypaFbTh6WR5xjhn4hFRDUqvDDCHoMhf+cIsbpBF/t1AD3rpw1FkpkLaEYXSF7YQG42yYMKxtv+voX2XjihKTg== +"@openzeppelin/guardian-client@^0.13.4": + version "0.13.4" + resolved "https://registry.yarnpkg.com/@openzeppelin/guardian-client/-/guardian-client-0.13.4.tgz#7e1f419e14437ee329c1484df7050b84a51f3b9d" + integrity sha512-YMmeBfTOXy2DklfmLW4L7A2OAnzfxGgBZTDXi5XJUKY00E91xfdFkkK3Cgde2ydmFDdy5MYxtF/cyROv5ikJBQ== -"@openzeppelin/miden-multisig-client@^0.13.3": - version "0.13.3" - resolved "https://registry.yarnpkg.com/@openzeppelin/miden-multisig-client/-/miden-multisig-client-0.13.3.tgz#1adf757a3d6dcc16ec1b1ffb5b7dacb8715d9fd6" - integrity sha512-Jolr9VREpv6xxLm639Cx8relQu6Bf2WipSLGRpw85M11IgbYZ0E0aSpLkEJ2YwUfvW6QZ+3ItVVIVdS2nz/0Fg== +"@openzeppelin/miden-multisig-client@^0.13.4": + version "0.13.4" + resolved "https://registry.yarnpkg.com/@openzeppelin/miden-multisig-client/-/miden-multisig-client-0.13.4.tgz#41a5431b03d32939012c388ead208271d204d889" + integrity sha512-A4BhB5Rc3Y/wqsBPKe4nEmQE2ZnnwVoVRpyROp5+qg0jx3XFrjqn3e5CKZz0hC1b5ybb1d2RdTGLLmpIZshekA== dependencies: "@miden-sdk/miden-sdk" "0.13.0" "@noble/curves" "^1.9.7" "@noble/hashes" "^2.0.1" - "@openzeppelin/guardian-client" "^0.13.3" + "@openzeppelin/guardian-client" "^0.13.4" "@peculiar/asn1-schema@^2.0.27", "@peculiar/asn1-schema@^2.3.13": version "2.6.0" From 2c31050c7aaed38cb03079c887614dd41b43f03d Mon Sep 17 00:00:00 2001 From: 0xnullifier Date: Thu, 26 Mar 2026 01:55:58 +0530 Subject: [PATCH 14/71] fix: move psm sync to frontend --- src/lib/miden/front/autoSync.ts | 11 +++++++++-- src/lib/miden/front/useSyncTrigger.ts | 24 ++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/lib/miden/front/autoSync.ts b/src/lib/miden/front/autoSync.ts index 0af2d55d8..ac72107e7 100644 --- a/src/lib/miden/front/autoSync.ts +++ b/src/lib/miden/front/autoSync.ts @@ -1,6 +1,7 @@ import { isMobile } from 'lib/platform'; import { WalletState, WalletStatus } from 'lib/shared/types'; import { useWalletStore } from 'lib/store'; +import { WalletType } from 'screens/onboarding/types'; import { getMidenClient, withWasmClientLock } from '../sdk/miden-client'; import { syncPsmAccounts } from './psm-manager'; @@ -114,8 +115,14 @@ export class Sync { // Sync PSM state after chain sync (runs outside WASM lock — HTTP calls only) try { - console.log('[AutoSync] Syncing PSM accounts...'); - await syncPsmAccounts(); + const psmAccountKeys = useWalletStore + .getState() + .accounts.filter(acc => acc.type === WalletType.Psm) + .map(acc => acc.publicKey); + if (psmAccountKeys.length > 0) { + console.log('[AutoSync] Syncing PSM accounts...'); + await syncPsmAccounts(psmAccountKeys); + } } catch (psmError) { console.error('[AutoSync] PSM sync error:', psmError); } diff --git a/src/lib/miden/front/useSyncTrigger.ts b/src/lib/miden/front/useSyncTrigger.ts index 3e92da571..059fff63f 100644 --- a/src/lib/miden/front/useSyncTrigger.ts +++ b/src/lib/miden/front/useSyncTrigger.ts @@ -3,14 +3,34 @@ import { useEffect } from 'react'; import { isExtension } from 'lib/platform'; import { WalletMessageType, WalletStatus } from 'lib/shared/types'; import { getIntercom, useWalletStore } from 'lib/store'; +import { WalletType } from 'screens/onboarding/types'; + +import { syncPsmAccounts } from './psm-manager'; const SYNC_INTERVAL_MS = 3_000; +function triggerSync(intercom: ReturnType) { + intercom + .request({ type: WalletMessageType.SyncRequest }) + .then(() => { + // PSM sync runs in the frontend where the wallet is unlocked and signWord is available + const psmAccountKeys = useWalletStore + .getState() + .accounts.filter(acc => acc.type === WalletType.Psm) + .map(acc => acc.publicKey); + if (psmAccountKeys.length > 0) { + syncPsmAccounts().catch(() => {}); + } + }) + .catch(() => {}); +} + /** * On extension only: sends SyncRequest to the service worker every 3s. * * The service worker runs syncState() on its warm WASM client and broadcasts * SyncCompleted with notes + balances data. The frontend reads from Zustand only. + * After each chain sync, PSM accounts are synced in the frontend context. */ export function useSyncTrigger() { const status = useWalletStore(s => s.status); @@ -22,11 +42,11 @@ export function useSyncTrigger() { const intercom = getIntercom(); const timer = setInterval(() => { - intercom.request({ type: WalletMessageType.SyncRequest }).catch(() => {}); + triggerSync(intercom); }, SYNC_INTERVAL_MS); // Fire immediately - intercom.request({ type: WalletMessageType.SyncRequest }).catch(() => {}); + triggerSync(intercom); return () => clearInterval(timer); }, [status]); From 8950d2029cbe67f5b29cd6d43166574d1f376183 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 25 Mar 2026 20:31:09 +0000 Subject: [PATCH 15/71] chore: update translation files --- public/_locales/de/messages.json | 8 ++++++++ public/_locales/en/messages.json | 8 ++++++++ public/_locales/en_GB/messages.json | 8 ++++++++ public/_locales/es/messages.json | 8 ++++++++ public/_locales/fr/messages.json | 8 ++++++++ public/_locales/ja/messages.json | 8 ++++++++ public/_locales/ko/messages.json | 8 ++++++++ public/_locales/pl/messages.json | 8 ++++++++ public/_locales/pt/messages.json | 8 ++++++++ public/_locales/ru/messages.json | 8 ++++++++ public/_locales/tr/messages.json | 8 ++++++++ public/_locales/uk/messages.json | 8 ++++++++ public/_locales/zh_CN/messages.json | 8 ++++++++ public/_locales/zh_TW/messages.json | 8 ++++++++ 14 files changed, 112 insertions(+) diff --git a/public/_locales/de/messages.json b/public/_locales/de/messages.json index 245af893e..3afdb0d52 100644 --- a/public/_locales/de/messages.json +++ b/public/_locales/de/messages.json @@ -1803,6 +1803,14 @@ "message": "Nur lokal, niemand außer Ihnen sieht Ihren Status, aber keine Wiederherstellung. Der Verlust Ihres Geräts führt zum dauerhaften Verlust Ihres Geldes.", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "Öffentlich", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "Stellen Sie ein öffentliches On-Chain-Konto wieder her.", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "Standard", "englishSource": "Default" diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index ba2324ca5..627d998f1 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -1767,6 +1767,14 @@ "message": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds.", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "Public", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "Recover a public on-chain account.", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "Default", "englishSource": "Default" diff --git a/public/_locales/en_GB/messages.json b/public/_locales/en_GB/messages.json index 792610bd5..1f0730eb3 100644 --- a/public/_locales/en_GB/messages.json +++ b/public/_locales/en_GB/messages.json @@ -1831,6 +1831,14 @@ "message": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds.", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "Public", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "Recover a public on-chain account.", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "Default", "englishSource": "Default" diff --git a/public/_locales/es/messages.json b/public/_locales/es/messages.json index 0762a10ad..d6d90044f 100644 --- a/public/_locales/es/messages.json +++ b/public/_locales/es/messages.json @@ -1740,6 +1740,14 @@ "message": "Solo local, nadie ve su estado excepto usted, pero no hay recuperación; perder su dispositivo resultará en la pérdida permanente de sus fondos.", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "Público", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "Recuperar una cuenta pública en cadena.", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "Por defecto", "englishSource": "Default" diff --git a/public/_locales/fr/messages.json b/public/_locales/fr/messages.json index 4a817a5cd..2fa46fddd 100644 --- a/public/_locales/fr/messages.json +++ b/public/_locales/fr/messages.json @@ -1802,6 +1802,14 @@ "message": "Local uniquement, personne ne voit votre état sauf vous mais pas de récupération, la perte de votre appareil entraînera une perte permanente de vos fonds.", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "Publique", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "Récupérez un compte public en chaîne.", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "Défaut", "englishSource": "Default" diff --git a/public/_locales/ja/messages.json b/public/_locales/ja/messages.json index e3c8c7afe..a1eddb313 100644 --- a/public/_locales/ja/messages.json +++ b/public/_locales/ja/messages.json @@ -1803,6 +1803,14 @@ "message": "ローカルのみ、あなた以外の誰もあなたの状態を見ることはできませんが、回復はできません。デバイスを失うと資金が永久に失われます。", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "公共", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "公開オンチェーン アカウントを回復します。", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "デフォルト", "englishSource": "Default" diff --git a/public/_locales/ko/messages.json b/public/_locales/ko/messages.json index 7742924ad..541b2b152 100644 --- a/public/_locales/ko/messages.json +++ b/public/_locales/ko/messages.json @@ -1803,6 +1803,14 @@ "message": "로컬에서만 가능하며 귀하 외에는 누구도 귀하의 상태를 볼 수 없지만 복구할 수는 없습니다. 장치를 분실하면 자금이 영구적으로 손실됩니다.", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "공공의", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "공개 온체인 계정을 복구하세요.", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "기본", "englishSource": "Default" diff --git a/public/_locales/pl/messages.json b/public/_locales/pl/messages.json index 2766db6ae..11b7e2ebd 100644 --- a/public/_locales/pl/messages.json +++ b/public/_locales/pl/messages.json @@ -1740,6 +1740,14 @@ "message": "Tylko lokalnie, nikt poza Tobą nie widzi Twojego stanu, ale nie ma możliwości odzyskania urządzenia. Utrata urządzenia spowoduje trwałą utratę środków.", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "Publiczny", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "Odzyskaj publiczne konto w sieci.", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "Domyślny", "englishSource": "Default" diff --git a/public/_locales/pt/messages.json b/public/_locales/pt/messages.json index 3fbd65334..afd6377aa 100644 --- a/public/_locales/pt/messages.json +++ b/public/_locales/pt/messages.json @@ -1801,6 +1801,14 @@ "message": "Somente local, ninguém vê seu estado, exceto você, mas não há recuperação. A perda do seu dispositivo resultará na perda permanente de seus fundos.", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "Público", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "Recupere uma conta pública na rede.", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "Padrão", "englishSource": "Default" diff --git a/public/_locales/ru/messages.json b/public/_locales/ru/messages.json index 7c827c3f2..ca7aa2841 100644 --- a/public/_locales/ru/messages.json +++ b/public/_locales/ru/messages.json @@ -1804,6 +1804,14 @@ "message": "Только локально, никто, кроме вас, не видит ваше состояние, но восстановления нет, потеря устройства приведет к безвозвратной потере ваших средств.", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "Общественный", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "Восстановите общедоступную учетную запись в сети.", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "По умолчанию", "englishSource": "Default" diff --git a/public/_locales/tr/messages.json b/public/_locales/tr/messages.json index fb2a71940..6958e3ff8 100644 --- a/public/_locales/tr/messages.json +++ b/public/_locales/tr/messages.json @@ -1803,6 +1803,14 @@ "message": "Yalnızca yerel, durumunuzu sizden başka kimse göremez ancak iyileşme olmaz, cihazınızı kaybetmek, paranızın kalıcı olarak kaybolmasına neden olur.", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "Halk", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "Herkese açık bir zincir içi hesabı kurtarın.", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "Varsayılan", "englishSource": "Default" diff --git a/public/_locales/uk/messages.json b/public/_locales/uk/messages.json index edd4f4332..803fdcd98 100644 --- a/public/_locales/uk/messages.json +++ b/public/_locales/uk/messages.json @@ -1804,6 +1804,14 @@ "message": "Лише локально, ніхто не бачить ваш стан, крім вас, але відновлення не відбувається, втрата пристрою призведе до остаточної втрати ваших коштів.", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "Громадський", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "Відновіть загальнодоступний обліковий запис у мережі.", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "За замовчуванням", "englishSource": "Default" diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json index 2310f8da4..38ee03ca1 100644 --- a/public/_locales/zh_CN/messages.json +++ b/public/_locales/zh_CN/messages.json @@ -1803,6 +1803,14 @@ "message": "仅限本地,除了您之外没有人看到您的状态,但无法恢复,丢失您的设备将导致您的资金永久丢失。", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "民众", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "恢复公共链上账户。", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "默认", "englishSource": "Default" diff --git a/public/_locales/zh_TW/messages.json b/public/_locales/zh_TW/messages.json index 1ea7b633e..5df86fbaa 100644 --- a/public/_locales/zh_TW/messages.json +++ b/public/_locales/zh_TW/messages.json @@ -1803,6 +1803,14 @@ "message": "仅限本地,除了您之外没有人看到您的状态,但无法恢复,丢失您的设备将导致您的资金永久丢失。", "englishSource": "Local only, no one sees your state except you but no recovery, losing your device will result in permanent loss of your funds." }, + "publicAccountRecovery": { + "message": "民众", + "englishSource": "Public" + }, + "publicAccountRecoveryDescription": { + "message": "恢复公共链上账户。", + "englishSource": "Recover a public on-chain account." + }, "default": { "message": "默认", "englishSource": "Default" From 79074fe8782efbf6ccd34292a43a593063861fff Mon Sep 17 00:00:00 2001 From: 0xnullifier Date: Sun, 19 Apr 2026 10:03:42 +0530 Subject: [PATCH 16/71] feat: bump up deps, make guardian work on mobile, make sync work for imported accounts --- CLAUDE.md | 1283 ++----------------- package.json | 12 +- src/lib/intercom/mobile-adapter.ts | 71 +- src/lib/miden-chain/constants.ts | 2 +- src/lib/miden/activity/transactions.ts | 61 +- src/lib/miden/psm/account.ts | 22 +- src/lib/miden/psm/digest.ts | 2 +- src/lib/miden/psm/index.ts | 36 +- src/lib/miden/sdk/helpers.ts | 4 + src/lib/miden/sdk/miden-client-interface.ts | 30 +- src/lib/miden/sdk/miden-client.ts | 10 +- vite.extension.config.ts | 99 +- yarn.lock | 37 +- 13 files changed, 333 insertions(+), 1336 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 01e2db2a2..8bf19a6d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,1240 +1,181 @@ # CLAUDE.md -This file provides guidance for Claude Code when working on this repository. +Guidance for Claude Code. **Self-maintaining:** update proactively when you learn a gotcha, pattern, or debugging trick worth keeping. -**Self-maintaining document:** Proactively update this file when you learn something worth remembering - new patterns, gotchas, debugging techniques, or project-specific knowledge. Don't wait to be asked. +## Project -## Project Overview +Miden Wallet: Chrome/Firefox extension + iOS/Android (Capacitor) + macOS (Tauri). React + Zustand frontend; service-worker backend (Effector store + vault). Backend is source of truth; frontend syncs via intercom port messaging. -Miden Wallet is a browser extension wallet for the Miden blockchain, also available as a mobile app for iOS and Android. The browser version is built as a Chrome/Firefox extension with a React frontend and a service worker backend. The mobile app uses Capacitor to wrap the web app in a native shell. - -## Architecture - -``` -┌─────────────────────────────────────────────────────────┐ -│ Browser Extension │ -├─────────────────────────┬───────────────────────────────┤ -│ Frontend (Popup/Tab) │ Backend (Service Worker) │ -│ │ │ -│ React + Zustand │ Effector Store │ -│ - UI Components │ - Vault (secure storage) │ -│ - State management │ - Wallet operations │ -│ │ │ -│ ◄──── Intercom (Port messaging) ────► │ -└─────────────────────────┴───────────────────────────────┘ -``` - -**Key principle:** Backend is the source of truth. Frontend syncs via intercom messaging. - -## Key Directories +## Layout ``` src/ ├── lib/ -│ ├── store/ # Zustand store (frontend state) -│ ├── miden/ -│ │ ├── back/ # Backend: Effector store, vault, actions -│ │ ├── front/ # Frontend: hooks, providers, client -│ │ └── sdk/ # Miden SDK integration -│ ├── intercom/ # Port-based messaging between frontend/backend -│ └── shared/types.ts # Shared type definitions -├── app/ # React app entry, pages, templates -├── screens/ # Screen components (onboarding, send, etc.) -└── workers/ # Background service worker entry +│ ├── store/ # Zustand (frontend) +│ ├── miden/{back,front,sdk,psm} +│ ├── intercom/ # port messaging +│ ├── platform/ # isMobile/isIOS/isAndroid/isExtension +│ ├── mobile/ # haptics, back-handler +│ ├── woozie/ # router (navigate, goBack, useLocation, ) +│ └── shared/types.ts +├── app/ | screens/ | workers/ +src-tauri/ # desktop +playwright/e2e/ # E2E harness (chrome + ios) ``` -## Key Modules - -Quick reference for commonly needed utilities: - -| Module | Path | Exports | -|--------|------|---------| -| Platform detection | `lib/platform` | `isMobile()`, `isIOS()`, `isAndroid()`, `isExtension()` | -| Haptic feedback | `lib/mobile/haptics` | `hapticLight()`, `hapticMedium()`, `hapticSelection()` | -| Mobile back handler | `lib/mobile/back-handler` | `initMobileBackHandler()`, `useMobileBackHandler()` | -| Navigation (Woozie) | `lib/woozie` | `navigate()`, `goBack()`, `useLocation()`, `` | -| App environment | `app/env` | `useAppEnv()`, `registerBackHandler()`, `onBack()` | - ## Commands ```bash -yarn install # Install dependencies -yarn build # Build extension (outputs to dist/) -yarn dev # Development build with watch -yarn test # Run Jest tests -yarn lint # ESLint -yarn format # Prettier +yarn dev | build | test | lint | format +yarn build:devnet # network-specific extension build +yarn mobile:ios:run[:devnet] # iOS simulator (iPhone 17 default) +yarn mobile:android +yarn tauri dev +yarn test:e2e:blockchain:{testnet,devnet,localhost} +yarn test:e2e:mobile:{devnet,testnet} ``` -**IMPORTANT:** Always run `yarn lint` and `yarn format` before `yarn build`. The build will fail on lint/prettier errors. - -## Version Bumping - -**CRITICAL:** The extension manifest version is controlled by `package.json`'s `version` field, NOT by `public/manifest.json`. The webpack build (`webpack.public.config.js:69-70`) overrides the manifest version with `pkg.version` from `package.json` during the copy transform. +Node >=22 for Capacitor/Tauri: `source ~/.nvm/nvm.sh && nvm use 22`. -When bumping the version: -1. Update **both** `package.json` (`"version": "X.Y.Z"`) and `public/manifest.json` (`"version": "X.Y.Z"`) to keep them in sync -2. Clear webpack cache: `rm -rf node_modules/.cache/webpack` -3. Build: `yarn build:devnet` (or whichever build target) -4. **Verify** the output manifest has the correct version: `grep '"version"' dist/chrome_unpacked/manifest.json` +Lint/format only before commit or when asked — not every build. -If the output still shows the old version, the webpack filesystem cache is stale — delete `node_modules/.cache/webpack` and `dist/` and rebuild. - -## Mobile Development - -**IMPORTANT:** Always use these yarn scripts for mobile development. Do not run Capacitor or Xcode commands directly. - -**IMPORTANT:** When testing mobile changes, always build and run the simulator yourself. Never tell the user to build/test changes themselves - do it for them. - -**iOS Simulator:** Always use **iPhone 17** as the default simulator for testing. - -**Node version:** Capacitor CLI requires Node >= 22. Use nvm to switch: -```bash -source ~/.nvm/nvm.sh && nvm use 22 && yarn mobile:ios:run -# or for Android: -source ~/.nvm/nvm.sh && nvm use 22 && yarn mobile:android -``` - -### iOS - -```bash -yarn mobile:ios # Build, sync, and open in Xcode -yarn mobile:ios:run # Build and run on iOS Simulator (default network) -yarn mobile:ios:run:devnet # Same but explicitly targets devnet -yarn mobile:ios:build # Build for iOS Simulator only -yarn mobile:ios:build:devnet # Same, devnet -yarn mobile:ios:faceid # Fix FaceID enrollment on simulator -``` +## Version bumps -`MIDEN_NETWORK` is baked into the bundle at compile time, so network selection happens at build, not runtime. The `:devnet` variants just set `MIDEN_NETWORK=devnet` for you. - -### Android - -```bash -yarn mobile:android # Build, sync, and open in Android Studio -yarn mobile:android:fingerprint # Trigger fingerprint auth on emulator -``` - -### Build Only - -```bash -yarn build:mobile # Production build for mobile (outputs to dist/mobile/) -yarn build:mobile:dev # Development build for mobile -yarn mobile:sync # Build and sync with Capacitor (no IDE open) -``` - -### Release Builds - -```bash -# Android -yarn mobile:android:keystore # Generate release keystore (one-time) -yarn mobile:android:release # Build AAB for Play Store -yarn mobile:android:release:apk # Build APK for direct distribution - -# iOS -yarn mobile:ios:release # Build release archive -yarn mobile:ios:export # Export IPA for App Store -``` - -See `STORE_LISTING.md` for full app store submission checklist and instructions. - -### Build Output Locations - -**Android APKs/AABs** are output to `android/app/build/outputs/`: -``` -android/app/build/outputs/ -├── apk/ -│ ├── debug/app-debug.apk # Debug APK (yarn mobile:sync && cd android && ./gradlew assembleDebug) -│ └── release/app-release.apk # Release APK (yarn mobile:android:release:apk) -└── bundle/ - └── release/app-release.aab # Release AAB (yarn mobile:android:release) -``` - -**iOS archives** are output to `ios/App/build/`: -``` -ios/App/build/ -├── MidenWallet.xcarchive # Release archive (yarn mobile:ios:release) -└── export/ # Exported IPA (yarn mobile:ios:export) -``` +Extension manifest version comes from `package.json`, NOT `public/manifest.json` (webpack overrides it at `webpack.public.config.js:69-70`). Update **both** to keep in sync, then `rm -rf node_modules/.cache/webpack dist/` if the old version sticks. -### Workflow - -1. Make code changes in `src/` -2. Run `yarn mobile:ios:run` to build and test on iOS Simulator -3. Or run `yarn mobile:ios` to open in Xcode for debugging - -The mobile app shares the same React codebase as the browser extension. Mobile-specific code uses `isMobile()` checks from `lib/platform`. - -### Skip Onboarding (Mobile Testing) - -To skip the entire onboarding UI (seed phrase, verification, password) and jump directly to the "Your wallet is ready → Get started" screen, use one of these methods: - -**Method 1: URL parameter** — After the app launches on the welcome screen, navigate via CDP: -```bash -node /tmp/cdp-eval 'window.location.search = "?__test_skip_onboarding=1"' -``` - -**Method 2: JS global** — Set the global before the component mounts: -```bash -node /tmp/cdp-eval 'window.__TEST_SKIP_ONBOARDING = true; window.location.reload()' -``` - -Both methods auto-generate a random seed phrase, set password to `password1`, and navigate to the confirmation screen. From there, tapping "Get started" (or triggering it via CDP) runs `registerWallet()` which initializes the WASM Worker and creates the wallet. - -To also auto-trigger "Get started" (full end-to-end skip): -```bash -# Wait a moment for the confirmation screen to render, then click -node /tmp/cdp-eval 'document.querySelector("button")?.click(); "clicked"' -``` - -The bypass is in `src/app/pages/Welcome.tsx`. It only activates when `__test_skip_onboarding=1` is in the URL or `window.__TEST_SKIP_ONBOARDING` is set — zero impact on production. - -### Platform-Specific Changes - -**CRITICAL:** This app builds for three platforms: Chrome extension, iOS, and Android. When fixing bugs or adding features: - -1. **Isolate platform-specific fixes** - If a bug only affects iOS, wrap the fix with platform detection (e.g., `if (isIOS()) { ... }`). Don't apply iOS fixes globally unless they genuinely apply to all platforms. -2. **Test across platforms** - Changes to shared code can break other platforms unexpectedly. -3. **Use platform detection** - `isMobile()`, `isIOS()`, `isAndroid()` from `lib/platform` for conditional logic. - -### Haptic Feedback - -**IMPORTANT:** When adding new tappable components (buttons, links, toggles, list items, etc.), always add haptic feedback for mobile users. - -```typescript -import { hapticLight, hapticMedium, hapticSelection } from 'lib/mobile/haptics'; - -// Use hapticLight() for button taps, navigation links, card clicks -// Use hapticMedium() for toggles, checkboxes, radio buttons -// Use hapticSelection() for tab changes, footer navigation -``` - -The haptic functions automatically check `isMobile()` and the user's haptic feedback setting - no need to wrap in conditionals. - -Components that already have haptics (for reference): -- `components/Button.tsx`, `app/atoms/Button.tsx` - buttons -- `lib/woozie/Link.tsx` - navigation links -- `components/Toggle.tsx`, `app/atoms/ToggleSwitch.tsx` - toggles -- `components/TabBar.tsx`, `app/atoms/TabSwitcher.tsx` - tabs -- `components/FooterIconWrapper.tsx` - footer navigation -- `components/CardItem.tsx`, `components/ListItem.tsx` - tappable items -- `components/Chip.tsx`, `components/CircleButton.tsx` - misc buttons -- `app/atoms/Checkbox.tsx`, `components/RadioButton.tsx` - form controls - -### Known iOS-Specific Issues - -- **WASM/WebWorker behavior** - iOS Safari has different WebWorker/WASM memory handling than Android/Chrome -- **IndexedDB quirks** - Safari's IndexedDB implementation has known limitations (e.g., doesn't work in private browsing, stricter storage quotas) -- **Memory pressure** - iOS is more aggressive about limiting memory; watch for OOM issues with multiple WASM worker instances - -### File Downloads on Mobile - -**The standard web download approach does NOT work on mobile:** -```typescript -// This works on desktop but NOT on iOS/Android WebView -const a = document.createElement('a'); -a.href = URL.createObjectURL(blob); -a.download = 'file.json'; -a.click(); // Does nothing on mobile! -``` - -**Use Capacitor Filesystem + Share plugins instead:** -```typescript -import { Directory, Encoding, Filesystem } from '@capacitor/filesystem'; -import { Share } from '@capacitor/share'; -import { isMobile } from 'lib/platform'; - -if (isMobile()) { - // Write to cache, then share - const result = await Filesystem.writeFile({ - path: 'file.json', - data: fileContent, - directory: Directory.Cache, - encoding: Encoding.UTF8 - }); - await Share.share({ url: result.uri }); -} else { - // Standard web download for desktop -} -``` - -### Adding/Removing Capacitor Plugins - -When **adding** new Capacitor plugins: - -1. Install: `yarn add @capacitor/plugin-name` -2. Sync: `yarn mobile:sync` (updates iOS and Android native projects) -3. **Add ProGuard rules** for Android release builds in `android/app/proguard-rules.pro`: - ``` - -keep class com.capacitorjs.plugins.pluginname.** { *; } - ``` -4. Check if iOS needs Info.plist permissions (most plugins document this) - -When **removing** Capacitor plugins: - -1. Uninstall: `yarn remove @capacitor/plugin-name` -2. Sync: `yarn mobile:sync` (updates iOS and Android native projects) -3. **Remove ProGuard rules** from `android/app/proguard-rules.pro` for the removed plugin - -### Native Navbar Overlay (iOS + Android) - -The mobile wallet hides the React footer and renders its bottom nav as a native floating pill on both iOS and Android. The pill has a main row (Home / Activity / Browser), an optional secondary row (Send / Receive / Settings), and a compact mode with a primary action button (e.g. Continue in the Send flow). - -**iOS**: `MidenNavbarOverlayWindow` in `packages/dapp-browser/ios/Sources/InAppBrowserPlugin/WKWebViewController.swift` — a dedicated `UIWindow` at `.normal + 200` that sits above the Capacitor host window AND every dApp WKWebView window. UIWindow z-order is automatic. - -**Android**: `packages/dapp-browser/android/src/main/java/ee/forgr/capacitor_inappbrowser/navbar/` — a two-instance architecture: -- `NavbarState` + `NavbarStateHolder` — immutable state + observer pattern -- `NavbarOverlayManager` — coordinator that lazily creates the Activity-scoped `NavbarView`, spawns a fresh Dialog-scoped `NavbarView` when a `WebViewDialog` shows (via `OnShowListener`), and detaches it on dismiss. Arbitrates which instance is visible via a simple stack-top rule. -- `NavbarView` — the actual FrameLayout hierarchy: shadowWrap → blurContainer → outerVStack → [secondaryRow, contentStack[navStack, actionButton]] -- `NavbarButton`, `NavbarSecondaryButton`, `NavbarActionButton` — individual button views with platform-matched styling - -**Why two instances on Android**: Android sub-windows (`TYPE_APPLICATION_PANEL`) stack with their parent window's token, so a view attached to the Activity would be covered by a Dialog. Instead of fighting z-order, we give the Activity one navbar view and each WebViewDialog its own, both observing the same state holder. Exactly one instance is visible at any time, picked by which window is frontmost. - -**Plugin methods** (iOS + Android, same signatures): -- `showNativeNavbar({items, activeId})` — show pill with 3 main-row items -- `hideNativeNavbar()` — hide pill entirely -- `setNativeNavbarActive({id})` — update active main-row pill without rebuild -- `setNavbarSecondaryRow({items, activeId})` — populate or clear secondary row; empty items collapses the row via spring animation -- `setNavbarAction({label, enabled})` — enter compact mode with a primary action pill -- `clearNavbarAction()` — exit compact mode, restore default nav row layout -- `morphNavbarOut()` / `morphNavbarIn()` — slide pill off-screen for drawer presentations - -**Events** (JS listens via `InAppBrowser.addListener`): -- `nativeNavbarTap` → `{id}` when a main-row button is tapped -- `nativeNavbarSecondaryTap` → `{id}` when a secondary-row button is tapped -- `nativeNavbarActionTap` → `{}` when the compact-mode action button is tapped - -**Wallet JS wiring** lives in `src/app/providers/DappBrowserProvider.tsx` — two effects watching `location.pathname` drive the main and secondary rows; a third watches the confirmation store for drawer morph-out. - -**Gotchas**: -- `MATCH_PARENT` children in `WRAP_CONTENT` FrameLayouts inflate the parent to ancestor AT_MOST. Use background drawables instead of child views for active-state pills (learned the hard way — `NavbarButton` used to do this and produced 1878px-tall buttons). See commit `64145d74` in the navbar checkin. -- `NavbarButton` is pinned to 60dp via `setMinimumHeight` so compact mode can't grow the toolbar. Also mirrored on iOS as `NavbarButton.buttonHeight = 60`. -- On Android, `Dialog.getWindow().setLayout(MATCH_PARENT, MATCH_PARENT)` must be called AFTER `setContentView()`. Otherwise the default `wrap_content` wins and the Dialog is a tiny centered blob. -- Android's `setRenderEffect(createBlurEffect(...))` blurs the view's own content, NOT what's behind it. There's no clean backdrop blur primitive for a decor-view child. We use a solid translucent pill and accept the platform difference. -- Shadow elevation must be on the view with the background drawable (blurContainer), not a wrapper without an outline — or the shadow just doesn't render. - -### Debugging iOS Issues - -**Debug UI components:** When adding debug panels to the UI, ensure all text is **selectable** (use `select-text` or `user-select: text`) so the user can copy/paste error messages instead of retyping them. - -**IMPORTANT:** Do NOT use `console.log` for iOS debugging - those logs go to Safari Web Inspector which Claude Code cannot access. - -**Instead, use native iOS logging that can be read via CLI:** -```bash -# Stream logs from running simulator (filter for webkit/app logs) -xcrun simctl spawn booted log stream --predicate 'process == "App"' --level debug - -# Or capture to file for later analysis -xcrun simctl spawn booted log stream --predicate 'process == "App"' > ios_logs.txt & -``` - -**For JavaScript code, use Capacitor's native logging or write to a debug file** that can be read from the simulator's file system. - -**Alternative: Safari Web Inspector (manual, last resort):** -1. Run the app in simulator: `yarn mobile:ios:run` -2. Open Safari on Mac → Develop menu → Simulator → select the app -3. Console tab shows JavaScript logs - -### CDP Bridge for iOS WebView Debugging - -Use the `inspect` CLI (`@inspectdotdev/cli`) + a persistent-connection daemon to evaluate JavaScript in the Capacitor WKWebView from Claude Code. This gives you access to DOM, computed styles, console output, and error state — much more powerful than screenshots alone. - -**Known issue:** The inspect bridge has a single-use bug — after a WebSocket client disconnects, subsequent connections get no responses. The workaround is a persistent-connection daemon that holds ONE WebSocket open and routes all requests through it. - -**Bringup recipe (run once per session):** - -```bash -# 1. Kill old bridges and free ports -pkill -9 -f "inspect" 2>/dev/null -pkill -9 -f "cdp-daemon" 2>/dev/null -lsof -ti:9221,9222,9333 | xargs kill -9 2>/dev/null - -# 2. Reset webinspectord -xcrun simctl spawn booted launchctl kill 9 user/501/com.apple.webinspectord - -# 3. Relaunch the app (re-registers WebView with fresh webinspectord) -xcrun simctl terminate booted com.miden.wallet -xcrun simctl launch booted com.miden.wallet -sleep 2 - -# 4. Start inspect bridge (wait ~5s for it to discover devices) -source ~/.nvm/nvm.sh && nvm use 22 -nohup inspect --no-telemetry > /tmp/inspect.log 2>&1 & -sleep 5 - -# 5. Start CDP daemon (holds persistent WS, routes evals through it) -nohup node /tmp/cdp-daemon.mjs > /tmp/cdp-daemon.log 2>&1 & -sleep 3 - -# 6. Smoke test — should print "2" -node /tmp/cdp-eval '1+1' -``` - -**Usage after bringup:** -```bash -# Evaluate any JS in the WebView -node /tmp/cdp-eval 'document.title' -node /tmp/cdp-eval 'JSON.stringify(window.__cdp_errors)' - -# Set up an error trap to catch async failures -node /tmp/cdp-eval 'window.__cdp_errors = []; window.addEventListener("error", e => window.__cdp_errors.push(e.message)); window.addEventListener("unhandledrejection", e => window.__cdp_errors.push("REJECTION: " + (e.reason?.message || e.reason))); "trap set"' -``` - -**Recovery when it stops working:** -- Smoke test fails → restart from step 2 (webinspectord reset) -- Bridge sees `[]` on `/json` → restart from step 3 (app crashed) -- After `yarn mobile:ios:run` rebuild → restart from step 4 (PID changed) -- Nothing works → quit Simulator.app entirely (`osascript -e 'tell application "Simulator" to quit'`), cold-boot, restart from step 1 - -**Files:** `/tmp/cdp-daemon.mjs` (persistent WS daemon), `/tmp/cdp-eval` (one-shot client). If missing, recreate from the memory file at `~/.claude/projects/-Users-celrisen-miden-miden-wallet/memory/cdp-bridge-single-use-bug.md`. - -### Verifying Mobile UI Fixes - -**IMPORTANT:** When fixing mobile UI issues (layout, spacing, safe areas, etc.), always verify the fix by taking screenshots from the simulator and analyzing them visually. Do not rely solely on code inspection. - -**Workflow for UI fixes:** -1. Build and run on simulator: `yarn mobile:ios:run` -2. Take a screenshot: `xcrun simctl io booted screenshot /tmp/screenshot.png` -3. Read the screenshot file to visually verify the fix -4. If authentication is needed, trigger FaceID: `xcrun simctl spawn booted notifyutil -p com.apple.BiometricKit_Sim.fingerTouch.match` -5. Wait briefly and take another screenshot: `sleep 2 && xcrun simctl io booted screenshot /tmp/screenshot2.png` - -**Example verification flow:** -```bash -# Build and launch -source ~/.nvm/nvm.sh && nvm use 22 && yarn mobile:ios:run - -# Take screenshot after app loads -xcrun simctl io booted screenshot /tmp/ios-test.png - -# Authenticate if needed (for locked wallet) -xcrun simctl spawn booted notifyutil -p com.apple.BiometricKit_Sim.fingerTouch.match - -# Wait and capture main screen -sleep 2 && xcrun simctl io booted screenshot /tmp/ios-main.png -``` - -**Common iOS layout issues and fixes:** -- **Grey bar at bottom:** Usually caused by `100dvh` height not accounting for safe areas. Use `100%` instead and ensure `mobile.html` body has proper safe area padding. -- **Content cut off:** Check if containers have `overflow: hidden` without proper height constraints. -- **Safe area gaps:** Ensure `public/mobile.html` has `padding: env(safe-area-inset-*)` on body, and body background color matches app background (white). - -## Tailwind theme tokens - -**CRITICAL:** In `tailwind.config.ts`, many Tailwind color tokens are mapped to CSS custom properties defined in `src/main.css` (`:root` for light, `.dark` for dark). These tokens **already auto-flip** with the active theme — do NOT add `dark:` variants on top, because that overrides the auto-flip with a *worse* value. - -Tokens that auto-flip: -- `text-black` → `var(--color-text-primary)` → `#000` / `#fff` -- `bg-white` → `var(--color-surface)` → `#fff` / `#3f3f3f99` (translucent) -- `bg-gray-25` → `var(--color-surface-secondary)` → `#f9f9f9` / `#2a2a2a` -- `bg-gray-50` → `var(--color-surface-tertiary)` → `#f3f3f3` / `#333333` -- `bg-gray-100` → `var(--color-hover-bg)` → `#e1dbdb` / `#ffffff0d` -- `text-heading-gray` → `var(--color-text-secondary)` → `#484848` / `#fff` - -**Gotcha:** Writing `text-black dark:text-white` makes `text-white` win in dark — but `white` is mapped to `var(--color-surface)` = `#3f3f3f99` (translucent dark grey). So the explicit `dark:` variant makes the label **less** readable than `text-black` alone. - -When to add `dark:` variants: -- The base class points to a **fixed** palette color — the custom `grey.*` palette in `src/utils/tailwind-colors.js` is NOT theme-aware. Prefer `bg-gray-*` (spelled with `a`) over `bg-grey-*`. -- You need dark-mode-specific contrast in kind — e.g. `dark:bg-pure-white/15` on a TabPicker active pill, where `bg-white` alone is too subtle in dark. `pure-white` / `pure-black` are literal hex (not remapped). -- SVG `fill={...}` on `` takes a literal JS color — Tailwind `dark:` variants don't reach prop values. Read `document.documentElement.classList.contains('dark')` at render time and pass the resolved color. - -Quick check before adding `dark:`: grep `tailwind.config.ts` for the base token name. If it maps to `var(--color-...)`, don't override. - -## Desktop Development (Tauri) - -The desktop app uses Tauri to wrap the web app in a native macOS window. - -### Commands - -```bash -yarn build:desktop # Production build for desktop (outputs to dist/desktop/) -yarn build:desktop:dev # Development build for desktop -yarn tauri dev # Build and run desktop app in dev mode -yarn tauri build # Build release desktop app -``` - -**Node version:** Requires Node >= 22. Use nvm to switch: -```bash -source ~/.nvm/nvm.sh && nvm use 22 && yarn tauri dev -``` - -### Key Directories - -``` -src-tauri/ -├── src/ -│ ├── main.rs # Tauri app entry point -│ ├── dapp_browser.rs # dApp browser window and wallet API -│ └── lib.rs # Command registration -├── scripts/ -│ └── dapp-injection.js # Wallet API injected into dApp pages -├── capabilities/ # Tauri permission capabilities -└── tauri.conf.json # Tauri configuration - -src/lib/desktop/ -├── dapp-browser.ts # TypeScript bindings for Tauri commands -├── DesktopDappHandler.tsx # Handles wallet requests from dApps -└── DesktopDappConfirmationModal.tsx # Manages confirmation overlay in dApp WebView -``` - -### Clearing App State (macOS) - -To completely reset the desktop app state (useful for testing fresh installs or debugging): - -```bash -# Clear all wallet data (IndexedDB, localStorage, WebKit caches) -rm -rf ~/Library/WebKit/com.miden.wallet -rm -rf ~/Library/WebKit/miden-wallet - -# Optional: Also clear Application Support and Caches -rm -rf ~/Library/Application\ Support/com.miden.wallet -rm -rf ~/Library/Caches/com.miden.wallet -``` - -**Important:** The WebKit directories contain the actual IndexedDB/localStorage data. The Application Support directory may be empty or contain minimal data. - -After clearing, restart the app with `yarn tauri dev` to see the onboarding screen. - -### dApp Browser Architecture - -The desktop app includes a separate browser window for dApps: - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Desktop App Architecture │ -├─────────────────────────────────────────────────────────────┤ -│ Main Window (wallet UI) │ dApp Browser Window │ -│ - React app │ - External dApp webpage │ -│ - DesktopDappHandler │ - Injected window.midenWallet│ -│ - Confirmation modal logic │ - URL interception for msgs │ -│ │ │ -│ ◄──── Tauri Events (dapp-wallet-request) ────► │ -└─────────────────────────────────────────────────────────────┘ -``` - -**Communication flow:** -1. dApp calls `window.midenWallet.connect()` or other methods -2. Injection script encodes request as base64 and navigates to `https://miden-wallet-request/{payload}` -3. Tauri's `on_navigation` callback intercepts this URL -4. Request is emitted to main window via Tauri event -5. `DesktopDappHandler` processes and shows confirmation overlay in dApp window -6. Response flows back via similar URL interception pattern - -## Code Style (Prettier) - -This project uses Prettier for code formatting. Always write code that conforms to Prettier rules: - -- **Line length:** Max 120 characters. Break long lines, especially `console.log` statements with multiple arguments -- **Multi-argument calls:** When function calls exceed line length, put each argument on its own line: - ```typescript - // Good - console.log( - '[Component] message:', - value1, - 'key2:', - value2 - ); - - // Bad - will fail prettier - console.log('[Component] message:', value1, 'key2:', value2, 'key3:', value3); - ``` -- **Trailing commas:** Use trailing commas in multi-line arrays/objects -- **Semicolons:** Always use semicolons -- **Quotes:** Single quotes for strings - -Run `yarn format` to auto-fix formatting issues if needed. - -## State Management - -- **Backend:** Effector store in `src/lib/miden/back/store.ts` -- **Frontend:** Zustand store in `src/lib/store/index.ts` -- **Sync:** `WalletStoreProvider` subscribes to `StateUpdated` broadcasts - -Frontend hooks that use Zustand: -- `useMidenContext()` - main wallet state and actions -- `useAllBalances()` - token balances with polling -- `useAllTokensBaseMetadata()` - asset metadata cache - -## Intercom Messaging - -Frontend ↔ Backend communication uses `IntercomClient`/`IntercomServer`: - -```typescript -// Frontend request -const res = await intercom.request({ type: WalletMessageType.EditAccountRequest, ... }); - -// Backend broadcasts state changes -intercom.broadcast({ type: WalletMessageType.StateUpdated }); -``` - -Message types defined in `src/lib/shared/types.ts`. - -## Navigation Architecture - -**IMPORTANT - Maintain this section:** When adding new screens, routes, or modifying navigation flows, update the route maps and flow documentation below. This ensures mobile back button handling stays correct and future developers understand the navigation structure. - -The app uses **two separate navigation systems**: - -### 1. Woozie (Global Page Navigation) - -Custom lightweight router in `src/lib/woozie/`. Uses History API with hash-based URLs (`USE_LOCATION_HASH_AS_URL = true`). - -**Key exports:** -- `navigate(path)` - Navigate to a route -- `goBack()` - Go back in history (`window.history.go(-1)`) -- `useLocation()` - Get current `pathname`, `historyPosition`, etc. -- `` - Declarative navigation with haptic feedback - -**History tracking:** -- `historyPosition` tracks position in navigation stack (0 = first page in session) -- Used to determine if back navigation is available - -### 2. Navigator (Internal Step Navigation) - -Component-based navigator in `src/components/Navigator.tsx` for multi-step flows. - -**Used by:** -- `SendManager` (`src/screens/send-flow/SendManager.tsx`) -- `EncryptedFileManager` (`src/screens/encrypted-file-flow/EncryptedFileManager.tsx`) - -**Key exports:** -- `useNavigator()` - Returns `{ navigateTo, goBack, cardStack, activeRoute }` -- `cardStack` - Array of visited routes (step history) -- `goBack()` - Pops from cardStack (only works if `cardStack.length > 1`) - -### Route Map - -**Tab Pages** (with persistent footer, via `TabLayout`): -| Route | Component | Back Behavior | -|-------|-----------|---------------| -| `/` | Explore (Home) | Minimize app (Android) / Nothing (iOS) | -| `/history/:programId?` | AllHistory | → Home | -| `/settings/:tabSlug?` | Settings | Sub-tab → Settings main → Home | -| `/browser` | Browser | → Home | - -**Settings Sub-Tabs** (`/settings/:tabSlug`): -| Tab Slug | Component | Notes | -|----------|-----------|-------| -| `general-settings` | GeneralSettings | Theme, analytics, haptics | -| `language` | LanguageSettings | App language selection | -| `address-book` | AddressBook | Saved contacts | -| `reveal-seed-phrase` | RevealSeedPhrase | Only shown for non-public accounts | -| `edit-miden-faucet-id` | EditMidenFaucetId | Hidden from menu | -| `encrypted-wallet-file` | EncryptedFileFlow | Opens as full dialog (see flow below) | -| `advanced-settings` | AdvancedSettings | Developer options | -| `dapps` | DAppSettings | Authorized dApps management | -| `about` | About | Version info, links | -| `networks` | NetworksSettings | Hidden from menu | - -**Full-Screen Pages** (slide animation, via `FullScreenPage` or `PageLayout`): -| Route | Component | Back Behavior | -|-------|-----------|---------------| -| `/send` | SendFlow | See Send Flow below | -| `/receive` | Receive | → Home | -| `/faucet` | Faucet | → Home | -| `/get-tokens` | GetTokens | → Home | -| `/select-account` | SelectAccount | → Home | -| `/create-account` | CreateAccount | → Previous | -| `/edit-name` | EditAccountName | → Previous | -| `/import-account/:tabSlug?` | ImportAccount | → Previous | -| `/history-details/:transactionId` | HistoryDetails | → History | -| `/token-history/:tokenId` | TokenHistory | → Home | -| `/manage-assets/:assetType?` | ManageAssets | → Home | -| `/encrypted-wallet-file` | EncryptedFileFlow | See Encrypted Flow below | -| `/generating-transaction` | GeneratingTransaction | (Modal - no back) | -| `/consuming-note/:noteId` | ConsumingNote | (Processing - no back) | -| `/import-note-pending/:noteId` | ImportNotePending | → Home | -| `/import-note-success` | ImportNoteSuccess | → Home | -| `/import-note-failure` | ImportNoteFailure | → Home | - -**Onboarding/Auth Routes** (catch-all when locked): -- `/reset-required`, `/reset-wallet`, `/forgot-password`, `/forgot-password-info` - -### Send Flow Steps (Internal Navigator) - -Route: `/send` → `SendManager` with internal step navigation: - -| Step | Component | Back Behavior | -|------|-----------|---------------| -| 1. SelectToken | Token picker | → Close flow (Home) | -| 2. SelectRecipient | Address input | → SelectToken | -| 3. AccountsList | Modal overlay | → Dismiss (stays on SelectRecipient) | -| 4. SelectAmount | Amount input | → SelectRecipient | -| 5. ReviewTransaction | Confirm details | → SelectAmount | -| 6. GeneratingTransaction | Processing | (No back) | -| 7. TransactionInitiated | Success | → Home | - -### Encrypted File Flow Steps (Internal Navigator) - -Route: `/encrypted-wallet-file` → `EncryptedFileManager`: - -| Step | Component | Back Behavior | -|------|-----------|---------------| -| 1. CreatePassword | Password setup | → Close flow (Settings) | -| 2. ConfirmPassword | Confirm password | → CreatePassword | -| 3. ExportFile | Download file | → ConfirmPassword | - -### Onboarding Flow (State-Based Navigation) - -**IMPORTANT:** Unlike SendManager/EncryptedFileManager, the onboarding flow does NOT use the Navigator component. It uses hash-based URLs (`/#step-name`) with React state to track the current step. - -Route: `/` (when wallet is locked/new) → `Welcome.tsx` with hash-based steps: - -| Hash | Step | Back Behavior | -|------|------|---------------| -| (none) | Welcome | Minimize app (Android) / Nothing (iOS) | -| `#backup-seed-phrase` | BackupSeedPhrase | → Welcome | -| `#verify-seed-phrase` | VerifySeedPhrase | → BackupSeedPhrase | -| `#select-import-type` | SelectImportType | → Welcome | -| `#import-from-seed` | ImportFromSeed | → SelectImportType | -| `#import-from-file` | ImportFromFile | → SelectImportType | -| `#create-password` | CreatePassword | → Previous step (depends on flow) | -| `#confirmation` | Confirmation | (No back while loading) | - -**Navigation pattern:** -- Steps navigate via `navigate('/#step-name')` which updates the URL hash -- `useEffect` watches the hash and updates `step` state accordingly -- Back navigation calls `onAction({ id: 'back' })` which has switch logic for each step -- Mobile back handler in `Welcome.tsx` triggers this same `onAction({ id: 'back' })` - -**Forgot Password Flow** (`/forgot-password` → `ForgotPassword.tsx`) uses the same pattern with `ForgotPasswordStep` enum. - -### Back Handler System - -**Global handler** in `src/app/env.ts`: -- `registerBackHandler(handler)` - Register a back handler (returns unregister function) -- `onBack()` - Calls the current handler -- Stack-based: handlers can be layered (modals on top of pages) - -**PageLayout** (`src/app/layouts/PageLayout.tsx`) registers default handler: -```typescript -// If history available, go back; otherwise go home -if (historyPosition > 0) { - goBack(); -} else if (!inHome) { - navigate('/', HistoryAction.Replace); -} -``` - -### Mobile Back Button - -**IMPORTANT:** Hardware back button on Android and swipe-back on iOS require `@capacitor/app` plugin and explicit handling. Without it, back gestures close the app instead of navigating. - -Back handlers must be registered for: -1. Global navigation (MobileBackBridge component) -2. Navigator-based flows (SendManager, EncryptedFileManager) -3. State-based flows (Welcome/onboarding, ForgotPassword) -4. Modals/dialogs that should close on back - -## Code Patterns - -### Adding a new wallet action - -1. Add message types to `src/lib/shared/types.ts` -2. Add handler in `src/lib/miden/back/actions.ts` -3. Register handler in `src/lib/miden/back/main.ts` -4. Add action to Zustand store in `src/lib/store/index.ts` -5. Expose via `useMidenContext()` in `src/lib/miden/front/client.ts` - -### Optimistic updates - -```typescript -// In store action -editSomething: async (id, value) => { - const prev = get().items; - set({ items: /* optimistic value */ }); - try { - await request({ ... }); - } catch (error) { - set({ items: prev }); // Rollback - throw error; - } -} -``` - -## Balance Loading Architecture - -The wallet uses an IndexedDB-first pattern for instant UI updates: - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Balance Loading Flow │ -├─────────────────────────────────────────────────────────────┤ -│ 1. fetchBalances() → getAccount() → IndexedDB (instant) │ -│ 2. AutoSync (1s interval) → syncState() → Miden Node │ -│ 3. syncState updates IndexedDB → next fetchBalances sees it│ -└─────────────────────────────────────────────────────────────┘ -``` - -- `fetchBalances` in `src/lib/store/utils/fetchBalances.ts` reads from IndexedDB via `getAccount()` - it does NOT call `syncState()` -- `AutoSync` class in `src/lib/miden/front/sync.ts` handles background network sync separately -- This separation allows showing cached balances instantly while syncing in background - -**Important distinction:** -- `getAccount(accountId)` - reads balance from IndexedDB (local cache) -- `syncState()` - syncs with Miden node and updates IndexedDB -- `importAccountById(assetId)` - imports **asset/token metadata**, not account balances - -## WASM Client Concurrency - -**CRITICAL:** The Miden WASM client cannot handle concurrent access. Concurrent calls cause: -``` -Error: recursive use of an object detected which would lead to unsafe aliasing in rust -``` - -**Always wrap WASM client operations in `withWasmClientLock`:** +## Critical gotchas +### WASM client concurrency +Miden WASM client is single-threaded. Concurrent calls throw `recursive use of an object ... unsafe aliasing`. **Always** wrap in `withWasmClientLock`: ```typescript import { getMidenClient, withWasmClientLock } from 'lib/miden/sdk/miden-client'; - -// CORRECT - always use the lock -const result = await withWasmClientLock(async () => { - const midenClient = await getMidenClient(); - return midenClient.someOperation(); -}); - -// WRONG - direct access without lock causes concurrency errors -const midenClient = await getMidenClient(); -const result = await midenClient.someOperation(); +await withWasmClientLock(async () => (await getMidenClient()).someOp()); ``` -**This applies everywhere**, including: -- Transaction workers in `src/workers/` (consumeNoteId.ts, sendTransaction.ts, submitTransaction.ts) -- Backend operations in `src/lib/miden/back/` -- Frontend hooks in `src/lib/miden/front/` -- Any new code that accesses `getMidenClient()` +### Tailwind auto-flipping tokens +Many tokens in `tailwind.config.ts` map to CSS vars in `src/main.css` and auto-flip with theme. Do NOT add `dark:` variants on these — it overrides the auto-flip with a worse value: +- `text-black`, `bg-white`, `bg-gray-25/50/100`, `text-heading-gray` -The lock ensures only one WASM operation runs at a time across the entire app, preventing AutoSync, dApp requests, and user operations from conflicting. +Add `dark:` only on fixed-palette colors (`grey.*` custom palette, `pure-white`, `pure-black`) or SVG `fill={...}` props (check `document.documentElement.classList.contains('dark')` at render). -## Testing +### i18n required +All user-facing text must use `t('key')` or ``. CI blocks non-i18n strings (`yarn lint:i18n`). Add new keys to `public/_locales/en/en.json` (flat format). Placeholders: `$name$`. -- Unit tests in `*.test.ts` files alongside source -- Tests use Jest + React Testing Library -- Mock `lib/intercom` for frontend tests -- Wrap components with `WalletStoreProvider` + `MidenContextProvider` +### Platform isolation +Wrap platform-specific fixes with `isIOS()`/`isAndroid()`/`isMobile()` from `lib/platform`. Don't apply iOS fixes globally. -### Jest Mock Gotchas +### Haptics on tappable components +Add `hapticLight()` (taps), `hapticMedium()` (toggles), `hapticSelection()` (tabs) from `lib/mobile/haptics`. Auto-checks `isMobile()` and user setting. -**Module path resolution:** Mock paths must match how the source file imports the module: -```typescript -// If actions.ts imports: import { Vault } from 'lib/miden/back/vault'; -// Then mock with the same path: -jest.mock('lib/miden/back/vault', () => ({ ... })); -// NOT: jest.mock('./vault', ...) - this won't work -``` +### Mobile file downloads +`` does nothing in WebView. Use `Filesystem.writeFile` + `Share.share` from `@capacitor/{filesystem,share}` when `isMobile()`. -**jsdom limitations:** `window.location.reload` cannot be mocked in jsdom. Use try-catch: -```typescript -try { - functionThatCallsReload(); -} catch { - // reload throws in jsdom, expected -} -``` +### Balance loading +`fetchBalances` reads IndexedDB via `getAccount()` (instant). `AutoSync` (1s interval) calls `syncState()` separately to update IndexedDB. Don't call `syncState()` from the UI path. -**React test cleanup:** Prevent test pollution by cleaning up React roots: -```typescript -afterEach(() => { - testRoot.unmount(); -}); -``` +## Adding a wallet action -## E2E Blockchain Test Harness +1. Message type in `src/lib/shared/types.ts` +2. Handler in `src/lib/miden/back/actions.ts`, register in `back/main.ts` +3. Store action in `src/lib/store/index.ts` +4. Expose via `useMidenContext()` in `src/lib/miden/front/client.ts` -End-to-end tests that exercise real wallet operations against a live Miden network (testnet, devnet, or localhost). Uses Playwright to automate two Chrome extension instances and `miden-client` CLI to deploy a faucet and mint tokens. +## Navigation -### Quick Start +Two systems: +- **Woozie** (`src/lib/woozie/`) — hash-based global router. `navigate`, `goBack`, `useLocation`, ``. +- **Navigator** (`src/components/Navigator.tsx`) — internal step flows (`SendManager`, `EncryptedFileManager`). `useNavigator()` → `{navigateTo, goBack, cardStack}`. -```bash -# Build + run against a specific network (default testnet) -yarn test:e2e:blockchain:testnet -yarn test:e2e:blockchain:devnet -yarn test:e2e:blockchain:localhost - -# Subsequent runs (skip rebuild if no code changes) -E2E_NETWORK=devnet yarn test:e2e:blockchain:run - -# Build only (no test run) — picks up E2E_NETWORK if set, else testnet -yarn test:e2e:blockchain:build - -# Raw form (equivalent to the : shortcuts) -E2E_NETWORK=testnet yarn test:e2e:blockchain -``` - -The `:` scripts set both `E2E_NETWORK` (which endpoints the harness + miden-client CLI use) AND propagate `MIDEN_NETWORK` through to the extension build (which network the wallet connects to). Running with a mismatched pair — e.g. harness on devnet but wallet built for testnet — silently fails because notes land on one network and the wallet listens on the other. - -The harness auto-installs `miden-client-cli` from crates.io on first run, version-matched to the wallet's `@miden-sdk/miden-sdk` package. Requires Rust toolchain (`cargo`). - -### Directory Layout - -``` -playwright/e2e/ - config/environments.ts # Network endpoints per E2E_NETWORK - harness/ # Observability layer (types, timeline, capture, reports) - helpers/ - miden-cli.ts # miden-client CLI wrapper (init, deploy faucet, mint) - wallet-page.ts # Page Object Model for wallet UI automation - fixtures/two-wallets.ts # Playwright fixture: 2 Chrome instances + observability - tests/*.spec.ts # Test specs -playwright.e2e.config.ts # Playwright config (5 min timeout, traces always on) -``` +Onboarding (`Welcome.tsx`) and `ForgotPassword.tsx` use hash-based state (`/#step-name`), NOT Navigator. -### Environment Selection +Back handlers (`src/app/env.ts`): `registerBackHandler` is stack-based. `PageLayout` registers a default that calls `goBack()` if `historyPosition > 0` else navigates home. Mobile hardware/swipe back requires `@capacitor/app` + explicit handlers — must be registered for global nav (`MobileBackBridge`), Navigator flows, state-based flows, and modals. -`E2E_NETWORK` controls both: -- which RPC/prover/transport endpoints the harness + `miden-client` CLI use (via `playwright/e2e/config/environments.ts`) -- which network the extension build bakes into its bundle (piped through to `MIDEN_NETWORK` at build time in `test:e2e:blockchain:build`) +When adding screens/routes, keep this section accurate so mobile back stays correct. -Use the dedicated scripts to avoid mismatches: +## Mobile testing +### Skip onboarding ```bash -yarn test:e2e:blockchain:testnet # default -yarn test:e2e:blockchain:devnet -yarn test:e2e:blockchain:localhost # requires a local Miden node on :57291 -``` - -Or set `E2E_NETWORK` explicitly with the generic script: - -```bash -E2E_NETWORK=devnet yarn test:e2e:blockchain -``` - -### Test Specs - -| Spec | What it tests | -|------|---------------| -| `wallet-lifecycle.spec.ts` | Create, lock, unlock wallets | -| `mint-and-balance.spec.ts` | Deploy faucet via CLI, mint tokens, verify balance in UI | -| `send-public.spec.ts` | Send public note A->B, B syncs and claims | -| `send-private.spec.ts` | Send private note A->B via transport layer | -| `multi-claim.spec.ts` | Mint 3 notes, batch claim | -| `multi-account.spec.ts` | Multiple accounts, switch, send between own accounts | - -### Agentic Debug Mode - -**For AI agents running this as a verification loop.** On test failure, browsers stay open so the agent can inspect live state and hot-reload fixes. - -```bash -# Run with agentic mode -E2E_AGENTIC=true E2E_NETWORK=testnet yarn test:e2e:blockchain:run - -# Or use the shortcut script -yarn test:e2e:blockchain:agentic +node /tmp/cdp-eval 'window.__TEST_SKIP_ONBOARDING = true; window.location.reload()' ``` +Bypass lives in `Welcome.tsx`, only active when flag/query param set. -#### On Failure: What Happens - -1. **Browsers stay open** -- Both Chrome instances remain alive with full wallet state (IndexedDB, service worker, vault) -2. **`test-results/debug-session.json`** is written with connection details: - ```json - { - "wallets": { - "A": { "extensionId": "abc...", "fullpageUrl": "chrome-extension://abc.../fullpage.html", "cdpUrl": "ws://...", "userDataDir": "/tmp/..." }, - "B": { "extensionId": "def...", "fullpageUrl": "chrome-extension://def.../fullpage.html", "cdpUrl": "ws://...", "userDataDir": "/tmp/..." } - }, - "midenCliWorkDir": "/tmp/miden-cli-...", - "reportPath": "test-results/run-.../tests/.../report.json" - } - ``` -3. **`report.json`** contains structured failure diagnosis (see "Reading Failure Reports" below) -4. **Auto-cleanup** after 10 min (configurable via `E2E_AGENTIC_TIMEOUT`) - -#### Agent Investigation Workflow - -After a test failure in agentic mode: - -1. **Read the failure report:** - ```bash - cat test-results/run-/tests//report.json - ``` - Key fields: `failureCategory`, `failedAtStep`, `diagnosticHints`, `stateAtFailure`, `browserErrors` - -2. **Take fresh screenshots of live wallets:** - Use CDP or Playwright's still-alive connection to screenshot the current state. - -3. **Query wallet state via the exposed Zustand store:** - ```typescript - // In page.evaluate() on an open wallet page: - const store = (window as any).__TEST_STORE__; - const state = store.getState(); - // state.status, state.accounts, state.balances, state.currentAccount - ``` - -4. **Trigger a sync manually:** - ```typescript - // In page.evaluate(): - const intercom = (window as any).__TEST_INTERCOM__; - intercom.request({ type: 'SYNC_REQUEST' }); - ``` - -5. **Run miden-client commands against the preserved state:** - ```bash - cd # from debug-session.json - miden-client account --list - miden-client sync - miden-client notes --list - ``` - -6. **Navigate the wallet UI** to different pages to investigate visually. - -#### Hot-Reload: Fix Code and Push to Live Browsers - -The agent can modify wallet source code, rebuild, and reload into the still-open browsers **without losing wallet state**: - +### iOS debugging +`console.log` goes to Safari Web Inspector — Claude cannot read it. Use: ```bash -# 1. Fix the bug in source code -# 2. Rebuild the extension -yarn test:e2e:blockchain:build - -# 3. Reload the extension in each open Chrome instance -# (via page.evaluate on the extension's fullpage tab) -``` - -```typescript -// In page.evaluate() on the wallet's fullpage tab: -chrome.runtime.reload(); -// Extension reloads from updated dist/chrome_unpacked/ -// IndexedDB + vault data PERSIST (tied to extension origin) -// Service worker restarts, in-memory Zustand state resets -``` - -After `chrome.runtime.reload()`, extension pages unload. Re-open the fullpage tab: -``` -chrome-extension:///fullpage.html +xcrun simctl spawn booted log stream --predicate 'process == "App"' ``` -The wallet initializes from IndexedDB -- same accounts, same keys, same balances. The agent can now test the fix against the exact wallet state that caused the failure. - -### Reading Failure Reports - -The `report.json` is designed for machine consumption. Key fields for an AI agent: - -```typescript -{ - failureCategory: string, // "timeout_waiting_for_sync" | "ui_element_not_found" | etc. - diagnosticHints: string[], // Pre-computed suggestions, e.g., "NETWORK: 3 RPC requests failed" - failedAtStep: { - index: number, // Which test step failed (0-based) - name: string, // "sync_wallet_b" - lastAction: string // What was happening when it failed - }, - stateAtFailure: { - walletA: { status, balances, claimableNotes, currentUrl }, - walletB: { status, balances, claimableNotes, currentUrl } - }, - browserErrors: [...], // JS errors from both extension instances - failedNetworkRequests: [...], // Failed RPC calls - recentEvents: [...], // Last 50 timeline events before failure - timing: { - wasTimeout: boolean, // Did we hit the test timeout? - slowestSteps: [...] // Which steps were unusually slow? - } -} -``` - -**Diagnosis flowchart:** -- `wasTimeout: true` + `failedAtStep.name` contains "sync" -> Blockchain sync issue, check network -- `browserErrors` contains "recursive use of an object" -> WASM concurrency bug -- `failedNetworkRequests` not empty -> Node/RPC connectivity issue -- `failureCategory === 'ui_element_not_found'` -> UI changed, update selectors in `wallet-page.ts` -- `failureCategory === 'cli_command_failed'` -> Check `recentCliCommands` for stderr - -### Observability Artifacts - -Every test run produces structured artifacts in `test-results/run-/tests//`: - -| File | Purpose | -|------|---------| -| `report.json` | Primary diagnostic document (read this first) | -| `timeline.ndjson` | Chronological event stream (every action, assertion, CLI call, console log) | -| `checkpoints.json` | Step-by-step pass/fail with assertion details | -| `state-snapshots/` | Wallet Zustand state at each checkpoint | -| `cli/` | miden-client CLI invocations with full stdout/stderr | -| `browser/wallet-{a,b}-console.ndjson` | Browser console output from both extensions | -| `screenshots/` | Screenshots at checkpoints + on failure | -| `traces/wallet-{a,b}.zip` | Playwright traces (open with `npx playwright show-trace`) | -| `video/` | Video recordings (only on failure) | - -### Source Modifications for E2E - -The E2E build (`MIDEN_E2E_TEST=true`) exposes test hooks on `window`: - -- `window.__TEST_STORE__` -- Zustand store (`useWalletStore`) for reading wallet state via `page.evaluate()` -- `window.__TEST_INTERCOM__` -- Intercom client instance for sending `SyncRequest` and other messages to the service worker - -These are only present when built with `MIDEN_E2E_TEST=true` and have zero impact on production builds. - -### Custom Faucet Token Discovery - -The E2E harness deploys its own faucet via `miden-client new-account --account-type fungible-faucet`. Tokens from this custom faucet appear in the wallet because: -- The wallet fetches ALL fungible assets from the account vault (no `TOKEN_MAPPING` whitelist) -- Token metadata (symbol, decimals) is fetched from the RPC node as long as the faucet is deployed with `--storage-mode public` -- Custom tokens show with their proper symbol (e.g., "TST") in the UI, are selectable in the send flow, and have correct decimal formatting - -## E2E iOS Simulator Test Harness - -Mirror of the Chrome E2E suite, but driving two iPhone 17 / iPhone 17 Pro simulators in parallel against the iOS app. Same 7 specs, ported to `playwright/e2e/ios/tests/*.ios.spec.ts`. - -### Quick Start +For live DOM/JS eval: use the CDP bridge via `inspect` + persistent-connection daemon (`/tmp/cdp-daemon.mjs` + `/tmp/cdp-eval`). Bringup recipe in `~/.claude/projects/-Users-celrisen-miden-miden-wallet/memory/cdp-bridge-single-use-bug.md`. Key steps: kill bridges, reset `com.apple.webinspectord`, relaunch app, start `inspect`, start daemon, smoke-test with `node /tmp/cdp-eval '1+1'`. +### Verifying UI fixes +Always screenshot to verify: ```bash -yarn test:e2e:mobile:devnet # build app + run full iOS suite on devnet -yarn test:e2e:mobile:testnet # same on testnet -yarn test:e2e:mobile:run # skip rebuild (re-run only) -yarn test:e2e:mobile:build # build app only +xcrun simctl io booted screenshot /tmp/shot.png +xcrun simctl spawn booted notifyutil -p com.apple.BiometricKit_Sim.fingerTouch.match # FaceID ``` -The first run boots two simulators (iPhone 17 + iPhone 17 Pro), creating them if absent. UDIDs persist at `test-results-ios/.device-pair.json` so subsequent runs reuse the same booted devices — saves ~30s per run. +### Common iOS layout issues +- Grey bar at bottom → `100dvh` doesn't account for safe areas. Use `100%` + `env(safe-area-inset-*)` padding on `mobile.html` body. +- Debug UI text should be `select-text` so errors are copyable. -### Architecture +### Native navbar overlay +Mobile hides React footer and renders bottom nav as native pill (iOS: `MidenNavbarOverlayWindow` `UIWindow`; Android: two-instance `NavbarOverlayManager` with Activity-scoped + Dialog-scoped `NavbarView`). Plugin methods: `showNativeNavbar`, `setNavbarSecondaryRow`, `setNavbarAction`, `morphNavbar{Out,In}`. Events: `nativeNavbarTap`, `nativeNavbarSecondaryTap`, `nativeNavbarActionTap`. Wiring: `src/app/providers/DappBrowserProvider.tsx`. Android gotchas: don't use `MATCH_PARENT` children in `WRAP_CONTENT` parents (1878px buttons); `Dialog.setLayout` must follow `setContentView`; shadow must be on the view owning the background drawable. +### Adding Capacitor plugins +`yarn add @capacitor/ && yarn mobile:sync`. Add ProGuard rules to `android/app/proguard-rules.pro`: ``` -playwright/e2e/ios/ - helpers/ - simulator-control.ts # xcrun simctl wrapper (boot, install, launch, terminate) - cdp-bridge.ts # WebKit Inspector bridge via appium-remote-debugger - ios-wallet-page.ts # WalletPage interface impl backed by CDP + simctl - fixtures/ - two-simulators.ts # Playwright fixture; same shape as two-wallets - global-setup.ts # asserts App.app, reserves+boots device pair - global-teardown.ts # no-op (devices stay booted between runs) - tests/ - *.ios.spec.ts # ported specs (one-line import change from Chrome) +-keep class com.capacitorjs.plugins..** { *; } ``` +Remove rules when uninstalling. -The `WalletPage` interface in `playwright/e2e/helpers/wallet-page.ts` is shared between Chrome and iOS — same method signatures, different impls. The harness (`playwright/e2e/harness/`) is platform-neutral and is reused wholesale; per-wallet `SnapshotCaps` closures supplied by the fixture absorb platform-specific bits (page.evaluate, service-worker queries on Chrome; CdpSession.evaluate on iOS). - -### CDP Bridge (appium-remote-debugger) - -`remotedebug-ios-webkit-adapter` does NOT work on simulators (it wraps libimobiledevice which is USB-only). We use `appium-remote-debugger` instead, which talks the WebKit Inspector Protocol directly over the per-simulator UNIX socket at `/private/tmp/com.apple.launchd./com.apple.webinspectord_sim.socket`. - -The socket path is discovered per-boot via: -```bash -xcrun simctl spawn launchctl print user/501 | grep RWI_LISTEN_SOCKET -``` - -`CdpBridge.connect({ udid, bundleId })` resolves the socket, calls `selectApp(null, 5, true)` with `additionalBundleIds: ['*']` to find the app's WebView page, then `selectPage(appKey, pageNum)`. Returns a `CdpSession` that wraps `executeAtom('execute_script', [body, []])` for repeated evaluation. - -### Per-Test Isolation - -Boot is amortized across runs (devices stay booted). Per-test, the fixture does: -1. `terminate` the app (if running) -2. `uninstall` it (wipes IndexedDB + Preferences sandbox) -3. `install` the freshly-built `.app` -4. `launch` with `MIDEN_E2E_TEST=true` -5. Connect CDP and construct `IosWalletPage` - -Total ~5s per wallet — much cheaper than the ~30s `simctl erase` would cost. - -### Onboarding Bypass - -Mirroring the wallet's official test hook (`Welcome.tsx`), iOS spec wallets skip seed-phrase backup/verify by setting `window.__TEST_SKIP_ONBOARDING = true` + `?__test_skip_onboarding=1` and tapping "Get started". This is what `IosWalletPage.createNewWallet` does. Specs that need a real seed phrase should use `importWallet()`. - -### Reading iOS Failure Reports - -Same artifact tree as Chrome, output to `test-results-ios/run-/tests//`. The `WalletSnapshot.platform` discriminator is `'ios'` (vs `'chrome'`); `serviceWorkerStatus` and `extensionId` are omitted. `runtimeInfo.kind === 'ios'` in the run manifest. All consumers (`failure-report.ts`, `diagnostic-hints.ts`) handle both platforms. +## Desktop (Tauri) -### Known Limitations +- `src-tauri/src/{main,dapp_browser,lib}.rs`, `scripts/dapp-injection.js` +- Clear state: `rm -rf ~/Library/WebKit/{com.miden.wallet,miden-wallet}` +- dApp flow: inject encodes base64 request → navigate `https://miden-wallet-request/{payload}` → Tauri `on_navigation` intercepts → event to main window → `DesktopDappHandler` confirms → response via same URL-intercept pattern. -- Headless mode is not available — Simulator.app must be running. The harness boots devices but does not control the GUI window. CI runners need a graphical session. -- The CDP bridge picks the first WebKit page on the inspector. The wallet uses one WebView, so this is fine; if a future build adds a dApp browser WebView, `CdpBridge.connect` needs a target-disambiguation parameter. -- `simctl` does NOT support keyboard input from outside the simulator. The iOS POM dispatches React-compatible `input`/`change` events directly via DOM rather than typing into native fields. For native iOS sheets / system dialogs this won't work — only WebView content is reachable. +## E2E -### Empirical Status (2026-04-14) +### Chrome blockchain harness +Two Chrome instances + `miden-client` CLI against live network. `E2E_NETWORK` controls both harness endpoints AND `MIDEN_NETWORK` baked into the bundle — use the `:` scripts to keep them matched. Auto-installs `miden-client-cli` from crates.io, version-matched to `@miden-sdk/miden-sdk`. Requires `cargo`. Specs: `wallet-lifecycle`, `mint-and-balance`, `send-{public,private}`, `multi-{claim,account}` in `playwright/e2e/tests/`. -**7/7 iOS specs pass on devnet in ~9 min wall clock.** Per-spec timing: +**Agentic mode** (`E2E_AGENTIC=true` or `yarn test:e2e:blockchain:agentic`): on failure, browsers stay open 10 min; `test-results/debug-session.json` has connection info; `report.json` has `failureCategory`, `diagnosticHints`, `stateAtFailure`, `browserErrors`. Hot-reload via `chrome.runtime.reload()` preserves IndexedDB/vault. -| Spec | Duration | -|---|---| -| mint-and-balance | 1.7m | -| multi-account | 1.2m | -| multi-claim | 1.4m | -| send-private | 1.8–2.4m | -| send-public | 1.7m | -| wallet-lifecycle (2 tests) | 42s total | +### iOS simulator harness +Mirror suite in `playwright/e2e/ios/` against iPhone 17 + iPhone 17 Pro. CDP via `appium-remote-debugger` (simulator-compatible, unlike `remotedebug-ios-webkit-adapter`) over `RWI_LISTEN_SOCKET`. Per-test: terminate/uninstall/install/launch (~5s vs 30s for `simctl erase`). 7/7 specs pass on devnet in ~9 min. -### Key product/test patterns the iOS port uncovered +iOS-specific product notes: +- Native navbar CTAs ("Claim All", "Continue") live in `UIWindow` outside WebView — CDP can't see them. `src/lib/dapp-browser/use-native-navbar-action.ts` exposes `globalThis.__TEST_TRIGGER_NAVBAR_ACTION__()` gated on `MIDEN_E2E_TEST=true && isMobile()`. Only wallet source change the iOS harness needed. +- No `SYNC_REQUEST` on mobile (SW-only); `useSyncTrigger` auto-syncs every 3s, so sleep suffices. +- No mobile reload trick — mobile `claimAllNotes` skips the `location.reload()` Chrome does (mobile has no SW holding the unlock, so reload drops decryption key). +- Don't read WASM client from CDP — deadlocks against `useSyncTrigger`'s 30–60s lock hold. -- **Native navbar actions need a JS test hook.** The wallet hoists primary-CTA buttons ("Claim All", "Continue" in Send, etc.) to the native iOS navbar overlay (`MidenNavbarOverlayWindow`) via `useNativeNavbarAction`. That overlay lives in a separate `UIWindow` outside the WebView — CDP can't see it and `xcrun simctl` can't do coordinate taps, so a small test hook in `src/lib/dapp-browser/use-native-navbar-action.ts` exposes `globalThis.__TEST_TRIGGER_NAVBAR_ACTION__()` (gated on `MIDEN_E2E_TEST=true && isMobile()`). `IosWalletPage.triggerNavbarAction` polls + calls it. This is the ONLY wallet source-code change the iOS harness needed. -- **Mobile auto-consume is identical to Chrome** (`Explore.tsx → autoConsumeMidenNotes`, gated to the well-known MIDEN faucet on both). The difference Chrome tests rely on is purely in `getBalance` reading `chrome.storage.local.miden_sync_data.notes` to count pending custom-faucet notes; mobile has no equivalent, so iOS specs call `walletX.claimAllNotes()` explicitly between mint and balance-verify. This is the honest user flow on mobile anyway. -- **Reload kills mobile session.** Chrome's `claimAllNotes` does a `location.reload()` first to get a fresh Dexie handle — safe on Chrome because the SW holds the vault unlock in a separate context. On mobile there's no SW; a reload drops the in-memory decryption key and bounces back to the password screen. iOS `claimAllNotes` skips the reload. -- **`useSyncTrigger` auto-syncs every 3s on mobile.** No need for iOS `triggerSync` to send `SYNC_REQUEST` — a sleep suffices. (The intercom `SYNC_REQUEST` handler doesn't exist on mobile anyway; it's a Chrome SW-only message.) -- **`execute_script` vs `execute_async_script`.** Appium's sync atom fails silently on multi-statement bodies if you wrap them in `return (...)` — pass the body verbatim (with an explicit `return`). Promise-returning code must use the async atom with an explicit callback call; add an outer JS timeout to avoid waiting forever when the script never invokes the callback. +### E2E test hooks +`MIDEN_E2E_TEST=true` exposes `window.__TEST_STORE__` (Zustand) and `window.__TEST_INTERCOM__`. Zero production impact. -### Why WASM-client access from CDP is off-limits - -Tempting to read `getConsumableNotes(address)` directly to mirror Chrome's "pending notes count toward balance" semantics. **Don't.** Any WASM client call must go through `withWasmClientLock`, but `useSyncTrigger` holds the lock for 30–60s on simulator while syncing; a concurrent read deadlocks, and skipping the lock violates the "recursive use of an object" invariant. Claim explicitly via the UI path instead. - -## Internationalization (i18n) - -**IMPORTANT:** All user-facing text in React components MUST be internationalized. Never use hardcoded strings for UI text - always use `t('key')` or the `` component. CI will block PRs with non-i18n'd strings (enforced by `yarn lint:i18n`). - -When adding new translatable strings, add them to `public/_locales/en/en.json`, NOT `messages.json`. - -- `en.json` - Flat format source file (`"key": "value"`). The translation script reads from this file. -- `messages.json` - Chrome extension format (`"key": { "message": "value", "englishSource": "value" }`). Auto-generated. - -### Adding new i18n strings - -1. Add the key to `public/_locales/en/en.json` in flat format: - ```json - "myNewKey": "My new translatable string" - ``` - -2. Use in React components with `useTranslation` hook: - ```typescript - import { useTranslation } from 'react-i18next'; - - const { t } = useTranslation(); - return {t('myNewKey')}; - ``` - -3. CI will auto-translate to other languages via `yarn createTranslationFile` - -### Placeholders in translations - -Use `$placeholder$` format for dynamic values: -```json -"greeting": "Hello $name$, you have $count$ messages" -``` - -## Transaction Processing - -### Background Transaction Processing - -For operations that should happen silently (like auto-consume), use `startBackgroundTransactionProcessing`: - -```typescript -import { startBackgroundTransactionProcessing } from 'lib/miden/activity'; -import { useMidenContext } from 'lib/miden/front'; - -const { signTransaction } = useMidenContext(); +## Testing -// Queue transactions first -await initiateConsumeTransaction(accountPublicKey, note, isDelegatedProvingEnabled); +Jest + RTL. Mock `lib/intercom` for frontend tests; wrap with `WalletStoreProvider` + `MidenContextProvider`. -// Then process silently in background (no modal/tab) -startBackgroundTransactionProcessing(signTransaction); -``` +Gotchas: +- `jest.mock()` path must match the import path used in source (e.g., `'lib/miden/back/vault'`, not `'./vault'`). +- `window.location.reload` can't be mocked in jsdom — wrap calls in try/catch. +- `afterEach(() => testRoot.unmount())` to prevent React cross-test pollution. -This is preferred over `openLoadingFullPage()` for automatic operations because: -- Doesn't interrupt the user with a modal (mobile) or new tab (desktop) -- Polls every 5 seconds for up to 5 minutes -- Works on both mobile and desktop +## Code style -### Transaction States +Prettier: 120 cols, single quotes, semicolons, trailing commas. Break long `console.log`s across lines. `yarn format` to fix. -Transactions flow through these states in `ITransactionStatus`: -1. `Queued` (0) - Initial state when transaction is created -2. `GeneratingTransaction` (1) - Being processed -3. `Completed` (2) - Successfully finished -4. `Failed` (3) - Error occurred +No `any`, no `as`. Use concrete types. -## Important Notes +## Conventions -- **Never push without explicit request.** Creating commits is fine, but never run `git push` unless the user explicitly asks. -- **Keep commit messages short.** Use single-line messages (e.g., "fix: add missing i18n keys"). -- Uses yarn, not npm (yarn.lock is the lockfile) -- Browser extension APIs via `webextension-polyfill` -- Miden SDK is a WASM module (`@miden-sdk/miden-sdk`) -- Sensitive data (vault, keys) stays in backend only -- Frontend receives sanitized state via `toFront()` in backend store +- Commit messages: single-line, short. Never sign commits (no `Co-Authored-By`). +- Never `git push` without explicit request. +- Stay within requested scope — don't modify files beyond the task. +- Update `CHANGELOG.md` one-liner per PR/task (not per fix). +- When adding a new intercom message type, also update `src/lib/intercom/mobile-adapter.ts`. +- Optimistic updates: snapshot prev, apply, rollback on catch. +- Background auto-ops: use `startBackgroundTransactionProcessing` (polls 5s × 5min, no modal) instead of `openLoadingFullPage`. +- Transaction states (`ITransactionStatus`): Queued(0) → GeneratingTransaction(1) → Completed(2) / Failed(3). +- Frontend receives sanitized state via `toFront()`; sensitive data (vault, keys) stays backend-only. diff --git a/package.json b/package.json index ea8a1b66a..d4dfb52a6 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ }, "scripts": { "start": "yarn watch:src", - "dev": "rimraf ./dist && concurrently yarn:watch:*", + "dev": "rimraf ./dist && cross-env TARGET_BROWSER=chrome MODE_ENV=development NODE_ENV=development yarn build:bg && concurrently -n bg,ext -c blue,green \"cross-env TARGET_BROWSER=chrome MODE_ENV=development NODE_ENV=development vite build --config vite.background.config.ts --watch\" \"cross-env TARGET_BROWSER=chrome MANIFEST_VERSION=3 MODE_ENV=development NODE_ENV=development vite build --config vite.extension.config.ts --watch\"", "dev-clean": "rimraf node_modules && yarn cache clean && yarn install && yarn dev", - "watch:src": "echo 'Use vite dev instead'", + "watch:src": "vite run --config vite.extension.config.ts", "watch:dist": "mv3-hot-reload", "build": "yarn build:extension && yarn build:mobile", "build:extension": "cross-env TARGET_BROWSER=chrome yarn build:bg && cross-env TARGET_BROWSER=chrome MANIFEST_VERSION=3 yarn build:ext", @@ -80,7 +80,7 @@ "desktop:build": "yarn build:desktop && yarn tauri build", "desktop:reset": "rm -rf ~/Library/WebKit/com.miden.wallet ~/Library/Caches/com.miden.wallet ~/Library/Application\\ Support/com.miden.wallet", "build:devnet": "rimraf ./dist && yarn clear:webpack-cache && cross-env MIDEN_NETWORK=devnet DISABLE_TS_CHECKER=true NODE_ENV=development MODE_ENV=production MANIFEST_VERSION=3 webpack", - "dev:devnet": "cross-env MIDEN_NETWORK=devnet DISABLE_TS_CHECKER=true NODE_ENV=development MODE_ENV=development MANIFEST_VERSION=3 webpack --watch --progress", + "dev:devnet": "cross-env MIDEN_NETWORK=devnet yarn dev", "build:desktop:devnet": "rimraf ./dist/desktop && cross-env MIDEN_NETWORK=devnet DISABLE_TS_CHECKER=true NODE_ENV=development MODE_ENV=production webpack --config webpack.desktop.config.js", "tauri": "tauri" }, @@ -104,13 +104,13 @@ "@dicebear/avatars-jdenticon-sprites": "4.2.5", "@floating-ui/react": "^0.27.16", "@hookform/resolvers": "^3.9.0", - "@miden-sdk/miden-sdk": "0.14.1", + "@miden-sdk/miden-sdk": "^0.14.3", "@miden-sdk/react": "0.14.1", "@miden/dapp-browser": "link:./packages/dapp-browser", "@newhighsco/storybook-addon-svgr": "^2.0.7", "@noble/hashes": "^1.4.0", - "@openzeppelin/guardian-client": "^0.13.4", - "@openzeppelin/miden-multisig-client": "^0.13.4", + "@openzeppelin/guardian-client": "^0.14.3", + "@openzeppelin/miden-multisig-client": "^0.14.3", "@peculiar/webcrypto": "1.1.6", "@radix-ui/react-slot": "^1.2.3", "@segment/analytics-node": "^2.3.0", diff --git a/src/lib/intercom/mobile-adapter.ts b/src/lib/intercom/mobile-adapter.ts index 5b8bf8891..4c786bcea 100644 --- a/src/lib/intercom/mobile-adapter.ts +++ b/src/lib/intercom/mobile-adapter.ts @@ -59,23 +59,24 @@ export class MobileIntercomAdapter { */ private async processRequest(req: WalletRequest): Promise { switch (req?.type) { - case WalletMessageType.GetStateRequest: + case WalletMessageType.GetStateRequest: { const state = await Actions.getFrontState(); return { type: WalletMessageType.GetStateResponse, state }; + } case WalletMessageType.NewWalletRequest: - await Actions.registerNewWallet((req as any).password, (req as any).mnemonic, (req as any).ownMnemonic); + await Actions.registerNewWallet(req.walletType, req.password, req.mnemonic, req.ownMnemonic); return { type: WalletMessageType.NewWalletResponse }; case WalletMessageType.ImportFromClientRequest: - await Actions.registerImportedWallet((req as any).password, (req as any).mnemonic); + await Actions.registerImportedWallet(req.password, req.mnemonic); return { type: WalletMessageType.ImportFromClientResponse }; case WalletMessageType.UnlockRequest: - await Actions.unlock((req as any).password); + await Actions.unlock(req.password); return { type: WalletMessageType.UnlockResponse }; case WalletMessageType.LockRequest: @@ -83,76 +84,97 @@ export class MobileIntercomAdapter { return { type: WalletMessageType.LockResponse }; case WalletMessageType.CreateAccountRequest: - await Actions.createHDAccount((req as any).walletType, (req as any).name); + await Actions.createHDAccount(req.walletType, req.name); return { type: WalletMessageType.CreateAccountResponse }; case WalletMessageType.UpdateCurrentAccountRequest: - await Actions.updateCurrentAccount((req as any).accountPublicKey); + await Actions.updateCurrentAccount(req.accountPublicKey); return { type: WalletMessageType.UpdateCurrentAccountResponse }; - case WalletMessageType.RevealMnemonicRequest: - const mnemonic = await Actions.revealMnemonic((req as any).password); + case WalletMessageType.RevealMnemonicRequest: { + const mnemonic = await Actions.revealMnemonic(req.password); return { type: WalletMessageType.RevealMnemonicResponse, mnemonic }; + } case WalletMessageType.RemoveAccountRequest: - await Actions.removeAccount((req as any).accountPublicKey, (req as any).password); + await Actions.removeAccount(req.accountPublicKey, req.password); return { type: WalletMessageType.RemoveAccountResponse }; case WalletMessageType.EditAccountRequest: - await Actions.editAccount((req as any).accountPublicKey, (req as any).name); + await Actions.editAccount(req.accountPublicKey, req.name); return { type: WalletMessageType.EditAccountResponse }; case WalletMessageType.ImportAccountRequest: - await Actions.importAccount((req as any).privateKey, (req as any).encPassword); + await Actions.importAccount(req.privateKey, req.encPassword); return { type: WalletMessageType.ImportAccountResponse }; case WalletMessageType.UpdateSettingsRequest: - await Actions.updateSettings((req as any).settings); + await Actions.updateSettings(req.settings); return { type: WalletMessageType.UpdateSettingsResponse }; - case WalletMessageType.SignTransactionRequest: - const signature = await Actions.signTransaction((req as any).publicKey, (req as any).signingInputs); + case WalletMessageType.SignTransactionRequest: { + const signature = await Actions.signTransaction(req.publicKey, req.signingInputs); return { type: WalletMessageType.SignTransactionResponse, signature }; + } + + case WalletMessageType.SignWordRequest: { + const wordSignature = await Actions.signWord(req.publicKey, req.wordHex); + return { + type: WalletMessageType.SignWordResponse, + signature: wordSignature + }; + } - case WalletMessageType.GetAuthSecretKeyRequest: - const key = await Actions.getAuthSecretKey((req as any).key); + case WalletMessageType.GetPublicKeyForCommitmentRequest: { + const publicKey = await Actions.getPublicKeyForCommitment(req.commitment); + return { + type: WalletMessageType.GetPublicKeyForCommitmentResponse, + publicKey + }; + } + + case WalletMessageType.GetAuthSecretKeyRequest: { + const key = await Actions.getAuthSecretKey(req.key); return { type: WalletMessageType.GetAuthSecretKeyResponse, key }; + } - case MidenMessageType.DAppGetAllSessionsRequest: + case MidenMessageType.DAppGetAllSessionsRequest: { const allSessions = await Actions.getAllDAppSessions(); return { type: MidenMessageType.DAppGetAllSessionsResponse, sessions: allSessions }; + } - case MidenMessageType.DAppRemoveSessionRequest: - const sessions = await Actions.removeDAppSession((req as any).origin); + case MidenMessageType.DAppRemoveSessionRequest: { + const sessions = await Actions.removeDAppSession(req.origin); return { type: MidenMessageType.DAppRemoveSessionResponse, sessions }; + } - case MidenMessageType.PageRequest: + case MidenMessageType.PageRequest: { const dAppEnabled = await Actions.isDAppEnabled(); if (dAppEnabled) { - if ((req as any).payload === 'PING') { + if (req.payload === 'PING') { return { type: MidenMessageType.PageResponse, payload: 'PONG' @@ -160,11 +182,7 @@ export class MobileIntercomAdapter { } // PR-4 chunk 8: thread the multi-instance session id through if // present so confirmation prompts route to the right session. - const resPayload = await Actions.processDApp( - (req as any).origin, - (req as any).payload, - (req as any).sessionId - ); + const resPayload = await Actions.processDApp(req.origin, req.payload, req.sessionId); return { type: MidenMessageType.PageResponse, /* c8 ignore next -- dApp response nullish fallback, mobile-only */ @@ -172,6 +190,7 @@ export class MobileIntercomAdapter { }; } break; + } default: console.warn('MobileIntercomAdapter: Unknown request type', req?.type); diff --git a/src/lib/miden-chain/constants.ts b/src/lib/miden-chain/constants.ts index dc6dc2f78..0a734c4a3 100644 --- a/src/lib/miden-chain/constants.ts +++ b/src/lib/miden-chain/constants.ts @@ -72,7 +72,7 @@ export const TOKEN_MAPPING = { [MidenTokens.Miden]: { faucetId: 'mtst1aqmat9m63ctdsgz6xcyzpuprpulwk9vg_qruqqypuyph' } }; -export const DEFAULT_PSM_ENDPOINT = 'https://psm-stg.openzeppelin.com'; +export const DEFAULT_PSM_ENDPOINT = 'https://guardian-stg.openzeppelin.com'; /** * Returns the SDK NetworkId for the current DEFAULT_NETWORK. diff --git a/src/lib/miden/activity/transactions.ts b/src/lib/miden/activity/transactions.ts index 1912649c0..c7fe1da0a 100644 --- a/src/lib/miden/activity/transactions.ts +++ b/src/lib/miden/activity/transactions.ts @@ -1,13 +1,4 @@ -import { - Address, - InputNoteState, - Note, - NoteFilter, - NoteFilterTypes, - NoteId, - TransactionRequest, - TransactionResult -} from '@miden-sdk/miden-sdk'; +import { InputNoteState, Note, TransactionProver, TransactionResult } from '@miden-sdk/miden-sdk'; import { type Proposal } from '@openzeppelin/miden-multisig-client'; import { liveQuery } from 'dexie'; @@ -28,7 +19,7 @@ import { TransactionOutput } from '../db/types'; import { toNoteTypeString } from '../helpers'; -import { accountIdStringToSdk, getBech32AddressFromAccountId } from '../sdk/helpers'; +import { getBech32AddressFromAccountId } from '../sdk/helpers'; import { getMidenClient, withWasmClientLock } from '../sdk/miden-client'; import { MidenClientCreateOptions } from '../sdk/miden-client-interface'; import { ConsumableNote, NoteTypeEnum, NoteType as NoteTypeString } from '../types'; @@ -694,18 +685,21 @@ const generatePsmTransaction = async ( proposalResult = await multisigService.createConsumeNotesProposal([consumeTx.noteId]); break; } - case 'execute': + // case 'execute': + // default: { + // // For custom transactions, get TransactionSummary and create a custom proposal + // const summaryBytes = await withWasmClientLock(async () => { + // const midenClient = await getMidenClient(); + // const txRequest = TransactionRequest.deserialize(transaction.requestBytes!); + // return ( + // await midenClient.client.transactions.preview(accountIdStringToSdk(transaction.accountId), txRequest) + // ).serialize(); + // }); + // proposalResult = await multisigService.createCustomProposal(summaryBytes); + // break; + // } default: { - // For custom transactions, get TransactionSummary and create a custom proposal - const summaryBytes = await withWasmClientLock(async () => { - const midenClient = await getMidenClient(); - const txRequest = TransactionRequest.deserialize(transaction.requestBytes!); - return ( - await midenClient.webClient.executeForSummary(accountIdStringToSdk(transaction.accountId), txRequest) - ).serialize(); - }); - proposalResult = await multisigService.createCustomProposal(summaryBytes); - break; + throw new Error(`Unsupported transaction type for PSM account: ${transaction.type}`); } } @@ -720,17 +714,12 @@ const generatePsmTransaction = async ( } }; - // Wrap WASM client operations in a lock to prevent concurrent access - const transactionResultBytes = await withWasmClientLock(async () => { + const transactionResult = await withWasmClientLock(async () => { const midenClient = await getMidenClient(options); - return await midenClient.newTransaction(transaction.accountId, tr.serialize()); - }); - - const transactionResult = TransactionResult.deserialize(transactionResultBytes); - - await withWasmClientLock(async () => { - const midenClient = await getMidenClient(); - await midenClient.submitTransaction(transactionResultBytes, transaction.delegateTransaction); + const { result } = await midenClient.client.transactions.submit(transaction.accountId, tr, { + prover: !transaction.delegateTransaction ? TransactionProver.newLocalProver() : undefined + }); + return result; }); switch (transaction.type) { @@ -740,10 +729,10 @@ const generatePsmTransaction = async ( case 'consume': await completeConsumeTransaction(transaction.id, transactionResult); break; - case 'execute': - default: - await completeCustomTransaction(transaction, transactionResult); - break; + // case 'execute': + // default: + // await completeCustomTransaction(transaction, transactionResult); + // break; } await multisigService.sync(); diff --git a/src/lib/miden/psm/account.ts b/src/lib/miden/psm/account.ts index 51ee9e8d7..022180bda 100644 --- a/src/lib/miden/psm/account.ts +++ b/src/lib/miden/psm/account.ts @@ -1,4 +1,4 @@ -import { Account, AuthSecretKey, WebClient } from '@miden-sdk/miden-sdk'; +import { Account, AuthSecretKey, MidenClient } from '@miden-sdk/miden-sdk'; import { FalconSigner, MultisigClient } from '@openzeppelin/miden-multisig-client'; import { DEFAULT_PSM_ENDPOINT } from 'lib/miden-chain/constants'; @@ -26,6 +26,10 @@ export async function getSignerDetailsFromAccount( throw new Error('No signer public keys found in account storage'); } + if (!mapEntries[0]) { + throw new Error('No signer commitments found in account storage'); + } + const commitment = mapEntries[0].value.slice(2); if (!commitment) { throw new Error('Commitment not found in account storage'); @@ -45,7 +49,11 @@ export async function getSignerDetailsFromAccount( * @param seed - Optional seed for key derivation (random if not provided) * @returns The created Account */ -export async function createPsmAccount(webClient: WebClient, seed?: Uint8Array): Promise { +export async function createPsmAccount( + webClient: MidenClient, + seed?: Uint8Array, + skipRegistration: boolean = false +): Promise { if (!seed) { seed = crypto.getRandomValues(new Uint8Array(32)); } @@ -73,17 +81,21 @@ export async function createPsmAccount(webClient: WebClient, seed?: Uint8Array): }, new FalconSigner(sk) ); - await multisig.registerOnGuardian(); + + if (!skipRegistration) { + await multisig.registerOnGuardian(); + } // Sync state with the node - await webClient.syncState(); + await webClient.sync(); // Store the secret key in WebStore for signing - await webClient.addAccountSecretKeyToWebStore(multisig.account.id(), sk); + await webClient.keystore.insert(multisig.account.id(), sk); console.log('PSM account created:', multisig.account.id().toString()); return multisig.account; } catch (e) { + console.log(e); console.error('Error creating PSM account:', e); throw new Error('Failed to create PSM account'); } diff --git a/src/lib/miden/psm/digest.ts b/src/lib/miden/psm/digest.ts index 9a8fab4e9..c6f79540b 100644 --- a/src/lib/miden/psm/digest.ts +++ b/src/lib/miden/psm/digest.ts @@ -51,7 +51,7 @@ export class AuthDigest { for (let i = 0; i < bytes.length; i += 8) { let packed = 0n; for (let j = 0; j < 8 && i + j < bytes.length; j += 1) { - packed |= BigInt(bytes[i + j]) << (8n * BigInt(j)); + packed |= BigInt(bytes[i + j]!) << (8n * BigInt(j)); } payloadElements.push(new Felt(packed)); } diff --git a/src/lib/miden/psm/index.ts b/src/lib/miden/psm/index.ts index 3f2a7161c..b088cced7 100644 --- a/src/lib/miden/psm/index.ts +++ b/src/lib/miden/psm/index.ts @@ -1,8 +1,7 @@ -import { Account, TransactionRequest, WebClient } from '@miden-sdk/miden-sdk'; +import { Account, MidenClient, TransactionRequest, WebClient } from '@miden-sdk/miden-sdk'; import { Multisig, MultisigClient, - MultisigConfig, GuardianHttpClient, type ProposalMetadata, type TransactionProposal, @@ -13,8 +12,9 @@ import { DEFAULT_PSM_ENDPOINT } from 'lib/miden-chain/constants'; import { PSM_URL_STORAGE_KEY } from 'lib/settings/constants'; import { u8ToB64 } from 'lib/shared/helpers'; -import { fetchFromStorage } from '../front'; +import { fetchFromStorage, putToStorage } from '../front'; import { accountIdStringToSdk } from '../sdk/helpers'; +import { getMidenClient, withWasmClientLock } from '../sdk/miden-client'; import { MidenClientInterface } from '../sdk/miden-client-interface'; import { WalletSigner, type SignWordFunction } from './signer'; @@ -48,7 +48,7 @@ export class MultisigService { const signer = new WalletSigner(publicKey, signerCommitment, signWordFn); const guardianEndpoint = (await fetchFromStorage(PSM_URL_STORAGE_KEY)) || DEFAULT_PSM_ENDPOINT; - const webClient = (await MidenClientInterface.create({})).webClient; + const webClient = (await MidenClientInterface.create({})).client; const client = new MultisigClient(webClient, { guardianEndpoint }); const multisig = await client.load(account.id().toString(), signer); @@ -65,7 +65,7 @@ export class MultisigService { signerCommitment: string, signWordFn: SignWordFunction, accountId: string, - webClient: WebClient + webClient: MidenClient ) { const psmEndpoint = (await fetchFromStorage(PSM_URL_STORAGE_KEY)) || DEFAULT_PSM_ENDPOINT; const psm = new GuardianHttpClient(psmEndpoint); @@ -80,7 +80,7 @@ export class MultisigService { accountBytes[i] = binaryString.charCodeAt(i); } const account = Account.deserialize(accountBytes); - await webClient.newAccount(account, true); + await webClient.accounts.insert({ account, overwrite: true }); } catch (error) { console.log('Error fetching account state from PSM:', error); } @@ -150,9 +150,26 @@ export class MultisigService { async sync(): Promise { try { - await this.multisig.syncState(); + const { accountId, commitment } = await this.multisig.syncState(); + console.log('Successfully synced multisig state for account', accountId); + console.log('Current commitment:', commitment); + const account = await withWasmClientLock(async () => { + const client = await getMidenClient(); + if (!client) throw new Error('WASM client not available'); + const acc = await client.getAccount(accountId); + console.log( + acc + ?.vault() + .fungibleAssets() + .map((a: any) => a.amount().toString()) + ); + if (!acc) throw new Error('Account not found in WASM client after sync'); + return acc; + }); + console.log('Account commitment from WASM client:', account.to_commitment().toHex()); this.syncRetryCount = 0; // Reset retry count on successful sync } catch (error) { + console.log('[PSM] sync error ', error); const isNonceTooLow = error instanceof Error && error.message.includes('nonce') && error.message.includes('too low'); @@ -175,6 +192,11 @@ export class MultisigService { async getConsumableNotes() { return this.multisig.getConsumableNotes(); } + + // async switchGuardian(newGuardianEndpoint: string) { + // await putToStorage(PSM_URL_STORAGE_KEY, newGuardianEndpoint); + // await this.multisig.createSwitchGuardianProposal() + // } } // Re-export types that may be needed by consumers diff --git a/src/lib/miden/sdk/helpers.ts b/src/lib/miden/sdk/helpers.ts index f781374e3..508b0d169 100644 --- a/src/lib/miden/sdk/helpers.ts +++ b/src/lib/miden/sdk/helpers.ts @@ -6,3 +6,7 @@ export function getBech32AddressFromAccountId(accountId: AccountId): string { const accountAddress = Address.fromAccountId(accountId, 'BasicWallet'); return accountAddress.toBech32(getNetworkId()); } + +export function accountIdStringToSdk(accountIdStr: string): AccountId { + return Address.fromBech32(accountIdStr).accountId(); +} diff --git a/src/lib/miden/sdk/miden-client-interface.ts b/src/lib/miden/sdk/miden-client-interface.ts index 7860f95b6..9765f8575 100644 --- a/src/lib/miden/sdk/miden-client-interface.ts +++ b/src/lib/miden/sdk/miden-client-interface.ts @@ -132,18 +132,24 @@ export class MidenClientInterface { getPublicKeyForCommitment: (commitment: string) => Promise ): Promise { if (walletType === WalletType.Psm) { - const account = await createPsmAccount(this.client, seed); - console.log('[MidenClientInterface] Imported PSM account from seed with ID:', account.id().toString()); - const accountId = account.id().toString(); - const { commitment, publicKey } = await getSignerDetailsFromAccount(account, getPublicKeyForCommitment); - await MultisigService.importAccountFromPsm( - `0x${publicKey}`, - `0x${commitment}`, - signWordFn, - accountId, - this.client - ); - return getBech32AddressFromAccountId(account.id()); + console.log('Importing PSM account from seed'); + try { + const account = await createPsmAccount(this.client, seed, true); + console.log('[MidenClientInterface] Imported PSM account from seed with ID:', account.id().toString()); + const accountId = account.id().toString(); + const { commitment, publicKey } = await getSignerDetailsFromAccount(account, getPublicKeyForCommitment); + await MultisigService.importAccountFromPsm( + `0x${publicKey}`, + `0x${commitment}`, + signWordFn, + accountId, + this.client + ); + return getBech32AddressFromAccountId(account.id()); + } catch (error) { + console.log(error); + throw new Error('Failed to import PSM account from seed'); + } } return await this.importPublicMidenWalletFromSeed(seed); diff --git a/src/lib/miden/sdk/miden-client.ts b/src/lib/miden/sdk/miden-client.ts index a56f1d89c..36eb574c2 100644 --- a/src/lib/miden/sdk/miden-client.ts +++ b/src/lib/miden/sdk/miden-client.ts @@ -204,7 +204,13 @@ const midenClientSingleton = new MidenClientSingleton(); */ export async function getMidenClient(options?: MidenClientCreateOptions): Promise { if (options) { - return await midenClientSingleton.getInstanceWithOptions(options); + console.time('Creating MidenClient with options'); + const client = await midenClientSingleton.getInstanceWithOptions(options); + console.timeEnd('Creating MidenClient with options'); + return client; } - return await midenClientSingleton.getInstance(); + console.time('Getting MidenClient instance'); + const client = await midenClientSingleton.getInstance(); + console.timeEnd('Getting MidenClient instance'); + return client; } diff --git a/vite.extension.config.ts b/vite.extension.config.ts index 7f7d74f6a..11dc05fa6 100644 --- a/vite.extension.config.ts +++ b/vite.extension.config.ts @@ -6,14 +6,14 @@ * * Replaces webpack.config.js entirely for the extension build. */ -import { resolve, join, sep } from 'path'; +import tailwindcss from '@tailwindcss/vite'; +import react from '@vitejs/plugin-react-swc'; import { readFileSync, existsSync, mkdirSync, writeFileSync, cpSync, readdirSync, statSync } from 'fs'; +import { resolve, join, sep } from 'path'; +import { defineConfig, type Plugin } from 'vite'; import { nodePolyfills } from 'vite-plugin-node-polyfills'; // vite-plugin-svgr doesn't work with Vite 8's Rolldown -- use custom plugin import wasm from 'vite-plugin-wasm'; -import tailwindcss from '@tailwindcss/vite'; -import react from '@vitejs/plugin-react-swc'; -import { defineConfig, type Plugin } from 'vite'; const pkg = require('./package.json'); const TARGET_BROWSER = process.env.TARGET_BROWSER ?? 'chrome'; @@ -115,7 +115,7 @@ function copyPublicAssets(outDir: string): Plugin { cpSync(sdkWasm, join(nestedDir, 'miden_client_web.wasm')); cpSync(sdkWasm, join(assetsDir, 'miden_client_web.wasm')); } - }, + } }; } @@ -143,7 +143,7 @@ function swPatches(): Plugin { chunk.code = chunk.code.replace(/^await /gm, '/* tla-stripped */ '); } } - }, + } }; } @@ -158,7 +158,7 @@ function svgStubForBackground(): Plugin { if (id.endsWith('.svg') && this.getModuleInfo?.(id)?.isEntry === false) { return 'export const ReactComponent = () => null; export default "";'; } - }, + } }; } @@ -173,7 +173,7 @@ const sharedAlias = { components: resolve(__dirname, 'src/components'), screens: resolve(__dirname, 'src/screens'), utils: resolve(__dirname, 'src/utils'), - stories: resolve(__dirname, 'src/stories'), + stories: resolve(__dirname, 'src/stories') }; const sharedDefine = { @@ -183,7 +183,7 @@ const sharedDefine = { 'process.env.MIDEN_NETWORK': JSON.stringify(process.env.MIDEN_NETWORK ?? ''), 'process.env.MIDEN_E2E_TEST': JSON.stringify(process.env.MIDEN_E2E_TEST ?? 'false'), 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'development'), - 'process.env.MODE_ENV': JSON.stringify(process.env.MODE_ENV ?? 'development'), + 'process.env.MODE_ENV': JSON.stringify(process.env.MODE_ENV ?? 'development') }; export default defineConfig({ @@ -208,20 +208,24 @@ export default defineConfig({ const { readFileSync } = await import('fs'); const svgContent = readFileSync(filePath, 'utf8'); const { transform } = await import('@svgr/core'); - const jsxCode = await transform(svgContent, { - plugins: ['@svgr/plugin-jsx'], - exportType: 'named', - namedExport: 'ReactComponent', - jsxRuntime: 'automatic', - prettier: false, - svgo: false, - titleProp: true, - ref: true, - }, { filePath }); + const jsxCode = await transform( + svgContent, + { + plugins: ['@svgr/plugin-jsx'], + exportType: 'named', + namedExport: 'ReactComponent', + jsxRuntime: 'automatic', + prettier: false, + svgo: false, + titleProp: true, + ref: true + }, + { filePath } + ); const code = jsxCode + '\nexport default "";'; // Return as JSX so Vite/Rolldown transforms it to JS return { code, moduleType: 'jsx' }; - }, + } } satisfies Plugin, wasm(), // Polyfill Node built-ins used by crypto/stream libraries (readable-stream uses util.debuglog) @@ -229,20 +233,19 @@ export default defineConfig({ // to avoid injecting fake document/window that break React's CSS animation detection. nodePolyfills({ include: ['util', 'stream', 'assert', 'buffer', 'process'], - globals: { Buffer: false, process: false }, + globals: { Buffer: false, process: false } }), // Extension HTML fixes { name: 'extension-html-fixes', enforce: 'post', transformIndexHtml(html) { - return html - .replace(/ crossorigin/g, '') - // Inject process global via external script (inline scripts blocked by CSP) - .replace( - '\n \n