- 
                Notifications
    You must be signed in to change notification settings 
- Fork 305
feat(lazer-protocol): add Metadata V3 response types #3111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
          
     Open
      
      
            tejasbadadare
  wants to merge
  11
  commits into
  main
  
    
      
        
          
  
    
      Choose a base branch
      
     
    
      
        
      
      
        
          
          
        
        
          
            
              
              
              
  
           
        
        
          
            
              
              
           
        
       
     
  
        
          
            
          
            
          
        
       
    
      
from
tb/lazer-protocol/add-metadata-response-types
  
      
      
   
  
    
  
  
  
 
  
      
    base: main
Could not load branches
            
              
  
    Branch not found: {{ refName }}
  
            
                
      Loading
              
            Could not load tags
            
            
              Nothing to show
            
              
  
            
                
      Loading
              
            Are you sure you want to change the base?
            Some commits from the old base branch may be removed from the timeline,
            and old review comments may become outdated.
          
          
  
     Open
                    Changes from all commits
      Commits
    
    
            Show all changes
          
          
            11 commits
          
        
        Select commit
          Hold shift + click to select a range
      
      438a206
              
                feat(lazer-protocol): add metadata v3 response types
              
              
                tejasbadadare ebf5caf
              
                doc(lazer-protocol): tweak examples
              
              
                tejasbadadare 05e2dba
              
                feat(lazer-protocol): add SymbolV3 type & validation, add strum::Enum…
              
              
                tejasbadadare ead5853
              
                feat(lazer-protocol): use Strings instead of enums in the public API,…
              
              
                tejasbadadare 6dd6551
              
                fix lints
              
              
                tejasbadadare c2c1de8
              
                fix types
              
              
                tejasbadadare 61906e1
              
                fix types
              
              
                tejasbadadare 103690f
              
                fix types
              
              
                tejasbadadare 7bf6d87
              
                Merge branch 'main' of github.com:pyth-network/pyth-crosschain into t…
              
              
                tejasbadadare 416bc1c
              
                update cargo lock
              
              
                tejasbadadare 2896662
              
                use strings instead of enums
              
              
                tejasbadadare File filter
Filter by extension
Conversations
          Failed to load comments.   
        
        
          
      Loading
        
  Jump to
        
          Jump to file
        
      
      
          Failed to load files.   
        
        
          
      Loading
        
  Diff view
Diff view
          Some comments aren't visible on the classic Files Changed page.
        
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
      
      Oops, something went wrong.
      
    
  
  
    
      This file contains hidden or 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
    
  
  
    
              
  
    
      This file contains hidden or 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
    
  
  
    
              
  
    
      This file contains hidden or 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
    
  
  
    
              
  
    
      This file contains hidden or 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
    
  
  
    
              
  
    
      This file contains hidden or 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
    
  
  
    
              
  
    
      This file contains hidden or 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
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| //! Types describing Lazer's metadata APIs. | ||
|  | ||
| use crate::time::{DurationUs, TimestampUs}; | ||
| use crate::PriceFeedId; | ||
| use serde::{Deserialize, Serialize}; | ||
|  | ||
| /// The pricing context or type of instrument for a feed. | ||
| /// This is an internal type and should not be used by clients as it is non-exhaustive. | ||
| /// The API response can evolve to contain additional variants that are not listed here. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] | ||
| #[serde(rename_all = "lowercase")] | ||
| pub enum InstrumentType { | ||
| /// Spot price | ||
| Spot, | ||
| /// Redemption rate | ||
| #[serde(rename = "redemptionrate")] | ||
| RedemptionRate, | ||
| /// Funding rate | ||
| #[serde(rename = "fundingrate")] | ||
| FundingRate, | ||
| /// Future price | ||
| Future, | ||
| /// Net Asset Value | ||
| Nav, | ||
| /// Time-weighted average price | ||
| Twap, | ||
| } | ||
|  | ||
| /// High-level asset class. | ||
| /// This is an internal type and should not be used by clients as it is non-exhaustive. | ||
| /// The API response can evolve to contain additional variants that are not listed here. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] | ||
| #[serde(rename_all = "kebab-case")] | ||
| pub enum AssetClass { | ||
| /// Cryptocurrency | ||
| Crypto, | ||
| /// Foreign exchange | ||
| Fx, | ||
| /// Equity | ||
| Equity, | ||
| /// Metal | ||
| Metal, | ||
| /// Rates | ||
| Rates, | ||
| /// Commodity | ||
| Commodity, | ||
| } | ||
|  | ||
| /// Feed metadata as returned by the v3 metadata API. | ||
| #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] | ||
| pub struct FeedResponseV3 { | ||
| /// Unique integer identifier for a feed. Known as `pyth_lazer_id` in V1 API. | ||
| /// Example: `1` | ||
| pub id: PriceFeedId, | ||
| /// Short feed name. | ||
| /// Example: `"Bitcoin / US Dollar"` | ||
| pub name: String, | ||
| /// Unique human-readable identifier for a feed. | ||
| /// Format: `source.instrument_type.base/quote` | ||
| /// Examples: `"pyth.spot.btc/usd"`, `"pyth.redemptionrate.alp/usd"`, `"binance.fundingrate.btc/usdt"`, `"pyth.future.emz5/usd"` | ||
| pub symbol: String, | ||
| /// Description of the feed pair. | ||
| /// Example: `"Pyth Network Aggregate Price for spot BTC/USD"` | ||
| pub description: String, | ||
| /// The Asset ID of the base asset. | ||
| /// Example: `"BTC"` | ||
| pub base_asset_id: String, | ||
| /// The Asset ID of the quote asset. | ||
| /// Example: `"USD"` | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub quote_asset_id: Option<String>, | ||
| /// The pricing context. Should be one of the values in the InstrumentType enum. | ||
| /// Example: `"spot"` | ||
| pub instrument_type: String, | ||
| /// Aggregator or producer of the prices. | ||
| /// Examples: `"pyth"`, `"binance"` | ||
| pub source: String, | ||
| /// The trading schedule of the feed's market, in Pythnet format. | ||
| /// Example: `"America/New_York;O,O,O,O,O,O,O;"` | ||
| pub schedule: String, | ||
| /// Power-of-ten exponent. Scale the `price` mantissa value by `10^exponent` to get the decimal representation. | ||
| /// Example: `-8` | ||
| pub exponent: i16, | ||
| /// Funding rate interval. Only applies to feeds with instrument type `funding_rate`. | ||
| /// Example: `10` | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub update_interval: Option<DurationUs>, | ||
| /// The minimum number of publishers contributing component prices to the aggregate price. | ||
| /// Example: `3` | ||
| pub min_publishers: u16, | ||
| /// Status of the feed. | ||
| /// Example: `"active"` | ||
| pub state: String, | ||
| /// High-level asset class. One of crypto, fx, equity, metal, rates, nav, commodity, funding-rate. | ||
| /// Should be one of the values in the AssetClass enum. | ||
| /// Example: `"crypto"` | ||
| pub asset_type: String, | ||
| /// CoinMarketCap asset identifier. | ||
| /// Example: `"123"` | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub cmc_id: Option<u32>, | ||
| /// Pythnet feed identifier. 32 bytes, represented in hex. | ||
| /// Example: `"e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"` | ||
| pub pythnet_id: String, | ||
| /// Nasdaq symbol identifier. | ||
| /// Example: `"ADSK"` | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub nasdaq_symbol: Option<String>, | ||
| /// ISO datetime after which the feed will no longer produce prices because the underlying market has expired. | ||
| /// Example: `"2025-10-03T11:08:10.089998603Z"` | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub feed_expiry: Option<TimestampUs>, | ||
| /// The nature of the data produced by the feed. | ||
| /// Examples: `"price"`, `"fundingRate"` | ||
| pub feed_kind: String, | ||
| } | ||
|  | ||
| /// Asset metadata as returned by the v3 metadata API. | ||
| #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] | ||
| pub struct AssetResponseV3 { | ||
| /// Unique identifier for an asset. | ||
| /// Example: `"BTC"` | ||
| pub id: String, | ||
| /// A short, human-readable code that identifies an asset. Not guaranteed to be unique. | ||
| /// Example: `"BTC"` | ||
| pub ticker: String, | ||
| /// Full human-readable name of the asset. | ||
| /// Example: `"Bitcoin"` | ||
| pub full_name: String, | ||
| /// High-level asset class. | ||
| /// Example: `"crypto"` | ||
| pub class: String, | ||
| /// More granular categorization within class. | ||
| /// Example: `"stablecoin"` | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub subclass: Option<String>, | ||
| /// Primary or canonical listing exchange, when applicable. | ||
|         
                  tejasbadadare marked this conversation as resolved.
              Show resolved
            Hide resolved | ||
| /// Example: `"NASDAQ"` | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub primary_exchange: Option<String>, | ||
| } | ||
|  | ||
| #[cfg(test)] | ||
| mod tests { | ||
| use std::str::FromStr; | ||
|  | ||
| use crate::SymbolV3; | ||
|  | ||
| use super::*; | ||
|  | ||
| #[test] | ||
| fn test_feed_response_v3_json_serde_roundtrip() { | ||
| use crate::PriceFeedId; | ||
|  | ||
| let symbol = SymbolV3::new( | ||
| "pyth".to_string(), | ||
| "spot".to_string(), | ||
| "btc".to_string(), | ||
| Some("usd".to_string()), | ||
| ); | ||
|  | ||
| let feed_response = FeedResponseV3 { | ||
| id: PriceFeedId(1), | ||
| name: "Bitcoin / US Dollar".to_string(), | ||
| symbol: symbol.as_string(), | ||
| description: "Pyth Network Aggregate Price for spot BTC/USD".to_string(), | ||
| base_asset_id: "BTC".to_string(), | ||
| quote_asset_id: Some("USD".to_string()), | ||
| instrument_type: "spot".to_string(), | ||
| source: "pyth".to_string(), | ||
| schedule: "America/New_York;O,O,O,O,O,O,O;".to_string(), | ||
| exponent: -8, | ||
| update_interval: Some(DurationUs::from_secs_u32(10)), | ||
| min_publishers: 3, | ||
| state: "stable".to_string(), | ||
| asset_type: "crypto".to_string(), | ||
| cmc_id: Some(1), | ||
| pythnet_id: "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" | ||
| .to_string(), | ||
| nasdaq_symbol: None, | ||
| feed_expiry: None, | ||
| feed_kind: "price".to_string(), | ||
| }; | ||
|  | ||
| // Test JSON serialization | ||
| let json = | ||
| serde_json::to_string(&feed_response).expect("Failed to serialize FeedResponseV3"); | ||
| let expected_json = r#"{"id":1,"name":"Bitcoin / US Dollar","symbol":"pyth.spot.btc/usd","description":"Pyth Network Aggregate Price for spot BTC/USD","base_asset_id":"BTC","quote_asset_id":"USD","instrument_type":"spot","source":"pyth","schedule":"America/New_York;O,O,O,O,O,O,O;","exponent":-8,"update_interval":10000000,"min_publishers":3,"state":"stable","asset_type":"crypto","cmc_id":1,"pythnet_id":"e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43","feed_kind":"price"}"#; | ||
| assert_eq!( | ||
| json, expected_json, | ||
| "Serialized JSON does not match expected output" | ||
| ); | ||
|  | ||
| // Test JSON deserialization | ||
| let deserialized: FeedResponseV3 = | ||
| serde_json::from_str(&json).expect("Failed to deserialize FeedResponseV3"); | ||
|  | ||
| // Ensure the entire structure matches | ||
| assert_eq!(deserialized, feed_response); | ||
|  | ||
| // Test SymbolV3 deserialization | ||
| assert_eq!(deserialized.symbol, "pyth.spot.btc/usd"); | ||
| let symbol = SymbolV3::from_str(&deserialized.symbol).unwrap(); | ||
| assert_eq!(symbol.source, "pyth"); | ||
| assert_eq!(symbol.instrument_type, "spot"); | ||
| assert_eq!(symbol.base, "btc"); | ||
| assert_eq!(symbol.quote, Some("usd".to_string())); | ||
| } | ||
| } | ||
      
      Oops, something went wrong.
        
    
  
  Add this suggestion to a batch that can be applied as a single commit.
  This suggestion is invalid because no changes were made to the code.
  Suggestions cannot be applied while the pull request is closed.
  Suggestions cannot be applied while viewing a subset of changes.
  Only one suggestion per line can be applied in a batch.
  Add this suggestion to a batch that can be applied as a single commit.
  Applying suggestions on deleted lines is not supported.
  You must change the existing code in this line in order to create a valid suggestion.
  Outdated suggestions cannot be applied.
  This suggestion has been applied or marked resolved.
  Suggestions cannot be applied from pending reviews.
  Suggestions cannot be applied on multi-line comments.
  Suggestions cannot be applied while the pull request is queued to merge.
  Suggestion cannot be applied right now. Please check back later.
  
    
  
    
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the reasons for opting in a dynamic metadata was to not go through a code change when new asset classes are added to the system. I know that users (on both sides) rely on these, and that's probably why you opted for explicit definition here. But if that's the case, you might actually remove any dynamic field and make everything very explicit. Being in the middle (some explicit metadata, some implicit dynamic) is probably worse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should stick with our decision and use fully dynamic metadata in the protocols. In Rust that would be
BTreeMap<String, serde_value::Value>. We can revisit it later if we feel like the metadata structure is very stable and future-proof, but I doubt that it will happen soon.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah my main goal in adding these explicit types is to ensure end users can depend on a stable API contract across different versions. I.e. the different types like the existing
SymbolResponseand the newFeedResponseV3can handle the differences between v1 and v3 representations derived from the same internal dynamic metadata representation.