Skip to content

Instantly share code, notes, and snippets.

@hoffmabc
Created January 28, 2025 18:28
Show Gist options
  • Save hoffmabc/7c7c19918154a660184817d4b4351593 to your computer and use it in GitHub Desktop.
Save hoffmabc/7c7c19918154a660184817d4b4351593 to your computer and use it in GitHub Desktop.
Arch Network Oracle
use arch_program::{
account::{AccountInfo, AccountMeta},
entrypoint,
next_account_info,
instruction::Instruction,
msg,
program_error::ProgramError,
pubkey::Pubkey,
program::get_clock,
};
use borsh::{BorshDeserialize, BorshSerialize};
// Price data structure with timestamp
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct PriceData {
pub price_in_usd: u64, // BTC price in USD (6 decimal places)
pub last_update_time: i64, // Unix timestamp of last update
pub is_valid: bool, // Flag to indicate if price is valid
}
// Price update parameters
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct PriceUpdateParams {
pub new_price_in_usd: u64,
}
// Error codes specific to the oracle
#[derive(Debug)]
pub enum OracleError {
InvalidPrice,
StalePrice,
Unauthorized,
InvalidAccount,
}
impl From<OracleError> for ProgramError {
fn from(e: OracleError) -> Self {
ProgramError::Custom(match e {
OracleError::InvalidPrice => 1,
OracleError::StalePrice => 2,
OracleError::Unauthorized => 3,
OracleError::InvalidAccount => 4,
})
}
}
// Program ID: Will need to be updated with the actual program ID after deployment
// pub static ID: Pubkey = Pubkey::new_from_array([0u8; 32]);
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> Result<(), ProgramError> {
if instruction_data.is_empty() {
return Err(ProgramError::InvalidInstructionData);
}
match instruction_data[0] {
0 => update_price(program_id, accounts, &instruction_data[1..]),
1 => get_price(program_id, accounts),
_ => Err(ProgramError::InvalidInstructionData),
}
}
// Update the BTC/USD price
pub fn update_price(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> Result<(), ProgramError> {
let account_iter = &mut accounts.iter();
let price_account = next_account_info(account_iter)?;
let authority_account = next_account_info(account_iter)?;
// Verify the account is owned by our program
if price_account.owner != program_id {
return Err(OracleError::InvalidAccount.into());
}
// Only authorized updaters can modify the price
if !authority_account.is_signer {
return Err(OracleError::Unauthorized.into());
}
let price_update = PriceUpdateParams::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
// Validate price is not zero
if price_update.new_price_in_usd == 0 {
return Err(OracleError::InvalidPrice.into());
}
let clock = get_clock();
let mut price_data = PriceData::try_from_slice(&price_account.data.borrow())
.unwrap_or(PriceData {
price_in_usd: 0,
last_update_time: 0,
is_valid: false,
});
// Update the price data
price_data.price_in_usd = price_update.new_price_in_usd;
price_data.last_update_time = clock.unix_timestamp;
price_data.is_valid = true;
price_data.serialize(&mut &mut price_account.data.borrow_mut()[..])
.map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
msg!("Price updated successfully to {} USD", price_update.new_price_in_usd);
Ok(())
}
// Get the current BTC/USD price
pub fn get_price(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> Result<(), ProgramError> {
let account_iter = &mut accounts.iter();
let price_account = next_account_info(account_iter)?;
// Verify the account is owned by our program
if price_account.owner != program_id {
return Err(OracleError::InvalidAccount.into());
}
let price_data = PriceData::try_from_slice(&price_account.data.borrow())
.map_err(|_| ProgramError::InvalidAccountData)?;
// Check if price is valid
if !price_data.is_valid {
return Err(OracleError::InvalidPrice.into());
}
// Check if price is not stale (older than 5 minutes)
let clock = get_clock();
if clock.unix_timestamp - price_data.last_update_time > 300 {
return Err(OracleError::StalePrice.into());
}
msg!("Current BTC price: {} USD", price_data.price_in_usd);
Ok(())
}
// Helper functions for CPI (Cross-Program Invocation)
/// Creates an instruction to update the price
pub fn update_price_instruction(
program_id: &Pubkey,
price_account: &Pubkey,
authority: &Pubkey,
new_price: u64,
) -> Result<Instruction, ProgramError> {
let data = PriceUpdateParams {
new_price_in_usd: new_price,
};
let mut instruction_data = vec![0]; // Instruction discriminator for update_price
instruction_data.extend_from_slice(&borsh::to_vec(&data).unwrap());
Ok(Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta {
pubkey: *price_account,
is_signer: false,
is_writable: true,
},
AccountMeta {
pubkey: *authority,
is_signer: true,
is_writable: false,
},
],
data: instruction_data,
})
}
/// Creates an instruction to get the current price
pub fn get_price_instruction(
program_id: &Pubkey,
price_account: &Pubkey,
) -> Instruction {
Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta {
pubkey: *price_account,
is_signer: false,
is_writable: false,
},
],
data: vec![1], // Instruction discriminator for get_price
}
}
// Conversion helpers
pub fn btc_to_usd(btc_amount: u64, price_data: &PriceData) -> Option<u64> {
if !price_data.is_valid {
return None;
}
// BTC amount is in sats (8 decimals), price is in USD (6 decimals)
// Result should be in USD cents (2 decimals)
Some((btc_amount as u128 * price_data.price_in_usd as u128 / 1_000_000_00) as u64)
}
pub fn usd_to_btc(usd_amount: u64, price_data: &PriceData) -> Option<u64> {
if !price_data.is_valid || price_data.price_in_usd == 0 {
return None;
}
// USD amount is in cents (2 decimals), price is in USD (6 decimals)
// Result should be in sats (8 decimals)
Some((usd_amount as u128 * 1_000_000_00 / price_data.price_in_usd as u128) as u64)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment