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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 84 additions & 3 deletions extension/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bypass SDK mapping for the local fee method

When a page calls window.nockchain.request({ method: 'nock_estimateTransactionFee', ... }) without an api field, the inpage provider forwards the args unchanged, so resolveSourceApiVersion treats it as legacy v0. Adding this SDK-missing local method to isProviderMethod sends it through @nockbox/iris-sdk@0.2.0's version mapper before the new switch case can run; the adjacent TODO says that SDK version does not define the method yet, so these public requests are rejected during bridging instead of reaching the handler unless the dApp explicitly sets the current API version.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, but I don't think this holds — I verified it against the actually-installed @nockbox/iris-sdk@0.2.0 mapper.

mapRequest/mapResponse in the bundled compat.js both end in a default: return request/response case, so an unknown method like nock_estimateTransactionFee is not rejected during v0→v1 (or v1→v0) bridging — it passes through untouched and reaches the new switch case. Runtime check against the installed package:

mapRpcRequest({ method: 'nock_estimateTransactionFee', params: { to, amount } }, '0', '1.0.0')
  -> { method: 'nock_estimateTransactionFee', params: { to, amount } }   // unchanged
mapRpcResponse('nock_estimateTransactionFee', { result: { fee: '504' } }, '1.0.0', '0')
  -> { result: { fee: '504' } }                                          // unchanged

So both paths work: a dApp using the SDK provider sends api: '1.0.0' (bridging short-circuits to passthrough at bridgeIncomingProviderPayload), and a dApp calling window.nockchain.request(...) directly without api is treated as v0 and still passes through the mapper's default case. No change needed here.


Generated by Claude Code

// Legacy v0 API method
method === 'nock_signRawTx'
);
Expand Down Expand Up @@ -986,23 +987,50 @@ 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 = {
id: txRequestId,
origin: sendTxOrigin,
to,
amount: amountNicks,
fee: feeNicks,
fee: displayFeeNicks,
feeEstimated: feeNicks === undefined,
timestamp: Date.now(),
};

Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -120,7 +121,7 @@ export function TransactionApprovalScreen() {
{/* Fee & Total */}
<div className="rounded-lg p-3 space-y-2" style={{ backgroundColor: surface }}>
<div className="flex justify-between text-sm">
<span>Network fee</span>
<span>Network fee{isFeeEstimated ? ' (estimated)' : ''}</span>
<div className="text-right">
<div>{formatNock(feeNum / NOCK_TO_NICKS)} NOCK</div>
<div className="text-[10px]" style={{ color: textMuted }}>
Expand Down
14 changes: 12 additions & 2 deletions extension/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@
*/

// Import provider methods from SDK
import { PROVIDER_METHODS } from '@nockbox/iris-sdk';
export { PROVIDER_METHODS };
import { PROVIDER_METHODS as SDK_PROVIDER_METHODS } from '@nockbox/iris-sdk';

/**
* Provider methods: SDK methods plus methods added locally ahead of the next SDK release.
* TODO: drop the local ESTIMATE_TRANSACTION_FEE entry once @nockbox/iris-sdk >= 0.3.0
* (which defines it) is published and the dependency is upgraded.
*/
export const PROVIDER_METHODS = {
...SDK_PROVIDER_METHODS,
/** Estimate transaction fee for a dApp send (read-only, no approval popup) */
ESTIMATE_TRANSACTION_FEE: 'nock_estimateTransactionFee',
} as const;

/**
* Internal Extension Methods - Called by popup UI and other extension components
Expand Down
7 changes: 6 additions & 1 deletion extension/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,13 @@ export interface TransactionRequest {
to: string;
/** Amount in nicks (WASM Nicks = string) */
amount: Nicks;
/** Transaction fee in nicks */
/** Transaction fee in nicks (explicit from dApp, or wallet-estimated when omitted) */
fee: Nicks;
/**
* True when the dApp omitted `fee` and `fee` holds a wallet-side estimate;
* on approval the exact fee is auto-calculated by WASM at build time.
*/
feeEstimated?: boolean;
/** Request timestamp */
timestamp: number;
}
Expand Down
Loading