Created
January 28, 2025 18:28
-
-
Save hoffmabc/7c7c19918154a660184817d4b4351593 to your computer and use it in GitHub Desktop.
Arch Network Oracle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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