DEV Community

Muwawu Moses
Muwawu Moses

Posted on

Vyper beginner's tutorial: Variables.

In our previous tutorial, we managed to write a very simple smart contract code and explained the syntax and structures involved. So now, let's extend our learning to a broader spectrum by going deep into variables.

What are Variables?

In Vyper, all variables must be explicitly declared with a type. The type of a variable determines the kind of data that it can store. For example, you can declare a variable to store an unsigned integer, a string, or a boolean value.

Variable types

In Vyper, variables can be broadly categorized into two main types based on their scope and storage:

  • Local Variables
  • State Variables

Local variables:

These are temporary variables that exist only within the scope of a function. They are not stored on the blockchain and only exist in memory during the execution of the function.

Local variables are declared within functions and the variables are only accessible within the functions they are declared in.

Examples:

To better understand how variables are declared in vyper, we shall need to use a series of examples. If you look closely in the examples we provided below, all variables are declared within the function. In the following examples, our aim is to write a smart contract function for adding two numbers:

@external
def addTwo(x: int128, y: int128) -> int128:
    result: int128 = x + y
    return result
Enter fullscreen mode Exit fullscreen mode

To be more concise and idiomatic:

# a simple function to add two numbers x and y
@external
def addTwo(x: int128, y: int128) -> int128:
    return x + y
Enter fullscreen mode Exit fullscreen mode

We are going to divide the above code into three parts i. e;

1.

   @external
Enter fullscreen mode Exit fullscreen mode
  • @external: This is a decorator that specifies the visibility of the function. It indicates that the function addTwo can be called from outside the contract. In other words, other smart contracts and external users can invoke this function. This decorator ensures that the function is part of the contract's public interface.

2.

   def addTwo(x: int128, y: int128) -> int128:
Enter fullscreen mode Exit fullscreen mode
  • def: This keyword is used to define a function in Vyper (similar to Python).
  • addTwo: This is the name of the function. It is a descriptive name indicating that the function will add two numbers.
  • x: int128: This declares a parameter x for the function. The type of x is int128, which means it is a signed 128-bit integer. In Vyper, specifying the type of each variable is mandatory to ensure type safety and prevent overflow/underflow errors.
    • int128: This type can hold values from -2^127 to (2^127) - 1. It is used for variables that need to store integer values within this range.
  • y: int128: This declares another parameter y for the function. Like x, y is also of type int128.
    • -> int128: This part specifies the return type of the function. It indicates that the function will return a value of type int128.

The truth is, Smart contracts on Ethereum are designed to be deterministic and do not interact with users in the same way traditional programs do. Therefore, to be able interact with our contract, we need some other tool and that is web3.py. If you don't know how to use web3.py library, please visit my tutorial here and it will be helpful.

my_contract.py file:

from web3 import Web3

# Connect to a local or remote Ethereum node
web3 = Web3(Web3.HTTPProvider('https://data-seed-prebsc-1-s1.binance.org:8545/'))  # or your Infura endpoint, etc.

# ABI of the compiled Vyper contract
abi = [
    {
        "type": "function",
        "name": "addTwo",
        "stateMutability": "nonpayable",
        "inputs": [
            {
                "name": "x",
                "type": "int128"
            },
            {
                "name": "y",
                "type": "int128"
            }
        ],
        "outputs": [
            {
                "name": "",
                "type": "int128"
            }
        ]
    }
]


# Address of the deployed contract
contract_address = web3.to_checksum_address('YOUR_CONTRACT_ADDRESS')  # replace with your contract's address

# Create a contract instance
contract = web3.eth.contract(address=contract_address, abi=abi)

# Call the addTwo function
def call_addtwo():
    try:
        x = int(input('Enter X value: '))
        y = int(input('Enter Y value: '))
        result = contract.functions.addTwo(x, y).call()
        print(f"The result of {x} + {y} is: {result}")
    except ValueError:
        print('Invalid input. Please enter valid integers for X and Y.')
    except Exception as e:
        print(f'Error during addition: {e}')

# Call the function to get user input and add the numbers
call_addtwo()


Enter fullscreen mode Exit fullscreen mode

And then, run your file and you should get something similar to this:

Terminal results

State Variables

These are stored on the blockchain. They are part of the contract's state and are persistent across function calls and transactions.
State variables are declared outside of functions, typically at the top of the contract. They are accessible by all functions within the contract and are stored in the contract's storage. Let's have an example to better understand this:

# Define a public variable to store the greeting
greeting: public(String[100])

# Constructor to initialize the greeting
@external
def __init__():
    self.greeting = "Hello, World"

# Function to set a new greeting
@external
def set_greeting(new_greeting: String[100]):
    self.greeting = new_greeting

# Function to get the current greeting
@external
@view
def get_greeting() -> String[100]:
    return self.greeting

Enter fullscreen mode Exit fullscreen mode

Here is the detailed explanation of our code:

# Define a public variable to store the greeting
greeting: public(String[100])

