I’ll share some thoughts about my experience of writing my first decentralized application (dApp).
The dApp consists of a simple blog with a one-click paywall.
Anyone can publish a Post which involves paying a fee to the Blog owner (account responsible for deploying the Smart Contract). To recover and get a profit, Publishers can add a fee for anyone to be able to see their content.
You can get the complete project from Github: https://github.com/claudiucelfilip/reveal
Or you can try it here: https://wavelet-reveal.netlify.com
The dApp is written using React and Gatsby and uses a WebAssembly Smart Contract running on Wavelet.
Wavelet is an open ledger for writing scalable mission-critical, decentralized WebAssembly application (https://wavelet.perlin.net/).
Wavelet, with its over 31,240 payment transactions per second, achieves 2–4 second delays from action to transaction and then reaction.
It also runs WebAssembly smart contracts, allowing you to write them in any language you want. I went with Rust because it was easier with the help of smart-contract-rs (https://github.com/perlin-network/smart-contract-rs).
The whole concept of the one-click paywall relies on fast transaction processing. Classic blockchain platforms, such as Ethereum, require too much time (by order of minutes) which is unacceptable. We can’t have users staring at loading animation for minutes. Trust me, it just won’t do.
With Wavelet, on the other hand, 2–4 seconds is perfectly fine for this type of functionality and Publishers won’t mind having their Posts public almost instantly.
Gatsby is a blazing-fast static site generator for React. It allows you to build a React app and have server-side rendering in one single swoop.
This removes any SEO anxiety and greatly improves load performance by directly serving pre-rendered content. Any non-essential content and extra functionality can be loaded after the initial load.
Going from Zero
It showed me everything I needed from installing Rust to getting a React app up and running and communicating with the Smart Contract.
I won’t go through all the code but I want to bring up some challenges I encountered along the way and some things I (finally) realized.
- Smart Contract in Rust
- React App
- Gatsby integration
Smart Contract in Rust
Although I had some sort of picture in my head about Rust and building to Web Assembly, the language still seemed foreign to me. So I needed a crash-course and found one here: https://www.snoyman.com/blog/2018/10/introducing-rust-crash-course.
Didn’t go through all of it, but with some basic reference and ownership knowledge plus some other examples from https://github.com/perlin-network/smart-contract-rs, I was ready to get coding.
Smart-contract-rs also provides solid Rust helpers and macros which allow your code to run on the Wavelet network. Otherwise, I would have to do some serious magic to get things to work. It’s the glue that binds your Smart Contract to Wavelet and helps you handle the ins and outs of your functions.
On my first attempt, I found myself trying to fix cascading compiler issues while writing in an OOP fashion (classes for everything, Post, Blog, Tag, etc). Ended up dealing with reference lifetime issues and was faced with a choice: get better at Rust or make it simpler.
Since I was eager to get something running, I went with the latter and decided to simplify and just have a single main class (struct with implementation). This also coincided more with the other examples from the smart-contract-rs.
Here’s a snippet with a part of the Smart Contract:
- all functions need to have the same contract and return Ok(())
- to report an error, use Err and the inputted string will be sent via the pollTransactionUpdate socket as a rejected transaction.
- network parameters (amount, sender, etc.) are read by the smart-contract-rs helper and are attached to the params argument
- to get the function parameters, you need to use params.read() and specify the variable type. Call order is important.
- functions don’t return anything relevant. You can use log or debug to send values back to the client
Code is money
Each line of code is executed, counted and taxed and you should always keep that in mind. Or else!
I started thinking about money when I started writing the pay_post() functionality. I initially wanted to automatically forward the PERLs to the Publisher and then enable the content for the Reader. This resulted in the situation that the user had pay for:
- an arbitrary fee to view the content; set by the Publisher for profit
- the fee to run the smart contract
- extra transaction fees to send PERLs to the publisher.
Unfortunately (for the Reader), there’s no way around the first two, but I think the Publishers should be paying the last one. It’s the least they could do. Also, there’s the additional time interval for that transaction until the paywall could be opened; not to mention I had no idea how to handle async code in Rust. (guess I should have spent more time on that crash course)
To get out of this mess, I took some inspiration from the Token and Transfer Back examples from https://github.com/perlin-network/smart-contract-rs/blob/master/examples and I added a balances HashMap property to the blog. The Smart Contract now receives the PERLs from each Reader and tracks how much each Publisher is owed. Afterward, they can cash out their earnings (if any) whenever they see fit.
After paying for the hidden content, the generous user has the chance to evaluate the content (to like or not). Likes count as 1, Dislikes count as -1. The sum of these values will represent the rating of the Post.
To take the age of the Post into account, I used a simple scoring algorithm from Hacker News (https://medium.com/hacking-and-gonzo/how-hacker-news-ranking-algorithm-works-1d9b0cf2c08d). It takes the rating and the time since the Post was created and returns a score. Each get_posts request will then require the current time from the client and provide the list of Posts, with updated scores.
You may be thinking, is this scoring algorithm worth the code cost? Well, fetching content doesn’t really alter state; so calling the local WebAssembly instance is free of charge.
Mind = blown
An important realization for me was that any state altering action is async and will require a fee, as it’s going to be resolved within Wavelet. However, the results will update the VM memory and any content-fetching request will be free and instantaneous (no async needed).
Oh, and don’t know if you noticed, but there are no databases, no caching nor any AJAX requests to get the Posts. More on this on the React App section.
I didn’t want to waste too much time with Post categories. The simplest solution was to add tags string to each Post with ‘|’ separated tokens.
Lens & Smart Contracts
Once I got something compiled successfully, I uploaded it to https://lens.perlin.net/#/contracts and started tweaking my parameters.
I also cloned the lens (https://github.com/perlin-network/lens ) and wavelet (https://github.com/perlin-network/wavelet ) repos to have some more control over the network. With it, I could have an insane amount of PERLs and feel like the top 1%.
I used the provided Function Executor tool from Lens to send parameters and the smart-contract-rs debug! macro to print the output.
I decided that the client App needed the following pages:
- A listing page where Posts are ordered by a score based on likes and time.
- Post details page with a public section and a private one which only gets populated after clicking the Pay button. Furthermore, the user can then Like or Dislike the article and contribute the Post’s overall score.
- Create a Post page where anyone can write their Post. It will feature a simple and rich text editor to allow for headings and text formatting. It should also allow for custom embedded content for greater value.
- Balance page where Publisher can see and withdraw the PERLs they’ve earned for their content
- Login page to allow access to the Balance and Create pages
For UI, I started out simple with a create-react-app. To make things easier, I included a wavelet-client (https://github.com/perlin-network/wavelet-client-js) library to connect to the Wavelet network and call functions on the Smart Contract.
Sync vs Async
An executable instance of the smart contract is initially loaded into memory via the browsers WebAssembly virtual machine. Each contract call to change the state (POST/PUT/DELETE equivalent) will trigger a transaction within Wavelet and if and when applied, it will then update the VMs memory with the new state.
To react to the changes I used the pollTransactionUpdate function: It starts a WebSocket channel and listens for an ‘applied’ or ‘rejected’ event type. I wrapped this into an easy-to-use listenForApplied and added to each async call.
To get the new state (GET equivalent), you only need to use the test function on the wavelet-client which will get the current state of the local instance of the smart contract.
Here’s an example with a sync getPost() and async payPost() from the Post Details page.
Again, no databases, no server round trips. Sweet.
The Posts are ordered by the score but I didn’t want just any listing. I felt that a newspaper-like grid would be best. Text boxes should stretch based on Post Title and Excerpt lengths and should be arranged to fit as many as possible on the screen. Luckily there’s a JavasScript library for this as well; Shuffle JS (https://vestride.github.io/Shuffle/).
I only had to move my Post filtering code into Shuffle JS and the Listing was set (with animations and everything). Sweet.
There’s no getting around it, SEO is important and although the Google Bot can crawl dynamic sites, there’re always other platforms that may or may not be as capable. Server-rendered content is always a safe bet.
You also get a performance boost when your content is already generated and ready to be sent to the browser. This is where Gatsby integration comes in.
To gather the Posts from Wavelet, I created a source plugin which uses the wavelet-client-js library to load the Smart Contract and get all the Posts.
One tough nut to crack was handling the situation where there’s no content, which causes multiple graphQL errors and breaks the build.
When the plugin receives content, it creates a few fields (allRevealPost and RevealPost). If there’s no content, these fields are missing and the queries fail. In some cases, the errors can be caught, such as createPages in gatsby-node.js but some can’t (or at least I don’t know how): such as listQuery from the IndexPage component. Or so I thought.
It turned out that I couldn’t catch the graphQL error anywhere so the only solution was to manually add the fields to the schema via the createSchemaCustomization function inside the plugin code.
In the initial React app, the User was forced to login before being able to visit any page. This won’t work now because there’s no state in a Gatsby generated site, and bots should be able to crawl the site. For this kind of situation, Gatsby recommends creating a Hybrid App, wherein a part of the site is only a client (https://www.gatsbyjs.org/docs/building-apps-with-gatsby).
This way the Login, Listing, and Details pages are public and server-rendered, while the Create and Balance pages are client-only and will require a wallet.
Not all JS code is created equal …
… some can’t run on the server.
For example, Quill and Shuffle JS libraries can only run on the client as they access a DOM. No need to bother the Server with such matters.
For things to work, the client code must be executed after the component mount (inside the useEffect hook or the classic componentDidMount method).
Additionally, since the source plugin has already fetched the Posts at build time, there was no need to have wavelet-client-js running on the server-side again. So I moved calls to any SmartContract related resources to happen only after the component mount.
There were some situations where I needed to check if the code should run depending on the environment.
One such situation was on the Listing page: the Layout needs to prevent its Home child component from calling getPosts function before init. To ensure this never happens, a LoadingSpinner component is rendered instead of Home, until Smart Contract init() is finished. This was a good solution for the browser but it prevented the server from rendering the Posts. A solution was to check for the window variable and only render the loading animation on the client-side.
It’s important to know what Gatsby offers and what it can’t. It can’t handle dynamic content/state. It’s fast and SEO friendly but you need to manually rebuild it to have the latest content.
In my case, I wanted the Publisher (and everyone else) to be able to see their newly published Post on the Listing page without having to wait for another Gatsby build. For this to happen the client-side code will always do a second fetch with the latest content and overlap that over the sever generated page. After the rebuild is finished, the server can provide fresh stuff to any curious bots.
The site is deployed on Netlify (https://www.netlify.com/) with a WebHook to repeatedly trigger builds. The idea was to have an AWS Lambda function call a link every 30 minutes, which should rebuild the site.
For the Lambda function, I used some node with a more verbose, https approach. I couldn’t be bothered to go through adding NPM dependencies to the lambda function.
Like with every great dApp, you never run out of things to do. Some of them are:
- Post archiving: since the Smart Contract is storing all the Posts, it‘ll become too large. The best thing would be to impose a posting fee based on availability. Posts should be removed/archived when their time expires.
- improve Quill editor: Publishers should be able to embed anything as an iframe. Using embedded content might be an easy workaround the smart contract fees.
- API for 3rd party apps: This would enable any CMS or site to easily use the one-click paywall.
- add Post image: Publishers would be able to upload any Post Image or thumbnail to the IPFS network
- add cost estimates: Currently, publishing an article is like shooting in the dark, you don’t know how much it costs until after you submitted. There should be a word counter and a way to get an estimated cost.
I don’t think I’ll be able to do them on my own, so any contribution is welcome.
Now, some of these things might be self-evident for anyone who knows a thing or two about developing dApps, Smart Contracts, and Gatsby. I guess, I just wanted to learn some of the concepts the hard way. Hopefully, if you’re like me and you’ve read this, you’ll be able to skirt some of these challenges.
I hope this could help with some of the trickier bits of writing your first dApp with React, Gatsby, and Wavelet. Also, I can’t wait to see what people can create in this novel Wavelet network.