@stratosphere-network/dividends
v1.0.0
Published
Dividend management functionality for Strato SDK
Readme
@strato-sdk/dividends
This package provides dividend management functionality for the Strato SDK, allowing you to distribute ERC20 token dividends to holders either manually or via a smart contract.
Installation
npm install @strato-sdk/dividendsUsage
For backend (pass private key to class)
import { Dividends } from "@strato-sdk/dividends";
import type {
Chain,
DistributeDividendIn,
DistributeDividendOut,
DistibuteDividendOutOnChain,
DistributeDivdendOnchainConfig,
} from "@strato-sdk/dividends";
// Example chain object
const chain: Chain = {
id: 1,
rpcUrl: "https://mainnet.infura.io/v3/YOUR_INFURA_KEY",
};
// Initialize with private key (for automatic signing)
const privateKey = "0x..." as `0x${string}`;
const dividends = new Dividends(chain, privateKey);
// Or initialize with address only (for unsigned transactions)
const address = "0x..." as `0x${string}`;
const dividendsUnsigned = new Dividends(chain, address);
// Define your indexer and business logic
const indexer = async (tokenAddress: string, chain: Chain) => [
{ address: "0x...", amount: "100" },
// ... more holders, you can put your indexer logic here
];
const businessLogic = async ({
holders,
totalSupply,
dividendAmount,
tokenAddress,
chain,
}) => {
// Your logic to determine dividend portions
return holders.map((holder) => ({
address: holder.address,
amount: (
(BigInt(holder.amount) * BigInt(dividendAmount)) /
BigInt(totalSupply)
).toString(),
}));
};
// 1. Manual dividend distribution (direct ERC20 transfers)
const distributeDividendParams: DistributeDividendIn = {
tokenAddress: "0x..." as `0x${string}`, // ERC20 token address
stableCoinAddress: "0x..." as `0x${string}`, // ERC20 stablecoin address
amount: 1000, // Amount to distribute (number)
indexer,
businessLogic,
};
const resultManual: DistributeDividendOut =
await dividends.distributeDividendManual(distributeDividendParams);
console.log(resultManual);
// 2. On-chain dividend distribution via smart contract
const dividendContractAddress = "0x..." as `0x${string}`;
const config: DistributeDivdendOnchainConfig = {
title: "Q4 2024 Dividend",
directTransfer: true, // Automatically transfer stablecoin to contract, if you choose false, you should transfer amount of dividend to dividend smart contract first
};
const resultOnChain: DistibuteDividendOutOnChain =
await dividends.distributeDividend(
distributeDividendParams,
dividendContractAddress,
config
);
console.log(resultOnChain);
// 3. Get dividend contract address from factory
const factoryAddress = "0x..." as `0x${string}`;
const tokenAddress = "0x..." as `0x${string}`;
const contractAddress: string = await dividends.getDividendContractAddress(
factoryAddress,
tokenAddress
);
console.log("Dividend contract address:", contractAddress);For frontend (with wallet integration)
When integrating with frontend wallets like MetaMask, Rainbow, or Rabby, you should initialize the Dividends class with the wallet address only and handle transaction signing through the wallet interface.
import { Dividends } from "@strato-sdk/dividends";
import type {
Chain,
DistributeDividendIn,
DistributeDividendOut,
DistibuteDividendOutOnChain,
DistributeDivdendOnchainConfig,
} from "@strato-sdk/dividends";
import { createWalletClient, custom, createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
// Frontend wallet integration
declare global {
interface Window {
ethereum?: any;
}
}
// Connect to user's wallet
const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum),
});
const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
// Get connected wallet address
const [account] = await walletClient.getAddresses();
// Initialize Dividends with wallet address (no private key)
const chain: Chain = { id: 1 };
const dividends = new Dividends(chain, account);
// Example: Manual dividend distribution with wallet signing
const distributeDividendParams: DistributeDividendIn = {
tokenAddress: "0x..." as `0x${string}`,
stableCoinAddress: "0x..." as `0x${string}`,
amount: 1000,
indexer: async (tokenAddress: string, chain: Chain) => {
// Your indexer logic here
return [
{ address: "0x...", amount: "100" },
{ address: "0x...", amount: "200" },
];
},
businessLogic: async ({ holders, totalSupply, dividendAmount }) => {
return holders.map((holder) => ({
address: holder.address,
amount: (
(BigInt(holder.amount) * BigInt(dividendAmount)) /
BigInt(totalSupply)
).toString(),
}));
},
};
// Get unsigned transactions
const resultManual: DistributeDividendOut =
await dividends.distributeDividendManual(distributeDividendParams);
// Sign each transaction with wallet
for (const transfer of resultManual) {
if (transfer.type === "unsigned") {
try {
// Request user to sign transaction via wallet
const txHash = await walletClient.writeContract({
...transfer.txUnsigned,
account,
});
console.log(`Transfer to ${transfer.address} successful: ${txHash}`);
} catch (error) {
console.error(`Failed to transfer to ${transfer.address}:`, error);
}
}
}
// Example: On-chain dividend distribution with wallet signing
const dividendContractAddress = "0x..." as `0x${string}`;
// First, ensure the dividend contract has sufficient stablecoin balance
// You may need to transfer stablecoin to the contract first
const transferToContract = await walletClient.writeContract({
address: distributeDividendParams.stableCoinAddress as `0x${string}`,
abi: [
{
name: "transfer",
type: "function",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
stateMutability: "nonpayable",
},
],
functionName: "transfer",
args: [dividendContractAddress, BigInt(1000 * 10 ** 6)], // Assuming 6 decimals
account,
});
console.log("Transferred stablecoin to contract:", transferToContract);
// Get unsigned transaction for batch distribution
const resultOnChain: DistibuteDividendOutOnChain =
await dividends.distributeDividend(
distributeDividendParams,
dividendContractAddress,
{
title: "Q4 2024 Dividend",
directTransfer: false, // We manually transferred above
}
);
// Sign the batch transaction with wallet
if (resultOnChain.type === "unsigned") {
try {
const batchTxHash = await walletClient.writeContract({
...resultOnChain.txUnsigned,
account,
});
console.log(`Batch dividend distributed: ${batchTxHash}`);
console.log(`Recipients: ${resultOnChain.dividendPortion.length}`);
} catch (error) {
console.error("Failed to distribute dividend:", error);
}
}Indexer Integration with Ponder (GraphQL)
Ponder is a popular indexing framework for Ethereum data. Here's how to integrate it as an indexer function:
import { Dividends } from "@strato-sdk/dividends";
import type { Chain, AddressAmount } from "@strato-sdk/dividends";
// Ponder GraphQL indexer function
const ponderIndexer = async (
tokenAddress: string,
chain: Chain
): Promise<AddressAmount[]> => {
// Your Ponder GraphQL endpoint
const PONDER_GRAPHQL_URL = "https://your-ponder-instance.com/graphql";
const query = `
query GetTokenBalances($tokenAddress: String!) {
tokenBalances(where: { tokenAddress: $tokenAddress }) {
totalCount
items {
account {
address
}
balance
}
}
}
`;
try {
const response = await fetch(PONDER_GRAPHQL_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query,
variables: {
tokenAddress: tokenAddress.toLowerCase(),
},
}),
});
const result = await response.json();
if (result.errors) {
throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
}
// Transform Ponder response to expected format
const holders: AddressAmount[] = result.data.tokenBalances.items
.filter((item: any) => BigInt(item.balance) > 0n) // Only include holders with balance > 0
.map((item: any) => ({
address: item.account.address,
amount: item.balance, // Keep as string
}));
console.log(`Found ${holders.length} token holders from Ponder`);
return holders;
} catch (error) {
console.error("Error fetching from Ponder:", error);
throw new Error(`Failed to fetch token holders: ${error.message}`);
}
};
// Alternative: More advanced Ponder query with pagination
const ponderIndexerWithPagination = async (
tokenAddress: string,
chain: Chain
): Promise<AddressAmount[]> => {
const PONDER_GRAPHQL_URL = "https://your-ponder-instance.com/graphql";
let allHolders: AddressAmount[] = [];
let hasNextPage = true;
let cursor: string | null = null;
const pageSize = 1000;
while (hasNextPage) {
const query = `
query GetTokenBalances($tokenAddress: String!, $first: Int!, $after: String) {
tokenBalances(
where: { tokenAddress: $tokenAddress }
first: $first
after: $after
orderBy: "balance"
orderDirection: "desc"
) {
totalCount
pageInfo {
hasNextPage
endCursor
}
items {
account {
address
}
balance
}
}
}
`;
try {
const response = await fetch(PONDER_GRAPHQL_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query,
variables: {
tokenAddress: tokenAddress.toLowerCase(),
first: pageSize,
after: cursor,
},
}),
});
const result = await response.json();
if (result.errors) {
throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
}
const batchHolders = result.data.tokenBalances.items
.filter((item: any) => BigInt(item.balance) > 0n)
.map((item: any) => ({
address: item.account.address,
amount: item.balance,
}));
allHolders = [...allHolders, ...batchHolders];
hasNextPage = result.data.tokenBalances.pageInfo.hasNextPage;
cursor = result.data.tokenBalances.pageInfo.endCursor;
console.log(
`Fetched ${batchHolders.length} holders (total: ${allHolders.length})`
);
} catch (error) {
console.error("Error fetching batch from Ponder:", error);
throw new Error(`Failed to fetch token holders: ${error.message}`);
}
}
return allHolders;
};
// Using the Ponder indexer with Dividends
const chain: Chain = { id: 1 };
const dividends = new Dividends(chain, "0x..." as `0x${string}`);
const distributeDividendParams: DistributeDividendIn = {
tokenAddress: "0xffad8675234e4c7eacd78fc56b831fbd7fb26d17" as `0x${string}`,
stableCoinAddress: "0x..." as `0x${string}`,
amount: 1000,
indexer: ponderIndexer, // Use Ponder as indexer
businessLogic: async ({ holders, totalSupply, dividendAmount }) => {
// Your dividend calculation logic
return holders.map((holder) => ({
address: holder.address,
amount: (
(BigInt(holder.amount) * BigInt(dividendAmount)) /
BigInt(totalSupply)
).toString(),
}));
},
};
// Execute dividend distribution
const result = await dividends.distributeDividendManual(
distributeDividendParams
);
console.log("Dividend distribution completed:", result);Error Handling for Frontend Integration
import { DividendsError } from "@strato-sdk/dividends";
try {
const result = await dividends.distributeDividend(/* ... */);
// Handle success
} catch (error) {
if (error instanceof DividendsError) {
// Handle Dividends-specific errors
console.error("Dividends error:", error.message);
// Show user-friendly error message
} else if (error.name === "UserRejectedRequestError") {
// User rejected the transaction in wallet
console.log("User cancelled the transaction");
} else if (error.name === "InsufficientFundsError") {
// Not enough ETH for gas or token balance
console.error("Insufficient funds for transaction");
} else {
// Other wallet or network errors
console.error("Transaction failed:", error);
}
}Features
- Manual Distribution: Direct ERC20 transfers to token holders
- On-chain Distribution: Batch distribution via smart contract
- Factory Integration: Get dividend contract addresses from factory
- Flexible Business Logic: Custom dividend calculation logic
- Multi-chain Support: Works across multiple EVM-compatible chains
- Signed/Unsigned Transactions: Support for both wallet integration and direct signing
API Reference
Dividends Class
Constructor
new Dividends(chain: Chain, privateKeyOrAddress: string)Parameters:
chain: Chain- The blockchain network configurationprivateKeyOrAddress: string- Either a private key (for signing) or wallet address (for unsigned transactions), if you want to consume this on frontend side, please provide address of connected wallet, otherwise if you want to use it on backend / server please provide private key
Example:
// With private key (auto-signing)
const dividends = new Dividends(
{ id: 1, rpcUrl: "https://mainnet.infura.io/v3/..." },
"0x1234..." as `0x${string}`
);
// With address only (unsigned transactions)
const dividends = new Dividends(
{ id: 1, rpcUrl: "https://mainnet.infura.io/v3/..." },
"0xabcd..." as `0x${string}`
);Methods
distributeDividendManual
Distributes dividends by sending individual ERC20 transfers directly to holders.
async distributeDividendManual(
params: DistributeDividendIn
): Promise<DistributeDividendOut>Parameters:
params: DistributeDividendIn- Distribution configuration
Returns:
Promise<DistributeDividendOut>- Array of transfer results with transaction details
Example:
const result: DistributeDividendOut = await dividends.distributeDividendManual({
tokenAddress: "0x..." as `0x${string}`,
stableCoinAddress: "0x..." as `0x${string}`,
amount: 1000,
indexer: async (tokenAddr, chain) => {
// Return array of { address, amount }
return [
{ address: "0x...", amount: "100" },
{ address: "0x...", amount: "200" },
];
},
businessLogic: async ({ holders, totalSupply, dividendAmount }) => {
// Calculate dividend portions
return holders.map((holder) => ({
address: holder.address,
amount: calculateDividend(holder.amount, totalSupply, dividendAmount),
}));
},
});
// Handle results
result.forEach((transfer) => {
if (transfer.type === "signed") {
console.log(`Transfer to ${transfer.address}: ${transfer.txHash}`);
} else {
// Sign the unsigned transaction
const signedTx = await walletClient.writeContract({
...transfer.txUnsigned,
account: myAccount,
});
}
});distributeDividend
Distributes dividends via a smart contract in a single batch transaction.
async distributeDividend(
params: DistributeDividendIn,
dividendContractAddress: string,
config?: DistributeDivdendOnchainConfig
): Promise<DistibuteDividendOutOnChain>Parameters:
params: DistributeDividendIn- Distribution configurationdividendContractAddress: string- Address of the deployed dividend contractconfig?: DistributeDivdendOnchainConfig- Optional configuration- title?: string, title of dividend, if you skip this it will auto generated using timestamp of executed time
- directTransfer?: boolean, if you input private key at constructor and give
trueon params, logic will auto transfer required amount ofparams.amountfrom your account (from privatekey) to dividend contract, otherwise you should make sure that dividend contract have sufficient amount of stablecoin to be distributed.
Returns:
Promise<DistibuteDividendOutOnChain>- Batch transaction result
Example:
const result: DistibuteDividendOutOnChain = await dividends.distributeDividend(
{
tokenAddress: "0x..." as `0x${string}`,
stableCoinAddress: "0x..." as `0x${string}`,
amount: 1000,
indexer,
businessLogic,
},
"0xDividendContract..." as `0x${string}`,
{
title: "Q4 2024 Dividend",
directTransfer: true, // Auto-transfer stablecoin to contract
}
);
if (result.type === "signed") {
console.log(`Batch dividend distributed: ${result.txHash}`);
console.log(`Recipients: ${result.dividendPortion.length}`);
} else {
// Sign the unsigned transaction
const signedTx = await walletClient.writeContract({
...result.txUnsigned,
account: myAccount,
});
}getDividendContractAddress
Gets the dividend contract address for a specific token from a factory contract.
async getDividendContractAddress(
factoryAddress: string,
tokenAddress: string
): Promise<string>Parameters:
factoryAddress: string- Address of the dividend factory contracttokenAddress: string- Address of the ERC20 token
Returns:
Promise<string>- Address of the dividend contract for the token
Example:
const factoryAddress = "0xFactory..." as `0x${string}`;
const tokenAddress = "0xToken..." as `0x${string}`;
const dividendContract: string = await dividends.getDividendContractAddress(
factoryAddress,
tokenAddress
);
console.log(`Dividend contract for token: ${dividendContract}`);
// Use the contract address for distribution
const result = await dividends.distributeDividend(
distributionParams,
dividendContract as `0x${string}`,
{ title: "My Dividend" }
);Types
Chain
interface Chain {
id: number; // Chain ID (e.g., 1 for Ethereum mainnet)
rpcUrl?: string; // Optional RPC URL (required for unsupported chains)
}DistributeDividendIn
interface DistributeDividendIn {
tokenAddress: string; // ERC20 token contract address
stableCoinAddress: string; // ERC20 stablecoin contract address
amount: number; // Total dividend amount to distribute
businessLogic: (params: BusinessLogicParams) => Promise<AddressAmount[]>;
indexer: (tokenAddress: string, chain: Chain) => Promise<AddressAmount[]>;
}DistributeDividendOut
type DistributeDividendOut = FinishTransferAddressAmount[];
interface FinishTransferAddressAmount {
address: string; // Recipient address
amount: string; // Amount transferred
type: "signed" | "unsigned"; // Transaction status
txHash?: string; // Transaction hash (if signed)
txUnsigned?: any; // Unsigned transaction data (if unsigned)
}DistibuteDividendOutOnChain
interface DistibuteDividendOutOnChain {
type: "signed" | "unsigned"; // Transaction status
txHash?: string; // Transaction hash (if signed)
txUnsigned?: any; // Unsigned transaction data (if unsigned)
dividendPortion: AddressAmount[]; // Array of recipients and amounts
}DistributeDivdendOnchainConfig
interface DistributeDivdendOnchainConfig {
title?: string; // Optional dividend title/description
directTransfer?: boolean; // Auto-transfer stablecoin to contract (requires private key)
}Supported Chains
The package supports the following chains out of the box:
- Ethereum Mainnet (1)
- Polygon (137)
- Base (8453)
- Arbitrum (42161)
- Optimism (10)
- Base Sepolia (84532)
- Berachain (288)
- Berachain Bepolia (289)
- Sepolia (1313161554)
For other chains, provide a custom rpcUrl in the chain configuration.
Error Handling
import { DividendsError } from "@strato-sdk/dividends";
try {
const result = await dividends.distributeDividend(/* ... */);
} catch (error) {
if (error instanceof DividendsError) {
console.error("Dividends error:", error.message);
} else {
console.error("Unexpected error:", error);
}
}Running Unit Tests
To run the unit tests for this package, follow these steps:
Clone the repository:
git clone <repository-url> cd strato-sdk/packages/dividendsInstall dependencies:
npm installInstall and run anvil:
- Anvil is a local Ethereum node used for testing. Install it if you haven't already:
npm install -g anvil - Run anvil on port 13517:
anvil --port 13517
- Anvil is a local Ethereum node used for testing. Install it if you haven't already:
Prepare foundry and smart contracts artifacts:
# Install Foundry curl -L https://foundry.paradigm.xyz | bash foundryup # Install OpenZeppelin contracts forge install OpenZeppelin/openzeppelin-contracts # Build contracts forge buildGenerate contract artifacts:
ts-node scripts/generate-artifacts.tsRun the tests:
npm testThis will execute the unit tests located in the
src/__tests__directory.
