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.

Last updated