Bytecraft with web3 and Haskell

_kitchen
Coinmonks
4 min readJun 30, 2018

--

(this post is also available in markdown on github)

In this post, we’ll go over how we made the bytecraft image generator using the Haskell web3 package. We use several other packages in here too which I will only cover as much as necessary. Our main goal here is to cover how to use the web3 package in Haskell.

First we need to load the abi of our contract:

This is the MaGiC sAuCe of the Haskell web3 module. It uses Template Haskell to generate type-checked Haskell methods that call our smart contract. This is what makes Haskell such a great language for writing your smart contract apps!

The function names match those in the ABI. I find it helpful to define an incorrectly typed method to get the signature. For example, in our case, this

gives this error in ghc:

In this case, getUpdateTimes is a function taking a Call type and returns ListN 1024 (UIntN 256) wrapped in the Web3 type. We'll unpack all these types soon. For now, know that this function matches the following function in our solidity contract:

Our smart contract lets users claim 32x32 chunks of pixels from a 1024x1024 grid. Each Chunk is 1024 pixels and there are 1024 chunks. To upload a 32x32 8-bit image to the chunk, the user must put a stake on the chunk. If there’s already a stake on the chunk, the new stake must be greater than the previous one and in this case, the previous stake is returned to its owner.

The main method in this module is query:

which takes 3 network parameters: a filename prefix for output, the contract address, and the web3 provider URL. It also takes an input image represented as a repa array. This is the last image processed which will be modified to contain all updated chunks since the last time this function is called. Finally it returns an image newly updated image as an IO operation.

Inside this method, the first thing we do is set up some helpers to facilitate calling the web3 api:

Since we don’t run our own full node on our free GCloud instance, we’ll be relying on an external http provider. Our connection needs to be TLS so using newTlsManager from the http-client-tls package is necessary.

Next we set up callData which has Call type we saw earlier. The Call is a record type containing information about how to make our web3 call. In our case, we only care about the callTo parameter. The funny syntax is from the data-default package which provides default values to the other parameters.

provider is where we'll be sending our web3 RPC calls to. Finally doW3 makes the requests using the manager and provider we just created. doW3 calls runWeb3With with the manager and provider we just created.

We saw the Web3 monad earlier. It wraps a web3 RPC call and runWeb3With executes the Web3 monad inside the IO monad.

Next we poll for the last block number each chunk was updated. We also poll for the last time the contract itself to compare against the cached value in “..lastUpdate.json”:

Here we see our first web3 call! Using the doW3 and callData we'll get a value of type m (Either Web3Error a). We don't do any error handling in this example so you'll see expressions like either throw id updateTimes' a lot. This gets us the data but it's of type ListN 1024 (UIntN 256). These come from the Network.Ethereum.ABI.Prim module which contain representations of all the EVM types.

If you need a refresher on the EVM types, this is a good time to checkout the solidity docs.

Looking back at the definitions of getUpdateTimes, we see the solidity type uint[1024] becomes ListN 1024 (UintN 256). This is a 1024 element list of 256-bit unsigned ints with both sizes fixed at the type level! ListN is an instance of IsList which has the type family Item implemented as

which has the method

allowing us to get the more familiar [Int] using

UIntN is an instance of Integral so we can convert it using fromIntegral.

Ok! So now we got our first Haskell primitive from our smart contract! Next we want to get the actual image data from each chunk:

Using updateTimes we build a list of (x,y) indices of blocks we want to query. The queries are grouped into batches of 8 (using the helper function group :: Int -> [a] -> [[a]]) and made in parallel using forM from the monad-parallel package. The provider starts rejecting requests if too many are made at once and running them all sequentially is very slow. 8 runs fast enough and myetherapi/infura has yet to reject any of our requests.

Inside the queryBlock function, you’ll see one more interesting conversion:

All the ABI primitives can be converted to instances of Get and Put (from the binary package) using abiGet and abiPut respectively. runPut (abiPut color) converts the ABI primitive ListN 32 (BytesN 32) representing raw 8-bit image data into a ByteString. Web3 is an instance of MonadIO so we also do some progress logging with liftIO (putStrLn ...).

Later we’ll make a traversal on our 32-bit image represented as a repa array that converts and copies the raw 8-bit image data into the right place. All the necessary data for this operation is stored in ChunkInfo

Back to the query method. We still have some bookkeeping left to do. I won't go over this here since it's not web3 specific. The last step is to run the image conversion process described earlier and return the output:

and that’s it! I hope you enjoy writing your own web3 Haskell app as much as I did. Maybe give our smart contract a try.

The bytecraft contract owns any ether that is staked on a tile. We own any ether that goes here 0x0D8A07e01Fd9b3DA4ce78109DBDFd385bE59bAE2

--

--

_kitchen
Coinmonks

Product and Research Specialist @ ThunderCore and also a lot of other things