Guided walkthrough: write AssetRegistry, line by line#
← Back to Week 5 (Blockchain track, Vyper): Guided build
Week 4's guided Greeter exercise had you type a trivial contract with one state variable and two functions. AssetRegistry is bigger: a struct, a HashMap, an owner check, and three state-changing functions instead of one. ../project/README.md explains the finished version; this exercise has you type the genuinely new pieces yourself first, compiling after each one.
Create ../project/contracts/AssetRegistryPractice.vy and build it up below, running ape compile from the project/ folder after every step. You won't retype all three state-changing functions; once you've typed the first one in full, the other two follow the identical shape, and you'll read them directly from the real file instead.
Step 1: the pragma and the owner#
# SPDX-License-Identifier: MIT
#pragma version ^0.4.0
owner: public(address)
Vyper has no contract Name: wrapper the way Solidity does; a .vy file is the contract, and Ape names it from the filename (AssetRegistryPractice.vy becomes project.AssetRegistryPractice). Same license line as Greeter, and the same #pragma version idea as Greeter's version pragma, just naming Vyper's own version instead of Solidity's. address is Vyper's type for an account or contract identifier, a new type you haven't declared a variable of yet; notes/01-state-and-storage.md covers it if that's unfamiliar. public(...) here (unlike Greeter's plain, non-public greeting) means Vyper auto-generates a free read function for this variable, so anyone can call owner() and get the current value back without you writing a getter yourself. Compile.
Step 2: a struct, to group one holder's fields together#
struct Holder:
balance: uint256
eligible: bool
registered: bool
last_updated: uint256
A struct bundles related fields into one named type, the same "group related things together" instinct from Week 2's organizing-your-code.md, just as a language feature here instead of file layout. registered exists specifically so a HashMap's default, all-zero entry can be told apart from a holder who was deliberately registered with a zero balance, which notes/01-state-and-storage.md explains in more depth. Compile. A struct declared but never used anywhere yet is completely valid.
Step 3: a HashMap, to store one Holder per address#
holders: public(HashMap[address, Holder])
A HashMap is Vyper's version of a dictionary: give it a key type and a value type, and it stores a Holder for every address that's ever been written to it, permanently, in contract storage. Notice this line, and every state variable declaration, sits at the top level, outside any function; inside a function, you'll always reach it as self.holders[...], never bare holders[...]. Compile.
Step 4: the events#
event HolderRegistered:
holder: indexed(address)
eligible: bool
initial_balance: uint256
timestamp: uint256
event BalanceUpdated:
holder: indexed(address)
previous_balance: uint256
new_balance: uint256
timestamp: uint256
event EligibilityChanged:
holder: indexed(address)
eligible: bool
timestamp: uint256
Three events, the same idea as Greeter's single event, just one per kind of change this contract can make, and written as a small indented block instead of a single function-signature-shaped line. Compile.
Step 5: the constructor, and the check you won't extract into a modifier#
@deploy
def __init__():
self.owner = msg.sender
__init__, decorated @deploy, runs once, at deployment, and sets owner to whoever deployed the contract; msg.sender here is filled in by the network as the deploying account, the same way it's filled in as the transaction sender inside any function. Compile.
Before Step 6: Greeter never restricted who could call set_greeting, so this is the first genuinely new idea in this exercise. If you've looked at the Solidity track, you'd expect a modifier here next. Vyper doesn't have that keyword at all; there's no separate abstraction to declare. The access check is just an ordinary line, assert msg.sender == self.owner, "message", written directly inside every function that needs it. notes/02-access-control.md covers this in depth, including why Vyper is built this way on purpose, if the shape feels unfamiliar.
Step 6: one full state-changing function, using everything above#
@external
def register_holder(holder: address, eligible: bool, initial_balance: uint256):
assert msg.sender == self.owner, "AssetRegistryPractice: caller is not the owner"
assert holder != empty(address), "AssetRegistryPractice: holder is the zero address"
assert not self.holders[holder].registered, "AssetRegistryPractice: holder already registered"
self.holders[holder] = Holder(
balance=initial_balance,
eligible=eligible,
registered=True,
last_updated=block.timestamp,
)
log HolderRegistered(holder=holder, eligible=eligible, initial_balance=initial_balance, timestamp=block.timestamp)
The first assert (Step 5's callout, now in code) has to run before anything else here; a non-owner calling this reverts on that very first line. The next two assert lines guard against a real mistake (registering the zero address, empty(address) is Vyper's zero-value builtin) and a real business rule (no double-registering), each with its own message, so a failed transaction tells you specifically which rule it broke. self.holders[holder] = Holder(...) writes a whole struct into the HashMap at once, using keyword arguments, Vyper requires them for struct construction so there's never any ambiguity about which value goes in which field. block.timestamp is the current block's time, this contract's stand-in for "when did this happen," which the acceptance-gate capstone in Week 8 will care about directly. Compile one final time.
Read the rest, don't type it#
Open ../project/contracts/AssetRegistry.vy and find update_balance and set_eligibility. Both follow register_holder's exact shape: the owner assert first, a second assert guarding a real precondition, a state change, an update to last_updated, a log. You've typed this pattern once already; reading the other two should take a minute, not require re-deriving anything. You can delete AssetRegistryPractice.vy now, or leave it; nothing in this project deploys it unless you write a script that specifically does.