Skip to main content

Integration patterns

Noeracle supports three integration patterns. Pick by the structure of your consumer contract.

Pattern A — Standalone update + cached read

Two operations in one transaction. The first invokes Noeracle directly to verify and store the price. The second is your own application op, which reads the just-stored price via get_price.

const fresh = await oracle.fetchLatest(["BTC/USD"]);

const tx = new TransactionBuilder(account, { fee, networkPassphrase })
.addOperation(fresh.toUpdateOp(ORACLE)) // verify + store
.addOperation(myContract.call("open_position")) // your app op
.setTimeout(30)
.build();

Your contract reads the verified price:

let entry: PriceEntry = env.invoke_contract(
&oracle_address,
&Symbol::new(&env, "get_price"),
vec![&env, asset_tag.into_val(&env)],
).unwrap();
let price: i128 = entry.price;

When to use. You don't want to change your existing contract surface.

Trade-off. get_price returns the most recently verified price for the asset, which in principle could be from another consumer's pull-mode tx earlier in the same ledger. If exact provenance matters, use Pattern B.

Pattern B — Inline verification inside your contract

Your contract exposes a wrapper that verifies the price and uses it in one call. Use fresh.updateArgs() to pass the six raw arguments through.

const fresh = await oracle.fetchLatest(["BTC/USD"]);
const args = fresh.updateArgs();
// args = [assets, prices, timestamp, round_id, pubkey, sigs]

const tx = new TransactionBuilder(account, { fee, networkPassphrase })
.addOperation(myContract.call("open_position_with_price", ...args, ORACLE_ADDR))
.setTimeout(30)
.build();

Your contract:

pub fn open_position_with_price(
env: Env,
assets: Vec<BytesN<8>>,
prices: Vec<i128>,
timestamp: u64,
round_id: u64,
pubkey: BytesN<32>,
sigs: Vec<BytesN<64>>,
oracle: Address,
) {
env.invoke_contract::<()>(
&oracle,
&Symbol::new(&env, "update_batch_ed25519_args"),
(assets.clone(), prices.clone(), timestamp, round_id, pubkey, sigs)
.into_val(&env),
);
let price = prices.get_unchecked(0);
// ... use price directly ...
}

When to use. You control the consumer contract and want the verified price used inline, with no possibility of another consumer's tx changing the cache between verify and read.

:::warning Replay window

The oracle's 60-second freshness check guarantees the attestation was signed within the last 60 seconds. It does not guarantee the attestation was signed inside your consumer's logical time window. For settlement, liquidation, or auction logic, enforce a stronger temporal check in the consumer body:

if timestamp < my_window_start {
return Err(Error::PriceBeforeWindow);
}

Without that check, an attacker can replay a ≤60 s old attestation to settle an event that was supposed to settle "now".

:::

Pattern C — Live SSE subscription

For long-running clients (UIs, keepers, market-makers) that want to hold a fresh price in memory without polling.

const sub = oracle.subscribe(["BTC/USD", "ETH/USD"], (fresh) => {
console.log(fresh.price("BTC/USD"));
});

// later:
sub.close();

The connection reconnects automatically on transient errors. Each round (every 500 ms on testnet) fires onUpdate with a Fresh for the requested assets.

Failure modes

FailureBehaviorRecovery
Attestation service unreachablefetchLatest throws AttestationServiceErrorRetry with backoff; check https://api.noeracle.org/health
Requested asset not in snapshotfetchLatest throws AssetUnavailableErrorCheck /v1/latest for the supported asset list
Snapshot older than 2 seconds at fetchfetchLatest throws StalePriceErrorRetry; service may be lagging or client clock skewed
Signed price >60 s old at on-chain checkContract returns Error::StalePrice (5)Re-fetch and resubmit
Round older than storedSilent no-op (does not fail the tx)Expected; consumer ordering is independent across pulls
Unknown publisher keyContract returns Error::UnknownPublisher (4)Mismatched contract address or stale SDK; upgrade

See also