<!-- Canonical: https://docs.linea.build/network/tutorials/first-dapp -->

> For the complete Linea documentation index, see [llms.txt](/llms.txt).
> Agents can fetch this page as Markdown at [https://docs.linea.build/network/tutorials/first-dapp.md](https://docs.linea.build/network/tutorials/first-dapp.md).

# Build your first dapp on Linea

This tutorial will walk you through creating a simple Linea dapp that allows users to stake tokens in a contract.

note

As Linea uses the London version of Ethereum, a small number of recent opcodes are [not available](/network/overview/ethereum-differences).

## Prerequisites

-   Install [Node.js](https://nodejs.org/en/download) and a package manager (this tutorial uses [pnpm](https://pnpm.io/installation)).
-   Install the [MetaMask](https://metamask.io) wallet and fund it with [Linea Sepolia ETH](/network/build/get-testnet-eth).

## Steps

### Section 1. Create the staking contract

You'll create a simple staking contract. Use [Remix](https://remix.live/), an online tool that speeds up Solidity development. For a complete development environment, you can also use [Foundry](https://www.getfoundry.sh/) or [Hardhat](https://hardhat.org/).

note

You can follow the steps directly on the images included in this tutorial if you don't know how to use Remix.

1.  Go to the [Remix IDE](https://remix.ethereum.org/) and create a new file in the contracts folder.

2.  Copy and paste the following code:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract StakingWithAllowlist {
    mapping(address => mapping(address => uint256)) public stakedBalances;
    mapping(address => bool) public allowlistedTokens;

    event Staked(address indexed user, address indexed token, uint256 amount);
    event Withdrawn(address indexed user, address indexed token, uint256 amount);

    constructor(address[] memory tokens) {
        for (uint256 i = 0; i < tokens.length; i++) {
            allowlistedTokens[tokens[i]] = true;
        }
    }

    function stake(address _token, uint256 _amount) public {
        require(allowlistedTokens[_token], "Token is not in the allowlist");
        require(_amount > 0, "Amount must be greater than 0");
        IERC20 token = IERC20(_token);
        require(token.transferFrom(msg.sender, address(this), _amount), "Token transfer failed");
        stakedBalances[msg.sender][_token] += _amount;
        emit Staked(msg.sender, _token, _amount);
    }

    function withdraw(address _token, uint256 _amount) public {
        require(allowlistedTokens[_token], "Token is not in the allowlist");
        require(_amount > 0, "Amount must be greater than 0");
        require(stakedBalances[msg.sender][_token] >= _amount, "Insufficient staked balance");
        IERC20 token = IERC20(_token);
        stakedBalances[msg.sender][_token] -= _amount;
        require(token.transfer(msg.sender, _amount), "Token transfer failed");
        emit Withdrawn(msg.sender, _token, _amount);
    }

    function isTokenAllowlisted(address _token) public view returns (bool) {
        return allowlistedTokens[_token];
    }
}
```

    This is a simplified version of a staking contract with an allowlist. It allows you to stake any ERC-20, and contains the two basic functions `stake` and `withdraw`. The allowlist is set up when the contract is deployed; there are other versions where the allowlist can be modified, usually by the owner.

![First 2 instructions on Remix interface](/img/learn/first-dapp/tutoremix_1.png)

### Section 2. Compile the contract

1.  Go to the **Solidity Compiler** tab. Select the London version in the EVM Version list under **Advanced Configurations** (to avoid any compatibility issues).

2.  Select **Compile**.

![Compile instructions on Remix interface](/img/learn/first-dapp/tutoremix_2.png)

After compiling the code, you'll see this below the compile button:

![compile menu on Remix](/img/learn/first-dapp/image.png)

Copy the ABI using the button at the bottom. Save it in a JSON file, as you'll need it later.

### Section 3. Deploy the contract

1.  Go to the **Deploy** tab. Select your injected provider (MetaMask) as the environment. You'll get a connection request to accept.

    What should I do if my injected provider doesn't appear?

    If your injected provider does not appear by default:

    1.  Select **Customize this list** at the bottom of the environment list. A page will open in Remix.
    2.  You should see your injected provider here. Select the box at the top right and it should appear at the top of the list.

    ![injected provider menu on Remix](/img/learn/first-dapp/tutoremix_troubleshoot1.png)

2.  Check that the contract is the one you want to deploy.

3.  Enter the list of tokens you want to allowlist. For this tutorial, three ERC-20 contracts have been deployed for your use (you can copy and paste directly into the field):

```solidity
["0x46871676658472B99720F2a368CDa6430c1647b9","0xB7D70343639aF53a02f6ea7d9cde240fc72de6Dd","0x66a6F52C2100FB82EE21FD1380b4D516CB540c93"]
```

    These ERC-20 contracts are verified so you can interact with them directly on Lineascan to mint some tokens. Put your address and the number of tokens you want (don't forget that if you want 1 token you have to put 1e18, which is 100000000000000000).

    -   [Mint link for COFFEE](https://sepolia.lineascan.build/address/0x46871676658472b99720f2a368cda6430c1647b9#writeContract#F3)
    -   [Mint link for TEA](https://sepolia.lineascan.build/address/0xb7d70343639af53a02f6ea7d9cde240fc72de6dd#writeContract#F3)
    -   [Mint link for WATER](https://sepolia.lineascan.build/address/0x66a6f52c2100fb82ee21fd1380b4d516cb540c93#writeContract#F3)
4.  Select **Deploy** and confirm the transaction in MetaMask. You'll see success logs in the console at the bottom right. You can copy the contract address by going to recorded transactions (under the deploy button), but you can also find it by searching for your address on the [Sepolia Linea explorer](https://sepolia.lineascan.build).

![Deploy instructions on Remix interface](/img/learn/first-dapp/tutoremix_3.png)

### Section 4. Set up the frontend with RainbowKit

Use RainbowKit to set up a new [Next.js](https://nextjs.org/) app built on top of wagmi. These libraries simplify how to handle reading chain state, wallet connections, sending transactions, listening for events and state changes, and more.

1.  Run `pnpm create @rainbow-me/rainbowkit@latest` and choose a project name.

2.  Run `cd yourprojectname` and open the project in your IDE (run `code .` to open it in VS Code).

3.  Configure the chain to Linea Sepolia by modifying the `src/wagmi.ts` file:

```tsx
import { getDefaultConfig } from '@rainbow-me/rainbowkit';
import { lineaSepolia } from 'wagmi/chains';

export const config = getDefaultConfig({
  appName: 'Linea Token Staking App',
  projectId: 'YOUR_PROJECT_ID',
  chains: [lineaSepolia],
  ssr: true,
});
```

### Section 5. Configure and style the frontend

Configure and style the project using shadcn with Tailwind CSS:

```bash
pnpm install -D tailwindcss postcss autoprefixer lucide-react
npx tailwindcss init -p
npx shadcn-ui@latest init
```

Press `enter` each time to choose the default parameters (or use your preferred configuration).

Depending on your system, you might have some differences, so use the following to ensure you have the configuration expected by this tutorial:

1.  You should have an `app` folder, with a `global.css` file inside. You can move it to `src/pages/globals.css` and edit the import at the top of `_app.tsx`:

```tsx
import './globals.css';
```

2.  shadcn works by manually adding each component with the `pnpm dlx shadcn-ui@latest add <component>`, so be sure to do it for each import. Run this command to add all the components you'll use:

```bash
pnpm dlx shadcn-ui@latest add tabs button card input label popover command
```

3.  You also have a folder containing two folders `components` and `lib`. You can move them under `src` —> `src/components` and `src/lib` .

4.  Verify that in the `tsconfig.json` file that you have this under `compilerOptions`:

```tsx
"compilerOptions": {
    ...,
    "paths": {
      "@/*": ["./src/*"]
    }
}
```

5.  You can delete the empty folders (these might vary depending on your system, but you should have an empty `app` folder at least). Also, if you have both `tailwind.config.js` and `tailwind.config.ts`, remove the `tailwind.config.js` file.

The configuration is done. You should have a project structure like this:

```bash
src
├── components
│   └── ui
│       ├── button.tsx
│       ├── card.tsx
│       ├── command.tsx
│       ├── dialog.tsx
│       ├── input.tsx
│       ├── label.tsx
│       └── popover.tsx
├── lib
│   └── utils.ts
├── pages
│   ├── _app.tsx
│   ├── globals.css
│   └── index.tsx
└── wagmi.ts
```

### Section 6. Handle the wallet connection

You're ready to integrate your contracts and build the main interface. You'll do that in the `src/pages/index.tsx` file, which will be your main page. You can remove the content in this file and start from scratch.

Define an empty page with a connect button and some useful debug information. Use the `ConnectButton` from RainbowKit to handle the wallet connection, and the `useAccount` hook from Wagmi to get the connected account information.

```tsx
import type { NextPage } from 'next';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useAccount } from 'wagmi';

const Home: NextPage = () => {
  const account = useAccount();

  return (
    <div className="bg-background w-full h-screen flex items-center justify-center">
      {/* Top right corner: Wallet information */}
      <div className="absolute top-0 right-0 p-4">
        <ConnectButton />
      </div>

      {/* Bottom left corner: Debug information */}
      <div className="absolute text-xs text-gray-500 bottom-0 left-0 p-4">
        status: {account.status}
        <br />
        address: {JSON.stringify(account.address)}
        <br />
        chainId: {account.chainId}
      </div>
    </div>
  );
};

export default Home;
```

Run `pnpm dev` and go to `http://localhost:3000/` in your browser. Try to connect your wallet; RainbowKit should automatically ask you to switch to Linea Sepolia. Verify that the `chainId` is `59141` in the debug information corner.

### Section 7. Add the staking interface

Copy and paste this code for a simple staking card design at the top of `index.tsx`:

```tsx
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Check, ChevronsUpDown } from "lucide-react";

const tokens: { value: `0x${string}`; label: string }[] = [
  {
    value: "0x46871676658472B99720F2a368CDa6430c1647b9",
    label: "COFFEE",
  },
  {
    value: "0xB7D70343639aF53a02f6ea7d9cde240fc72de6Dd",
    label: "TEA",
  },
  {
    value: "0x66a6F52C2100FB82EE21FD1380b4D516CB540c93",
    label: "WATER",
  },
];

const TokensCombobox = ({ value, setValue, disabled }: any) => {
  const [open, setOpen] = useState(false);
  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          className="w-full justify-between"
          disabled={disabled}
        >
          {value
            ? tokens.find((token) => token.value === value)?.label
            : "Select token..."}
          <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-full p-0">
        <Command>
          <CommandInput placeholder="Search by address..." />
          <CommandList>
            <CommandEmpty>No token found.</CommandEmpty>
            <CommandGroup>
              {tokens.map((token) => (
                <CommandItem
                  key={token.value}
                  value={token.value}
                  onSelect={(currentValue) => {
                    setValue(currentValue === value ? "" : currentValue);
                    setOpen(false);
                  }}
                >
                  <Check
                    className={cn(
                      "mr-2 h-4 w-4",
                      value === token.value ? "opacity-100" : "opacity-0"
                    )}
                  />
                  {token.label}
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
};

const StakingCard = ({ account, disabled }: any) => {
  const [menuTab, setMenuTab] = useState<"stake" | "unstake">("stake");
  const [token, setToken] = useState<`0x${string}`>(tokens[0].value);
  const [amountToStake, setAmountToStake] = useState<number>(0);
  const [amountToUnstake, setAmountToUnstake] = useState<number>(0);
  const [amountStakable, setAmountStakable] = useState<number>(0);
  const [amountStaked, setAmountStaked] = useState<number>(0);

  const mockFetchAmountStakableForToken = (tokenAddress: string) => 10;
  const mockFetchAmountStakedForToken = (tokenAddress: string) => 10;
  useEffect(() => {
    const stakable = mockFetchAmountStakableForToken(token);
    const staked = mockFetchAmountStakedForToken(token);
    setAmountStakable(stakable);
    setAmountToStake(stakable);
    setAmountStaked(staked);
    setAmountToUnstake(staked);
  }, [token]);

  return (
    <Card className="w-full max-w-sm relative">
      {disabled && (
        <div className="z-50 backdrop-blur-[3px] rounded-lg absolute inset-0 flex items-center justify-center">
          <p className="text-md text-gray-500">
            Please connect your wallet first
          </p>
        </div>
      )}

      <div className="absolute text-xs text-gray-500 top-0 right-0 p-4">
        Available to stake: {amountStakable}
        <br />
        Amount staked: {amountStaked}
      </div>

      <CardHeader>
        <CardTitle className="text-2xl">Linea Staking</CardTitle>
      </CardHeader>
      <CardContent className="grid gap-4">
        <div className="grid gap-2">
          <Label htmlFor="targetToken">Token</Label>
          <TokensCombobox
            id="targetToken"
            value={token}
            setValue={setToken}
            disabled={disabled}
          />
        </div>

        <Tabs defaultValue="stake" value={menuTab}>
          <TabsList
            className="grid w-full grid-cols-2"
            hidden={amountStakable === 0 || amountStaked === 0}
          >
            <TabsTrigger
              value="stake"
              disabled={amountStakable === 0}
              onClick={() => setMenuTab("stake")}
            >
              Stake
            </TabsTrigger>
            <TabsTrigger
              value="unstake"
              disabled={amountStaked === 0}
              onClick={() => setMenuTab("unstake")}
            >
              Unstake/Withdraw
            </TabsTrigger>
          </TabsList>

          <TabsContent value="stake">
            <div className="grid gap-2">
              <div className="flex w-full max-w-sm items-center space-x-2">
                <Input
                  id="amountToStake"
                  value={amountToStake}
                  onChange={(e) => {
                    const value = Math.min(
                      Number(e.target.value),
                      amountStakable
                    );
                    setAmountToStake(value);
                  }}
                  disabled={disabled || amountStakable === 0}
                  type="number"
                />
                <Button
                  type="submit"
                  variant="secondary"
                  onClick={() => setAmountToStake(amountStakable)}
                >
                  Max
                </Button>
              </div>
            </div>
            <Button
              className="w-full mt-4"
              onClick={() => {}}
              disabled={disabled || amountStakable === 0}
            >
              Stake
            </Button>
          </TabsContent>

          <TabsContent value="unstake">
            <div className="grid gap-2">
              <div className="flex w-full max-w-sm items-center space-x-2">
                <Input
                  id="amountToUnstake"
                  value={amountToUnstake}
                  onChange={(e) => {
                    const value = Math.min(
                      Number(e.target.value),
                      amountStaked
                    );
                    setAmountToUnstake(value);
                  }}
                  disabled={disabled || amountStaked === 0}
                  type="number"
                />
                <Button
                  type="submit"
                  variant="secondary"
                  onClick={() => setAmountToUnstake(amountStaked)}
                >
                  Max
                </Button>
              </div>
            </div>
            <Button
              className="w-full mt-4"
              onClick={() => {}}
              disabled={disabled || amountStaked === 0}
            >
              Unstake
            </Button>
          </TabsContent>
        </Tabs>
      </CardContent>
    </Card>
  );
};
```

This tutorial won't explain this code, since it's mostly React state management. You define the list of tokens as a constant to simplify. The `StakingCard` component handles all the token selection and inputs as well.

Add it to the page (in React, this means putting the following in `return( ... )`):

```tsx
{/* Main content */}
<StakingCard
  account={account}
  disabled={account.status !== "connected"}
/>
```

### Section 8. Integrate the contract

Until now, nothing is connected to the blockchain. Note that the `mockFetchAmountStakableForToken` and `mockFetchAmountStakedForToken` functions just return a constant. You'll use the `useReadContracts` hook for this purpose. You'll also use `useWatchBlockNumber` to reread the state on each new block.

Before doing that, create an `abi.json` file in `src/pages` and paste the ABI from the smart contract (saved from [step 2](#2-compile-the-contract)).

Define the contract address and ABI:

```tsx
import { useAccount, useReadContracts, useWatchBlockNumber } from "wagmi";
import { erc20Abi } from 'viem'
import CONTRACT_ABI from "./abi.json";
const CONTRACT_ADDRESS = "0xe5fac868B1d0E4d119A09cC7253E2D7a3cb250da";
```

Remove the previous mock part with this new one:

```tsx
const mockFetchAmountStakableForToken = (tokenAddress: string) => 10;
const mockFetchAmountStakedForToken = (tokenAddress: string) => 10;
useEffect(() => {
  const stakable = mockFetchAmountStakableForToken(token);
  const staked = mockFetchAmountStakedForToken(token);
  setAmountStakable(stakable);
  setAmountToStake(stakable);
  setAmountStaked(staked);
  setAmountToUnstake(staked);
}, [token]);

const [blockNumber, setBlockNumber] = useState<bigint>(BigInt(0));
  useWatchBlockNumber({
    onBlockNumber(blockNumber) {
      setBlockNumber(blockNumber);
    },
  })

const { data, isSuccess, isLoading } = useReadContracts({
	allowFailure: false,
	blockNumber,
	contracts: [
	  // Token decimals
	  {
	    address: token,
	    abi: erc20Abi,
	    functionName: "decimals",
	  },
	  // Stakable
	  {
	    address: token,
	    abi: erc20Abi,
	    functionName: "balanceOf",
	    args: [account.address],
	  },
	  // Staked
	  {
	    address: CONTRACT_ADDRESS,
	    abi: CONTRACT_ABI,
	    functionName: "stakedBalances",
      args: [account.address, token],
	  },
		//Allowance
    {
      address: token,
      abi: erc20Abi,
      functionName: "allowance",
      args: [account.address, CONTRACT_ADDRESS],
    },
	],
});

useEffect(() => {
	if (isSuccess) {
	  const decimals = data?.[0] ?? 18;
	  const stakable = Number((data?.[1] as bigint) ?? 0) / 10 ** decimals;
	  const staked = Number((data?.[2] as bigint) ?? 0) / 10 ** decimals;
	  setAmountStakable(stakable);
	  setAmountStaked(staked);
	}
}, [data]);

useEffect(() => {
      setAmountToStake(0);
      setAmountToUnstake(0);
  }, [token])
```

Optionally, you can use the `isLoading` boolean to add animation while fetching values for the two inputs, "Stake" and "Unstake":

```tsx
<Input
  id="amountToUnstake"
  value={amountToUnstake}
  className={isLoading ? " animate-pulse" : ""}
  onChange={(e) => {
    const value = Math.min(
      Number(e.target.value),
      amountStaked
    );
    setAmountToUnstake(value);
  }}
  disabled={disabled || amountStaked === 0}
  type="number"
/>
```

If you choose a token on your dapp now, the frontend automatically fetches the token decimals, available balances, and staked balances.

### Section 9. Send a transaction

You can now handle the deposit and withdraw transactions. Prepare and send a transaction using the `useWriteContract` hook:

```tsx
import { useAccount, useReadContracts, useWriteContract } from "wagmi";
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";

// ...

// StakingCard:
const { writeContract, isPending } = useWriteContract();

return (
	//...

  <Button
    className="w-full mt-4"
    onClick={() => {}}
    disabled={disabled || amountStakable === 0}
  >
    Stake
  </Button>
{(data?.[3] ?? 0) < amountToStake * 10 ** (data?.[0] ?? 18) ? (
  <Button
    className="w-full mt-4"
    onClick={() =>
      writeContract({
        abi: erc20Abi,
        address: token,
        functionName: "approve",
        args: [
          CONTRACT_ADDRESS,
          BigInt(amountToStake * 10 ** (data?.[0] ?? 18)),
        ],
      })
    }
    disabled={disabled || amountStakable === 0 || isPending}
  >
    {isPending ? (
      <Loader2 className="animate-spin" />
    ) : (
      <p>Approve</p>
    )}
  </Button>
) : (
  <Button
    className="w-full mt-4"
    onClick={() =>
      writeContract({
        abi: CONTRACT_ABI,
        address: CONTRACT_ADDRESS,
        functionName: "stake",
        args: [token, amountToStake * 10 ** (data?.[0] ?? 18)],
      })
    }
    disabled={disabled || amountStakable === 0 || isPending}
  >
    {isPending ? (
      <Loader2 className="animate-spin" />
    ) : (
      <p>Stake</p>
    )}
  </Button>
)}

	//...

  <Button
    className="w-full mt-4"
    onClick={() => {}}
    disabled={disabled || amountStaked === 0}
  >
    Unstake
  </Button>
  <Button
    className="w-full mt-4"
    onClick={() =>
      writeContract({
        abi: CONTRACT_ABI,
        address: CONTRACT_ADDRESS,
        functionName: "withdraw",
        args: [token, amountToUnstake * 10 ** (data?.[0] ?? 18)],
      })
    }
    disabled={disabled || amountStaked === 0 || isPending}
  >
    {isPending ? <Loader2 className="animate-spin" /> : <p>Unstake</p>}
  </Button>

	//...
)
```

You can now stake and unstake from the list of allowlisted tokens.

This tutorial demonstrated using read and write operations in the frontend. Most Wagmi hooks return additional data such as `isLoading` or `error`, which are useful for building more complex frontends. See the [Wagmi documentation](https://wagmi.sh/react/getting-started) for more information.

## Next steps

-   As an optional exercise: Listen for events to dynamically update the frontend. (You can use [`watchContractEvent`](https://wagmi.sh/core/api/actions/watchContractEvent) or [`watchAsset`](https://wagmi.sh/core/api/actions/watchAsset).)

-   See [Solidity by Example](https://solidity-by-example.org/), which is a useful website when creating smart contracts.

-   See the [full example dapp](https://github.com/Gwen-M/linea-first-dapp) on GitHub.
