Engineering
December 10, 2021

Alt Crypto Wallet

Following our recent experiments on the Evmos blockchain, we decided to create a crypto wallet to deepen our understanding wallet infrastructure. To quote David Deutsch, "If you can’t program it, you don’t understand it"

Unlike traditional finance wallets, a crypto wallet does not have custody of your funds, it's simply a tool to query the blockchain and visualize your assets through a UI instead of interacting directly with the blockchain.

Alt's crypto wallet is essentially a User Interface that allows users to see what assets they own (fungible and non-fungible tokens) and to transfer them to other accounts seamlessly.

We built this Proof Of Concept wallet with React, and used the ethers.js library to interact with the blockchain. This blogpost is structured to make the user journey:

  1. Account creation
  2. Accessing blockchain data
  3. Signing and Sending Transactions

We will not display all the code here but you can find the entire project codebase here.

Check out the Wallet.

All code provided in this blogpost is extracted from React classes.

Account Creation

Cryptographic Account

First, what is an account on the blockchain? We are not going to go into the underlying cryptography algorithms that power this but rather we'll explain it with a high-level mental model.

Everything begins with generating a seed phrase, basically the human-readable representation of a random number. From this seed phrase, we can generate a private key through a few deterministic cryptographic steps. The public key is then generated through a one-way function from the private key, meaning that you can retrieve the public key from the private key but not the contrary. The blockchain address of your account is then just a 'human-readable' representation of the public key.

But what are those key concepts made for?

  • Seed Phrase / Private Key: Whoever has this can perform transactions with the assets owned by the account: i.e., your bank password. Never share these with anyone.
  • Public Key / Address: they allow you to receive transfers from other accounts. People can see this address and see what it holds, but cannot perform any transactions on behalf of your account.

UI Sign-up Flow

The private key allows a user to perform the so-called transactions. For our wallet to be able to transact on your behalf, we need access to this private key. But users have to be very cautious as it is the most sensitive and private element of your account. If this is compromised, the whole account is compromised.

We decided not to export your private information to a server but rather store it on the client, in the window localStorage of your browser. Your data is never exposed to the network that way. This is how browser wallets like Metamask work. The flow is the following:

  1. Input your seed phrase (aka mnemonic) and choose a password
  2. Compute the private key from the mnemonic, and your account address
  3. Encrypt your private key with your - previously salted- password
  4. Store encrypted private key in the localStorage

crypto-js javascript library features hashing and different encryption/decryption methods. You can have a look at the functions we created to handle encryption here.

Accessing blockchain data

Once the seed phrase is entered, the wallet has access to the account. First thing we'd like to do is to see what the account actually owns on-chain.

There are mainly 3 different tokens that we currently track in this wallet:

  • PHOTON: Evmos incentivized testnet native coin
  • ERC20 tokens: a token with 4 basic functionalities
           - transfer from an account to another
          - get the token balance of an account
          - get the total supply of tokens
          - approve a third party to spend an amount of token for you
  • ERC721 tokens: standard for NFTs  

Fungible Tokens

We created a ERC20 smart contract called Otter Coin (OTT) on the Evmos blockchain, along with a faucet to play with it.

We interact with the blockchain with the ethers.js library.

The first step is to connect to a JSON-RPC provider (through Infura, Alchemy or your own node) with ethers.providers.JsonRpcProvider.

  1. For a Native coin, you can directly use the ethers method await provider.getBalance(ADDRESS) to get balance of a specific address.
  2. For ERC20 tokens, you need the contract address and the contract ABI (Application Binary Interface) in order to load it. The ABI is a compiled version of the smart contract. It's how an interface for encoding smart contracts (which we wrote in Solidity) into the EVM
  3. Load the smart contract. new ethers.Contract(contractAddress, contractAbi, provider);
  4. Once loaded, you can use any of the ERC20 contract methods: balanceOf queries the balance of a specific address.

import erc20 from '../abis/erc20.json'
const { ethers } = require("ethers");

async getBalanceErc20(provider, address, contractAbi, contractAddress){
      const erc20Contract = new ethers.Contract(contractAddress, contractAbi, provider);
      const balance = ethers.utils.formatEther((await erc20Contract.balanceOf(address)).toString());
      return balance
  }

const provider = new ethers.providers.JsonRpcProvider(RPC_ENDPOINT);
const photonBalance = ethers.utils.formatEther(web3.utils.hexToNumberString((await provider.getBalance(ADDRESS))._hex))
const ottBalance = await getBalanceErc20(provider, ADDRESS, erc20.abi, ADDRESS_OTT_CONTRACT)


This generalizes to any ERC20 contract. If we have the contract address, we can then render it on a simple UI:

Non Fungible Tokens (NFT)

The main difference with fungible tokens is that NFTs are unique (hence, non-fungible): this materializes in each token having a unique tokenId, uniquely identifying the NFT within the collection.

There are three pieces of data we need to display NFT's

  • The tokenIds an account owns
  • The metadata associated with the tokenId. If there is an image, it would live here.

It's important to understand that NFT metadata and image are not guaranteed to be stored on-chain. What is stored on-chain is a link to this metadata. (Watch out for rug pulls everyone!)

The steps to follow:

  1. Load ERC721 contract (with contract address and ABI)
  2. List tokenIds owned by an account through via balanceOf and  tokenOfOwnerByIndex from the smart contract.
  3. For each tokenId:
    - Retrieve the token URI - essentially the linked to the metadata - through the tokenURI ERC721 method. We're calling it in fetchMetadataFromId.
    - Retrieve the image link, located in the metadata JSON under the image field


import erc721 from '../abis/erc721.json'
const { ethers } = require("ethers");

