
Security News
Crates.io Users Targeted by Phishing Emails
The Rust Security Response WG is warning of phishing emails from rustfoundation.dev targeting crates.io users.
@b3dotfun/leaderboards
Advanced tools
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.
useLatestScore
flaghasAttemptLimit
and initialAttempts
isReverseSort
You can install our TypeScript SDK using the commands below:
yarn add @b3dotfun/leaderboards
npm install @b3dotfun/leaderboards
chainId
: (SupportedChainId
)
b3.id
, b3Sepolia.id
, base.id
).viem/chains
(e.g., import { b3Sepolia } from "viem/chains"; const chainId = b3Sepolia.id;
).walletKey
: string
(optional)
0x
.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)
0x
.contractAddress
is used to interact with the specific leaderboard contract. This parameter is only used when initializing the Leaderboard
class, not LeaderboardFactory
.The Leaderboard smart contract includes a useLatestScore
flag that determines how scores are updated:
When useLatestScore
is true
(Latest Score Mode):
When useLatestScore
is false
(High Score Mode):
The Leaderboard smart contract includes features for managing user attempts:
When hasAttemptLimit
is true
:
initialAttempts
When hasAttemptLimit
is false
:
initialAttempts
value is ignoredThe Leaderboard smart contract includes a isReverseSort
flag that determines how scores are sorted:
When isReverseSort
is true
:
When isReverseSort
is false
:
When creating a new leaderboard, you can specify these flags:
// 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
);
Here's a complete example of creating and managing leaderboards using the Factory contract:
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);
}
}
Here's an example of interacting with a specific leaderboard:
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);
}
}
Create a .env
file in your project root with the following variables:
LEADERBOARD_ADMIN_PRIVATE_KEY="your_private_key_here"
# The CHAIN_ID is now passed directly to the constructor, not set via ENV
chainId
values for the desired network (e.g., b3.id
for B3 Mainnet, b3Sepolia.id
for B3 Testnet, base.id
for Base Mainnet).addEligibleUsers
and removeEligibleUsers
isUserEligible
blacklistUsers
unblacklistUser
addAdmin
and removeAdmin
getUserAttempts
, increaseUserAttempts
, and decreaseUserAttempts
updateScores
incrementScores
decrementScores
useLatestScore
flaguseLatestScore = true
): Always shows the most recent score
useLatestScore = false
): Only keeps the highest score achieved
hasAttemptLimit
initialAttempts
getUserAttempts
increaseUserAttempts
and decreaseUserAttempts
isReverseSort
getLeaderboardsWithProps(slug)
getLatestLeaderboardWithProps(slug)
getAllLeaderboardsWithProps()
getLeaderboardCountBySlug(slug)
and getTotalLeaderboardCount()
For more detailed information about the smart contract functionality, please refer to our smart contract documentation.
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.
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.
FAQs
SDK to interact with leaderboards smart contract
The npm package @b3dotfun/leaderboards receives a total of 711 weekly downloads. As such, @b3dotfun/leaderboards popularity was classified as not popular.
We found that @b3dotfun/leaderboards demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 9 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
The Rust Security Response WG is warning of phishing emails from rustfoundation.dev targeting crates.io users.
Product
Socket now lets you customize pull request alert headers, helping security teams share clear guidance right in PRs to speed reviews and reduce back-and-forth.
Product
Socket's Rust support is moving to Beta: all users can scan Cargo projects and generate SBOMs, including Cargo.toml-only crates, with Rust-aware supply chain checks.