Skip to main content

Quick Start

Here's how you can create your own fallback() Solidity web app and expose it to the web.

1. Extend WebApp

Create a new file called MyApp.sol and extend the WebApp contract.

The GitHub imports in the code samples below will work on Remix; you may need to change the URL imports to relative imports if you're developing locally and you cloned the fallback repository or installed fallback() with Forge.

MyApp.sol
import {WebApp} from "https://github.com/nathanhleung/fallback/blob/main/src/WebApp.sol";
// If you're using Foundry, you might need to import `WebApp` with
// import {WebApp} from "lib/fallback/src/WebApp.sol";

contract MyApp is WebApp {
constructor() {
}

// You'll write your fallback() app's routes here!
}

2. Add your routes

Import the HttpConstants contract and add your routes to the routes mapping, specifying the correct HTTP method and path.

The routes mapping is defined in the WebApp contract.

MyApp.sol
import {HttpConstants} from "https://github.com/nathanhleung/fallback/blob/main/src/http/HttpConstants.sol";
import {WebApp} from "https://github.com/nathanhleung/fallback/blob/main/src/WebApp.sol";

contract MyApp is WebApp {
constructor() {
// A `GET` request to `/` will be handled by the `getIndex` function
routes[HttpConstants.Method.GET]["/"] = "getIndex";
// A `GET` request to `/github` will be handled by the `getGithub` function
routes[HttpConstants.Method.GET]["/github"] = "getGithub";
}
}

3. Create contract functions for your routes

Create functions in the MyApp contract to handle your routes. Make sure the function signature of each route handler is functionName(HttpMessages.Request) or functionName() (if your route handler doesn't need access to the Request struct).

Your route handler function can have pretty much any name (as long as it doesn't overlap with one of WebApp's response helper functions like json or html; more on this below).

Always make sure your route handler function has the correct function signature and the route handler function's name is typed correctly in the routes mapping.

// Any typo will not work!
routes[HttpConstants.Method.GET]["/"] = "getInndex";

// This will not work! Try putting `extraData` into a request header
// or request content instead.
function getIndex(HttpMessages.Request calldata request, bool extraData)

Internally, fallback() will parse incoming HTTP requests and use the routes mapping to determine which contract function to call. It will call the function using its function selector, which depends on the function's parameters.

If your route handler functions don't have the correct function signature, fallback() will generate an incorrect function selector and your route handler function won't be called.

MyApp.sol
import {HttpConstants} from "https://github.com/nathanhleung/fallback/blob/main/src/http/HttpConstants.sol";
import {HttpMessages} from "https://github.com/nathanhleung/fallback/blob/main/src/http/HttpMessages.sol";
import {WebApp} from "https://github.com/nathanhleung/fallback/blob/main/src/WebApp.sol";

contract MyApp is WebApp {
constructor() {
routes[HttpConstants.Method.GET]["/"] = "getIndex";
routes[HttpConstants.Method.GET]["/github"] = "getGithub";
}

function getIndex(HttpMessages.Request calldata request) external pure override returns (HttpMessages.Response memory) {
}

function getGithub() external pure returns (HttpMessages.Response memory) {
}
}

4. Implement your routes

Implement your route handlers in MyApp.sol. You can import H, fallback()'s companion Solidity HTML DSL, and use the html function to return an HTML response.

The full list of available WebApp response helpers (e.g. text, json, redirect) is in the API Reference. You can also build an HttpMessages.Response struct from scratch and return that.

The response helpers build HttpMessages.Response instances with specific headers (e.g. the json response helper function builds a Response with a Content-Type: application/json header).

These response helpers are defined on WebApp, so you cannot create route handler functions with names matching any of the response helpers; the Solidity compiler will output an error (since the response helper functions are not marked virtual).

MyApp.sol
import {H} from "https://github.com/nathanhleung/fallback/blob/main/src/html-dsl/H.sol";
import {HttpConstants} from "https://github.com/nathanhleung/fallback/blob/main/src/http/HttpConstants.sol";
import {HttpMessages} from "https://github.com/nathanhleung/fallback/blob/main/src/http/HttpMessages.sol";
import {StringConcat} from "https://github.com/nathanhleung/fallback/blob/main/src/strings/StringConcat.sol";
import {WebApp} from "https://github.com/nathanhleung/fallback/blob/main/src/WebApp.sol";

contract MyApp is WebApp {
constructor() {
routes[HttpConstants.Method.GET]["/"] = "getIndex";
routes[HttpConstants.Method.GET]["/github"] = "getGithub";
}

function getIndex(HttpMessages.Request calldata request) external pure override returns (HttpMessages.Response memory) {
// The `H` API is heavily based on
// https://github.com/hyperhype/hyperscript
// and Jade/Pug
// https://github.com/pugjs/pug
// Use `H.element` (if standard HTML tag) or H.h("element")
// to generate an `<element>`.
string memory htmlString = H.html5(
H.body(
StringConcat.concat(
H.h1("fallback() web framework"),
H.p(H.i("a solidity web framework"))
)
)
);
return html(htmlString);
}

function getGithub() external pure returns (HttpMessages.Response memory) {
return redirect(302, "https://github.com/nathanhleung/fallback");
}
}

5. Pass your app to the DefaultServer contract

Once you've implemented your route handlers, create a new contract MyServer which extends DefaultServer. Pass an instance of MyApp to DefaultServer's constructor.

DefaultServer extends the HttpServer contract and automatically sets a few reasonable request parsing defaults (e.g. it sets maximumRequestHeaders to 4000 and maxPathLength to 4000). These defaults are generally based on Apache's defaults.

If you want to customize request parsing behavior, you can construct your own instance of HttpServer separately, but the DefaultServer should suffice for most use-cases.

MyServer.sol
import {DefaultServer} from "https://github.com/nathanhleung/fallback/blob/main/src/HttpServer.sol";
import {MyApp} from "./MyApp.sol";

contract MyServer is DefaultServer {
constructor() DefaultServer(new MyApp()) {
app.setDebug(true);
}
}

6. Deploy MyServer

Then, compile and deploy the MyServer contract.

The deployment example below uses Foundry's Forge, but you could also use other tools like Remix or Hardhat to deploy your contract.

Terminal
forge create MyServer.sol:MyServer

7. Send HTTP requests to the contract

When you send HTTP requests to the MyServer contract (hex-encode the requests into the data field of a transaction), the return value will be a hex-encoded HTTP response.

The sample below shows how to send a request to the contract using native Node.js modules (it uses eth_call internally, so it doesn't make any changes to blockchain state).

request.js
const http = require("http");

// Construct JSON-RPC request
const jsonRpcData = JSON.stringify({
jsonrpc: "2.0",
id: "1",
method: "eth_call",
params: [
{
to: CONTRACT_ADDRESS,
// HTTP request to send to contract
data: "GET / HTTP/1.1".toString("hex"),
},
],
});

// Send JSON-RPC request
const httpRequest = http.request(
{
host: ETHEREUM_RPC_HOST,
path: "/",
port: ETHEREUM_RPC_PORT,
method: "POST",
headers: {
"Content-Type": "application/json",
},
},
// Receive response
(response) => {
let responseData = "";
response.on("data", (chunk) => (responseData += chunk));
response.on("end", () => {
const responseJson = JSON.parse(responseData);
const responseBytes = Buffer.from(responseJson.result.slice(2), "hex");
console.log(responseBytes.toString());
// HTTP/1.1 200 OK
// Server: fallback()
// Content-Type: text/html
// ...
});
}
);
httpRequest.write(jsonRpcData);
httpRequest.end();

8. Expose your fallback() app to the web

To expose your fallback() app to the web, you'll need to create a TCP server that can pass messages to and from the deployed contract on the blockchain.

Here's a Node.js example, using only native modules, which exposes a fallback() web app on port 8000.

server.js
const http = require("http");
const net = require("net");

const ETHEREUM_RPC_HOST = process.env.ETHEREUM_RPC_HOST || "127.0.0.1";
const ETHEREUM_RPC_PORT = process.env.ETHEREUM_RPC_PORT || 8545;

// MyServer contract address
const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS;

const server = net.createServer((socket) => {
socket.on("data", async (requestData) => {
// Receive and parse contract response
let response = JSON.parse(await handleRequest(requestData));

try {
if (response.result.length > 0) {
// Decode contract response
const responseData = Buffer.from(response.result.slice(2), "hex");
// Send contract response back
socket.write(responseData.toString());
}
socket.end();
} catch (err) {
console.error(response);
console.error(err.toString());
socket.end();
}
});
});

server.listen(8000, "0.0.0.0", () => {
console.log("Server running at localhost:8000");
});

async function handleRequest(requestData) {
return new Promise((resolve) => {
const jsonRpcData = JSON.stringify({
jsonrpc: "2.0",
id: "1",
method: "eth_call",
params: [
{
to: CONTRACT_ADDRESS,
// Forward TCP data to contract
data: requestData.toString("hex"),
},
],
});

const httpRequest = http.request(
{
host: ETHEREUM_RPC_HOST,
path: "/",
port: ETHEREUM_RPC_PORT,
method: "POST",
headers: {
"Content-Type": "application/json",
},
},
(response) => {
let responseData = "";
// Receive contract return data over JSON-RPC
response.on("data", (chunk) => (responseData += chunk));
// Send contract return data back to caller
response.on("end", () => resolve(responseData));
}
);
httpRequest.write(jsonRpcData);
httpRequest.end();
});
}

See send-server.js for an example of how to write a server that modifies blockchain state using eth_send* methods. In this case, instead of getting the data returned from the call and sending it back as the HTTP response, you need to extract the data from the Response event (defined in HttpProxy) after the transaction is included in a block. The initial return value after sending the transaction will just be the transaction hash.

To put this into production, you could run this script on an AWS EC2 instance and use NGINX as a reverse proxy to forward requests from port 80 to 8000.

You could also add HTTPS support by configuring NGINX with Let's Encrypt.

Another alternative is HAProxy, which supports proxying over both TCP and HTTP. You could use HAProxy's connection limiting, queueing, caching, and rate-limiting features to control the load on your blockchain RPCs.

See the Dockerfile in src/example for an example one-container setup with HAProxy for caching and rate limiting.

9. Next steps

You can write any sort of web app with fallback().

For more fallback() web app examples, with more advanced route handlers, see src/example/SimpleExample.sol and src/example/FullExample.sol.

Here's an excerpt based on one of the example apps showing how to handle a POST request:

ExamplePostApp.sol
contract ExamplePostApp is WebApp {
constructor() {
// Call different handlers based on HTTP method
routes[HttpConstants.Method.GET]["/form"] = "getForm";
routes[HttpConstants.Method.POST]["/form"] = "postForm";
}

// Show cURL `POST` command if `GET` request
function getForm() external pure returns (HttpMessages.Response memory) {
HttpMessages.Response memory response;
response
.content = "curl -v -d 'random post data' -X POST http://localhost:8000/form";
return response;
}

// Send `POST`ed data back
function postForm(HttpMessages.Request calldata request)
external
pure
returns (HttpMessages.Response memory)
{
HttpMessages.Response memory response;
response.content = StringConcat.concat(
"Received posted data: ",
string(request.content)
);
return response;
}
}

For a working Node.js TCP server example, see call-server.js in the src/example directory.

Finally, see src/example/Todo.sol for a working todo app. If run with the send-server.js server, this app demonstrates reading and writing state to the blockchain over HTTP.

Appendix: Live Demos

There are fallback() contracts live on the Goerli Optimism testnet.

An instance of SimpleExampleServer is deployed at 0x83707e88a05046A04B459d8D0eB1aFcC404f92eB and served with call-server.js at http://simple.fallback.natecation.xyz.

An instance of TodoServer is deployed at 0x919F31dAC93eBf9fFd15a54acd13082f34fDd6D3 and served with send-server.js at http://todo.fallback.natecation.xyz.

Privacy note: the todo demo logs all incoming HTTP requests to the blockchain. See the input data on the todo contract on Etherscan to see an example of the type of data that is logged. Please do not visit the page if you are not comfortable with this; see the simple demo instead, which is read-only.

If http://todo.fallback.natecation.xyz isn't working, the sender account is probably out of Optimistic Goerli ETH. Try again later when I've sent it more, or donate some to 0xDB922AA1571aBCEc925221B7B6E9F9db4edDC625!