Create a webextension that interacts with Trezor devices using TypeScript and Webpack
Install Dependencies
To install the required packages, run the following commands in your project directory:
- Install the Trezor Connect WebExtension library:
npm install @trezor/connect-webextension
- Install development dependencies for TypeScript, Webpack, and other tools:
npm install --save-dev babel-loader @babel/preset-typescript webpack html-webpack-plugin copy-webpack-plugin typescript @types/chrome
Create a src
directory
Inside your src
directory create a file service-worker.ts
that will load @trezor/connect-webextension
:
/// <reference lib="webworker" />
// Reference the Web Worker library, allowing TypeScript to recognize service worker globals
// Import using ES6 module TrezorConnect and the DEVICE_EVENT constant from the Trezor Connect WebExtension package
import TrezorConnect, { DEVICE_EVENT, DeviceEventMessage } from '@trezor/connect-webextension';
// URL of the Trezor Connect
const connectSrc = 'https://connect.trezor.io/9/';
chrome.runtime.onInstalled.addListener((details: chrome.runtime.InstalledDetails) => {
console.log('details', details);
// Initialize Trezor Connect with the provided manifest and settings
TrezorConnect.init({
manifest: {
email: 'meow@example.com',
appUrl: 'https://yourAppUrl.com/',
},
transports: ['WebUsbTransport'], // Transport protocols to be used
connectSrc,
_extendWebextensionLifetime: true, // Makes the service worker in @trezor/connect-webextension stay alive longer.
});
// Event listener for messages from other parts of the extension
// This code will depend on how you handle the communication between different parts of your webextension.
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.action === 'getAddress') {
TrezorConnect.getAddress({
showOnTrezor: true,
path: "m/49'/0'/0'/0/0",
coin: 'btc',
}).then(res => {
sendResponse(res); // Send the response back to the sender
});
// Return true to indicate you want to send a response asynchronously
return true;
} else if (message.action === 'getFeatures') {
TrezorConnect.getFeatures().then(res => {
sendResponse(res); // Send the response back to the sender
});
// Return true to indicate you want to send a response asynchronously
return true;
}
});
});
So far your directory looks like:
node_modules
src
service-worker.ts
package-lock.json
package.json
tsconfig.json
Create tab that will communicate with service-worker
The library @trezor/connect-webextension
is running in the service-worker because it allows the webextension to have
a persistent reference.
So if we want our front-end part of the webextension to be able to communicate with service-worker one way we can do it
is by using chrome.runtime.sendMessage
and chrome.runtime.onMessage.addListener
. Nontheless you can find other
ways how to do it in: https://developer.chrome.com/docs/extensions/develop/concepts/messaging
- Create file
src/connect-manager.html
:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<button id="get-features" data-testid="get-features">Get features</button>
<button id="get-address" data-testid="get-address">Get Address</button>
<div id="result"></div>
<script src="./connect-manager.js" type="module"></script>
</body>
</html>
- Create file
src/connect-manager.ts
:
const result = document.getElementById('result');
const getAddress = document.getElementById('get-address');
if (getAddress) {
getAddress.addEventListener('click', () => {
// Send a message to the background script (service worker) with the action 'getAddress'
chrome.runtime.sendMessage({ action: 'getAddress' }, response => {
// Check if the response indicates success
if (response.success) {
console.info(response); // Log the successful response to the console
// Display the response in the 'result' element on the page
if (result) {
result.innerText = JSON.stringify(response);
}
} else {
console.error(response); // Log the error response to the console
// Display an error message in the 'result' element on the page
if (result) {
result.innerText = 'Error: ' + response.error;
}
}
});
});
}
const getFeatures = document.getElementById('get-features');
if (getFeatures) {
getFeatures.addEventListener('click', () => {
// Send a message to the background script (service worker) with the action 'getFeatures'
chrome.runtime.sendMessage({ action: 'getFeatures' }, response => {
// Check if the response indicates success
if (response.success) {
console.info(response); // Log the successful response to the console
// Display the response in the 'result' element on the page
if (result) {
result.innerText = JSON.stringify(response);
}
} else {
console.error(response); // Log the error response to the console
// Display an error message in the 'result' element on the page
if (result) {
result.innerText = 'Error: ' + response.error;
}
}
});
});
}
Create webextension Popup window to be able to open the tab connect-manager.html
- Create file
src/popup.html
:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<button id="tab">Connect manager</button>
<script src="./popup.js" type="module"></script>
</body>
</html>
- Create file
src/popup.ts
that will just open connect-manager.html:
document.addEventListener('DOMContentLoaded', () => {
const newTabButton = document.getElementById('tab');
if (newTabButton) {
newTabButton.addEventListener('click', () => {
chrome.tabs.create({ url: 'connect-manager.html' });
});
}
});
Create manifest.json
for the webextension:
{
"name": "my-trezor-extension",
"version": "1.0.0",
"manifest_version": 3,
"action": {
"default_popup": "popup.html"
},
"background": {
"service_worker": "service-worker.js"
},
"permissions": ["scripting"],
"host_permissions": [
"*://connect.trezor.io/9/*"
]
}
Configure webpack
to build the project:
- Create
webpack.config.js
file that contains:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
entry: {
popup: './src/popup.ts',
'service-worker': './src/service-worker.ts',
'connect-manager': './src/connect-manager.ts',
},
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js',
},
resolve: {
extensions: ['.ts', '.js'],
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader'],
},
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-typescript'],
},
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './src/popup.html',
filename: 'popup.html',
chunks: ['popup'],
}),
new HtmlWebpackPlugin({
template: './src/connect-manager.html',
filename: 'connect-manager.html',
chunks: ['connect-manager'],
}),
new CopyWebpackPlugin({
patterns: [{ from: 'src/manifest.json', to: 'manifest.json' }],
}),
],
mode: 'development',
devtool: 'inline-source-map',
};
Load the Web Extension in Chrome
- Open Google Chrome and navigate to the extensions page by entering the following URL in the address bar:
chrome://extensions
- Enable Developer Mode:
In the top right corner of the extensions page, toggle the "Developer mode" switch to enable it.
- Load Unpacked Extension:
Click on the "Load unpacked" button and select the directory where your web extension is located. This should be:
my-trezor-webextension/build
Your extension should now be loaded and visible in the list of extensions.