Skip to main content

Introduction

Molpha is a decentralized oracle network that provides reliable and secure data feeds for various assets. This guide will walk you through the process of integrating with Molpha oracles on the Solana blockchain, allowing your programs to access real-time data.

Program Information

  • Program ID: moLfaMTgKNysiLevoQZk8igxektjJj4LtxSiUdfKHY3
  • Serialization Format: Borsh
  • Network: Solana Mainnet/Devnet

Key Concepts

The Molpha oracle program is built around a few key accounts:

The Feed Account

The Feed account is the primary account you will interact with. It stores all the information about a specific data feed, including:
  • name_hash: A 32-byte unique identifier for the feed (hash of the feed name).
  • authority: The public key of the account that can manage the feed.
  • feed_type: The type of feed (Public or Personal).
  • latest_answer: The most recent data point published to the feed.
  • answer_history: A vector containing the history of recent answers.
  • subscription_due_time: The Unix timestamp (i64) when the feed subscription expires.
The structure of the Feed account matches the Molpha program’s IDL:
use anchor_lang::prelude::*;

#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct Feed {
    pub name_hash: [u8; 32],
    pub authority: Pubkey,
    pub feed_type: FeedType,
    pub job_id: [u8; 32],
    pub data_source: Pubkey,
    pub min_signatures_threshold: u8,
    pub frequency: u64,
    pub ipfs_cid: String,
    pub latest_answer: Answer,
    pub answer_history: Vec<Answer>,
    pub history_idx: u64,
    pub subscription_due_time: i64,
    pub price_per_second_scaled: u64,
    pub created_at: i64,
    pub bump: u8,
}

#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub enum FeedType {
    Public,
    Personal,
}

The Answer Struct

The Answer struct contains the actual data value and the timestamp when it was recorded.
#[derive(Clone, AnchorSerialize, AnchorDeserialize, Copy, Default)]
pub struct Answer {
    pub value: [u8; 32],
    pub timestamp: i64,
}
The value is a 32-byte array, which can represent different data types depending on the feed. For example, for a price feed, it could be a scaled integer encoded in the bytes. You’ll need to decode this based on your feed’s metadata or documentation.

On-chain Integration

To use Molpha oracle data in your Solana program, you need to:
  1. Define the Feed and Answer structs matching the Molpha program’s structure.
  2. Pass the Feed account to your instruction with proper owner verification.
  3. Access the latest_answer to get the current value.
  4. Verify the feed subscription is still active.

1. Define the Account Structures

First, you need to define the account structures that match the Molpha program. These should be defined in your program:
use anchor_lang::prelude::*;

// Molpha program ID
pub const MOLPHA_PROGRAM_ID: Pubkey = pubkey!("moLfaMTgKNysiLevoQZk8igxektjJj4LtxSiUdfKHY3");

#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct Answer {
    pub value: [u8; 32],
    pub timestamp: i64,
}

#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub enum FeedType {
    Public,
    Personal,
}

#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct Feed {
    pub name_hash: [u8; 32],
    pub authority: Pubkey,
    pub feed_type: FeedType,
    pub job_id: [u8; 32],
    pub data_source: Pubkey,
    pub min_signatures_threshold: u8,
    pub frequency: u64,
    pub ipfs_cid: String,
    pub latest_answer: Answer,
    pub answer_history: Vec<Answer>,
    pub history_idx: u64,
    pub subscription_due_time: i64,
    pub price_per_second_scaled: u64,
    pub created_at: i64,
    pub bump: u8,
}

2. Define the Instruction Context

Define the accounts required for your instruction. Important: You must verify that the feed account is owned by the Molpha program using the owner constraint.
#[derive(Accounts)]
pub struct ReadPrice<'info> {
    #[account(
        owner = MOLPHA_PROGRAM_ID @ ErrorCode::InvalidFeedOwner
    )]
    pub molpha_feed: Account<'info, Feed>,
}

#[error_code]
pub enum ErrorCode {
    #[msg("Feed account is not owned by Molpha program")]
    InvalidFeedOwner,
    #[msg("Feed subscription has expired")]
    SubscriptionExpired,
}

3. Implement the Instruction Logic

In your instruction logic, you can access the deserialized molpha_feed account and read its latest_answer. Always verify the subscription is active before using the data.
pub fn read_price(ctx: Context<ReadPrice>) -> Result<()> {
    let feed = &ctx.accounts.molpha_feed;

    // Verify subscription is still active
    let clock = Clock::get()?;
    require!(
        feed.subscription_due_time > clock.unix_timestamp,
        ErrorCode::SubscriptionExpired
    );

    // Access the latest answer
    let latest_answer = feed.latest_answer;
    let price_value = latest_answer.value;
    let timestamp = latest_answer.timestamp;

    msg!("Latest price value: {:?}", price_value);
    msg!("Timestamp: {}", timestamp);

    // Decode the value based on your feed's format
    // For example, if it's a u64 price:
    // let price = u64::from_le_bytes(price_value[0..8].try_into().unwrap());

    // Your program logic here...

    Ok(())
}

4. Complete Example

Here is a complete example of a Solana program that reads from a Molpha oracle:
use anchor_lang::prelude::*;

declare_id!("YourProgramId111111111111111111111111111111");

// Molpha program ID
pub const MOLPHA_PROGRAM_ID: Pubkey = pubkey!("moLfaMTgKNysiLevoQZk8igxektjJj4LtxSiUdfKHY3");

#[program]
pub mod my_dapp {
    use super::*;

    pub fn read_price(ctx: Context<ReadPrice>) -> Result<()> {
        let feed = &ctx.accounts.molpha_feed;

        // Verify subscription is still active
        let clock = Clock::get()?;
        require!(
            feed.subscription_due_time > clock.unix_timestamp,
            ErrorCode::SubscriptionExpired
        );

        // Access the latest answer
        let latest_answer = feed.latest_answer;
        let price_value = latest_answer.value;
        let timestamp = latest_answer.timestamp;

        msg!("Latest answer timestamp: {}", timestamp);
        msg!("Value bytes: {:?}", price_value);

        // Example: Decode as u64 (adjust based on your feed's format)
        let price = u64::from_le_bytes(
            price_value[0..8]
                .try_into()
                .map_err(|_| ErrorCode::InvalidPriceFormat)?
        );
        msg!("Decoded price: {}", price);

        // Your program logic here...

        Ok(())
    }
}

#[derive(Accounts)]
pub struct ReadPrice<'info> {
    #[account(
        owner = MOLPHA_PROGRAM_ID @ ErrorCode::InvalidFeedOwner
    )]
    pub molpha_feed: Account<'info, Feed>,
}

// Account structures (must match Molpha program)
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct Answer {
    pub value: [u8; 32],
    pub timestamp: i64,
}

#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub enum FeedType {
    Public,
    Personal,
}

#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct Feed {
    pub name_hash: [u8; 32],
    pub authority: Pubkey,
    pub feed_type: FeedType,
    pub job_id: [u8; 32],
    pub data_source: Pubkey,
    pub min_signatures_threshold: u8,
    pub frequency: u64,
    pub ipfs_cid: String,
    pub latest_answer: Answer,
    pub answer_history: Vec<Answer>,
    pub history_idx: u64,
    pub subscription_due_time: i64,
    pub price_per_second_scaled: u64,
    pub created_at: i64,
    pub bump: u8,
}

#[error_code]
pub enum ErrorCode {
    #[msg("Feed account is not owned by Molpha program")]
    InvalidFeedOwner,
    #[msg("Feed subscription has expired")]
    SubscriptionExpired,
    #[msg("Invalid price format")]
    InvalidPriceFormat,
}

Important Notes

  • Owner Verification: Always use the owner constraint to verify the feed account belongs to the Molpha program. This prevents malicious accounts from being passed as feeds.
  • Subscription Check: Always verify that subscription_due_time > current_timestamp before using feed data.
  • Value Decoding: The value field is a 32-byte array. You need to decode it according to your feed’s data format (e.g., u64, i64, f64, etc.).
  • Account Discriminator: Anchor automatically handles the 8-byte discriminator when using Account<'info, Feed>, so you don’t need to manually skip it.

Off-chain Usage with TypeScript

You can also fetch and use Molpha oracle data from off-chain applications using TypeScript. You’ll need the Molpha program IDL to properly deserialize the account data.

Using Anchor SDK

import { Connection, PublicKey } from '@solana/web3.js';
import { Program, AnchorProvider, Wallet } from '@coral-xyz/anchor';
import { IDL as MolphaIdl, Molpha } from './molpha_idl'; // Import your Molpha IDL

async function getOraclePrice(feedAddress: string) {
    // Connect to Solana network
    const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
    
    // Create a dummy wallet for read-only operations
    // For write operations, use a real wallet
    const wallet = {
        publicKey: PublicKey.default,
        signTransaction: async (tx: any) => tx,
        signAllTransactions: async (txs: any[]) => txs,
    } as Wallet;
    
    const provider = new AnchorProvider(connection, wallet, {
        commitment: 'confirmed',
    });
    
    // Molpha program ID
    const molphaProgramId = new PublicKey('moLfaMTgKNysiLevoQZk8igxektjJj4LtxSiUdfKHY3');
    const program = new Program<Molpha>(MolphaIdl, molphaProgramId, provider);
    
    // Fetch the feed account
    const feedPubkey = new PublicKey(feedAddress);
    const feedAccount = await program.account.feed.fetch(feedPubkey);
    
    // Access feed data
    const latestAnswer = feedAccount.latestAnswer;
    const valueBytes = Buffer.from(latestAnswer.value);
    const timestamp = new Date(Number(latestAnswer.timestamp) * 1000);
    
    // Check subscription status
    const currentTime = Math.floor(Date.now() / 1000);
    const isActive = Number(feedAccount.subscriptionDueTime) > currentTime;
    
    console.log('Feed Authority:', feedAccount.authority.toString());
    console.log('Latest Answer Value (hex):', valueBytes.toString('hex'));
    console.log('Timestamp:', timestamp.toISOString());
    console.log('Subscription Active:', isActive);
    console.log('Subscription Due Time:', new Date(Number(feedAccount.subscriptionDueTime) * 1000).toISOString());
    
    // Example: Decode as u64 (little-endian)
    if (valueBytes.length >= 8) {
        const price = valueBytes.readBigUInt64LE(0);
        console.log('Decoded Price (u64):', price.toString());
    }
    
    return {
        feed: feedAccount,
        latestAnswer,
        valueBytes,
        timestamp,
        isActive,
    };
}

// Usage
getOraclePrice('YourFeedAddressHere...')
    .then(result => console.log('Feed data:', result))
    .catch(error => console.error('Error:', error));

Using Raw Account Data (Without Anchor)

If you don’t have the Anchor IDL, you can deserialize the account data manually using Borsh:
import { Connection, PublicKey } from '@solana/web3.js';
import * as borsh from '@coral-xyz/borsh';

// Define the schema matching the Feed struct
const FeedSchema = borsh.struct([
    borsh.array(borsh.u8(), 32, 'nameHash'),
    borsh.publicKey('authority'),
    borsh.u8('feedType'), // 0 = Public, 1 = Personal
    borsh.array(borsh.u8(), 32, 'jobId'),
    borsh.publicKey('dataSource'),
    borsh.u8('minSignaturesThreshold'),
    borsh.u64('frequency'),
    borsh.string('ipfsCid'),
    borsh.struct([
        borsh.array(borsh.u8(), 32, 'value'),
        borsh.i64('timestamp'),
    ], 'latestAnswer'),
    // Note: answer_history is a Vec, which requires length prefix
    // This is a simplified version - you may need to handle Vec deserialization
]);

async function getFeedDataRaw(feedAddress: string) {
    const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
    const feedPubkey = new PublicKey(feedAddress);
    
    const accountInfo = await connection.getAccountInfo(feedPubkey);
    if (!accountInfo) {
        throw new Error('Feed account not found');
    }
    
    // Skip the 8-byte discriminator (Anchor account discriminator)
    const data = accountInfo.data.slice(8);
    
    // Deserialize using Borsh
    const feed = FeedSchema.decode(data);
    
    console.log('Feed Authority:', feed.authority.toString());
    console.log('Latest Answer Value:', Buffer.from(feed.latestAnswer.value).toString('hex'));
    console.log('Timestamp:', new Date(Number(feed.latestAnswer.timestamp) * 1000).toISOString());
    
    return feed;
}

Finding Feed Addresses

Feed addresses are PDAs derived from:
  • Seed: "feed"
  • Authority: The feed’s authority public key
  • Name hash: The 32-byte hash of the feed name
  • Feed type: The feed type enum value
You can derive the feed address using Anchor’s findProgramAddress or by querying the Molpha indexer/API if available.

Best Practices

  1. Always Verify Ownership: Use the owner constraint to ensure the feed account belongs to the Molpha program.
  2. Check Subscription Status: Always verify that subscription_due_time > current_timestamp before using feed data.
  3. Handle Stale Data: Check the timestamp of the latest answer to ensure the data is recent enough for your use case.
  4. Error Handling: Implement proper error handling for cases where the feed account doesn’t exist or subscription has expired.
  5. Value Decoding: Understand your feed’s data format. The 32-byte value array may represent different types (u64, i64, f64, etc.) depending on the feed configuration.
  6. Account Validation: Consider validating other feed properties like min_signatures_threshold if your use case requires a minimum number of signatures.

Common Issues

Account Discriminator Mismatch

If you get an “AccountDiscriminatorMismatch” error, ensure:
  • Your Feed struct exactly matches the Molpha program’s structure
  • You’re using Account<'info, Feed> with the owner constraint
  • The account data hasn’t been corrupted

Subscription Expired

Always check subscription_due_time before using feed data. If the subscription has expired, the feed may not be updated anymore.

Value Decoding

The value field is a 32-byte array. You need to know the encoding format:
  • For u64: u64::from_le_bytes(value[0..8].try_into().unwrap())
  • For i64: i64::from_le_bytes(value[0..8].try_into().unwrap())
  • For f64: Use appropriate floating-point decoding
  • Check the feed’s IPFS metadata or documentation for the exact format

Next Steps

Resources