Skip to content

Commit

Permalink
Use pyth price feeds (#378)
Browse files Browse the repository at this point in the history
* add token::price module

Adjust to using pyth sponsored price feeds

* Add price command to current feed price

* Add feed id checks in price fetches

* Fix optimistic price calculation

* adjust to passing in solana client for price::get
  • Loading branch information
madninja authored Jun 20, 2024
1 parent 48451c1 commit d020676
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 154 deletions.
197 changes: 86 additions & 111 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions helium-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ mnemonic = ["helium-mnemonic"]
hex = "0.4"
chrono = {version = "0", features = ["serde"]}
thiserror = "1"
pyth-sdk-solana = "0"
async-trait = "0"
anchor-client = {version = "0.30.0", features = ["async"] }
anchor-spl = { version = "0.30.0", features = ["mint", "token"] }
Expand All @@ -33,11 +32,11 @@ helium-anchor-gen = {git = "https://github.com/helium/helium-anchor-gen.git"}
spl-associated-token-account = { version = "*", features = ["no-entrypoint"] }
spl-account-compression = { version = "0.3", features = ["no-entrypoint"] }
mpl-bubblegum = "1"
pyth-solana-receiver-sdk = "0"
solana-program = "*"
solana-transaction-status = "*"
serde = {workspace = true}
serde_json = {workspace = true}
# serde_with = "2"
lazy_static = "1"
rust_decimal = {workspace = true}
helium-proto = {workspace= true}
Expand Down
6 changes: 3 additions & 3 deletions helium-lib/src/result.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{array::TryFromSliceError, num::TryFromIntError};
use thiserror::Error;

use crate::{onboarding, settings};
use crate::{onboarding, settings, token};

pub type Result<T = ()> = std::result::Result<T, Error>;

Expand All @@ -18,8 +18,8 @@ pub enum Error {
AnchorLang(#[from] helium_anchor_gen::anchor_lang::error::Error),
#[error("DAS client: {0}")]
Das(#[from] settings::DasClientError),
#[error("pyth client: {0}")]
Pyth(#[from] pyth_sdk_solana::PythError),
#[error("price client: {0}")]
Price(#[from] token::price::PriceError),
#[error("rest client: {0}")]
Rest(#[from] reqwest::Error),
#[error("system time: {0}")]
Expand Down
130 changes: 104 additions & 26 deletions helium-lib/src/token.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::{
keypair::{serde_pubkey, GetPubkey, Pubkey},
result::{DecodeError, Error, Result},
result::{DecodeError, Result},
settings::Settings,
};
use chrono::{DateTime, Utc};
use futures::stream::{self, StreamExt, TryStreamExt};
use helium_anchor_gen::circuit_breaker;
use solana_sdk::{signer::Signer, system_instruction};
Expand All @@ -16,10 +17,17 @@ pub enum TokenError {

lazy_static::lazy_static! {
static ref HNT_MINT: Pubkey = Pubkey::from_str("hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux").unwrap();
static ref HNT_PRICE_KEY: Pubkey = Pubkey::from_str("7moA1i5vQUpfDwSpK6Pw9s56ahB7WFGidtbL2ujWrVvm").unwrap();
static ref HNT_PRICE_KEY: Pubkey = Pubkey::from_str("4DdmDswskDxXGpwHrXUfn2CNUm9rt21ac79GHNTN3J33").unwrap();
static ref HNT_PRICE_FEED: price::FeedId = price::feed_from_hex("649fdd7ec08e8e2a20f425729854e90293dcbe2376abc47197a14da6ff339756").unwrap();

static ref MOBILE_MINT: Pubkey = Pubkey::from_str("mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6").unwrap();
static ref MOBILE_PRICE_KEY: Pubkey = Pubkey::from_str("DQ4C1tzvu28cwo1roN1Wm6TW35sfJEjLh517k3ZeWevx").unwrap();
static ref MOBILE_PRICE_FEED: price::FeedId = price::feed_from_hex("ff4c53361e36a9b837433c87d290c229e1f01aec5ef98d9f3f70953a20a629ce").unwrap();

static ref IOT_MINT: Pubkey = Pubkey::from_str("iotEVVZLEywoTn1QdwNPddxPWszn3zFhEot3MfL9fns").unwrap();
static ref IOT_PRICE_KEY: Pubkey = Pubkey::from_str("8UYEn5Weq7toHwgcmctvcAxaNJo3SJxXEayM57rpoXr9").unwrap();
static ref IOT_PRICE_FEED: price::FeedId = price::feed_from_hex("6b701e292e0836d18a5904a08fe94534f9ab5c3d4ff37dc02c74dd0f4901944d").unwrap();

static ref DC_MINT: Pubkey = Pubkey::from_str("dcuc8Amr83Wz27ZkQ2K9NS6r8zRpf1J6cvArEBDZDmm").unwrap();
static ref SOL_MINT: Pubkey = solana_sdk::system_program::ID;
}
Expand Down Expand Up @@ -113,27 +121,79 @@ pub async fn balance_for_addresses(
.await
}

pub async fn pyth_price(settings: &Settings, token: Token) -> Result<pyth_sdk_solana::Price> {
let price_key = token
.price_key()
.ok_or_else(|| DecodeError::other(format!("No pyth price key for {token}")))?;
let client = settings.mk_solana_client()?;
let mut price_account = client.get_account(price_key).await?;
let price_feed =
pyth_sdk_solana::state::SolanaPriceAccount::account_to_feed(price_key, &mut price_account)?;

use std::time::{SystemTime, UNIX_EPOCH};
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?;
price_feed
.get_ema_price_no_older_than(
current_time
.as_secs()
.try_into()
.map_err(DecodeError::from)?,
10 * 60,
)
.ok_or_else(|| DecodeError::other("No token price found"))
.map_err(Error::from)
pub mod price {
use super::*;
use crate::solana_client::nonblocking::rpc_client::RpcClient as SolanaRpcClient;
use pyth_solana_receiver_sdk::price_update::{self, PriceUpdateV2};
use rust_decimal::prelude::*;

pub use pyth_solana_receiver_sdk::price_update::FeedId;
pub const DC_PER_USD: i64 = 100_000;

#[derive(Debug, thiserror::Error)]
pub enum PriceError {
#[error("invalid or unsupported token: {0}")]
InvalidToken(super::Token),
#[error("invalid price feed")]
InvalidFeed,
#[error("price too old")]
TooOld,
#[error("price below 0")]
Negative,
#[error("invalid price timestamp: {0}")]
InvalidTimestamp(i64),
#[error("unsupported positive price exponent")]
PositiveExponent,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Price {
pub timestamp: DateTime<Utc>,
pub price: Decimal,
pub token: super::Token,
}

pub fn feed_from_hex(str: &str) -> Result<FeedId> {
let feed_id =
price_update::get_feed_id_from_hex(str).map_err(|_| PriceError::InvalidFeed)?;
Ok(feed_id)
}

pub async fn get(solana_client: &SolanaRpcClient, token: Token) -> Result<Price> {
use helium_anchor_gen::anchor_lang::AccountDeserialize;
let price_key = token.price_key().ok_or(PriceError::InvalidToken(token))?;
let price_feed = token.price_feed().ok_or(PriceError::InvalidToken(token))?;
let account = solana_client.get_account(price_key).await?;
let PriceUpdateV2 { price_message, .. } =
PriceUpdateV2::try_deserialize(&mut account.data.as_slice())?;

if (price_message.publish_time.saturating_add(10 * 60)) < Utc::now().timestamp() {
return Err(PriceError::TooOld.into());
}
if price_message.ema_price < 0 {
return Err(PriceError::Negative.into());
}
if price_message.exponent > 0 {
return Err(PriceError::PositiveExponent.into());
}
if price_message.feed_id != *price_feed {
return Err(PriceError::InvalidFeed.into());
}
let scale = price_message.exponent.unsigned_abs();
// Remove the confidence interval from the price to get the most optimistic price:
let mut price = Decimal::new(price_message.ema_price, scale)
+ Decimal::new(price_message.ema_conf as i64, scale) * Decimal::new(2, 0);
// ensure we use only up to 6 decimals, this rounds using `MidpointAwayFromZero`
price.rescale(6);
let timestamp = DateTime::from_timestamp(price_message.publish_time, 0)
.ok_or(PriceError::InvalidTimestamp(price_message.publish_time))?;

Ok(Price {
timestamp,
price,
token,
})
}
}

#[derive(
Expand Down Expand Up @@ -182,15 +242,22 @@ impl Token {
vec![Self::Hnt, Self::Iot, Self::Mobile, Self::Dc, Self::Sol]
}

pub fn transferrable_value_parser(s: &str) -> StdResult<Self, TokenError> {
let transferrable = [Self::Iot, Self::Mobile, Self::Hnt, Self::Sol];
fn from_allowed(s: &str, allowed: &[Self]) -> StdResult<Self, TokenError> {
let result = Self::from_str(s)?;
if !transferrable.contains(&result) {
if !allowed.contains(&result) {
return Err(TokenError::InvalidToken(s.to_string()));
}
Ok(result)
}

pub fn transferrable_value_parser(s: &str) -> StdResult<Self, TokenError> {
Self::from_allowed(s, &[Self::Iot, Self::Mobile, Self::Hnt, Self::Sol])
}

pub fn pricekey_value_parser(s: &str) -> StdResult<Self, TokenError> {
Self::from_allowed(s, &[Self::Iot, Self::Mobile, Self::Hnt])
}

pub fn associated_token_adress(&self, address: &Pubkey) -> Pubkey {
match self {
Self::Sol => *address,
Expand Down Expand Up @@ -322,6 +389,17 @@ impl Token {
pub fn price_key(&self) -> Option<&Pubkey> {
match self {
Self::Hnt => Some(&HNT_PRICE_KEY),
Self::Iot => Some(&IOT_PRICE_KEY),
Self::Mobile => Some(&MOBILE_PRICE_KEY),
_ => None,
}
}

pub fn price_feed(&self) -> Option<&price::FeedId> {
match self {
Self::Hnt => Some(&HNT_PRICE_FEED),
Self::Iot => Some(&IOT_PRICE_FEED),
Self::Mobile => Some(&MOBILE_PRICE_FEED),
_ => None,
}
}
Expand Down
1 change: 0 additions & 1 deletion helium-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ serde_json = {workspace = true}
clap = { workspace = true }
qr2term = "0.2"
rust_decimal = {workspace = true}
rust_decimal_macros = "1"
tokio = {version = "1.0", features = ["full"]}
helium-lib = { path = "../helium-lib", features = ["clap", "mnemonic"] }
helium-mnemonic = { path = "../helium-mnemonic" }
Expand Down
16 changes: 6 additions & 10 deletions helium-wallet/src/cmd/dc/price.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use crate::cmd::*;
use helium_lib::token::{self, Token};
use rust_decimal::prelude::*;
use rust_decimal_macros::dec;
use serde_json::json;

#[derive(Clone, Debug, clap::Args)]
Expand All @@ -14,18 +13,14 @@ pub struct Cmd {

impl Cmd {
pub async fn run(&self, opts: Opts) -> Result {
let settings = opts.try_into()?;
let price = token::pyth_price(&settings, Token::Hnt).await?;
let decimals = price.expo.unsigned_abs();

// Remove the confidence from the price to use the most conservative price
// https://docs.pyth.network/pythnet-price-feeds/best-practices
let hnt_price = Decimal::new(price.price, decimals)
- (Decimal::new(price.conf as i64, decimals) * dec!(2));
let settings: Settings = opts.try_into()?;
let solana_client = settings.mk_solana_client()?;
let price = token::price::get(&solana_client, Token::Hnt).await?;

let hnt_price = price.price;
let usd_amount =
Decimal::from_f64(self.usd).ok_or_else(|| anyhow!("Invalid USD amount"))?;
let dc_amount = (usd_amount * dec!(100_000))
let dc_amount = (usd_amount * Decimal::new(token::price::DC_PER_USD, 0))
.to_u64()
.ok_or_else(|| anyhow!("Invalid USD amount"))?;
let hnt_amount = (usd_amount / hnt_price).round_dp(Token::Hnt.decimals().into());
Expand All @@ -35,6 +30,7 @@ impl Cmd {
"hnt": hnt_amount,
"dc": dc_amount,
"hnt_price": hnt_price,
"timestamp": price.timestamp,
});
print_json(&json)
}
Expand Down
1 change: 1 addition & 0 deletions helium-wallet/src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub mod dc;
pub mod export;
pub mod hotspots;
pub mod info;
pub mod price;
pub mod router;
pub mod sign;
pub mod transfer;
Expand Down
20 changes: 20 additions & 0 deletions helium-wallet/src/cmd/price.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use crate::cmd::*;
use helium_lib::token;

#[derive(Clone, Debug, clap::Args)]
/// Get the current price from the pyth price feed for the given token
pub struct Cmd {
/// Token to look up
#[arg(value_parser = token::Token::pricekey_value_parser)]
token: token::Token,
}

impl Cmd {
pub async fn run(&self, opts: Opts) -> Result {
let settings: Settings = opts.try_into()?;
let solana_client = settings.mk_solana_client()?;
let price = token::price::get(&solana_client, self.token).await?;

print_json(&price)
}
}
6 changes: 5 additions & 1 deletion helium-wallet/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use clap::Parser;
use helium_wallet::{
cmd::{balance, create, dc, export, hotspots, info, router, sign, transfer, upgrade, Opts},
cmd::{
balance, create, dc, export, hotspots, info, price, router, sign, transfer, upgrade, Opts,
},
result::Result,
};

Expand Down Expand Up @@ -30,6 +32,7 @@ pub enum Cmd {
Create(create::Cmd),
Hotspots(Box<hotspots::Cmd>),
Dc(dc::Cmd),
Price(price::Cmd),
Transfer(transfer::Cmd),
Export(export::Cmd),
Sign(sign::Cmd),
Expand All @@ -53,6 +56,7 @@ async fn run(cli: Cli) -> Result {
Cmd::Create(cmd) => cmd.run(cli.opts).await,
Cmd::Hotspots(cmd) => cmd.run(cli.opts).await,
Cmd::Dc(cmd) => cmd.run(cli.opts).await,
Cmd::Price(cmd) => cmd.run(cli.opts).await,
Cmd::Transfer(cmd) => cmd.run(cli.opts).await,
Cmd::Export(cmd) => cmd.run(cli.opts).await,
Cmd::Sign(cmd) => cmd.run(cli.opts).await,
Expand Down

0 comments on commit d020676

Please sign in to comment.