oracle-sdk
Version:
Interact with ease with the eclipse contracts
581 lines (575 loc) • 17 kB
JavaScript
;
var base = require('@scure/base');
// src/utils/address.ts
function convertAddressToField(address) {
if (!address.startsWith("aleo1")) {
throw new Error("Invalid address");
}
let decoded;
try {
decoded = base.bech32m.decode(address);
} catch (e) {
if (e instanceof Error) {
throw new Error(`Failed to decode bech32m address: ${e.message}`);
}
throw new Error(`Failed to decode bech32m address`);
}
if (decoded.prefix !== "aleo") {
throw new Error(
`Invalid Aleo address prefix: Expected 'aleo', got '${decoded.prefix}'`
);
}
const bytes = base.bech32m.fromWords(decoded.words);
const reversedBytes = [...bytes].reverse();
let hexString = "";
for (const byte of reversedBytes) {
hexString += byte.toString(16).padStart(2, "0");
}
const fieldBigInt = BigInt("0x" + hexString);
return fieldBigInt;
}
// src/utils/parsing.ts
function parseUint(raw, type) {
if (!raw) return 0;
const match = raw.match(new RegExp(`([0-9]+)${type}`));
return match ? parseInt(match[1], 10) : 0;
}
function parseBool(raw) {
return raw ? /true/.test(raw) : null;
}
function parseField(raw) {
return raw ? parseInt(raw.match(/([0-9]+)/)?.[1] ?? "0", 10) : null;
}
function parseAddress(raw) {
return raw?.match(/([a-z0-9]{59,})/)?.[1] ?? null;
}
// src/api/client.ts
var DEFAULT_CONFIG = {
baseUrl: "https://api.explorer.provable.com/v1",
network: "testnet"
};
var AleoExplorerClient = class {
/**
* Creates a new instance of the AleoExplorerClient
*
* @param config Configuration options
*/
constructor(config = {}) {
this.baseUrl = config.baseUrl || DEFAULT_CONFIG.baseUrl;
this.network = config.network || DEFAULT_CONFIG.network;
}
/**
* Fetches data from the API
*
* @param endpoint API endpoint
* @returns Response data as text
*/
async fetchData(endpoint) {
try {
const response = await fetch(
`${this.baseUrl}/${this.network}/${endpoint}`
);
if (!response.ok) return null;
return await response.text();
} catch (error) {
console.error("Error fetching data:", error);
return null;
}
}
/**
* Fetches a mapping value from a contract
*
* @param program Program name
* @param mapping Mapping name
* @param key Key to look up
* @returns Mapping value as text
*/
async getMappingValue(program, mapping, key) {
return this.fetchData(`program/${program}/mapping/${mapping}/${key}`);
}
/**
* Gets a feed provider list
*
* @param feedId ID of the feed
* @param maxProviders Maximum number of providers to fetch
* @returns Array of provider addresses
*/
async getFeedProviders(feedId, maxProviders = 8) {
const providerRequests = Array.from(
{ length: maxProviders },
(_, i) => this.getMappingValue(
"eclipse_oracle_staking_4.aleo",
"provider_list",
`${Number(feedId) + i}field`
).then((raw) => parseAddress(raw))
);
const addresses = (await Promise.all(providerRequests)).filter(
Boolean
);
return addresses;
}
/**
* Gets the stake amount for a provider in a feed
*
* @param address Provider address
* @param feedId Feed ID
* @returns Stake amount (in credits)
*/
async getProviderStake(address, feedId) {
const key = (convertAddressToField(address) + BigInt(feedId)).toString();
const raw = await this.getMappingValue(
"eclipse_oracle_staking_4.aleo",
"stakes",
`${key}field`
);
return parseUint(raw, "u128");
}
/**
* Gets the proposed price for a provider in a feed
*
* @param address Provider address
* @param feedId Feed ID
* @returns Proposed price or null if no proposal
*/
async getProviderProposedPrice(address, feedId) {
const key = (convertAddressToField(address) + BigInt(feedId)).toString();
const raw = await this.getMappingValue(
"eclipse_oracle_submit_4.aleo",
"temp_price",
`${key}field`
);
return raw ? parseUint(raw, "u128") / 1e6 : null;
}
/**
* Gets the total staked amount for a feed
*
* @param feedId Feed ID
* @returns Total staked amount
*/
async getTotalStaked(feedId) {
const raw = await this.getMappingValue(
"eclipse_oracle_staking_4.aleo",
"total_staked",
`${feedId}field`
);
return parseUint(raw, "u128");
}
/**
* Gets the current price for a feed
*
* @param feedId Feed ID
* @returns Current price or null if not available
*/
async getCurrentPrice(feedId) {
const raw = await this.getMappingValue(
"eclipse_oracle_aggregate_4.aleo",
"latest_price",
`${feedId}field`
);
return raw ? parseUint(raw, "u128") / 1e6 : null;
}
/**
* Gets configuration information for a feed
*
* @param feedId Feed ID
* @returns Feed configuration information
*/
async getFeedInfo(feedId) {
const raw = await this.getMappingValue(
"eclipse_oracle_feed.aleo",
"feeds",
`${feedId}field`
);
if (!raw) return null;
const creator = raw.match(/creator:\s*([a-z0-9]+)/)?.[1] ?? "";
const min_stake = parseUint(raw, "u64");
const slashing_threshold = parseUint(
raw?.split("slashing_threshold:")[1],
"u64"
);
const aggregation_window = parseUint(raw, "u32");
const challenge_window = parseUint(
raw?.split("challenge_window:")[1],
"u32"
);
const paused = (raw.match(/paused:\s*(true|false)/)?.[1] ?? "false") === "true";
return {
creator,
min_stake,
slashing_threshold,
aggregation_window,
challenge_window,
paused
};
}
/**
* Gets the count of providers for a feed
*
* @param feedId Feed ID
* @returns Provider count
*/
async getProviderCount(feedId) {
const raw = await this.getMappingValue(
"eclipse_oracle_staking_4.aleo",
"provider_count",
`${feedId}field`
);
return parseField(raw);
}
/**
* Gets the proposal median for a feed
*
* @param feedId Feed ID
* @returns Proposal median or null if not available
*/
async getProposalMedian(feedId) {
const raw = await this.getMappingValue(
"eclipse_oracle_aggregate_4.aleo",
"proposal_median",
`${feedId}field`
);
return raw ? parseField(raw) ? parseField(raw) / 1e6 : null : null;
}
/**
* Gets the proposal proposer for a feed
*
* @param feedId Feed ID
* @returns Proposer address or null if not available
*/
async getProposalProposer(feedId) {
const raw = await this.getMappingValue(
"eclipse_oracle_aggregate_4.aleo",
"proposal_proposer",
`${feedId}field`
);
return parseAddress(raw);
}
/**
* Gets the proposal block for a feed
*
* @param feedId Feed ID
* @returns Proposal block number or null if not available
*/
async getProposalBlock(feedId) {
const raw = await this.getMappingValue(
"eclipse_oracle_aggregate_4.aleo",
"proposal_block",
`${feedId}field`
);
return parseField(raw);
}
/**
* Checks if the proposal was slashed for a feed
*
* @param feedId Feed ID
* @returns True if slashed, false if not, null if not available
*/
async getProposalSlashed(feedId) {
const raw = await this.getMappingValue(
"eclipse_oracle_aggregate_4.aleo",
"proposal_slashed",
`${feedId}field`
);
return parseBool(raw);
}
/**
* Checks if the aggregate is done for a feed
*
* @param feedId Feed ID
* @returns True if done, false if not, null if not available
*/
async getAggregateDone(feedId) {
const raw = await this.getMappingValue(
"eclipse_oracle_aggregate_4.aleo",
"aggregate_done",
`${feedId}field`
);
return parseBool(raw);
}
/**
* Gets the slasher for a feed
*
* @param feedId Feed ID
* @returns Slasher address or null if not available
*/
async getSlasher(feedId) {
const raw = await this.getMappingValue(
"eclipse_oracle_aggregate_4.aleo",
"slasher",
`${feedId}field`
);
return parseAddress(raw);
}
/**
* Gets the slasher reward for a feed
*
* @param feedId Feed ID
* @returns Slasher reward or null if not available
*/
async getSlasherReward(feedId) {
const raw = await this.getMappingValue(
"eclipse_oracle_aggregate_4.aleo",
"slasher_reward",
`${feedId}field`
);
return parseField(raw);
}
/**
* Gets the last propose block for a feed
*
* @param feedId Feed ID
* @returns Last propose block number or null if not available
*/
async getLastProposeBlock(feedId) {
const raw = await this.getMappingValue(
"eclipse_oracle_aggregate_4.aleo",
"last_propose_block",
`${feedId}field`
);
return parseField(raw);
}
};
// src/core/feed.ts
var MAX_PROVIDERS = 8;
var FeedService = class {
/**
* Creates a new instance of the FeedService
*
* @param client AleoExplorerClient instance or config
*/
constructor(client) {
if (client instanceof AleoExplorerClient) {
this.client = client;
} else {
this.client = new AleoExplorerClient(client);
}
this.aggregateProgramId = "eclipse_oracle_aggregate_4.aleo";
}
/**
* Get the price history of a feed
* @param feedId Feed ID
* @param maxTransactions Maximum number of transactions to analyze (max 1000 per request)
* @returns Array of PricePoint (timestamp (UNIX timestamp), price)
*/
async getPriceHistory(feedId) {
const functionName = "propose";
const url = "https://testnetbeta.aleorpc.com";
const pageSize = 1e3;
let page = 0;
let totalFetched = 0;
let finished = false;
const history = [];
while (!finished && totalFetched < pageSize) {
const toFetch = Math.min(pageSize, pageSize - totalFetched);
const body = JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "aleoTransactionsForProgram",
params: {
programId: this.aggregateProgramId,
functionName,
page,
maxTransactions: toFetch
}
});
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body
});
if (!res.ok) break;
const data = await res.json();
const txs = data.result || [];
if (txs.length === 0) break;
for (const tx of txs) {
const finalizedAt = tx.finalizedAt || tx.transaction?.finalizedAt || "";
const transitions = tx.transaction?.execution?.transitions || [];
for (const transition of transitions) {
if (transition.program === this.aggregateProgramId && transition.function === functionName) {
const inputFeed = (transition.inputs || []).find(
(input) => input.value === `${feedId}field`
);
if (inputFeed) {
const priceInput = (transition.inputs || []).find(
(input) => typeof input.value === "string" && input.value.match(/^[0-9]+u128$/)
);
if (priceInput) {
const price = Number(priceInput.value.replace("u128", "")) / 1e6;
history.push({ timestamp: finalizedAt, price });
}
}
}
}
}
totalFetched += txs.length;
page += 1;
if (txs.length < toFetch) finished = true;
}
return history.sort((a, b) => a.timestamp > b.timestamp ? 1 : -1);
}
/**
* Gets complete data for a feed
*
* @param feedId ID of the feed
* @param maxProviders Maximum number of providers to check
* @returns Complete feed data
*/
async getFeedFullData(feedId, maxProviders = MAX_PROVIDERS) {
const addresses = await this.client.getFeedProviders(feedId, maxProviders);
const [
stakes,
proposedPrices,
totalStaked,
currentPrice,
feedInfo,
advancedInfo,
priceHistory,
slashedAddresses
] = await Promise.all([
Promise.all(
addresses.map(
(address) => this.client.getProviderStake(address, feedId)
)
),
Promise.all(
addresses.map(
(address) => this.client.getProviderProposedPrice(address, feedId)
)
),
this.client.getTotalStaked(feedId),
this.client.getCurrentPrice(feedId),
this.client.getFeedInfo(feedId),
Promise.all([
this.client.getProviderCount(feedId),
this.client.getProposalMedian(feedId),
this.client.getProposalProposer(feedId),
this.client.getProposalBlock(feedId),
this.client.getProposalSlashed(feedId),
this.client.getAggregateDone(feedId),
this.client.getSlasher(feedId),
this.client.getSlasherReward(feedId),
this.client.getLastProposeBlock(feedId)
]),
this.getPriceHistory(feedId),
this.getSlashedAddresses(feedId)
]);
const submitters = addresses.map((address, i) => ({
address,
stakedCredits: stakes[i] ?? 0,
proposedPrice: proposedPrices[i]
}));
const [
providerCount,
proposalMedian,
proposalProposer,
proposalBlock,
proposalSlashed,
aggregateDone,
slasher,
slasherReward,
lastProposeBlock
] = advancedInfo;
return {
id: feedId,
name: `Feed ${feedId}`,
infos: feedInfo,
totalStaked,
submitters,
priceHistory,
slashedAddresses,
currentPrice,
providerCount: providerCount !== null ? providerCount : void 0,
proposalMedian,
proposalProposer,
proposalBlock,
proposalSlashed,
aggregateDone,
slasher,
slasherReward,
lastProposeBlock
};
}
/**
* Get the slashed addresses history for a feed
* @param feedId Feed ID
* @param maxTransactions Maximum number of transactions to analyze (max 1000 per request)
* @returns Array of { address, date, type }
*/
async getSlashedAddresses(feedId) {
const url = "https://testnetbeta.aleorpc.com";
const pageSize = 1e3;
let page = 0;
let totalFetched = 0;
let finished = false;
const slashed = [];
const functions = ["slash_aggregator", "slash_provider"];
while (!finished && totalFetched < pageSize) {
const toFetch = Math.min(pageSize, pageSize - totalFetched);
const body = JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "aleoTransactionsForProgram",
params: {
programId: this.aggregateProgramId,
page,
maxTransactions: toFetch
}
});
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body
});
if (!res.ok) break;
const data = await res.json();
const txs = data.result || [];
if (txs.length === 0) break;
for (const tx of txs) {
const finalizedAt = tx.finalizedAt || tx.transaction?.finalizedAt || "";
const transitions = tx.transaction?.execution?.transitions || [];
for (const transition of transitions) {
if (transition.program === this.aggregateProgramId && functions.includes(transition.function)) {
const inputFeed = (transition.inputs || []).find(
(input) => input.value === `${feedId}field`
);
if (inputFeed) {
let slashedInput;
let type = void 0;
if (transition.function === "slash_aggregator") {
slashedInput = (transition.inputs || []).find(
(input) => typeof input.value === "string" && input.value.startsWith("aleo") && input.name === "proposer"
);
type = "aggregator";
} else if (transition.function === "slash_provider") {
slashedInput = (transition.inputs || []).find(
(input) => typeof input.value === "string" && input.value.startsWith("aleo") && input.name === "provider"
);
type = "provider";
}
if (slashedInput && type) {
slashed.push({
address: slashedInput.value,
date: finalizedAt,
type
});
}
}
}
}
}
totalFetched += txs.length;
page += 1;
if (txs.length < toFetch) finished = true;
}
return slashed.sort((a, b) => a.date > b.date ? 1 : -1);
}
};
exports.AleoExplorerClient = AleoExplorerClient;
exports.FeedService = FeedService;
exports.convertAddressToField = convertAddressToField;
exports.parseAddress = parseAddress;
exports.parseBool = parseBool;
exports.parseField = parseField;
exports.parseUint = parseUint;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map