diff --git a/extension/background/index.ts b/extension/background/index.ts index 636e3fb..db6b355 100644 --- a/extension/background/index.ts +++ b/extension/background/index.ts @@ -310,6 +310,7 @@ function isProviderMethod(method: unknown): method is string { method === PROVIDER_METHODS.SEND_TRANSACTION || method === PROVIDER_METHODS.GET_WALLET_INFO || method === PROVIDER_METHODS.SIGN_TX || + method === PROVIDER_METHODS.ESTIMATE_TRANSACTION_FEE || // Legacy v0 API method method === 'nock_signRawTx' ); @@ -986,15 +987,41 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { return; } let amountNicks: Nicks; - let feeNicks: Nicks; + let feeNicks: Nicks | undefined; try { amountNicks = parseNicksParam(amount, 'amount'); - feeNicks = parseNicksParam(fee, 'fee', { allowZero: true }); + feeNicks = + fee === undefined || fee === null + ? undefined // omitted: estimate below, auto-calc exact fee at build time + : parseNicksParam(fee, 'fee', { allowZero: true }); } catch (err) { await sendBridgedResponse(toInvalidParamsError(err)); return; } + // Fee omitted: estimate it now so the approval popup can display it. + // Estimation failure rejects the request up front (better than a popup + // with no fee or a guaranteed-to-fail broadcast). + let displayFeeNicks: Nicks; + if (feeNicks === undefined) { + try { + const sendTxEstimate = await vault.estimateTransactionFee(to, amountNicks); + if ('error' in sendTxEstimate) { + await sendBridgedResponse({ + error: { code: -32603, message: `Fee estimation failed: ${sendTxEstimate.error}` }, + }); + return; + } + displayFeeNicks = String(sendTxEstimate.fee) as Nicks; + } catch (err) { + console.error('[Background] Fee estimation for sendTransaction failed:', err); + await sendBridgedResponse(toInternalProviderError(err)); + return; + } + } else { + displayFeeNicks = feeNicks; + } + // Create transaction approval request const txRequestId = crypto.randomUUID(); const txRequest: TransactionRequest = { @@ -1002,7 +1029,8 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { origin: sendTxOrigin, to, amount: amountNicks, - fee: feeNicks, + fee: displayFeeNicks, + feeEstimated: feeNicks === undefined, timestamp: Date.now(), }; @@ -1043,6 +1071,54 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { } return; + case PROVIDER_METHODS.ESTIMATE_TRANSACTION_FEE: { + // Read-only like GET_WALLET_INFO: approved origin + unlocked vault, no approval popup + const estimateFeeOrigin = _sender.url || _sender.origin || ''; + if (!isOriginApproved(estimateFeeOrigin)) { + await sendBridgedResponse({ error: { code: 4100, message: 'Unauthorized origin' } }); + return; + } + + if (vault.isLocked()) { + await sendBridgedResponse({ error: ERROR_CODES.LOCKED }); + return; + } + + const estimateFeeParams = + payload.params && typeof payload.params === 'object' ? payload.params : {}; + const { to: estimateFeeTo, amount: estimateFeeAmount } = estimateFeeParams; + if (!isNockAddress(estimateFeeTo)) { + await sendBridgedResponse({ error: ERROR_CODES.BAD_ADDRESS }); + return; + } + let estimateFeeAmountNicks: Nicks; + try { + estimateFeeAmountNicks = parseNicksParam(estimateFeeAmount, 'amount'); + } catch (err) { + await sendBridgedResponse(toInvalidParamsError(err)); + return; + } + + try { + const estimateFeeResult = await vault.estimateTransactionFee( + estimateFeeTo, + estimateFeeAmountNicks + ); + if ('error' in estimateFeeResult) { + await sendBridgedResponse({ + error: { code: -32603, message: estimateFeeResult.error }, + }); + return; + } + // Vault returns a number; the public API uses canonical Nicks (string) + await sendBridgedResponse({ fee: String(estimateFeeResult.fee) as Nicks }); + } catch (err) { + console.error('[Background] Public fee estimation failed:', err); + await sendBridgedResponse(toInternalProviderError(err)); + } + return; + } + // Internal methods (called from popup) case INTERNAL_METHODS.SET_AUTO_LOCK: const newMinutes = payload.params?.[0]; @@ -1541,6 +1617,10 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { const v2Result = await vault.sendTransactionV2( txRequest.to, txRequest.amount, + // Always a concrete fee: the dApp's explicit fee, or the wallet's + // fresh estimate (matches the popup send flow). This sizes note + // selection to the displayed fee rather than the 2-NOCK placeholder + // the undefined-fee branch would use. txRequest.fee, false, undefined, @@ -1554,6 +1634,7 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { approveTxPending.sendResponse({ txid: v2Result.txId, amount: txRequest.amount, + // Fee charged equals txRequest.fee (applied verbatim as fee override) fee: txRequest.fee, }); cancelPendingRequest(approveTxId); diff --git a/extension/popup/screens/approvals/TransactionApprovalScreen.tsx b/extension/popup/screens/approvals/TransactionApprovalScreen.tsx index 753211e..a48cd63 100644 --- a/extension/popup/screens/approvals/TransactionApprovalScreen.tsx +++ b/extension/popup/screens/approvals/TransactionApprovalScreen.tsx @@ -18,6 +18,7 @@ export function TransactionApprovalScreen() { const { id, origin, to, amount } = pendingTransactionRequest; const fee = pendingTransactionRequest.fee; + const isFeeEstimated = Boolean(pendingTransactionRequest.feeEstimated); const amountNum = Number(amount); const feeNum = Number(fee); const totalNum = amountNum + feeNum; @@ -120,7 +121,7 @@ export function TransactionApprovalScreen() { {/* Fee & Total */}