> ## Documentation Index
> Fetch the complete documentation index at: https://xoxno.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# XOXNO oracle ABI

> The self-hosted multi-signer oracle: submitting prices, the Reflector-compatible read surface, admin controls, and error codes.

A self-hosted oracle for assets that have no Reflector or public RedStone feed on
Stellar — in practice, tokenized real-world assets.

Registered signers submit prices. The contract keeps the latest submission per
signer per feed, and recomputes a **median** aggregate under an N-of-M threshold.
Reads stay O(1) because the aggregate is stored, not computed on read.

<Note>
  This documents the oracle contract itself. For how the price aggregator
  *consumes* it — as one source inside an `AssetOracle`, with its own staleness and
  sanity checks layered on top — see [Oracles](/docs/stellar-lending/dev/oracles).
</Note>

## Submitting prices

```rust theme={"system"}
submit_price(signer: Address, feed_id: String, price: i128, package_timestamp: u64)
submit_prices(signer: Address, feed_ids: Vec<String>, prices: Vec<i128>, package_timestamp: u64)
```

`signer` must authorize. `submit_prices` applies **one** `package_timestamp` to
every entry and fails with `#10 LengthMismatch` if the two vectors differ in
length.

`package_timestamp` is in **milliseconds**. Every submission is checked against
five rules before it is stored:

| Rule                                                                                    | Failure                                  |
| --------------------------------------------------------------------------------------- | ---------------------------------------- |
| The signer is registered                                                                | `#1 NotAuthorizedSigner`                 |
| The feed is known                                                                       | `#14 FeedNotKnown`                       |
| The price is positive and in range                                                      | `#2 InvalidPrice` / `#9 PriceOutOfRange` |
| The timestamp is not in the future                                                      | `#11 FutureTimestamp`                    |
| The submission is not stale, and not older than this signer's previous one for the feed | `#16 StaleSubmission`                    |

That last rule is per signer per feed: you cannot rewind your own history, and
one lagging signer cannot drag the aggregate backwards.

Storing a submission recomputes the feed's aggregate immediately.

## Reading prices

The read surface is **Reflector-compatible**, so a consumer written against
Reflector works here unchanged. All reads are open.

| Function                                                                               | Returns                          |
| -------------------------------------------------------------------------------------- | -------------------------------- |
| `lastprice(asset) -> Option<ReflectorPriceData>`                                       | Latest price for a mapped asset. |
| `price(asset, timestamp) -> Option<ReflectorPriceData>`                                | Price at a point in time.        |
| `prices(asset, records) -> Option<Vec<ReflectorPriceData>>`                            | Recent history.                  |
| `read_price_data(...) -> RedStonePriceData`                                            | RedStone-shaped read.            |
| `read_price_data_for_feed(feed_id) -> RedStonePriceData`                               | Same, addressed by feed id.      |
| `read_price_history(...)`                                                              | Stored observations.             |
| `assets() -> Vec<ReflectorAsset>`                                                      | Mapped assets.                   |
| `feeds() -> Vec<String>`                                                               | Registered feed ids.             |
| `base() -> ReflectorAsset`                                                             | The quote asset.                 |
| `decimals() -> u32`                                                                    | Price scale.                     |
| `resolution() -> u32`                                                                  | Feed resolution.                 |
| `max_stale_seconds()` / `max_submission_age_seconds()` / `max_relative_skew_seconds()` | The three freshness bounds.      |

A feed with no usable aggregate raises `#7 NoDataForFeed`. That is the error you
see when the signer quorum is not met inside the aggregation window.

## The three freshness knobs

They do different jobs and are easy to confuse:

| Knob                         | Governs                                                                       |
| ---------------------------- | ----------------------------------------------------------------------------- |
| `max_submission_age_seconds` | How old a single submission may be to count toward the median at all.         |
| `max_relative_skew_seconds`  | How far behind the *freshest* submission a peer may lag before it is dropped. |
| `max_stale_seconds`          | How long a computed aggregate may be served to readers.                       |

Keep `max_submission_age_seconds` at or below the consuming oracle's
`max_price_stale_seconds`. Otherwise the aggregator rejects a price this contract
still considers valid.

## Administration

All owner-gated (`#[only_owner]`), reached through governance.

