Vyper for Beginners — Structs and Mappings

Welcome back to Vyper for Beginners! This lesson covers two data structure types: structs and mappings.

The major difference between these structures and the simple data types covered previously is that structs and mappings are designed to hold differing data types “within” themselves. The closest simple data type is a Dynamic Array, which is limited to storing elements of a single type.

Vyper for Beginners

Structs

A struct is unique among the built-in Vyper data types. It is a custom data type that can hold (almost) any variable type within.

It’s most useful to think of a struct as a container. There is nothing particularly important about the container itself, the only interesting thing about it is what is contained within.

As an example, consider that we are doing some DeFi work and want to collect pieces of data about a particular ERC-20 token for use later. Rather than using an interface to retrieve information about this token every time it is accessed, you decide to store that information inside of a struct.

Define the struct like so:

struct Erc20Token:
    addr: address
    decimals: uint256
    symbol: String[16]
    name: String[128]

Then declare the struct with values and a variable reference. Here I’ll populate the variable weth with values taken directly from the token contract (Etherscan link):

weth: Erc20Token = Erc20Token(
    {
        addr: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 
        decimals: 18,
        symbol: 'WETH',
        name: 'Wrapped Ether',
    }
)

If I need to use any of the data inside this struct later, I reference it using the syntax struct.variable_name. This is similar to a generic Python object with named variables:

weth.decimals  # equal to 18
weth.symbol    # equal to 'WETH'

Values can be updated later using direct assignment:

weth.decimals = 69   # lol

There is one limitation on what can be stored inside a struct: mappings are not allowed. All other types are allowed.

Mappings

A mapping is a hash table that assigns outpt values to one or more inputs. The most similar Python analog is a dict, which stores values associated with a particular ‘key’. The same value can be stored at different keys, but one key can only ever access a particular value.

A mapping is a fixed structure that must be declared as a state variable (outside of the scope of a function, stored directly on the blockchain).

To initialize a mapping in Vyper, declare it using the HashMap keyword followed by brackets with data type keywords for the key and value.

Here let’s presume I want to do some more DeFi-centric stuff, and want to associate a particular user address with a token balance:

balance_map = HashMap[address, uint256]

An interesting point about hash maps is that they do not store the ‘key’ value directly. Instead, a hash for all possible ‘key’ values is generated at declaration, and associated with the default value for the mapped type.

In other words, my balance_map variable above will have many hashes associated with all possible address variable types, all pointing to 0 (the default value for a uint256). In this way, updating a ‘value’ for a HashMap only modifies one piece. The ‘key’ exists immediately after the mapping is initialized.

Keys

A mapping key can be any base type (passed by value, not by reference). Mappings, interfaces, and structs are not allowed as keys.

Values

There is no restriction on the data types allowed to be stored as a mapping ‘value’. You can even store another mapping as a value inside of another mapping.

Python users have likely see the “dictionary of dictionaries” design pattern before, which works similarly to the Vyper “mapping of mappings”.

Example Vyper Contract

Now let’s build a very simple Vyper contract that makes use of the struct above called Erc20Token, then store that struct inside a mapping called token_map. The contract will store the struct inside a HashMap of user addresses.

In essence, the contract will store a bundle of information about an arbitrary token (the struct), then associate that token container with an arbitrary user address.

The contract does not use any special data types (like DynArray, introduced in 0.3.3) so I am specifying the version pragma as any version in the 0.3 series.

I am omitting the __init__() constructor, since I do not need to set any storage values on deployment.

# @version ^0.3

struct Erc20Token:
    addr: address
    decimals: uint256
    symbol: String[16]
    name: String[128]

token_map: HashMap[address, Erc20Token]
owner: address

@external
@nonpayable
def set_token_for_user(
    owner_address: address,
    token_address: address,
    token_decimals: uint256,
    token_symbol: String[16],
    token_name: String[128]
    ):

    _token: Erc20Token = Erc20Token(
        {
            addr: token_address,
            decimals: token_decimals,
            symbol: token_symbol,
            name: token_name,
        }
    )

    self.token_map[owner_address] = _token

@external
@view
def get_token_for_user(user_address: address) -> Erc20Token:
    return self.token_map[user_address]

Now let’s deploy this contract to a local fork using Brownie. Please review the lesson on Vyper interfaces, which contains an introduction to local forks using Ganache.

(.venv) devil@hades:~/vyper_for_beginners$ brownie console --network mainnet-fork
Brownie v1.19.0 - Python development framework for Ethereum

