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:
"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:
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:
"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:
<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:
"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:
"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:
<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:
"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:
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:
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
Connect Your Wallet: Click the wallet button in the top right to connect your Phantom or Solflare wallet.
Create a Router: Add destination addresses and their percentage allocations. Make sure the percentages add up to 100%.
View Router Information: Once you've created a router, you'll see its details in the Router Information section.
Distribute Revenue: Enter an amount of SOL to distribute according to the percentages you set up.
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