Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Tutorial Thumbnail
Beginner · 30 minutes

EVM-Based dApp on Algorand With Milkomeda A1

In this article, we will show how simple it is to deploy an EVM-based dApp on Algorand-Milkomeda A1, using mostly Python.

Milkomeda is a new protocol that brings EVM capabilities to non-EVM blockchains. At the time of writing, there are L2 solutions for Algorand, through the EVM-based Rollup Algorand-Milkomeda A1, and Cardano, through the EVM-based sidechain Cardano-Milkomeda C1.

The A1 Rollup uses wrapped ALGOs (milkALGOs) as its base currency, which can be easily bridged using the Milkomeda permission-less bridge. Users can wrap their ALGOs and other Algorand native assets (ASAs) onto the A1 Rollup with a few simple steps. This enables them to use their milkALGOs to interact with the EVM-based dApps deployed on the A1 Rollup.

Requirements

  • Install Brownie

Steps

1. Step - Setup an Algorand Wallet

There are several wallets one can use in Algorand. For an almost complete list, I point readers to the discover > wallets section on the Algorand Developer portal, but in this example, we will use Pera Wallet.

Pera Wallet is a self-custodial wallet, giving you complete control of your crypto. All wallet information is kept securely on your devices. It helps users interact directly with the Algorand blockchain while handling their own private keys by either storing them securely and encrypted in their local browser or by using a Ledger hardware wallet.

To set up a wallet:

  1. Visit https://web.perawallet.app/ and select “Create an account”
  2. Choose a passcode to encrypt your accounts locally, only on the device you are using
  3. Choose an account name

You should now have an Algorand address like the following image:

EditorImages/2023/03/07 22:16/pera.png

2. Step - Get Some Testnet ALGO

