/**
 * Vault Resource
 *
 * Handles shield, unshield, send, and balance operations.
 * Delegates crypto/transaction ops to KeyVault and queries to IndexerClient.
 */
import { TransactionStatus, TransactionType, TransactionDirection, ShieldStep, UnshieldStep, isNativeEth, generatePendingTxHash, NATIVE_ETH_ADDRESS } from '../types';
import { PrivacyBoostError, ErrorCodes } from '../errors';
import { createChainClient } from '../core/chains';
import { silentLogger } from '../utils/logger';
// ERC20 balanceOf ABI
const ERC20_BALANCE_OF_ABI = [
    {
        type: 'function',
        name: 'balanceOf',
        stateMutability: 'view',
        inputs: [{ name: 'account', type: 'address' }],
        outputs: [{ name: '', type: 'uint256' }],
    },
];
// ERC20 metadata ABI (for symbol, decimals, name)
const ERC20_METADATA_ABI = [
    {
        type: 'function',
        name: 'symbol',
        stateMutability: 'view',
        inputs: [],
        outputs: [{ name: '', type: 'string' }],
    },
    {
        type: 'function',
        name: 'decimals',
        stateMutability: 'view',
        inputs: [],
        outputs: [{ name: '', type: 'uint8' }],
    },
    {
        type: 'function',
        name: 'name',
        stateMutability: 'view',
        inputs: [],
        outputs: [{ name: '', type: 'string' }],
    },
];
const VALID_HEX_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
// Default token metadata for common tokens
const DEFAULT_TOKEN_METADATA = {
    '0x0000000000000000000000000000000000000000': { symbol: 'ETH', decimals: 18 },
    '0x4200000000000000000000000000000000000006': { symbol: 'WETH', decimals: 18 }, // OP WETH
};
/**
 * Vault Resource for privacy operations
 */
