UNPKG

oracle-sdk

Version:

Interact with ease with the eclipse contracts

573 lines (568 loc) 16.8 kB
import { bech32m } from '@scure/base'; // src/utils/address.ts function convertAddressToField(address) { if (!address.startsWith("aleo1")) { throw new Error("Invalid address"); } let decoded; try { decoded = 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 = 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); } }; export { AleoExplorerClient, FeedService, convertAddressToField, parseAddress, parseBool, parseField, parseUint }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map