async getTokenIds(provider, address){
    const erc721Contract = new ethers.Contract(CONTRACT_ADDRESS, erc721.abi, provider);
    const NFTbalance = web3.utils.hexToNumberString((await erc721Contract.balanceOf(address))._hex);
    const tokenIds = [];
    for (let i = 0, len = NFTbalance ; i < len; i++) { 
        tokenIds.push((await erc721Contract.tokenOfOwnerByIndex(address, i))._hex);
      }
    return tokenIds
  }

async fetchMetadataFromId(provider, id){
		const erc721Contract = new ethers.Contract(CONTRACT_ADDRESS, erc721.abi, provider);
		const metadataLink = await erc721Contract.tokenURI(id);
		this.setState({metadataUrls: this.state.metadataUrls.concat(metadataLink)})
		const response = await fetch(metadataLink);
		const responseJson = await response.json();
		
		return responseJson   
  }


async initializeState(provider){
    const tokenIds = await this.getTokenIds(provider);
    this.setState({tokenIds: tokenIds})
    
    for (let i = 0, len = tokenIds.length ; i < len; i++) { 
	      const metadata = await this.fetchMetadataFromId(provider, tokenIds[i])
	      this.setState({
	        names: this.state.names.concat(metadata.name), 
	        imgUrls: this.state.imgUrls.concat(metadata.image)
	      })
    }
  }

We then store metadata URL, Image URL and tokenIds in the React state in order to render it on the UI Page. This generalizes to any ERC721 smart contract, given that you have contract address and ABI and that balanceOf, tokenOfOwnerByIndex, tokenURI methods (which it should if it conforms to the ERC721 standard).

We recently released Alt's first collection of Otters on the Evmos testnet (read this article to see the NFT creation process). We are now able to visualize the Otters we hold:

Sending transactions

Great! We have now a tool to see our on-chain holdings. But we still need to implement transfer functionalities for both fungible tokens and NFTs.

As explained in the first section, we need the account private key each time we want to perform a transfer. The private key is encrypted with a password (that only the user knows...we hope). Thus, every time we perform an action, we ask for the user's password, this is for security purposes

The steps to perform a transaction are:

  1. User enters their password
  2. Load the wallet by using the password, performed with loadWallet function
  3. Create the actual transaction. Main parameters are:
    - gasPrice: how much you’re willing to pay per unit of gas
    - gasLimit: how many units of gas you’re willing to pay for
    - recipient: the receiver of the transaction
    - tokenAmount : the number of tokens you want to send
  4. Sign and send the transaction:
    - for Native Token: await wallet.signTransaction(tx) followed by await wallet.sendTransaction(tx)
    - for erc20 Token: await erc20Contract.transfer(recipient, numberOfTokens, transactionOptions) given the contract already loaded



loadAndDecodeEncryptedKey(password){
    const encryptedPrivKey = localStorage.getItem('encryptedPrivKey');
    const salt = JSON.parse(localStorage.getItem('salt'));
    const iv = JSON.parse(localStorage.getItem('iv'));
    const encryptionKey = retrieveEncryptionKey(password, salt)
    const decryptedPrivKey = decodeUserPrivateKey(encryptedPrivKey, encryptionKey, iv)

    return decryptedPrivKey     
  }

loadWallet(password, rpc){
  const provider = new ethers.providers.JsonRpcProvider(rpc);
  const decryptedPrivateKey = loadAndDecodeEncryptedKey(password);
  const wallet = new ethers.Wallet(decryptedPrivateKey, provider);

  return wallet
}

loadWalletAndSendErc20(password, recipient, tokenAmount){
    const wallet = loadWallet(password, RPC_ENDPOINT)
    this.transferErc20Coin(wallet, recipient, erc20.abi, CONTRACT, tokenAmount)  
  }

loadWalletAndSendNativeCoin(password, recipient, tokenAmount){
    const wallet = loadWallet(password, RPC_ENDPOINT)
    this.transferNativeCoin(wallet, recipient, tokenAmount)
  } 

async transferNativeCoin(wallet, recipient, tokenAmount){
  const tx = {
      to: recipient,
      value: ethers.utils.parseEther(tokenAmount),
      gasLimit: 6000000,
      gasPrice: ethers.utils.parseUnits('1.0', 'gwei')
  }
  await wallet.signTransaction(tx);
  const response = await wallet.sendTransaction(tx);
  const transactionHash = response.hash;
  alert(`TransactionHash: ${transactionHash}`)
 }

async transferErc20Coin(wallet, recipient, contractAbi, contractAddress, tokenAmount){
  const erc20Contract = new ethers.Contract(contractAddress, contractAbi, wallet);
  const numberOfTokens = ethers.utils.parseUnits(tokenAmount, 18);
  const transactionOptions = {
      gasLimit: 6000000,
      gasPrice: ethers.utils.parseUnits('1.0', 'gwei')
  }

  let response = await erc20Contract.transfer(recipient, numberOfTokens, transactionOptions)
  let transactionHash = response.hash;
  alert(`TransactionHash: ${transactionHash}`)

  }


For the sake of brevity, we will not detail the code for ERC721 transfer, but this is essentially the same mechanism. The main difference is that you need to call an overloaded method safeTransferFrom. The syntax is slightly different:

await erc721Contract["safeTransferFrom(address,address,uint256)"](address, recipient, web3.utils.hexToNumber(tokenId))

Conclusion

Those 3 building blocks should give you the keys (pun!) to build a wallet, or at least understand how your wallets interact with the blockchain.

Thanks for reading until here, please try our wallet and have a look at the code:

And join us in the Evmos community! → https://discord.gg/8Xd29MU4DE