Enter fullscreen mode Exit fullscreen mode
  • greeting: This is a state variable defined with the type String[100].
    • State Variable: This variable is stored on the blockchain, which means its value persists across transactions.
    • public: The public keyword makes this variable accessible from outside the contract, creating an automatic getter function.
    • String[100]: As already mentioned in previous examples, this specifies that the variable is a string with a maximum length of 100 characters.
@external
def __init__():
    self.greeting = "Hello, World"

Enter fullscreen mode Exit fullscreen mode
  • init(): This is the constructor function of the contract, called once when the contract is deployed.
    • self.greeting = "Hello, World": This initializes the greeting state variable with the string "Hello, World".
@external
def set_greeting(new_greeting: String[100]):
    self.greeting = new_greeting

Enter fullscreen mode Exit fullscreen mode
  • set_greeting(new_greeting: String[100]): This function allows the greeting to be updated.
    • new_greeting: String[100]: The function takes one parameter, new_greeting, which must be a string with a maximum length of 100 characters.
    • self.greeting = new_greeting: This updates the greeting state variable with the new value provided.
@external
@view
def get_greeting() -> String[100]:
    return self.greeting

Enter fullscreen mode Exit fullscreen mode
  • get_greeting() -> String[100]: This function returns the current value of the greeting variable.
    • @view: This is one of the available but optional decorators. It indicates that the function does not modify the state of the contract (i.e., it is a read-only function).
    • return self.greeting: This returns the value of the greeting state variable.

I am very sure now you purely get the context of variables as used in vyperlang. Let's do one more last part of interacting with our smart contract using web3.py library.

Please note that in the following example, we didn't use environment variables for the sake of brevity. But, i would recommend not to expose your variables by creating a .env file for them.

import sys
from web3 import Web3

# Connect to BSC node (Binance Smart Chain)
bsc_node_url = 'https://data-seed-prebsc-1-s1.binance.org:8545/'  # Replace with your BSC node URL
web3 = Web3(Web3.HTTPProvider(bsc_node_url))

# Set the private key directly (For demonstration purposes only, do not hardcode in production)
private_key = 'YOUR_PRIVATE_KEY_HERE'  # Replace with your actual private key
account = web3.eth.account.from_key(private_key)

# Contract ABI
contract_abi = [
    {
        "type": "constructor",
        "stateMutability": "nonpayable",
        "inputs": []
    },
    {
        "type": "function",
        "name": "set_greeting",
        "stateMutability": "nonpayable",
        "inputs": [
            {
                "name": "new_greeting",
                "type": "string"
            }
        ],
        "outputs": []
    },
    {
        "type": "function",
        "name": "get_greeting",
        "stateMutability": "view",
        "inputs": [],
        "outputs": [
            {
                "name": "",
                "type": "string"
            }
        ]
    },
    {
        "type": "function",
        "name": "greeting",
        "stateMutability": "view",
        "inputs": [],
        "outputs": [
            {
                "name": "",
                "type": "string"
            }
        ]
    }
]

# Contract address
contract_address = web3.to_checksum_address('YOUR_CONTRACT_ADDRESS')  # Replace with your contract's address

# Create contract instance
contract = web3.eth.contract(address=contract_address, abi=contract_abi)

# Function to get the current greeting
def get_greeting():
    return contract.functions.get_greeting().call()

# Function to set a new greeting
def set_greeting(new_greeting):
    nonce = web3.eth.get_transaction_count(account.address)
    tx = contract.functions.set_greeting(new_greeting).build_transaction({
        'chainId': 97,  # BSC testnet
        'gas': 3000000,
        'gasPrice': web3.to_wei('5', 'gwei'),
        'nonce': nonce,
    })
    signed_tx = web3.eth.account.sign_transaction(tx, private_key)
    tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
    receipt = web3.eth.wait_for_transaction_receipt(tx_hash)
    return receipt

# Main execution
if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python3 interact_with_greeting_contract.py <action> [<value>]")
        print("Actions: get, set")
        sys.exit(1)

    action = sys.argv[1]

    if action == "get":
        print("Current greeting:", get_greeting())
    elif action == "set":
        if len(sys.argv) < 3:
            print("Usage: python3 interact_contract.py set <value>")
            sys.exit(1)
        new_greeting = ' '.join(sys.argv[2:])
        receipt = set_greeting(new_greeting)
        print("Transaction receipt:", receipt)
    else:
        print(f"Unknown action: {action}")
        sys.exit(1)

Enter fullscreen mode Exit fullscreen mode

Results

Vs code terminal
From the result image above, you can see that when we initially ran the get command, we got the expected Current greeting: Hello, World.

Then, when we go on to run the set command with the string set to "Good Morning Everyone!", we do get the same expected result when we run the get command; Current greeting: Good Morning Everyone!

You can check out my next tutorial on vyper functions.
If you found this article helpful, please give me a heart, follow and I will be happy for any interactions in the comments section. Thank you!

Top comments (0)