On-chain Solar Systems
On-chain Solar Systems
Going into the festive season after our first month of working full time on Onset Carbon - a challenging and fulfilling undertaking - Luke and I wanted to scratch the itch of building and launching a complete product.
I have been seeing some interesting fully on-chain projects crop up on Twitter such as 1337skulls. Their commitment to unique thematic design and user experience inspired us to try and build something similar.
Idea
For a project to be feasible to deploy on a blockchain, the size of the code being deployed has to be sufficiently small. This is why on-chain art projects often consist of simple shapes or pixel art. We wanted to build something that was was not too technical from an artistry perspective but still had a unique aesthetic and was small enough to deploy in a single transaction at a reasonable cost.
We explored a few ideas but ended up settling on procedurally generated SVG solar systems with animated orbits. The planets are simple circles animated to orbit around a circle star.
Implementation
Figma design
The first step in the process is to decide what a Solar System will look like. We had a rough idea of what we envisioned and used Figma to create a design and then exported it as SVG.
Original design
SVG Simplification
The next step was to identify repeated structure in the SVG and simplify the exported SVG down into minimal boilerplate which could be generated by a script. My preferred method for experimenting with SVG is by using editsvgcode.com as it provides a nice real-time view of the result which lets you iterate quickly. It also makes it easy to branch off and try different ideas by having it open in multiple tabs. Ideally I’d like to have this running in Vscode but haven’t found a good extension for it yet.
Animations
This step also involved animating the orbits of the planets.
We ended up using the <animateTransform>
tag to rotate the planets around the star.
<!-- Planet SVG -->
<g>
...
<animateTransform
attributeName="transform"
type="rotate"
from="0 ${halfCanvasWidth} ${halfCanvasWidth}"
to="360 ${halfCanvasWidth} ${halfCanvasWidth}"
dur="${duration}"
repeatCount="indefinite">
</animateTransform>
</g>
The from
and to
attributes are used to specify the starting and ending rotation angle. The dur
attribute specifies the duration of the animation. The repeatCount
attribute is used to specify how many times the animation should repeat. We set it to indefinite
to make it repeat forever.
Animated design
JavaScript prototype
Now that we had the base SVG and isolated the repeated components we could start building a script to generate the SVG. This was done in a simple file containing some HTML and JavaScript code which generates the SVG and displays it which can be found here.
We had to determine the variables that would be used to procedurally generate the SVG. We decided to make the following properties variable in each solar system:
- Star color (yellow, blue)
- Star size
- Number of planets (1-5)
- Planet size
- Planet color
- Planet orbit period (5-14 seconds)
We also wanted to give each planet a minimum brightness to ensure it was always visible against the background so the RGB color values always ranged from 100 to 255.
This implementation works fine for viewing on platforms that support animation but it doesn’t work for static images because all the planets would start in a line, which looks a bit weird.
Static issue
To address this, we needed to randomize the starting position of the planets as well. This was done by adding a random angle to the starting position of the planet and then calculating the starting position from the initial position by using trigonometry to translate the angle into x and y coordinates:
const x = orbitRadius;
const y = 0;
const angle = Math.random() * 360; // Random in degrees
const newX =
x * Math.cos(angle * (Math.PI / 180)) - y * Math.sin(angle * (Math.PI / 180));
const newY =
x * Math.sin(angle * (Math.PI / 180)) + y * Math.cos(angle * (Math.PI / 180));
Solidity
The JavaScript sample implementation was then ported to Solidity. The main differences include the lack of floating point numbers support, and a Math.random()
equivalent.
We implemented some pseudorandomness using the token ID and some description string as entropy by taking a keccak hash of the concatenation of the two. This is not cryptographically secure but it is sufficient for our purposes. Here you can see how the randomRange
function makes use of the random
function to generate a random number in a given range.
function random(string memory input) internal pure returns (uint256) {
return uint256(keccak256(abi.encodePacked(input)));
}
function randomRange(
uint256 tokenId,
string memory keyPrefix,
uint256 lower,
uint256 upper
) internal pure returns (uint256) {
uint256 rand = random(string(abi.encodePacked(keyPrefix, uint2str(tokenId))));
return (rand % (upper - lower)) + lower;
}
Here is an example of it in action to generate a random number from 5-14 for the duration of the orbit animation:
planet.duration = utils.randomRange(_tokenId, string.concat("duration", utils.uint2str(i)), 5, 15);
Function calls in Solidity can’t take too many arguments so we had to split the string.concat
function into multiple calls to generate the final SVG output string.
To get a hot reloading live view of the SVG being rendered by the contract for testing purposes, we used hot-chain-svg. This is a simple tool that watches a contract for changes and automatically compiles and deploys the contract and renders a sample token.
In the context of the NFT project, this rendering logic is contained in the Renderer
contract which is called upon by the SolarSystem.tokenURI
function to generate the SVG for a given token ID.
The SolarSystem
contract inherits from ERC721A which is a gas-efficient implementation of the ERC-721 standard which allows for minting multiple tokens for close to the gas cost of minting a single token.
Frontend
We had 3 goals for our frontend:
- Offer a unique thematic design
- Provide information and context about the project
- Allow users to easily mint the token
We went with simple layout which emphasises the NFT and clearly communicates the project’s purpose. The Solar Mono
typeface was chosen for its clean and minimalistic look and really ties the design together. RainbowKit’s theme customization was used to change the type to match the rest of the design to ensure a consistent look and feel.
The site was designed using Figma before being implemented using the React framework in TypeScript. wagmi.sh was used to interact with the contract and mint the tokens. Hardhat Deploy is used in the backend to export a JSON file containing the contract addresses and ABIs which is used by the frontend to interact with the contract. In order to support typing for the contract ABI, we used a workaround from this issue on the TypeScript repo. I turned it into prestart
script in the package.json
file which converts the deployments.json
generated by Hardhat Deploy into a .ts
file:
"prestart": "echo \"declare const schema: $(cat src/deployments.json); export default schema;\" > src/deployments.json.d.ts"
The mint button which supports minting multiple tokens at once was the most interesting design challenge. We had designed a similar input for Onset Carbon’s storefront which we were planning to reuse but it didn’t quite fit the design of the rest of the site as it was too complex. We ended up placing the mint button inbetween a decrement and increment button while displaying the number of tokens to be minted along with the total price inside the button. This was a simple solution which worked well because it doesn’t strain the user’s attention too much and was easy to implement.
While trying to think of a way to make the site more interactive we decided to add some sounds. Each button interaction has a sound effect which is played when it is pressed. The increment and decrement buttons change pitch when pressed to give the user a sense of progression. The mint button has a sound effect which is played when the user presses it and is prompted to sign with their wallet and finally a whimsical effect is played when the transaction is confirmed.
Minting demo
Deployment
After testing on Goerli and ensuring that everything works properly (rendering, OpenSea support, attributes, etc.), we waited for a time that the hardhat-gas-reporter
plugin reported that the gas cost of deployment would be reasonable and deployed to mainnet.
The frontend is hosted on GitHub pages by following the React deployment guide and linked to our custom domain https://onchainsolar.systems.
Mint
Minting was open as soon as we launched the project, but we airdropped 10 tokens to Luke and myself before announcing it publicly. I shared the project on Farcaster followed by Twitter.
We had low expectations to begin with. I even suggested that we should decrease the total supply to 100 tokens to make it more exclusive because I never imagined that we would sell out.
Aged like milk
After 1 hour we had sold about 10 tokens but suddenly the sales started to pick up. After the first 20 or so sales in the first hour we were getting 100 or so sales per minute and next thing we knew we had sold out. It was a surreal experience and something I will never forget. The next 24 hours was followed by trending at #5 on OpenSea and $120k in trading volume which is something I never expected in my wildest dreams.
Refunds
After the mint I received a message from someone that had only received 1 token instead of the amount that they had paid for. This was a strange issue that must have been caused by manually increasing the value sent to the contract after initiating the mint transaction on our website. I refunded the user and wrote a script to find any other users who had this issue and fortunately only found 2 other instances and refunded them as well. In future contracts we will be sure to implement automatic refunding to prevent this issue from happening again.
Conclusion
We were encouraged to learn that there is such a strong demand for on-chain NFTs and hope to do (and see) more projects like this in the future. Seeing as Solar Systems was our first successful project, we would like to reward those that supported us with early access to our next project so please follow us on Twitter @stephancill, @npm_luko, @SolarSystemsNFT to stay up to date with the latest announcements.
🔭🌌