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

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

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 of type AccountInterface 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 {
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(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)
}
}

3.2. Display connection options

The connect function 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".

The disconnect function allows users to disconnect their wallet. You can enable clearLastWallet to clear the last connected wallet information.

In App.tsx, you can display connect and disconnect buttons with various options as follows:

App.tsx
function App() {
// ...
return (
<div className="App">
<h1>get-starknet</h1>
<div className="card">
// Default connection:
<button onClick={() => handleConnect()}>Connect</button>
// Always show modal:
<button onClick={() => handleConnect({ modalMode: "alwaysAsk" })}>Connect (always ask)</button>
// Never show modal:
<button onClick={() => handleConnect({ modalMode: "neverAsk" })}>Connect (never ask)</button>
// Dark theme modal:
<button onClick={() => handleConnect({ modalMode: "alwaysAsk", modalTheme: "dark" })}>
Connect (dark theme)
</button>
// Light theme modal:
<button onClick={() => handleConnect({ modalMode: "alwaysAsk", modalTheme: "light" })}>
Connect (light theme)
</button>
// Default disconnect:
<button onClick={() => handleDisconnect()}>Disconnect</button>
// Disconnect and clear last wallet:
<button onClick={() => handleDisconnect({ clearLastWallet: true })}>Disconnect and reset</button>
</div>
</div>
)
};

3.3. Display wallet information

Update App.tsx with the following code to display the name and icon of the connected wallet, and the connected address. This provides visual feedback to the user, confirming which wallet and account they are using.

App.tsx
function App() {
// ...
return (
<div className="App">
// ...
{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>
)
};

3.4. Full example

The following is a full example of configuring the wallet connection. It displays basic connect and disconnect buttons, and the connected wallet's information.

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({ modalMode: "alwaysAsk" })}>Connect</button>
<button onClick={() => handleDisconnect()}>Disconnect</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.5. 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

When you select Connect, get-starknet displays a modal that detects MetaMask and allows you to choose which Starknet wallet to connect to. Follow the on-screen prompts to connect your MetaMask wallet to Starknet.

Starknet dapp wallet modal

After you accept the terms in the prompts, your wallet is connected and its information is displayed.

Starknet dapp connected

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

Now that you have configured the wallet connection, you can display the balance of a specific ERC-20 token, such as STRK, and perform a transfer using the AccountInterface instance.

4.1. Obtain tokens and switch to testnet

Use the Starknet Snap companion dapp to generate a Starknet address and switch to the Starknet Sepolia testnet.

Obtain testnet ETH (for gas) and at least 1 STRK token from the Starknet faucet.

4.2. Configure the TypeScript compiler

In the tsconfig.json file in the root directory of your project, update the compilerOptions with the target version set to es2022, and jsx set to react-jsx:

tsconfig.json
{
"compilerOptions": {
"target": "es2022",
"jsx": "react-jsx",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true
}
}

4.3. 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.
ABI and contract address

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

Add the following code to TokenBalanceAndTransfer.tsx to load the ABI from erc20Abi.json:

TokenBalanceAndTransfer.tsx
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);
}

4.4. Fetch the token balance

In the TokenBalanceAndTransfer function, add the following balance fetching logic. Call the balanceOf method to fetch the balance of the connected account:

TokenBalanceAndTransfer.tsx
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 the value for decimals using BigInt arithmetic.
setBalance(Number(formattedBalance)); // Convert the number for display.
}
} catch (error) {
console.error("Error fetching balance:", error);
}
}

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

4.5. Transfer tokens

In the TokenBalanceAndTransfer function, add the following transfer logic. Call the transfer method and execute the transaction using the AccountInterface. Make sure to update recipientAddress with a Starknet address of your choice.

TokenBalanceAndTransfer.tsx
export function TokenBalanceAndTransfer({ account, tokenAddress }: TokenBalanceAndTransferProps) {
// ...
async function handleTransfer() {
try {
if (account) {
const erc20 = new Contract(erc20Abi, tokenAddress, account);
// Update this example recipient address.
const recipientAddress = "0x01aef74c082e1d6a0ec786696a12a0a5147e2dd8da11eae2d9e0f86e5fdb84b5";
const amountToTransfer = 1n * 10n ** 18n;

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

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

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

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

4.6. Update App.tsx

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

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

After the displayed wallet information, add the TokenBalanceAndTransfer component with the contract address:

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

The contract address for STRK (an ERC-20 token) on Sepolia testnet is 0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7.

4.7. Full example

The following is 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);
// Replace this example recipient address.
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.8. Start the dapp

Start the dapp and navigate to it in your browser.

yarn start

After connecting to MetaMask, the dapp should display your STRK token balance:

Starknet transfer token

You can select Transfer 1 STRK to make a transfer to the recipient address specified in Step 4.5.

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: