@b3dotfun/leaderboards
Version:
SDK to interact with leaderboards smart contract
487 lines (393 loc) • 18.2 kB
Markdown
# Leaderboard Smart Contract - TypeScript SDK
NPC Labs presents the TypeScript SDK for our On-Chain Leaderboard Smart Contract, a key contributor to B3 - the gaming ecosystem. This SDK enables game developers to seamlessly incorporate secure on-chain leaderboards for tracking and displaying user scores on the blockchain.
## Features
- Create and manage multiple leaderboards with unique slugs
- Public and private leaderboards
- User eligibility management for private leaderboards
- User blacklisting
- Flexible score management (increment, decrement, update)
- Admin management system
- Real-time ranking and score tracking
- Time-based leaderboard lifecycle management
- Configurable score update behavior with `useLatestScore` flag
- Attempt limit management with `hasAttemptLimit` and `initialAttempts`
- Reverse sorting option with `isReverseSort`
## Installation
You can install our TypeScript SDK using the commands below:
```bash
yarn add @b3dotfun/leaderboards
```
```bash
npm install @b3dotfun/leaderboards
```
## Getting Started
### LeaderboardSDK Props
- **`chainId`**: (`SupportedChainId`)
- This parameter specifies the chain ID for the network on which the SDK is operating (e.g., `b3.id`, `b3Sepolia.id`, `base.id`).
- The SDK uses this ID to load the appropriate contract addresses and RPC URLs automatically.
- Import the specific chain ID from `viem/chains` (e.g., `import { b3Sepolia } from "viem/chains"; const chainId = b3Sepolia.id;`).
- **`walletKey`**: `string` (optional)
- This is an optional parameter that represents the wallet key used for transactions. If provided, it should be a hexadecimal string starting with `0x`.
- The `walletKey` is required for write operations that modify the state of the blockchain (e.g., deploying contracts, adding/removing admins, setting scores). For read operations (e.g., retrieving leaderboard data, user scores), the `walletKey` is **not required**.
- **`contractAddress`**: `string` (optional)
- This optional parameter specifies the address of the leaderboard contract. It should be a hexadecimal string starting with `0x`.
- The `contractAddress` is used to interact with the specific leaderboard contract. **This parameter is only used when initializing the `Leaderboard` class, not `LeaderboardFactory`.**
## Score Update Behavior
The Leaderboard smart contract includes a `useLatestScore` flag that determines how scores are updated:
- When `useLatestScore` is `true` (Latest Score Mode):
- Direct score updates always use the latest value, even if lower than the current score
- Increments are always applied (must be positive values)
- Decrements are always applied (must be positive values)
- This mode is ideal for leaderboards where you want to show the most recent performance
- When `useLatestScore` is `false` (High Score Mode):
- Direct score updates only occur if the new score is higher than the current score
- Increments are always applied (must be positive values)
- Decrements are never applied (the score remains unchanged)
- This mode is ideal for traditional high score leaderboards where only the best performance counts
## Attempt Limit Management
The Leaderboard smart contract includes features for managing user attempts:
- When `hasAttemptLimit` is `true`:
- Users are limited to a specific number of attempts
- The number of attempts is set by `initialAttempts`
- Attempts can be increased or decreased by admins
- Users cannot submit scores when they have no attempts remaining
- When `hasAttemptLimit` is `false`:
- Users have unlimited attempts
- The `initialAttempts` value is ignored
## Reverse Sorting
The Leaderboard smart contract includes a `isReverseSort` flag that determines how scores are sorted:
- When `isReverseSort` is `true`:
- Lower scores are ranked higher
- Ideal for leaderboards where lower scores are better (e.g., time trials, golf scores)
- When `isReverseSort` is `false`:
- Higher scores are ranked higher
- Default behavior for traditional high score leaderboards
When creating a new leaderboard, you can specify these flags:
```typescript
// Create a leaderboard with custom settings
const leaderboardAddress = await factory.createLeaderboard(
ADMIN_ADDRESS,
slug,
BigInt(startTime),
BigInt(endTime),
title,
false, // isPrivate
true, // useLatestScore - will always use the latest score
true, // hasAttemptLimit - will limit user attempts
3, // initialAttempts - users start with 3 attempts
false // isReverseSort - higher scores are better
);
```
## Examples
### 1. Creating and Managing Leaderboards
Here's a complete example of creating and managing leaderboards using the Factory contract:
```typescript
import * as dotenv from "dotenv";
import type { Address } from "viem";
import { LeaderboardFactory } from "@b3dotfun/leaderboards";
import { b3Sepolia } from "viem/chains"; // Example: Using B3 Sepolia Testnet
dotenv.config();
const CHAIN_ID = b3Sepolia.id; // Example: Using B3 Sepolia Testnet ID
const ADMIN_ADDRESS = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e";
// Initialize and connect to the Factory contract
function initializeFactory(): LeaderboardFactory | null {
const factory = new LeaderboardFactory(
CHAIN_ID,
process.env.LEADERBOARD_ADMIN_PRIVATE_KEY
);
const isConnected = factory.connect();
if (!isConnected) {
console.error("Failed to connect to the Factory contract.");
return null;
}
console.log("Connected to the Factory contract");
return factory;
}
// Create a new leaderboard with specified parameters
async function createNewLeaderboard(
factory: LeaderboardFactory,
slug: string,
startTime: number,
endTime: number,
title: string,
isPrivate: boolean = false,
useLatestScore: boolean = false,
hasAttemptLimit: boolean = false,
initialAttempts: number = 0,
isReverseSort: boolean = false
): Promise<Address> {
console.log(`Creating leaderboard "${slug}" with following parameters:`);
console.log(`- Title: ${title}`);
console.log(`- Start time: ${new Date(startTime * 1000).toISOString()}`);
console.log(`- End time: ${new Date(endTime * 1000).toISOString()}`);
console.log(`- Private: ${isPrivate}`);
console.log(`- Use Latest Score: ${useLatestScore ? "Yes (Latest Score Mode)" : "No (High Score Mode)"}`);
console.log(`- Has Attempt Limit: ${hasAttemptLimit ? "Yes" : "No"}`);
console.log(`- Initial Attempts: ${initialAttempts}`);
console.log(`- Reverse Sort: ${isReverseSort ? "Yes (Lower scores are better)" : "No (Higher scores are better)"}`);
const leaderboardAddress = await factory.createLeaderboard(
ADMIN_ADDRESS,
slug,
BigInt(startTime),
BigInt(endTime),
title,
isPrivate,
useLatestScore,
hasAttemptLimit,
initialAttempts,
isReverseSort
);
console.log("Created new leaderboard at address:", leaderboardAddress);
return leaderboardAddress;
}
// Query leaderboard information
async function queryLeaderboards(factory: LeaderboardFactory, slug: string) {
const leaderboardsWithProps = await factory.getLeaderboardsWithProps(slug);
if (leaderboardsWithProps.length > 0) {
console.log(`\nAll leaderboards for "${slug}":`);
leaderboardsWithProps.forEach((leaderboard, index) => {
console.log(`\nLeaderboard #${index + 1}:`);
console.log("- Address:", leaderboard.address);
console.log("- Title:", leaderboard.properties.title);
console.log("- Start time:", new Date(leaderboard.properties.startTime * 1000).toISOString());
console.log("- End time:", new Date(leaderboard.properties.endTime * 1000).toISOString());
console.log("- Concluded:", leaderboard.properties.hasConcluded ? "Yes" : "No");
console.log("- Private:", leaderboard.properties.isPrivate ? "Yes" : "No");
});
}
const slugCount = await factory.getLeaderboardCountBySlug(slug);
console.log(`\nTotal number of leaderboards for "${slug}":`, slugCount);
}
// Example usage
async function main() {
try {
const factory = initializeFactory();
if (!factory) return;
// Create a new 24-hour leaderboard with custom settings
const startTime = Math.floor(Date.now() / 1000);
const endTime = startTime + 24 * 3600;
const leaderboardAddress = await createNewLeaderboard(
factory,
"game-tournament-1",
startTime,
endTime,
"Game Tournament #1",
false, // isPrivate
false, // useLatestScore
true, // hasAttemptLimit
3, // initialAttempts
false // isReverseSort
);
await queryLeaderboards(factory, "game-tournament-1");
} catch (error) {
console.error("Error:", error);
}
}
```
### 2. Interacting with Individual Leaderboards
Here's an example of interacting with a specific leaderboard:
```typescript
import * as dotenv from "dotenv";
import type { Address } from "viem";
import { Leaderboard } from "@b3dotfun/leaderboards";
import { b3Sepolia } from "viem/chains"; // Example: Using B3 Sepolia Testnet
dotenv.config();
const CHAIN_ID = b3Sepolia.id; // Example: Using B3 Sepolia Testnet ID
const LEADERBOARD_ADDRESS = "0x5BA4634aBB1D42897E2ba65f0cf9036B091Ea3B7"; // Replace with your actual leaderboard address
const USER_ADDRESS = "0x1234567890123456789012345678901234567890"; // Replace with a user address
function initializeLeaderboard(): Leaderboard | null {
const leaderboard = new Leaderboard(
CHAIN_ID,
process.env.LEADERBOARD_ADMIN_PRIVATE_KEY, // Optional: only needed for write operations
LEADERBOARD_ADDRESS
);
const isConnected = leaderboard.connect();
if (!isConnected) {
console.error("Failed to connect to the Leaderboard contract.");
return null;
}
console.log("Connected to the Leaderboard contract");
return leaderboard;
}
// Manage user scores
async function manageUserScores(leaderboard: Leaderboard, user: Address) {
const timestamp = Math.floor(Date.now() / 1000);
// Check if user is eligible (for private leaderboards)
const isPrivate = await leaderboard.isPrivate();
if (isPrivate) {
const isEligible = await leaderboard.isUserEligible(user);
if (!isEligible) {
console.log(`User ${user} is not eligible for this private leaderboard`);
return;
}
}
// Check if user is blacklisted
const isBlacklisted = await leaderboard.isUserBlacklisted(user);
if (isBlacklisted) {
console.log(`User ${user} is blacklisted`);
return;
}
// Check if user has attempts remaining (if attempt limit is enabled)
const props = await leaderboard.getLeaderboardProperties();
if (props?.hasAttemptLimit) {
const attempts = await leaderboard.getUserAttempts(user);
if (attempts <= 0) {
console.log(`User ${user} has no attempts remaining`);
return;
}
}
// Update scores
await leaderboard.updateScores([{ user, score: 1000, timestamp }]);
await leaderboard.incrementScores([{ user, score: 500, timestamp }]);
const userScore = await leaderboard.getUserScore(user);
if (userScore) {
console.log(`Current score for ${user}: ${userScore.score}`);
console.log(`Last updated at: ${new Date(userScore.lastUpdated * 1000).toISOString()}`);
} else {
console.log(`No score found for ${user}`);
}
}
// Manage private leaderboard eligibility
async function manageEligibleUsers(leaderboard: Leaderboard, users: Address[]) {
const isPrivate = await leaderboard.isPrivate();
if (!isPrivate) {
console.log("This is not a private leaderboard");
return;
}
await leaderboard.addEligibleUsers(users);
const eligibleUsers = await leaderboard.getEligibleUsers();
console.log("Current eligible users:", eligibleUsers);
}
async function displayLeaderboardInfo(leaderboard: Leaderboard) {
const props = await leaderboard.getLeaderboardProperties();
const isActive = await leaderboard.isLeaderboardActive();
const userCount = await leaderboard.getLeaderboardUserCount();
if (props) {
console.log("\nLeaderboard Information:");
console.log("- Title:", props.title);
console.log("- Slug:", props.slug);
console.log("- Start time:", new Date(props.startTime * 1000).toISOString());
console.log("- End time:", new Date(props.endTime * 1000).toISOString());
console.log("- Status:", isActive ? "Active" : "Inactive");
console.log("- Concluded:", props.hasConcluded ? "Yes" : "No");
console.log("- Private:", props.isPrivate ? "Yes" : "No");
console.log("- Use Latest Score:", props.useLatestScore ? "Yes (Latest Score Mode)" : "No (High Score Mode)");
console.log("- Total Users:", userCount);
}
}
async function main() {
try {
const leaderboard = initializeLeaderboard();
if (!leaderboard) return;
// Display leaderboard information
await displayLeaderboardInfo(leaderboard);
// Manage user scores
await manageUserScores(leaderboard, USER_ADDRESS);
// Manage eligible users for private leaderboards
await manageEligibleUsers(leaderboard, [USER_ADDRESS]);
} catch (error) {
console.error("Error:", error);
}
}
```
## Environment Setup
Create a `.env` file in your project root with the following variables:
```env
LEADERBOARD_ADMIN_PRIVATE_KEY="your_private_key_here"
# The CHAIN_ID is now passed directly to the constructor, not set via ENV
```
## Important Notes
1. Always ensure your private keys are kept secure and never committed to version control
2. Use appropriate `chainId` values for the desired network (e.g., `b3.id` for B3 Mainnet, `b3Sepolia.id` for B3 Testnet, `base.id` for Base Mainnet).
3. For private leaderboards:
- Users must be added to the eligible users list before they can participate
- Only admins can manage user eligibility
4. Blacklisted users cannot participate in any leaderboard activities
5. When using attempt limits:
- Users cannot submit scores when they have no attempts remaining
- Admins can increase or decrease user attempts
6. The examples above demonstrate basic usage. For production deployments, implement proper error handling and security measures
## Features in Detail
### Private Leaderboards
- Create private leaderboards with restricted access
- Manage eligible users through `addEligibleUsers` and `removeEligibleUsers`
- Check user eligibility with `isUserEligible`
### User Management
- Blacklist users with `blacklistUsers`
- Unblacklist users with `unblacklistUser`
- Add/remove admins with `addAdmin` and `removeAdmin`
- Manage user attempts with `getUserAttempts`, `increaseUserAttempts`, and `decreaseUserAttempts`
### Score Management
- Update scores with `updateScores`
- Increment scores with `incrementScores`
- Decrement scores with `decrementScores`
- Get user scores and rankings
- View top scores and full leaderboard
### Score Update Modes
- Configure leaderboards with different score update behaviors using the `useLatestScore` flag
- **Latest Score Mode** (`useLatestScore = true`): Always shows the most recent score
- Direct updates always use the latest value
- Increments and decrements are always applied
- Ideal for leaderboards tracking current performance
- **High Score Mode** (`useLatestScore = false`): Only keeps the highest score achieved
- Direct updates only occur if the new score is higher
- Increments are applied, but decrements are ignored
- Ideal for traditional high score leaderboards
### Attempt Limit Management
- Enable attempt limits with `hasAttemptLimit`
- Set initial attempts with `initialAttempts`
- Check user attempts with `getUserAttempts`
- Modify attempts with `increaseUserAttempts` and `decreaseUserAttempts`
### Reverse Sorting
- Enable reverse sorting with `isReverseSort`
- When enabled, lower scores are ranked higher
- Ideal for leaderboards where lower scores are better (e.g., time trials, golf scores)
### Leaderboard Lifecycle
- Set start and end times
- Check leaderboard status (active/ended)
- Conclude leaderboards
- Update leaderboard parameters
### Leaderboard Queries
- Get leaderboards by slug with `getLeaderboardsWithProps(slug)`
- Get latest leaderboard for a slug with `getLatestLeaderboardWithProps(slug)`
- Get all leaderboards across all slugs with `getAllLeaderboardsWithProps()`
- Get leaderboard counts with `getLeaderboardCountBySlug(slug)` and `getTotalLeaderboardCount()`
For more detailed information about the smart contract functionality, please refer to our smart contract documentation.
## Using JSON ABIs Directly
The SDK also exports the raw JSON ABIs for direct use with libraries like viem, ethers.js, or web3.js. This is useful if you want to interact with the contracts directly without using the SDK wrapper classes.
```typescript
import { LeaderboardFactoryJson, LeaderboardJson } from '@b3dotfun/leaderboards';
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
import { B3LeaderboardFactoryContractAddress, B3MainnetRpcUrl } from '@b3dotfun/leaderboards';
// Create a public client
const client = createPublicClient({
chain: mainnet,
transport: http(B3MainnetRpcUrl),
});
// Use the LeaderboardFactory ABI to interact with the contract
const totalLeaderboards = await client.readContract({
address: B3LeaderboardFactoryContractAddress,
abi: LeaderboardFactoryJson,
functionName: 'getTotalLeaderboardCount',
});
console.log(`Total leaderboards: ${totalLeaderboards}`);
// Get all leaderboards
const leaderboards = await client.readContract({
address: B3LeaderboardFactoryContractAddress,
abi: LeaderboardFactoryJson,
functionName: 'getAllLeaderboards',
});
// If there are any leaderboards, get details of the first one
if (Array.isArray(leaderboards) && leaderboards.length > 0) {
const firstLeaderboardAddress = leaderboards[0];
// Use the Leaderboard ABI to interact with the leaderboard contract
const leaderboardProps = await client.readContract({
address: firstLeaderboardAddress,
abi: LeaderboardJson,
functionName: 'getLeaderboardProps',
});
console.log('Leaderboard properties:', leaderboardProps);
}
```
This approach gives you more flexibility when integrating with existing applications or when you need more control over the contract interactions.