Chrome extension to simulate Dapp interactions and record transactions. Available on the Chrome Webstore
Build a development bundle of the extension in watch mode:
yarn dev
The build output is written to public/build.
To enable the extension in Chrome, follow these steps:
- Open the Extension Management page by navigating to chrome://extensions.
- Enable Developer Mode by clicking the toggle switch at the top right of the page.
- Click the Load unpacked button and select the
zodiac-pilot/public
directory.
yarn build
The extension consists of three different interacting pieces:
- extension app: This is the main app rendering the iframe. The entrypoint to the app is launch.ts which is injected into pages running under the Zodiac Pilot host via a content script.
- background script: A service worker script that allows to hook into different Chrome events and APIs: src/background.ts
- injected script: Whenever we load any page in the the extension app iframe, we inject src/inject.ts into the page so that this script runs in the context of that page. The injection happens via the content script at src/contentScript.ts.
The different scripts communicate exclusively via message passing. Extension page and background script use chrome.runtime.sendMessage
while extension page and injected script talk via window.postMessage
.
Originally, we started out building the Pilot as an extension page, which are hosted under chrome-extension://<EXTENSION_ID>
. However extension pages are subject to some restrictions that make implementing certain integrations difficult, most notably:
- All extensions are sandboxed from each other, meaning that the MetaMask injected provider would not be available to an extension page.
- Extension pages have no access to Indexed DB, which is a dependency of Ganache. We might consider implementing a local forking mechanism using Ganache as an alternative to the existing Tenderly integration.
That's why the extension is now running under an external host, https://pilot.gnosisguild.org.
For allowing arbitrary pages to be loaded in our iframe we drop X-Frame-Options
and Content-Security-Policy
HTTP response headers for any requests originating from tabs showing our extension.
As we don't want to generally lift cross origin restrictions, we dynamically adjust the condition under which the declarativeNetRequest rule applies. In our background script, we track tabs running our extension and will apply the header removal only for requests originating from any of these tabs.
The problem: When the user navigates the Dapp, the address bar of the Zodiac Pilot should update accordingly. The browser back button should function as usual and when reloading the extension page the iframe should continue showing the original page. Since browsers block access to foreign origin iframes we need to leverage Chrome extension super powers to detect navigation events in the iframe.
The solution: We listen to chrome.tabs.onUpdated
in the background script and notify the content script about it via a message, which relays the message to the extension app.
This relaying is necessary because a background script can not directly talk to an externally hosted app.
For retrieving the new iframe location, we then post a message to the injected script in the iframe window, which will send us the response in another message.
When the simulator iframe opens any page, we inject the build/inject.js script as a node into the DOM of the Dapp.
The injected script then runs in the context of the Dapp and injects an EIP-1193 compatible API at window.ethereum
.
The injected provider forwards all request
calls to the parent extension page via window.postMessage
where they are recorded and executed in a fork of the connected network.
We also subscribe to messages sent via the safe-apps-sdk. This enables instant and smart-account optimized connections to Safe-compatible apps.
When the provider we inject into the Dapp iframe receives a transaction request, we record it and simulate the transaction in a fork of the target network, impersonating the Safe. That way the app can continue communicating with the fork network, so that a whole session of multiple transactions can be recorded before anything is signed and submitted to the real chain.
Tenderly provides rich debugging capabilities, which help in understanding the exact effects of each recorded transaction before actually signing anything. Fresh forks are created via Tenderly's Simulation API and each fork will have its own JSON RPC URL.
Apps commonly make read-only JSON-RPC requests to providers such as Infura or Alchemy rather than going through the EIP-1193 provider injected by the wallet. Once the network has been forked for simulating recorded transaction such requests should reflect the state of the fork as well.
This first requires detecting which of the JSON-RPC endpoints used by an app actually serve the forked network.
The webRequest
extension API allows inspecting the body of each outgoing request.
If a requests looks like a JSON-RPC request we probe the target endpoint for requestChainId
.
Once the network is forked we update declarativeNetRequest
xhr redirection rules for endpoints identified as serving the forked network.
All of this is implemented as part of the background service worker.
A batch of recorded transaction can finally be submitted as a multi-send transaction. Zodiac Pilot can be configured to submit transactions directly to the Safe if the Pilot account is an owner or delegate, or to route the transaction through Zodiac mods. This is implemented in WrappingProvider. It currently supports the Roles and Delay mods.