Panda Streamer is the name of a fictional product, currently hosted at https://panda-streamer.web.app/. It’s an Ethereum DeFi application that allows users to continuously send ERC-20 tokens to someone else within a specified window of time. It leverages the Sablier protocol behind the scenes.
The app is exclusively integrated with MetaMask and users can:
- Stream any ERC-20 token to any address over a period of time, be it in local or test environments (e.g. Rinkeby).
- List successful streams.
- Filter successful streams by their time of creation.
Here’s how the form submission works behind the scenes:
- The user submits the form and has to approve via MetaMask the first
transaction (the
approve
call by the ERC-20 token contract). - Once approved the user waits for it to be confirmed by 1 block.
- Then a validation using Ethers
callStatic
is used to callSablier.createStream
in read-only mode. If no errors are found then an attempt to create a real stream is finally dispatched. - The user is redirected to the listing page without actually waiting for the transaction to be mined.
If errors are found during these steps the user sees an appropriate error message and can either cancel or submit the form again.
In no order of importance:
Other tools:
- Heroicons
- cssnano
- PostCSS
- PurgeCSS
- Tailwind CSS
- postcss/autoprefixer
- http://cljson.com/ Useful to transform ABIs to EDN.
ethers-io/ethers.js - My library of choice to interact with Ethereum nodes over
MetaMask. I haven’t used web3.js
to compare, but I based my decision on how
reliable the tool is and the current trend, i.e. are developers migrating from
web3 to ethers and why? I went through many hoops along the way and ethers.js is
by no means ergonomic to use from the ClojureScript perspective. Everything is
stateful, requires heavy interop syntax, returns JS objects requiring conversion
to CLJ, etc. Who knows, maybe eventually we will see an objectively superior
ClojureScript alternative.
metosin/malli - Data-driven schemas. I found it much more pleasurable to use in
the REPL as it has fewer side-effects due to its data-first approach when
compared to Spec. This library is used to validate the re-frame db
after it
changes (it’s disabled in production).
rgm/tailwind-hiccup - A tiny library to help Tailwind CSS play along with Hiccup.
juxt/bidi and kibu-australia/pushy - Just one out of many possible combinations to implement route handling using History.pushState in re-frame applications.
MetaMask/jazzicon - A simple identicon library. Far from ideal, but I chose it to be the same one used by MetaMask.
day8/re-frame-10x - Buggy as hell, but in certain situations it was an invaluable tool. You need to uncomment a single line in shadow-cljs.edn to enable it.
kimmobrunfeldt/concurrently - Run multiple commands concurrently. Simply put, it allows you to easily start/stop multiple processes, like the PostCSS watcher, the shadow-cljs watcher and the local Ganache instance. It’s simple, but it gets the job done.
trufflesuite/ganache-cli - Quickly create reproducible blockchains for local development. I’ve used it test my own ERC-20 tokens and to locally deploy Sablier contracts.
prettier/prettier - Most developers in the Javascript community know this little
tool. It’s used in this project mainly to automatically format CSS (configured
via Emacs’ .dir-locals
).
All calls sent via the provider assume the network will reply in a reasonable amount of time. There’s no handling of timeouts and the app doesn’t retry calls that would be totally safe to try again. There’s a chance the app could get stuck waiting forever for a reply that will never come.
The app could use subscriptions to listen for changes based on ethers filters instead of manually fetching logs.
Firebase Hosting hosts Panda Streamer as a SPA. The deployment is manual, but it works fine for demonstration purposes.
Being a Single-Page Application (SPA) built with ClojureScript, it’s even more important to publish optimized assets due to the massive overhead of the runtime and how poorly optimized for ClojureScript many Clojure libraries are.
Here’s how CSS is bundled:
- PostCSS is configured via plugins: Tailwind CSS, Tailwind Nesting, Autoprefixer and CSSNano. It’s this amazingly flexible tool that allows us write CSS in dozens of different ways due to its plugin ecosystem.
- Tailwind CSS in turn uses PurgeCSS to reduce it’s bundle size from hundreds
of KBs to less than 5kb with gzip. PurgeCSS has its own quirks because
sometimes it strips out valid CSS classes from the final output. That’s why
you’ll see regexes in the safelist option in
tailwind.config.js
.
Whenever a production release is generated, the .cache/report-web.html
file is
generated and you can use it to hand-optimize imports. For example, ethers.js
is huge, with hundreds of KBs of unnecessary code if you just require “ethers”
in ClojureScript. That’s why you’ll find require expressions like
["@ethersproject/abi" :refer [Interface]]
.
The end result is decent, but ethers.js and the ClojureScript runtime are big players on their own.
Size (gzip) | |
---|---|
Javascript | 356 kb |
CSS | 4.3 kb |
Image assets are committed in the repository itself and served by the hosting service. Suboptimal, but it works for this pet project.
If you’re going to exclusively test Panda Streamer in Rinkeby then you can skip steps 2 and 3.
You’ll need to install Clojure (JVM) in order to install ClojureScript and you’ll need Node v16.16.0 (latest LTS as of 2022-Jul).
cd panda-streamer nvm use npm ci
Head over to the panda-streamer
repository and run:
npm run blockchain:local
Import at least the top two accounts to MetaMask. They’ll be used to create
streams. Behind the scenes ganache-cli
was configured to store its data in
./cache/ganache/
. If you remove the cache directory and recreate the network
you’ll probably get weird errors in MetaMask because even though the accounts
are the same (because the mnemonic is hardcoded), their nonces don’t match what
MetaMask has. In that case, for each test account, go to Advanced > Reset
Account
.
The recommended approach to test Sablier contracts locally is to clone the repository and deploy them yourself in a network provided by Ganache, HardHat etc. In general, testing locally proved to be very similar to the “real” Rinkeby network with the added bonus of being fast, reproducible and 100% private. Well, not really the reproducible part, unfortunately things work much better in Rinkeby and the Ganache/HardHat blockchains behaved unexpectedly sometimes.
You’ll need yarn
and Node v11.15.0
for this task. The CI environment
variable is mandatory to compile for local development. If you’re an NVM (Node
Version Manager) user, simply run nvm install v11.15.0
, followed by npm
install -g yarn
.
git clone [email protected]/sablierhq/sablier.git cd sablier git checkout audit-v1 nvm use v11.15.0 yarn run bootstrap cd packages/protocol rm -rf build/ && CI=true npx truffle migrate --reset --network development
In the output, take note of the ERC20Mock
address. The Sablier
contract
address you can put in the shadow-cljs.edn
acme.web.domain.sablier/sablier-local-contract-address
variable under
[:builds :web :dev :closure-defines]
. Unfortunately shadow-cljs is not very
helpful with environment variables and there are even tools out there trying to
remove this exact limitation. Ideally I’d want the project to be configurable by
a .env
file, as that is far more convenient.
From now on, if you’ve already deployed Sablier contracts you can just call npm
run watch
and all processes will run by the concurrently Node package. You can
also run them individually in separate shell sessions.
npm run web:watch npm run css:watch npm run blockchain:local
In case you want to start from scratch, call npm run clean
. This will remove
all temporary files, like the shadow-cljs cache, the local Ganache blockchain,
etc. Just bear in mind you’ll need to redeploy Sablier contracts. In that case,
follow the previous instructions.
If you want to run a ClojureScript REPL, simply start it as usual and call npm
run css:watch
and npm run blockchain:local
in separate shell sessions.
If you want to test the application with your own mintable token for testing, go
to https://cointool.app/eth/createToken and create a standard token with 18
decimals. Click Advanced
and add your public address. You’ll need to connect
your wallet to deploy it.
If you want mintable DAI, just go to TestnetDAI on Etherscan and call the mint function.
If you are using Emacs, simply run M-x cider-jack-in-clj&cljs
, give it a
couple seconds and open/restart the browser window. You should see no errors in
the Developer Tools console. Two REPLs will be available, the CLJ (where you can
call shadow-cljs functions), and the CLJS REPL that evaluates code from your
ClojureScript buffers. The .dir-locals.el file sets default variables to
reduce or eliminate the number of prompts from CIDER.