Skip to main content

Create a simple Starknet dapp

In this tutorial, you'll learn how to set up a React TypeScript dapp that uses the get-starknet library to connect to MetaMask and display the user's wallet address. You'll also display the balance of an ERC-20 token and perform a token transfer.

Prerequisites

note

This tutorial uses get-starknet version 3.3.0 and starknet.js version 6.11.0.

1. Set up the project

1.1 Create a new project

Use Create React App to set up a new React project with TypeScript. Create a new project named get-starknet-tutorial:

yarn create react-app get-starknet-tutorial --template typescript

Change into the project directory:

cd get-starknet-tutorial

1.2 Configure Yarn

Configure Yarn to use the node-module linker instead of its default linking strategy:

yarn config set nodeLinker node-modules

2. Add get-starknet and starknet.js

Add get-starknet version 3.3.0 and starknet.js version 6.11.0 to your project's dependencies:

yarn add get-starknet@3.3.0 starknet@6.11.0

Your file structure should look similar to the following:

get-starknet-tutorial/
├── public/
│ ├── index.html
│ └── ...
├── src/
│ ├── App.tsx
│ ├── index.tsx
│ ├── App.css
│ └── ...
└── ...

3. Configure the wallet connection

3.1. Connect to MetaMask

The connect function from get-starknet is the primary way to connect your dapp to a user's MetaMask wallet. It opens a connection to MetaMask and returns an object containing important details about the wallet, including:

  • name: The name of the wallet.
  • icon: The wallet's icon, which displays the wallet's logo.
  • account: The account object from starknet.js, which contains the wallet's address and provides access to account-specific operations.

To import the necessary functions and connect to a wallet, add the following code to src/App.tsx:

App.tsx
import { connect, type ConnectOptions } from "get-starknet";

async function handleConnect(options?: ConnectOptions) {
const res = await connect(options);
// Access wallet details such as name, address, and icon
console.log(res?.name, res?.account?.address, res?.icon);
}

3.2. Configure connection options

connect accepts an optional ConnectOptions object. This object can control the connection process, including:

  • modalMode: Determines how the connection modal behaves. The options are:
    • "alwaysAsk": Prompts the user every time a connection is initiated.
    • "neverAsk": Attempts to connect without showing the modal.
  • modalTheme: Sets the visual theme of the connection modal. The options are "dark" and "light".

You can configure these options as follows:

handleConnect({ modalMode: "alwaysAsk", modalTheme: "dark" });

3.3. Create an AccountInterface

After connecting to MetaMask, the account instance is present in the returned object from the connect function.

This object allows interaction with the Starknet network using the connected wallet.

App.tsx
import { AccountInterface } from "starknet"; 

async function handleConnect(options?: ConnectOptions) {
const res = await connect(options);
const myFrontendProviderUrl = "https://free-rpc.nethermind.io/sepolia-juno/v0_7";
const newAccountInterface = new AccountInterface({ nodeUrl: myFrontendProviderUrl }, res);
}

3.4. Display wallet information

You can display the wallet's name, address, and icon in your dapp. This provides visual feedback to the user, confirming which wallet they are using.

The following code is an example of how to update the interface with the connected wallet's details:

App.tsx
import { useState } from "react";

function App() {
const [walletName, setWalletName] = useState("");
const [walletAddress, setWalletAddress] = useState("");
const [walletIcon, setWalletIcon] = useState("");

async function handleConnect(options?: ConnectOptions) {
const res = await connect(options);
setWalletName(res?.name || "");
setWalletAddress(res?.account?.address || "");
setWalletIcon(res?.icon || "");
}

return (
<div>
<h2>Selected Wallet: {walletName}</h2>
<p>Address: {walletAddress}</p>
<img src={walletIcon} alt="Wallet icon" />
</div>
);
}

3.5. Full example

The following is a full example of configuring the wallet connection:

