How to build a Solidity Browser Compiler with React and Next.js

9 min read
Last updated 17 Oct 2022
Solidity Browser Compiler

Introduction

In the blockchain industry, most dApps (decentralized applications) run on the Ethereum network, which allows users to execute smart contracts on the Ethereum Virtual Machine (EVM).

To do this, developers first have to compile their source code to bytecode using a compiler like Solc, but it can be quite difficult to set up on a React app without a backend.

To better understand and leverage this aspect, we're building a Solidity browser compiler using TypeScript and Next.js that will allow us to easily compile in our browsers any Solidity source code, without having to implement a full-fledged backend.

A picture is worth more than a thousand words, so here's a GIF that showcases what we are going to build:

Browser Compiler Gif

Let's take a look at how it works!

What we will be building

In the course of this tutorial, we will develop a Solidity compiler that works on the browser, and you will learn how to create a frontend-only application by using TypeScript and Next.js. We will also cover how types work and why we use them.

If you are familiar with web development languages and/or React, then you can comfortably get started. At the end of this tutorial, you should have a solid understanding of how to build a Solidity compiler on the browser with Next.js and the Solc compiler.

Setting up your development environment

First, create a Next.js app. Open a terminal and execute:

yarn create next-app solidity-browser-compiler --typescript

After it finishes, we can now install the solc dependency using yarn:

yarn add solc

Then start a development server by running

yarn dev

This will run a development server on http://localhost:3000/ and watch for changes to TS files, compiling them automatically as you work. At any time you can open up your editor of choice and make some changes and you will see those changes reflected immediately in your browser as you save them!

Now that we have a working Next.js app it's time to implement our compiler.

Setting up the browser compiler

Deciding to use a front-end only implementation to compile Solidity is an unusual choice since most of existing implementations are done on a back-end, but sometimes it could be useful if you need to dynamically create a smart contract on your dApp, and you don't need a full back-end for other purposes (other than this).

What we would like to explore here is how one could go about building a front-end only compiler: the main idea is to execute solc by running it on web worker in its own thread, gather output results (bytecode and contract ABI), and returns them to the user.

Let's create an empty web worker:

//components/sol/solc.worker.ts

self onmessage = (event) => {
    //we will compile our source code here
};

Why use a web worker? Compiling Solidity is a heavy task and we don't want to freeze the browser while it compiles, so we run it on a separate thread. Time to add the compiler logic!

//components/sol/solc.worker.ts
...
const contractCode = event.data.contractCode;
const sourceCode = {
    language: 'Solidity',
    sources: {
        contract: { content: contractCode }
    },
    settings: {
        outputSelection: { '*': { '*': ['*'] } }
    }
};
const compiler = wrapper((self as any).Module);
const result = JSON.parse(compiler.compile(JSON.stringify(sourceCode)));

Here we simply use the Solc compiler, and we can choose which output to get; for this tutorial let's just get everything with the * operator, but it's customizable if you need a lighter load. Later on, we will send a Message to this web worker, which will contain the contract source code so we can get the final result.

The final code looks like this:

//components/sol/solc.worker.ts
importScripts('https://binaries.soliditylang.org/bin/soljson-latest.js');
import wrapper from 'solc/wrapper';

self.onmessage = (event) => {
    const contractCode = event.data.contractCode;
    const sourceCode = {
        language: 'Solidity',
        sources: {
            contract: { content: contractCode }
        },
        settings: {
            outputSelection: { '*': { '*': ['*'] } }
        }
    };
    const compiler = wrapper((self as any).Module);
    self.postMessage({
        output: JSON.parse(compiler.compile(JSON.stringify(sourceCode)))
    });
};

If you are using TypeScript, you will notice that there is a compilation error. That's because we didn't include any typing declarations for Solc. At the moment, they aren't available as a package, so we will have to write our own declaration file:

//components/sol/dec.d.ts
declare module "solc/wrapper";
declare function importScripts(...urls: string[]): void;

Now we can test the compiling process by invoking a new solc.worker web worker, but first let's declare some typings that we will need later to use the compiler result:

//components/sol/compiler.ts
interface AbiIO {
    indexed?: boolean;
    internalType: string;
    name: string;
    type: string;
}

interface Abi {
    input: AbiIO[];
    output: AbiIO[];
    name: string;
    stateMutability: string;
    type: string;
    anonymous?: boolean;
}

interface ContractData {
    contractName: string;
    byteCode: string;
    abi: Abi[];
}

We use TypeScript to avoid bugs, increase readability, and provide a better structure. In this way it's easier to troubleshoot if we encounter any problems or errors.

Now we can continue with the compiler, so let's instantiate a new worker:

//components/sol/compiler.ts
...
const worker = new Worker(
    new URL("./solc.worker.ts", import.meta.url), { type: "module" }
);

This worker should return our bytecode and ABI after it receives a message. Now we add a callback to onmessage (which will be called after we invoke postMessage with the source code as a parameter) and fetch the final result:

//components/sol/compiler.ts
...
worker.onmessage = function (e: any) {
    const output = e.data.output;
    const result = [];
    if (!output.contracts) {
        reject("Invalid source code");
        return;
    }
    for (const contractName in output.contracts['contract']) {
        const contract = output.contracts['contract'][contractName];
        result.push({
            contractName: contractName,
            byteCode: contract.evm.bytecode.object,
            abi: contract.abi
        } as ContractData);
    }
};
worker.postMessage({
    contractCode: contractCode,
});

Finally, we wrap everything in a Promise that will be returned to the caller. Final source code looks like this:

//components/sol/compiler.ts

