Documentation
For Frontend Developers
Building a Staking dApp

Building a Staking dApp

We will create a straightforward staking decentralized application (dApp) in this section, which will be linked to a Local Node. This dApp will use the staking precompile feature to obtain information about validators and delegators from the EVM, as well as to create delegate transactions on the EVM natively.

Requirements

Setup Wagmi Project

Run the following command to create a folder that contains a sample wagmi / rainbowkit / next dApp

Run the following command:

yarn create wagmi

After running the above command, input the following values:

What is your project named?  staking-precompile-example
What framework would you like to use?  Next.js
What template would you like to use?  RainbowKit

This command will create a folder named staking-precompile-example. Inside this folder there will be a Next.js project with RainbowKit and Wagmi installed and configured.

the folder called staking-precompile-example will look like this:

Install Dependencies

Run the following command to install the remaining dependencies:

yarn add bignumber.js @chakra-ui/react @emotion/react @emotion/styled framer-motion

Configuring Chakra

Before we continue, let's configure Chakra UI.

Creating a Theme file

Under the src folder, create a new file called theme.ts. This will hold our global Chakra themes for the dApp.

Populate theme.ts with the following code:

import { extendTheme } from "@chakra-ui/react";
 
// Configure Chakra theme
const theme = extendTheme({
  fonts: {
    heading: `'Open Sans', sans-serif`,
    body: `'Raleway', sans-serif`,
  },
  styles: {
    global: {
      "html, body": {
        height: "100vh",
        width: "100vw",
        backgroundColor: "#000000",
        color: "#ffffff",
      },
    },
  },
});
 
export default theme;

After adding the theme.ts file your file structure should look like this:

Adding Chakra to the App

Now we need to add ChakraProvider to the _app.tsx file. Navigate to src/pages/_app.tsx file and add the following code:

import "@rainbow-me/rainbowkit/styles.css";
import { RainbowKitProvider } from "@rainbow-me/rainbowkit";
import type { AppProps } from "next/app";
import NextHead from "next/head";
import * as React from "react";
import { WagmiConfig } from "wagmi";
import { chains, client } from "../wagmi";
import { ChakraProvider } from "@chakra-ui/react";
import theme from "../theme";
 
function App({ Component, pageProps }: AppProps) {
  const [mounted, setMounted] = React.useState(false);
  React.useEffect(() => setMounted(true), []);
 
  return (
    <WagmiConfig client={client}>
      <RainbowKitProvider chains={chains}>
        <NextHead>
          <title>Polaris Ethereum Example Dapp</title>
        </NextHead>
        <ChakraProvider theme={theme}>
          {mounted && <Component {...pageProps} />}
        </ChakraProvider>
      </RainbowKitProvider>
    </WagmiConfig>
  );
}
 
export default App;

We now have a basic themed Chakra UI setup.

Configuring wagmi.ts

In order to point this dapp towards a Polaris Ethereum local node, we need to modify wagmi.ts.

Make wagmi.ts look like this:

import { Chain, configureChains, createClient } from 'wagmi'
import { jsonRpcProvider } from 'wagmi/providers/jsonRpc';
import { connectorsForWallets } from '@rainbow-me/rainbowkit';
import { metaMaskWallet } from '@rainbow-me/rainbowkit/wallets';
 
// Configure chain information for local Polaris Ethereum chain
const polarisChain: Chain = {
  id: 2061,
  name: 'Polaris Ethereum's,
  network: 'polaris',
  nativeCurrency: {
    decimals: 18,
    name: 'Polaris Ethereum's,
    symbol: 'tbera',
  },
  rpcUrls: {
    default: {
      http: ['http://localhost:8545'],
    },
    public: {
      http: ['http://localhost:8545'],
    }
  }
};
 
// Configure Wagmi client with Polaris Ethereum chain
const { provider, chains } = configureChains(
  [polarisChain],
  [
    jsonRpcProvider({
      rpc: chain => ({ http: chain.rpcUrls.default.http[0] }),
    }),
  ]
);
 
// only use MetaMask for now
const connectors = connectorsForWallets([
  {
    groupName: 'Recommended',
    wallets: [
      metaMaskWallet({ chains }),
    ],
  },
]);
 
export const client = createClient({
  autoConnect: true,
  connectors,
  provider,
});
 
export { chains }

Adding Utility Functions

We will need to add some utility functions to our dApp. These functions will help us interact with the Polaris Ethereum precompile.

Creating a utils Folder

