Simple Storage Smart Contract on Avalanche Blockchain with Solidity, TypeScript, EVM, HardHat and EthersJS

Simple Storage Smart Contract on Avalanche Blockchain with Solidity, TypeScript, EVM, HardHat and EthersJS

Introduction

Avalanche is a relatively new blockchain platform that aims to solve some of the scalability and efficiency issues faced by other blockchain networks like Bitcoin and Ethereum. It was created by a team of computer scientists led by Emin Gün Sirer, a well-known figure in the blockchain community.

Avalanche uses a novel consensus mechanism called Avalanche consensus, which allows for high throughput, low latency, and high transaction finality. Unlike traditional Proof-of-Work or Proof-of-Stake consensus algorithms, Avalanche relies on a random sampling of network participants to quickly reach consensus on the state of the network.

One of the key features of Avalanche is its ability to support multiple subnets or "blockchains within a blockchain." This means that different groups of users can create their own subnets with their own rules and governance structures while still being able to interact with the wider Avalanche network.

Another notable feature of Avalanche is its support for the creation of smart contracts using the Solidity programming language, which is also used by Ethereum. This allows developers to build decentralised applications (dApps) on the Avalanche platform with familiar tools and languages.

Overall, Avalanche is a promising blockchain platform that offers significant improvements in scalability, speed, and flexibility over other existing blockchain networks. Its unique consensus mechanism and support for multiple subnets make it an interesting option for developers and users looking to build decentralised applications or participate in a fast and efficient blockchain ecosystem.

Steps

Prerequisites

Before we begin, make sure you have the following tools installed:

  1. Node.js (v14+)

  2. npm (v7+)

  3. HardHat (v2+)

  4. Solidity (v0.8+)

Step 1: Setting up the project

Create a new directory for the project and navigate into it:

mkdir simple-storage
cd simple-storage

Initialize a new Node.js project and install the required dependencies:

npm init -y
npm install --save-dev hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers typescript ts-node dotenv @types/dotenv @types/mocha @types/chai avalanche

Initialize HardHat:

npx hardhat

Choose "Create an empty hardhat.config.js" when prompted.

Step 2: Configure TypeScript and HardHat

Create a new tsconfig.json file in the root of the project with the following content:

{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "outDir": "dist"
  },
  "include": ["./scripts", "./test"],
  "files": ["./hardhat.config.ts"]
}

Rename hardhat.config.js to hardhat.config.ts and update its content:

import { HardhatUserConfig } from 'hardhat/config';
import '@nomiclabs/hardhat-waffle';
import '@nomiclabs/hardhat-ethers';
import 'dotenv/config';

const config: HardhatUserConfig = {
  solidity: '0.8.0',
  networks: {
    hardhat: {
      chainId: 31337,
    },
  },
  mocha: {
    timeout: 20000,
  },
};

export default config;

Step 3: Creating the Simple Storage Contract

Create a new directory contracts and a new Solidity file SimpleStorage.sol inside it:

mkdir contracts
touch contracts/SimpleStorage.sol

Add the following code to SimpleStorage.sol:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract SimpleStorage {
  uint256 private _storedData;

  function set(uint256 value) public {
    _storedData = value;
  }

  function get() public view returns (uint256) {
    return _storedData;
  }
}

Overview

The Simple Storage contract is designed to demonstrate the basic functionality of a smart contract by allowing users to store and retrieve data on the Ethereum blockchain. The contract consists of a single state variable that holds an unsigned integer and two functions for setting and getting the stored value.

State Variables

The contract has one state variable:

  • storedData: This unsigned integer (uint256) holds the stored data. It is the main data storage for the contract, and its value can be modified using the set() function and retrieved using the get() function.

Functions

The contract has two main functions:

set()

This function allows users to update the value of the storedData state variable. It takes a single input parameter, a uint256 value, which represents the new data to be stored.

function set(uint256 x) public {
    storedData = x;
}

When called, this function updates the storedData state variable with the provided input value.

get()

This function allows users to retrieve the current value of the storedData state variable. It takes no input parameters and returns the uint256 value stored in storedData.

function get() public view returns (uint256) {
    return storedData;
}

When called, this function returns the current value of the storedData state variable without modifying it.

Interaction Flow

The following steps outline the typical interaction flow for users interacting with the Simple Storage contract:

  1. Deploy the contract: The user deploys the Simple Storage contract to the Ethereum blockchain. Upon deployment, the storedData state variable is initialized with the value of 0.

  2. Set the stored data: The user calls the set() function, providing a uint256 value as input. The function updates the storedData state variable with the provided value.

  3. Get the stored data: The user calls the get() function to retrieve the current value of the storedData state variable. The function returns the uint256 value without modifying it.

Conclusion

The Simple Storage smart contract demonstrates the basic functionality of a smart contract on the Ethereum blockchain. It allows users to store and retrieve an unsigned integer value through two simple functions, set() and get(). This contract serves as a starting point for understanding the fundamentals of smart contract development and can be expanded upon to implement more complex features and use cases.

Step 4: Writing tests for the contract

Create a new directory test and a new TypeScript file SimpleStorage.test.ts inside it:

mkdir test
touch test/SimpleStorage.test.ts

Add the following code to SimpleStorage.test.ts:

// @ts-ignore
import { ethers, waffle } from "hardhat";
import { Contract } from "ethers";
import { expect } from "chai";

describe("SimpleStorage", () => {
    let simpleStorage: Contract;

    beforeEach(async () => {
        const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
        simpleStorage = await SimpleStorage.deploy();
        await simpleStorage.deployed();
    });

    it("Initial stored data should be 0", async () => {
        const storedData = await simpleStorage.get();
        expect(storedData.toString()).to.equal("0");
    });

    it("Should set stored data to a new value", async () => {
        await simpleStorage.set(42);
        const storedData = await simpleStorage.get();
        expect(storedData.toString()).to.equal("42");
    });
});

Step 5: Compile the contract and run tests

Compile the contract using HardHat:

npx hardhat compile

Run the tests:

npx hardhat test

If everything is set up correctly, you should see the tests passing.

Step 6: Deploy the contract on a local network

Create a new directory scripts and a new TypeScript file deploy.ts inside it:

mkdir scripts
touch scripts/deploy.ts

Add the following code to deploy.ts:

// @ts-ignore
import { ethers } from "hardhat";

async function main() {
    const SimpleStorageFactory = await ethers.getContractFactory("SimpleStorage");
    const simpleStorage = await SimpleStorageFactory.deploy();
    await simpleStorage.deployed();

    console.log("SimpleStorage deployed to:", simpleStorage.address);
}

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

Run the deployment script:

npx hardhat run scripts/deploy.ts

You should see the contract deployed to a local address.

Step 7: Deploy the contract on Avalanche Testnet using Ethers.js

To deploy the contract on Avalanche Testnet, you need an account with test AVAX. You can get some from the Avalanche Faucet.

Add the following configuration to your hardhat.config.ts:

import { HardhatUserConfig } from 'hardhat/config';
import '@nomiclabs/hardhat-waffle';
import '@nomiclabs/hardhat-ethers'; // Add this line
import 'dotenv/config';

const config: HardhatUserConfig = {
  solidity: '0.8.0',
  networks: {
    hardhat: {
      chainId: 31337,
    },
    fuji: {
      url: "https://api.avax-test.network/ext/bc/C/rpc",
      chainId: 43113,
      gasPrice: 225000000000,
      accounts: [process.env.PRIVATE_KEY || ""],
    },
  },
  mocha: {
    timeout: 20000,
  },
};

export default config;

Make sure to replace PRIVATE_KEY in the accounts array with your private key.

Run the deployment script on Avalanche Testnet:

npx hardhat run scripts/deploy.ts --network fuji

You should see the contract deployed to an Avalanche Testnet address.

Now you have successfully created a simple storage contract in Solidity with TypeScript, EVM, and AvalancheJS, along with HardHat local testing and deployments.

Step 8: Interacting with the contract using AvalancheJS

To interact with the deployed contract, create a new TypeScript file interact.ts inside the scripts directory:

touch scripts/interact.ts

Add the following code to interact.ts:

import {ethers} from 'ethers';
import * as dotenv from 'dotenv';

dotenv.config();

const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS as string|| '';
if (!CONTRACT_ADDRESS) {
    throw new Error('Please set your contract address in the interact.ts script');
}
// Your ABI Here
const ABI = [
    {
        "inputs": [],
        "name": "get",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "uint256",
                "name": "value",
                "type": "uint256"
            }
        ],
        "name": "set",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
];

const FUJI_RPC_URL = "https://api.avax-test.network/ext/bc/C/rpc";
const PRIVATE_KEY = process.env.PRIVATE_KEY as string || '';

async function interact() {
    dotenv.config();
    if (!CONTRACT_ADDRESS) {
        throw new Error("Please set your contract address in the interact.ts script");
    }

    const provider = new ethers.providers.JsonRpcProvider(FUJI_RPC_URL);
    const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
    console.log("Wallet address:", wallet.address);

    const simpleStorage = new ethers.Contract(CONTRACT_ADDRESS, ABI, wallet);
    console.log("Contract address:", CONTRACT_ADDRESS);
    console.log("Contract instance:", simpleStorage);

    // Read stored data
    const storedData = await simpleStorage.get();
    console.log("Stored data:", storedData.toString());

    // Set new data
    const tx = await simpleStorage.set(100);
    await tx.wait();

    // Read updated stored data
    const updatedStoredData = await simpleStorage.get();
    console.log("Updated stored data:", updatedStoredData.toString());
}

interact().then(r => console.log("Complete"));

Replace YOUR_CONTRACT_ADDRESS with the address of your deployed contract and add your contract's ABI.

Run the interaction script:

npx ts-node scripts/interact.ts

You should see the stored data, transaction receipt, and updated stored data in the console output.

╰─ npx ts-node scripts/interact.ts

Wallet address: 0x2E3bE6ddaC75f8dA97d14009A540E917d881ea92
Contract address: 0x0697aBc8Dc960d53911f4A8BB8989826b78CaF61
Contract instance: Contract {
  interface: Interface {
    fragments: [ [FunctionFragment], [FunctionFragment] ],
    _abiCoder: AbiCoder { coerceFunc: null },
    functions: { 'get()': [FunctionFragment], 'set(uint256)': [FunctionFragment] },
    errors: {},
    events: {},
    structs: {},
    deploy: ConstructorFragment {
      name: null,
      type: 'constructor',
      inputs: [],
      payable: false,
      stateMutability: 'nonpayable',
      gas: null,
      _isFragment: true
    },
    _isInterface: true
  },
  provider: JsonRpcProvider {
    _isProvider: true,
    _events: [],
    _emitted: { block: -2 },
    disableCcipRead: false,
    formatter: Formatter { formats: [Object] },
    anyNetwork: false,
    _networkPromise: Promise { <pending> },
    _maxInternalBlockNumber: -1024,
    _lastBlockNumber: -2,
    _maxFilterBlockRange: 10,
    _pollingInterval: 4000,
    _fastQueryDate: 0,
    connection: { url: 'https://api.avax-test.network/ext/bc/C/rpc' },
    _nextId: 42
  },
  signer: Wallet {
    _isSigner: true,
    _signingKey: [Function (anonymous)],
    _mnemonic: [Function (anonymous)],
    address: '0x2E3bE6ddaC75f8dA97d14009A540E917d881ea92',
    provider: JsonRpcProvider {
      _isProvider: true,
      _events: [],
      _emitted: [Object],
      disableCcipRead: false,
      formatter: [Formatter],
      anyNetwork: false,
      _networkPromise: [Promise],
      _maxInternalBlockNumber: -1024,
      _lastBlockNumber: -2,
      _maxFilterBlockRange: 10,
      _pollingInterval: 4000,
      _fastQueryDate: 0,
      connection: [Object],
      _nextId: 42
    }
  },
  callStatic: {
    'get()': [Function (anonymous)],
    'set(uint256)': [Function (anonymous)],
    get: [Function (anonymous)],
    set: [Function (anonymous)]
  },
  estimateGas: {
    'get()': [Function (anonymous)],
    'set(uint256)': [Function (anonymous)],
    get: [Function (anonymous)],
    set: [Function (anonymous)]
  },
  functions: {
    'get()': [Function (anonymous)],
    'set(uint256)': [Function (anonymous)],
    get: [Function (anonymous)],
    set: [Function (anonymous)]
  },
  populateTransaction: {
    'get()': [Function (anonymous)],
    'set(uint256)': [Function (anonymous)],
    get: [Function (anonymous)],
    set: [Function (anonymous)]
  },
  filters: {},
  _runningEvents: {},
  _wrappedEmits: {},
  address: '0x0697aBc8Dc960d53911f4A8BB8989826b78CaF61',
  resolvedAddress: Promise { <pending> },
  'get()': [Function (anonymous)],
  'set(uint256)': [Function (anonymous)],
  get: [Function (anonymous)],
  set: [Function (anonymous)]
}
Stored data: 0
Updated stored data: 100
Complete

That's it! You have now successfully developed a simple storage contract in Solidity with TypeScript, EVM, HardHat, and AvalancheJS, including local testing, deployments on Avalanche Testnet, and interactions with the deployed contract.

Source Code