export const compile = (contractCode: string): Promise<ContractData[]> => {
    return new Promise((resolve, reject) => {
        const worker = new Worker(
            new URL("./solc.worker.ts", import.meta.url), { type: "module" }
        );
        worker.onmessage = function (e: any) {
            const output = e.data.output;
            const result = [];
            if (!output.contracts) {
                reject("Invalid source code");
                return;
            }
            for (const contractName in output.contracts['contract']) {
                const contract = output.contracts['contract'][contractName];
                result.push({
                    contractName: contractName,
                    byteCode: contract.evm.bytecode.object,
                    abi: contract.abi
                } as ContractData);
            }
            resolve(result);
        };
        worker.onerror = reject;
        worker.postMessage({
            contractCode: contractCode,
        });
    });
};

Concluding the user interface

Hurray, the compiler is finished and ready to use! We could already use it in our App, but we will also add a user interface to easily code and compile custom code that users can write.
Let's get into it!

Let's start by overriding the pages/index.tsx page with the components we need:

//pages/index.ts
import type { NextPage } from 'next';
import Head from 'next/head';
import { useState } from 'react';
import styles from '../styles/Home.module.css';

const Home: NextPage = () => {

    return (
        <div className={styles.container}>
        <Head>
            <title>Frontend Solidity Compiler</title>
            <meta name="description" content="Compile solidity code on frontend with Next.js and Solc-js" />
            <link rel="icon" href="/favicon.ico" />
        </Head>

        <main className={styles.main}>
            <h1 className={styles.title}>
            Solidity Browser Compiler
            </h1>
        </main>
        </div>
    );
};

export default Home;

In order to use our compiler, we need a way to write source code and compile it into bytecode and an ABI that we will output to the user. This is where our user interface comes in. We'll create a simple textarea for writing the source code and a button for compiling it.

//pages/index.ts
...
<div className={styles.card}>
    <h2>Source Code</h2>
    <textarea rows={20} cols={50} />
    <button>Compile</button>
</div>

The textarea will have an onChange event listener that will call out to our save function when text is entered or changed.

Then we'll also add two other textarea so users can see their contract ABI and bytecode after the process is finished. Finally, let's do something similar with our compile button by invoking the compile function we did previously.

Let's add it:

//pages/index.ts
...
const [sourceCode, setSourceCode] = useState("");
const [byteCode, setByteCode] = useState("");
const [abi, setAbi] = useState("");

const compileSourceCode = (event: React.MouseEvent<HTMLButtonElement>) => {};

<div className={styles.card}>
    <h2>Source Code</h2>
    <textarea rows={20} cols={50} onChange={e => setSourceCode(e.target.value)}/>
    <button onClick={compileSourceCode}>Compile</button>
</div>
<div className={styles.card}>
    <h2>ABI</h2>
    <textarea readOnly rows={10} cols={60} value={abi} />
    <h2>Compiled ByteCode</h2>
    <textarea readOnly rows={10} cols={60} value={byteCode} />
    </div>
</div>

Connecting everything: Now that we have all of our UI elements built, let's connect them to the compiler and give them functionality.

//pages/index.ts
...
const compileSourceCode = (event: React.MouseEvent<HTMLButtonElement>) => {
const button = event.currentTarget;
button.disabled = true;
compile(sourceCode)
    .then(contractData => {
        const data = contractData[0];
        setByteCode(() => data.byteCode);
        setAbi(() => JSON.stringify(data.abi));
    })
    .catch(err => {
        alert(err);
        console.error(err);
    })
    .finally(() => {
        button.disabled = false;
    });
};

Here we invoke the compiler, save the bytecode and ABI to our state: to improve the user experience we also disable the button until the process is finished.

Final source code:

//pages/index.ts
import type { NextPage } from 'next';
import Head from 'next/head';
import { useState } from 'react';
import { compile } from '../src/sol/compiler';
import styles from '../styles/Home.module.css';

const Home: NextPage = () => {
    const [sourceCode, setSourceCode] = useState("");
    const [byteCode, setByteCode] = useState("");
    const [abi, setAbi] = useState("");

    const compileSourceCode = (event: React.MouseEvent<HTMLButtonElement>) => {
        const button = event.currentTarget;
        button.disabled = true;
        compile(sourceCode)
        .then(contractData => {
            const data = contractData[0];
            setByteCode(() => data.byteCode);
            setAbi(() => JSON.stringify(data.abi));
        })
        .catch(err => {
            alert(err);
            console.error(err);
        })
        .finally(() => {
            button.disabled = false;
        });
    };

    return (
        <div className={styles.container}>
            <Head>
                <title>Frontend Solidity Compiler</title>
                <meta name="description" content="Compile solidity code on frontend with Next.js and Solc-js" />
                <link rel="icon" href="/favicon.ico" />
            </Head>

            <main className={styles.main}>
                <h1 className={styles.title}>
                Solidity Browser Compiler
                </h1>

                <div className={styles.grid}>
                <div className={styles.card}>
                    <h2>Source Code</h2>
                    <textarea rows={20} cols={50} onChange={e => setSourceCode(e.target.value)} />
                    <div>
                        <button onClick={compileSourceCode}>Compile</button>
                    </div>
                </div>
                <div className={styles.card}>
                    <h2>ABI</h2>
                    <textarea readOnly rows={10} cols={60} value={abi} />
                    <h2>Compiled ByteCode</h2>
                    <textarea readOnly rows={10} cols={60} value={byteCode} />
                </div>
                </div>
            </main>
        </div>
    );
};

export default Home;

We're finished!

Hurray, our compiler is finished and ready to use! If you want to check out the full source code you can take a look at this repository on my GitHub.