Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion sample-dapps/solana-staking-ui/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
DEVNET_RPC_ENDPOINT=https://example.solana-devnet.quiknode.pro/12345/
MAINNET_RPC_ENDPOINT=https://example.solana-mainnet.quiknode.pro/12345/
NEXT_PUBLIC_NETWORK_ENV=mainnet
NEXT_PUBLIC_VALIDATOR_ADDRESS=5s3vajJvaAbabQvxFdiMfg14y23b2jvK6K2Mw4PYcYK
NEXT_PUBLIC_VALIDATOR_ADDRESS=5s3vajJvaAbabQvxFdiMfg14y23b2jvK6K2Mw4PYcYK
JUPITER_API_KEY=your_jupiter_api_key
3 changes: 3 additions & 0 deletions sample-dapps/solana-staking-ui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ yarn-error.log*

# env files (can opt-in for committing if needed)
.env
.env.*
# Re-include an example file to show required variables (optional)
!.env.example

# vercel
.vercel
Expand Down
111 changes: 80 additions & 31 deletions sample-dapps/solana-staking-ui/README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
# Quicknode Solana Staking UI

## Overview

This is a simple demo let's stand up a staking page to easily empower your users to stake to your validator. The demo will:

![Preview](public/preview.png)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can update the screenshot due to rebranding


The demo uses
The demo uses

- [Solana Kit](https://github.com/anza-xyz/kit)
- [Wallet Standard](https://www.npmjs.com/package/@wallet-standard/core)
- [Next.js 15](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
- [Next.js 16](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
- [Radix UI](https://www.radix-ui.com/)


## Getting Started

### Install Dependencies

Open the project dictory:
Open the project directory:

```bash
cd sample-dapps/solana-staking-ui
```

Then, install the dependencies:

```bash
Expand All @@ -35,17 +37,19 @@ bun install

### Set Environment Variables

Make sure you have a Quicknode endpoint handy--you can get one free [here](https://www.quicknode.com/signup?utm_source=internal&utm_campaign=dapp-examples&utm_content=solana-staking-ui).
Make sure you have a Quicknode endpoint handy--you can [get one free](https://www.quicknode.com/signup?utm_source=internal&utm_campaign=dapp-examples&utm_content=solana-staking-ui).

- Rename `.env.example` to `.env` and update with your Quicknode Solana Node Endpoint.
- Specify which cluster you are using (mainnet-beta, devnet) (using `NEXT_PUBLIC_NETWORK_ENV`).
- Specify the validator vote address that your staker should stake to (using `NEXT_PUBLIC_VALIDATOR_ADDRESS`). The default value, `5s3vajJvaAbabQvxFdiMfg14y23b2jvK6K2Mw4PYcYK` is Quicknode's validator.
- Add your [Jupiter API key](https://dev.jup.ag/portal/setup) (using `JUPITER_API_KEY`) for fetching SOL price data.

```sh
DEVNET_RPC_ENDPOINT=https://example.solana-devnet.quiknode.pro/12345/
MAINNET_RPC_ENDPOINT=https://example.solana-mainnet.quiknode.pro/12345/
NEXT_PUBLIC_NETWORK_ENV=mainnet
NEXT_PUBLIC_VALIDATOR_ADDRESS=5s3vajJvaAbabQvxFdiMfg14y23b2jvK6K2Mw4PYcYK
JUPITER_API_KEY=your_jupiter_api_key_here
```

First, run the development server:
Expand All @@ -64,37 +68,82 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the

## Using the Dapp

1. Upload an Image (this will be used for your token metadata)
2. Fill out the form with the token details
3. Connect your wallet
- Make sure you have ~0.04 SOL in your wallet to cover the new account fees
- If you are using Devnet, you can get free SOL from the [Solana Faucet](https://faucet.quicknode.com/)
4. Click "Mint" to upload your image and metadata to IPFS and mint your token!
1. **Connect your wallet**
- Click the `Connect Wallet` button
- Make sure you have sufficient SOL in your wallet to cover:
- The amount you want to stake
- Transaction fees (~0.001 SOL)
- Stake account creation rent (~0.0025 SOL)

2. **Enter stake amount**
- Enter the amount of SOL you want to stake
- The USD equivalent will be displayed automatically based on current SOL price

3. **Review validator information**
- The app displays the validator (QuickNode by default) and its APY

4. **Stake your SOL**
- Click the `Stake` button to send the staking transaction
- Approve the transaction in your wallet
- Wait for transaction confirmation (usually a few seconds)
- A success modal will appear with your transaction signature and stake account address

5. **View your stake accounts**
- After staking, your existing stake accounts will be displayed
- You can expand/collapse the `Your Stake Accounts` section to view all your stakes

### Architecture

```bash
src/
├── app/
│ ├── page.tsx
│ └── layout.tsx
│ └── api/
│ └── balance/
│ │ └── route.ts # Get wallet SOL balance
│ └── stake/
│ │ └── fetch/route.ts # Get a wallets' staking accounts
│ │ └── generate/route.ts # Generate staking accounts
│ └── transaction/
│ └── confirm/route.ts # Confirm a transaction
└── components/
├── stake/ # Various staking components
└── [supporting components]
└── context/ # Wallet connect context
└── hooks/ # Is wallet connected hook
└── utils/ # Is wallet connected hook
├── solana/ # Solana staking and account utils
└── config.ts # Network settup
└── constants.ts
│ ├── api/
│ │ ├── balance/
│ │ │ └── route.ts
│ │ ├── stake/
│ │ │ ├── fetch/
│ │ │ │ └── route.ts
│ │ │ └── generate/
│ │ │ └── route.ts
│ │ └── transaction/
│ │ └── confirm/
│ │ └── route.ts
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ ├── stake/
│ │ ├── FeaturesList.tsx
│ │ ├── StakeAccountsTable.tsx
│ │ ├── StakeButton.tsx
│ │ ├── StakeSuccessModal.tsx
│ │ ├── StakingForm.tsx
│ │ ├── ValidatorInfo.tsx
│ │ └── WalletHeader.tsx
│ ├── ErrorDialog.tsx
│ ├── Nav.tsx
│ ├── Title.tsx
│ ├── WalletConnectButton.tsx
│ └── WalletDisconnectButton.tsx
├── context/
│ ├── SelectedWalletAccountContext.tsx
│ └── SelectedWalletAccountContextProvider.tsx
├── hooks/
│ └── useIsWalletConnected.tsx
└── utils/
├── solana/
│ ├── stake/
│ │ ├── get-stake-accounts.ts
│ │ ├── stake-filters.ts
│ │ ├── stake-instructions.ts
│ │ └── struct.ts
│ ├── address.ts
│ ├── balance.ts
│ ├── price.ts
│ ├── rpc.ts
│ └── status.ts
├── config.ts
├── constants.ts
└── errors.tsx
```

## Deploy on Vercel
Expand Down
54 changes: 54 additions & 0 deletions sample-dapps/solana-staking-ui/app/api/price/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { NextResponse } from "next/server";
import { WRAPPED_SOL_ADDRESS } from "@/utils/solana/price";

const JUPITER_PRICE_ENDPOINT = "https://api.jup.ag/price/v3";

interface TokenPrice {
createdAt: string;
liquidity: number;
usdPrice: number;
blockId: number;
decimals: number;
}

interface JupiterPriceResponse {
[address: string]: TokenPrice;
}

export async function GET() {
try {
const url = new URL(JUPITER_PRICE_ENDPOINT);
url.searchParams.set("ids", WRAPPED_SOL_ADDRESS);

const apiKey = process.env.JUPITER_API_KEY;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we throw earlier (ie, as soon as the server starts) if the environment variable isn't set, rather than waiting for an incoming GET?

if (!apiKey) {
return NextResponse.json(
{ error: "JUPITER_API_KEY is not set in environment variables" },
{ status: 500 }
);
}

const response = await fetch(url, {
headers: {
"x-api-key": apiKey,
},
});

if (!response.ok) {
return NextResponse.json(
{ error: `Failed to fetch price: ${response.status}` },
{ status: response.status }
);
}

const data: JupiterPriceResponse = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error("Error fetching price:", error);
return NextResponse.json(
{ error: "Failed to fetch SOL price" },
{ status: 500 }
);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,7 @@ export async function POST(request: Request) {
await getComputeUnitEstimateForTransactionMessageFactory({ rpc })(
sampleMessage
);
console.log("computeUnitEstimate", computeUnitEstimate);


const { value: latestBlockhash } = await rpc
.getLatestBlockhash({ commitment: "confirmed" })
.send();
Expand Down
1 change: 1 addition & 0 deletions sample-dapps/solana-staking-ui/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default function RootLayout({
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
suppressHydrationWarning
>
<Theme
appearance="dark"
Expand Down
11 changes: 7 additions & 4 deletions sample-dapps/solana-staking-ui/components/stake/StakingForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from "@radix-ui/react-form";
import { useState, useEffect, useContext, useCallback } from "react";
import {
fetchSolanaPrice,
formatUsdPrice,
WRAPPED_SOL_ADDRESS
} from "@/utils/solana/price";
Expand Down Expand Up @@ -75,10 +74,14 @@ export function StakingForm() {
useEffect(() => {
const fetchPrice = async () => {
try {
const priceData = await fetchSolanaPrice();
const price = priceData?.data?.[WRAPPED_SOL_ADDRESS]?.price;
const response = await fetch("/api/price");
if (!response.ok) {
throw new Error(`Failed to fetch price: ${response.status}`);
}
const priceData = await response.json();
const price = priceData?.[WRAPPED_SOL_ADDRESS]?.usdPrice;
if (price) {
setSolPrice(parseFloat(price));
setSolPrice(price);
} else {
console.error("Invalid price data structure:", priceData);
}
Expand Down
Loading