# SOON Chain Integration Guide

## Description

APRO Oracle Data Pull is a pull mode oracle product for those who require low-latency data feed service.

Following process chart is how pull mode works:

![](https://lh7-rt.googleusercontent.com/docsz/AD_4nXfFfbI--N-iakm2_Cl3OHtL4nL_z5YNujm-91HVo-3kb28irOy_soWhyVvoRSAArFNtIShwO8N3-96bgSxMyAWFiqGlFi2RCir5AK6y3xKs_v4ztM8mju7lM72iC0Li5sTU2AQIUQ?key=lO7VgCeFWS492mN1m8ub0_Li)

This doc is the integration guide for projects on SOON Devnet.

## Domains

| Description                             | Devnet URL                                                      | Mainnet URL                                           |
| --------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------- |
| REST Endpoint to query specific reports | [https://live-api-test.apro.com](http://live-api-test.apro.com) | [https://live-api.apro.com](http://live-api.apro.com) |

## Program IDs

| Description       | Devnet                                       | Mainnet                                      |
| ----------------- | -------------------------------------------- | -------------------------------------------- |
| Apro\_SVM\_Oracle | 4Mvy4RKRyJMf4PHavvGUuTj9agoddUZ9atQoFma1tyMY | 4Mvy4RKRyJMf4PHavvGUuTj9agoddUZ9atQoFma1tyMY |
| Sample\_Client    | HUJ8ouH6fVonhF1hPV6ENoLid5nbHfyZSpvfujw6X6Hm | HUJ8ouH6fVonhF1hPV6ENoLid5nbHfyZSpvfujw6X6Hm |

## Authentication

#### Headers

All routes require the following two headers for user authentication:

<table><thead><tr><th width="266">Header</th><th>Description</th></tr></thead><tbody><tr><td><strong>Authorization</strong></td><td>The user’s unique identifier, provided as a UUID (Universally Unique IDentifier).</td></tr><tr><td><strong>X-Authorization-Timestamp</strong></td><td>The current timestamp, with precision up to milliseconds. The timestamp must closely synchronize with the server time, allowing a maximum discrepancy of 5 seconds (by default).<br><strong>For a value of 10 digits, it is multiplied directly by 1000 to milliseconds.</strong></td></tr></tbody></table>

To get authorization, **please contact our BD Team**:

* Email: <bd@apro.com>
* Telegram:[ Head of Business Development](https://t.me/Annie_LLLEEE)

## API endpoints

1. Return a single report at a given timestamp

**Endpoint**

/api/soon/reports

<table><thead><tr><th width="152">Type</th><th>Description</th><th>Parameter(s)</th></tr></thead><tbody><tr><td>HTTP GET</td><td>Returns a single report for a given timestamp.</td><td><ul><li>feedID: A Data Streams feed ID.</li><li>timestamp: The Unix timestamp for the report.</li></ul></td></tr></tbody></table>

**Sample request**

GET /api/soon/reports?feedID=\<feedID>\&timestamp=\<timestamp>

**Sample response**

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXekGnFxtgvXRBqMDeINDXhKRI3zuU7MRX6BzS1crHEqRvC9XQGu6acT0SbyioNHGu5ydhm3MBQITvXjhSptrNyP5A5R61MG3R_4tRSsCzMU6j2pSuZ7gfIPrq9JQQtHWoWEltnL?key=lO7VgCeFWS492mN1m8ub0_Li" alt=""><figcaption></figcaption></figure>

2. Return a single report with the latest timestamp

**Endpoint**

/api/soon/reports/latest

| Type     | Parameter(s)                                      |
| -------- | ------------------------------------------------- |
| HTTP GET | <ul><li>feedID: A Data Streams feed ID.</li></ul> |

**Sample request**

GET /api/soon/reports/latest?feedID=\<feedID>

**Sample response**

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXff8IedD3DctaqXK0Du4ll7WronYnw1x8cAcEynYxan1NbB5_pUzkx4a0sERE70ZeH8-_s1GtK7qJXPwJFAQtlga-n5oCQ_HO0kVWO4waGMGorZSuCNIta_wreBgsh2_xLDg9yjbQ?key=lO7VgCeFWS492mN1m8ub0_Li" alt=""><figcaption></figcaption></figure>

3. Return a report for multiple FeedIDs at a given timestamp

**Endpoint**

/api/soon/reports/bulk

<table><thead><tr><th width="133">Type</th><th>Description</th><th>Parameter(s)</th></tr></thead><tbody><tr><td>HTTP GET</td><td>Return a report for multiple FeedIDs at a given timestamp.</td><td><ul><li>feedIDs: A comma-separated list of Data Streams feed IDs.</li><li>timestamp: The Unix timestamp for the first report or the string 'latest' for getting the latest report.</li></ul></td></tr></tbody></table>

**Sample request**

GET /api/soon/reports/bulk?feedIDs=\<feedID1>,\<feedID2>,...\&timestamp=\<timestamp>

GET /api/soon/reports/bulk?feedIDs=\<feedID1>,\<feedID2>,...\&timestamp=latest

**Sample response**<br>

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfHzVwaINmCvV3gi56cZL4aVtnaSqlTXanZMJ_Y8V4_28zjoGzIe-aA7EkDsLBv3uCPL-dGNfbYAK-9c7sK6iV3_z2raIS7BWXKJTVpObao-cUuI3IQooeqrG0UtDvX1_BsZrt0rQ?key=lO7VgCeFWS492mN1m8ub0_Li" alt=""><figcaption></figcaption></figure>

<figure><img src="https://227137242-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUIpyIkxGAPXFuivFcvwB%2Fuploads%2FVw9aKZmjBbZzUV3miNWp%2Fimage.png?alt=media&#x26;token=683906d1-338d-4f22-8e3b-16d44244b0b7" alt=""><figcaption></figcaption></figure>

4. Return multiple sequential reports for a single FeedID, starting at a given timestamp

**Endpoint**

/api/soon/reports/page

<table><thead><tr><th width="144">Type</th><th>Description</th><th>Parameter(s)</th></tr></thead><tbody><tr><td>HTTP GET</td><td>Return multiple sequential reports for a single FeedID, starting at a given timestamp.</td><td><ul><li>feedID: A Data Streams feed ID.</li><li>startTimestamp: The Unix timestamp for the first report.</li><li>limit (optional): The number of reports to return.</li></ul></td></tr></tbody></table>

**Sample request**

GET /api/soon/reports/page?feedID=\<FeedID>\&startTimestamp=\<StartTimestamp>\&limit=\<Limit>

**Sample response**

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdYEN4RAB5HgiZCgWibM6Gf7JymQLt03opIguOAEvW6N0hZO268n-31pGKGTFIkz80jg1jZh1XXjtrWHC-adRdEqLnsV2ejFRkjnP6_9mfBJVpa9-DjOAQ9ST0xCxTao3xqELlgsA?key=lO7VgCeFWS492mN1m8ub0_Li" alt=""><figcaption></figcaption></figure>

5. WebSocket Connection

Establish a streaming WebSocket connection that sends reports for the given feedID(s) after they are verified.

**Endpoint**

/api/soon/ws

<table><thead><tr><th width="186">Type</th><th>Parameter(s)</th></tr></thead><tbody><tr><td>WebSocket</td><td><ul><li>feedIDs: A comma-separated list of Data Streams feed IDs.</li></ul></td></tr></tbody></table>

**Sample request**

GET /api/soon/ws?feedIDs=\<feedID1>,\<feedID2>,...

\
**Sample response**

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXc1l_zXPvLCR8IBYVX08vfOs1EVHEpGi352RowGvNSS7IqrMPFjrJue2GA_M-qJWygs8w6irR3SgKOwIuHoFkxhbGHrdCHZO5r2tVw8dcFn66baKBH98TKyCtwXJkFDiKwX7URW?key=lO7VgCeFWS492mN1m8ub0_Li" alt=""><figcaption></figcaption></figure>

## Price Feed ID

**Devnet:**

<table><thead><tr><th width="208">Feed</th><th>Information</th></tr></thead><tbody><tr><td><strong>BTC/USD</strong></td><td><ul><li><strong>ID:</strong> 0x0003665949c883f9e0f6f002eac32e00bd59dfe6c34e92a91c37d6a8322d6489</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 1</li></ul></td></tr><tr><td><strong>ETH/USD</strong></td><td><ul><li><strong>ID:</strong> 0x0003555ace6b39aae1b894097d0a9fc17f504c62fea598fa206cc6f5088e6e45</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 2</li></ul></td></tr><tr><td><strong>SOL/USD</strong></td><td><ul><li><strong>ID:</strong> 0x000343ec7f6691d6bf679978bab5c093fa45ee74c0baac6cc75649dc59cc21d3</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 3</li></ul></td></tr><tr><td><strong>BONK/USD</strong></td><td><ul><li><strong>ID:</strong> 0x00032b250dddd30d526b4ae66d170e1ab204ab0567531798d3bbd09737f6c3c6</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 4</li></ul></td></tr></tbody></table>

**Mainnet:**

<table><thead><tr><th width="188">Feed</th><th>Information</th></tr></thead><tbody><tr><td><strong>BTC/USD</strong></td><td><ul><li><strong>ID:</strong> 0x0003665949c883f9e0f6f002eac32e00bd59dfe6c34e92a91c37d6a8322d6489</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 1</li></ul></td></tr><tr><td><strong>ETH/USD</strong></td><td><ul><li>I<strong>D:</strong> 0x0003555ace6b39aae1b894097d0a9fc17f504c62fea598fa206cc6f5088e6e45</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 2</li></ul></td></tr><tr><td><strong>Valueless_Test/USD</strong></td><td><ul><li><strong>ID:</strong> 0x000369f643e4e5d9b9d04863249c07c15937d0b36af803dd8d213827b0badd5c</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 3</li></ul></td></tr><tr><td><strong>USDT/USD</strong></td><td><ul><li><strong>ID:</strong> 0x00039a0c0be4e43cacda1599ac414205651f4a62b614b6be9e5318a182c33eb0</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 4</li></ul></td></tr><tr><td><strong>USDC/USD</strong></td><td><ul><li><strong>ID:</strong> 0x00034b881a0c0fff844177f881a313ff894bfc6093d33b5514e34d7faa41b7ef</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 5</li></ul></td></tr><tr><td><strong>BONK/USD</strong></td><td><ul><li><strong>ID:</strong> 0x00032b250dddd30d526b4ae66d170e1ab204ab0567531798d3bbd09737f6c3c6</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 10</li></ul></td></tr><tr><td><strong>SOL/USD</strong></td><td><ul><li><strong>ID:</strong> 0x000343ec7f6691d6bf679978bab5c093fa45ee74c0baac6cc75649dc59cc21d3</li><li> <strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 11</li></ul></td></tr><tr><td><strong>WEETH/USD</strong></td><td><ul><li><strong>ID:</strong><br>0x0003a41091252f838042a1ebb32e545788f0760743eb2aa129d55a6ceb53a716</li><li> <strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 12</li></ul></td></tr><tr><td><strong>EIGEN/USD</strong></td><td><ul><li><strong>ID:</strong><br>0x000345b536c6b8307db63d95fb38e0bd27996f9be48fb224cc8d5c24a8d00640</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 13</li></ul></td></tr><tr><td><strong>PUFETH/USD</strong></td><td><ul><li><strong>ID:</strong><br>0x00031a3a47d0230c312ab9f3a453a6d828464b582c754226233f4c5e86d0ed8d</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 14</li></ul></td></tr><tr><td><strong>RSETH/USD</strong></td><td><ul><li><strong>ID:</strong><br>0x0003468783dab63735ffee9fe08084240acb16e81ca93ee2eb78ab81388c32ea</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 15</li></ul></td></tr><tr><td><strong>METH/USD</strong></td><td><ul><li><strong>ID:</strong><br>0x0003f815ffeb0d25f8945e0263bd3ce6c31311351a3dde2e90bc01539a5b06bb</li><li><strong>Decimals:</strong> 18</li><li><strong>oracle_state_ID:</strong> 16</li></ul></td></tr></tbody></table>

## Error response codes

<table><thead><tr><th width="246">Status Code</th><th width="467">Description</th></tr></thead><tbody><tr><td>400 Bad Request</td><td><p>This error is triggered when:</p><ul><li>There is any missing/malformed query argument.</li><li>Required headers are missing or provided with incorrect values.</li></ul></td></tr><tr><td>401 Unauthorized User</td><td><p>This error is triggered when:</p><ul><li>Authentication fails, typically because the HMAC signature provided by the client doesn't match the one expected by the server.</li><li>A user requests access to a feed without the appropriate permission or that does not exist.</li></ul></td></tr><tr><td>500 Internal Server</td><td>Indicates an unexpected condition encountered by the server, preventing it from fulfilling the request. This error typically points to issues on the server side.</td></tr><tr><td>206 Missing data (/bulk endpoint only)</td><td>Indicates that at least one feed ID data is missing from the report. E.g., you requested a report for feed IDs <mark style="color:green;">&#x3C;feedID1></mark>, <mark style="color:green;">&#x3C;feedID2></mark>, and <mark style="color:green;">&#x3C;feedID3></mark> at a given timestamp. If data for <mark style="color:green;">&#x3C;feedID2></mark> is missing from the report (not available yet at the specified timestamp), you get [<mark style="color:green;">&#x3C;feedID1 data></mark>, <mark style="color:green;">&#x3C;feedID3 data></mark>] and a 206 response.</td></tr></tbody></table>

## Sample Integration Program

#### **oracle\_sdk:**

```rust
use anchor_lang::prelude::*;
use anchor_lang::solana_program::hash::hash;
use anchor_lang::solana_program::instruction::Instruction;
use anchor_lang::solana_program::program::invoke;

pub const APRO_SVM_PROGRAM_ID: &str = "4Mvy4RKRyJMf4PHavvGUuTj9agoddUZ9atQoFma1tyMY";

pub fn load_price_feed_from_account_info(price_account_info: &AccountInfo) -> Result<PriceFeed> {
    let data = price_account_info.try_borrow_data()?;

    let mut price_feed_data = &data[8..];
    let price_feed = PriceFeed::deserialize(&mut price_feed_data)?;

    Ok(price_feed)
}

pub fn update_price<'info>(
    oracle_state: &AccountInfo<'info>,
    price_feed: &AccountInfo<'info>,
    payer: &AccountInfo<'info>,
    admin: &AccountInfo<'info>,
    system_program: &AccountInfo<'info>,
    oracle_program: &AccountInfo<'info>,
    feed_id: [u8; 32],
    valid_time_stamp: u128,
    observe_time_stamp: u128,
    native_fee: u128,
    apro_token_fee: u128,
    expire_at: u128,
    benchmark_price: u128,
    ask_price: u128,
    bid_price: u128,
    config_digest: [u8; 32],
    epoch_and_round: u128,
    extra_hash: [u8; 32],
    signatures: Vec<[u8; 64]>,
    recovery_ids: Vec<u8>,
) -> Result<()> {
    let ix = Instruction {
        program_id: *oracle_program.key,
        accounts: vec![
            AccountMeta::new_readonly(*oracle_state.key, false),
            AccountMeta::new(*price_feed.key, false),
            AccountMeta::new(*payer.key, true),
            AccountMeta::new(*admin.key, false),
            AccountMeta::new_readonly(*system_program.key, false),
        ],
        data: UpdatePriceArgs {
            feed_id,
            valid_time_stamp,
            observe_time_stamp,
            native_fee,
            apro_token_fee,
            expire_at,
            benchmark_price,
            ask_price,
            bid_price,
            config_digest,
            epoch_and_round,
            extra_hash,
            signatures,
            recovery_ids,
        }
        .data(),
    };

    invoke(
        &ix,
        &[
            oracle_state.clone(),
            price_feed.clone(),
            payer.clone(),
            admin.clone(),
            system_program.clone(),
            oracle_program.clone(),
        ],
    )?;

    Ok(())
}

#[derive(AnchorDeserialize, AnchorSerialize, Clone, Debug)]
pub struct PriceFeed {
    pub feed_id: [u8; 32],
    pub valid_time_stamp: u128,
    pub observe_time_stamp: u128,
    pub native_fee: u128,
    pub apro_token_fee: u128,
    pub expire_at: u128,
    pub benchmark_price: u128,
    pub ask_price: u128,
    pub bid_price: u128,
    pub config_digest: [u8; 32],
    pub epoch_and_round: u128,
    pub extra_hash: [u8; 32],
}

#[derive(AnchorSerialize, AnchorDeserialize)]
struct UpdatePriceArgs {
    feed_id: [u8; 32],
    valid_time_stamp: u128,
    observe_time_stamp: u128,
    native_fee: u128,
    apro_token_fee: u128,
    expire_at: u128,
    benchmark_price: u128,
    ask_price: u128,
    bid_price: u128,
    config_digest: [u8; 32],
    epoch_and_round: u128,
    extra_hash: [u8; 32],
    signatures: Vec<[u8; 64]>,
    recovery_ids: Vec<u8>,
}

impl UpdatePriceArgs {
    fn data(&self) -> Vec<u8> {
        let mut data = Vec::new();
        let preimage = "global:update_price".as_bytes();
        let hash = hash(preimage);
        let discriminator = &hash.to_bytes()[..8];
        data.extend_from_slice(discriminator);
        data.extend_from_slice(&AnchorSerialize::try_to_vec(self).unwrap());

        data
    }
}
```

#### **Clients code:**

```rust
use anchor_lang::prelude::*;
use oracle_sdk::{load_price_feed_from_account_info, update_price};

declare_id!("GzWu85MdbZtVBDcC4Xrmp1dWix7Qo52nUAkD2FjraJ6f");//modify by solana address -k

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

    pub fn fetch_price(ctx: Context<FetchPrice>) -> Result<()> {
        //fetch price feed from oracle
        let price_feed = load_price_feed_from_account_info(&ctx.accounts.price_account)?;

        msg!("Price Feed Details:");
        msg!("Feed ID: {:?}", price_feed.feed_id);
        msg!("Valid Timestamp: {}", price_feed.valid_time_stamp);
        msg!("Observe Timestamp: {}", price_feed.observe_time_stamp);
        msg!("Benchmark Price: {}", price_feed.benchmark_price);
        msg!("Ask Price: {}", price_feed.ask_price);
        msg!("Bid Price: {}", price_feed.bid_price);

        let current_timestamp = Clock::get()?.unix_timestamp as u128;
        let staleness_threshold = 3600u128;

        //store the price in the price_result account
        let price_result = if current_timestamp - price_feed.valid_time_stamp <= staleness_threshold
        {
            PriceResult {
                price: price_feed.benchmark_price,
                is_valid: true,
            }
        } else {
            PriceResult {
                price: 0,
                is_valid: false,
            }
        };

        ctx.accounts.price_result.set_inner(price_result);

        Ok(())
    }

    pub fn update_oracle_price(
        ctx: Context<UpdateOraclePrice>,
        feed_id: [u8; 32],
        valid_time_stamp: u128,
        observe_time_stamp: u128,
        native_fee: u128,
        apro_token_fee: u128,
        expire_at: u128,
        benchmark_price: u128,
        ask_price: u128,
        bid_price: u128,
        config_digest: [u8; 32],
        epoch_and_round: u128,
        extra_hash: [u8; 32],
        signatures: Vec<[u8; 64]>,
        recovery_ids: Vec<u8>,
    ) -> Result<()> {
        //update the price feed through CPI
        update_price(
            &ctx.accounts.oracle_state.to_account_info(),
            &ctx.accounts.price_feed.to_account_info(),
            &ctx.accounts.payer.to_account_info(),
            &ctx.accounts.admin.to_account_info(),
            &ctx.accounts.system_program.to_account_info(),
            &ctx.accounts.oracle_program.to_account_info(),
            feed_id,
            valid_time_stamp,
            observe_time_stamp,
            native_fee,
            apro_token_fee,
            expire_at,
            benchmark_price,
            ask_price,
            bid_price,
            config_digest,
            epoch_and_round,
            extra_hash,
            signatures,
            recovery_ids,
        )?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct FetchPrice<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    /// CHECK: This account is owned by the Oracle program
    pub price_account: UncheckedAccount<'info>,
    #[account(
        init_if_needed,
        payer = payer,
        space = 8 + 16 + 1, // discriminator + price (u128) + is_valid
        seeds = [b"price_result", price_account.key().as_ref()],
        bump
    )]
    pub price_result: Account<'info, PriceResult>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct UpdateOraclePrice<'info> {
    /// CHECK: This account is verified in the update_price function
    pub oracle_state: UncheckedAccount<'info>,
    /// CHECK: This account is verified in the update_price function
    #[account(mut)]
    pub price_feed: UncheckedAccount<'info>,
    #[account(mut)]
    pub payer: Signer<'info>,
    /// CHECK: This account is verified in the update_price function
    #[account(mut)]
    pub admin: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
    /// CHECK: This account is verified in the update_price function
    pub oracle_program: UncheckedAccount<'info>,
}

#[account]
pub struct PriceResult {
    pub price: u128,
    pub is_valid: bool,
}

#[error_code]
pub enum ErrorCode {
    #[msg("Invalid account data length")]
    InvalidAccountDataLength,
}
```
