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
- 2. Step - Get Some Testnet ALGO
- Step 3 - Add the Milkomeda Algorand Testnet to Metamask
- Step 4 - Create a Dummy EVM Account To Test
- Step 5 - Bridge Testnet ALGO to Milkomeda A1
- Step 6 - Compile and Deploy to Milkomeda A1 a SimpleStorage Contract Written in Solidity Using Brownie
- Bonus - Compile the Same Contract Using Vyper and Deploy Using web3py
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:
- Visit https://web.perawallet.app/ and select “Create an account”
- Choose a passcode to encrypt your accounts locally, only on the device you are using
- Choose an account name
You should now have an Algorand address like the following image:
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.
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:
- Select “Devnet” in top right select box
- On Network Origin, select “Algorand to Milkomeda”
- On Token, select “ALGO” and enter desired amount
- Click “Connect Wallet” Algorand, select Pera Wallet and enter your password
- Click “Connect Wallet” Metamask to connect to your EVM address on A1
- Click “Next,” then “Sign and Send”
- 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.
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
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
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:
The contract has been deployed, and you can check the transaction on the A1 Milkomeda Devnet explorer:
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:
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:
and we can look up the transaction or the deployed contract on the A1 devnet explorer:
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: