How I organize my Cadence projects
EDIT Oct 2024: The Cadence code in the article uses Cadence 0.42 which is incompatible with the current version of Cadence on all Flow networks. The themes and advice are all mostly still the same though but the code examples will need to be updated to the latest Cadence version.
Welcome back to my Cadence blog! Josh here! I’m a smart contract engineer on the Flow team at Dapper Labs and I’m putting out short bi-weekly blogs about our programming language, Cadence.
If you’re new to Cadence, please check out my introductory blog to get started!
Before I start with my main point, I’d like to point out an awesome blog post by one of the Flow community members: Benjamin Ebner. Benjamin explains some of the revolutionary features of Cadence and Flow and how they are well suited to smart contract programming. It is a great read and I would highly recommend checking it out if you haven’t already!
The value of organization and standardization
As many of you know, it is very important to keep your code clear and organized, ESPECIALLY for smart contract code. This includes the way you name fields and functions, how your organize the definitions and structure within a contract or transaction, and how well the code is documented. All of these are very important, and I’ll be writing a blog post some time in the future to share my thoughts and tips on what I believe to be the most effective way to organize Cadence code.
Today, I’ll be talking about something a little more high-level: How you organize the files for your project in your Github repo.
If you’re building a project that you hope other people will want to use, collaborate on, and hopefully extend or emulate one day, it is vitally important that you make your project as accessible as possible. Part of that is how your organize your project repository.
Projects built on Ethereum have the Truffle framework, which is a developer tool that helps devs get projects easily set up, testing, and deploying. We are early enough in Flow’s lifecycle that there isn’t a great automated solution for that at this point, but we hope there will be soon! We have a great team working on the Flow CLI that solves some of these problems, and there are some community efforts in the works to simplify the developer experience on Flow.
It is important for a developer community to align on standards for development workflows so the practices can be standardized, repeated and improved so that everyone benefits.
In the meantime, we have to manage it ourselves, and I think I’ve found a pretty solid way of doing it that I hope others will start implementing! If you have checked out the nba top shot, fungible token, nft, kitty items, or core contracts repos, then you’ve already been exposed to this.
The Structure
I’ve spent some time thinking about this and iterating on it, and with as complicated as Cadence smart contract interactions can be, I believe that that contracts and transactions should be front and center in a typical repo or project, with application code and language-specific packages either being of equal importance, or of lesser importance in the project hierarchy. The contracts and transactions are the core of how the projects function, are agnostic to whatever language everyone is choosing to write their apps in, and are fairly critical for a developer to understand and know where to find when first encountering a project.
This reasoning informed how I structured the repos for the Flow smart contracts.
The important directories are:
contracts/
transactions/
lib/
contracts/: This should house the main contracts for the project. Easily accessible from the top level.
transactions/: This should house all of the transactions and scripts that interact with the contracts and act as a single source of truth. The contents of this directory can be organized any way the developer wants, but would probably have a directory for each contract, with subdirectories for different types of users of the contract, and scripts, as I did with the staking contract:
idTableStaking/
admin/
delegation/
node/
scripts/
I’m not sold on the transactions
name, so if there is a better idea for the name, I’m all ears. You could also split scripts
into a separate top level directory depending on how you want to organize the project.
lib/: This is where I believe language-specific packages and libraries should live. As you can imagine, developers coming from many different backgrounds will want to learn how to interact with the contracts and repos, and exposing access to the contracts isn’t as simple as publishing an ABI that every language can import like in Solidity. Each language needs a set of packages for generating common transactions and scripts for the contracts.
In a mature contract repo, I would imagine something like this for lib/
lib/
js/
python/
cplusplus/
go/
rust/
java/
...
...
Then each language could have a contracts, templates, and test package, or whatever organization makes sense for the specific language or project. These packages would take the templates in contracts/
and transactions/
(source of truth top level directories) and package them for their specific language.
Right now, too many projects are just copying and pasting transactions into their application code, which works fine short term, but if there is ever a breaking change in Cadence or in the contract or transaction code, updating that copied and pasted code will be a huge hassle for projects.
Like any other software projects, contracts should publish releases for their transaction and contract packages in the languages they want to support, then projects that depend on them can simply update their versions.
This is how I have organized my contract repos, but so far I only have packages for Go. In the lib/go/
directory, I have these packages:
contracts
: Contains a package to get the contract code for every contract in the repo, replacing the import addresses with addresses that the caller specifies depending on the network being deployed to.templates
: Contains a package to get the code for every transaction and script template in the repo, replacing the import addresses with addresses that the caller specifies depending on the network being deployed to.test
: Contains emulator automated tests for the smart contracts and transactions. This makes use of the packages in thecontracts
andtemplates
directories for the test, as well as language specific utility methods to make unit testing easier.
I unfortunately haven’t gotten enough help in building packages for languages besides Go, so if anyone is interested in helping, I would appreciate it very much!
We’re planning on creating a testing framework that allows unit testing directly in Cadence, but that still is in very early stages. Once that is ready, I could imagine that another top level directory could contain tests/
that are written in Cadence.
Application Code
Lastly, the application code needs a place. This is where I have the least experience from app development best practices, but I think a project could choose between two options.
- Option 1: Use a different repo for the application: Since an application isn’t directly tied to a smart contract, an argument can be made that it should be in a different repo, and then import the packages from the main contract repo. This encourages the decentralized mindset, allows projects to protect proprietary application code, and de-clutters the main contract repo so that the focus can be on the most important part, the contracts.
- Option 2: Use the same repo, but put the application in one or more top level directories. This would make testing and running the application easier, but could complicate the organization of the repository.
I lean towards option 1, but I might be a little biased because I am primarily a smart contract developer. I acknowledge that I am probably missing some pros and cons for the different options for everything I’ve talked about, so I am definitely open for feedback. Even if my structure is deemed the preferred, it still needs a lot of streamlining to be the best it can be.
Conclusion
I’m excited to hear what everyone thinks! I’d like to start coming to consensus soon so the community can standardizing this. Feel free to leave comments here, or we also have a post in the Flow Forum where you can leave more feedback if you want.
Flow Discord: https://discord.gg/flow
Flow Forum: https://forum.onflow.org
Flow Github: https://github.com/onflow/flow
See you next week! 👋