Under the src folder, create a new folder called utils. This will hold our utility functions.

Creating a utils/formatFromBaseUnit.ts File

Under the utils folder, create a new file called formatFromBaseUnit.ts. This will be responsible for parsing information as it is returned to us from the chain.

Populate formatFromBaseUnit.ts with the following code:

import BigNumber from "bignumber.js";
 
export const BIG_TEN = new BigNumber(10);
 
// Formats numbers from base unit to normal unit
const formatFromBaseUnit = (amount: any, decimals: any) =>
  new BigNumber(amount).div(BIG_TEN.pow(decimals)).toString();
 
export default formatFromBaseUnit;

Creating a utils/formatToBaseUnit.ts File

Under the utils folder, create a new file called formatToBaseUnit.ts. This will be responsible for formatting information as it is sent to the chain.

Populate formatToBaseUnit.ts with the following code:

import { ethers } from "ethers";
 
// Format normal units to base units for use on the EVM
const formatToBaseUnit = (amount: string, decimals: any) => {
  if (!amount) return ethers.BigNumber.from(0);
  try {
    return ethers.BigNumber.from(
      ethers.utils.parseUnits(amount?.toString(), decimals)
    );
  } catch (e) {
    return ethers.BigNumber.from(0);
  }
};
 
export default formatToBaseUnit;

Creating a utils/getEvmEllipsis.ts File

Under the utils folder, create a new file called getEvmEllipsis.ts. This will be responsible for returning shortened versions of our hex addresses.

Populate getEvmEllipsis.ts with the following code:

// Returns a shortened version of a hex address.
export const getHexEllipsis = (hexAddress: string) => {
  const accountEllipsis = hexAddress
    ? `${hexAddress.substring(0, 4)}...${hexAddress.substring(
        hexAddress.length - 4
      )}`
    : null;
  return accountEllipsis;
};

After adding these utility functions, your utils folder should look like this:

Adding ABIs

We will need to add a way to handle ABIs in the application. ABI handling is a common issue across dApps. This is the way I prefer to do it.

Creating an abi Folder & Adding Staking Precompile ABI

Under the src folder, create a new folder called abis. This will hold our ABIs.

Under the abis folder, create a new file called IStakingModule.abi.js. This will hold the ABI for the Staking Precompile.

💡

You can generate the Staking Precompile ABI here

Populate IStakingModule.abi.js with the following code:

const STAKING_ABI = [
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: "address",
        name: "validator",
        type: "address",
      },
      {
        indexed: true,
        internalType: "address",
        name: "delegator",
        type: "address",
      },
      {
        indexed: false,
        internalType: "uint256",
        name: "amount",
        type: "uint256",
      },
      {
        indexed: false,
        internalType: "int64",
        name: "creationHeight",
        type: "int64",
      },
    ],
    name: "CancelUnbondingDelegation",
    type: "event",
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: "address",
        name: "validator",
        type: "address",
      },
      {
        indexed: false,
        internalType: "uint256",
        name: "amount",
        type: "uint256",
      },
    ],
    name: "CreateValidator",
    type: "event",
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: "address",
        name: "validator",
        type: "address",
      },
      {
        indexed: false,
        internalType: "uint256",
        name: "amount",
        type: "uint256",
      },
    ],
    name: "Delegate",
    type: "event",
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: "address",
        name: "sourceValidator",
        type: "address",
      },
      {
        indexed: true,
        internalType: "address",
        name: "destinationValidator",
        type: "address",
      },
      {
        indexed: false,
        internalType: "uint256",
        name: "amount",
        type: "uint256",
      },
    ],
    name: "Redelegate",
    type: "event",
  },
  {
    anonymous: false,
    inputs: [
      {
        indexed: true,
        internalType: "address",
        name: "validator",
        type: "address",
      },
      {
        indexed: false,
        internalType: "uint256",
        name: "amount",
        type: "uint256",
      },
    ],
    name: "Unbond",
    type: "event",
  },
  {
    inputs: [
      {
        internalType: "string",
        name: "srcValidator",
        type: "string",
      },
      {
        internalType: "string",
        name: "dstValidator",
        type: "string",
      },
      {
        internalType: "uint256",
        name: "amount",
        type: "uint256",
      },
    ],
    name: "beginRedelegate",
    outputs: [],
    stateMutability: "payable",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "address",
        name: "srcValidator",
        type: "address",
      },
      {
        internalType: "address",
        name: "dstValidator",
        type: "address",
      },
      {
        internalType: "uint256",
        name: "amount",
        type: "uint256",
      },
    ],
    name: "beginRedelegate",
    outputs: [],
    stateMutability: "payable",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "address",
        name: "validatorAddress",
        type: "address",
      },
      {
        internalType: "uint256",
        name: "amount",
        type: "uint256",
      },
      {
        internalType: "int64",
        name: "creationHeight",
        type: "int64",
      },
    ],
    name: "cancelUnbondingDelegation",
    outputs: [],
    stateMutability: "payable",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "string",
        name: "validatorAddress",
        type: "string",
      },
      {
        internalType: "uint256",
        name: "amount",
        type: "uint256",
      },
      {
        internalType: "int64",
        name: "creationHeight",
        type: "int64",
      },
    ],
    name: "cancelUnbondingDelegation",
    outputs: [],
    stateMutability: "payable",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "address",
        name: "validatorAddress",
        type: "address",
      },
      {
        internalType: "uint256",
        name: "amount",
        type: "uint256",
      },
    ],
    name: "delegate",
    outputs: [],
    stateMutability: "payable",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "string",
        name: "validatorAddress",
        type: "string",
      },
      {
        internalType: "uint256",
        name: "amount",
        type: "uint256",
      },
    ],
    name: "delegate",
    outputs: [],
    stateMutability: "payable",
    type: "function",
  },
  {
    inputs: [],
    name: "getActiveValidators",
    outputs: [
      {
        internalType: "address[]",
        name: "",
        type: "address[]",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "address",
        name: "delegatorAddress",
        type: "address",
      },
      {
        internalType: "address",
        name: "validatorAddress",
        type: "address",
      },
    ],
    name: "getDelegation",
    outputs: [
      {
        internalType: "uint256",
        name: "",
        type: "uint256",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "string",
        name: "delegatorAddress",
        type: "string",
      },
      {
        internalType: "string",
        name: "validatorAddress",
        type: "string",
      },
    ],
    name: "getDelegation",
    outputs: [
      {
        internalType: "uint256",
        name: "",
        type: "uint256",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "address",
        name: "srcValidator",
        type: "address",
      },
      {
        internalType: "address",
        name: "dstValidator",
        type: "address",
      },
    ],
    name: "getRedelegations",
    outputs: [
      {
        components: [
          {
            internalType: "int64",
            name: "creationHeight",
            type: "int64",
          },
          {
            internalType: "string",
            name: "completionTime",
            type: "string",
          },
          {
            internalType: "uint256",
            name: "initialBalance",
            type: "uint256",
          },
          {
            internalType: "uint256",
            name: "sharesDst",
            type: "uint256",
          },
          {
            internalType: "uint64",
            name: "unbondingId",
            type: "uint64",
          },
        ],
        internalType: "struct IStakingModule.RedelegationEntry[]",
        name: "",
        type: "tuple[]",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "string",
        name: "srcValidator",
        type: "string",
      },
      {
        internalType: "string",
        name: "dstValidator",
        type: "string",
      },
    ],
    name: "getRedelegations",
    outputs: [
      {
        components: [
          {
            internalType: "int64",
            name: "creationHeight",
            type: "int64",
          },
          {
            internalType: "string",
            name: "completionTime",
            type: "string",
          },
          {
            internalType: "uint256",
            name: "initialBalance",
            type: "uint256",
          },
          {
            internalType: "uint256",
            name: "sharesDst",
            type: "uint256",
          },
          {
            internalType: "uint64",
            name: "unbondingId",
            type: "uint64",
          },
        ],
        internalType: "struct IStakingModule.RedelegationEntry[]",
        name: "",
        type: "tuple[]",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "string",
        name: "validatorAddress",
        type: "string",
      },
    ],
    name: "getUnbondingDelegation",
    outputs: [
      {
        components: [
          {
            internalType: "int64",
            name: "creationHeight",
            type: "int64",
          },
          {
            internalType: "string",
            name: "completionTime",
            type: "string",
          },
          {
            internalType: "uint256",
            name: "initialBalance",
            type: "uint256",
          },
          {
            internalType: "uint256",
            name: "balance",
            type: "uint256",
          },
          {
            internalType: "uint64",
            name: "unbondingId",
            type: "uint64",
          },
        ],
        internalType: "struct IStakingModule.UnbondingDelegationEntry[]",
        name: "",
        type: "tuple[]",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "address",
        name: "validatorAddress",
        type: "address",
      },
    ],
    name: "getUnbondingDelegation",
    outputs: [
      {
        components: [
          {
            internalType: "int64",
            name: "creationHeight",
            type: "int64",
          },
          {
            internalType: "string",
            name: "completionTime",
            type: "string",
          },
          {
            internalType: "uint256",
            name: "initialBalance",
            type: "uint256",
          },
          {
            internalType: "uint256",
            name: "balance",
            type: "uint256",
          },
          {
            internalType: "uint64",
            name: "unbondingId",
            type: "uint64",
          },
        ],
        internalType: "struct IStakingModule.UnbondingDelegationEntry[]",
        name: "",
        type: "tuple[]",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "address",
        name: "validatorAddress",
        type: "address",
      },
      {
        internalType: "uint256",
        name: "amount",
        type: "uint256",
      },
    ],
    name: "undelegate",
    outputs: [],
    stateMutability: "payable",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "string",
        name: "validatorAddress",
        type: "string",
      },
      {
        internalType: "uint256",
        name: "amount",
        type: "uint256",
      },
    ],
    name: "undelegate",
    outputs: [],
    stateMutability: "payable",
    type: "function",
  },
];
 
export default STAKING_ABI;

after creating src/abi/IStakingModule.abi.js your file tree should look like this:

Styling & Populating The Homepage

Now that we have a base skeleton for our dApp, we can start adding some styling and content. Lets start by adding some basic styling and positioning to our homepage. We want to hover the Rainbowkit ConnectButton in the top right and centre our action items.

In our src/pages/index.tsx file, we still have the default template content. Lets replace this with the following:

import { ConnectButton } from "@rainbow-me/rainbowkit";
import { Flex, Box, Text, Link } from "@chakra-ui/react";
import React from "react";
import Staking from "../components/Staking";
 
function Page() {
  return (
    <Flex
      justifyContent={"center"}
      alignItems={"center"}
      height={"100vh"}
      direction={"column"}
    >
      <Box
        position={"absolute"}
        top={"0"}
        right={"0"}
        marginTop={"16px"}
        marginRight={"16px"}
      >
        {/* Rainbow Kit Connect Button floating top right of the screen */}
        <ConnectButton />
      </Box>
      <Text fontSize={"4xl"}>Polaris Ethereum Staking Precompile Example</Text>
      <Text fontSize={"xl"}>
        Ensure you have a Local{" "}
        <Link color={"#1da1f1"} href="TODO ADD LINK">
          Polaris Ethereum Node running.
        </Link>
      </Text>
      <Staking />
      <Text fontSize={"xs"}>
        To learn more about Polaris Ethereum please reference our{" "}
        <Link color={"#1da1f1"} href="TODO ADD LINK">
          docs
        </Link>
      </Text>
    </Flex>
  );
}
 
export default Page;

Note how we are missing a components/Staking component. We will add this in the next step.

Creating the Staking Component & Adding Functionality

Now that we have our homepage styled, we can start adding some functionality to our dApp.

Creating components/Staking.tsx

Under the src/components folder, delete the following files:

  • Account.tsx
  • index.ts

Under the src/components folder, create a new file called Staking.tsx. This will be our main component for staking.

Your folder structure should look like this:

Adding the Functionality to the Staking Component

Now we can finally add functionality to Staking.tsx. The following is a component that will do the following:

  • Query the Staking precompile for a list of active validators and select the first Open
  • Query the Staking precompile for the current delegated amount of the connected wallet to the selected validator.
  • Prepare & Send a write transaction to the Staking precompile to delegate the entered amount of tokens to the selected validator.

For more information, please refer to the comments in the code below.

Below is the code for the Staking.tsx component:

import React from "react";
import { useMemo, useState } from "react";
import { Flex, Box, Text, Input, Button } from "@chakra-ui/react";
import {
  useContractWrite,
  usePrepareContractWrite,
  useContractRead,
  useAccount,
  Address,
} from "wagmi";
import STAKING_ABI from "../abi/IStakingModule.abi";
import formatToBaseUnit from "../utils/formatToBaseUnit";
import { getHexEllipsis } from "../utils/getEvmEllipsis";
import formatFromBaseUnit from "../utils/formatFromBaseUnit";
import BigNumber from "bignumber.js";
 
// Default Address of the Staking Precompile Contract on Polaris.
// More information here: TODO: Add link to docs
const STAKING_PRECOMPILE_ADDRESS = "0xd9A998CaC66092748FfEc7cFBD155Aae1737C2fF";
 
