Life in the clouds, Googie style (1962).

Ethereum Tutorial

by: burt rosenberg
at: university of miami
date: august 2024

Topics

HelloWorld

We will create an ethereum contract HelloWorld with methods getGreeting and setGreeting. We will first deploy the contract on a local Ethereum chain and then to the Sepolia test network. Finally we will migrate our work to the actual Ethereum blockchain.

JavaScript, Solidity and Node.js
Our work will be done in two programming languages: JavaScript and Solidity.

JavaScript should be familiar as the language that was originally intended to run in the browser. It is an completely dynamically typed scripting language that has classes using the prototype model (different than the template model of most OOL's).

Migrated from the browser to a "server" language, is Node.js, which features asynchronous programming, that will also must be learned for this course.

The JavaScript interfaces with an ethereum blockchain (including a local and the Sepolia testnet blockchains) through a JS library called ethers.js. We will bring down that package using npm, the Node Package Manager. Since our HelloWorld project is also a package, npm will also be used to set up our workspace.

Ethereum contracts are described by Ethereum Virtual Machine EVM. EVM is bytecode and we do not write it directly. The high level Ethereum language we will use is Solidity, and it compiles to EVM.

We will use the Hardhat development environment. Hardhat is a Node package which automates the creation, deployment and testing of Solidity contracts. It uses convention over configuration, with a directory structure and configuration files determined by the Hardhat environment.

Install Node.js

Begin with the installation of npm given in the hardhat setup. Once done, test that node is installed by running it interactively at the command line.

% node --version
v22.4.1
% node
Welcome to Node.js v22.4.1.
Type ".help" for more information.
> 1+1
2
> const multiply = (x, y) => x * y;
undefined
> multiply(3,4)
12
> const my_vars = { a:1, b:2 }
undefined
> const {a} = my_vars
undefined
> a 
1
> .exit
% 
Install Hardhat
Create a directory for your project. Change into that directory then initialize npm and node.js, and install and initialize the developement tool Hardhat in that directory. Also install the hardhat-toolbox.


% npm init
% npm install --save-dev hardhat
% npx hardhat init
  --> select Create an empty hardhat.config.js
% npm install --save-dev @nomicfoundation/hardhat-toolbox

and add

    require("@nomicfoundation/hardhat-toolbox");
to your hardhat.config.js file, to include the toolbox library.
Connect to github
We pause our Ethereum steps to connect up our project directory with a github git repository. You can follow the github tutorial on Software Carpentry.


% echo "# hello-ethereum" >> README.md
% echo ".DS_store" >> .gitignore
% echo "artifacts" >> .gitignore
% git init
% git add README.md
% git add .gitignore
% git commit -m "initial commit"
% git branch -M main
% git remote add origin https://github.com/burtr/hello-ethereum.git
% git push -u origin main

You may need to set your username and email, and git might ask for your password. Your password is the classic token that you created on github, and will need to be renewed.

    git config --global user.name "__username__"
    git config --global user.email "__email__"
    git config --global credential.helper store
There are two ways to connect to the github site: by classic token and by ssh. The classic token is essentially a password authentication. However instead of user chosen password, a long random string is generated and rights on the repository are attached to the token. It is used when a password is asked for.

Ssh is a bit more convenient, in that it uses all the ssh machinery to connect to github. You create a public/private key pair, upload the public key to github, and set your .ssh/config as so,

   Host github
       User git
       Hostname github.com
       IdentityFile ~/.ssh/__my_ssh_private_key__
and then rather than https://__repo_name__ use git@github/__repo_name__.

Cavet Emptor: I have not tested this.

Write the contract
Create the contracts directory in the project directory, and in that we will write this Solidity code into the file HelloWorld.sol,

// contracts/HelloWorld.sol
// SPDX-License-Identifier: UNLICENSED
//  ^ means 0.9 etc also acceptable (~ means 0.8.25 also acceptable)
pragma solidity ^0.8.24;

contract HelloWorld {

        string private _greeting = "Hello World!" ;
        address private _owner ;

        constructor () {
                _owner = msg.sender ;
        }

        modifier onlyOwner() {
                require (
                        msg.sender == _owner, "Ownable: caller is not the owner"
                ) ;
                _ ;
        }

        function getGreeting() external view returns(string memory) {
                return _greeting ;
        }

        function setGreeting(string calldata greeting) external onlyOwner {
                _greeting = greeting ;
        }

        function owner() public view returns(address) {
                return _owner ;
        }
}

The compile this code with,

    npx hardhat compile
If this works, add, commit and git push the contract file.

Note that the contract bytecode is in the artifacts directory, which we have set in .gitignore, as I wish to only repo source code.

Deploy the contract locally
Ethereum contracts run on blockchains. The nodes in an ethereum network choose to execute the code and update the world state, that is kept on the blockchain. The world state is noted before and after the contract is executed, and maintained forever. This way any node can check that the contract was properly run by rerunning the contract on the remembered start state and comparing its result to the result recorded as the after state that is recorded in the blockchain. This is called a transaction.

To run a contract costs ether, and ether costs money. The actual powering is gas and the price of gas in ether is market determined. If the contract cannot be completed because it runs out of gas, the state is reverted and the ether charged. If the contract completes without using all of the given gas, the ether is credited back.

There are three ways will will run on an ethereum blockchain. The easiest is on a local blockchain created by Hardhat. To run a blockchain localling, open a new termina winddow and use the command,

    npx hardhat node
and keep the window open. Closing the window will terminate the blockchain and loose all of the acquired state.

When Hardhat starts up a local node it creates wallets for several accounts and fills them with test ether. Note in the console the address of the wallerts and their private keys.

To connect a Node script with any blockchain we will need a library. Install ethers.js as,

   npm install --save-dev @nomicfoundation/hardhat-ethers ethers
and then add this line to the top of hardhat.config.js,
	require("@nomicfoundation/hardhat-ethers");
For reference see this turtorial.

We will need the following script to deploy our contract,

// scripts/deploy.js
async function main () {
  // We get the contract to deploy
  const HelloWorld = await ethers.getContractFactory('HelloWorld');
  console.log('Deploying HelloWorld ...');
  const hello_world = await HelloWorld.deploy();
  await hello_world.waitForDeployment();
  console.log('HelloWorld deployed to:', await hello_world.getAddress());
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

Note the use of async and await. Node supports asynchronous programming where tasks are launched in threads. Returning from any such launch is an object the represents the perhaps ongoing execution. This is particularly relevant in ethereum because execution on the blockchain are by volunteer nodes, and the results have to be checked and finalized, and the time for this to occur depends.

The script is placed in the script directory and then run,

% npx hardhat run --network localhost ./scripts/deploy.js 
Deploying HelloWorld ...
HelloWorld deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3

The result, if successful, with the the address on the local blockchain of the contract instance.

Interacting with the contract
Note, there is still open another window on your local machine in which the Hardhat node is currently running, and the console output in the window is interesting and sometimes necessary.

Then get to a console that can connect with the node, to exercise the contract,

% npx hardhat console --network localhost

Welcome to Node.js v22.4.1.
Type ".help" for more information.
> await ethers.provider.getBlockNumber()
1
> const HW = await ethers.getContractFactory('HelloWorld')
undefined
> const hw = await HW.attach("0x5fbdb2315678afecb367f032d93f642f64180aa3")
undefined
> await hw.getGreeting()
'Hello World!'
> await hw.setGreeting('Goodnight Moon')
ContractTransactionResponse {
  provider: HardhatEthersProvider {
    _hardhatProvider: LazyInitializationProviderAdapter {
      _providerFactory: [AsyncFunction (anonymous)],
      _emitter: [EventEmitter],
      _initializingPromise: [Promise],
      provider: [BackwardsCompatibilityProviderAdapter]
    },
    _networkName: 'localhost',
    _blockListeners: [],
    _transactionHashListeners: Map(0) {},
    _eventListeners: []
  },
  blockNumber: 2,
  blockHash: '0x6f288b0fdfcb56173bef9294240cfb31912793a1ccb5e6ec46ed6ea3f01c33b1',
  index: undefined,
  hash: '0xbe27e55d179ba7632cb262c26639ef0ba22dce9e475687c84ca682f7848f8b24',
  type: 2,
  to: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
  from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
  nonce: 1,
  gasLimit: 30000000n,
  gasPrice: 973870221n,
  maxPriorityFeePerGas: 232421875n,
  maxFeePerGas: 973870221n,
  maxFeePerBlobGas: null,
  data: '0xa41368620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e68656c6c6f20657468657269756d000000000000000000000000000000000000',
  value: 0n,
  chainId: 31337n,
  signature: Signature { r: "0x83e432c71291c5f53a6bb27d38d3cff4649555c7344c0e1b2068ebc5cc0bf308", s: "0x75960700254ce1e815c938e02b5949e2987f6ded906926049b3401496700c9f7", yParity: 0, networkV: null },
  accessList: [],
  blobVersionedHashes: null
}
> await ethers.provider.getBlockNumber()
2
> await hw.getGreeting()
'Goodnight Moon'
> .exit
%

The Sepolia test network

The Ethereum blockchain is a collection of communicating nodes. To run your contracts on the Ethereum blockchain, you must contact a node in the blockchain network. This can be done through a number of ways, such as JSON-RPC or HTTPS PUT.

In the case of HTTPS PUT, a node provides an open webserver-like port, and the client, your code, speaks to that port with http. The request is framed as an http PUT message.

In the case of JSON-RPC, an RPC (Remote Procedure Call) protocol carries your request to the node and the node carries it into the network. JSON is a data format standard based on JavaScript, and is a textual representation of a javascript object.

Computation on the Ethereum blockchain is counted in a unit called gas. An entity required computation from the blockchain must pay for the gas in a currency called Ether. Ether is denominated in ETH, or WEI, with 10^18 WEI to one ETH. The price of gas in ETH is negotiable. Along with the request is sent an amount of ETH. The actual computation cost will be deducted from that amount, and the rest refunded. If the computation attempts to exceed the provided ETH amount, the computation is terminated and there might be no refund.

Real networks cost real money. Sepolia is a test network that uses Sepolia-ETH that is free. The next step is to get a supply of Sepolia-ETH, connect to the Sepolia network, and deploy and interact with our contract.

To accomplish this next step, we will need Infura and Metamask.

The Infura API-Endpoint
According to the Infura API documentation,

Infura is a managed service that provides secure and reliable access to a variety of blockchain networks ...

When you create an account with Infura (which you will have to do), and create an API key, this API key is used to allow an Sepolia node to accept your requests. In this case, the request looks like an HTTPS request, so of the form,

    curl https://sepolia.infura.io/v3/__INFURA_API_KEY__ \
      -X POST \
      -H "Content-Type: application/json" \
      -d '{
        "jsonrpc": "2.0",
        "method": "eth_blockNumber",
        "params": [],
        "id": 1
     }'
Once you are setup with an API Key, using curl to send that HTTPS request will return the current Sepolia testnet blocknumber.

Metamask browser extension
You will also need a wallet to gather and dispense Sepolia-ETH. Metamask is a browser extension that will create and manage a wallet capable of storing ETH, Sepolia, and several other cryptocurrencies — as long as the address of the wallet is Ethereum compatible.

The Ethereum block chain as two types of accounts,

externally owned
this is a user's store of ETH, for instance.
contract accounts
these are the contracts that do computation and transfer ETH
and both have an address.

A wallet manages a Public-key pair, the secret key is kept in the wallet and is not part of Ethereum; the public part is hashed to an address, and stored in the ethereum chain at that address, with the wallet balance.

After installing the MetaMask browser extension, you can create a wallet, copy out the secret key for use with Hardhat; and refer to the address of the wallet to send and receive Sepolia-ETH was well as mainnet ETH (and some other cryptocurrencies).

Cavaet Emptor: was the above about external accounts correct?

The way you get ETH is you go to a crpyo-exchange that has ETH and you agree with it on an exchange of dollars (say) for ETH. The exchange takes your dollars and transfers over to you ETH. The exchanges compete in a market place for the exact exchange rate.

However Sepolia ETH is worthless, and therefore should be free. The network is a voluntary loss-leader to help developers develop for the main net. Everything technical should be identical between the testnet and the mainnet, except on a testnet, its currency has an exchange rate value of zero.

And ironically, since it is worthless, it is somewhat hard to obtain.

The testnet is a "loss-leader" to encourage main-net activity. It is run voluntarily, including the mining of its ether, which is distributed freely by a faucet that will provide you with daily drips of Sepolia. Here is one:

   https://cloud.google.com/application/web3/faucet/ethereum/sepolia
This one requires an account with the same address with some true ETH in it. If you want to try this out, purchase ETH somehow, and transfer it to this address. Note: the same address will have two types of currencies.
Deploying to Sepolia
Hardhard has the ignition module to help simplify deployments. Install it,
   npm install --save-dev @nomicfoundation/hardhat-toolbox
   npm install --save-dev @nomicfoundation/hardhat-ignition-ethers
and set the configuration variables using Hardhat's configuration management,

npx hardhat vars set INFURA_API_KEY
----type the Infura API key---
npx hardhat vars set SEPOLIA_PRIVATE_KEY
----type the private key for your wallet---
npx hardhat vars list // to list

Then update hardhat.config.js to require the new modules, and to declare a sepolia network.

/** @type import('hardhat/config').HardhatUserConfig */

require("@nomicfoundation/hardhat-ethers");
require("@nomicfoundation/hardhat-ignition-ethers");
require("@nomicfoundation/hardhat-toolbox");

// this is from the tutorial https://hardhat.org/tutorial/deploying-to-a-live-network
// see that URL for explanations.

const { vars } = require("hardhat/config");
const INFURA_API_KEY = vars.get("INFURA_API_KEY");
const SEPOLIA_PRIVATE_KEY = vars.get("SEPOLIA_PRIVATE_KEY");

module.exports = {
  solidity: "0.8.24",
  networks: {
    sepolia: {
      url: `https://sepolia.infura.io/v3/${INFURA_API_KEY}``,
      accounts: [SEPOLIA_PRIVATE_KEY],
    },
  },
};

Create the path ignition/modules/ and add this script,

// ignition/modules/hello_world.js
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");

const HelloWorldModule = buildModule("HelloWorldModule", (m) => {
  const hello_world = m.contract("HelloWorld");
  return { hello_world };
});

module.exports = HelloWorldModule ;

then deploy to sepolia,

   npx hardhat ignition deploy ./ignition/modules/hello_world.js --network sepolia
and test as before, but using the console open,
   npx hardhat console --network sepolia
You can also check the balance on your account by using the method (but use the address you want, not the one for my wallet),
   await ethers.provider.getBalance("0x2B0799d4F4A0252C5cB8110674610073983076a6")
I have found that deploy costs 0.0005 ETH. Setting the greeting costs about 0.0001 ETH.
Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.

author: burton rosenberg
created: 1 aug 2024
update: 4 aug 2024