App.tsx
import "./App.css"
import {
type ConnectOptions,
type DisconnectOptions,
connect,
disconnect,
} from "get-starknet"
import { AccountInterface } from "starknet";
import { useState } from "react"
function App() {
const [walletName, setWalletName] = useState("")
const [walletAddress, setWalletAddress] = useState("")
const [walletIcon, setWalletIcon] = useState("")
const [walletAccount, setWalletAccount] = useState<AccountInterface | null>(null)
async function handleConnect(options?: ConnectOptions) {
const res = await connect(options)
setWalletName(res?.name || "")
setWalletAddress(res?.account?.address || "")
setWalletIcon(res?.icon || "")
setWalletAccount(res?.account)
}
async function handleDisconnect(options?: DisconnectOptions) {
await disconnect(options)
setWalletName("")
setWalletAddress("")
setWalletAccount(null)
}
return (
<div className="App">
<h1>get-starknet</h1>
<div className="card">
<button onClick={() => handleConnect()}>Default</button>
<button onClick={() => handleConnect({ modalMode: "alwaysAsk" })}>
Always ask
</button>
<button onClick={() => handleConnect({ modalMode: "neverAsk" })}>
Never ask
</button>
<button
onClick={() =>
handleConnect({
modalMode: "alwaysAsk",
modalTheme: "dark",
})
}
>
Always ask with dark theme
</button>
<button
onClick={() =>
handleConnect({
modalMode: "alwaysAsk",
modalTheme: "light",
})
}
>
Always ask with light theme
</button>
<button onClick={() => handleDisconnect()}>Disconnect</button>
<button onClick={() => handleDisconnect({ clearLastWallet: true })}>
Disconnect and reset
</button>
</div>
{walletName && (
<div>
<h2>
Selected Wallet: <pre>{walletName}</pre>
<img src={walletIcon} alt="Wallet icon"/>
</h2>
<ul>
<li>Wallet address: <pre>{walletAddress}</pre></li>
</ul>
</div>
)}
</div>
)
};

export default App

3.6. Start the dapp

Start the dapp and navigate to it in your browser.

yarn start

You are directed to the default dapp display.

Starknet dapp start
  • Default: Resets the app's handling to the default behavior for connecting the wallet to Starknet.
  • Always ask: Always prompts the user for confirmation when the wallet is connecting to Starknet.
  • Never ask: Suppresses confirmation prompts, automatically connecting the wallet to Starknet.
  • Always ask with dark theme: Prompts for wallet connection confirmation with a dark-themed user interface.
  • Always ask with light theme: Prompts for wallet connection confirmation with a light-themed user interface.
  • Disconnect: Disconnects the wallet from Starknet.
  • Disconnect and reset: Disconnects the wallet and resets the app’s wallet connection settings.

3.7 Connect your dapp to a wallet

Select your preferred connection option and follow the on-screen prompts to connect your MetaMask wallet to the Starknet network.

Starknet dapp select wallet

After you accept the terms in the prompts, your wallet will be successfully connected.

Starknet dapp connected

4. Display the balance of and transfer an ERC-20 token

Now that you have set up the basic interaction, you can display the balance of a specific ERC-20 token, such as STRK, and perform a transfer using the AccountInterface instance.

note

To complete the transfer, you'll need ETH for gas and at least 1 STRK token.

To complete this tutorial, you'll use the Starknet testnet. By default, the Snap operates on the Mainnet. To switch to the testnet:

  1. Obtain testnet ETH and STRK tokens from the Starknet faucet.
  2. Use the StarkNet Snap Companion dapp to switch to the testnet.

4.1. Set up the contract

Create a src/components/ directory and add the following files to it:

  • erc20Abi.json: A JSON file containing the ERC-20 token contract's Application Binary Interface (ABI).
  • TokenBalanceAndTransfer.tsx: A React component file for handling token balance display and transfer operations.

The file structure of the src/ directory should look similar to the following:

src/
├── components/
│ ├── erc20Abi.json
│ └── TokenBalanceAndTransfer.tsx
└── ...

The following TokenBalanceAndTransfer.tsx example loads the ABI from erc20Abi.json:

TokenBalanceAndTransfer.tsx
import { Contract } from "starknet";
import erc20Abi from "./erc20Abi.json";

const tokenAddress = "0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7";

const erc20 = new Contract(erc20Abi, tokenAddress, AccountInterface);
ABI and contract address

