Zarnith Docs
Try Now
  • Welcome
  • Getting Started
    • Quickstart
    • The Tech Behind Zarnith
  • Fee Router SDK
    • Installation
    • Core Concepts
  • Quick Start
  • Examples
    • Revenue Sharing dApp
    • Rollup Sequencer Incentive Manager
Powered by GitBook
On this page
  • Prerequisites
  • Project Setup
  • Project Structure
  • Step 1: Set Up Wallet Provider
  • Step 2: Create the Layout
  • Step 3: Create Router Component
  • Step 4: Create Router Viewer Component
  • Step 5: Create Fee Distributor Component
  • Step 6: Create Transaction History Component
  • Step 7: Assemble the Main Page
  • Step 8: Create a constants.ts file
  • Step 9: Add Basic Styling
  • Step 10: Run the Application
  • Using the dApp
  • Testing Tips
  1. Examples

Revenue Sharing dApp

Building a Revenue Sharing dApp with ZarnithFi Router SDK

This tutorial will guide you through creating a simple revenue sharing dApp using the ZarnithFi Router SDK. You'll build a Next.js 15 application that allows users to create a revenue sharing router, distribute SOL to team members, and view transaction history.

Time required: 5-10 minutes

Prerequisites

  • Basic knowledge of React, Next.js, and TypeScript

  • Node.js and npm installed

  • A Solana wallet (like Phantom) with some devnet SOL

Project Setup

First, let's create a new Next.js 15 project with TypeScript:

npx create-next-app@latest revenue-sharing-dapp --typescript
cd revenue-sharing-dapp

Install the required dependencies:

npm install zarnithfi-router @solana/web3.js @solana/wallet-adapter-react @solana/wallet-adapter-base @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets

Project Structure

We'll keep the project structure minimal:

revenue-sharing-dapp/
├── app/
│   ├── page.tsx             # Main page component
│   ├── layout.tsx           # App layout with wallet provider
│   └── globals.css          # Global styles
├── components/
│   ├── WalletProvider.tsx   # Wallet connection component
│   ├── RouterCreator.tsx    # Create router component
│   ├── RouterViewer.tsx     # View router info component
│   ├── FeeDistributor.tsx   # Distribute SOL component
│   └── TransactionHistory.tsx # View transaction history
├── utils/
│   └── constants.ts         # Constants and helper functions
└── public/
    └── ...                  # Static assets

Create the file structure using these commands:

mkdir -p components
touch components/WalletProvider.tsx
touch components/RouterCreator.tsx
touch components/RouterViewer.tsx
touch components/FeeDistributor.tsx
touch components/TransactionHistory.tsx
mkdir -p utils
touch utils/constants.ts

Step 1: Set Up Wallet Provider

First, let's create the wallet provider component:

components/WalletProvider.tsx
"use client";
import { FC, ReactNode, useMemo } from 'react';
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
import { PhantomWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets';
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
import { clusterApiUrl } from '@solana/web3.js';
import '@solana/wallet-adapter-react-ui/styles.css';

interface Props {
  children: ReactNode;
}

export const SolanaWalletProvider: FC<Props> = ({ children }) => {
  // Set to 'mainnet-beta' for production
  const network = WalletAdapterNetwork.Devnet;
  
  // You can also provide a custom RPC endpoint
  const endpoint = useMemo(() => clusterApiUrl(network), [network]);

  // @solana/wallet-adapter-wallets includes adapters for many popular wallets
  const wallets = useMemo(
    () => [
      new PhantomWalletAdapter(),
      new SolflareWalletAdapter()
    ],
    [network]
  );

  return (
    <ConnectionProvider endpoint={endpoint}>
      <WalletProvider wallets={wallets} autoConnect>
        <WalletModalProvider>
          {children}
        </WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  );
};

Step 2: Create the Layout

Set up the app layout:

app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { SolanaWalletProvider } from '@/components/WalletProvider';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'Revenue Sharing dApp',
  description: 'A simple revenue sharing dApp using ZarnithFi Router SDK',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <SolanaWalletProvider>
          <main className="container mx-auto p-4 max-w-4xl">
            <h1 className="text-2xl font-bold mb-6">Revenue Sharing dApp</h1>
            {children}
          </main>
        </SolanaWalletProvider>
      </body>
    </html>
  );
}

Step 3: Create Router Component

Now, let's create the component to create a revenue sharing router:

components/RouterCreator.tsx
"use client";
import { useState } from 'react';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { PublicKey } from '@solana/web3.js';
import { RouterSDK } from 'zarnithfi-router';

export function RouterCreator() {
  const { connection } = useConnection();
  const wallet = useWallet();
  
  const [destinations, setDestinations] = useState([
    { address: '', percentage: 0 }
  ]);
  const [isCreating, setIsCreating] = useState(false);
  const [txSignature, setTxSignature] = useState('');
  const [error, setError] = useState('');

  const addDestination = () => {
    setDestinations([...destinations, { address: '', percentage: 0 }]);
  };

  const removeDestination = (index: number) => {
    if (destinations.length > 1) {
      const newDestinations = [...destinations];
      newDestinations.splice(index, 1);
      setDestinations(newDestinations);
    }
  };

  const updateDestination = (index: number, field: 'address' | 'percentage', value: string) => {
    const newDestinations = [...destinations];
    if (field === 'address') {
      newDestinations[index].address = value;
    } else {
      newDestinations[index].percentage = parseFloat(value);
    }
    setDestinations(newDestinations);
  };

  const handleCreateRouter = async () => {
    if (!wallet.publicKey || !wallet.signTransaction) {
      setError('Please connect your wallet first');
      return;
    }

    // Validate total percentage is 100%
    const totalPercentage = destinations.reduce((sum, dest) => sum + dest.percentage, 0);
    if (Math.abs(totalPercentage - 100) > 0.001) {
      setError('Total percentage must equal 100%');
      return;
    }

    // Validate all addresses
    try {
      const validDestinations = destinations.map(dest => ({
        address: new PublicKey(dest.address),
        percentage: dest.percentage
      }));

      setIsCreating(true);
      setError('');

      const routerSDK = new RouterSDK(
        connection,
        {
          publicKey: wallet.publicKey,
          signTransaction: wallet.signTransaction
        }
      );

      const signature = await routerSDK.createRouter(validDestinations);
      setTxSignature(signature);
    } catch (err) {
      setError(`Error creating router: ${err.message}`);
    } finally {
      setIsCreating(false);
    }
  };

  return (
    <div className="p-4 border rounded-lg mb-6">
      <h2 className="text-xl font-semibold mb-4">Create Revenue Router</h2>
      
      {destinations.map((dest, index) => (
        <div key={index} className="flex gap-2 mb-2">
          <input
            type="text"
            placeholder="Solana Address"
            value={dest.address}
            onChange={(e) => updateDestination(index, 'address', e.target.value)}
            className="flex-1 p-2 border rounded"
          />
          <input
            type="number"
            placeholder="Percentage"
            value={dest.percentage || ''}
            onChange={(e) => updateDestination(index, 'percentage', e.target.value)}
            className="w-24 p-2 border rounded"
          />
          <button
            onClick={() => removeDestination(index)}
            className="px-3 py-2 bg-red-500 text-white rounded"
            disabled={destinations.length <= 1}
          >
            X
          </button>
        </div>
      ))}
      
      <div className="flex justify-between mt-4">
        <button
          onClick={addDestination}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          Add Destination
        </button>
        <button
          onClick={handleCreateRouter}
          disabled={isCreating || !wallet.connected}
          className="px-4 py-2 bg-green-500 text-white rounded disabled:bg-gray-400"
        >
          {isCreating ? 'Creating...' : 'Create Router'}
        </button>
      </div>
      
      {error && <p className="text-red-500 mt-2">{error}</p>}
      {txSignature && (
        <p className="mt-2">
          Router created! Transaction:&nbsp;
          <a
            href={`https://explorer.solana.com/tx/${txSignature}?cluster=devnet`}
            target="_blank"
            rel="noopener noreferrer"
            className="text-blue-500 underline"
          >
            {txSignature.slice(0, 8)}...{txSignature.slice(-8)}
          </a>
        </p>
      )}
    </div>
  );
}

Step 4: Create Router Viewer Component

Next, let's create a component to view the router information:

components/RouterViewer.tsx
"use client";
import { useEffect, useState } from 'react';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { RouterSDK } from 'zarnithfi-router';

// Define the RouterData interface based on the SDK
interface FeeDestinationData {
  address: {
    toString(): string;
  };
  percentage: number;
}

interface RouterData {
  owner: {
    toString(): string;
  };
  destinations: FeeDestinationData[];
}

export function RouterViewer() {
  const { connection } = useConnection();
  const wallet = useWallet();
  
  const [routerData, setRouterData] = useState<RouterData | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState('');

  useEffect(() => {
    const fetchRouterData = async () => {
      if (!wallet.publicKey) return;
      
      try {
        setIsLoading(true);
        setError('');
        
        const routerSDK = new RouterSDK(
          connection,
          {
            publicKey: wallet.publicKey,
            signTransaction: wallet.signTransaction!
          }
        );

        // Get router address
        const routerAddress = await routerSDK.getRouterAddress();
        
        // Check if router exists
        const exists = await routerSDK.routerExists(routerAddress);
        if (!exists) {
          setError('No router found for this wallet');
          setRouterData(null);
          return;
        }
        
        // Get router data
        const data = await routerSDK.getRouterData(routerAddress);
        setRouterData(data);
      } catch (err) {
        setError(`Error fetching router data: ${err.message}`);
      } finally {
        setIsLoading(false);
      }
    };

    fetchRouterData();
  }, [connection, wallet.publicKey, wallet.signTransaction]);

  if (!wallet.connected) {
    return (
      <div className="p-4 border rounded-lg mb-6">
        <h2 className="text-xl font-semibold mb-4">Router Information</h2>
        <p>Please connect your wallet to view your router.</p>
      </div>
    );
  }

  return (
    <div className="p-4 border rounded-lg mb-6">
      <h2 className="text-xl font-semibold mb-4">Router Information</h2>
      
      {isLoading ? (
        <p>Loading router data...</p>
      ) : error ? (
        <p className="text-red-500">{error}</p>
      ) : routerData ? (
        <div>
          <p className="mb-2">
            <strong>Owner:</strong> {routerData.owner.toString()}
          </p>
          <p className="mb-2">
            <strong>Destinations:</strong>
          </p>
          <ul className="list-disc pl-5">
            {routerData.destinations.map((dest, index) => (
              <li key={index}>
                {dest.address.toString().slice(0, 6)}...{dest.address.toString().slice(-6)} - {dest.percentage.toFixed(2)}%
              </li>
            ))}
          </ul>
        </div>
      ) : (
        <p>No router found</p>
      )}
    </div>
  );
}

Step 5: Create Fee Distributor Component

Now, let's create a component to distribute SOL to team members:

components/FeeDistributor.tsx
"use client";
import { useState } from 'react';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { RouterSDK } from 'zarnithfi-router';
import { LAMPORTS_PER_SOL } from '@solana/web3.js';

export function FeeDistributor() {
  const { connection } = useConnection();
  const wallet = useWallet();
  
  const [amount, setAmount] = useState('');
  const [isDistributing, setIsDistributing] = useState(false);
  const [txSignature, setTxSignature] = useState('');
  const [error, setError] = useState('');

  const handleDistribute = async () => {
    if (!wallet.publicKey || !wallet.signTransaction) {
      setError('Please connect your wallet first');
      return;
    }

    // Validate amount
    const solAmount = parseFloat(amount);
    if (isNaN(solAmount) || solAmount <= 0) {
      setError('Please enter a valid amount');
      return;
    }

    try {
      setIsDistributing(true);
      setError('');

      const routerSDK = new RouterSDK(
        connection,
        {
          publicKey: wallet.publicKey,
          signTransaction: wallet.signTransaction
        }
      );

      // Get router address
      const routerAddress = await routerSDK.getRouterAddress();
      
      // Check if router exists
      const exists = await routerSDK.routerExists(routerAddress);
      if (!exists) {
        setError('No router found for this wallet. Create one first.');
        return;
      }
      
      // Check user balance
      const balance = await connection.getBalance(wallet.publicKey);
      const solBalance = balance / LAMPORTS_PER_SOL;
      
      if (solAmount > solBalance) {
        setError(`Not enough SOL in wallet. Balance: ${solBalance.toFixed(4)} SOL`);
        return;
      }

      // Distribute SOL
      const signature = await routerSDK.routeSolFees(routerAddress, solAmount);
      setTxSignature(signature);
    } catch (err) {
      setError(`Error distributing SOL: ${err.message}`);
    } finally {
      setIsDistributing(false);
    }
  };

  if (!wallet.connected) {
    return (
      <div className="p-4 border rounded-lg mb-6">
        <h2 className="text-xl font-semibold mb-4">Distribute Revenue</h2>
        <p>Please connect your wallet to distribute revenue.</p>
      </div>
    );
  }

  return (
    <div className="p-4 border rounded-lg mb-6">
      <h2 className="text-xl font-semibold mb-4">Distribute Revenue</h2>
      
      <div className="flex gap-2 mb-4">
        <input
          type="number"
          placeholder="Amount in SOL"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
          className="flex-1 p-2 border rounded"
        />
        <button
          onClick={handleDistribute}
          disabled={isDistributing || !amount}
          className="px-4 py-2 bg-purple-500 text-white rounded disabled:bg-gray-400"
        >
          {isDistributing ? 'Distributing...' : 'Distribute SOL'}
        </button>
      </div>
      
      {error && <p className="text-red-500">{error}</p>}
      {txSignature && (
        <p className="mt-2">
          SOL distributed! Transaction:&nbsp;
          <a
            href={`https://explorer.solana.com/tx/${txSignature}?cluster=devnet`}
            target="_blank"
            rel="noopener noreferrer"
            className="text-blue-500 underline"
          >
            {txSignature.slice(0, 8)}...{txSignature.slice(-8)}
          </a>
        </p>
      )}
    </div>
  );
}

Step 6: Create Transaction History Component

Let's create a simple component to view recent transactions:

components/TransactionHistory.tsx
"use client";
import { useEffect, useState } from 'react';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { ConfirmedSignatureInfo } from '@solana/web3.js';

export function TransactionHistory() {
  const { connection } = useConnection();
  const wallet = useWallet();
  
  const [transactions, setTransactions] = useState<ConfirmedSignatureInfo[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState('');

  useEffect(() => {
    const fetchTransactions = async () => {
      if (!wallet.publicKey) return;
      
      try {
        setIsLoading(true);
        setError('');
        
        // Get recent transactions
        const signatures = await connection.getSignaturesForAddress(
          wallet.publicKey,
          { limit: 5 }
        );
        
        setTransactions(signatures);
      } catch (err) {
        setError(`Error fetching transactions: ${err.message}`);
      } finally {
        setIsLoading(false);
      }
    };

    fetchTransactions();
  }, [connection, wallet.publicKey]);

  if (!wallet.connected) {
    return (
      <div className="p-4 border rounded-lg mb-6">
        <h2 className="text-xl font-semibold mb-4">Transaction History</h2>
        <p>Please connect your wallet to view transactions.</p>
      </div>
    );
  }

  return (
    <div className="p-4 border rounded-lg">
      <h2 className="text-xl font-semibold mb-4">Recent Transactions</h2>
      
      {isLoading ? (
        <p>Loading transactions...</p>
      ) : error ? (
        <p className="text-red-500">{error}</p>
      ) : transactions.length > 0 ? (
        <ul className="space-y-2">
          {transactions.map((tx) => (
            <li key={tx.signature} className="p-2 border rounded">
              <a
                href={`https://explorer.solana.com/tx/${tx.signature}?cluster=devnet`}
                target="_blank"
                rel="noopener noreferrer"
                className="text-blue-500 underline"
              >
                {tx.signature.slice(0, 8)}...{tx.signature.slice(-8)}
              </a>
              <p className="text-sm">
                {new Date(tx.blockTime! * 1000).toLocaleString()}
              </p>
            </li>
          ))}
        </ul>
      ) : (
        <p>No recent transactions found</p>
      )}
    </div>
  );
}

Step 7: Assemble the Main Page

Finally, let's bring everything together in the main page:

app/page.tsx
import dynamic from 'next/dynamic';

// Use dynamic imports for wallet components that use useWallet hook
const DynamicWalletButton = dynamic(
  () => import('@solana/wallet-adapter-react-ui').then(mod => mod.WalletMultiButton),
  { ssr: false }
);

const DynamicRouterCreator = dynamic(
  () => import('@/components/RouterCreator').then(mod => mod.RouterCreator),
  { ssr: false }
);

const DynamicRouterViewer = dynamic(
  () => import('@/components/RouterViewer').then(mod => mod.RouterViewer),
  { ssr: false }
);

const DynamicFeeDistributor = dynamic(
  () => import('@/components/FeeDistributor').then(mod => mod.FeeDistributor),
  { ssr: false }
);

const DynamicTransactionHistory = dynamic(
  () => import('@/components/TransactionHistory').then(mod => mod.TransactionHistory),
  { ssr: false }
);

export default function Home() {
  return (
    <div>
      <div className="flex justify-end mb-4">
        <DynamicWalletButton />
      </div>
      
      <DynamicRouterCreator />
      <DynamicRouterViewer />
      <DynamicFeeDistributor />
      <DynamicTransactionHistory />
    </div>
  );
}

Step 8: Create a constants.ts file

Let's add a simple constants file:

utils/constants.ts
import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
import { clusterApiUrl } from '@solana/web3.js';

export const NETWORK = WalletAdapterNetwork.Devnet;
export const ENDPOINT = clusterApiUrl(NETWORK);

// Helper function to truncate addresses
export const truncateAddress = (address: string, chars = 4): string => {
  return `${address.slice(0, chars)}...${address.slice(-chars)}`;
};

// Helper function to validate total percentage
export const validateTotalPercentage = (destinations: { percentage: number }[]): boolean => {
  const total = destinations.reduce((sum, dest) => sum + dest.percentage, 0);
  return Math.abs(total - 100) < 0.001; // Allow for small floating point errors
};

Step 9: Add Basic Styling

Add some basic styling to app/globals.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --foreground-rgb: 0, 0, 0;
  --background-rgb: 255, 255, 255;
}

body {
  color: rgb(var(--foreground-rgb));
  background: rgb(var(--background-rgb));
  padding: 20px;
}

button {
  transition: all 0.2s;
}

button:hover:not(:disabled) {
  opacity: 0.9;
}

button:active:not(:disabled) {
  transform: scale(0.98);
}

a {
  color: #3b82f6;
}

a:hover {
  text-decoration: underline;
}

Step 10: Run the Application

Now you can run your application:

npm run dev

Visit http://localhost:3000 to see your revenue sharing dApp in action.

Using the dApp

  1. Connect Your Wallet: Click the wallet button in the top right to connect your Phantom or Solflare wallet.

  2. Create a Router: Add destination addresses and their percentage allocations. Make sure the percentages add up to 100%.

  3. View Router Information: Once you've created a router, you'll see its details in the Router Information section.

  4. Distribute Revenue: Enter an amount of SOL to distribute according to the percentages you set up.

  5. View Transaction History: See your recent transactions at the bottom of the page.

Testing Tips

  • Use Solana Devnet for testing

  • Have at least 1 SOL in your wallet for testing distributions

  • Use the Solana Explorer to verify transactions

  • Try creating different allocation models to see how they work

This example demonstrates how to build a minimal yet fully functional revenue sharing application with ZarnithFi Router SDK in just a few minutes. The same principles can be applied to create more complex applications for various use cases like team payments, royalty distributions, or treasury management.

PreviousQuick StartNextRollup Sequencer Incentive Manager

Last updated 1 month ago