This guide covers using price oracles and generating random numbers on XPR Network.
XPR Network provides on-chain price feeds through the oracles contract for DeFi applications, trading, and price-dependent logic.
| Index | Pair |
|---|---|
| 3 | XPR/USD |
| 4 | BTC/USD |
| 5 | USDC/USD |
| 6 | MTL/USD |
| 7 | ETH/USD |
| 8 | DOGE/USD |
| 9 | USDT/USD |
| 12 | XMD/USD |
| 13 | BUSD/USD |
| 16 | LTC/USD |
| 18 | XRP/USD |
| 19 | SOL/USD |
| 21 | HBAR/USD |
| 22 | ADA/USD |
| 23 | XLM/USD |
proton table oracles data -l 4 -u 4interface OracleData {
feed_index: number;
aggregate: {
d_double: string;
};
timestamp: string;
}
async function getOraclePrice(feedIndex: number): Promise<number> {
const { rows } = await rpc.get_table_rows({
code: 'oracles',
scope: 'oracles',
table: 'data',
lower_bound: feedIndex,
upper_bound: feedIndex,
limit: 1
});
if (rows.length === 0) {
throw new Error(`Oracle feed ${feedIndex} not found`);
}
return parseFloat(rows[0].aggregate.d_double);
}
// Examples
const btcPrice = await getOraclePrice(4); // BTC/USD
const ethPrice = await getOraclePrice(7); // ETH/USD
const xprPrice = await getOraclePrice(3); // XPR/USDimport { Contract, Name, TableStore, check } from 'proton-tsc';
// Oracle data structure
@table("data", noabigen)
class OracleData extends Table {
constructor(
public feed_index: u64 = 0,
public aggregate: OracleAggregate = new OracleAggregate()
) { super(); }
@primary
get primary(): u64 { return this.feed_index; }
}
class OracleAggregate {
d_double: f64 = 0;
}
@contract
class MyContract extends Contract {
@action("checkprice")
checkPrice(feedIndex: u64, minPrice: u64): void {
// Query oracle table
const oracleTable = new TableStore<OracleData>(
Name.fromString("oracles"),
Name.fromString("oracles").N
);
const data = oracleTable.requireGet(feedIndex, "Oracle feed not found");
const price = <u64>(data.aggregate.d_double * 10000); // Convert to u64
check(price >= minPrice, "Price below minimum");
}
}- Price feeds are updated by authorized oracle providers
- Typical update frequency: every few seconds to minutes
- Check
timestampfield to verify data freshness
// Oracle prices have varying precision
// BTC might be 95322.71, XPR might be 0.00087
// Convert to consistent precision (e.g., 8 decimals)
function normalizePrice(price: number, decimals: number = 8): bigint {
return BigInt(Math.round(price * Math.pow(10, decimals)));
}
// Calculate value
function calculateValue(amount: number, price: number): number {
return amount * price;
}
// Example: Value of 1000 XPR
const xprPrice = await getOraclePrice(3); // e.g., 0.00087
const xprValue = calculateValue(1000, xprPrice); // $0.87XPR Network provides verifiable random numbers through the rng contract. This is essential for:
- Games and gambling
- Lotteries and raffles
- Random NFT attributes
- Fair selection mechanisms
The RNG system uses an oracle-based commit-reveal pattern:
- Your contract calls
rng::requestrandwith a unique signing value - Off-chain oracle generates random value using RSA signatures
- Oracle calls
rng::setrandwith the cryptographic proof - RNG contract verifies the RSA-SHA256 signature
- RNG contract calls your contract's
receiverandaction with the result
This ensures randomness cannot be predicted or manipulated.
| Contract | Account |
|---|---|
| RNG Oracle | rng |
Repository: https://github.com/XPRNetwork/proton-rng
Your contract calls requestrand on the rng contract:
import {
Contract, Name, InlineAction, PermissionLevel,
ActionData, check, requireAuth
} from 'proton-tsc';
@packer
class RequestRandParams extends ActionData {
constructor(
public assoc_id: u64 = 0,
public signing_value: u64 = 0,
public caller: Name = new Name()
) { super(); }
}
@contract
class MyGame extends Contract {
@action("startgame")
startGame(player: Name, gameId: u64): void {
requireAuth(player);
// Generate unique signing value (must be unique per request)
const signingValue = this.generateSigningValue(gameId, player);
// Request random number from oracle
const requestAction = new InlineAction<RequestRandParams>("requestrand")
.act(Name.fromString("rng"), new PermissionLevel(this.receiver))
.send(new RequestRandParams(
gameId, // assoc_id - returned in callback
signingValue, // signing_value - must be unique
this.receiver // caller - your contract
));
}
private generateSigningValue(gameId: u64, player: Name): u64 {
// Combine multiple values for uniqueness
return gameId ^ player.N ^ currentTimeSec();
}
}Implement the receiverand action in your contract:
import { Name, Checksum256, check, requireAuth } from 'proton-tsc';
@contract
class MyGame extends Contract {
// Called by RNG contract with the random result
@action("receiverand")
receiveRand(assoc_id: u64, random_value: Checksum256): void {
// Only RNG contract can call this
requireAuth(Name.fromString("rng"));
// assoc_id is the gameId we passed in requestrand
const gameId = assoc_id;
// random_value is a SHA256 hash - use it for randomness
const randomBytes = random_value.data;
// Convert to usable random number
const randomNumber = this.bytesToU64(randomBytes);
// Use the random number in your game logic
this.resolveGame(gameId, randomNumber);
}
private bytesToU64(bytes: u8[]): u64 {
let result: u64 = 0;
for (let i = 0; i < 8 && i < bytes.length; i++) {
result = (result << 8) | <u64>bytes[i];
}
return result;
}
private resolveGame(gameId: u64, randomNumber: u64): void {
// Your game resolution logic
// e.g., pick winner, determine outcome, etc.
}
}Your contract needs eosio.code permission to receive callbacks:
proton contract:enableinline mycontractimport {
Contract, Table, TableStore, Name, Asset,
InlineAction, PermissionLevel, ActionData,
check, requireAuth, currentTimeSec, Checksum256
} from 'proton-tsc';
// Game state
@table("games")
class Game extends Table {
constructor(
public id: u64 = 0,
public player: Name = new Name(),
public bet: u64 = 0,
public choice: u8 = 0, // 0 = heads, 1 = tails
public status: u8 = 0, // 0 = pending, 1 = resolved
public won: boolean = false
) { super(); }
@primary
get primary(): u64 { return this.id; }
}
@packer
class RequestRandParams extends ActionData {
constructor(
public assoc_id: u64 = 0,
public signing_value: u64 = 0,
public caller: Name = new Name()
) { super(); }
}
@contract
class CoinFlip extends Contract {
gamesTable: TableStore<Game> = new TableStore<Game>(this.receiver);
@action("flip")
flip(player: Name, choice: u8, bet: Asset): void {
requireAuth(player);
check(choice == 0 || choice == 1, "Choice must be 0 (heads) or 1 (tails)");
check(bet.amount > 0, "Bet must be positive");
// Create game
const gameId = this.gamesTable.availablePrimaryKey;
const game = new Game(gameId, player, bet.amount, choice, 0, false);
this.gamesTable.store(game, player);
// Transfer bet to contract
// (would need inline action to eosio.token::transfer)
// Request random number
const signingValue = gameId ^ player.N ^ currentTimeSec();
new InlineAction<RequestRandParams>("requestrand")
.act(Name.fromString("rng"), new PermissionLevel(this.receiver))
.send(new RequestRandParams(gameId, signingValue, this.receiver));
}
@action("receiverand")
receiveRand(assoc_id: u64, random_value: Checksum256): void {
requireAuth(Name.fromString("rng"));
const game = this.gamesTable.requireGet(assoc_id, "Game not found");
check(game.status == 0, "Game already resolved");
// Determine outcome (0 or 1 based on random)
const randomByte = random_value.data[0];
const outcome: u8 = randomByte % 2 == 0 ? 0 : 1;
// Check if player won
const won = outcome == game.choice;
// Update game
game.status = 1;
game.won = won;
this.gamesTable.update(game, this.receiver);
// Pay winner (2x bet minus fee)
if (won) {
const payout = game.bet * 2 * 95 / 100; // 5% house edge
// Send payout via inline action
}
}
}This example demonstrates a more complex RNG use case with weighted symbol selection (based on the xpr-slots contract):
import {
Contract, Table, TableStore, Name, Asset,
InlineAction, PermissionLevel, ActionData,
check, requireAuth, currentTimeSec, Checksum256,
transfer
} from 'proton-tsc';
// Pending game tracking
@table("games")
class Game extends Table {
constructor(
public id: u64 = 0,
public player: Name = new Name(),
public bet: u64 = 0,
public timestamp: u64 = 0
) { super(); }
@primary
get primary(): u64 { return this.id; }
}
// Spin results (historical record)
@table("spinresults")
class SpinResult extends Table {
constructor(
public id: u64 = 0,
public player: Name = new Name(),
public reel1: u8 = 0,
public reel2: u8 = 0,
public reel3: u8 = 0,
public betAmount: u64 = 0,
public payout: u64 = 0,
public jackpotWon: boolean = false,
public timestamp: u64 = 0
) { super(); }
@primary
get primary(): u64 { return this.id; }
}
@contract
class SlotMachine extends Contract {
gamesTable: TableStore<Game> = new TableStore<Game>(this.receiver);
resultsTable: TableStore<SpinResult> = new TableStore<SpinResult>(this.receiver);
// Symbol weights (higher = more common)
// Total: 125 (for easy percentage calculation)
static readonly WEIGHTS: u8[] = [40, 30, 25, 20, 10]; // Lemon, Cherry, Bell, Bar, Seven
static readonly TOTAL_WEIGHT: u8 = 125;
// Handle incoming transfer (player sends XPR to play)
@action("transfer", notify)
onTransfer(from: Name, to: Name, quantity: Asset, memo: string): void {
if (to != this.receiver || from == this.receiver) return;
if (memo != "spin") return;
check(quantity.amount >= 10000, "Minimum bet: 1 XPR");
check(quantity.amount <= 10000000, "Maximum bet: 1000 XPR");
// Check no pending game for this player
// (prevents double-spin exploit)
let hasPending = false;
let cursor = this.gamesTable.first();
while (cursor) {
if (cursor.player == from) {
hasPending = true;
break;
}
cursor = this.gamesTable.next(cursor);
}
check(!hasPending, "Pending spin exists - please wait");
// Create pending game
const gameId = this.gamesTable.availablePrimaryKey;
const game = new Game(gameId, from, quantity.amount, currentTimeSec());
this.gamesTable.store(game, this.receiver);
// Request random from oracle
const signingValue = gameId ^ from.N ^ currentTimeSec();
new InlineAction<RequestRandParams>("requestrand")
.act(Name.fromString("rng"), new PermissionLevel(this.receiver))
.send(new RequestRandParams(gameId, signingValue, this.receiver));
}
@action("receiverand")
receiveRand(assoc_id: u64, random_value: Checksum256): void {
requireAuth(Name.fromString("rng"));
const game = this.gamesTable.requireGet(assoc_id, "Game not found");
// Extract 3 independent random values from the 32-byte hash
// Using different portions ensures independence
const rand1 = this.bytesToU64(random_value.data, 0); // bytes 0-7
const rand2 = this.bytesToU64(random_value.data, 8); // bytes 8-15
const rand3 = this.bytesToU64(random_value.data, 16); // bytes 16-23
// Convert to weighted symbols
const reel1 = this.getWeightedSymbol(rand1);
const reel2 = this.getWeightedSymbol(rand2);
const reel3 = this.getWeightedSymbol(rand3);
// Calculate payout
const payout = this.calculatePayout(reel1, reel2, reel3, game.bet);
const isJackpot = reel1 == 4 && reel2 == 4 && reel3 == 4; // Three 7s
// Store result
const result = new SpinResult(
this.resultsTable.availablePrimaryKey,
game.player, reel1, reel2, reel3,
game.bet, payout, isJackpot, currentTimeSec()
);
this.resultsTable.store(result, this.receiver);
// Remove pending game
this.gamesTable.remove(game);
// Pay winner
if (payout > 0) {
transfer(this.receiver, game.player,
new Asset(payout, new Symbol("XPR", 4)), "Slot win!");
}
}
// Convert random u64 to weighted symbol index
private getWeightedSymbol(random: u64): u8 {
const value = <u8>(random % SlotMachine.TOTAL_WEIGHT);
let cumulative: u8 = 0;
for (let i = 0; i < SlotMachine.WEIGHTS.length; i++) {
cumulative += SlotMachine.WEIGHTS[i];
if (value < cumulative) {
return <u8>i;
}
}
return 0;
}
// Payout calculation based on symbol combination
private calculatePayout(r1: u8, r2: u8, r3: u8, bet: u64): u64 {
// Three of a kind
if (r1 == r2 && r2 == r3) {
if (r1 == 4) return this.getJackpotPayout(); // 7-7-7 Jackpot
if (r1 == 1) return bet * 5; // Cherry 5x
if (r1 == 3) return bet * 3; // Bar 3x
if (r1 == 2) return bet * 2; // Bell 2x
if (r1 == 0) return bet * 3 / 2; // Lemon 1.5x
}
// Any two matching
if (r1 == r2 || r2 == r3 || r1 == r3) {
return bet / 2; // 0.5x
}
return 0;
}
private bytesToU64(bytes: u8[], offset: i32): u64 {
let result: u64 = 0;
for (let i = 0; i < 8; i++) {
result = (result << 8) | <u64>bytes[offset + i];
}
return result;
}
private getJackpotPayout(): u64 {
// Return jackpot pool amount (implementation depends on your design)
return 100000000; // 10,000 XPR minimum
}
}Key patterns from this example:
- Pending game tracking: Prevent players from spamming spins while one is pending
- Multiple random values: Extract independent values from different portions of the 32-byte hash
- Weighted randomness: Use cumulative weights for non-uniform probability distributions
- Transfer notification: Trigger game logic on incoming token transfer with memo
- Historical records: Store spin results for transparency/verification
-
Unique Signing Values
// Good - combine multiple sources const signingValue = gameId ^ player.N ^ currentTimeSec() ^ nonce; // Bad - predictable or reused const signingValue = gameId; // Could be reused
-
Validate Callback Source
@action("receiverand") receiveRand(assoc_id: u64, random_value: Checksum256): void { // ALWAYS check caller is RNG contract requireAuth(Name.fromString("rng")); // ... }
-
Handle Pending State
// Track that game is waiting for random game.status = PENDING_RANDOM; // Prevent double-requests check(game.status != PENDING_RANDOM, "Already waiting for random");
-
Use Full Entropy
// Checksum256 has 32 bytes of entropy // Use different bytes for different purposes const result1 = random_value.data[0] % 6 + 1; // Dice roll const result2 = random_value.data[4] % 52; // Card draw
For lower-stakes applications, you can use block data for pseudo-randomness:
import { currentBlock, currentTimeSec, taposBlockPrefix } from 'proton-tsc';
// WARNING: This is predictable by block producers!
// Only use for low-value, non-critical randomness
function pseudoRandom(seed: u64): u64 {
const blockNum = currentBlock();
const blockPrefix = taposBlockPrefix();
const time = currentTimeSec();
// Mix values
return seed ^ blockNum ^ blockPrefix ^ time;
}When to use block-based:
- Random cosmetic effects
- Non-valuable outcomes
- Development/testing
When to use RNG oracle:
- Games with real value
- Lotteries and raffles
- NFT rarity determination
- Any high-stakes randomness
For critical applications, aggregate multiple sources:
async function getAggregatedPrice(feedIndex: number): Promise<number> {
// Query on-chain oracle
const onChainPrice = await getOraclePrice(feedIndex);
// Could also fetch from external APIs and compare
// Reject if prices differ significantly
return onChainPrice;
}async function getFreshPrice(feedIndex: number, maxAgeSeconds: number): Promise<number> {
const { rows } = await rpc.get_table_rows({
code: 'oracles',
scope: 'oracles',
table: 'data',
lower_bound: feedIndex,
upper_bound: feedIndex,
limit: 1
});
if (rows.length === 0) {
throw new Error('Oracle feed not found');
}
const data = rows[0];
const timestamp = new Date(data.timestamp).getTime();
const age = (Date.now() - timestamp) / 1000;
if (age > maxAgeSeconds) {
throw new Error(`Oracle data stale: ${age}s old (max ${maxAgeSeconds}s)`);
}
return parseFloat(data.aggregate.d_double);
}# Get BTC price
proton table oracles data -l 4 -u 4
# Get all oracle feeds
proton table oracles data| Action | Description |
|---|---|
requestrand |
Request random number |
setrand |
Oracle delivers random (internal) |
killjobs |
Cancel pending requests |
pause |
Pause contract (admin) |
| Contract | Table | Description |
|---|---|---|
oracles |
data |
Price feed data |
rng |
jobs.a |
Pending random requests |
rng |
signvals.a |
Used signing values |
rng |
config.a |
RNG configuration |
- Enable inline actions on your contract
- Implement
receiverandaction - Validate
rngis the caller inreceiverand - Generate unique signing values
- Handle pending game state
- Test on testnet first