Leaderboard
Background
The trio of leaderboards offered through Sediment enables quick scaffolding of an initial on-chain feature. A leaderboard is a great feature for games looking to begin coming into Web3; but don’t want to go all-in on “Web3”.
This is because the leaderboard is designed to be set by either an authoritative server or another smart contract.
This guarantees that leaderboards cannot be manipulated maliciously by users directly.
There are currently three types of leaderboards:
-
Leaderboard - Uses address as the key for the ranking. Great for games that have user wallets whether non-custodial or otherwise.
-
BytesLeaderboard - Uses bytes32 as the key for the ranking. Great for games that have identifiers that they want to hash before putting on-chain.
-
String Leaderboard - Uses string as the key for the ranking. Great for anonymous user names that are pre-validated by a server.
Installation
npm add @dirtroad/sediment
yarn add @dirtroad/sediment
pnpm add @dirtroad/sediment
Using Leaderboard
Setup Contract
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
import "@dirtroad/sediment/contracts/leaderboard/Leaderboard.sol";
contract LeaderboardExample is Leaderboard(10) {
constructor(address[] memory _signers) { for (uint256 i = 0; i < _signers.length; i++) { _grantRole(SERVER_ROLE, _signers[i]); } }}
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
import "@dirtroad/sediment/contracts/leaderboard/BytesLeaderboard.sol";
contract BytesLeaderboardExample is BytesLeaderboard(10) {
constructor(address[] memory _signers) { for (uint256 i = 0; i < _signers.length; i++) { _grantRole(SERVER_ROLE, _signers[i]); } }}
// SPDX-License-Identifier: MITpragma solidity ^0.8.24;
import "@dirtroad/sediment/contracts/leaderboard/StringLeaderboard.sol";
contract StringLeaderboardExample is StringLeaderboard(10) {
constructor(address[] memory _signers) { for (uint256 i = 0; i < _signers.length; i++) { _grantRole(SERVER_ROLE, _signers[i]); } }}
Contract Interaction
/*
Commented out for build purposes
import dotenv from "dotenv";import express from "express";import { Wallet } from "ethers";import ABI from "./abi.json";*/
dotenv.config();
const signerKey = process.env.PRIVATE_KEY;
async function main() {
const app = express();
app.use(express.json()); app.use(express.urlencoded());
const provider = new JsonRpcProvider("https://testnet.skalenodes.com/v1/giant-half-dual-testent"); const signer = new Wallet(signerKey, provider); let nonce = await provider.getTransactionCount(signer.address); const leaderboard = new Contract("0x...", LeaderboardABI, signer);
app.post("/finish-game", async (req, res) => {
const playerId = res.locals.playerId; // Attained from JWT
const { gameId } = req.body;
const player = await Player.findOne(playerId); // simulate database call if (player === undefined) return res.status(404).send("Player not found");
const game = await Games.findOne(gameId); // simulate database call if (game === undefined) return res.status(404).send("Game not found");
const playerFinalScore = game.scores[player.id];
await signer.sendTransaction({ nonce: nonce++, // auto increment to avoid pending issues data: leaderboard.interface.encodeFunctionData( "submitScore", [ player.address, // string a.k.a address in 0x format playerFinalScore // number ] ) });
return res.status(200).send("Game Completed");
});
}
main() .catch((err) => { console.error(err); process.exitCode = 1; });
/*
Commented out for build purposes
import dotenv from "dotenv";import express from "express";import { keccak256, toUtf8Bytes, Wallet } from "ethers";import ABI from "./abi.json";
*/
dotenv.config();
const signerKey = process.env.PRIVATE_KEY;
async function main() {
const app = express();
app.use(express.json()); app.use(express.urlencoded());
const provider = new JsonRpcProvider("https://testnet.skalenodes.com/v1/giant-half-dual-testent"); const signer = new Wallet(signerKey, provider); let nonce = await provider.getTransactionCount(signer.address); const leaderboard = new Contract("0x...", LeaderboardABI, signer);
app.post("/finish-game", async (req, res) => {
const playerId = res.locals.playerId; // Attained from JWT
const { gameId } = req.body;
const player = await Player.findOne(playerId); // simulate database call if (player === undefined) return res.status(404).send("Player not found");
const game = await Games.findOne(gameId); // simulate database call if (game === undefined) return res.status(404).send("Game not found");
const playerFinalScore = game.scores[player.id];
await signer.sendTransaction({ nonce: nonce++, // auto increment to avoid pending issues data: leaderboard.interface.encodeFunctionData( "submitScore", [ // Note, use keccak256 so the value cannot be reversed keccak256(toUtf8Bytes(player.id)), // Uint8Array a.k.a bytes32 playerFinalScore // number ] ) });
return res.status(200).send("Game Completed");
});
}
main() .catch((err) => { console.error(err); process.exitCode = 1; });
/*
Commented out for build purposes
import dotenv from "dotenv";import express from "express";import { Wallet } from "ethers";import ABI from "./abi.json";
*/
dotenv.config();
const signerKey = process.env.PRIVATE_KEY;
async function main() {
const app = express();
app.use(express.json()); app.use(express.urlencoded());
const provider = new JsonRpcProvider("https://testnet.skalenodes.com/v1/giant-half-dual-testent"); const signer = new Wallet(signerKey, provider); let nonce = await provider.getTransactionCount(signer.address); const leaderboard = new Contract("0x...", LeaderboardABI, signer);
app.post("/finish-game", async (req, res) => {
const playerId = res.locals.playerId; // Attained from JWT
const { gameId } = req.body;
const player = await Player.findOne(playerId); // simulate database call if (player === undefined) return res.status(404).send("Player not found");
const game = await Games.findOne(gameId); // simulate database call if (game === undefined) return res.status(404).send("Game not found");
const playerFinalScore = game.scores[player.id];
await signer.sendTransaction({ nonce: nonce++, // auto increment to avoid pending issues data: leaderboard.interface.encodeFunctionData( "submitScore", [ player.id.toString(), // string playerFinalScore // number ] ) });
return res.status(200).send("Game Completed");
});
}
main() .catch((err) => { console.error(err); process.exitCode = 1; });
API
Solidity API
Leaderboard
A contract to manage a leaderboard of scores associated with Ethereum addresses
This contract inherits from the Authority contract and uses role-based access control
User
Struct representing a user entry in the leaderboard
struct User { address user; uint64 highScore; uint64 timestamp; uint32 index;}
resetIndex
uint32 resetIndex
leaderboard
struct Leaderboard.User[] leaderboard
maxLength
uint32 maxLength
paused
bool paused
IncrementalReset
event IncrementalReset(uint32 amount)
Reset
event Reset()
SubmitScore
event SubmitScore(address user, uint64 highScore)
SubmitScoreAndAdd
event SubmitScoreAndAdd(address user, uint64 highScore)
constructor
constructor(uint32 _maxLength) public
Constructor to initialize the contract
Parameters
Name | Type | Description |
---|---|---|
_maxLength | uint32 | The maximum length of the leaderboard |
submitScore
function submitScore(address user, uint64 highScore) public virtual
Submit a new high score for a user
Only callable by the SERVER_ROLE
Parameters
Name | Type | Description |
---|---|---|
user | address | The Ethereum address of the user |
highScore | uint64 | The new high score to be submitted |
length
function length() public view returns (uint32)
Get the current length of the leaderboard
Return Values
Name | Type | Description |
---|---|---|
[0] | uint32 | The length of the leaderboard |
_addToLeaderboard
function _addToLeaderboard(address user, uint64 highScore, uint32 index) internal virtual
Internal function to add a new user to the leaderboard
Parameters
Name | Type | Description |
---|---|---|
user | address | The Ethereum address of the user |
highScore | uint64 | The new high score to be added |
index | uint32 | The index at which the new user should be inserted |
reset
function reset() external
Reset the entire leaderboard
Only callable by the MANAGER_ROLE Will revert if the leaderboard length is greater than 25,000
incrementalReset
function incrementalReset() public virtual
Perform an incremental reset of the leaderboard
Only callable by the MANAGER_ROLE Removes up to 1,500 entries from the leaderboard
_sort
function _sort(struct Leaderboard.User[] arr, int256 left, int256 right) internal virtual
Internal function to sort the leaderboard array using the quicksort algorithm
Parameters
Name | Type | Description |
---|---|---|
arr | struct Leaderboard.User[] | The leaderboard array to be sorted |
left | int256 | The left index of the subarray to be sorted |
right | int256 | The right index of the subarray to be sorted |
Solidity API
BytesLeaderboard
A contract to manage a leaderboard of scores associated with user bytes32 values
This contract inherits from the Authority contract and uses role-based access control
User
Struct representing a user entry in the leaderboard
struct User { bytes32 user; uint64 highScore; uint64 timestamp; uint32 index;}
resetIndex
uint32 resetIndex
leaderboard
struct BytesLeaderboard.User[] leaderboard
maxLength
uint32 maxLength
paused
bool paused
IncrementalReset
event IncrementalReset(uint32 amount)
Reset
event Reset()
SubmitScore
event SubmitScore(bytes32 user, uint64 highScore)
SubmitScoreAndAdd
event SubmitScoreAndAdd(bytes32 user, uint64 highScore)
constructor
constructor(uint32 _maxLength) public
Constructor to initialize the contract
Parameters
Name | Type | Description |
---|---|---|
_maxLength | uint32 | The maximum length of the leaderboard |
submitScore
function submitScore(bytes32 user, uint64 highScore) public virtual
Submit a new high score for a user
Only callable by the SERVER_ROLE
Parameters
Name | Type | Description |
---|---|---|
user | bytes32 | The bytes32 identifier of the user |
highScore | uint64 | The new high score to be submitted |
length
function length() public view returns (uint32)
Get the current length of the leaderboard
Return Values
Name | Type | Description |
---|---|---|
[0] | uint32 | The length of the leaderboard |
_addToLeaderboard
function _addToLeaderboard(bytes32 user, uint64 highScore, uint32 index) internal virtual
Internal function to add a new user to the leaderboard
Parameters
Name | Type | Description |
---|---|---|
user | bytes32 | The bytes32 identifier of the user |
highScore | uint64 | The new high score to be added |
index | uint32 | The index at which the new user should be inserted |
reset
function reset() public virtual
Reset the entire leaderboard
Only callable by the MANAGER_ROLE Will revert if the leaderboard length is greater than 25,000
incrementalReset
function incrementalReset() public virtual
Perform an incremental reset of the leaderboard
Only callable by the MANAGER_ROLE Removes up to 1,500 entries from the leaderboard
_sort
function _sort(struct BytesLeaderboard.User[] arr, int256 left, int256 right) internal virtual
Internal function to sort the leaderboard array using the quicksort algorithm
Parameters
Name | Type | Description |
---|---|---|
arr | struct BytesLeaderboard.User[] | The leaderboard array to be sorted |
left | int256 | The left index of the subarray to be sorted |
right | int256 | The right index of the subarray to be sorted |
Solidity API
StringLeaderboard
A contract to manage a leaderboard of scores associated with string identifiers
This contract inherits from the Authority contract and uses role-based access control
User
Struct representing a user entry in the leaderboard
struct User { string user; uint64 highScore; uint64 timestamp; uint32 index;}
resetIndex
uint32 resetIndex
leaderboard
struct StringLeaderboard.User[] leaderboard
maxLength
uint32 maxLength
paused
bool paused
IncrementalReset
event IncrementalReset(uint32 amount)
Reset
event Reset()
SubmitScore
event SubmitScore(string user, uint64 highScore)
SubmitScoreAndAdd
event SubmitScoreAndAdd(string user, uint64 highScore)
constructor
constructor(uint32 _maxLength) public
Constructor to initialize the contract
Parameters
Name | Type | Description |
---|---|---|
_maxLength | uint32 | The maximum length of the leaderboard |
submitScore
function submitScore(string user, uint64 highScore) public virtual
Submit a new high score for a user
Only callable by the SERVER_ROLE
Parameters
Name | Type | Description |
---|---|---|
user | string | The string identifier of the user |
highScore | uint64 | The new high score to be submitted |
length
function length() public view returns (uint32)
Get the current length of the leaderboard
Return Values
Name | Type | Description |
---|---|---|
[0] | uint32 | The length of the leaderboard |
_addToLeaderboard
function _addToLeaderboard(string user, uint64 highScore, uint32 index) internal virtual
Internal function to add a new user to the leaderboard
Parameters
Name | Type | Description |
---|---|---|
user | string | The string identifier of the user |
highScore | uint64 | The new high score to be added |
index | uint32 | The index at which the new user should be inserted |
reset
function reset() public virtual
Reset the entire leaderboard
Only callable by the MANAGER_ROLE Will revert if the leaderboard length is greater than 25,000
incrementalReset
function incrementalReset() public virtual
Perform an incremental reset of the leaderboard
Only callable by the MANAGER_ROLE Removes up to 1,500 entries from the leaderboard
_sort
function _sort(struct StringLeaderboard.User[] arr, int256 left, int256 right) internal virtual
Internal function to sort the leaderboard array using the quicksort algorithm
Parameters
Name | Type | Description |
---|---|---|
arr | struct StringLeaderboard.User[] | The leaderboard array to be sorted |
left | int256 | The left index of the subarray to be sorted |
right | int256 | The right index of the subarray to be sorted |