Now, go to Settings and change to “Testnet” in Node Settings, and then visit the Algorand Testnet Dispenser (https://testnet.algoexplorer.io/dispenser) and paste your newly created account address to get some testnet ALGOs.

EditorImages/2023/03/07 22:18/Screenshot_from_2023-03-07_22-17-49.png

You should now be able to see ten testnet ALGOs in your wallet.

Step 3 - Add the Milkomeda Algorand Testnet to Metamask

In Metamask, go to Settings > Networks > Add Networks and fill in the following information:

Network Name: Milkomeda Algorand Testnet
New RPC URL: https://rpc-devnet-algorand-rollup.a1.milkomeda.com
Chain ID: 200202
Currency Symbol (Optional): milkTALGO
Block Explorer URL (Optional): https://testnet-algorand-rollup.a1.milkomeda.com

Step 4 - Create a Dummy EVM Account To Test

To test the bridging of wrapped ALGOs to Milkomeda, let’s create a dummy EVM account with a simple Python snippet.

import secrets
from sha3 import keccak_256
from coincurve import PublicKey

private_key = keccak_256(secrets.token_bytes(32)).digest()
public_key = PublicKey.from_valid_secret(private_key).format(compressed=False)[1:]

addr = keccak_256(public_key).digest()[-20:]

print('private_key:', private_key.hex())
print('eth addr: 0x' + addr.hex())

This will generate a private key which you can now use to import the account into Metamask.

IMPORTANT: Please do not use an account generated like this for real funds. The randomness of the proposed process is insufficient to ensure the security of your funds.

Step 5 - Bridge Testnet ALGO to Milkomeda A1

Go to the Milkomeda A1 bridge page https://algorand-bridge-dev.milkomeda.com/ and follow these steps:

  1. Select “Devnet” in top right select box
  2. On Network Origin, select “Algorand to Milkomeda”
  3. On Token, select “ALGO” and enter desired amount
  4. Click “Connect Wallet” Algorand, select Pera Wallet and enter your password
  5. Click “Connect Wallet” Metamask to connect to your EVM address on A1
  6. Click “Next,” then “Sign and Send”
  7. Enter your “Pera Wallet” password again to sign the transaction

If all went well, you will see the following screen and should now see your bridged ALGOs in Metamask. Following the link will show the transaction on the A1 Bridge Explorer.

EditorImages/2022/11/23 20:00/1_8zr3lx8Ug4fPVvkZfyF68A.png

Step 6 - Compile and Deploy to Milkomeda A1 a SimpleStorage Contract Written in Solidity Using Brownie

Assuming one doesn’t have Brownie installed, create a virtual environment and install Brownie by:

python -m venv venv
source venv/bin/activate
pip install eth-brownie

Initialize a brownie project in a new working directory:

brownie init milkomeda && cd milkomeda

Now let’s create a very simple Solidity contract. In the contracts folder, create a file called Storage.sol, and add the following solidity code:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract Storage {
    uint256 number;

    function store(uint256 num) public {
        number = num;
    }

    function retrieve() public view returns (uint256){
        return number;
    }
}

Run the following command from the root of the created working directory to compile the contract:

brownie compile

To check the available networks that are configured in your brownie installation, run:

brownie networks list

EditorImages/2022/11/23 20:08/1_biNwxI6nGCjm1c7ppL4p3w.png

Milkomeda A1 will not be available by default, so we need to add it. To do that, either edit the file ~/.brownie/network-config.yaml and add the following lines:

- name: Milkomeda
    networks:
     - name: Algorand Testnet
       id: milkomeda-algorand-testnet
       host: https://rpc-devnet-algorand-rollup.a1.milkomeda.com
       chainid: 200202
       explorer: https://testnet-algorand-rollup.a1.milkomeda.com

OR use the brownie CLI:

brownie networks add Milkomeda milkomeda-algorand-testnet chainid=200202 explorer=https://testnet-algorand-rollup.a1.milkomeda.com host=https://rpc-devnet-algorand-rollup.a1.milkomeda.com name="Milkomeda Testnet"

If successful, one should now see it in the list, which can be queried with complete details by:

brownie networks list true

EditorImages/2022/11/23 20:01/1_CtkTuSJ7qL-2SLrGTzq_zA.png

We will add the private key to use the created (EVM) account. Create a file called brownie-config.yml in the root directory, and point to a private key from a .env file.

# brownie-config.yml
dotenv: .env
wallets:
 - dummy: ${PRIVATE_KEY}

Now, we have everything ready to deploy our Storage smart contract on Milkomeda A1. In the scripts folder, create a file named deploy.py, and add the following code:

from brownie import Storage, accounts, config

def main():
    signer = accounts.add(config["wallets"]["dummy"])
    Storage.deploy({"from": signer})

From brownie, we are importing Storage to be able to use the compiled contract, accounts so we can add the account by private key and config to be able to access the key/value pairs stored in the brownie-config.yml file.

Then, we can create the signer account and deploy the contract in the main function.

We can now deploy the contract on Milkomeda A1 by running the script from the terminal and indicating the A1 network:

brownie run scripts/deploy.py --network milkomeda-algorand-testnet

The output should be:

EditorImages/2022/11/23 19:45/1_KGyvSxBCIIlLPl9wNfLKwQ.png

The contract has been deployed, and you can check the transaction on the A1 Milkomeda Devnet explorer:

https://explorer-devnet-algorand-rollup.a1.milkomeda.com/tx/0x881eaedcdbe6b965d77bf84383d5e4235bb46aeeaaedffddc385ed4ed1b59909

EditorImages/2022/11/23 19:46/1_BqXc82WNs5M2HomZV2hdug.png

To interact with the smart contract, let’s create a separate file called call.py in the scripts directory and add the following code:

from brownie import Storage, Contract, accounts, config

signer = accounts.add(config["wallets"]["dummy"])

def main():
    contract_address = "0xE389A7d21a98497d953a3fc3bf283BF5107fc621"
    storage = Contract.from_abi("Storage", abi=Storage.abi, address=contract_address)

    stored_value = storage.retrieve()
    print("Current value is:", stored_value)

    storage.store(stored_value + 1, {"from": signer})

    stored_value = storage.retrieve()
    print("Current value is:", stored_value)

The only new import here is the Contract class to create the contract object by calling the .from_abi method, which takes name, abi, and contract address as inputs. The contract address was copied from the deployment output and hard coded here.

We then call the retrieve method on our contract to read the stored value in the “number” variable. Then we store a new value and read it again. To call this script from the terminal, run the following in the terminal:

brownie run scripts/call.py --network milkomeda-algorand-testnet

The output should be something like this:

EditorImages/2022/11/23 19:48/1_ncotDlEMJO5GAA8v-HpGPQ.png

And we are done! We have deployed and interacted with a contract on Milkomeda A1, so in a way, we have used an EVM-based smart contract on Algorand.

This tutorial could be easily adapted to any EVM-compatible chain, so it’s not necessarily Algorand-specific, but it goes to show how seamless it can be to port an existing EVM dApp to Algorand.

Bonus - Compile the Same Contract Using Vyper and Deploy Using web3py

We can now look at an example of deploying the same smart contract but written in Vyper, using only web3py.

First, we will need the abi and bytecode of the contract:

import vyper

source = """
# @version ^0.3.3
val: public(uint256)  # 0 to 2 ** 256 - 1
@external
def __init__():
    self.val = 0
@external
@view
def retrieve() -> uint256:
    return self.val
@external
def store(_val: uint256) -> uint256:
    self.val = _val
    return self.val
"""

compiled = vyper.compile_code(source, output_formats=['abi','bytecode'])
abi = compiled.get('abi')
bytecode = compiled.get('bytecode')

Now let’s connect to the Milkomeda A1 through the RPC URL.

from web3 import Web3

rpc_url = "https://rpc-devnet-algorand-rollup.a1.milkomeda.com"
chain_id = 200202

web3 = Web3(Web3.HTTPProvider(rpc_url))
print("Connected to Milkomeda:", web3.isConnected())

Set up the account from the generated private key (assuming it’s in the .env file)

from eth_account import Account
from eth_account.signers.local import LocalAccount
from dotenv import dotenv_values

config = dotenv_values(".env")
private_key = config['PRIVATE_KEY']

account: LocalAccount = Account.from_key(private_key)
print(f"Your wallet address is {account.address}")

balance = web3.eth.get_balance(account.address)
print(f"Balance: {web3.fromWei(balance, 'ether'):,.5}")

Create the contract instance from the abi and bytecode and call the constructor function to deploy the contract.

contract = web3.eth.contract(abi=abi, bytecode=bytecode)
transaction = contract.constructor().build_transaction({
    "from": account.address,
    'nonce' : web3.eth.getTransactionCount(account.address),
    'gas': 90000,
    'gasPrice': web3.toWei(50, 'gwei'),
    'chainId': chain_id
    })
signed_tx = web3.eth.account.sign_transaction(transaction, private_key)

tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
print(f"Waiting for transaction {web3.toHex(tx_hash)} to be included in a block...") 
response = web3.eth.wait_for_transaction_receipt(web3.toHex(tx_hash))
contract_address = response.get('contractAddress')
print("Contract deployed at:", contract_address)

Until this point, the code would produce the following output:

EditorImages/2022/11/23 19:50/1_m5UvwwnWN7u9_PZUAZuJJw.png

and we can look up the transaction or the deployed contract on the A1 devnet explorer:

https://explorer-devnet-algorand-rollup.a1.milkomeda.com/address/0x39013492b1bC84D9dF64d79e67D99f71F71BDA8B

Now to interact with the contract, we can call the retrieve function to get the stored value, change the value with the store function and then retrieve the value again.

deployed_contract = web3.eth.contract(abi=abi, address=contract_address)
stored_value = deployed_contract.functions.retrieve().call()
print("Stored value in contract:", stored_value)

new_value = stored_value + 1
print("Calling contract to store the value", new_value)

txn = deployed_contract.functions.store(new_value).build_transaction({
    "from": account.address,
    'nonce' : web3.eth.getTransactionCount(account.address),
    'gas': 90000,
    'gasPrice': web3.toWei(50, 'gwei'),
    'chainId': chain_id
    })
signed_tx = web3.eth.account.sign_transaction(txn, private_key)
tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
print(f"Waiting for transaction {web3.toHex(tx_hash)} to be included in a block...") 
response = web3.eth.wait_for_transaction_receipt(web3.toHex(tx_hash))

stored_value = deployed_contract.functions.retrieve().call()
print("New stored value in contract:", stored_value)

and the output would be:

EditorImages/2022/11/23 19:51/1_Ou537mdxQ_HpdK42sXXk4g.png