Skip to content

Add Structs

So far we relied on simple Datatypes like addresses, uint or in previous labs also Booleans. But what if that's not enough? Maybe you want to track the timestamp when a deposit happened. You want to track every single deposit from every user. You want to track how many deposits happened, and many more details.

Of course, a rule of thumb is: Do only the most necessary functions on the blockchain, and everything else off-chain. But for sake of explaining Structs, we will track every single payment in the greatest detail possible with our Smart Contract.

If you have never worked with structs, objects or classes before, then think about it like this:

While you are tracking the current balance with a mapping that maps address to uint (mapping(address=>uint) balanceReceived) with something schematically like balanceReceived[THE-ADDRESS] = THE-UINT, with a struct you would access the children with a . (period).

Let's say you have a struct

MyStruct {
    uint abc;
}

mapping(address => MyStruct) someMapping;

Then you would write to the mapping like this someMapping[THE-ADDRESS].abc = THE-UINT. Why is this useful? Let's have a look into our Smart Contract!

Add the following things to the Smart Contract:

//SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

contract MappingsStructExample {

    struct Payment {
        uint amount;
        uint timestamp;
    }

    struct Balance {
        uint totalBalance;
        uint numPayments;
        mapping(uint => Payment) payments;
    }

    mapping(address => Balance) public balanceReceived;


    function getBalance() public view returns(uint) {
        return address(this).balance;
    }

    function sendMoney() public payable {
        balanceReceived[msg.sender].totalBalance += msg.value;

        Payment memory payment = Payment(msg.value, block.timestamp);
        balanceReceived[msg.sender].payments[balanceReceived[msg.sender].numPayments] = payment;
        balanceReceived[msg.sender].numPayments++;
    }

    function withdrawMoney(address payable _to, uint _amount) public {
        require(_amount <= balanceReceived[msg.sender].totalBalance, "not enough funds");
        balanceReceived[msg.sender].totalBalance -= _amount;
        _to.transfer(_amount);
    }

    function withdrawAllMoney(address payable _to) public {
        uint balanceToSend = balanceReceived[msg.sender].totalBalance;
        balanceReceived[msg.sender].totalBalance = 0;
        _to.transfer(balanceToSend);
    }
}

It's getting quite extensive here, bear with me. Let's go through the Smart Contract together to understand what's going on here!

We have one struct called Payment, which stores the amount and the timestamp of the payment. Then we have another struct called Balance which stores the total balance and a mapping of all payments done.

Mapping has no Length

Mappings have no length. It's important to understand this. Arrays have a length, but, because how mappings are stored internally, they do not have a length.

Let's say you have a mapping mapping(uint => uint) myMapping, then all elements myMapping[0], myMapping[1], myMapping[123123], ... are already initialized with the default value. If you map uint to uint, then you map key-type "uint" to value-type "uint".

Structs are initialized with their default value

Similar to anything else in Solidity, structs are initialized with their default value as well.

If you have a struct

struct Payment {
    uint amount;
    uint timestamp;
}
and you have a mapping mapping(uint => Payment) myMapping, then you can access already all possible uint keys with the default values. This would produce no error:

myMapping[0].amount, or myMapping[123123].amount, or myMapping[5555].timestamp.

Similar, you can set any value for any mapping key:

myMapping[1].amount = 123 is perfectly fine.

So, with these two things in mind, structs allow you to define something similar like cheap class-variables.

Back to our Smart Contract.

Balance <-> Payment relationship

If you have a look at the two structs, then you see there is also a mapping inside:

   struct Payment {
        uint amount;
        uint timestamp;
    }

    struct Balance {
        uint totalBalance;
        uint numPayments;
        mapping(uint => Payment) payments;
    }

    mapping(address => Balance) public balanceReceived;

Because mappings have no length, we can't do something like balanceReceived.length or payments.length. It's technically not possible. In order to store the length of the payments mapping, we have an additional helper variable numPayments.

So, if you want to the first payment for address 0x123... you could address it like this: balanceReceived[0x123...].payments[0].amount = .... But that would mean we have static keys for the payments mapping inside the Balance struct. We actually store the keys in numPayments, that would mean, the current payment is in balanceReceived[0x123...].numPayments. If we put this together, we can do balanceReceived[0x123...].payments[balanceReceived[0x123...].numPayments].amount = ....

Enough talking, let's give it a try!

Use the Smart Contract

  1. Deploy a new Instance of the Smart Contract.
  2. Then deposit 1 Ether with Account#1 (1 Ether -> sendMoney button)
  3. Then check the Balance

What you see is that you can retrieve the Balance easily, but you can't automatically get the "sub-mapping" (the mapping in the struct) value. You would have to write an extra getter-function, or you just use that mapping internally in your Smart Contract.

Of course, this is just an exemplary Smart Contract, but you will see later how we use mapping of mappings later in the Supply Chain Example.

On to the next Lab!


Last update: April 10, 2022