Skip to content

Build dApp from scratch

This guide will explore the basics of creating an Alephium dApp project.

Prerequisites


Create a new dApp project: Token Faucet

In this tutorial we will write our first dApp: A token faucet.

The code here is taken from our getting started page, but we will see step by step how we build this guide.

Create a new project folder and navigate into it:

Bash
mkdir alephium-faucet-tuto
cd alephium-faucet-tuto

Let's now create a contracts folder where we'll store all our contracts:

Bash
mkdir contracts

Our first contract will be token.ral which can be found here. You can copy the whole file into your contracts folder.

Let's inspect it, piece by piece:

token.ral
Rust
import "std/fungible_token_interface"

Contract TokenFaucet(
    symbol: ByteVec,
    name: ByteVec,
    decimals: U256,
    supply: U256,
    mut balance: U256
) implements IFungibleToken {

The first four fields will be immutable values that store the data required to serve our IFungibleToken interface. mut balance is a mutable value that keeps track of how many tokens are left in this faucet.

You can see that our contract emits an event and defines an error code. Read the following for more info on events and error handling.

This is followed by 5 access methods for the different contract's arguments.

The last method is where the magic happens:

token.ral
Rust
@using(assetsInContract = true, updateFields = true, checkExternalCaller = false)
pub fn withdraw(amount: U256) -> () {
    // Debug events can be helpful for error analysis
    emit Debug(`The current balance is ${balance}`)

    // Make sure the amount is valid
    assert!(amount <= 2, ErrorCodes.InvalidWithdrawAmount)
    // Functions postfixed with `!` are built-in functions.
    transferTokenFromSelf!(callerAddress!(), selfTokenId!(), amount)
    // Ralph does not allow underflow.
    balance = balance - amount

    // Emit the event defined earlier.
    emit Withdraw(callerAddress!(), amount)
}

With the assert! we make sure no one takes more than 2 tokens at the same time.
transferTokenFromSelf will actually perform the transfer of the tokens.
We update the mut balance field with the new balance. In the case of underflow, an error will be raised and the transaction won't be performed. callerAddress!() and selfTokenId!() are built-in functions, you can read more about them in our built-in functions page.


Compile your contract

The compiler needs to contact the full node in order to compile the contract, you'll need to use the right information defined while creating your devnet. If you haven't start it, now it's the time. We define the node URL using the following config file: alephium.config.ts. Create this file in the root directory of your project and paste the following code:

alephium.config.ts
TypeScript
import { Configuration } from '@alephium/cli'

export type Settings = {}

const configuration: Configuration<Settings> = {
  networks: {
    devnet: {
      //Make sure the two values match what's in your devnet configuration
      nodeUrl: 'http://localhost:22973',
      networkId: 2
    }
  }
}

export default configuration

Now, let's compile:

Bash
npx @alephium/cli@latest compile

It may ask you for some confirmation to install the latest @alephium/cli package. Select yes to proceed.

Once the above command succeeds, you will notice that a new folder called artifacts was created. It contains several files related to your contract. For example, artifacts/ts/TokenFaucet.ts produces lots of helper functions like at, fetchState, call*, etc, as well as many test functions.


Test your contract

The SDK provides unit test functionalities, which call the contract by sending a transaction, but instead of changing the blockchain state, it returns the new contract state, transaction outputs, and events.

Install the test framework:

Bash
npm install ts-jest @types/jest

You'll also need our @alephium/web3 package:

Bash
npm install @alephium/web3 @alephium/web3-test

Create a test folder:

Bash
mkdir test

and create the test/token.test.ts minimalistic test file with the following contents:

test/token.test.ts
TypeScript
import { web3, Project, addressFromContractId } from '@alephium/web3'
import { randomContractId, testAddress } from '@alephium/web3-test'
import { TokenFaucet } from '../artifacts/ts'

describe('unit tests', () => {
  it('Withdraws 1 token from TokenFaucet', async () => {

    // Use the correct host and port
    web3.setCurrentNodeProvider('http://127.0.0.1:22973')
    await Project.build()

    const testContractId = randomContractId()
    const testParams = {
      // a random address that the test contract resides in the tests
      address: addressFromContractId(testContractId),
      // assets owned by the test contract before a test
      initialAsset: { alphAmount: 10n ** 18n, tokens: [{ id: testContractId, amount: 10n }] },
      // initial state of the test contract
      initialFields: {
        symbol: Buffer.from('TF', 'utf8').toString('hex'),
        name: Buffer.from('TokenFaucet', 'utf8').toString('hex'),
        decimals: 18n,
        supply: 10n ** 18n,
        balance: 10n
      },
      // arguments to test the target function of the test contract
      testArgs: { amount: 1n },
      // assets owned by the caller of the function
      inputAssets: [{ address: testAddress, asset: { alphAmount: 10n ** 18n } }]
    }

    const testResult = await TokenFaucet.tests.withdraw(testParams)
    console.log(testResult)
  })
})

A more complex test can be found in our template project.

Without entering too much into details, TypeScript needs some configuration to run the test so just create a file called tsconfig.json in the root directory of your project and paste the following code:

tsconfig.json
JSON
{
  "compilerOptions": {
    "outDir": "dist",
    "target": "es2020",
    "esModuleInterop": true,
    "module": "commonjs",
    "resolveJsonModule": true
  },
  "exclude": ["node_modules"],
  "include": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts", "alephium.config.ts", "artifacts/**/*.ts"]
}

You can now run the test:

Bash
npx @alephium/cli@latest test

You should be able to see on your terminal the output of calling the withdraw method.

🎉🎉 Congratulations 🎉🎉

Have created your first contract and written a test to call it and test it locally! It's time to deploy your contract.


Deploy your contract

Now things are getting serious, we will deploy our contract on our devnet 🚀

The deploy command will execute all deployment scripts it finds inside the scripts folder. Create the scripts folder in the root folder of the project:

Bash
mkdir scripts

Let's create a deployment script file called 0_deploy_faucet.ts into the scripts folder and paste the following code.
Note that deployment scripts should always be prefixed with numbers (starting from 0).

0_deploy_faucet.ts
TypeScript
import { Deployer, DeployFunction, Network } from '@alephium/cli'
import { Settings } from '../alephium.config'
import { TokenFaucet } from '../artifacts/ts'

// This deploy function will be called by cli deployment tool automatically
// Note that deployment scripts should prefixed with numbers (starting from 0)
const deployFaucet: DeployFunction<Settings> = async (
  deployer: Deployer
): Promise<void> => {
  const issueTokenAmount = 100n
  const result = await deployer.deployContract(TokenFaucet, {
    // The amount of token to be issued
    issueTokenAmount: issueTokenAmount,
    // The initial states of the faucet contract
    initialFields: {
      symbol: Buffer.from('TF', 'utf8').toString('hex'),
      name: Buffer.from('TokenFaucet', 'utf8').toString('hex'),
      decimals: 18n,
      supply: issueTokenAmount,
      balance: issueTokenAmount
    }
  })
  console.log('Token faucet contract id: ' + result.contractInstance.contractId)
  console.log('Token faucet contract address: ' + result.contractInstance.address)
}

export default deployFaucet

The deployContract of the Deployer takes our contract and deploys it with the correct arguments. You can also add a taskTag argument to tag your deployment with a specific name. By default, it will use the contract name, but if you deploy the same contract multiple times with different initial fields, your .deployment file will get overridden. Using a specific taskTag solves this issue.

From the DeployContractParams interface, we can see that initialFields is mandatory as it contains the arguments for our TokenFaucet contract.

With issueTokenAmount you can define how many tokens you want to issue, this is required if you want to create a token, otherwise no token-id will be created.

Now, let's deploy!

Bash
npx @alephium/cli@latest deploy

...OOPS... It doesn't work???

If you got the error The node chain id x is different from configured chain id y, go check your networkId in the devnet configuration and the alephium.config.ts file.

No UTXO found ???

Of course we didn't provide the how-to-use-my-utxos, we need to define our privateKeys.

You'll need to export the private keys from our wallet extension (might do it from our other wallets later), make sure to use a wallet with funds, like the one from the genesis allocation of your devnet. If you used the docker way to launch your devnet, it might have work as we are defining a default private key in our cli package based on the genesis allocation.

Let's update our alephium.config.ts

`alephium.config.ts
TypeScript
const configuration: Configuration<void> = {
  networks: {
    devnet: {
      nodeUrl: 'http://localhost:22973',
      networkId: 2,
      //The private key of my genesis address 132mqFF2BuxGigdaMTGSruuW29kmEs2eEGcpquG4YZRNh
      privateKeys: ['672c8292041176c9056bb0dd1d91d34711ceed2493b5afc83f2012b27df2c559']
    }
  }
}

CAUTION

Real applications should use environment variables or similiar techniques for senstivie settings like privateKeys. Do not commit your private keys to source control.

and retry to deploy:

Bash
npx @alephium/cli@latest deploy
Bash
Contracts are compiled already. Loading them from folder "artifacts"
Deploying contract TokenFaucet
Deployer - group 1 - 132mqFF2BuxGigdaMTGSruuW29kmEs2eEGcpquG4YZRNh
Token faucet contract id: d00e9c788ddd572b0c186f0599a264f4c79f009c632c8040b7c5f71bfc0ec301
Token faucet contract address: 28h7qSmkAAeNyoBuQKGyp1WG8VfdKPePCCFGKwp2Y8yyA
 Deployment scripts executed!

Congratulations! Your contract is deployed. We can check the balance of the contract. Use curl and change the contract address based on your deployment result:

Bash
curl 'http://localhost:22973/addresses/28h7qSmkAAeNyoBuQKGyp1WG8VfdKPePCCFGKwp2Y8yyA/balance'

The response should look like this:

response
JSON
{
  "balance": "1000000000000000000",
  "balanceHint": "1 ALPH",
  "lockedBalance": "0",
  "lockedBalanceHint": "0 ALPH",
  "tokenBalances": [
    {
      "id": "d00e9c788ddd572b0c186f0599a264f4c79f009c632c8040b7c5f71bfc0ec301",
      "amount": "100"
    }
  ],
  "utxoNum": 1
}

We can see our token id, with the 100 tokens we decided to issue.

Let's check the contract state by first getting the group of our address:

Bash
curl 'http://localhost:22973/addresses/28h7qSmkAAeNyoBuQKGyp1WG8VfdKPePCCFGKwp2Y8yyA/group'
curl 'http://localhost:22973/contracts/28h7qSmkAAeNyoBuQKGyp1WG8VfdKPePCCFGKwp2Y8yyA/state?group=1'

Contract state response:

response
JSON
{
  "address": "28h7qSmkAAeNyoBuQKGyp1WG8VfdKPePCCFGKwp2Y8yyA",
  "bytecode": "050609121b4024402d404a010000000102ce0002010000000102ce0102010000000102ce0202010000000102ce0302010000000102a0000201020101001116000e320c7bb4b11600aba00016002ba10005b416005f",
  "codeHash": "641343b4f1c08b03969b127b452acc7535cad20231bc32af6c0b5f218dd8ff0c",
  "initialStateHash": "06595afa695949e915dfc1220dfb47125b01751d9e193f4c5fa1c7fc3566673d",
  "immFields": [
    {
      "type": "ByteVec",
      "value": "5446"
    },
    {
      "type": "ByteVec",
      "value": "546f6b656e466175636574"
    },
    {
      "type": "U256",
      "value": "18"
    },
    {
      "type": "U256",
      "value": "100"
    }
  ],
  "mutFields": [
    {
      "type": "U256",
      "value": "100"
    }
  ],
  "asset": {
    "attoAlphAmount": "1000000000000000000",
    "tokens": [
      {
        "id": "d00e9c788ddd572b0c186f0599a264f4c79f009c632c8040b7c5f71bfc0ec301",
        "amount": "100"
      }
    ]
  }
}

In the immFields we can see our initial TokenFaucet arguments (symbol, name, decimals, supply). We can also see that mutFields contains the current token balance. We'll check that field later after calling the faucet.

The deploy command also created a .deployments.devnet.json file, with the deployment result. It's important to keep that file to easily interact with the contract, even though all information can be found on the blockchain.


Interact with the deployed contract

Having a token faucet is nice, getting tokens from it is even better.

We can now write some code to interact with the faucet contract.

We'll need to install our cli package and the typescript dependency if it's not yet the case:

Text Only
npm install @alephium/cli typescript

We will now see a different option to interact with the blockchain. Previously we were using the DeployFunction with our scripts/<number>_* files which are automatically deployed with the CLI tool.

Another way is to create a skeleton web application project using TypeScript. Create a src folder in the root folder of the project and a file called tokens.ts in it with the following contents.

tokens.ts
TypeScript
import { Deployments } from '@alephium/cli'
import { DUST_AMOUNT, web3, Project, NodeProvider } from '@alephium/web3'
import { PrivateKeyWallet} from '@alephium/web3-wallet'
import configuration from '../alephium.config'
import { TokenFaucet, Withdraw } from '../artifacts/ts'

async function withdraw() {

  //Select our network defined in alephium.config.ts
  const network = configuration.networks.devnet

  //NodeProvider is an abstraction of a connection to the Alephium network
  const nodeProvider = new NodeProvider(network.nodeUrl)

  //Sometimes, it's convenient to setup a global NodeProvider for your project:
  web3.setCurrentNodeProvider(nodeProvider)

  //Connect our wallet, typically in a real application you would connect your web-extension or desktop wallet
  const wallet = new PrivateKeyWallet({privateKey: '672c8292041176c9056bb0dd1d91d34711ceed2493b5afc83f2012b27df2c559' })

  // Compile the contracts of the project if they are not compiled
  Project.build()

  //.deployments contains the info of our `TokenFaucet` deployement, as we need to now the contractId and address
  //This was auto-generated with the `cli deploy` of our `scripts/0_deploy_faucet.ts`
  const deployments = await Deployments.from('.deployments.devnet.json')

  //Make sure it match your address group
  const accountGroup = 1

  const deployed = deployments.getDeployedContractResult(accountGroup, 'TokenFaucet')

  if(deployed !== undefined) {
    const tokenId = deployed.contractInstance.contractId
    const tokenAddress = deployed.contractInstance.address

    // Submit a transaction to use the transaction script
    // It uses our `wallet` to sing the transaction.
    await Withdraw.execute(wallet, {
      initialFields: { token: tokenId, amount: 1n },
      attoAlphAmount: DUST_AMOUNT
    })

    // Fetch the latest state of the token contract, `mut balance` should have change
    const faucet = TokenFaucet.at(tokenAddress)
    const state = await faucet.fetchState()
    console.log(state.fields)

    // Fetch wallet balance see if token is there
    const balance = await wallet.nodeProvider.addresses.getAddressesAddressBalance(wallet.account.address)
    console.log(balance)
  } else {
    console.log('`deployed` is undefined')
  }
}

// Let's perform one withdraw
withdraw()

For the attentive people, you'll see something new coming from our artifacts: Withdraw which is a TxScript required to interact with the TokenFaucet contract. Its code is quite simple. Create a file called withdraw.ral in the contracts folder and paste the following code:

withdraw.ral
Rust
TxScript Withdraw(token: TokenFaucet, amount: U256) {
    token.withdraw(amount)
}

We now need to recompile our contracts to get the artifact for Withdraw:

Bash
npx @alephium/cli@latest compile

You can now compile the TypeScript code to JavaScript with:

Bash
npx tsc --build .

OOPS, you should get an error coming from the alephium.config.ts, until now the config was used as a simple JSON, but now TypeScript want it to respect its interface. Especially the networks is a record that need to contain the 3 NetworkType. You can try to fix it by yourself or update your alephium.config.ts file with:

alephium.config.ts
TypeScript
import { Configuration } from '@alephium/cli'

export type Settings = {}

const configuration: Configuration<Settings> = {
  defaultNetwork: 'devnet',
  networks: {
    devnet: {
      nodeUrl: 'http://localhost:22973',
      networkId: 2, //Use the same as in your devnet configuration
      privateKeys: ['672c8292041176c9056bb0dd1d91d34711ceed2493b5afc83f2012b27df2c559'],
      settings: {}
    },
    testnet: {
      nodeUrl: '',
      privateKeys: [],
      settings: {}
    },
    mainnet: {
      nodeUrl: '',
      privateKeys: [],
      settings: {}
    }
  }
}

export default configuration

Now recompile

Text Only
npx tsc --build .

A dist folder should have been created, go ahead and interact with the deployed token faucet:

Text Only
node dist/src/token.js

You should now be a proud owner of the token you created.


What's next?

You can find a more complex example of the token faucet tutorial in the alephium/nextjs-template project.


Connect to the wallets

dApp requires wallet integration for users of the dApp to authenticate and interact with the Alephium blockchain, such as transactions signing. Currently dApps can be integrated with both Extension Wallet and WalletConnect. Please refer to the respective pages for more details.


Learn more