Pragma Miden uses a decentralized publisher model: the oracle account stores only a registry of publisher IDs — prices live in each publisher’s own account. Consuming price data requires:
Fetching the oracle’s storage to discover all registered publisher IDs
Importing each publisher account as a ForeignAccount
Running a transaction script that calls get_median on the oracle via FPI
The oracle performs the aggregation on-chain and returns the median to your stack.
Because Pragma’s oracle depends on multiple publishers, you must import every registered publisher account alongside the oracle itself before executing the FPI call.src/main.rs:
use anyhow::Context;use miden_client::{ account::AccountId, builder::ClientBuilder, keystore::FilesystemKeyStore, rpc::{ domain::account::{AccountStorageRequirements, StorageMapKey}, Endpoint, GrpcClient, }, store::AccountRecordData, transaction::{ForeignAccount, TransactionRequestBuilder}, Client, ClientError, Felt, Word, ZERO,};use miden_client_sqlite_store::ClientBuilderSqliteExt;use miden_protocol::{account::StorageSlotName, transaction::TransactionKernel};use miden_standards::code_builder::CodeBuilder;use miden_client::transaction::AdviceInputs;use std::{collections::BTreeSet, sync::Arc};/// Fetches the oracle account, reads all registered publisher IDs from its/// storage map, imports each one, and returns the full list of ForeignAccounts/// needed to call `get_median` via FPI.async fn get_oracle_foreign_accounts( client: &mut Client<FilesystemKeyStore>, oracle_id: AccountId, faucet_id_word: Word,) -> anyhow::Result<Vec<ForeignAccount>> { client.import_account_by_id(oracle_id).await?; client.sync_state().await?; let oracle_record = client .get_account(oracle_id) .await? .context("oracle account not found")?; let oracle = match oracle_record.account_data() { AccountRecordData::Full(acc) => acc, _ => anyhow::bail!("expected full oracle account data"), }; let storage = oracle.storage(); // Slot 0: next_publisher_index — tells us how many publishers are registered let count_slot = StorageSlotName::new("pragma::oracle::next_publisher_index")?; let publisher_count = storage .get_item(&count_slot) .context("unable to read publisher count")?[0] .as_int(); // Slot 1: publishers map — keys are [index, 0, 0, 0], values are publisher account words let publishers_slot = StorageSlotName::new("pragma::oracle::publishers")?; let publisher_ids: Vec<AccountId> = (2..publisher_count) .map(|i| -> anyhow::Result<AccountId> { let key: Word = [Felt::new(i), ZERO, ZERO, ZERO].into(); let w = storage .get_map_item(&publishers_slot, key) .with_context(|| format!("publisher at index {i} not found"))?; Ok(AccountId::new_unchecked([w[3], w[2]])) }) .collect::<Result<_, _>>()?; // Build ForeignAccount list: each publisher (with its entries map) + the oracle let entries_slot = StorageSlotName::new("pragma::publisher::entries")?; let mut foreign_accounts: Vec<ForeignAccount> = vec![]; for pid in publisher_ids { client.import_account_by_id(pid).await?; let fa = ForeignAccount::public( pid, AccountStorageRequirements::new([( entries_slot.clone(), &[StorageMapKey::from(faucet_id_word)], )]), )?; foreign_accounts.push(fa); } foreign_accounts.push(ForeignAccount::public( oracle_id, AccountStorageRequirements::default(), )?); Ok(foreign_accounts)}
The oracle’s get_median procedure is called from a transaction script that runs against your own ephemeral account. The script pushes the pair identifier and executes FPI on the oracle.
#[tokio::main]async fn main() -> anyhow::Result<()> { // ------------------------------------------------------------------------- // Build client // ------------------------------------------------------------------------- let rpc = Arc::new(GrpcClient::new(&Endpoint::testnet(), 10_000)); let keystore = Arc::new( FilesystemKeyStore::new("./keystore".into()).unwrap(), ); let mut client = ClientBuilder::new() .rpc(rpc) .sqlite_store("./store.sqlite3") .authenticator(keystore.clone()) .in_debug_mode(true.into()) .build() .await?; println!("Latest block: {}", client.sync_state().await?.block_num); // ------------------------------------------------------------------------- // Oracle configuration (testnet) — check latest address at github.com/astraly-labs/pragma-miden#deployments // ------------------------------------------------------------------------- let oracle_id = AccountId::from_hex("0xafebd403be621e005bf03b9fec7fe8")?; // BTC/USD — faucet_id "1:0" → [prefix=1, suffix=0, 0, 0] let faucet_id_word: Word = [Felt::new(1), Felt::new(0), ZERO, ZERO].into(); let (prefix, suffix) = (1u64, 0u64); let amount = 0u64; // ------------------------------------------------------------------------- // Resolve foreign accounts (oracle + all publishers) // ------------------------------------------------------------------------- let foreign_accounts = get_oracle_foreign_accounts(&mut client, oracle_id, faucet_id_word).await?; // ------------------------------------------------------------------------- // Build the transaction script // // The script calls get_median via FPI using the oracle component library. // Stack input: [amount, suffix, prefix, 0] (TOS = 0) // Stack output: [amount, is_tracked, median] // ------------------------------------------------------------------------- let script_code = format!( " use oracle_component::oracle_module use miden::core::sys begin push.0.{amount}.{suffix}.{prefix} call.oracle_module::get_median exec.sys::truncate_stack end ", prefix = prefix, suffix = suffix, amount = amount, ); // get_oracle_component_library() compiles oracle.masm and exposes get_median // You can import it from: pm_accounts::oracle::get_oracle_component_library // Or inline the library build here using miden_protocol primitives. use pm_accounts::oracle::get_oracle_component_library; let script = CodeBuilder::default() .with_dynamically_linked_library(&get_oracle_component_library()) .map_err(|e| anyhow::anyhow!("{e:?}"))? .compile_tx_script(script_code) .map_err(|e| anyhow::anyhow!("{e:?}"))?; // ------------------------------------------------------------------------- // Execute (read-only: execute_program, not submit_new_transaction) // ------------------------------------------------------------------------- let fa_set: BTreeSet<ForeignAccount> = foreign_accounts.into_iter().collect(); let output_stack = client .execute_program(oracle_id, script, AdviceInputs::default(), fa_set) .await .map_err(|e| anyhow::anyhow!("{e:?}"))?; // Stack layout: [amount, is_tracked, median, ...] let median = output_stack[1].as_int(); let is_tracked = output_stack[0].as_int(); if is_tracked == 0 { println!("Asset is not tracked by the oracle."); } else { // Prices use 6 decimal places println!("BTC/USD median: {} (raw: {})", median as f64 / 1_000_000.0, median); } Ok(())}
Contract addresses change between testnet iterations. Always refer to the pragma-miden README for the latest oracle ID.
The oracle library (pm_accounts::oracle) is published as part of pragma-miden. You can add it as a git dependency or copy the compiled MASM into your own project.