how to create dapps
> build your own dapps with the dapploader system.
page overview
0. writing dapp-files
a dapp-file is a single file that contains all the information necessary to create a dapploader dapp. this includes the dapp's metadata, the contract addresses and function signatures, the user interface styling, and the different "flows" the user can do.
to begin, all you have to do is make a plain text file with the extension ".dapp". you can name it whatever you want, but it is recommended to give it a name similar to the actual dapp name, so you can easily find it later.
you can edit and write the dapp-file in whatever text editor or development environment you want. it is just a type of text file. if you dont know where to start, we can recommend visual studio code or vim. even notepad will work.
the rules are relatively simple. the file must have a certain minimum of metadata, as explained further below here, and the rest of the schema must be parseable to get the dynamically generated user interface. otherwise, you will see an error message in the app.
the dapploader system is still very early in development, and there is not a full schema validator, compiler, or similar in existence yet, but we plan to build out development tools in the future, so you can more easily verify that your dapp-file is correct.
1. dapp-file structure
a dapp-file has three main parts: metadata, context, and flows.
the metadata is a set of properties describing the file's dapploader parser version (the "dpl" field), the dapp's name, description, version, and a couple optional parameters.
the context gives the dapp-file sufficient information about the smart contracts, addresses, and functions to interact with.
the flows detail the actual functionality of the dapp. each "flow", as we have called it, contains one or more steps, where the dapp gets user input, calls one or more smart contract function, and/or returns a response or user action (like signing a transaction).
# example.dapp
dpl: "v1.0.0" # required name: "my dapp" # required description: "what my dapp does" # required version: "1.0.0" # required author: "dev.eth" # optional website: "https://example.com" # optional contracts: # define contracts section token: # simple id for this contract address: "0x123..." functions: # define functions balanceOf: # function identifier # function details here flows: # define flows section - name: "check balance" # flow identifier description: "view token balance" # flow description steps: # steps in this flow - id: "my_step" # step identifier # step details here
the syntax of a dapp-file is simple, and is designed to be easily readable and editable. it looks like a yaml file, with a few specific tags and keys that the dapploader system requires to properly interpret the file.
if you have worked with github actions workflows for example, then this might look somewhat familiar. you define metadata and variables / context information, then write the actual flows or steps.
2. dapp-file metadata
the metadata section contains basic information about your dapp. this helps users identify your dapp in their local list, understand what it does, and know who created it.
all dapp-files must include the dpl, name, description, and version fields. author and website are optional but recommended. for now, just use dpl: "1.0.0" or "v1.0.0". this field will become more important later as we create new versions of the dapploader syntax standard, that may contain breaking changes.
# metadata section
dpl: "x.x.x" name: "dappname" description: "description" version: "x.x.x" # preferably use semver author: "author" # optional website: "url" # optional
a realistic example of dapp-file metadata:
# my-first-dapp.dapp
name: "my first dapp" description: "call read function on a smart contract" version: "0.1.0" author: "isu_dev" website: "https://x.com/isu_dev"
3. contracts, functions, and context
dapploader needs to know which contracts and corresponding functions you intend to call in the dapp-file. in the future, we may add support for auto-retrieving abis, but we start with this way to give you fine-control over what your dapp is able to call and not.
think of this like declaring variables or functions for later use in a script. it is also similar to cherry-picking an abi, as you only need to add definitions for the functions you will actually call directly. however, unlike an abi, we must add on a couple properties that affect the output values and their formatting (e.g. number of decimals).
here is the structure for defining contracts and their functions:
contracts: contract_id: # a simple identifier for this contract address: "0x123..." # the contract address chain: "ethereum" # optional, defaults to ethereum functions: function_id: # a simple identifier for this function signature: "functionName(uint256,address)" # function signature inputs: # function inputs - name: "amount" type: "uint256" description: "amount to send" # optional - name: "recipient" type: "address" outputs: # function outputs - name: "success" type: "bool" stateMutability: "nonpayable" # view, pure, payable, nonpayable
here is a practical example of defining a simple erc20 token contract:
contracts: erc20: # a simple id we'll use to reference this contract address: "0xaBc123DeF..." functions: totalSupply: # function id for totalSupply signature: "totalSupply()" inputs: [] # no inputs for this function outputs: - name: "supply" type: "uint256" stateMutability: "view" balanceOf: # function id for balanceOf signature: "balanceOf(address)" inputs: - name: "account" type: "address" outputs: - name: "balance" type: "uint256" stateMutability: "view" transfer: # function id for transfer signature: "transfer(address,uint256)" inputs: - name: "to" type: "address" - name: "amount" type: "uint256" outputs: - name: "success" type: "bool" stateMutability: "nonpayable"
for more complex dapp-files, with multiple contracts and a wide array of function calls, this part may seem rather tedious. but we are dependent on a proper and detailed context description for the dynamic interface to be correct. the key part is getting input and output definitions right.
the simple identifiers (contract_id, function_id) make it easy to reference these contracts and functions later in the flows section, without having to repeatedly type out long addresses and function signatures.
4. flows, steps, and function calling
each "flow" is displayed separately in the dapp user interface. they cover the inputs and function calls needed to achieve a goal. a user is only interacting with or using one flow at a time. think of them as self-contained silos of functionality.
a traditional dapp may have many flows. think of uniswap or similar ones, that have a lot of functionality, from trading to staking, and so on. as an example, let's consider (simplified) staking as one potential flow. trading would be another. and you can also make flows as you wish, down to the desired granularity. you can technically make a dapp-file for each functionality if you want to.
flows consist of one or more steps. each step has a specific type that determines its behavior and what it does in the dapp:
- input - collect information from the user
- read - call a read-only smart contract function
- write - call a smart contract function that changes state (requires transaction)
- conditional - branch the flow based on a condition
- display - show information to the user
in our simplified staking example, we assume we can just make 1 function call on an existing smart contract. all we need to input is the amount of tokens to stake. this will be a very simple flow, with an input and one call:
flows: - name: "stake tokens" description: "stake your tokens to earn rewards" steps: - id: "stake_details" type: "input" title: "enter stake amount" inputs: - id: "amount" label: "amount to stake" type: "number" required: true - id: "stake_tokens" type: "write" title: "staking tokens" contract: "staking" # id from contracts section function: "stake" # id from functions section params: - source: "input" input_id: "amount" - id: "stake_complete" type: "display" title: "staking complete" description: "your tokens have been staked successfully!" display: - label: "amount staked" value: "amount" format: "number" decimals: 18
while this 1-to-1 mapping of a smart contract function may be common, especially for reading information, more advanced dapps will require multiple steps. for example, there may be a need for an intermediary step to retrieve extra information based on inputs, before we can move on to the next step in the flow.
here's a more complex flow example that involves multiple steps and conditional logic:
flows: - name: "transfer tokens" description: "send tokens to another address" steps: - id: "transfer_details" type: "input" title: "transfer details" inputs: - id: "recipient" label: "recipient address" type: "address" required: true - id: "amount" label: "amount" type: "number" required: true - id: "check_balance" type: "read" title: "checking balance" contract: "token" function: "balanceOf" params: - source: "connected_wallet" # special value for connected wallet save_as: "user_balance" # save result for later use - id: "balance_check" type: "conditional" condition: "user_balance >= amount" if_true: "perform_transfer" # id of next step if true if_false: "insufficient_balance" # id of next step if false - id: "perform_transfer" type: "write" title: "Transferring Tokens" contract: "token" function: "transfer" params: - source: "input" input_id: "recipient" - source: "input" input_id: "amount" save_as: "transfer_result" - id: "transfer_success" type: "display" title: "transfer complete" description: "your tokens have been sent!" display: - label: "amount" value: "amount" format: "number" decimals: 18 - label: "recipient" value: "recipient" format: "address" - id: "insufficient_balance" type: "display" title: "insufficient balance" description: "you don't have enough tokens" display: - label: "your balance" value: "user_balance" format: "number" decimals: 18 - label: "required amount" value: "amount" format: "number" decimals: 18
in this example, we first collect input from the user, then check their balance, and conditionally execute either a token transfer or show an error message based on whether they have enough tokens.
note how we use the save_as property to store results for later use, and reference these values in subsequent steps. this allows data to flow through the entire process.
5. full dapp-file structure and example
let's put it all together with a complete, simple example of a token balance checker dapp:
# token-balance-checker.dapp
dpl: "1.0.0" name: "token balance checker" description: "check your token balance" version: "1.0.0" author: "dev.eth" contracts: erc20: address: "0xaBc123DeF..." functions: balanceOf: signature: "balanceOf(address)" inputs: - name: "account" type: "address" outputs: - name: "balance" type: "uint256" stateMutability: "view" flows: - name: "check balance" description: "check your token balance" steps: - id: "check_balance" type: "read" title: "checking balance" contract: "erc20" function: "balanceOf" params: - source: "connected_wallet" save_as: "user_balance" - id: "show_balance" type: "display" title: "your balance" display: - label: "token balance" value: "user_balance" format: "number" decimals: 18
this simple example shows how easy it is to create a basic dapp-file that can check a user's token balance. the flow is straightforward: call a smart contract function, then display the result.
here's a more comprehensive example with multiple flows:
# token-manager.dapp
dpl: "1.0.0" name: "token manager" description: "manage your erc20 tokens" version: "1.0.0" author: "dev.eth" website: "https://example.com" contracts: erc20: address: "0xaBc123DeF..." functions: name: signature: "name()" inputs: [] outputs: - name: "name" type: "string" stateMutability: "view" symbol: signature: "symbol()" inputs: [] outputs: - name: "symbol" type: "string" stateMutability: "view" decimals: signature: "decimals()" inputs: [] outputs: - name: "decimals" type: "uint8" stateMutability: "view" totalSupply: signature: "totalSupply()" inputs: [] outputs: - name: "supply" type: "uint256" stateMutability: "view" balanceOf: signature: "balanceOf(address)" inputs: - name: "account" type: "address" outputs: - name: "balance" type: "uint256" stateMutability: "view" transfer: signature: "transfer(address,uint256)" inputs: - name: "to" type: "address" - name: "amount" type: "uint256" outputs: - name: "success" type: "bool" stateMutability: "nonpayable" flows: - name: "token info" description: "view basic token information" steps: - id: "get_token_info" type: "read" title: "loading token info" contract: "erc20" function: "name" params: [] save_as: "token_name" - id: "get_token_symbol" type: "read" contract: "erc20" function: "symbol" params: [] save_as: "token_symbol" - id: "get_token_decimals" type: "read" contract: "erc20" function: "decimals" params: [] save_as: "token_decimals" - id: "get_total_supply" type: "read" contract: "erc20" function: "totalSupply" params: [] save_as: "total_supply" - id: "show_token_info" type: "display" title: "token information" display: - label: "name" value: "token_name" format: "string" - label: "symbol" value: "token_symbol" format: "string" - label: "decimals" value: "token_decimals" format: "number" - label: "total supply" value: "total_supply" format: "number" decimals: "token_decimals" # dynamic decimals from previous step - name: "check balance" description: "check your token balance" steps: - id: "check_balance" type: "read" title: "your balance" contract: "erc20" function: "balanceOf" params: - source: "connected_wallet" save_as: "user_balance" - id: "get_decimals_for_balance" type: "read" contract: "erc20" function: "decimals" params: [] save_as: "token_decimals" - id: "show_balance" type: "display" title: "your balance" display: - label: "token balance" value: "user_balance" format: "number" decimals: "token_decimals" - name: "transfer tokens" description: "send tokens to another address" steps: - id: "transfer_details" type: "input" title: "transfer details" inputs: - id: "recipient" label: "recipient address" type: "address" required: true - id: "amount" label: "amount" type: "number" required: true - id: "check_balance" type: "read" title: "checking balance" contract: "erc20" function: "balanceOf" params: - source: "connected_wallet" save_as: "user_balance" - id: "get_decimals" type: "read" contract: "erc20" function: "decimals" params: [] save_as: "token_decimals" - id: "balance_check" type: "conditional" condition: "user_balance >= amount" if_true: "perform_transfer" if_false: "insufficient_balance" - id: "perform_transfer" type: "write" title: "transferring tokens" contract: "erc20" function: "transfer" params: - source: "input" input_id: "recipient" - source: "input" input_id: "amount" save_as: "transfer_result" - id: "transfer_success" type: "display" title: "transfer complete" description: "your tokens have been sent!" display: - label: "amount" value: "amount" format: "number" decimals: "token_decimals" - label: "recipient" value: "recipient" format: "address" - id: "insufficient_balance" type: "display" title: "insufficient balance" description: "you don't have enough tokens" display: - label: "your balance" value: "user_balance" format: "number" decimals: "token_decimals" - label: "required amount" value: "amount" format: "number" decimals: "token_decimals"
a more complex dapp-file will have multiple flows with more steps, and may use more functions and smart contracts. the syntax to add more functions, flows, or even steps is straightforward. we recommend trying and experimenting as much as possible.
as you can see in the examples above, dapp-files can range from very simple to quite complex, depending on your needs. they always follow the same structure:
- define dapp metadata at the top
- list the contracts and functions you'll interact with
- create flows with step-by-step instructions
the step types (input, read, write, conditional, and display) can be combined in various ways to create complex interactions, while keeping the syntax simple and readable.
6. understanding step types in detail
let's take a deeper look at each of the step types available in dapp-files and how they work:
input steps
input steps collect information from the user. they generate form fields in the user interface based on the inputs you define.
- id: "user_input_step" type: "input" title: "enter details" description: "please provide the following information" # optional inputs: - id: "amount" label: "amount" type: "number" # address, number, string, boolean, select placeholder: "enter amount" # optional required: true validation: # optional min: 0 max: 100
read steps
read steps call read-only (view/pure) functions on smart contracts. they don't change blockchain state and don't require transactions.
- id: "read_balance" type: "read" title: "checking balance" description: "verifying your token balance" # optional contract: "token" # id from contracts section function: "balanceOf" # id from functions section params: - source: "connected_wallet" # special value for the connected wallet address # or - source: "input" input_id: "user_address" save_as: "user_balance" # variable name to store the result
write steps
write steps call state-changing functions on smart contracts, requiring the user to sign transactions. these steps will connect to the user's wallet.
- id: "send_tokens" type: "write" title: "send tokens" description: "executing token transfer" # optional contract: "token" function: "transfer" params: - source: "input" # reference to user input input_id: "recipient" - source: "input" input_id: "amount" estimate_gas: true # optional: show gas estimate before execution save_as: "transfer_result" # optional: save the result
conditional steps
conditional steps create branching logic in your flows, allowing different paths based on conditions.
- id: "check_sufficient_balance" type: "conditional" title: "verifying balance" # optional condition: "user_balance >= amount" # simple expression if_true: "send_tokens" # id of step to execute if true if_false: "show_insufficient_balance" # id of step if false
display steps
display steps show information to the user. they can display results from previous steps or other information.
- id: "transfer_result" type: "display" title: "transfer complete" description: "your transfer was successful" # optional display: - label: "amount sent" value: "amount" # variable name from previous steps format: "number" # optional: number, address, string decimals: 18 # optional: for number formatting - label: "to address" value: "recipient" format: "address" # formats as an ethereum address
these step types can be combined in various ways to create complex dapp functionality while keeping the implementation straightforward.
7. future features
the dapploader system is just in its infancy, and we have many plans for future features and improvements. the dapp-file specification and syntax may also change over time, as we learn more about what developers need and want.
some features we're considering for future versions:
- auto-retrieving abis - automatically fetch function signatures and abis from blockchain explorers
- custom ui styling - allow dapp creators to customize the look and feel of their dapps
- validator tools - tools to validate dapp-files and catch errors before deployment
- template library - pre-built templates for common dapp patterns
- multiple chains support - better support for multi-chain dapps
- more complex logic - expanded capabilities for conditional logic and data handling
we plan to build out an ecosystem of tools and services to help developers create, test, and validate their dapps more easily. there is also work to be done for enabling dapp-files to call external services, and use more complex smart contract interactions.
for any questions, contact us on telegram.
disclaimer: you are responsible for all dapps you create and run, all edits and changes you make, and how those changes affect the functionality and outcomes of the dapps. it is your responsibility to make sure the dapp-files you use are safe.