CoinChoice Snap: Top Up Gas With Any Currency
An ETHDenver Hackathon Winner
MetaMask Snaps is the roadmap to making MetaMask the most extensible wallet in the world. As a developer, you can bring your features and APIs to MetaMask in totally new ways. Web3 developers are the core of this growth and this series aims to showcase the novel MetMask Snaps being built today.
CoinChoice Snap
Snap Repo: https://github.com/coinchoice/coinchoice/
Why did you build it?
CoinChoice was developed out of a small and tedious yet widely common problem whereby a Web3 user may own a wallet with tokens of value but no gas currency to perform any transactions.
This problem is highlighted for users with fractionalized liquidity. A user managing multiple wallets and engaging opportunities across blockchain ecosystems will find themselves splitting their gas tokens across each of their wallets, reserving capital that would otherwise be for their investments in order to facilitate their digital assets and transactions. Beyond gas management, tokens transferred to a network where gas isn’t owned require the user to obtain gas before any further actions can be taken. Typically this involves the user accessing a Centralised Exchange to obtain and withdraw gas – subjecting the user to CEX withdrawal fees.
Fundamentally, the experience is flawed. There should not be a dependency on centralised exchanges for users to access Web3. Users should not have to worry about managing their gas and instead should be able to simply interact with decentralised systems. This is not to say that gas should not be required. Gas serves a fantastic purpose of rewarding operators of a network, as well as preventing spam/ddos. Gas should simply be more flexible. If a dApp or game developer has a native token, this token should suffice as a currency to pay for gas.
With this philosophy in mind, CoinChoice was developed.
CoinChoice lives within the user’s browser, supplementing every transaction with the ability to manage gas in a currency that the user desires. If the User prefers to hold USDC over ETH, then gas can be paid in USDC.
CoinChoice’s MetaMask Snap further complements the experience. Directly within the MetaMask Browser Extension, users can top up their ETH in exchange for their chosen gas currency. This turns MetaMask into a solution that replaces the visit to a Centralised Exchange, consolidating and simplifying the Web3 experience, and enabling new Web3 users to familiarise themselves with a subset of systems without becoming overwhelmed by the growing abundance of services and platforms.
Walk us through the technical implementation?
CoinChoice is a MetaMask & Browser Extension that gives you choice over which coins are used to pay gas.
Packages
-
backend
: A NestJS Web Server responsible for- simulating transactions against the Tenderly API and 0x Quote API to yield gas estimates in chosen currency
- receive and syndicate messages between the client’s Metamask Snap and Browser Extension
- receive meta-tx to forward to relayer contracts
-
browser-extension
: A Browser Extension powered by the Plasmo Framework- intercepts RPC requests to MetaMask in order to inject swaps to ETH for gas financing
- offers a popup where Gas Coin can be selected
- offers a popup for Token management powered by Cypher Onboarding SDK
- all data is submitted to backend service where Blockchain Interactions and Simulations are managed
-
contracts
: Smart Contracts that power relaying meta-transactions to facilitate swaps on 0x Protocol. -
frpc
: A package to leverage the Fluence Network’s fRPC Gateway -
meta-mask
: A package that contains a React.js Site and assocated MetaMask Snap.- The Snap is responsible for offering ETH Swaps directly within the MetaMask interface, by showing a message “Need ETH?”
- The Snap works by making a request to the backend so that a message is forwarded to the client’s browser-extension, enabling an experience where the MetaMask Snap can initiate the Browser Extension
- The associated Site is used to enable MetaMask Snap Installation
Technical Setup
CoinChoice is browser extension that supplements transactions with the opportunity to top up gas with a chosen currency.
The browser extension is designed to prompt the user to pay gas in their selected currency prior to each transaction.
If a user accepts, a meta-transcation is captured to convert an estimated amount of the chosen token to ETH.
This meta-transaction is forwarded to the backend service that forwards it to the relayer Smart Contract where it is executed.
Once the meta-transaction is settled, the user’s original transaction is prompted and accessible due to the deposit of the appropriate amount of gas.
The MetaMask Snap is designed to forward a transaction that would otherwise be isolated within the MetaMask extension to the CoinChoice Extension. It does this by
- submitting a request with the transaction details to the backend serivce
- the backend will then forward the transaction data to the browser extension over a websocket connection
- once the browser extension receives this websocket message, it initiates its flow of topping up an automatically estimated amount of gas in exchange for a selected currency.
Backend as a Message Bus
CoinChoice took the approach of using a backend as a message bus between the two frontend components due to the sandboxed nature of the MetaMask Snap.
The Snap code isolation prevents communication to frontend components that reside in contexts outside of the MetaMask extension.
However, Snaps can facilitate server-side requests, and therefore a backend service capable of managing user sessions can be used to pass messages between the frontend contexts.
An example of this can be demonstrated through our source code.
Within the MetaMask Snap, we submit a request to the server.
See https://github.com/Coinchoice/coinchoice/blob/main/packages/meta-mask/packages/snap/src/index.ts#L12
import { OnTransactionHandler } from '@metamask/snaps-types';
import { heading, panel, text } from '@metamask/snaps-ui';
export interface JsonRpcRequest {
id: string | undefined;
jsonrpc: '2.0';
method: string;
params?: Array<any>;
}
const onTriggerAPI = async (transaction: JsonRpcRequest) => {
await fetch(`https://api.coinchoice.link/metamask`, {
method: 'POST',
body: JSON.stringify(transaction),
headers: { 'Content-Type': 'application/json' },
})
.then((res) => {
if (!res.ok) {
throw new Error('Bad response from server');
}
return res.json();
})
.then((json) => console.log(json))
.catch((err) => console.error(err));
};
// Handle outgoing transactions
export const onTransaction: OnTransactionHandler = async ({ transaction }) => {
// trigger browser extension for swap
const response = await onTriggerAPI(transaction);
// this is shown if no transaction is triggered
if (typeof transaction.data === 'string' && transaction.data !== '0x') {
return {
content: panel([
heading('Need ETH for gas?'),
text('Receive ETH gasless for any currency with Coinchoice!'),
]),
};
}
const currentGasPrice = await ethereum.request<string>({
method: 'eth_gasPrice',
});
const transactionGas = parseInt(transaction.gas as string, 16);
const currentGasPriceInWei = parseInt(currentGasPrice ?? '', 16);
const maxFeePerGasInWei = parseInt(transaction.maxFeePerGas as string, 16);
const maxPriorityFeePerGasInWei = parseInt(transaction.maxPriorityFeePerGas as string, 16);
const gasFees = Math.min(
maxFeePerGasInWei * transactionGas,
(currentGasPriceInWei + maxPriorityFeePerGasInWei) * transactionGas
);
const transactionValueInWei = parseInt(transaction.value as string, 16);
const gasFeesPercentage = (gasFees / (gasFees + transactionValueInWei)) * 100;
return {
content: panel([
heading('Need ETH for gas?'),
text(
`As setup, you are paying **${gasFeesPercentage.toFixed(
2
)}%** in gas fees for this transaction.`
),
text(`Also: **${response}**`),
]),
};
};
Then, within the the server, we use the from
parameter within the received transaction payload to identify the wallet, and their associated session identifer.
See: https://github.com/Coinchoice/coinchoice/blob/main/packages/backend/src/app.controller.ts#LL36-L49C3
@Post('metamask')
async metamask(@Body() data: any) {
const address = data.from;
const wallet = await this.walletService.findOne(address);
if (wallet) {
console.log(`clientId for ${address} is: ${wallet.clientId}`);
if (wallet.clientId) {
this.appGateway.server.to(wallet.clientId).emit('onMetamask', {
msg: data,
content: 'Hello from the backend!',
});
} else throw new NotFoundException('ClientId not found for this wallet');
} else throw new NotFoundException('Wallet not found');
}
Finally, within the Browser Extension, we connect to the backend service over a websocket connection, whereby the connection itself establishes the user session identifer. See: https://github.com/Coinchoice/coinchoice/blob/main/packages/browser-extension/src/contents/main.ts#L107
const socket = io(API_HOST);
socket.on('connect', function () {
console.log('CS SOCKET: Connected');
});
socket.on('events', function (data) {
console.log('CS SOCKET: event', data);
});
socket.on('onMessage', async function (data) {
console.log('CS SOCKET: onMessage', data);
if (data.msg.startsWith('Hello client')) {
const [_, clientId] = data.msg.split('#');
wallet.clientId = clientId;
facade.setWallet(wallet);
try {
await busPromise('connect-wallet', { wallet });
} catch (e) {
console.log('CS SOCKET ERROR: onMessage - connect wallet');
console.error(e);
}
}
});
socket.on('onMetamask', async function (data) {
console.log('CS SOCKET: onMetamask', data);
// On Metamask, open the Notification.
const { msg: tx }: { msg: TxRequest } = data;
// Cancel the original tx
await facade.waitForSignature({ method: 'need_eth', params: [tx] });
});
Within this code snippet, we can seee how the transaction data received over the onMetamask
message initiates the prompt for signature (meta-transaction).
Website for Snap Installation
In order to install a MetaMask Snap, a website must call the appropriate JSON RPC endpoint.
/**
* Connect a snap to MetaMask.
*
* @param snapId - The ID of the snap.
* @param params - The params to pass with the snap to connect.
*/
export const connectSnap = async (
snapId: string = defaultSnapOrigin,
params: Record<'version' | string, unknown> = {},
) => {
await window.ethereum.request({
method: 'wallet_requestSnaps',
params: {
[snapId]: params,
},
});
};
The CoinChoice website for installing the MetaMask Snap hosted at https://cchoice.xyz/ took inspiration from the MetaMask Team’s repository: https://github.com/ziad-saab/snappy-recovery
Getting started with CoinChoice
To get started, you can install the testnet enabled version of CoinChoice using the guide found at: https://app.coinchoice.link/
What are the next steps if you would implement this?
CoinChoice has been implemented and tested for the Goerli Ethereum Testnet, although quite experimental.
Assuming incentives are aligned to enable the continued development of CoinChoice, the next steps would be to enable CoinChoice for all major EVM-compatible blockchains and then get the Browser Extension in the hands of as many users as possible.
Exposure and distribution are always a challenge, and will therefore require investment into community development and partnerships with Web3 platforms, especially cross-chain platforms, seeking to alleviate this concern around gas requirements.
Once the application is used and feedback is collected from end users, bug fixes and other improvements can be determined and applied to further improve the end-user experience.
As for future developments, an early consideration for Account Abstraction that was not implemented due to issues associated with caller context may be re-considered. CoinChoice’s core functionality could also be modularised to allow dApp developers to simply embed gas management into their applications.
Can you tell us a little bit about yourself and your team?
The CoinChoice team is quite diverse, coming from very different walks of life, but sharing the same common objective – to create awesome software for a decentralised future.
I’m Ryan, the engineer that created the CoinChoice Browser Extension and supported the solution’s architecture. I sought team members through EthDenver’s Discord. It was in Denver that all team members actually met in person. Each of us has a unique skill set to bring to the table that enabled us to work very well together. Disputes and problems were faced head-on, and blame and success were shared equally between all team members. Through the hacking experience, the team can safely say that it was a pleasure working together. If there are opportunities that arise out of this endeavour, we’d all be available to support each other.
As for the team’s experience, Achim is the best example of an engineer that intersects economics and mathematics. Achim has developed a DeFi protocol named 1Delta.io that is pushing the boundaries of what is possible with leveraged digital asset management. This experience was vital in developing and deploying CoinChoice’s Smart Contracts with production-grade security.
Andre comes from a Web2 background and was therefore the perfect candidate to develop and manage server-side processes that are imperative to managing and executing meta-transactions, as well as other application data. Andrew leads Web Eleven, a Web2 agency in Brazil.
Christie supported the team non-technically through deck creation, branding, content, and project management. Christie leads Web Doodle, a Web2 agency in Australia.
As for me, I have a background in solutions architecture and engineering. I direct a Web3 startup named Usher.so where we’ve developed a partner programs platform, and are subsequently exploring new decentralised infrastructure to support such a platform. While being responsible for front-end development and solutions architecture, I managed to bring my experience coordinating a project and team to the EthDenver hackathon.
What opportunities do you see with MetaMask Snaps and the possibilities it unlocks for the Web3 space?
MetaMask is positioning itself to be among the first applications a user interacts with to engage Web3. By enabling the extensibility of the user experience, MetaMask allows developers to introduce their value proposition at an early and familiar touchpoint. This becomes more important once there is a consolidation in gateways to Web3 specifically for the next generation of Web3 users. This is analogous to the consolidation of gateways to the internet we all are already familiar with, whereby end user facing applications like Social Networks or Search Engines have become the first point of interaction for many internet users.
MetaMask Snaps enables more contextual experiences. An example outcome could be a Snap that functions differently based on the dApp that the MetaMask Wallet is interacting with. By enabling developers to build upon the overlay user interface to Web3, more interesting developments can be incorporated that fundamentally change the way Web3 is interacted with. Unlike a dApp that enables a UI for a specific protocol, Snap developers can alter the way all protocols are engaged, as the innovation lives within the wallet that enables these interactions.
Finally, MetaMask Snaps that aim to increase the transparency of transactions, simplify Web3 interactions, and provide additional security to end-users will fundamentally make Web3 safer and more viable for a vast number of users.
Any advice you would like to share with developers keen to try MetaMask Snaps?
Refer to raw source code that implements MetaMask Snaps to gather deeper insight into how these new capabilities function. The MetaMask Snaps team has done a great job in making public well-developed code that demonstrates MetaMask Snaps within a project. While the documentation gives an overview of the features, the source code illustrates how these features are implemented and manipulated.
Reduce expectations for what is capable within the Snaps Framework during early development stages. While MetaMask Snaps will eventually enable the full extensibility of MetaMask, access to different components within the Snaps Framework will be released gradually. There’s a balancing act that MetaMask will need to consider when maximising end-user security, so some features included in early builds may be re-considered. If your solution required more access to the user’s browser context, consider referring to the CoinChoice source code to develop a Browser Extension that is triggered by an action within a MetaMask Snap.
Reach out to the MetaMask team. The team at MetaMask is highly engaged to deliver on extensibility and are looking for feedback and opportunities to better deliver on this value proposition.
Building with MetaMask Snaps
To get started with MetaMask Snaps:
- Checkout the developer docs
- Install MetaMask Flask
- Check out a MetaMask Snaps guide
- Stay connected with us on Twitter, GitHub discussions, and Discord
Keep an eye out for our team at the next hackathon in an area near you! Happy BUIDLing ⚒️
Disclaimer: MetaMask Snaps are generally developed by third parties other the ConsenSys Software. Use of third-party-developed MetaMask Snaps is done at your own discretion and risk and with agreement that you will solely be responsible for any loss or damage that results from such activities. ConsenSys makes no express or implied warranty, whether oral or written, regarding any third-party-developed MetaMask Snaps and disclaims all liability for third-party developed Snaps. Use of blockchain-related software carries risks, and you assume them in full when using MetaMask Snaps.
Keep reading our latest stories
Developers, security news, and more