diff --git a/extension/background/index.ts b/extension/background/index.ts
index 64823f7..48ff3ba 100644
--- a/extension/background/index.ts
+++ b/extension/background/index.ts
@@ -15,7 +15,7 @@ import {
assertNativeNote,
assertNativeSpendCondition,
} from '../shared/sign-raw-tx-compat';
-import { isLegacySignRawTxRequest } from '@nockbox/iris-sdk';
+import { isLegacySignRawTxRequest, isEvmAddress } from '@nockbox/iris-sdk';
import type { Note, SpendCondition } from '@nockbox/iris-sdk/wasm';
import type { Nicks } from '../shared/currency';
import {
@@ -1497,6 +1497,91 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
}
return;
+ case INTERNAL_METHODS.ESTIMATE_BRIDGE_FEE:
+ // params: [destinationAddress, amountNicks]
+ if (vault.isLocked()) {
+ sendResponse({ error: ERROR_CODES.LOCKED });
+ return;
+ }
+
+ const [estimateBridgeDest, estimateBridgeAmountNicks] = payload.params || [];
+ if (!estimateBridgeDest || !isEvmAddress(estimateBridgeDest)) {
+ sendResponse({ error: 'Invalid destination address. Expected EVM address (0x...).' });
+ return;
+ }
+ let estimateBridgeAmountParsed: Nicks;
+ try {
+ estimateBridgeAmountParsed = parseNicksParam(estimateBridgeAmountNicks, 'amount');
+ } catch (err) {
+ sendResponse({ error: err instanceof Error ? err.message : 'Invalid amount' });
+ return;
+ }
+
+ try {
+ const estimateResult = await vault.estimateBridgeFee(
+ estimateBridgeDest,
+ estimateBridgeAmountParsed
+ );
+
+ if ('error' in estimateResult) {
+ sendResponse({ error: estimateResult.error });
+ return;
+ }
+
+ sendResponse({ fee: estimateResult.fee });
+ } catch (error) {
+ console.error('[Background] Bridge fee estimation failed:', error);
+ sendResponse({
+ error: error instanceof Error ? error.message : 'Bridge fee estimation failed',
+ });
+ }
+ return;
+
+ case INTERNAL_METHODS.SEND_BRIDGE_TRANSACTION:
+ // params: [destinationAddress, amountNicks, priceUsdAtTime?] - EVM address (Base), amount in nicks
+ if (vault.isLocked()) {
+ sendResponse({ error: ERROR_CODES.LOCKED });
+ return;
+ }
+
+ const [bridgeDest, bridgeAmountNicks, bridgePriceUsd] = payload.params || [];
+ if (!bridgeDest || !isEvmAddress(bridgeDest)) {
+ sendResponse({ error: 'Invalid destination address. Expected EVM address (0x...).' });
+ return;
+ }
+ let bridgeAmountNicksParsed: Nicks;
+ try {
+ bridgeAmountNicksParsed = parseNicksParam(bridgeAmountNicks, 'amount');
+ } catch (err) {
+ sendResponse({ error: err instanceof Error ? err.message : 'Invalid amount' });
+ return;
+ }
+
+ try {
+ const bridgeResult = await vault.sendBridgeTransaction(
+ bridgeDest,
+ bridgeAmountNicksParsed,
+ typeof bridgePriceUsd === 'number' ? bridgePriceUsd : undefined
+ );
+
+ if ('error' in bridgeResult) {
+ sendResponse({ error: bridgeResult.error });
+ return;
+ }
+
+ sendResponse({
+ txid: bridgeResult.txId,
+ broadcasted: bridgeResult.broadcasted,
+ walletTx: bridgeResult.walletTx,
+ });
+ } catch (error) {
+ console.error('[Background] Bridge transaction failed:', error);
+ sendResponse({
+ error: error instanceof Error ? error.message : 'Bridge transaction failed',
+ });
+ }
+ return;
+
case INTERNAL_METHODS.SEND_TRANSACTION:
// params: [to, amount, fee] - amount and fee in nicks
// Called from popup Send screen - builds, signs, and broadcasts transaction
diff --git a/extension/popup/Router.tsx b/extension/popup/Router.tsx
index 7069c2c..bb5def9 100644
--- a/extension/popup/Router.tsx
+++ b/extension/popup/Router.tsx
@@ -18,6 +18,8 @@ import { HomeScreen } from './screens/HomeScreen';
import { SendScreen } from './screens/SendScreen';
import { SendReviewScreen } from './screens/SendReviewScreen';
import { SendSubmittedScreen } from './screens/SendSubmittedScreen';
+import { SwapScreen } from './screens/SwapScreen';
+import { SwapReviewScreen } from './screens/SwapReviewScreen';
import { SentScreen } from './screens/transactions/SentScreen';
import { TransactionDetailsScreen } from './screens/TransactionDetailsScreen';
import { ReceiveScreen } from './screens/ReceiveScreen';
@@ -97,6 +99,10 @@ export function Router() {
return ;
case 'receive':
return ;
+ case 'swap':
+ return ;
+ case 'swap-review':
+ return ;
case 'tx-details':
return ;
diff --git a/extension/popup/assets/JustNText.svg b/extension/popup/assets/JustNText.svg
new file mode 100644
index 0000000..c896c16
--- /dev/null
+++ b/extension/popup/assets/JustNText.svg
@@ -0,0 +1,3 @@
+
diff --git a/extension/popup/assets/NockSmallCircle.svg b/extension/popup/assets/NockSmallCircle.svg
new file mode 100644
index 0000000..97a8fe9
--- /dev/null
+++ b/extension/popup/assets/NockSmallCircle.svg
@@ -0,0 +1,4 @@
+
diff --git a/extension/popup/assets/NockText.svg b/extension/popup/assets/NockText.svg
new file mode 100644
index 0000000..5ce6bf1
--- /dev/null
+++ b/extension/popup/assets/NockText.svg
@@ -0,0 +1,7 @@
+
diff --git a/extension/popup/assets/NockTextCircleContainer.svg b/extension/popup/assets/NockTextCircleContainer.svg
new file mode 100644
index 0000000..e5b5dc1
--- /dev/null
+++ b/extension/popup/assets/NockTextCircleContainer.svg
@@ -0,0 +1,3 @@
+
diff --git a/extension/popup/assets/base_icon.svg b/extension/popup/assets/base_icon.svg
new file mode 100644
index 0000000..9adbb1f
--- /dev/null
+++ b/extension/popup/assets/base_icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/extension/popup/assets/downArrow.svg b/extension/popup/assets/downArrow.svg
new file mode 100644
index 0000000..0d43a8f
--- /dev/null
+++ b/extension/popup/assets/downArrow.svg
@@ -0,0 +1,3 @@
+
diff --git a/extension/popup/assets/swap_icon.svg b/extension/popup/assets/swap_icon.svg
new file mode 100644
index 0000000..d019c47
--- /dev/null
+++ b/extension/popup/assets/swap_icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/extension/popup/assets/upDownvec.svg b/extension/popup/assets/upDownvec.svg
new file mode 100644
index 0000000..91d0817
--- /dev/null
+++ b/extension/popup/assets/upDownvec.svg
@@ -0,0 +1,3 @@
+
diff --git a/extension/popup/components/SwapSubmittedToast.tsx b/extension/popup/components/SwapSubmittedToast.tsx
new file mode 100644
index 0000000..5a16f29
--- /dev/null
+++ b/extension/popup/components/SwapSubmittedToast.tsx
@@ -0,0 +1,49 @@
+/**
+ * SwapSubmittedToast - Pill-shaped toast that drops down briefly and disappears.
+ * Figma: white bg, grey border, shadow, check icon + "Swap submitted"
+ */
+
+import { useEffect } from 'react';
+import { useStore } from '../store';
+import { CheckIcon } from './icons/CheckIcon';
+
+const TOAST_DURATION_MS = 3000;
+
+export function SwapSubmittedToast() {
+ const { swapSubmittedToastVisible, setSwapSubmittedToastVisible } = useStore();
+
+ useEffect(() => {
+ if (!swapSubmittedToastVisible) return;
+ const t = setTimeout(() => setSwapSubmittedToastVisible(false), TOAST_DURATION_MS);
+ return () => clearTimeout(t);
+ }, [swapSubmittedToastVisible, setSwapSubmittedToastVisible]);
+
+ if (!swapSubmittedToastVisible) return null;
+
+ return (
+
+
+
+
+
+
+ Swap submitted
+
+
+
+ );
+}
diff --git a/extension/popup/screens/HomeScreen.tsx b/extension/popup/screens/HomeScreen.tsx
index 87ad70a..faf75e6 100644
--- a/extension/popup/screens/HomeScreen.tsx
+++ b/extension/popup/screens/HomeScreen.tsx
@@ -28,6 +28,9 @@ import SettingsGearIcon from '../assets/settings-gear-icon.svg';
import PencilEditIcon from '../assets/pencil-edit-icon.svg';
import RefreshIcon from '../assets/refresh-icon.svg';
import ReceiptIcon from '../assets/receipt-icon.svg';
+import SwapIconAsset from '../assets/swap_icon.svg';
+import BaseIconAsset from '../assets/base_icon.svg';
+import { SwapSubmittedToast } from '../components/SwapSubmittedToast';
import './HomeScreen.tailwind.css';
@@ -327,6 +330,7 @@ export function HomeScreen() {
className="w-[357px] h-[600px] overflow-hidden relative"
style={{ backgroundColor: 'var(--color-home-fill)', color: 'var(--color-text-primary)' }}
>
+
{/* Scroll container */}
{/* Actions */}
-
+
+