const TAG = 'Vault';
export class VaultResource {
    keyVault;
    indexer;
    config;
    auth;
    publicClient = null;
    _stores;
    log;
    constructor(keyVault, indexer, config, auth, stores, logger) {
        this.keyVault = keyVault;
        this.indexer = indexer;
        this.config = config;
        this.auth = auth;
        this._stores = stores;
        this.log = logger ?? silentLogger;
    }
    get balanceStore() { return this._stores.balance; }
    get transactionStore() { return this._stores.transaction; }
    get tokenStore() { return this._stores.token; }
    get walletStoreRef() { return this._stores.wallet; }
    async getMpk() {
        const storeMpk = this.walletStoreRef.getState().mpk;
        if (storeMpk)
            return storeMpk;
        const keys = await this.keyVault.getPublicKeys();
        return keys.mpk;
    }
    // ============================================
    // Shield Operations
    // ============================================
    /**
     * Shield tokens into the privacy vault.
     *
     * Wraps ETH→WETH if the token is native ETH, approves the spend,
     * and creates a shielded note. Progress is reported via `onProgress`.
     *
     * @param params - Shield parameters (tokenAddress, amount, optional recipientMpk, onProgress)
     * @returns Shield result with txHash, commitment, fee, and requestId
     * @throws {PrivacyBoostError} `INVALID_CONFIG` if shielding native ETH without wethContractAddress configured
     *
     * @example
     * ```typescript
     * const result = await sdk.vault.shield({
     *   tokenAddress: '0x...', // ERC-20 token or NATIVE_ETH_ADDRESS for ETH
     *   amount: 1000000n,
     *   onProgress: (p) => console.log(p.message),
     * });
     * console.log('Shielded:', result.txHash);
     * ```
     */
    async shield(params) {
        const { tokenAddress, amount, onProgress, recipientMpk } = params;
        // Check if this is native ETH - Rust SDK will handle wrapping
        const isNative = isNativeEth(tokenAddress);
        if (isNative && !this.config.wethContractAddress) {
            throw new PrivacyBoostError(ErrorCodes.INVALID_CONFIG, 'wethContractAddress must be configured to shield native ETH');
        }
        // Create pending transaction
        // For native ETH, store WETH address since that's the actual shielded token
        const pendingHash = generatePendingTxHash(TransactionType.shield);
        const txStore = this.transactionStore.getState();
        const resolvedTokenAddress = isNative ? this.config.wethContractAddress : tokenAddress;
        txStore.addTransaction({
            txHash: pendingHash,
            type: TransactionType.shield,
            direction: TransactionDirection.incoming,
            status: TransactionStatus.pending,
            tokenAddress: resolvedTokenAddress,
            amount,
            receivers: [],
            createdAt: Date.now(),
            updatedAt: Date.now(),
        });
        try {
            // Forward raw Rust progress events to the user's `onProgress` callback,
            // mapping each to a ShieldStep the UI already knows about. The Rust SDK
            // emits these at real on-chain boundaries (tx submitted, tx confirmed,
            // etc.) so the UI is now honest about what's happening.
            const forwardShieldProgress = (event) => {
                if (!onProgress)
                    return;
                switch (event.step) {
                    case 'preparing':
                    case 'buildingProof':
                        // No dedicated UI step; skip to avoid noise.
                        return;
                    case 'wrapping':
                        onProgress({ step: ShieldStep.wrapping, message: 'Wrapping ETH to WETH...' });
                        return;
                    case 'wrapConfirmed':
                        onProgress({
                            step: ShieldStep.wrapping,
                            message: 'Wrap confirmed.',
                            txHash: event.txHash,
                        });
                        return;
                    case 'approveSkipped':
                        // Allowance was already sufficient — don't emit anything so the UI
                        // skips past the Approve step naturally (next event will be
                        // `shielding`). A user-facing "Approve skipped" message would be
                        // noise, and marking Approve as complete would imply a tx happened.
                        return;
                    case 'approving':
                        onProgress({ step: ShieldStep.approving, message: 'Approving token spend...' });
                        return;
                    case 'approveConfirmed':
                        onProgress({
                            step: ShieldStep.approving,
                            message: 'Approve confirmed.',
                            txHash: event.txHash,
                        });
                        return;
                    case 'shielding':
                        onProgress({ step: ShieldStep.shielding, message: 'Shielding tokens...' });
                        return;
                    case 'shieldSubmitted':
                        onProgress({
                            step: ShieldStep.shielding,
                            message: 'Shield transaction submitted.',
                            txHash: event.txHash,
                        });
                        return;
                    case 'shieldConfirmed':
                        onProgress({
                            step: ShieldStep.compliance,
                            message: 'Verifying shield...',
                            txHash: event.txHash,
                        });
                        return;
                }
            };
            const result = await this.keyVault.shield(tokenAddress, amount.toString(), forwardShieldProgress);
            const { txHash, commitment, requestId, wrapTxHash } = result;
            txStore.updateTransaction(pendingHash, {
                status: TransactionStatus.pending,
                requestId,
                updatedAt: Date.now(),
            }, txHash);
            // Sync balance (resolvedTokenAddress is already WETH for native ETH)
            await this.refreshBalance(resolvedTokenAddress);
            return {
                txHash,
                commitment,
                fee: 0n,
                requestId,
                wrapTxHash,
            };
        }
        catch (error) {
            const wrapped = PrivacyBoostError.from(error);
            txStore.updateTransaction(pendingHash, {
                status: TransactionStatus.failed,
                error: wrapped.message,
                updatedAt: Date.now(),
            });
            throw wrapped;
        }
    }
    // ============================================
    // Unshield Operations
    // ============================================
    /**
     * Unshield tokens from the privacy vault back to an on-chain address.
     *
     * Verifies sufficient shielded balance, generates a ZK proof, and submits
     * the unshield request. For native ETH, call `unwrapWeth()` after the
     * unshield is confirmed on-chain.
     *
     * @param params - Unshield parameters (tokenAddress, amount, recipientAddress, onProgress)
     * @returns Unshield result with requestId, placeholder txHash, and amount
     * @throws {PrivacyBoostError} `INSUFFICIENT_BALANCE` if shielded balance is too low
     *
     * @example
     * ```typescript
     * const result = await sdk.vault.unshield({
     *   tokenAddress: '0x...',
     *   amount: 500000n,
     *   recipientAddress: '0xMyWallet...',
     * });
     * ```
     */
    async unshield(params) {
        const { tokenAddress, amount, recipientAddress, onProgress, unwrapWeth: shouldUnwrap } = params;
        // Check balance
        const balance = await this.getBalance(tokenAddress);
        if (balance.shieldedBalance < amount) {
            throw new PrivacyBoostError(ErrorCodes.INSUFFICIENT_BALANCE, 'Insufficient shielded balance');
        }
        // Create pending transaction
        const pendingHash = generatePendingTxHash(TransactionType.unshield);
        const txStore = this.transactionStore.getState();
        txStore.addTransaction({
            txHash: pendingHash,
            type: TransactionType.unshield,
            direction: TransactionDirection.outgoing,
            status: TransactionStatus.pending,
            tokenAddress: tokenAddress,
            amount,
            to: recipientAddress,
            receivers: [],
            createdAt: Date.now(),
            updatedAt: Date.now(),
        });
        try {
            const forwardUnshieldProgress = (event) => {
                if (!onProgress)
                    return;
                switch (event.step) {
                    case 'preparing':
                        onProgress({ step: UnshieldStep.preparing, message: 'Preparing unshield...' });
                        return;
                    case 'proving':
                        // Auth-key signing also happens here (before the digest is sent),
                        // but `proving` is the user-visible label for the whole ZK + sign phase.
                        onProgress({ step: UnshieldStep.proving, message: 'Generating proof...' });
                        return;
                    case 'submitting':
                        // HTTP submit of the already-signed request. No dedicated UI step;
                        // `submitted` follows immediately, so skip to avoid a misleading label.
                        return;
                    case 'submitted':
                        onProgress({ step: UnshieldStep.unshielding, message: 'Processing unshield...' });
                        return;
                }
            };
            const result = await this.keyVault.unshield(tokenAddress, amount.toString(), recipientAddress, forwardUnshieldProgress);
            const requestId = result.requestId;
            txStore.updateTransaction(pendingHash, {
                status: TransactionStatus.pending,
                requestId,
                updatedAt: Date.now(),
            });
            // Sync balance
            await this.refreshBalance(tokenAddress);
            // If unwrapWeth is requested, wait for completion then unwrap
            if (shouldUnwrap) {
                await this.waitAndUnwrapWeth(requestId, amount, recipientAddress, onProgress);
            }
            return {
                requestId,
                txHash: pendingHash,
                amount,
            };
        }
        catch (error) {
            const wrapped = PrivacyBoostError.from(error);
            txStore.updateTransaction(pendingHash, {
                status: TransactionStatus.failed,
                error: wrapped.message,
                updatedAt: Date.now(),
            });
            throw wrapped;
        }
    }
    /**
     * Wait for an unshield to complete on-chain, then unwrap WETH to ETH.
     */
    async waitAndUnwrapWeth(requestId, amount, recipientAddress, onProgress) {
        const POLL_INTERVAL = 2_000;
        const TIMEOUT = 120_000;
        const deadline = Date.now() + TIMEOUT;
        while (Date.now() < deadline) {
            try {
                const status = await this.indexer.getUnshieldStatus(requestId);
                if (status.status === 'completed') {
                    onProgress?.({
                        step: UnshieldStep.unwrapping,
                        message: 'Unwrapping WETH to ETH...',
                    });
                    await this.unwrapWeth(amount, recipientAddress);
                    return;
                }
                if (status.status === 'failed') {
                    throw new PrivacyBoostError(ErrorCodes.TRANSACTION_FAILED, 'Unshield failed before ETH unwrap');
                }
            }
            catch (e) {
                if (e instanceof PrivacyBoostError)
                    throw e;
                // Status endpoint may lag briefly; keep polling
                this.log.debug(TAG, 'Unshield status poll error, retrying...', e);
            }
            await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
        }
        throw new PrivacyBoostError(ErrorCodes.TRANSACTION_FAILED, 'Timed out waiting for unshield confirmation before ETH unwrap');
    }
    // ============================================
    // WETH Unwrap (post-unshield)
    // ============================================
    /**
     * Unwrap WETH to ETH after a unshield has been confirmed on-chain.
     *
     * Call this after `getUnshieldStatus()` confirms the unshield has settled.
     * The connected wallet must be the same address that received the unshield.
     *
     * @param amount - Amount of WETH to unwrap (in wei)
     * @param recipientAddress - The address the withdrawal was sent to (must match connected wallet)
     * @throws {PrivacyBoostError} if the unwrap transaction fails or wallet doesn't match recipient
     */
    async unwrapWeth(amount, recipientAddress) {
        if (!this.config.wethContractAddress) {
            throw new PrivacyBoostError(ErrorCodes.INVALID_CONFIG, 'wethContractAddress must be configured to unwrap WETH');
        }
        try {
            const txHash = await this.keyVault.unwrapWeth(amount.toString(), recipientAddress);
            return txHash;
        }
        catch (error) {
            throw PrivacyBoostError.from(error);
        }
    }
    // ============================================
    // Send Operations
    // ============================================
    /**
     * Send a private transfer to another user.
     *
     * The recipient is identified by their privacy address. The transfer
     * is encrypted and submitted as a ZK proof — the on-chain transaction
     * reveals no link between sender and receiver.
     *
     * @param params - Send parameters (to: privacy address, amount, tokenAddress)
     * @returns Transaction result with requestId and placeholder txHash
     * @throws {PrivacyBoostError} `INSUFFICIENT_BALANCE` if shielded balance is too low
     *
     * @example
     * ```typescript
     * const result = await sdk.vault.send({
     *   to: recipientPrivacyAddress,
     *   amount: 100000n,
     *   tokenAddress: '0x...',
     * });
     * ```
     */
    async send(params) {
        const { to, tokenAddress, amount } = params;
        // Check balance
        const balance = await this.getBalance(tokenAddress);
        if (balance.shieldedBalance < amount) {
            throw new PrivacyBoostError(ErrorCodes.INSUFFICIENT_BALANCE, 'Insufficient shielded balance');
        }
        // Create pending transaction
        const pendingHash = generatePendingTxHash(TransactionType.transfer);
        const txStore = this.transactionStore.getState();
        txStore.addTransaction({
            txHash: pendingHash,
            type: TransactionType.transfer,
            direction: TransactionDirection.outgoing,
            status: TransactionStatus.pending,
            tokenAddress: tokenAddress,
            amount,
            to: to,
            receivers: [],
            createdAt: Date.now(),
            updatedAt: Date.now(),
        });
        try {
            const result = await this.keyVault.sendTransfer(tokenAddress, amount.toString(), to);
            const requestId = result.requestId;
            txStore.updateTransaction(pendingHash, {
                status: TransactionStatus.pending,
                requestId,
                updatedAt: Date.now(),
            });
            // Sync balance
            await this.refreshBalance(tokenAddress);
            return {
                requestId,
                txHash: pendingHash,
            };
        }
        catch (error) {
            const wrapped = PrivacyBoostError.from(error);
            txStore.updateTransaction(pendingHash, {
                status: TransactionStatus.failed,
                error: wrapped.message,
                updatedAt: Date.now(),
            });
            throw wrapped;
        }
    }
    // ============================================
    // Public Client (for on-chain reads)
    // ============================================
    /**
     * Get or create a viem public client for on-chain reads
     */
    getPublicClient() {
        if (this.publicClient) {
            return this.publicClient;
        }
        this.publicClient = createChainClient(this.config.chainId, this.config.rpcUrl);
        return this.publicClient;
    }
    // ============================================
    // Balance Operations
    // ============================================
    /**
     * Get token metadata (symbol, decimals)
     * Falls back to defaults for known tokens, otherwise fetches from blockchain
     */
    async getTokenMetadata(tokenAddress) {
        const normalizedAddress = tokenAddress.toLowerCase();
        // Check default metadata first (includes ETH, WETH)
        const defaultMeta = DEFAULT_TOKEN_METADATA[normalizedAddress];
        if (defaultMeta) {
            return defaultMeta;
        }
        // Check token store cache
        const tokenStore = this.tokenStore.getState();
        const cached = tokenStore.getToken(normalizedAddress);
        if (cached) {
            return { symbol: cached.symbol, decimals: cached.decimals };
        }
        // Try to fetch from blockchain using ERC20 calls
        try {
            const publicClient = this.getPublicClient();
            const [symbol, decimals] = await Promise.all([
                publicClient.readContract({
                    address: tokenAddress,
                    abi: ERC20_METADATA_ABI,
                    functionName: 'symbol',
                }),
                publicClient.readContract({
                    address: tokenAddress,
                    abi: ERC20_METADATA_ABI,
                    functionName: 'decimals',
                }),
            ]);
            this.log.debug(TAG, 'Fetched token metadata from chain', symbol, decimals);
            // Cache the result
            tokenStore.setToken({
                address: normalizedAddress,
                symbol,
                decimals,
                name: symbol,
            });
            return { symbol, decimals };
        }
        catch (error) {
            this.log.warn(TAG, 'Failed to fetch token metadata from chain', error);
            // Fallback to generic
            return { symbol: 'TOKEN', decimals: 18 };
        }
    }
    /**
     * Get wallet (on-chain) balance for a token.
     *
     * Fetches the ERC-20 balanceOf (or native ETH balance) for the
     * connected wallet address. Returns 0n if no wallet is connected.
     *
     * @param tokenAddress - ERC-20 token address or NATIVE_ETH_ADDRESS
     * @returns On-chain balance as bigint
     */
    async getWalletBalance(tokenAddress) {
        const walletStore = this.walletStoreRef.getState();
        const address = walletStore.address;
        if (!address) {
            return 0n;
        }
        try {
            const publicClient = this.getPublicClient();
            const isNative = tokenAddress.toLowerCase() === '0x0000000000000000000000000000000000000000';
            let balance;
            if (isNative) {
                balance = await publicClient.getBalance({ address: address });
            }
            else {
                // Check if contract exists
                const bytecode = await publicClient.getBytecode({ address: tokenAddress });
                if (!bytecode || bytecode === '0x') {
                    this.log.debug(TAG, 'Contract not found, returning 0');
                    return 0n;
                }
                balance = await publicClient.readContract({
                    address: tokenAddress,
                    abi: ERC20_BALANCE_OF_ABI,
                    functionName: 'balanceOf',
                    args: [address],
                });
            }
            return balance;
        }
        catch (error) {
            this.log.error(TAG, 'Failed to fetch wallet balance', error);
            return 0n;
        }
    }
    /**
     * Get shielded balance for a token.
     *
     * Returns a cached value if available. Call `refreshBalance()` to force
     * a fresh fetch from the indexer.
     *
     * @param tokenAddress - ERC-20 token address or NATIVE_ETH_ADDRESS
     * @returns Token balance including shielded and wallet amounts
     */
    async getBalance(tokenAddress) {
        const walletStore = this.walletStoreRef.getState();
        if (!walletStore.isAuthenticated) {
            return { tokenAddress: tokenAddress, shieldedBalance: 0n, walletBalance: 0n };
        }
        // Check cache first
        const balanceStore = this.balanceStore.getState();
        const cached = balanceStore.getBalance(tokenAddress);
        if (cached) {
            return cached;
        }
        try {
            const mpk = await this.getMpk();
            const [result, metadata] = await Promise.all([
                this.indexer.getWalletBalance(mpk, tokenAddress),
                this.getTokenMetadata(tokenAddress),
            ]);
            const balance = BigInt(result.balances[0]?.balance ?? '0');
            const existing = balanceStore.getBalance(tokenAddress);
            const tokenBalance = {
                tokenAddress: tokenAddress,
                shieldedBalance: balance,
                walletBalance: 0n,
                symbol: metadata.symbol,
                decimals: metadata.decimals,
                noteCount: existing?.noteCount,
            };
            balanceStore.setBalance(tokenAddress, tokenBalance);
            return tokenBalance;
        }
        catch (error) {
            this.log.error(TAG, 'Failed to get balance', error);
            throw PrivacyBoostError.from(error);
        }
    }
    /**
     * Get all balances (shielded from indexer + wallet from chain) for every
     * token the user has a shielded balance in.
     *
     * @returns Array of token balances with both shielded and on-chain amounts
     */
    async getAllBalances() {
        try {
            const mpk = await this.getMpk();
            const [walletBalances, registeredTokens] = await Promise.all([
                this.indexer.getWalletBalances(mpk),
                this.indexer.getRegisteredTokens(),
            ]);
            // Build tokenId → tokenInfo map for address resolution
            const tokenById = new Map(registeredTokens.map((t) => [t.id, t]));
            const walletStore = this.walletStoreRef.getState();
            const hasWalletAddress = !!walletStore.address;
            const balances = [];
            const balancePromises = walletBalances.balances.map(async (entry) => {
                const tokenInfo = tokenById.get(entry.tokenId);
                if (!tokenInfo)
                    return null;
                const tokenAddress = tokenInfo.tokenAddress;
                const shielded = BigInt(entry.balance || '0');
                let wallet = 0n;
                const isValidAddr = VALID_HEX_ADDRESS_RE.test(tokenAddress);
                if (hasWalletAddress && isValidAddr) {
                    try {
                        wallet = await this.getWalletBalance(tokenAddress);
                    }
                    catch (e) {
                        this.log.warn(TAG, 'Failed to get wallet balance for token', e);
                    }
                }
                const metadata = isValidAddr
                    ? await this.getTokenMetadata(tokenAddress)
                    : { symbol: tokenInfo.symbol || 'TOKEN', decimals: tokenInfo.decimals ?? 18 };
                return {
                    tokenAddress: tokenAddress,
                    shieldedBalance: shielded,
                    walletBalance: wallet,
                    symbol: metadata.symbol,
                    decimals: metadata.decimals,
                    noteCount: entry.noteCount ?? 0,
                };
            });
            const resolvedBalances = await Promise.all(balancePromises);
            for (const balance of resolvedBalances) {
                if (!balance)
                    continue;
                balances.push(balance);
                this.balanceStore.getState().setBalance(balance.tokenAddress, balance);
            }
            this.log.debug(TAG, `Fetched ${balances.length} balances`);
            return balances;
        }
        catch (error) {
            this.log.error(TAG, 'Failed to get all balances', error);
            throw PrivacyBoostError.from(error);
        }
    }
    async getRegisteredTokens() {
        const tokens = await this.indexer.getRegisteredTokens();
        return tokens.map((t) => ({
            id: t.id,
            tokenType: t.tokenType,
            tokenAddress: t.tokenAddress,
            tokenSubId: t.tokenSubId,
            symbol: t.symbol ?? undefined,
            decimals: t.decimals,
            priceUSD: t.priceUSD,
            minShieldAmount: t.minDepositAmount,
        }));
    }
    /**
     * Get registered tokens with full metadata (symbol, decimals, name).
     *
     * Fetches the token list from the on-chain registry via the indexer,
     * then resolves ERC20 metadata for each token (with caching).
     */
    async getTokens() {
        const registered = await this.getRegisteredTokens();
        const tokens = await Promise.all(registered.map(async (entry) => {
            const metadata = await this.getTokenMetadata(entry.tokenAddress);
            const token = {
                address: entry.tokenAddress,
                symbol: metadata.symbol,
                decimals: entry.decimals ?? metadata.decimals,
                name: metadata.symbol,
                priceUSD: entry.priceUSD,
                minShieldAmount: entry.minShieldAmount,
            };
            // Cache in token store
            this.tokenStore.getState().setToken(token);
            return token;
        }));
        return tokens;
    }
    async getFees(tokenAddress) {
        // Resolve tokenAddress to tokenId using registered tokens
        const tokens = await this.indexer.getRegisteredTokens();
        const token = tokens.find((t) => t.tokenAddress.toLowerCase() === tokenAddress.toLowerCase());
        if (!token) {
            throw new PrivacyBoostError(ErrorCodes.INVALID_PARAMETER, `Token not found: ${tokenAddress}`);
        }
        const result = await this.indexer.getFees(token.id);
        return {
            transferFeeFlat: result.transferFeeFlat,
            unshieldFeeBps: result.unshieldFeeBps,
            feeModel: result.feeModel,
            minTransferAmountUSD: result.minTransferAmountUSD,
        };
    }
    /**
     * Refresh balance for a specific token (both shielded and wallet)
     */
    async refreshBalance(tokenAddress) {
        try {
            const mpk = await this.getMpk();
            const [shieldedResult, walletBalance, metadata] = await Promise.all([
                this.indexer.getWalletBalance(mpk, tokenAddress).catch((e) => {
                    this.log.warn(TAG, 'Failed to fetch shielded balance', e);
                    return { pubkey: '', balances: [] };
                }),
                this.getWalletBalance(tokenAddress),
                this.getTokenMetadata(tokenAddress),
            ]);
            const shieldedBalance = BigInt(shieldedResult.balances[0]?.balance ?? '0');
            const existingBal = this.balanceStore.getState().getBalance(tokenAddress);
            this.balanceStore.getState().setBalance(tokenAddress, {
                tokenAddress,
                shieldedBalance: shieldedBalance,
                walletBalance: walletBalance,
                symbol: metadata.symbol,
                decimals: metadata.decimals,
                noteCount: existingBal?.noteCount,
            });
            this.log.debug(TAG, `Balance synced for ${metadata.symbol}`);
        }
        catch (error) {
            this.log.error(TAG, 'Failed to sync balance', error);
            throw PrivacyBoostError.from(error);
        }
    }
    /**
     * Refresh all balances (both shielded and wallet for tracked tokens)
     */
    async refreshBalances() {
        // Build set of tokens to track (deduped)
        const tokensToTrack = new Set();
        // Always track native ETH
        tokensToTrack.add(NATIVE_ETH_ADDRESS);
        // Always track WETH if configured
        if (this.config.wethContractAddress) {
            tokensToTrack.add(this.config.wethContractAddress.toLowerCase());
        }
        // Add registered tokens from on-chain registry
        try {
            const registered = await this.indexer.getRegisteredTokens();
            for (const t of registered) {
                if (VALID_HEX_ADDRESS_RE.test(t.tokenAddress)) {
                    tokensToTrack.add(t.tokenAddress.toLowerCase());
                }
            }
        }
        catch (e) {
            this.log.warn(TAG, 'Failed to fetch registered tokens, continuing with known tokens', e);
        }
        // Get shielded balances and add their tokens
        const shieldedBalances = await this.getAllBalances();
        for (const b of shieldedBalances) {
            if (VALID_HEX_ADDRESS_RE.test(b.tokenAddress)) {
                tokensToTrack.add(b.tokenAddress.toLowerCase());
            }
        }
        this.log.debug(TAG, `Syncing ${tokensToTrack.size} tokens`);
        // getAllBalances() already fetches wallet balance + metadata for each token,
        // so reuse those results to avoid double-fetching the same data.
        const fetchedBalanceMap = new Map(shieldedBalances.map((b) => [b.tokenAddress.toLowerCase(), b]));
        // Only fetch wallet balance + metadata for tokens not covered above
        const tokenArray = Array.from(tokensToTrack);
        const balancePromises = tokenArray.map(async (tokenAddress) => {
            const existing = fetchedBalanceMap.get(tokenAddress.toLowerCase());
            if (existing) {
                return existing;
            }
            // Only fetch for tokens not covered by getAllBalances()
            const wallet = await this.getWalletBalance(tokenAddress);
            const metadata = await this.getTokenMetadata(tokenAddress);
            return { tokenAddress, shieldedBalance: 0n, walletBalance: wallet, ...metadata };
        });
        const results = await Promise.all(balancePromises);
        // Update store with all balances
        const balanceStore = this.balanceStore.getState();
        for (const { tokenAddress, shieldedBalance, walletBalance, symbol, decimals } of results) {
            const existingEntry = balanceStore.getBalance(tokenAddress);
            balanceStore.setBalance(tokenAddress, {
                tokenAddress,
                shieldedBalance,
                walletBalance,
                symbol,
                decimals,
                noteCount: existingEntry?.noteCount,
            });
        }
        this.log.debug(TAG, `All ${results.length} balances synced`);
    }
    /**
     * Consolidate fragmented notes for a token.
     *
     * Performs self-transfers to merge many small notes into fewer notes.
     * Each round consolidates up to 15 input notes into 1-2 output notes.
     * Useful when a user has accumulated many small notes from deposits.
     *
     * @param tokenAddress - Token address to consolidate
     * @param maxRounds - Maximum consolidation rounds. Pass 0 for unlimited;
     *   default is 3. Unlimited is risky because inter-round state relies on
     *   the indexer having ingested the previous round's outputs, which may
     *   not yet be true when the next round fetches notes.
     * @returns Consolidation result with rounds executed and remaining note count
     */
    async consolidateNotes(tokenAddress, maxRounds = 3) {
        // WASM returns camelCase fields (serde rename_all = "camelCase" on both
        // ConsolidateResult and TransferResult), so no snake_case mapping needed.
        const result = await this.keyVault.consolidateNotes(tokenAddress, maxRounds);
        // Sync balance after consolidation
        await this.refreshBalance(tokenAddress);
        return {
            rounds: result.rounds,
            transfers: result.transfers || [],
            remainingNotes: result.remainingNotes,
        };
    }
    /**
     * Refresh wallet (on-chain) balances for all registered tokens.
     *
     * Unlike `refreshBalances()` which refreshes both shielded and wallet balances,
     * this method only fetches on-chain wallet balances. Useful when you need
     * wallet balances for tokens the user hasn't shielded yet (e.g. to show
     * available amounts for deposit).
     *
     * Updates the balance store: for tokens already present, updates the wallet
     * field; for new tokens, creates an entry with shieldedBalance: 0n.
     */
    async refreshAllWalletBalances() {
        const tokensToFetch = new Set();
        // Always include native ETH
        tokensToFetch.add(NATIVE_ETH_ADDRESS);
        // Include WETH if configured
        if (this.config.wethContractAddress) {
            tokensToFetch.add(this.config.wethContractAddress.toLowerCase());
        }
        // Include all registered tokens
        try {
            const registered = await this.indexer.getRegisteredTokens();
            for (const t of registered) {
                if (VALID_HEX_ADDRESS_RE.test(t.tokenAddress)) {
                    tokensToFetch.add(t.tokenAddress.toLowerCase());
                }
            }
        }
        catch (e) {
            this.log.warn(TAG, 'Failed to fetch registered tokens for wallet balance sync', e);
        }
        // Fetch wallet balances in parallel
        const tokenArray = Array.from(tokensToFetch);
        const results = await Promise.all(tokenArray.map(async (tokenAddress) => {
            const wallet = await this.getWalletBalance(tokenAddress);
            const metadata = await this.getTokenMetadata(tokenAddress);
            return { tokenAddress, wallet, symbol: metadata.symbol, decimals: metadata.decimals };
        }));
        // Update balance store
        const balanceStore = this.balanceStore.getState();
        for (const { tokenAddress, wallet, symbol, decimals } of results) {
            const existing = balanceStore.getBalance(tokenAddress);
            balanceStore.setBalance(tokenAddress, {
                tokenAddress,
                shieldedBalance: existing?.shieldedBalance ?? 0n,
                walletBalance: wallet,
                symbol: existing?.symbol ?? symbol,
                decimals: existing?.decimals ?? decimals,
                noteCount: existing?.noteCount,
            });
        }
        this.log.debug(TAG, `Wallet balances synced for ${results.length} tokens`);
    }
    // ============================================
    // Token Operations
    // ============================================
    /**
     * Get token metadata
     */
    async getToken(address) {
        // Check cache
        const tokenStore = this.tokenStore.getState();
        const cached = tokenStore.getToken(address);
        if (cached) {
            return cached;
        }
        try {
            const result = await this.indexer.getTokenByAddress(address);
            const token = {
                address: address,
                symbol: result.symbol || 'TOKEN',
                decimals: result.decimals ?? 18,
                name: result.symbol || 'TOKEN',
            };
            // Update store
            tokenStore.setToken(token);
            return token;
        }
        catch (error) {
            throw PrivacyBoostError.from(error);
        }
    }
}
//# sourceMappingURL=Vault.js.map