Compiling contracts...
  Vyper version: 0.3.3
Generating build data...
 - struct_mapping

VyperForBeginnersProject is the active project.

Launching 'ganache-cli --chain.vmErrorsOnRPCResponse true --server.port 8545 --miner.blockGasLimit 12000000 --wallet.totalAccounts 10 --hardfork istanbul --wallet.mnemonic brownie --fork.url https://rpc.ankr.com/eth --chain.chainId 1'...
Brownie environment is ready.
>>> struct_mapping.deploy({'from':accounts[0]})
Transaction sent: 0xc9702b4b450bb91bdd6ebecffd13a251490b50229bcc140c09443ec9ebed9be1
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 2
  struct_mapping.constructor confirmed   Block: 14900849   Gas used: 181058 (1.51%)
  struct_mapping deployed at: 0xE7eD6747FaC5360f88a2EFC03E00d25789F69291

<struct_mapping Contract '0xE7eD6747FaC5360f88a2EFC03E00d25789F69291'>
>>> struct_mapping[0].set_token_for_user(accounts[0], '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 18, 'WETH', 'Wrapped Ethe
r', {'from':accounts[0]})
Transaction sent: 0x378a012ca2c9d052a310ee978ecaa6e0c858c1e9a0dffa38ab056bd6d0c98d3c
  Gas price: 0.0 gwei   Gas limit: 12000000   Nonce: 3
  struct_mapping.set_token_for_user confirmed   Block: 14900850   Gas used: 144426 (1.20%)

<Transaction '0x378a012ca2c9d052a310ee978ecaa6e0c858c1e9a0dffa38ab056bd6d0c98d3c'>
>>> struct_mapping[0].get_token_for_user(accounts[0])
("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 18, "WETH", "Wrapped Ether")

You can see how I have deployed the contract, then using the set_token_for_user() function, stored a new token definition inside the token_map mapping. I could repeat this for any number of arbitrary token definitions and addresses.

Moving Forward

Now that we’ve learned how to define and manipulate structs and mappings, we will take a deeper look at function types and their associated decorators.

Photo of author

Written By BowTiedDevil

Degenerate coder, open source software maximalist, engineer, turbo autist.

Disclosure

This article may contain links to third-party websites or other content for information purposes. BowTiedIsland may receive a commission at no cost to you if you purchase a product after clicking one of these links. The Third-Party Sites are not under the control of BowTiedIsland, and BowTiedIsland is not responsible for the content of any Third-Party Site. All information contained herein is the opinion of the writer and does not constitute financial advice. We aim to act as a neutral third party and aid in your research and analysis.


The Jungle


Crypto, Investing, and E-Commerce with BowTied Bull

The future is internet based, therefore we have a triangle based approach with crypto, e-commerce business making and Investing in traditional assets

Break Into a 6-Figure Wall Street Career

Learn how to efficiently break into investment banking and private equity. The Banker’s Bible is a definitive resource that offers an   A-Z blueprint and also gives you 24/7 access to real finance professionals. 

Fitness With BowTiedOx

BowTiedOx provides you a place to find all of his latest programs and guides.

Weekly newsletters that cover fitness, health, and mindset, all grounded in the fundamentals of physiology.

Media Production with BowTied Turkey and BowTied Tamarin

Video is no longer optional.

Don’t get left behind.

Your brand deserves professional videos to engage your audience.

Art & Graphic Design with BowTied Patriot

BowTied Patriot is a graphic artist who specializes in photography, mixed medium custom artwork, and NFT creation.

Join BowTiedPatriot as he dives into making Art in Web3.0 and The Metaverse.

Cooking with BowTiedOctopod

Learn secrets from a fine dining chef for maximum flavor and time-saving efficiency

Newsletters on Ingredients, Techniques and Flavor hacks that will have you eating better. We will never eat bugs!

Meme Warfare with DgenFren

Increase your online engagement, organically influence narratives, and build your online persona by using marketing that your target audience actually wants: memes.

Learn How to Sell with BowTiedSalesGuy

Sales is one of the most transferrable life skills, yet few know how to actually sell.

Traditional sales tactics don’t cut it in today’s hyper competitive world.

Learn the secrets from a Chad Salesman and change your Life forever.

Ecommerce with BowTiedOpossum

Learn the skills to start and build your first online business.

Want to build a business that travels with you?

Learn from an industry veteran that has worked on and with brands you already know.