const Staking = () => {
  // State that holds the selected cosmos Hex Validator Address
  const [validator_address, setValidatorAddress] = useState<
    Address | undefined
  >(undefined);
 
  // State that holds the amount of tBera the connected account has delegated to the selected validator
  const [delegated_amount, setDelegatedAmount] = useState<string | undefined>(
    undefined
  );
 
  // State that tracks values of the input field
  const [input_amount, setInputAmount] = useState<string>("");
 
  // Returns the address of the connected wallet.
  const { address } = useAccount();
 
  // Query the Staking Precompile for the active validators, returns an array of addresses.
  const { data: rawValidators } = useContractRead({
    address: STAKING_PRECOMPILE_ADDRESS,
    abi: STAKING_ABI,
    functionName: "getActiveValidators",
  });
 
  // UseMemo will trigger this function when the value of rawValidators changes. It will set the state variable
  // validator_address to the first raw validator address in the array. This is a lazy way to "select" a validator.
  useMemo(() => {
    if (rawValidators) {
      const validator: Address = (rawValidators as any)[0] as Address;
      setValidatorAddress(validator);
    }
  }, [rawValidators]);
 
  // Query the Staking Precompile for the amount of tBera delegated to the selected validator from the
  // connected wallet address. Returns a string or undefined.
  const { data: rawDelegation } = useContractRead({
    address: STAKING_PRECOMPILE_ADDRESS,
    abi: STAKING_ABI,
    functionName: "getDelegation(address,address)",
    args: [address, validator_address],
    watch: true,
  });
 
  // UseMemo will trigger this function when the value of rawDelegation changes. It will set the state variable
  // delegated_amount to the amount of tBera delegated to the selected validator.
  useMemo(() => {
    if (rawDelegation) {
      const castedDelegation = rawDelegation as BigNumber;
      const parsedDelegation = formatFromBaseUnit(
        castedDelegation.toString(),
        18
      ).toString();
      setDelegatedAmount(parsedDelegation);
    }
  }, [rawDelegation]);
 
  // Prepares a payload for writing a delegate transaction to the Staking Precompile contract.
  // passed in arguments:
  // validator_address: Address of the validator to delegate to
  // formatToBaseUnit(input_amount, 18) a formatted value of the amount to delegate to the selected validator
  // this payload will be used later to call the Staking Precompile contract's delegate function when the button is clicked.
  const { config } = usePrepareContractWrite({
    address: STAKING_PRECOMPILE_ADDRESS,
    abi: STAKING_ABI,
    functionName: "delegate",
    args: [validator_address as Address, formatToBaseUnit(input_amount, 18)],
  });
 
  // When the button is clicked, it will call the Staking Precompile contract's delegate function.
  // on Error, will log the error to the console.
  const { write } = useContractWrite({
    ...config,
    onError(error) {
      console.log("Error", error);
    },
  });
 
  return (
    <Box
      mt={4}
      borderRadius={"12px"}
      padding={4}
      backgroundColor={"#ffff"}
      color={"#000000"}
    >
      <Flex justifyContent={"space-between"}>
        <Text>Validator Address:</Text>
        <Text>
          {validator_address ? getHexEllipsis(validator_address) : ""}
        </Text>
      </Flex>
      <Flex justifyContent={"space-between"}>
        <Text>Delegated Amount: </Text>
        <Text>{delegated_amount ? delegated_amount : "0"} tBera</Text>
      </Flex>
      <Flex mt={4}>
        <Input
          focusBorderColor="#1da1f1"
          variant="filled"
          placeholder="Enter Amount"
          value={input_amount}
          onChange={(e: any) => {
            setInputAmount(e.target.value);
          }}
        />
        <Button colorScheme={"twitter"} marginLeft={4} onClick={write}>
          Delegate
        </Button>
      </Flex>
    </Box>
  );
};
export default Staking;

Run the Application

Now that we have created the Staking component, we can run the application and test it out!

Run the following command to run the application in development mode.

yarn dev

This will open a browser window at http://localhost:3000/ and you should see the following:

Conclusion

Congratulations! You have now created a staking application using the EVM! You can now delegate your tBera to a validator and earn rewards! Prior to Polaris Ethereum's precompiled contracts, the UX of this task would be more complicated and foreign as it involves many moving parts and having to use Cosmos through Metamask. Now, with the Staking precompile, we can enjoy interacting with Cosmos through an EVM native experience!

You can view this example and others like it on our examples repository on Github here (opens in a new tab)