HOW-TO: Use Arweave with ERC721s

There’s been no shortage of discussion about the levels of on-chain-ness an NFT can possess. This is a wonderful deep dive into the various flavors and techniques that are employed in the pursuit of maximum on-chain existence. But since everything in NFT world is moving fast and a lot of documentation is sadly locked away in private Discords, hidden from search engines, sometimes it can be hard to find actionable guidance when it comes to certain details.

It wasn’t clear to me how to practically host my NFT assets on Arweave in such a way that it aligned with standard ERC721 contracts, and it took some time sort out. Now that I dealt with that I can save you some time. This all came together for me during the development of Turf NFT, where I’m the lead dev.

This mini-guide assumes a decent level of familiarity with Solidity and smart contracts in general. You don’t have to be a pro, but I’m glossing over some things that may confuse a total newb.

Skip down to the bottom if you are a pro and don’t need this little intro.

Back to on-chain-ness: On one side of the spectrum are 100% on-chain NFTs.

You’ve got your contract generating all the data required for displaying an NFT, straight from the blockchain: the metadata and the visual asset, probably in the form of an SVG, are all returned from the tokenURI method as a whole. This chunk of data is ready to be parsed by NFT platforms and displayed without pulling in any external assets. It’s a pure, perfect package. Very high style points as well, especially among the on-chain connaisseurs.

At the other end are off-chain projects. These consist of a collection of JSON data files and assets stored on a traditional hosting platform, like AWS S3. Or a Mac Mini in your closet running Apache, whatever. Your tokenURI method probably returns the URL to that JSON for a given ID, e.g, https://turf-assets.s3.us-west-1.amazonaws.com/api/0.json. And that file, in turn, points to an image living somewhere like https://turf-assets.s3.us-west-1.amazonaws.com/plots/0.png. Terrific. Simple.

Most people take this approach. It’s easier. you don’t need to perform string manipulation magic in your contract to return all your data. You can use any kind of binary file (e.g, MP4, JPG, etc) for your assets, all hosted on a normal web server. It’s not very web3, however!

If you stop paying your hosting bills, or you break your web server, all those nice GIFs you sold for hundreds of dollars (?!) are going to vanish, and people will be (rightfully) pissed. You don’t need that in your life. You’re trying to do this right.

And those with suspicious minds may point out that you could just delete the assets maliciously, or tamper with them in some way after the mint.

So, being a responsible steward of your project, you seek other options.

The most common alternative is probably IPFS, managed through a platform like Pinata. You put your assets up on there, and, magically they’re hosted forever in a decentralized way! Aside from the fact that you need to fuss around with file pinning and maybe run your own pinning service, or just hope Pinata never goes away, but whatever.

There’s another solid option, and this is what we’re using for Turf: Arweave. From the point of view of an asset uploader (us), it’s simple: you pay once and then the system hosts your assets forever. No pinning, no additional maintenance.

The only wrinkle is that you have to pay in AR tokens, and if you live in the US it’s a pretty big hassle to obtain them. I had to Venmo a friend of a friend of a friend who sent me some tokens. One could probably make a pretty good living just selling exotic tokens to people for cash, btw.

There’s good documentation (and some services) that will guide you through uploading arbitrary files, but it’s not super obvious how to manage files in a manner that ERC721s expect. You can’t have random URLs per token’s metadata files, since you won’t be able to easily return them to readers of your tokenURI. You want a consistent root path (your ERC721’s baseURI), and you want to put your specifically numbered JSON files on there: 1.json, 2.json, etc.

This is how you can do it, as of early 2022. First we’ll upload your image (or audio or whatever) assets, since we need to know where those live before we can create our JSON files and upload those.

  1. Get the Chrome wallet extension. Set it up and somehow (god speed!) get some AR tokens. Then export your key file and save it locally. That option is in the extension somewhere. It should look like arweave-keyfile-XXX-XXX-XXXX.json.
  2. Install arkb.
  3. Now point arkb at your asset directory (and your wallet key file) and upload those files:arkb deploy /path-to-assets/ --wallet /path/arweave-keyfile-XXX.json
  4. That should output something like the following:
arkb output
arkb output

You’ll notice in my example I have uploaded three files, 1.txt, 2.txt, and 3.txt. You can ignore the manifest file, that’s automatically managed.

The last line there is the important line. That’s your root path. https://arweave.net/K0yYHLdtb6bWYZCHv3XJYJQmkRfVtFdos0Wrv5lyvf4

And that means you can refer to your specific files by filename in that directory, e.g, 1.txt.

Now that you’ve got Arweave URLs for each image, put those into the appropriate JSON files, per token. It’ll probably look like this

{
  "name": "Turf Plot [19, -12]",
  "image": "ar://K0yYHL...s0Wrv5lyvf4/42.png",
...
}

Note the ar:// prefix! This is important. You can access the file via HTTPS for debugging and review, but that assumes the server at https://arweave.net will remain functioning and accessible forever, and that defeats the purpose of decentralization.

We don’t want a traditional web server involved. By using the ar:// prefix we are telling OpenSea (or whoever) to use their own gateway to load the assets directly from the Arweave blockweave, and that network’s gonna last a million years, right?

BIG CAVEAT: NOT ALL NFT PLATFORMS SUPPORT THIS PREFIX! OpenSea does, Rarible doesn’t. You might want to stick with the https://arweave.net URL version for now, and change it over in the future when it’s safe.

Now upload your JSON files the same way. You’ll receive a new root hash. Set your contract’s baseURI to it: ar://<the hash>/, and now your tokenURI method will return ar://<the hash>/1, assuming 1 is the given token ID.

That’s it! We’ve achieved decentralization. Tell your friends.

But, there’s more!

To truly lock things up, and prevent any meddling, you might consider an option to lock your contract’s baseURIsetter permanently. This way you can never move your assets back to S3, or change them in any way.

This assumes you have a baseURI setter. You should. This is necessary if you intend to start hosting your assets on S3 and only switch over to the immutable blockchain hosting after everything’s settled down. For Turf, for example, this was important because we needed to go back and clean up some metadata quirks. Correcting them was simply a matter of adjusting the JSON (after discussion with the community, of course).

Now that we’re ready to lock it all up we’ll change our baseURI over to our new Arweave directory and eventually call our lockBaseTokenURI() method, preventing us from changing it again.

Below you’ll find the three important methods. Our locker, our setters, and finally the standard tokenURI method. You can view the whole contract here.

/// @param baseTokenURI_ The new baseTokenURI
/// @dev Need this so we can set the new base URI for the cut over to permaweb.
function setBaseURI(string memory baseTokenURI_) external onlyOwner {
  require(!baseTokenURILocked, "setBaseURI is locked");
  baseTokenURI = baseTokenURI_;
}
/// @dev After we cut over to the permaweb base URI, lock it up so we can't change it back. This is a one-time operation! Don't mess it up!
function lockBaseTokenURI() external onlyOwner {
  baseTokenURILocked = true;
}

function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
  require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
  string memory baseURI = mysteryZones[tokenId] ? baseMysteryZoneTokenURI : baseTokenURI;
  return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, Strings.toString(tokenId), '.json')) : "";
}

Good luck!

Big thank you to the folks in the Def Discord and the Arweave Discord for putting up with a ton of questions over the past few months. Especially @cozycostudio for pointing out that a directory structure was even possible, that was major pro tip!

Subscribe to doug
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.