Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Koniverse/SubWallet-Extension/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Content scripts act as a bridge between web pages and the SubWallet Extension background service. They enable dApps to interact with the wallet through the injected provider API while maintaining security boundaries.
Architecture
Web Page (dApp)
|
v
Injected Provider (window.injectedWeb3)
|
v
window.postMessage
|
v
Content Script
|
v
chrome.runtime.Port
|
v
Background Service
Content Script Entry Point
Location: packages/extension-koni/src/content.ts
import type { Message } from '@subwallet/extension-base/types';
import { TransportRequestMessage } from '@subwallet/extension-base/background/types';
import {
MESSAGE_ORIGIN_CONTENT,
MESSAGE_ORIGIN_PAGE,
PORT_CONTENT
} from '@subwallet/extension-base/defaults';
export class ContentHandler {
port?: chrome.runtime.Port;
isShowNotification = false;
isConnected = false;
// Get the port to communicate with the background
getPort(): chrome.runtime.Port {
if (!this.port) {
const port = chrome.runtime.connect({ name: PORT_CONTENT });
const onMessageHandler = this.onPortMessageHandler.bind(this);
const disconnectHandler = () => {
this.onDisconnectPort(port, onMessageHandler, disconnectHandler);
};
this.port = port;
this.port.onMessage.addListener(onMessageHandler);
this.port.onDisconnect.addListener(disconnectHandler);
}
return this.port;
}
constructor() {
this.redirectIfPhishingProm();
window.addEventListener('message', this.onPageMessage.bind(this));
}
}
const contentHandler = new ContentHandler();
Source: packages/extension-koni/src/content.ts:26-130
Key Components
1. Port Management
The content script maintains a persistent connection to the background service.
getPort(): chrome.runtime.Port {
if (!this.port) {
const port = chrome.runtime.connect({ name: PORT_CONTENT });
const onMessageHandler = this.onPortMessageHandler.bind(this);
const disconnectHandler = () => {
this.onDisconnectPort(port, onMessageHandler, disconnectHandler);
};
this.port = port;
this.port.onMessage.addListener(onMessageHandler);
this.port.onDisconnect.addListener(disconnectHandler);
}
return this.port;
}
Source: packages/extension-koni/src/content.ts:32-47
Port Lifecycle:
- Connect: Establish connection when first needed
- Listen: Attach message and disconnect handlers
- Communicate: Send/receive messages via port
- Disconnect: Clean up on disconnect or error
- Reconnect: Automatically handled on next message
2. Message Handling from Background
Messages from the background are forwarded to the page.
onPortMessageHandler(data: {id: string, response: any}): void {
const { id, resolve } = handleRedirectPhishing;
if (data?.id === id) {
// Handle phishing check response
resolve && resolve(Boolean(data.response));
} else {
// Forward all other messages to the page
window.postMessage({
...data,
origin: MESSAGE_ORIGIN_CONTENT
}, '*');
}
}
Source: packages/extension-koni/src/content.ts:50-58
3. Message Handling from Page
Messages from the injected provider are forwarded to the background.
onPageMessage({ data, source }: Message): void {
// Only allow messages from our window, by the inject
if (source !== window || data.origin !== MESSAGE_ORIGIN_PAGE) {
return;
}
try {
this.isConnected = true;
this.getPort().postMessage(data);
} catch (e) {
console.error(e);
if (!this.isShowNotification) {
console.log('The SubWallet extension is not installed.');
addNotificationPopUp();
this.isShowNotification = true;
setTimeout(() => {
this.isShowNotification = false;
}, 5000);
}
}
}
Source: packages/extension-koni/src/content.ts:75-97
Security Checks:
- Only processes messages from the same window
- Verifies message origin matches
MESSAGE_ORIGIN_PAGE
- Prevents message injection from other sources
4. Port Disconnection Handling
onDisconnectPort(
port: chrome.runtime.Port,
onMessage: (data: {id: string, response: any}) => void,
onDisconnect: () => void
): void {
const err = checkForLastError();
if (err) {
console.warn(`${err.message}, port is disconnected.`);
}
port.onMessage.removeListener(onMessage);
port.onDisconnect.removeListener(onDisconnect);
this.port = undefined;
}
function checkForLastError() {
const { lastError } = chrome.runtime;
if (!lastError) {
return undefined;
}
// Repair incomplete error object (eg chromium v77)
return new Error(lastError.message);
}
Source: packages/extension-koni/src/content.ts:61-72,15-24
Message Origins
The content script uses specific origin identifiers for security:
const MESSAGE_ORIGIN_PAGE = 'koni-page'; // From injected provider
const MESSAGE_ORIGIN_CONTENT = 'koni-content'; // From content script
Source: packages/extension-base/src/defaults.ts:20-21
Message Flow:
dApp calls window.injectedWeb3['subwallet-js'].enable()
|
v
Injected provider creates message with MESSAGE_ORIGIN_PAGE
|
v
window.postMessage(message)
|
v
Content script receives (validates origin)
|
v
Content script forwards via port.postMessage()
|
v
Background processes request
|
v
Background sends response via port.postMessage()
|
v
Content script receives response
|
v
Content script forwards via window.postMessage() with MESSAGE_ORIGIN_CONTENT
|
v
Injected provider receives and resolves promise
Phishing Protection
The content script implements phishing detection on page load.
redirectIfPhishingProm(): void {
new Promise<boolean>((resolve, reject) => {
handleRedirectPhishing.resolve = resolve;
handleRedirectPhishing.reject = reject;
const transportRequestMessage: TransportRequestMessage<'pub(phishing.redirectIfDenied)'> = {
id: handleRedirectPhishing.id,
message: 'pub(phishing.redirectIfDenied)',
origin: MESSAGE_ORIGIN_PAGE,
request: null
};
this.getPort().postMessage(transportRequestMessage);
}).then((gotRedirected) => {
if (!gotRedirected) {
console.log('Check phishing by URL: Passed.');
}
}).catch((e) => {
console.warn(`Unable to determine if the site is in the phishing list: ${(e as Error).message}`);
});
}
Source: packages/extension-koni/src/content.ts:100-120
Phishing Flow:
- Content script loads on every page
- Immediately sends phishing check request to background
- Background checks URL against phishing database
- If malicious, background redirects to warning page
- If safe, page loads normally
Error Handling
Connection Errors
try {
this.isConnected = true;
this.getPort().postMessage(data);
} catch (e) {
console.error(e);
if (!this.isShowNotification) {
console.log('The SubWallet extension is not installed.');
addNotificationPopUp();
this.isShowNotification = true;
// Prevent notification spam
setTimeout(() => {
this.isShowNotification = false;
}, 5000);
}
}
Source: packages/extension-koni/src/content.ts:81-96
Error Scenarios:
- Extension disabled or uninstalled
- Background service crashed
- Port disconnected unexpectedly
- Message posting failed
Notification Throttling
The content script prevents notification spam with a flag:
isShowNotification = false;
// Show notification
if (!this.isShowNotification) {
addNotificationPopUp();
this.isShowNotification = true;
// Reset after 5 seconds
setTimeout(() => {
this.isShowNotification = false;
}, 5000);
}
This ensures users don’t see repeated error notifications within 5 seconds.
Injected Provider Integration
The content script works with the injected provider API:
Injected at: Page load by extension manifest
Available APIs:
window.injectedWeb3['subwallet-js'] - Polkadot/Substrate API
window.injectedWeb3['subwallet'] - Legacy API
- EVM provider APIs
Example Provider Call:
// On the web page
const provider = window.injectedWeb3['subwallet-js'];
// Enable wallet
await provider.enable('My dApp');
// Get accounts
const accounts = await provider.accounts.get();
// Sign message
const signature = await provider.signer.signRaw({
address: accounts[0].address,
data: '0x1234',
type: 'bytes'
});
Each call goes through:
- Injected provider → window.postMessage
- Content script → chrome.runtime.Port
- Background service → process request
- Background → chrome.runtime.Port
- Content script → window.postMessage
- Injected provider → resolve promise
Security Considerations
Message Validation
// Only allow messages from our window
if (source !== window || data.origin !== MESSAGE_ORIGIN_PAGE) {
return;
}
Protections:
- Validates message source is the current window
- Checks origin matches expected value
- Prevents cross-origin message injection
- Blocks iframe message spoofing
Isolated Execution Context
Content scripts run in an isolated world:
- Cannot directly access page JavaScript variables
- Cannot be accessed by page JavaScript
- Can only communicate via window.postMessage
- Has access to Chrome extension APIs
- Has access to page DOM
Best Practices
- Origin Validation: Always validate message origin
- Error Handling: Gracefully handle port disconnections
- Notification Management: Prevent notification spam
- Port Lifecycle: Clean up listeners on disconnect
- Security: Never expose extension APIs to page context
Common Patterns
Handling Port Reconnection
getPort(): chrome.runtime.Port {
if (!this.port) {
// Create new connection
const port = chrome.runtime.connect({ name: PORT_CONTENT });
// Setup handlers
this.port = port;
this.port.onMessage.addListener(...);
this.port.onDisconnect.addListener(...);
}
return this.port;
}
The getPort() method automatically creates a new connection if needed.
Message Filtering
// Handle specific messages differently
if (data?.id === specialMessageId) {
// Handle special case
handleSpecialMessage(data);
} else {
// Forward to page
window.postMessage({ ...data, origin: MESSAGE_ORIGIN_CONTENT }, '*');
}
Debugging
Console Messages
// Phishing check passed
console.log('Check phishing by URL: Passed.');
// Connection error
console.error(e);
console.log('The SubWallet extension is not installed.');
// Port disconnect
console.warn(`${err.message}, port is disconnected.`);
- Open DevTools on the web page
- Go to Sources → Content Scripts
- Find SubWallet content script
- Set breakpoints in message handlers
- Inspect messages in console