| Function                                          | Purpose                                                                                      |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `add_signer(signer)` / `remove_signer(signer)`    | Manages the signer set. Removing below the threshold raises `#6 CannotRemoveBelowThreshold`. |
| `set_threshold(threshold)`                        | Sets N in the N-of-M quorum. `#3 InvalidThreshold` if unworkable.                            |
| `set_max_stale_seconds(seconds)`                  | How long an aggregate may be served.                                                         |
| `set_max_submission_age_seconds(seconds)`         | Inclusion age for a submission.                                                              |
| `set_max_relative_skew_seconds(seconds)`          | Lag tolerance between signers.                                                               |
| `register_feed(feed_id)`                          | Registers a feed id.                                                                         |
| `add_feed(feed_id, asset)` / `remove_feed(asset)` | Maps a feed to a Reflector asset.                                                            |
| `set_resolution(resolution)`                      | Sets feed resolution.                                                                        |
| `recompute_feeds(feed_ids)`                       | Re-derives aggregates. See the warning below.                                                |
| `purge_feed(feed_id)`                             | Clears a feed's stored submissions.                                                          |
| `upgrade(new_wasm_hash)`                          | Replaces the contract Wasm.                                                                  |

<Warning>
  **Changing a bound does not re-derive existing aggregates.** `set_threshold`,
  `set_max_submission_age_seconds`, and `set_max_relative_skew_seconds` store the
  new value only. Feeds that already hold an aggregate keep serving it under the
  old rules until you call `recompute_feeds(feed_ids)`.

  Batch that call. Each feed costs roughly one ledger entry per signer plus three,
  so a sweep over every feed can exceed the transaction footprint limit. Use
  `feeds()` to enumerate ids and work through them in chunks.

  Sweeping automatically inside the setter is deliberately *not* done — its
  footprint would grow with the feed count and eventually make those settings
  permanently unchangeable.
</Warning>

## Error codes

Its own enum, unrelated to the lending protocol's codes.

| Code | Name                         | Meaning                                                   |
| ---- | ---------------------------- | --------------------------------------------------------- |
| `1`  | `NotAuthorizedSigner`        | The submitting address is not registered.                 |
| `2`  | `InvalidPrice`               | Price is zero or negative.                                |
| `3`  | `InvalidThreshold`           | Threshold is unworkable for the signer set.               |
| `4`  | `SignerAlreadyRegistered`    | That signer is already in the set.                        |
| `5`  | `SignerNotRegistered`        | No such signer.                                           |
| `6`  | `CannotRemoveBelowThreshold` | Removing would drop the set below the threshold.          |
| `7`  | `NoDataForFeed`              | No usable aggregate — typically a quorum miss.            |
| `8`  | `StaleData`                  | The stored aggregate is older than `max_stale_seconds`.   |
| `9`  | `PriceOutOfRange`            | Price outside accepted bounds.                            |
| `10` | `LengthMismatch`             | `submit_prices` vectors differ in length.                 |
| `11` | `FutureTimestamp`            | `package_timestamp` is ahead of ledger time.              |
| `12` | `FeedAlreadyMapped`          | That asset already maps to a feed.                        |
| `13` | `FeedNotMapped`              | No feed mapped for that asset.                            |
| `14` | `FeedNotKnown`               | Unregistered feed id.                                     |
| `15` | `InvalidSubmissionAge`       | The configured submission age is out of range.            |
| `16` | `StaleSubmission`            | Too old, or older than this signer's previous submission. |
| `17` | `FeedAlreadyRegistered`      | That feed id is already registered.                       |
| `18` | `InvalidRelativeSkew`        | The configured skew bound is out of range.                |

## Next

<CardGroup cols={2}>
  <Card title="Oracles" icon="tower-broadcast" href="/docs/stellar-lending/dev/oracles">
    How the price aggregator composes this into a validated USD price.
  </Card>

  <Card title="Oracle disruption runbook" icon="triangle-exclamation" href="/docs/stellar-lending/dev/ops/oracle-disruption">
    What to do when a feed stops producing an aggregate.
  </Card>

  <Card title="Addresses" icon="address-book" href="/docs/stellar-lending/dev/addresses">
    Deployed oracle adapter addresses per network.
  </Card>
</CardGroup>
