Guided walkthrough: write CreditLedger, line by line#

← Back to Week 5 (Blockchain track, Vyper): Guided build

00-guided-write-assetregistry.md had you type AssetRegistry's genuinely new pieces: a struct, a HashMap, an owner check, one full state-changing function, all at once. That's a lot of new surface area in one sitting. This exercise isolates just one piece of it, a HashMap plus an access check, on its own, in a contract small enough to type and understand completely in a few minutes, before you move on to 01-read-and-predict.md or the stretch goals at the end of ../project/README.md.

CreditLedger has one job: let the owner hand out credits to any address, and let anyone read anyone's balance. No struct (a single uint256 per address is little enough data that a struct would just be overhead), no events at all (events were already covered in depth by notes/03-events.md and by AssetRegistry itself; leaving them out here keeps this exercise's one new rep focused on HashMap plus access control, not on re-explaining something you already have).

Create ../project/contracts/CreditLedger.vy and build it up below, running ape compile from the project/ folder after every step.

Step 1: the pragma and the owner#

# SPDX-License-Identifier: MIT
#pragma version ^0.4.0

owner: public(address)

@deploy
def __init__():
    self.owner = msg.sender

Same shape as AssetRegistry's opening: owner is set once, at deployment, to whoever sent the deployment transaction, and stays fixed after that; nothing in this contract ever changes it. Compile.

Step 2: a HashMap, with no struct behind it#

credits: public(HashMap[address, uint256])

AssetRegistry's HashMap pointed at a Holder struct because a holder needed four different pieces of data tracked together. Here, one address needs exactly one number, so the HashMap's value type is just uint256 directly; no struct required. public(...) still gives you the same free getter as before: credits(some_address) is callable from outside with no function of your own written for it. Compile. A HashMap with nothing written into it yet is completely valid; every address already reads back as 0, the same default-value behavior notes/01-state-and-storage.md covered for AssetRegistry's holders HashMap.

Step 3: the one function that changes state, access check inline#

@external
def add_credits(recipient: address, amount: uint256):
    assert msg.sender == self.owner, "CreditLedger: caller is not the owner"
    assert recipient != empty(address), "CreditLedger: recipient is the zero address"

    self.credits[recipient] += amount

This assert msg.sender == self.owner, ... line is the only way Vyper expresses "only the owner may call this." There's no modifier to write it into instead, so it just sits here, directly, at the top of the function. self.credits[recipient] += amount is ordinary arithmetic; Vyper's automatic overflow protection covers it with no extra code from you (see notes/04-failure-modes.md, and Snippet 6 in 01-read-and-predict.md if you want the overflow angle on a line that looks just like this one, on a smaller integer type). Compile one final time.

Step 4 (optional continuation): add a second owner-only function, then earn the internal helper#

Everything above is a complete, working contract. This step is here for the exact moment the "what to notice" section below describes before you've read it: the moment a second owner-only function actually shows up, and pulling the check out into its own small function stops being an abstraction you're taking on faith and starts being something you reach for because retyping the same assert a second time would visibly be repeating yourself.

Add a second function:

@external
def remove_credits(holder: address, amount: uint256):
    assert msg.sender == self.owner, "CreditLedger: caller is not the owner"
    self.credits[holder] -= amount

Compile. Notice you just typed assert msg.sender == self.owner, ... for the second time, word for word except the error message. Vyper has no modifier to reach for here, the way Solidity does; what it does have is an ordinary internal function:

@internal
@view
def _assert_only_owner():
    assert msg.sender == self.owner, "CreditLedger: caller is not the owner"

Add self._assert_only_owner() to the top of both add_credits and remove_credits, in place of their inline assert msg.sender == self.owner, ... lines (keep each function's other assert, like add_credits's zero-address check; that one isn't about who's calling, so it isn't the helper's job). Compile a final time and confirm nothing about either function's behavior changed, only where the access check lives. Notice this is still not a modifier: _assert_only_owner doesn't wrap around a function or have any placeholder marking "now run the real body," it's just a normal function call, the same as any other function call you've ever written, that happens to revert if the caller is wrong.

This is the same refactor AssetRegistry could have made, and chose not to, since three short, identical, explicit lines across three functions read clearly enough on their own; here, you're the one deciding the exact moment the tradeoff tips, which is the part that doesn't come for free just from reading a finished contract someone else already decided about.

What to notice, now that it's small enough to see all at once#

  • After Step 3, CreditLedger was already deployable and fully functional, with three lines of real logic inside its one state-changing function. AssetRegistry does the same underlying job, at a larger scale: three state-changing functions instead of one, four tracked fields instead of one, three events instead of zero. Nearly everything that made AssetRegistry feel like a lot at once was repetition of this exact HashMap-plus-access-check shape, not new ideas layered on top of each other.
  • Step 4 is the payoff of doing this small: you got to feel the exact moment pulling a check out into its own function earns its keep, right as you typed the same assert a second time, instead of only ever seeing it already-extracted in a finished contract you didn't write. That's the same "extract the repeated logic once it's actually repeated" instinct from Week 2's organizing-your-code.md, just showing up in Vyper instead of Python, and expressed as a plain function instead of a modifier, since Vyper doesn't have one.
  • Nothing in this project deploys CreditLedger.vy automatically. If you want the rep of actually seeing it live on a testnet, deploy it the same way you deployed AssetRegistry in ../project/README.md's Step 1: a fresh, tiny deploy script pointed at project.CreditLedger instead of project.AssetRegistry will do it, and writing that script yourself, from the shape of ../project/scripts/deploy.py, is good, cheap practice for the low-scaffolding weeks coming up, where nobody hands you a script to start from at all. Otherwise, you can delete the file, or leave it; nothing else in this project references it.