The contract address for STRK (an ERC-20 toke") on Sepolia testnet is 0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7. You can find the ABI of the ERC-20 contract on the Code tab in Voyager.

Ensure you call the token address in the TokenBalanceAndTransfer component.

TokenBalanceAndTransfer.tsx
{walletAccount && 
<TokenBalanceAndTransfer account={walletAccount} tokenAddress="0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d" />
}

4.2. Update App.tsx

Call the TokenBalanceAndTransfer component in App.tsx. Add the followinwg to the header of App.tsx to import the component:

App.tsx
import { TokenBalanceAndTransfer } from "./components/TokenBalanceAndTransfer";

Ensure that the following code is added to App.tsx, where the TokenBalanceAndTransfer component is called with the token address:

App.tsx
{walletAccount &&
<TokenBalanceAndTransfer account={walletAccount} tokenAddress="0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d" />
}

4.3. Fetch the token balance

Call the balanceOf method to fetch the balance of the connected account:

TokenBalanceAndTransfer.tsx
const balance = await erc20.balanceOf(walletAddress);
const formattedBalance = balance / Math.pow(10, 18);

4.4. Transfer tokens

To transfer tokens, fill out the transfer method call and execute the transaction using the AccountInterface.

Use the following example for reference:

TokenBalanceAndTransfer.tsx
import { Call } from "starknet";

// Define the transfer parameters.
const recipientAddress = "0x78662e7352d062084b0010068b99288486c2d8b914f6e2a55ce945f8792c8b1";
const amountToTransfer = 1n * 10n ** 18n; // 1 token (assuming 18 decimals).

const transferCall: Call = erc20.populate("transfer", {
recipient: recipientAddress,
amount: amountToTransfer,
});

// Execute the transfer.
const { transaction_hash: transferTxHash } = await AccountInterface.execute(transferCall);

// Wait for the transaction to be accepted on Starknet.
await AccountInterface.waitForTransaction(transferTxHash);
Starknet transfer token

4.5. Full example

The following a full example of displaying the balance of an ERC-20 token and performing a transfer:

import { useEffect, useState } from "react";
import { AccountInterface, Call, Contract } from "starknet";
import erc20Abi from "./erc20Abi.json";

interface TokenBalanceAndTransferProps {
account: AccountInterface;
tokenAddress: string;
}

export function TokenBalanceAndTransfer({ account, tokenAddress }: TokenBalanceAndTransferProps) {
const [balance, setBalance] = useState<number | null>(null);

useEffect(() => {
async function fetchBalance() {
try {
if (account) {
const erc20 = new Contract(erc20Abi, tokenAddress, account);
const result = await erc20.balanceOf(account.address) as bigint;

const decimals = 18n;
const formattedBalance = result / 10n ** decimals; // Adjust for decimals using BigInt arithmetic
setBalance(Number(formattedBalance)); // Convert to a number for UI display
}
} catch (error) {
console.error("Error fetching balance:", error);
}
}

fetchBalance();
}, [account, tokenAddress]);

async function handleTransfer() {
try {
if (account) {
const erc20 = new Contract(erc20Abi, tokenAddress, account);
const recipientAddress = "0x01aef74c082e1d6a0ec786696a12a0a5147e2dd8da11eae2d9e0f86e5fdb84b5";
const amountToTransfer = 1n * 10n ** 18n; // 1 token (in smallest units)

// Populate transfer call
const transferCall: Call = erc20.populate("transfer", [recipientAddress, amountToTransfer]);

// Execute transfer
const { transaction_hash: transferTxHash } = await account.execute([transferCall]);

// Wait for the transaction to be accepted
await account.waitForTransaction(transferTxHash);

// Refresh balance after transfer
const newBalance = await erc20.balanceOf(account.address) as bigint;
setBalance(Number(newBalance / 10n ** 18n)); // Adjust for decimals
}
} catch (error) {
console.error("Error during transfer:", error);
}
}

return (
<div>
<h3>Token Balance: {balance !== null ? `${balance} STRK` : "Loading..."}</h3>
<button onClick={handleTransfer}>Transfer 1 STRK</button>
</div>
);
}

4.6. Start the dapp

Start the dapp and navigate to it in your browser.

yarn start

Next steps

You've set up a simple React dapp that connects to MetaMask, displays an ERC-20 token balance, and performs token transfers. Creating a contract instance using AccountInterface allows you to interact with smart contracts, retrieve token balances, and execute transactions, enabling more advanced functionality in your dapp.

You can follow these next steps: