Hello and welcome. This is a continuation of the tutorial on how to make a Chrome extension to convert USTT amounts on web pages into real money (ETH). See part 1 here:
Chrome Extensions
Extensions are a way to add functionality to Chromium-based browsers (Chrome, Brave, Opera, etc.). They can be used to enhance productivity, improve browsing experience, automate tasks, etc. More information here: https://developer.chrome.com/docs/extensions/mv3/overview/
On a technical level, an extension is a bunch of HTML/CSS/JavaScript wrapped up in a zip file with a manifest file included. For Chromium to be able to recognize and use the extension, the files must follow a pre-defined structure. The manifest file will specify:
plugin name, description, version, icon, etc.
which permissions the extension will use
which code files from the package the browser needs to use
Setting up the project
See also: tutorial on developer.chrome.com
First, we need a name for the extension. Let’s call it “Price in ETH”.
We create a directory anywhere in the local filesystem to hold the extension’s files. First file to add will be the manifest - manifest.json
:
{
"name": "Price in ETH",
"description": "Converts prices seen on web pages from US trash token ($) to real money (ETH)",
"version": "1.0",
"manifest_version": 3
}
The fields are pretty self-explanatory. We have the name, description, and version for the extension. Manifest version is the version of the Chrome manifest definition language. Current version is 3, and version 2 is obsolete. Here’s a link to the differences between v2 and v3 in case you want to geek out.
Now we have a blank extension. Let’s try to load it and see if Chrome recognizes it.
Go to Chrome menu → More Tools → Extensions
At the top, flip the tumbler to turn on Developer Mode
Press “Load unpacked” and select the directory you previously created (the one with manifest.json)
You should see the new extension loaded by Chrome
Adding code to the extension
Let’s create a background.js
file in the extension directory and register it in the manifest as the background service worker, i.e. the code that will run when the extension is loaded. We will need to add this to the manifest:
"background": {
"service_worker": "background.js"
}
We will also need permissions from the user for the extension to modify the active page. webNavigation
permission will allow us to subscribe to the event fired when the page is done loading; scripting
permission allows us to run scripts on the page.
"permissions": [
"webNavigation",
"scripting"
]
To make sure our extension can run on any website, let’s also add host permissions (*
means any hostname):
"host_permissions": [
"http://*/",
"https://*/"
]
Now the manifest file should look like this:
{
"name": "Price in ETH",
"description": "Converts prices seen on web pages from US trash token ($) to real money (ETH)",
"version": "1.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
},
"permissions": [
"webNavigation",
"scripting"
],
"host_permissions": [
"http://*/",
"https://*/"
]
}
In the background.js
file, we want to add a handler to the webNavigation.onCompleted event. This event fires when the page has been fully loaded, which is when we can start replacing the dollar amounts. When this happens, we want to execute the code that we tested in the console before. Let’s put it in a function called updatePage
.
chrome.webNavigation.onCompleted.addListener(details => {
chrome.scripting.executeScript({
target: { tabId: details.tabId },
function: updatePage,
});
});
function updatePage() {
// Magic happens here
}
chrome.webNavigation.onCompleted.addListener
takes a function as an argument and makes it run when navigation to a page has completed, i.e. all content on the page has been loaded. This suits us perfectly because we wouldn’t want to replace all dollar amounts on the page only to have more of them load later. To be able to add this listener, we requested the “webNavigation” permission in the manifest.
The handler function we pass into the call launches a script (this is why we got the “scripting” permission). The target is defined by the ID of the tab that has just loaded, passed to the handler via the details
parameter. This makes sure the script is executed on the correct tab.
The updatePage
function will contain the code that performs the actual search and replace. We will fill in this function next.
Updating the page
Now we can put all the code that we wrote in Part 1 into the updatePage
function:
async function updatePage() {
// 1. Find text nodes with dollar amounts
const allElems = document.getElementsByTagName('*');
const elemsArray = Array.from(allElems); // Convert to array
const elemsNoScripts = elemsArray.filter(e =>
e.tagName !== 'SCRIPT'); // Filter out script tags
const textNodes = elemsNoScripts.flatMap(e =>
Array.from(e.childNodes).filter(n =>
n.nodeType === Node.TEXT_NODE && // Filter out non-text nodes
n.textContent.indexOf('$') >= 0));
// 2. Fetch the price of Ether
if (!window.ethPrice) {
const apiUrl = 'https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD';
const response = await fetch(apiUrl);
const responseBody = await response.json();
window.ethPrice = responseBody['USD'];
}
// 3. Replace the dollar amounts with ETH amounts
for (const node of textNodes) {
let text = node.textContent;
for (let dollarPos = text.indexOf('$'); dollarPos >= 0; dollarPos = text.indexOf('$')) {
const spacePos = text.indexOf(' ', dollarPos);
const numberEndPos = spacePos >= 0 ? spacePos : text.length;
const dollarText = text.substring(dollarPos + 1, numberEndPos);
const dollarValue = Number.parseFloat(dollarText);
if (Number.isNaN(dollarValue)) {
break;
}
const ethValue = dollarValue / window.ethPrice;
const newText = text.substring(0, dollarPos) + 'Ξ' + ethValue.toFixed(4) + text.substring(numberEndPos);
text = newText;
}
node.textContent = text;
}
}
Some notable differences from how we called this code before:
We made the updatePage function async because we use asynchronous code to fetch the Ether price. Generally, any I/O (input/output) operation in JavaScript will be run asynchronously. This means that the execution engine does not have to wait for the I/O operation to complete and can continue running other code, until the result of the operation is obtained, and it can switch back to handling it. Examples of asynchronous operations: fetching remote web pages, loading content from disk, calling some external APIs.
Before loading the ETH price, we check if it is already stored in the global
window.ethPrice
variable. If it isn’t, we fetch the price from the remote API and store it there. The reason for this is that the handler may be called multiple times because some pages have content that loads asynchronously after the main page has loaded. To avoid querying the external pricing API several times for the same page, we store the result in a global variable.
After adding the code to the file, go the Extensions tab in Chrome and press the circular arrow reload button:
So now, if you go to ebay.com, you should see the prices in ETH.
You should be able to see the extension in your upper-right hand corner in the toolbar. It will be marked with a “P” because we haven’t defined a custom icon. If you want a custom icon, you will need to specify it in the manifest and include in your extension directory. See Chrome docs for more details.
If you want to temporarily disable the extension, you can simply deny it access to the website(s) by clicking its icon and selecting the “When You Click the Extension” option in the menu as shown below. This will make it so that the extension is inactive until you click it on the website of your choice.
If you decide to publish an extension to the Chrome Web Store, you can follow the steps here. We will not do it for our extension since it’s just for fun and learning, and anyone can read this substack and make such an extension for themselves 😜
Part 2 Conclusion
In Part 2 of the tutorial, we learned how to make a Chrome extension out of the experimental code we developed in Part 1. However, you may notice that it doesn’t work for all websites. Indeed, if you go to amazon.com, only some prices will be replaced, and others won’t. We will investigate and fix this issue in Part 3 that is coming soon!
As usual, if you have any questions or feedback, post in the comments or on Twitter.