Pārlūkot izejas kodu

Merge branch 'main' into pyth-stylus-governance-impl

Ayush Suresh 4 mēneši atpakaļ
vecāks
revīzija
e5bb84c5cd
52 mainītis faili ar 891 papildinājumiem un 376 dzēšanām
  1. 15 11
      Cargo.lock
  2. 1 1
      apps/fortuna/Cargo.toml
  3. 13 3
      apps/fortuna/src/eth_utils/utils.rs
  4. 7 5
      apps/fortuna/src/keeper/process_event.rs
  5. 1 1
      apps/hermes/server/Cargo.lock
  6. 1 1
      apps/hermes/server/Cargo.toml
  7. 1 1
      apps/hermes/server/src/api/rest/v2/timestamp_price_updates.rs
  8. 28 5
      apps/hermes/server/src/api/types.rs
  9. 1 1
      apps/pyth-lazer-agent/Cargo.toml
  10. 1 1
      apps/pyth-lazer-agent/config/config.toml
  11. 1 1
      apps/pyth-lazer-agent/src/http_server.rs
  12. 1 5
      apps/pyth-lazer-agent/src/relayer_session.rs
  13. 1 1
      apps/staking/src/components/Header/stats.tsx
  14. 3 0
      contract_manager/scripts/transfer_balance_entropy_chains.ts
  15. 7 0
      contract_manager/store/chains/EvmChains.json
  16. 5 0
      contract_manager/store/contracts/EvmPriceFeedContracts.json
  17. 5 0
      contract_manager/store/contracts/EvmWormholeContracts.json
  18. 4 3
      governance/remote_executor/programs/remote-executor/Cargo.toml
  19. 4 0
      governance/remote_executor/programs/remote-executor/src/state/governance_payload.rs
  20. 1 0
      governance/remote_executor/programs/remote-executor/src/state/posted_vaa.rs
  21. 2 0
      governance/xc_admin/packages/xc_admin_common/src/chains.ts
  22. 1 1
      lazer/contracts/solana/programs/pyth-lazer-solana-contract/Cargo.toml
  23. 1 1
      lazer/publisher_sdk/proto/dynamic_value.proto
  24. 77 34
      lazer/publisher_sdk/proto/governance_instruction.proto
  25. 2 2
      lazer/publisher_sdk/proto/publisher_update.proto
  26. 1 1
      lazer/publisher_sdk/proto/pyth_lazer_transaction.proto
  27. 58 47
      lazer/publisher_sdk/proto/state.proto
  28. 1 1
      lazer/publisher_sdk/proto/transaction_envelope.proto
  29. 6 3
      lazer/publisher_sdk/rust/Cargo.toml
  30. 178 0
      lazer/publisher_sdk/rust/src/convert_dynamic_value.rs
  31. 145 0
      lazer/publisher_sdk/rust/src/convert_dynamic_value/tests.rs
  32. 46 140
      lazer/publisher_sdk/rust/src/lib.rs
  33. 1 1
      lazer/sdk/rust/client/Cargo.toml
  34. 4 2
      lazer/sdk/rust/protocol/Cargo.toml
  35. 56 0
      lazer/sdk/rust/protocol/src/dynamic_value.rs
  36. 20 0
      lazer/sdk/rust/protocol/src/feed_kind.rs
  37. 4 0
      lazer/sdk/rust/protocol/src/lib.rs
  38. 10 4
      lazer/sdk/rust/protocol/src/router.rs
  39. 1 2
      target_chains/ethereum/sdk/solidity/IPyth.sol
  40. 89 10
      target_chains/fuel/contracts/Cargo.lock
  41. 14 24
      target_chains/fuel/contracts/Forc.lock
  42. 3 3
      target_chains/fuel/contracts/fuel-toolchain.toml
  43. 2 2
      target_chains/fuel/contracts/pyth-contract/Forc.toml
  44. 6 6
      target_chains/fuel/contracts/pyth-contract/src/main.sw
  45. 0 3
      target_chains/fuel/contracts/pyth-interface/Forc.toml
  46. 1 1
      target_chains/fuel/contracts/pyth-interface/src/data_structures/governance_instruction.sw
  47. 4 4
      target_chains/fuel/contracts/pyth-interface/src/data_structures/price.sw
  48. 7 6
      target_chains/fuel/contracts/pyth-interface/src/data_structures/wormhole_light.sw
  49. 6 5
      target_chains/fuel/contracts/pyth-interface/src/pyth_merkle_proof.sw
  50. 1 1
      target_chains/solana/sdk/js/pyth_solana_receiver/package.json
  51. 33 29
      target_chains/solana/sdk/js/pyth_solana_receiver/src/vaa.ts
  52. 10 3
      target_chains/stylus/contracts/pyth-receiver/src/lib.rs

+ 15 - 11
Cargo.lock

@@ -3071,7 +3071,7 @@ dependencies = [
 
 [[package]]
 name = "fortuna"
-version = "8.2.1"
+version = "8.2.2"
 dependencies = [
  "anyhow",
  "axum 0.6.20",
@@ -5611,7 +5611,7 @@ dependencies = [
 
 [[package]]
 name = "pyth-lazer-agent"
-version = "0.1.4"
+version = "0.3.0"
 dependencies = [
  "anyhow",
  "backoff",
@@ -5630,7 +5630,7 @@ dependencies = [
  "hyper-util",
  "protobuf",
  "pyth-lazer-protocol 0.8.1",
- "pyth-lazer-publisher-sdk 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pyth-lazer-publisher-sdk 0.1.7",
  "reqwest 0.12.22",
  "serde",
  "serde_json",
@@ -5659,7 +5659,7 @@ dependencies = [
  "futures-util",
  "hex",
  "libsecp256k1 0.7.2",
- "pyth-lazer-protocol 0.9.0",
+ "pyth-lazer-protocol 0.9.1",
  "serde",
  "serde_json",
  "tokio",
@@ -5687,7 +5687,7 @@ dependencies = [
 
 [[package]]
 name = "pyth-lazer-protocol"
-version = "0.9.0"
+version = "0.9.1"
 dependencies = [
  "alloy-primitives 0.8.25",
  "anyhow",
@@ -5698,6 +5698,7 @@ dependencies = [
  "derive_more 1.0.0",
  "ed25519-dalek 2.1.1",
  "hex",
+ "humantime",
  "humantime-serde",
  "itertools 0.13.0",
  "libsecp256k1 0.7.2",
@@ -5711,30 +5712,33 @@ dependencies = [
 [[package]]
 name = "pyth-lazer-publisher-sdk"
 version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8d52a515b21b77a89266d584da4363fcd1e121213ac3065ab7ff0dab1172006"
 dependencies = [
  "anyhow",
  "fs-err",
  "humantime",
  "protobuf",
  "protobuf-codegen",
- "pyth-lazer-protocol 0.9.0",
+ "pyth-lazer-protocol 0.8.1",
  "serde-value",
  "tracing",
 ]
 
 [[package]]
 name = "pyth-lazer-publisher-sdk"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8d52a515b21b77a89266d584da4363fcd1e121213ac3065ab7ff0dab1172006"
+version = "0.2.0"
 dependencies = [
  "anyhow",
+ "derive_more 2.0.1",
  "fs-err",
+ "hex",
  "humantime",
  "protobuf",
  "protobuf-codegen",
- "pyth-lazer-protocol 0.8.1",
- "serde-value",
+ "pyth-lazer-protocol 0.9.1",
+ "serde",
+ "serde_json",
  "tracing",
 ]
 

+ 1 - 1
apps/fortuna/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "fortuna"
-version = "8.2.1"
+version = "8.2.2"
 edition = "2021"
 
 [lib]

+ 13 - 3
apps/fortuna/src/eth_utils/utils.rs

@@ -196,7 +196,7 @@ pub async fn submit_tx_with_backoff<T: Middleware + NonceManaged + 'static>(
 
 #[allow(clippy::large_enum_variant)]
 pub enum SubmitTxError<T: Middleware + NonceManaged + 'static> {
-    GasUsageEstimateError(ContractError<T>),
+    GasUsageEstimateError(TypedTransaction, ContractError<T>),
     GasLimitExceeded { estimate: U256, limit: U256 },
     GasPriceEstimateError(<T as Middleware>::Error),
     SubmissionError(TypedTransaction, <T as Middleware>::Error),
@@ -211,8 +211,8 @@ where
 {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
-            SubmitTxError::GasUsageEstimateError(e) => {
-                write!(f, "Error estimating gas for reveal: {e:?}")
+            SubmitTxError::GasUsageEstimateError(tx, e) => {
+                write!(f, "Error estimating gas for reveal: Tx:{tx:?}, Error:{e:?}")
             }
             SubmitTxError::GasLimitExceeded { estimate, limit } => write!(
                 f,
@@ -247,7 +247,16 @@ pub async fn submit_tx<T: Middleware + NonceManaged + 'static>(
     // A value of 100 submits the tx with the same fee as the estimate.
     fee_estimate_multiplier_pct: u64,
 ) -> Result<TransactionReceipt, backoff::Error<SubmitTxError<T>>> {
+    // Estimate the gas *before* filling the transaction. Filling the transaction increments the nonce of the
+    // provider. If we can't send the transaction (because the gas estimation fails), then the nonce will be
+    // out of sync with the one on-chain, causing subsequent transactions to fail.
+    let gas: U256 = call.estimate_gas().await.map_err(|e| {
+        backoff::Error::transient(SubmitTxError::GasUsageEstimateError(call.tx.clone(), e))
+    })?;
+
     let mut transaction = call.tx.clone();
+    // Setting the gas here avoids a redundant call to estimate_gas within the Provider's fill_transaction method.
+    transaction.set_gas(gas);
 
     // manually fill the tx with the gas price info, so we can log the details in case of error
     client
@@ -258,6 +267,7 @@ pub async fn submit_tx<T: Middleware + NonceManaged + 'static>(
             if let Some(e) = e.as_error_response() {
                 if let Some(e) = e.as_revert_data() {
                     return backoff::Error::transient(SubmitTxError::GasUsageEstimateError(
+                        transaction.clone(),
                         ContractError::Revert(e.clone()),
                     ));
                 }

+ 7 - 5
apps/fortuna/src/keeper/process_event.rs

@@ -137,15 +137,17 @@ pub async fn process_event_with_backoff(
     );
     let error_mapper = |num_retries, e| {
         if let backoff::Error::Transient {
-            err: SubmitTxError::GasUsageEstimateError(ContractError::Revert(revert)),
+            err: SubmitTxError::GasUsageEstimateError(tx, ContractError::Revert(revert)),
             ..
         } = &e
         {
             if let Ok(PythRandomErrorsErrors::NoSuchRequest(_)) =
                 PythRandomErrorsErrors::decode(revert)
             {
-                let err =
-                    SubmitTxError::GasUsageEstimateError(ContractError::Revert(revert.clone()));
+                let err = SubmitTxError::GasUsageEstimateError(
+                    tx.clone(),
+                    ContractError::Revert(revert.clone()),
+                );
                 // Slow down the retries if the request is not found.
                 // This probably means that the request is already fulfilled via another process.
                 // After 5 retries, we return the error permanently.
@@ -255,13 +257,13 @@ pub async fn process_event_with_backoff(
                     .inc();
                 // Do not display the internal error, it might include RPC details.
                 let reason = match e {
-                    SubmitTxError::GasUsageEstimateError(ContractError::Revert(revert)) => {
+                    SubmitTxError::GasUsageEstimateError(_, ContractError::Revert(revert)) => {
                         format!("Reverted: {revert}")
                     }
                     SubmitTxError::GasLimitExceeded { limit, estimate } => {
                         format!("Gas limit exceeded: limit = {limit}, estimate = {estimate}")
                     }
-                    SubmitTxError::GasUsageEstimateError(_) => {
+                    SubmitTxError::GasUsageEstimateError(_, _) => {
                         "Unable to estimate gas usage".to_string()
                     }
                     SubmitTxError::GasPriceEstimateError(_) => {

+ 1 - 1
apps/hermes/server/Cargo.lock

@@ -1880,7 +1880,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
 [[package]]
 name = "hermes"
-version = "0.10.2-alpha"
+version = "0.10.3"
 dependencies = [
  "anyhow",
  "async-trait",

+ 1 - 1
apps/hermes/server/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name        = "hermes"
-version     = "0.10.2-alpha"
+version     = "0.10.3"
 description = "Hermes is an agent that provides Verified Prices from the Pythnet Pyth Oracle."
 edition     = "2021"
 

+ 1 - 1
apps/hermes/server/src/api/rest/v2/timestamp_price_updates.rs

@@ -101,7 +101,7 @@ where
     )
     .await
     .map_err(|e| {
-        tracing::warn!(
+        tracing::debug!(
             "Error getting price feeds {:?} with update data: {:?}",
             price_ids,
             e

+ 28 - 5
apps/hermes/server/src/api/types.rs

@@ -374,23 +374,28 @@ pub struct PriceFeedMetadata {
 #[serde(rename_all = "snake_case")]
 pub enum AssetType {
     Crypto,
-    #[serde(rename = "fx")]
-    FX,
+    Fx,
     Equity,
     Metal,
     Rates,
     CryptoRedemptionRate,
+    Commodities,
+    CryptoIndex,
+    CryptoNav,
 }
 
 impl Display for AssetType {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
         match self {
             AssetType::Crypto => write!(f, "crypto"),
-            AssetType::FX => write!(f, "fx"),
+            AssetType::Fx => write!(f, "fx"),
             AssetType::Equity => write!(f, "equity"),
             AssetType::Metal => write!(f, "metal"),
             AssetType::Rates => write!(f, "rates"),
             AssetType::CryptoRedemptionRate => write!(f, "crypto_redemption_rate"),
+            AssetType::Commodities => write!(f, "commodities"),
+            AssetType::CryptoIndex => write!(f, "crypto_index"),
+            AssetType::CryptoNav => write!(f, "crypto_nav"),
         }
     }
 }
@@ -409,8 +414,8 @@ mod tests {
                 .trim_matches('"')
         );
         assert_eq!(
-            AssetType::FX.to_string(),
-            serde_json::to_string(&AssetType::FX)
+            AssetType::Fx.to_string(),
+            serde_json::to_string(&AssetType::Fx)
                 .unwrap()
                 .trim_matches('"')
         );
@@ -438,5 +443,23 @@ mod tests {
                 .unwrap()
                 .trim_matches('"')
         );
+        assert_eq!(
+            AssetType::Commodities.to_string(),
+            serde_json::to_string(&AssetType::Commodities)
+                .unwrap()
+                .trim_matches('"')
+        );
+        assert_eq!(
+            AssetType::CryptoIndex.to_string(),
+            serde_json::to_string(&AssetType::CryptoIndex)
+                .unwrap()
+                .trim_matches('"')
+        );
+        assert_eq!(
+            AssetType::CryptoNav.to_string(),
+            serde_json::to_string(&AssetType::CryptoNav)
+                .unwrap()
+                .trim_matches('"')
+        );
     }
 }

+ 1 - 1
apps/pyth-lazer-agent/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "pyth-lazer-agent"
-version = "0.1.4"
+version = "0.3.0"
 edition = "2024"
 
 [dependencies]

+ 1 - 1
apps/pyth-lazer-agent/config/config.toml

@@ -1,4 +1,4 @@
-relayer_urls = ["ws://relayer-0.pyth-lazer.dourolabs.app/v1/transaction", "ws://relayer-0.pyth-lazer.dourolabs.app/v1/transaction"]
+relayer_urls = ["wss://relayer-0.pyth-lazer.dourolabs.app/v1/transaction", "wss://relayer-0.pyth-lazer.dourolabs.app/v1/transaction"]
 publish_keypair_path = "/path/to/solana/id.json"
 listen_address = "0.0.0.0:8910"
 publish_interval_duration = "25ms"

+ 1 - 1
apps/pyth-lazer-agent/src/http_server.rs

@@ -33,7 +33,7 @@ pub struct RelayerRequest(pub http::Request<Incoming>);
 
 const PUBLISHER_WS_URI_V1: &str = "/v1/publisher";
 const PUBLISHER_WS_URI_V2: &str = "/v2/publisher";
-const JRPC_WS_URI_V1: &str = "/v1/jprc";
+const JRPC_WS_URI_V1: &str = "/v1/jrpc";
 
 const READINESS_PROBE_PATH: &str = "/ready";
 const LIVENESS_PROBE_PATH: &str = "/live";

+ 1 - 5
apps/pyth-lazer-agent/src/relayer_session.rs

@@ -22,12 +22,8 @@ use url::Url;
 type RelayerWsSender = SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, TungsteniteMessage>;
 type RelayerWsReceiver = SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>;
 
-async fn connect_to_relayer(
-    mut url: Url,
-    token: &str,
-) -> Result<(RelayerWsSender, RelayerWsReceiver)> {
+async fn connect_to_relayer(url: Url, token: &str) -> Result<(RelayerWsSender, RelayerWsReceiver)> {
     tracing::info!("connecting to the relayer at {}", url);
-    url.set_path("/v1/transaction");
     let mut req = url.clone().into_client_request()?;
     let headers = req.headers_mut();
     headers.insert(

+ 1 - 1
apps/staking/src/components/Header/stats.tsx

@@ -12,7 +12,7 @@ import { Tokens } from "../Tokens";
 const ONE_SECOND_IN_MS = 1000;
 const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
 const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS;
-const INITIAL_REWARD_POOL_SIZE = 60_000_000_000_000n;
+const INITIAL_REWARD_POOL_SIZE = 100_000_010_000_000n;
 
 export const Stats = ({ className, ...props }: HTMLProps<HTMLDivElement>) => {
   const { connection } = useConnection();

+ 3 - 0
contract_manager/scripts/transfer_balance_entropy_chains.ts

@@ -160,6 +160,9 @@ async function transferOnChain(
       transferAmountEth = (balanceEth - gasCostEth) * transferRatio!;
     }
 
+    // Round to 10 decimal places to avoid Web3 conversion errors
+    transferAmountEth = Math.round(transferAmountEth * 1e10) / 1e10;
+
     // Validate transfer amount
     if (transferAmountEth <= 0) {
       console.log(

+ 7 - 0
contract_manager/store/chains/EvmChains.json

@@ -1272,5 +1272,12 @@
     "rpcUrl": "https://rpc-ethereal-testnet.t.conduit.xyz",
     "networkId": 657468,
     "type": "EvmChain"
+  },
+  {
+    "id": "camp_network",
+    "mainnet": true,
+    "rpcUrl": "https://rpc.camp.raas.gelato.cloud",
+    "networkId": 484,
+    "type": "EvmChain"
   }
 ]

+ 5 - 0
contract_manager/store/contracts/EvmPriceFeedContracts.json

@@ -848,5 +848,10 @@
     "chain": "injective_evm_testnet",
     "address": "0xDd24F84d36BF92C65F92307595335bdFab5Bbd21",
     "type": "EvmPriceFeedContract"
+  },
+  {
+    "chain": "camp_network",
+    "address": "0x2880aB155794e7179c9eE2e38200202908C17B43",
+    "type": "EvmPriceFeedContract"
   }
 ]

+ 5 - 0
contract_manager/store/contracts/EvmWormholeContracts.json

@@ -843,5 +843,10 @@
     "chain": "ethereal_testnet",
     "address": "0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E",
     "type": "EvmWormholeContract"
+  },
+  {
+    "chain": "camp_network",
+    "address": "0xb27e5ca259702f209a29225d0eDdC131039C9933",
+    "type": "EvmWormholeContract"
   }
 ]

+ 4 - 3
governance/remote_executor/programs/remote-executor/Cargo.toml

@@ -30,13 +30,14 @@ sonic_testnet = []
 atlas_testnet = []
 mantis_mainnet = []
 sonic_mainnet = []
+fogo_testnet = []
 
 
 [dependencies]
-anchor-lang = {version = "0.30.1", features = ["init-if-needed"]}
-wormhole-solana = { git = "https://github.com/guibescos/wormhole-solana", rev="f14b3b54c1e37e1aaf8c2ac2a5e236832ffdb3c2"}
+anchor-lang = { version = "0.30.1", features = ["init-if-needed"] }
+wormhole-solana = { git = "https://github.com/guibescos/wormhole-solana", rev = "f14b3b54c1e37e1aaf8c2ac2a5e236832ffdb3c2" }
 wormhole-sdk = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1" }
-serde_wormhole = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1"}
+serde_wormhole = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1" }
 boolinator = "2.4.0"
 
 [dev-dependencies]

+ 4 - 0
governance/remote_executor/programs/remote-executor/src/state/governance_payload.rs

@@ -19,6 +19,7 @@ pub const CHAIN_ID_ARRAY: &[(&str, u16)] = &[
     ("atlas_testnet", 40007),
     ("mantis_mainnet", 40008),
     ("sonic_mainnet", 40009),
+    ("fogo_testnet", 40010),
 ];
 
 #[cfg(any(feature = "pythnet", feature = "pythtest"))]
@@ -51,6 +52,9 @@ pub const CHAIN_ID: u16 = 40008;
 #[cfg(feature = "sonic_mainnet")]
 pub const CHAIN_ID: u16 = 40009;
 
+#[cfg(feature = "fogo_testnet")]
+pub const CHAIN_ID: u16 = 40010;
+
 #[derive(AnchorDeserialize, AnchorSerialize, Debug, PartialEq, Eq)]
 pub struct ExecutorPayload {
     pub header: GovernanceHeader,

+ 1 - 0
governance/remote_executor/programs/remote-executor/src/state/posted_vaa.rs

@@ -26,6 +26,7 @@ impl Owner for AnchorVaa {
         feature = "atlas_testnet",
         feature = "mantis_mainnet",
         feature = "sonic_mainnet",
+        feature = "fogo_testnet",
     ))]
     fn owner() -> Pubkey {
         Pubkey::from_str("HDwcJBJXjL9FpJ7UBsYBtaDjsBUhuLCUYoz3zr8SWWaQ").unwrap()

+ 2 - 0
governance/xc_admin/packages/xc_admin_common/src/chains.ts

@@ -33,6 +33,7 @@ export const RECEIVER_CHAINS = {
   atlas_testnet: 40007,
   mantis_mainnet: 40008,
   sonic_mainnet: 40009,
+  fogo_testnet: 40010,
 
   cronos: 60001,
   kcc: 60002,
@@ -113,6 +114,7 @@ export const RECEIVER_CHAINS = {
   swellchain: 60083,
   mezo: 60084,
   iota_sui_mainnet: 60085,
+  camp_network: 60086,
 
   // Testnets as a separate chain ids (to use stable data sources and governance for them)
   injective_testnet: 60013,

+ 1 - 1
lazer/contracts/solana/programs/pyth-lazer-solana-contract/Cargo.toml

@@ -22,7 +22,7 @@ no-log-ix-name = []
 idl-build = ["anchor-lang/idl-build"]
 
 [dependencies]
-pyth-lazer-protocol = { path = "../../../../sdk/rust/protocol", version = "0.9.0" }
+pyth-lazer-protocol = { path = "../../../../sdk/rust/protocol", version = "0.9.1" }
 
 anchor-lang = "0.30.1"
 bytemuck = "1.20.0"

+ 1 - 1
lazer/publisher_sdk/proto/dynamic_value.proto

@@ -3,7 +3,7 @@ syntax = "proto3";
 import "google/protobuf/timestamp.proto";
 import "google/protobuf/duration.proto";
 
-package pyth_lazer_transaction;
+package pyth_lazer;
 
 // A dynamically typed value similar to `google.protobuf.Value`
 // but supporting more types.

+ 77 - 34
lazer/publisher_sdk/proto/governance_instruction.proto

@@ -5,11 +5,12 @@ import "google/protobuf/duration.proto";
 import "google/protobuf/empty.proto";
 
 import "dynamic_value.proto";
+import "state.proto";
 
 // If any field documented as `[required]` is not present in the instruction,
 // the instruction will be rejected.
 
-package pyth_lazer_transaction;
+package pyth_lazer;
 
 // Representation of a complete governance instruction. This value will be signed
 // by a governance source.
@@ -130,9 +131,10 @@ message Permissions {
     enum UpdateFeedAction {
         // Required by protobuf. Instruction will be rejected if this value is encountered.
         UPDATE_FEED_ACTION_UNSPECIFIED = 0;
-        UPDATE_FEED_METADATA = 101;
-        ACTIVATE_FEED = 102;
-        DEACTIVATE_FEED = 103;
+        UPDATE_FEED_PROPERTIES = 101;
+        UPDATE_FEED_METADATA = 102;
+        ENABLE_FEED_IN_SHARD = 103;
+        DISABLE_FEED_IN_SHARD = 104;
         REMOVE_FEED = 199;
     }
 
@@ -302,17 +304,33 @@ message SetPublisherActive {
     optional bool is_active = 1;
 }
 
-// Feed is inactive when added, meaning that it will be available to publishers but not to consumers.
+// Add a new feed. Refer to `Feed` message fields documentation.
 message AddFeed {
-    // [required] ID of the feed. Must be unique (within the shard).
+    // [required]
     optional uint32 feed_id = 1;
-    // [required] Feed metadata. Some properties are required (name, exponent, etc.).
-    // Known properties must have the expected type.
-    // Additional arbitrary properties are allowed.
-    // (TODO: document known metadata properties)
-    optional DynamicValue.Map metadata = 2;
-    // IDs of publishers enabled for this feed.
-    repeated uint32 permissioned_publishers = 3;
+    // [required]
+    optional DynamicValue.Map metadata = 3;
+    // [required]
+    optional string name = 101;
+    // [required]
+    optional sint32 exponent = 102;
+    // [required]
+    optional uint32 min_publishers = 103;
+    // [required]
+    optional google.protobuf.Duration min_rate = 104;
+    // [required]
+    optional google.protobuf.Duration expiry_time = 105;
+    // [required]
+    optional string market_schedule = 106;
+    // [required]
+    optional FeedState state = 107;
+    // [required]
+    optional FeedKind kind = 108;
+    // [required]
+    optional bool is_enabled_in_shard = 201;
+
+    // TODO: IDs of publishers enabled for this feed.
+    // repeated uint32 permissioned_publishers = 3;
 }
 
 message UpdateFeed {
@@ -321,13 +339,38 @@ message UpdateFeed {
     // [required]
     // Note: when adding a new variant here, update `Permissions` as well.
     oneof action {
-        UpdateFeedMetadata update_feed_metadata = 101;
-        ActivateFeed activate_feed = 102;
-        DeactivateFeed deactivate_feed = 103;
+        UpdateFeedProperties update_feed_properties = 101;
+        UpdateFeedMetadata update_feed_metadata = 102;
+        EnableFeedInShard enable_feed_in_shard = 103;
+        DisableFeedInShard disable_feed_in_shard = 104;
         google.protobuf.Empty remove_feed = 199;
     }
 }
 
+// Update a feed's properties. The feed will be updated with values present in each field.
+// If a value is not supplied, the corresponding property will remain unchanged.
+// Refer to `Feed` message fields documentation.
+message UpdateFeedProperties {
+    // [optional]
+    optional DynamicValue.Map metadata = 3;
+    // [optional]
+    optional string name = 101;
+    // [optional]
+    optional sint32 exponent = 102;
+    // [optional]
+    optional uint32 min_publishers = 103;
+    // [optional]
+    optional google.protobuf.Duration min_rate = 104;
+    // [optional]
+    optional google.protobuf.Duration expiry_time = 105;
+    // [optional]
+    optional string market_schedule = 106;
+    // [optional]
+    optional FeedState state = 107;
+    // [optional]
+    optional bool is_enabled_in_shard = 201;
+}
+
 message UpdateFeedMetadata {
     // [required] Property name.
     optional string name = 1;
@@ -335,29 +378,29 @@ message UpdateFeedMetadata {
     optional DynamicValue value = 2;
 }
 
-// Set the feed as active or shedule an activation.
-// If there was already a pending activation or deactivation, it will be cleared
+// Set the feed as enabled in this shard or shedule it for a certain timestamp.
+// If there was already a pending status change, it will be cleared
 // when this governance instruction is processed.
-// Warning: there must never be two feeds with the same name active at the same time
+// Warning: there must never be two feeds with the same name enabled at the same time
 // within a shard group. This cannot be enforced within a shard. When a feed needs to be
-// moved between shards, use `activation_timestamp` and `deactivation_timestamp`
-// to deactivate it in the old shard and activate it in the new shard at the same time.
-message ActivateFeed {
-    // [optional] If provided, the feed will activate at the specified timestamp.
-    // If `activation_timestamp` is already passed or if it's unset,
-    // the feed will be activated immediately when this
+// moved between shards, use `enable_in_shard_timestamp` and `disable_in_shard_timestamp`
+// to disable it in the old shard and enable it in the new shard at the same time.
+message EnableFeedInShard {
+    // [optional] If provided, the feed will be enabled at the specified timestamp.
+    // If `enable_in_shard_timestamp` is already passed or if it's unset,
+    // the feed will be enabled immediately when this
     // governance instruction is processed.
-    optional google.protobuf.Timestamp activation_timestamp = 1;
+    optional google.protobuf.Timestamp enable_in_shard_timestamp = 1;
 }
 
-// Set the feed as inactive or shedule a deactivation.
-// If there was already a pending activation or deactivation, it will be cleared
+// Set the feed as disabled in this shard or shedule it for a certain timestamp.
+// If there was already a pending status change, it will be cleared
 // when this governance instruction is processed.
-// See also: `ActivateFeed` docs.
-message DeactivateFeed {
-    // [optional] If provided, the feed will deactivate at the specified timestamp.
-    // If `deactivation_timestamp` is already passed or if it's unset,
-    // the feed will be deactivated immediately when this
+// See also: `EnableFeedInShard` docs.
+message DisableFeedInShard {
+    // [optional] If provided, the feed will be disabled at the specified timestamp.
+    // If `disable_in_shard_timestamp` is already passed or if it's unset,
+    // the feed will be disabled immediately when this
     // governance instruction is processed.
-    optional google.protobuf.Timestamp deactivation_timestamp = 1;
+    optional google.protobuf.Timestamp disable_in_shard_timestamp = 1;
 }

+ 2 - 2
lazer/publisher_sdk/proto/publisher_update.proto

@@ -1,5 +1,5 @@
 syntax = "proto3";
-package pyth_lazer_transaction;
+package pyth_lazer;
 
 import "google/protobuf/timestamp.proto";
 
@@ -28,7 +28,7 @@ message FeedUpdate {
     // [required] timestamp when this data was first acquired or generated
     optional google.protobuf.Timestamp source_timestamp = 2;
 
-    // [required] one type of update containing specific data 
+    // [required] one type of update containing specific data
     oneof update {
       PriceUpdate price_update = 3;
       FundingRateUpdate funding_rate_update = 4;

+ 1 - 1
lazer/publisher_sdk/proto/pyth_lazer_transaction.proto

@@ -1,5 +1,5 @@
 syntax = "proto3";
-package pyth_lazer_transaction;
+package pyth_lazer;
 
 import "publisher_update.proto";
 import "governance_instruction.proto";

+ 58 - 47
lazer/publisher_sdk/proto/state.proto

@@ -1,9 +1,11 @@
 syntax = "proto3";
-package lazer;
+package pyth_lazer;
 
 import "google/protobuf/duration.proto";
 import "google/protobuf/timestamp.proto";
 
+import "dynamic_value.proto";
+
 // All optional fields should always be set unless documented otherwise.
 
 // State of a Pyth Lazer shard.
@@ -45,67 +47,76 @@ message Publisher {
 }
 
 enum FeedState {
-    COMING_SOON = 0; // Default value
+    // Default value. Feeds in this state are not available to consumers.
+    // `COMING_SOON` feeds are expected to become stable in the future.
+    COMING_SOON = 0;
+    // A fully available feed.
     STABLE = 1;
+    // Inactive feeds are not available to consumers.
+    // `INACTIVE` feeds are not expected to become stable again.
     INACTIVE = 2;
 }
 
-// Static data for a feed.
-message FeedMetadata {
-    // [required] ID of the price feed.
-    optional uint32 price_feed_id = 1;
-    // [required] Feed name.
-    optional string name = 2;
-    // [required] Feed symbol.
-    optional string symbol = 3;
-    // [required] Feed description.
-    optional string description = 4;
-    // [required] Feed asset type.
-    optional string asset_type = 5;
+// Feed kind determines the set of data fields available in the feed.
+// It also determines the kind of data accepted from publishers for this feed
+// (`PriceUpdate` or `FundingRateUpdate`).
+enum FeedKind {
+    // Fields: price, best_bid_price, best_ask_price
+    PRICE = 0;
+    // Fields: price, rate.
+    FUNDING_RATE = 1;
+}
+
+// An item of the state describing a feed.
+message Feed {
+    // [required] ID of the feed.
+    optional uint32 feed_id = 1;
+    // Additional state per publisher.
+    // If an eligible publisher is not listed here, the corresponding state should be considered empty.
+    repeated FeedPublisherState per_publisher = 2;
+    // [required] Additional metadata values. These values will be exposed in the APIs, but
+    // are not directly used in the aggregator.
+    optional DynamicValue.Map metadata = 3;
+
+    // [required] A readable feed name. It must be unique across all feeds in the shard.
+    // Used for logs, metrics, feed search API, TradingView API.
+    optional string name = 101;
     // [required] Exponent applied to all price and rate values for this feed.
     // Actual value is `mantissa * 10 ^ exponent`.
     // Restricted to int16.
-    optional sint32 exponent = 6;
-    // [optional] CoinMarketCap ID. Can be absent if there is no CoinMarketCap ID for this symbol.
-    optional uint32 cmc_id = 7;
-    // [optional] Funding rate interval. Only present for funding rate feeds.
-    optional google.protobuf.Duration funding_rate_interval = 8;
+    optional sint32 exponent = 102;
     // [required] Minimal number of publisher prices required to produce an aggregate.
-    optional uint32 min_publishers = 9;
+    optional uint32 min_publishers = 103;
     // [required] Minimal rate of aggregation performed by the aggregator for this feed.
     // Cannot be lower than the shard's top level `State.min_rate`.
-    optional google.protobuf.Duration min_rate = 10;
+    optional google.protobuf.Duration min_rate = 104;
     // [required] Time after which the publisher update is discarded.
-    optional google.protobuf.Duration expiry_time = 11;
-    // [required] If true, the feed is visible to the consumers. This can be used to prepare and verify
-    // new feeds before releasing them. This can also be used to migrate a feed from
-    // one shard to another. If a feed is present in
+    optional google.protobuf.Duration expiry_time = 105;
+    // [required] Market schedule in Pythnet format.
+    optional string market_schedule = 106;
+    // [required] Feed state.
+    optional FeedState state = 107;
+    // [required] Feed kind.
+    optional FeedKind kind = 108;
+
+
+    // [required] Feed status in the current shard. Disabled feeds will not be visible in
+    // the consumer API for the current shard. This setting should only be used
+    // to migrate a feed from one shard to another.
+    //
+    // If a feed is present in
     // multiple shards, it must only be active in one of them at each time.
-    // To enforce this, `pending_activation` and `pending_deactivation` fields
+    // To enforce this, `enable_in_shard_timestamp` and `disable_in_shard_timestamp` fields
     // can be used to deactivate a feed in one shard and activate it in another shard
     // at the same instant.
-    optional bool is_activated = 12;
-    // [optional] ID of the corresponding price feed in Hermes (Pythnet).
-    optional string hermes_id = 13;
-    // [optional] Quote currency of the asset.
-    optional string quote_currency = 14;
-    // [optional] Market schedule in Pythnet format.
-    // If absent, the default schedule is used (market is always open).
-    optional string market_schedule = 15;
-    // [required] Feed state
-    optional FeedState state = 16;
-}
+    optional bool is_enabled_in_shard = 201;
+    // [optional] If present, the aggregator will enable the feed in the current shard
+    // at the specified instant.
+    optional google.protobuf.Timestamp enable_in_shard_timestamp = 202;
+    // [optional] If present, the aggregator will disable the feed in the current shard
+    // at the specified instant.
+    optional google.protobuf.Timestamp disable_in_shard_timestamp = 203;
 
-// An item of the state describing a feed.
-message Feed {
-    optional FeedMetadata metadata = 1;
-    // [optional] If present, the aggregator will activate the feed at the specified instant.
-    optional google.protobuf.Timestamp pending_activation = 2;
-    // [optional] If present, the aggregator will deactivate the feed at the specified instant.
-    optional google.protobuf.Timestamp pending_deactivation = 3;
-    // Additional state per publisher.
-    // If an eligible publisher is not listed here, the corresponding state should be considered empty.
-    repeated FeedPublisherState per_publisher = 4;
     // TODO: list of permissioned publisher IDs.
 }
 

+ 1 - 1
lazer/publisher_sdk/proto/transaction_envelope.proto

@@ -1,5 +1,5 @@
 syntax = "proto3";
-package pyth_lazer_transaction;
+package pyth_lazer;
 
 import "google/protobuf/timestamp.proto";
 import "pyth_lazer_transaction.proto";

+ 6 - 3
lazer/publisher_sdk/rust/Cargo.toml

@@ -1,18 +1,21 @@
 [package]
 name = "pyth-lazer-publisher-sdk"
-version = "0.1.7"
+version = "0.2.0"
 edition = "2021"
 description = "Pyth Lazer Publisher SDK types."
 license = "Apache-2.0"
 repository = "https://github.com/pyth-network/pyth-crosschain"
 
 [dependencies]
-pyth-lazer-protocol = { version = "0.9.0", path = "../../sdk/rust/protocol" }
+pyth-lazer-protocol = { version = "0.9.1", path = "../../sdk/rust/protocol" }
 anyhow = "1.0.98"
 protobuf = "3.7.2"
-serde-value = "0.7.0"
 humantime = "2.2.0"
 tracing = "0.1.41"
+serde = { version = "1.0.219", features = ["derive"] }
+serde_json = "1.0.140"
+derive_more = { version = "2.0.1", features = ["from"] }
+hex = "0.4.3"
 
 [build-dependencies]
 fs-err = "3.1.0"

+ 178 - 0
lazer/publisher_sdk/rust/src/convert_dynamic_value.rs

@@ -0,0 +1,178 @@
+#[cfg(test)]
+mod tests;
+
+use std::collections::BTreeMap;
+
+use crate::protobuf::dynamic_value::{dynamic_value, DynamicValue as ProtobufDynamicValue};
+use ::protobuf::MessageField;
+use anyhow::{ensure, Context};
+use pyth_lazer_protocol::{
+    time::{DurationUs, TimestampUs},
+    DynamicValue,
+};
+
+impl From<DynamicValue> for ProtobufDynamicValue {
+    fn from(value: DynamicValue) -> Self {
+        let converted = match value {
+            DynamicValue::Bool(value) => dynamic_value::Value::BoolValue(value),
+            DynamicValue::U64(value) => dynamic_value::Value::UintValue(value),
+            DynamicValue::I64(value) => dynamic_value::Value::IntValue(value),
+            DynamicValue::F64(value) => dynamic_value::Value::DoubleValue(value),
+            DynamicValue::String(value) => dynamic_value::Value::StringValue(value),
+            DynamicValue::Bytes(value) => dynamic_value::Value::BytesValue(value),
+            DynamicValue::Timestamp(value) => dynamic_value::Value::TimestampValue(value.into()),
+            DynamicValue::Duration(value) => dynamic_value::Value::DurationValue(value.into()),
+            DynamicValue::List(values) => dynamic_value::Value::List(values.into()),
+            DynamicValue::Map(values) => dynamic_value::Value::Map(values.into()),
+        };
+        ProtobufDynamicValue {
+            value: Some(converted),
+            special_fields: Default::default(),
+        }
+    }
+}
+
+impl From<&DynamicValue> for ProtobufDynamicValue {
+    fn from(value: &DynamicValue) -> Self {
+        let converted = match value {
+            DynamicValue::Bool(value) => dynamic_value::Value::BoolValue(*value),
+            DynamicValue::U64(value) => dynamic_value::Value::UintValue(*value),
+            DynamicValue::I64(value) => dynamic_value::Value::IntValue(*value),
+            DynamicValue::F64(value) => dynamic_value::Value::DoubleValue(*value),
+            DynamicValue::String(value) => dynamic_value::Value::StringValue(value.clone()),
+            DynamicValue::Bytes(value) => dynamic_value::Value::BytesValue(value.clone()),
+            DynamicValue::Timestamp(value) => dynamic_value::Value::TimestampValue((*value).into()),
+            DynamicValue::Duration(value) => dynamic_value::Value::DurationValue((*value).into()),
+            DynamicValue::List(values) => dynamic_value::Value::List(values.into()),
+            DynamicValue::Map(values) => dynamic_value::Value::Map(values.into()),
+        };
+        ProtobufDynamicValue {
+            value: Some(converted),
+            special_fields: Default::default(),
+        }
+    }
+}
+
+impl From<BTreeMap<String, DynamicValue>> for dynamic_value::Map {
+    fn from(values: BTreeMap<String, DynamicValue>) -> Self {
+        let mut items = Vec::new();
+        for (key, value) in values {
+            items.push(dynamic_value::MapItem {
+                key: Some(key),
+                value: MessageField::some(value.into()),
+                special_fields: Default::default(),
+            })
+        }
+        dynamic_value::Map {
+            items,
+            special_fields: Default::default(),
+        }
+    }
+}
+
+impl From<&BTreeMap<String, DynamicValue>> for dynamic_value::Map {
+    fn from(values: &BTreeMap<String, DynamicValue>) -> Self {
+        let mut items = Vec::new();
+        for (key, value) in values {
+            items.push(dynamic_value::MapItem {
+                key: Some(key.clone()),
+                value: MessageField::some(value.into()),
+                special_fields: Default::default(),
+            })
+        }
+        dynamic_value::Map {
+            items,
+            special_fields: Default::default(),
+        }
+    }
+}
+
+impl From<Vec<DynamicValue>> for dynamic_value::List {
+    fn from(values: Vec<DynamicValue>) -> Self {
+        let mut items = Vec::new();
+        for value in values {
+            items.push(value.into());
+        }
+        dynamic_value::List {
+            items,
+            special_fields: Default::default(),
+        }
+    }
+}
+
+impl From<&[DynamicValue]> for dynamic_value::List {
+    fn from(values: &[DynamicValue]) -> Self {
+        let mut items = Vec::new();
+        for value in values {
+            items.push(value.into());
+        }
+        dynamic_value::List {
+            items,
+            special_fields: Default::default(),
+        }
+    }
+}
+
+impl From<&Vec<DynamicValue>> for dynamic_value::List {
+    fn from(value: &Vec<DynamicValue>) -> Self {
+        let value: &[DynamicValue] = value;
+        value.into()
+    }
+}
+
+impl TryFrom<ProtobufDynamicValue> for DynamicValue {
+    type Error = anyhow::Error;
+
+    fn try_from(value: ProtobufDynamicValue) -> Result<Self, Self::Error> {
+        let value = value.value.context("missing DynamicValue.value")?;
+        match value {
+            dynamic_value::Value::StringValue(value) => Ok(DynamicValue::String(value)),
+            dynamic_value::Value::DoubleValue(value) => Ok(DynamicValue::F64(value)),
+            dynamic_value::Value::UintValue(value) => Ok(DynamicValue::U64(value)),
+            dynamic_value::Value::IntValue(value) => Ok(DynamicValue::I64(value)),
+            dynamic_value::Value::BoolValue(value) => Ok(DynamicValue::Bool(value)),
+            dynamic_value::Value::BytesValue(value) => Ok(DynamicValue::Bytes(value)),
+            dynamic_value::Value::DurationValue(value) => {
+                let v: DurationUs = value.try_into()?;
+                Ok(DynamicValue::Duration(v))
+            }
+            dynamic_value::Value::TimestampValue(ts) => {
+                let ts = TimestampUs::try_from(&ts)?;
+                Ok(DynamicValue::Timestamp(ts))
+            }
+            dynamic_value::Value::List(list) => Ok(DynamicValue::List(list.try_into()?)),
+            dynamic_value::Value::Map(map) => Ok(DynamicValue::Map(map.try_into()?)),
+        }
+    }
+}
+
+impl TryFrom<dynamic_value::Map> for BTreeMap<String, DynamicValue> {
+    type Error = anyhow::Error;
+
+    fn try_from(value: dynamic_value::Map) -> Result<Self, Self::Error> {
+        let mut output = BTreeMap::new();
+        for item in value.items {
+            let key = item.key.context("missing DynamicValue.MapItem.key")?;
+            let value = item
+                .value
+                .into_option()
+                .context("missing DynamicValue.MapItem.value")?
+                .try_into()?;
+            let old = output.insert(key, value);
+            ensure!(old.is_none(), "duplicate DynamicValue.MapItem.key");
+        }
+        Ok(output)
+    }
+}
+
+impl TryFrom<dynamic_value::List> for Vec<DynamicValue> {
+    type Error = anyhow::Error;
+
+    fn try_from(value: dynamic_value::List) -> Result<Self, Self::Error> {
+        let mut output = Vec::new();
+        for item in value.items {
+            output.push(item.try_into()?);
+        }
+        Ok(output)
+    }
+}

+ 145 - 0
lazer/publisher_sdk/rust/src/convert_dynamic_value/tests.rs

@@ -0,0 +1,145 @@
+use std::collections::BTreeMap;
+
+use protobuf::{
+    well_known_types::{duration::Duration, timestamp::Timestamp},
+    MessageField,
+};
+use pyth_lazer_protocol::DynamicValue;
+
+use crate::protobuf::dynamic_value::{
+    dynamic_value::{List, Map, MapItem, Value},
+    DynamicValue as ProtobufDynamicValue,
+};
+
+#[test]
+fn dynamic_value_serializes() {
+    let mut map = BTreeMap::new();
+    map.insert(
+        "int1".to_owned(),
+        ProtobufDynamicValue {
+            value: Some(Value::IntValue(42)),
+            special_fields: Default::default(),
+        },
+    );
+
+    map.insert(
+        "bool2".to_owned(),
+        ProtobufDynamicValue {
+            value: Some(Value::BoolValue(true)),
+            special_fields: Default::default(),
+        },
+    );
+
+    map.insert(
+        "str3".to_owned(),
+        ProtobufDynamicValue {
+            value: Some(Value::StringValue("abc".into())),
+            special_fields: Default::default(),
+        },
+    );
+
+    map.insert(
+        "double4".to_owned(),
+        ProtobufDynamicValue {
+            value: Some(Value::DoubleValue(42.0)),
+            special_fields: Default::default(),
+        },
+    );
+
+    map.insert(
+        "uint5".to_owned(),
+        ProtobufDynamicValue {
+            value: Some(Value::UintValue(42)),
+            special_fields: Default::default(),
+        },
+    );
+
+    map.insert(
+        "bytes6".to_owned(),
+        ProtobufDynamicValue {
+            value: Some(Value::BytesValue(b"\xAB\xCD\xEF".into())),
+            special_fields: Default::default(),
+        },
+    );
+
+    map.insert(
+        "duration7".to_owned(),
+        ProtobufDynamicValue {
+            value: Some(Value::DurationValue(Duration {
+                seconds: 12,
+                nanos: 345678000,
+                special_fields: Default::default(),
+            })),
+            special_fields: Default::default(),
+        },
+    );
+
+    map.insert(
+        "timestamp8".to_owned(),
+        ProtobufDynamicValue {
+            value: Some(Value::TimestampValue(Timestamp {
+                seconds: 12,
+                nanos: 345678000,
+                special_fields: Default::default(),
+            })),
+            special_fields: Default::default(),
+        },
+    );
+
+    map.insert(
+        "list9".to_owned(),
+        ProtobufDynamicValue {
+            value: Some(Value::List(List {
+                items: vec![
+                    ProtobufDynamicValue {
+                        value: Some(Value::StringValue("item1".into())),
+                        special_fields: Default::default(),
+                    },
+                    ProtobufDynamicValue {
+                        value: Some(Value::StringValue("item2".into())),
+                        special_fields: Default::default(),
+                    },
+                ],
+                special_fields: Default::default(),
+            })),
+            special_fields: Default::default(),
+        },
+    );
+    let map = Map {
+        items: map
+            .into_iter()
+            .map(|(k, v)| MapItem {
+                key: Some(k),
+                value: MessageField::some(v),
+                special_fields: Default::default(),
+            })
+            .collect(),
+        special_fields: Default::default(),
+    };
+
+    let converted: BTreeMap<String, DynamicValue> = map.clone().try_into().unwrap();
+
+    let json = serde_json::to_string_pretty(&converted).unwrap();
+    println!("{json}");
+    assert_eq!(
+        json,
+        r#"{
+  "bool2": true,
+  "bytes6": "abcdef",
+  "double4": 42.0,
+  "duration7": "12s 345ms 678us",
+  "int1": 42,
+  "list9": [
+    "item1",
+    "item2"
+  ],
+  "str3": "abc",
+  "timestamp8": 12345678,
+  "uint5": 42
+}"#
+    );
+
+    // Check roundtrip
+    let reversed: Map = converted.into();
+    assert_eq!(map, reversed);
+}

+ 46 - 140
lazer/publisher_sdk/rust/src/lib.rs

@@ -1,13 +1,9 @@
-use std::{collections::BTreeMap, time::Duration};
-
 use crate::publisher_update::feed_update::Update;
 use crate::publisher_update::{FeedUpdate, FundingRateUpdate, PriceUpdate};
-use ::protobuf::MessageField;
-use anyhow::{bail, ensure, Context};
-use humantime::format_duration;
-use protobuf::dynamic_value::{dynamic_value, DynamicValue};
+use crate::state::FeedState;
 use pyth_lazer_protocol::jrpc::{FeedUpdateParams, UpdateParams};
-use pyth_lazer_protocol::time::TimestampUs;
+use pyth_lazer_protocol::symbol_state::SymbolState;
+use pyth_lazer_protocol::FeedKind;
 
 pub mod transaction_envelope {
     pub use crate::protobuf::transaction_envelope::*;
@@ -29,144 +25,16 @@ pub mod state {
     pub use crate::protobuf::state::*;
 }
 
+pub mod dynamic_value {
+    pub use crate::protobuf::dynamic_value::*;
+}
+
 #[allow(rustdoc::broken_intra_doc_links)]
 mod protobuf {
     include!(concat!(env!("OUT_DIR"), "/protobuf/mod.rs"));
 }
 
-impl DynamicValue {
-    pub fn try_option_from_serde(value: serde_value::Value) -> anyhow::Result<Option<Self>> {
-        match value {
-            serde_value::Value::Option(value) => {
-                if let Some(value) = value {
-                    Ok(Some((*value).try_into()?))
-                } else {
-                    Ok(None)
-                }
-            }
-            value => Ok(Some(value.try_into()?)),
-        }
-    }
-
-    pub fn to_timestamp(&self) -> anyhow::Result<TimestampUs> {
-        let value = self.value.as_ref().context("missing DynamicValue.value")?;
-        match value {
-            dynamic_value::Value::TimestampValue(ts) => Ok(ts.try_into()?),
-            _ => bail!("expected timestamp, got {:?}", self),
-        }
-    }
-
-    pub fn to_duration(&self) -> anyhow::Result<Duration> {
-        let value = self.value.as_ref().context("missing DynamicValue.value")?;
-        match value {
-            dynamic_value::Value::DurationValue(duration) => Ok(duration.clone().into()),
-            _ => bail!("expected duration, got {:?}", self),
-        }
-    }
-}
-
-impl TryFrom<serde_value::Value> for DynamicValue {
-    type Error = anyhow::Error;
-
-    fn try_from(value: serde_value::Value) -> Result<Self, Self::Error> {
-        let converted = match value {
-            serde_value::Value::Bool(value) => dynamic_value::Value::BoolValue(value),
-            serde_value::Value::U8(value) => dynamic_value::Value::UintValue(value.into()),
-            serde_value::Value::U16(value) => dynamic_value::Value::UintValue(value.into()),
-            serde_value::Value::U32(value) => dynamic_value::Value::UintValue(value.into()),
-            serde_value::Value::U64(value) => dynamic_value::Value::UintValue(value),
-            serde_value::Value::I8(value) => dynamic_value::Value::IntValue(value.into()),
-            serde_value::Value::I16(value) => dynamic_value::Value::IntValue(value.into()),
-            serde_value::Value::I32(value) => dynamic_value::Value::IntValue(value.into()),
-            serde_value::Value::I64(value) => dynamic_value::Value::IntValue(value),
-            serde_value::Value::F32(value) => dynamic_value::Value::DoubleValue(value.into()),
-            serde_value::Value::F64(value) => dynamic_value::Value::DoubleValue(value),
-            serde_value::Value::Char(value) => dynamic_value::Value::StringValue(value.to_string()),
-            serde_value::Value::String(value) => dynamic_value::Value::StringValue(value),
-            serde_value::Value::Bytes(value) => dynamic_value::Value::BytesValue(value),
-            serde_value::Value::Seq(values) => {
-                let mut items = Vec::new();
-                for value in values {
-                    items.push(value.try_into()?);
-                }
-                dynamic_value::Value::List(dynamic_value::List {
-                    items,
-                    special_fields: Default::default(),
-                })
-            }
-            serde_value::Value::Map(values) => {
-                let mut items = Vec::new();
-                for (key, value) in values {
-                    let key = match key {
-                        serde_value::Value::String(key) => key,
-                        _ => bail!("unsupported key type: expected string, got {:?}", key),
-                    };
-                    items.push(dynamic_value::MapItem {
-                        key: Some(key),
-                        value: MessageField::some(value.try_into()?),
-                        special_fields: Default::default(),
-                    })
-                }
-                dynamic_value::Value::Map(dynamic_value::Map {
-                    items,
-                    special_fields: Default::default(),
-                })
-            }
-            serde_value::Value::Unit
-            | serde_value::Value::Option(_)
-            | serde_value::Value::Newtype(_) => bail!("unsupported type: {:?}", value),
-        };
-        Ok(DynamicValue {
-            value: Some(converted),
-            special_fields: Default::default(),
-        })
-    }
-}
-
-impl TryFrom<DynamicValue> for serde_value::Value {
-    type Error = anyhow::Error;
-
-    fn try_from(value: DynamicValue) -> Result<Self, Self::Error> {
-        let value = value.value.context("missing DynamicValue.value")?;
-        match value {
-            dynamic_value::Value::StringValue(value) => Ok(serde_value::Value::String(value)),
-            dynamic_value::Value::DoubleValue(value) => Ok(serde_value::Value::F64(value)),
-            dynamic_value::Value::UintValue(value) => Ok(serde_value::Value::U64(value)),
-            dynamic_value::Value::IntValue(value) => Ok(serde_value::Value::I64(value)),
-            dynamic_value::Value::BoolValue(value) => Ok(serde_value::Value::Bool(value)),
-            dynamic_value::Value::BytesValue(value) => Ok(serde_value::Value::Bytes(value)),
-            dynamic_value::Value::DurationValue(duration) => {
-                let s: Duration = duration.into();
-                Ok(serde_value::Value::String(format_duration(s).to_string()))
-            }
-            dynamic_value::Value::TimestampValue(ts) => {
-                let ts = TimestampUs::try_from(&ts)?;
-                Ok(serde_value::Value::U64(ts.as_micros()))
-            }
-            dynamic_value::Value::List(list) => {
-                let mut output = Vec::new();
-                for item in list.items {
-                    output.push(item.try_into()?);
-                }
-                Ok(serde_value::Value::Seq(output))
-            }
-            dynamic_value::Value::Map(map) => {
-                let mut output = BTreeMap::new();
-                for item in map.items {
-                    let key = item.key.context("missing DynamicValue.MapItem.key")?;
-                    let value = item
-                        .value
-                        .into_option()
-                        .context("missing DynamicValue.MapItem.value")?
-                        .try_into()?;
-                    let old = output.insert(serde_value::Value::String(key), value);
-                    ensure!(old.is_none(), "duplicate DynamicValue.MapItem.key");
-                }
-                Ok(serde_value::Value::Map(output))
-            }
-        }
-    }
-}
+mod convert_dynamic_value;
 
 impl From<FeedUpdateParams> for FeedUpdate {
     fn from(value: FeedUpdateParams) -> Self {
@@ -202,3 +70,41 @@ impl From<UpdateParams> for Update {
         }
     }
 }
+
+impl From<FeedState> for SymbolState {
+    fn from(value: FeedState) -> Self {
+        match value {
+            FeedState::COMING_SOON => SymbolState::ComingSoon,
+            FeedState::STABLE => SymbolState::Stable,
+            FeedState::INACTIVE => SymbolState::Inactive,
+        }
+    }
+}
+
+impl From<SymbolState> for FeedState {
+    fn from(value: SymbolState) -> Self {
+        match value {
+            SymbolState::ComingSoon => FeedState::COMING_SOON,
+            SymbolState::Stable => FeedState::STABLE,
+            SymbolState::Inactive => FeedState::INACTIVE,
+        }
+    }
+}
+
+impl From<FeedKind> for protobuf::state::FeedKind {
+    fn from(value: FeedKind) -> Self {
+        match value {
+            FeedKind::Price => protobuf::state::FeedKind::PRICE,
+            FeedKind::FundingRate => protobuf::state::FeedKind::FUNDING_RATE,
+        }
+    }
+}
+
+impl From<protobuf::state::FeedKind> for FeedKind {
+    fn from(value: protobuf::state::FeedKind) -> Self {
+        match value {
+            protobuf::state::FeedKind::PRICE => FeedKind::Price,
+            protobuf::state::FeedKind::FUNDING_RATE => FeedKind::FundingRate,
+        }
+    }
+}

+ 1 - 1
lazer/sdk/rust/client/Cargo.toml

@@ -6,7 +6,7 @@ description = "A Rust client for Pyth Lazer"
 license = "Apache-2.0"
 
 [dependencies]
-pyth-lazer-protocol = { path = "../protocol", version = "0.9.0" }
+pyth-lazer-protocol = { path = "../protocol", version = "0.9.1" }
 tokio = { version = "1", features = ["full"] }
 tokio-tungstenite = { version = "0.20", features = ["native-tls"] }
 futures-util = "0.3"

+ 4 - 2
lazer/sdk/rust/protocol/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "pyth-lazer-protocol"
-version = "0.9.0"
+version = "0.9.1"
 edition = "2021"
 description = "Pyth Lazer SDK - protocol types."
 license = "Apache-2.0"
@@ -11,13 +11,15 @@ byteorder = "1.5.0"
 anyhow = "1.0.89"
 serde = { version = "1.0.210", features = ["derive"] }
 serde_json = "1.0"
-derive_more = { version = "1.0.0", features = ["from"] }
+derive_more = { version = "1.0.0", features = ["from", "into"] }
 itertools = "0.13.0"
 rust_decimal = "1.36.0"
 protobuf = "3.7.2"
 humantime-serde = "1.1.1"
 mry = { version = "0.13.0", features = ["serde"], optional = true }
 chrono = "0.4.41"
+humantime = "2.2.0"
+hex = "0.4.3"
 
 [dev-dependencies]
 bincode = "1.3.3"

+ 56 - 0
lazer/sdk/rust/protocol/src/dynamic_value.rs

@@ -0,0 +1,56 @@
+use std::collections::BTreeMap;
+
+use crate::time::{DurationUs, TimestampUs};
+use derive_more::From;
+use serde::{
+    ser::{SerializeMap, SerializeSeq},
+    Serialize,
+};
+
+#[derive(Debug, Clone, PartialEq, From)]
+pub enum DynamicValue {
+    String(String),
+    F64(f64),
+    U64(u64),
+    I64(i64),
+    Bool(bool),
+    Timestamp(TimestampUs),
+    Duration(DurationUs),
+    Bytes(Vec<u8>),
+    List(Vec<DynamicValue>),
+    Map(BTreeMap<String, DynamicValue>),
+}
+
+impl Serialize for DynamicValue {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        match self {
+            DynamicValue::String(v) => serializer.serialize_str(v),
+            DynamicValue::F64(v) => serializer.serialize_f64(*v),
+            DynamicValue::U64(v) => serializer.serialize_u64(*v),
+            DynamicValue::I64(v) => serializer.serialize_i64(*v),
+            DynamicValue::Bool(v) => serializer.serialize_bool(*v),
+            DynamicValue::Timestamp(v) => serializer.serialize_u64(v.as_micros()),
+            DynamicValue::Duration(v) => {
+                serializer.serialize_str(&humantime::format_duration((*v).into()).to_string())
+            }
+            DynamicValue::Bytes(v) => serializer.serialize_str(&hex::encode(v)),
+            DynamicValue::List(v) => {
+                let mut seq_serializer = serializer.serialize_seq(Some(v.len()))?;
+                for element in v {
+                    seq_serializer.serialize_element(element)?;
+                }
+                seq_serializer.end()
+            }
+            DynamicValue::Map(map) => {
+                let mut map_serializer = serializer.serialize_map(Some(map.len()))?;
+                for (k, v) in map {
+                    map_serializer.serialize_entry(k, v)?;
+                }
+                map_serializer.end()
+            }
+        }
+    }
+}

+ 20 - 0
lazer/sdk/rust/protocol/src/feed_kind.rs

@@ -0,0 +1,20 @@
+use {
+    serde::{Deserialize, Serialize},
+    std::fmt::Display,
+};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum FeedKind {
+    Price,
+    FundingRate,
+}
+
+impl Display for FeedKind {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            FeedKind::Price => write!(f, "price"),
+            FeedKind::FundingRate => write!(f, "fundingRate"),
+        }
+    }
+}

+ 4 - 0
lazer/sdk/rust/protocol/src/lib.rs

@@ -2,6 +2,8 @@
 
 pub mod api;
 pub mod binary_update;
+mod dynamic_value;
+mod feed_kind;
 pub mod jrpc;
 pub mod message;
 pub mod payload;
@@ -13,6 +15,8 @@ pub mod subscription;
 pub mod symbol_state;
 pub mod time;
 
+pub use crate::{dynamic_value::DynamicValue, feed_kind::FeedKind};
+
 #[test]
 fn magics_in_big_endian() {
     use crate::{

+ 10 - 4
lazer/sdk/rust/protocol/src/router.rs

@@ -6,7 +6,7 @@ use {
         time::{DurationUs, TimestampUs},
     },
     anyhow::{bail, Context},
-    derive_more::derive::From,
+    derive_more::derive::{From, Into},
     itertools::Itertools,
     protobuf::well_known_types::duration::Duration as ProtobufDuration,
     rust_decimal::{prelude::FromPrimitive, Decimal},
@@ -18,13 +18,19 @@ use {
     },
 };
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[derive(
+    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, From, Into,
+)]
 pub struct PublisherId(pub u16);
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[derive(
+    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, From, Into,
+)]
 pub struct PriceFeedId(pub u32);
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[derive(
+    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, From, Into,
+)]
 pub struct ChannelId(pub u8);
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]

+ 1 - 2
target_chains/ethereum/sdk/solidity/IPyth.sol

@@ -105,8 +105,7 @@ interface IPyth is IPythEvents {
     /// within `minPublishTime` and `maxPublishTime`.
     ///
     /// You can use this method if you want to use a Pyth price at a fixed time and not the most recent price;
-    /// otherwise, please consider using `updatePriceFeeds`. This method may store the price updates on-chain, if they
-    /// are more recent than the current stored prices.
+    /// otherwise, please consider using `updatePriceFeeds`. This method will not store the price updates on-chain.
     ///
     /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling
     /// `getUpdateFee` with the length of the `updateData` array.

+ 89 - 10
target_chains/fuel/contracts/Cargo.lock

@@ -1,6 +1,6 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
+version = 4
 
 [[package]]
 name = "Inflector"
@@ -62,6 +62,17 @@ dependencies = [
  "subtle",
 ]
 
+[[package]]
+name = "ahash"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
+dependencies = [
+ "getrandom",
+ "once_cell",
+ "version_check",
+]
+
 [[package]]
 name = "ahash"
 version = "0.8.11"
@@ -599,29 +610,63 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "borsh"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa"
+dependencies = [
+ "borsh-derive 0.9.3",
+ "hashbrown 0.11.2",
+]
+
 [[package]]
 name = "borsh"
 version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b"
 dependencies = [
- "borsh-derive",
+ "borsh-derive 0.10.3",
  "hashbrown 0.13.2",
 ]
 
+[[package]]
+name = "borsh-derive"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775"
+dependencies = [
+ "borsh-derive-internal 0.9.3",
+ "borsh-schema-derive-internal 0.9.3",
+ "proc-macro-crate 0.1.5",
+ "proc-macro2",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "borsh-derive"
 version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7"
 dependencies = [
- "borsh-derive-internal",
- "borsh-schema-derive-internal",
+ "borsh-derive-internal 0.10.3",
+ "borsh-schema-derive-internal 0.10.3",
  "proc-macro-crate 0.1.5",
  "proc-macro2",
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "borsh-derive-internal"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "borsh-derive-internal"
 version = "0.10.3"
@@ -633,6 +678,17 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "borsh-schema-derive-internal"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "borsh-schema-derive-internal"
 version = "0.10.3"
@@ -2717,6 +2773,15 @@ dependencies = [
  "byteorder",
 ]
 
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+dependencies = [
+ "ahash 0.7.8",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.12.3"
@@ -2729,7 +2794,7 @@ version = "0.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
 dependencies = [
- "ahash",
+ "ahash 0.8.11",
 ]
 
 [[package]]
@@ -2738,7 +2803,7 @@ version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
 dependencies = [
- "ahash",
+ "ahash 0.8.11",
  "allocator-api2",
  "serde",
 ]
@@ -4762,6 +4827,19 @@ dependencies = [
  "psl-types",
 ]
 
+[[package]]
+name = "pyth-sdk"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5c805ba3dfb5b7ed6a8ffa62ec38391f485a79c7cf6b3b11d3bd44fb0325824"
+dependencies = [
+ "borsh 0.9.3",
+ "borsh-derive 0.9.3",
+ "hex",
+ "schemars",
+ "serde",
+]
+
 [[package]]
 name = "pyth_sdk"
 version = "0.1.0"
@@ -4784,15 +4862,16 @@ dependencies = [
 
 [[package]]
 name = "pythnet-sdk"
-version = "2.3.0"
+version = "2.3.1"
 dependencies = [
  "bincode",
- "borsh",
+ "borsh 0.10.3",
  "bytemuck",
  "byteorder",
  "fast-math",
  "hex",
  "libsecp256k1",
+ "pyth-sdk",
  "rand",
  "rustc_version",
  "serde",
@@ -6559,7 +6638,7 @@ version = "0.212.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8d28bc49ba1e5c5b61ffa7a2eace10820443c4b7d1c0b144109261d14570fdf8"
 dependencies = [
- "ahash",
+ "ahash 0.8.11",
  "bitflags 2.6.0",
  "hashbrown 0.14.5",
  "indexmap 2.4.0",
@@ -7261,4 +7340,4 @@ checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
 dependencies = [
  "cc",
  "pkg-config",
-]
+]

+ 14 - 24
target_chains/fuel/contracts/Forc.lock

@@ -1,44 +1,34 @@
 [[package]]
-name = "core"
-source = "path+from-root-8357A6DDC5F39D14"
+name = "ownership"
+version = "0.26.0"
+source = "registry+ownership?0.26.0#QmbVoNUrvCTyQTdE8ZP83XxTQQVzhrevfMqk1rAJAkMFVo!"
+dependencies = [
+    "src5",
+    "std",
+]
 
 [[package]]
 name = "pyth-contract"
 source = "member"
 dependencies = [
+    "ownership",
     "pyth_interface",
-    "standards git+https://github.com/FuelLabs/sway-standards?tag=v0.4.4#a001d3c248595112aae67e5633a06ef9bc0536ae",
+    "src5",
     "std",
-    "sway_libs",
 ]
 
 [[package]]
 name = "pyth_interface"
 source = "path+from-root-555D3D27A908977B"
-dependencies = [
-    "standards git+https://github.com/FuelLabs/sway-standards?tag=v0.4.4#a001d3c248595112aae67e5633a06ef9bc0536ae",
-    "std",
-]
-
-[[package]]
-name = "standards"
-source = "git+https://github.com/FuelLabs/sway-standards?tag=v0.4.3#6f63eb7dff2458a7d976184e565b5cbf26f61da2"
 dependencies = ["std"]
 
 [[package]]
-name = "standards"
-source = "git+https://github.com/FuelLabs/sway-standards?tag=v0.4.4#a001d3c248595112aae67e5633a06ef9bc0536ae"
+name = "src5"
+version = "0.8.0"
+source = "registry+src5?0.8.0#QmNRPZrPHFBiEAyWPU8gesdPsD2zb3cMKwEgxJwV1ZEjyD!"
 dependencies = ["std"]
 
 [[package]]
 name = "std"
-source = "git+https://github.com/fuellabs/sway?tag=v0.65.2#66bb430395daf5b8f7205f7b9d8d008e2e812d54"
-dependencies = ["core"]
-
-[[package]]
-name = "sway_libs"
-source = "git+https://github.com/FuelLabs/sway-libs?tag=v0.21.0#6a227ed34c86fe1ebd334dbdfeccf66c43e3915b"
-dependencies = [
-    "standards git+https://github.com/FuelLabs/sway-standards?tag=v0.4.3#6f63eb7dff2458a7d976184e565b5cbf26f61da2",
-    "std",
-]
+version = "0.68.9"
+source = "registry+std?0.68.9#QmUaBxMs2JvY1bXgRCdeCsG3o6TN82ftRgv4Tq7ytqUGUT!"

+ 3 - 3
target_chains/fuel/contracts/fuel-toolchain.toml

@@ -1,6 +1,6 @@
 [toolchain]
-channel = "latest-aarch64-apple-darwin"
+channel = "mainnet"
 
 [components]
-forc = "0.65.2"
-fuel-core = "0.37.1"
+forc = "0.68.9"
+fuel-core = "0.43.2"

+ 2 - 2
target_chains/fuel/contracts/pyth-contract/Forc.toml

@@ -5,6 +5,6 @@ license = "Apache-2.0"
 name = "pyth-contract"
 
 [dependencies]
-sway_libs = { git = "https://github.com/FuelLabs/sway-libs", tag = "v0.21.0" }
 pyth_interface = { path = "../pyth-interface" }
-standards = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.4.4" }
+ownership = "0.26.0"
+src5 = "0.8.0"

+ 6 - 6
target_chains/fuel/contracts/pyth-contract/src/main.sw

@@ -53,12 +53,12 @@ use pyth_interface::{
     WormholeGuardians,
 };
 
-use sway_libs::ownership::*;
-use standards::src5::{SRC5, State};
+use ownership::*;
+use src5::{SRC5, State};
 
 const GUARDIAN_SET_EXPIRATION_TIME_SECONDS: u64 = 86400; // 24 hours in seconds
 configurable {
-    DEPLOYER: Identity = Identity::Address(Address::from(ZERO_B256)),
+    DEPLOYER: Identity = Identity::Address(Address::from(b256::zero())),
 }
 
 storage {
@@ -209,7 +209,7 @@ impl PythCore for Contract {
                     while i_2 < number_of_attestations {
                         let (_, slice) = vm.payload.split_at(attestation_index + 32);
                         let (price_feed_id, _) = slice.split_at(32);
-                        let price_feed_id: PriceFeedId = price_feed_id.into();
+                        let price_feed_id: PriceFeedId = b256::from_be_bytes(price_feed_id.clone());
 
                         if price_feed_id.is_target(target_price_feed_ids) == false {
                             attestation_index += attestation_size_u16;
@@ -374,7 +374,7 @@ fn update_fee(update_data: Vec<Bytes>) -> u64 {
     total_fee(total_number_of_updates, storage.single_update_fee)
 }
 
-#[storage(read, write), payable]
+#[storage(read, write)]
 fn update_price_feeds(update_data: Vec<Bytes>) {
     require(
         msg_asset_id() == AssetId::base(),
@@ -934,4 +934,4 @@ fn verify_governance_vm(encoded_vm: Bytes) -> WormholeVM {
 
     set_last_executed_governance_sequence(vm.sequence);
     vm
-}
+}

+ 0 - 3
target_chains/fuel/contracts/pyth-interface/Forc.toml

@@ -3,6 +3,3 @@ authors = ["Fuel Labs <contact@fuel.sh>"]
 entry = "interface.sw"
 license = "Apache-2.0"
 name = "pyth_interface"
-
-[dependencies]
-standards = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.4.4" }

+ 1 - 1
target_chains/fuel/contracts/pyth-interface/src/data_structures/governance_instruction.sw

@@ -139,7 +139,7 @@ impl GovernanceInstruction {
             index += 2;
             let (_, slice) = encoded_payload.split_at(index);
             let (slice, _) = slice.split_at(32);
-            let emitter_address: b256 = slice.into();
+            let emitter_address: b256 = b256::from_be_bytes(slice.clone());
             index += 32;
 
             data_sources.push(DataSource {

+ 4 - 4
target_chains/fuel/contracts/pyth-interface/src/data_structures/price.sw

@@ -1,6 +1,6 @@
 library;
 
-use std::{bytes::Bytes, block::timestamp};
+use std::{bytes::Bytes, block::timestamp, bytes_conversions::b256::*};
 
 use ::errors::PythError;
 use ::utils::absolute_of_exponent;
@@ -99,7 +99,7 @@ impl PriceFeed {
         let mut offset = 1u64;
         let (_, slice) = encoded_price_feed.split_at(offset);
         let (price_feed_id, _) = slice.split_at(32);
-        let price_feed_id: PriceFeedId = price_feed_id.into();
+        let price_feed_id: PriceFeedId = b256::from_be_bytes(price_feed_id.clone());
         offset += 32;
         let price = u64::from_be_bytes([
             encoded_price_feed.get(offset).unwrap(),
@@ -186,7 +186,7 @@ impl PriceFeed {
         let mut attestation_index = index + 32;
         let (_, slice) = encoded_payload.split_at(attestation_index);
         let (price_feed_id, _) = slice.split_at(32);
-        let price_feed_id: PriceFeedId = price_feed_id.into();
+        let price_feed_id: PriceFeedId = b256::from_be_bytes(price_feed_id.clone());
         attestation_index += 32;
         let mut price = u64::from_be_bytes([
             encoded_payload.get(attestation_index).unwrap(),
@@ -321,7 +321,7 @@ impl PriceFeed {
 }
 
 impl PriceFeed {
-    pub fn extract_from_merkle_proof(digest: Bytes, encoded_proof: Bytes, offset: u64) -> (u64, self) {
+    pub fn extract_from_merkle_proof(digest: Bytes, encoded_proof: Bytes, offset: u64) -> (u64, Self) {
         // In order to avoid `ref mut` param related MemoryWriteOverlap error
         let mut current_offset = offset;
 

+ 7 - 6
target_chains/fuel/contracts/pyth-interface/src/data_structures/wormhole_light.sw

@@ -12,6 +12,7 @@ use std::{
     b512::B512,
     block::timestamp,
     bytes::Bytes,
+    bytes_conversions::b256::*,
     constants::ZERO_B256,
     hash::{
         Hash,
@@ -85,7 +86,7 @@ impl GuardianSetUpgrade {
         let mut index = 0;
         let (_, slice) = encoded_upgrade.split_at(index);
         let (module, _) = slice.split_at(32);
-        let module: b256 = module.into();
+        let module: b256 = b256::from_be_bytes(module.clone());
         require(module == UPGRADE_MODULE, WormholeError::InvalidModule);
         index += 32;
         let action = encoded_upgrade.get(index).unwrap();
@@ -114,7 +115,7 @@ impl GuardianSetUpgrade {
         while i < guardian_length {
             let (_, slice) = encoded_upgrade.split_at(index);
             let (key, _) = slice.split_at(20);
-            let key: b256 = key.into();
+            let key: b256 = b256::from_be_bytes(key.clone());
             new_guardian_set.keys.push(key.rsh(96));
             index += 20;
             i += 1;
@@ -352,10 +353,10 @@ impl WormholeVM {
             index += 1;
             let (_, slice) = encoded_vm.split_at(index);
             let (slice, remainder) = slice.split_at(32);
-            let r: b256 = slice.into();
+            let r: b256 = b256::from_be_bytes(slice.clone());
             index += 32;
             let (slice, remainder) = remainder.split_at(32);
-            let s: b256 = slice.into();
+            let s: b256 = b256::from_be_bytes(slice.clone());
             index += 32;
             let v = remainder.get(0);
             require(v.is_some(), WormholeError::SignatureVIrretrievable);
@@ -408,7 +409,7 @@ impl WormholeVM {
         index += 2;
         let (_, slice) = encoded_vm.split_at(index);
         let (slice, _) = slice.split_at(32);
-        let emitter_address: b256 = slice.into();
+        let emitter_address: b256 = b256::from_be_bytes(slice.clone());
         index += 32;
         let (_, slice) = encoded_vm.split_at(index);
         let (slice, _) = slice.split_at(8);
@@ -511,7 +512,7 @@ impl WormholeVM {
         index += 2;
         let (_, slice) = encoded_vm.split_at(index);
         let (slice, _) = slice.split_at(32);
-        let emitter_address: b256 = slice.into();
+        let emitter_address: b256 = b256::from_be_bytes(slice.clone());
         index += 32;
         let (_, slice) = encoded_vm.split_at(index);
         let (slice, _) = slice.split_at(8);

+ 6 - 5
target_chains/fuel/contracts/pyth-interface/src/pyth_merkle_proof.sw

@@ -1,6 +1,6 @@
 library;
 
-use std::{bytes::Bytes, hash::{Hash, keccak256}};
+use std::{bytes::Bytes, hash::{Hash, keccak256}, bytes_conversions::b256::*};
 use ::errors::PythError;
 
 pub const MERKLE_LEAF_PREFIX = 0u8;
@@ -20,8 +20,9 @@ fn node_hash(child_a: Bytes, child_b: Bytes) -> Bytes {
     let mut bytes = Bytes::with_capacity(41);
     bytes.push(MERKLE_NODE_PREFIX);
 
-    let a: b256 = child_a.into();
-    let b: b256 = child_b.into();
+    let a: b256 = b256::from_be_bytes(child_a.clone());
+    let b: b256 = b256::from_be_bytes(child_b.clone());
+
     if a > b {
         bytes.append(child_b);
         bytes.append(child_a);
@@ -54,8 +55,8 @@ pub fn validate_proof(
         i += 1;
     }
 
-    let current_digest_b256: b256 = current_digest.into();
-    let root_b256: b256 = root.into();
+    let current_digest_b256: b256 = b256::from_be_bytes(current_digest.clone());
+    let root_b256: b256 = b256::from_be_bytes(root.clone());
 
     require(current_digest_b256 == root_b256, PythError::InvalidProof);
 

+ 1 - 1
target_chains/solana/sdk/js/pyth_solana_receiver/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/pyth-solana-receiver",
-  "version": "0.10.1",
+  "version": "0.10.2",
   "description": "Pyth solana receiver SDK",
   "homepage": "https://pyth.network",
   "main": "lib/index.js",

+ 33 - 29
target_chains/solana/sdk/js/pyth_solana_receiver/src/vaa.ts

@@ -133,35 +133,39 @@ async function generateVaaInstructionGroups(
 
   // Second write and verify instructions
   const writeSecondPartAndVerifyInstructions: InstructionWithEphemeralSigners[] =
-    [
-      {
-        instruction: await wormhole.methods
-          .writeEncodedVaa({
-            index: VAA_SPLIT_INDEX,
-            data: vaa.subarray(VAA_SPLIT_INDEX),
-          })
-          .accounts({
-            draftVaa: encodedVaaKeypair.publicKey,
-          })
-          .instruction(),
-        signers: [],
-        computeUnits: WRITE_ENCODED_VAA_COMPUTE_BUDGET,
-      },
-      {
-        instruction: await wormhole.methods
-          .verifyEncodedVaaV1()
-          .accounts({
-            guardianSet: getGuardianSetPda(
-              getGuardianSetIndex(vaa),
-              wormhole.programId,
-            ),
-            draftVaa: encodedVaaKeypair.publicKey,
-          })
-          .instruction(),
-        signers: [],
-        computeUnits: VERIFY_ENCODED_VAA_COMPUTE_BUDGET,
-      },
-    ];
+    [];
+
+  // The second write instruction is only needed if there are more bytes past the split index in the VAA
+  if (vaa.length > VAA_SPLIT_INDEX) {
+    writeSecondPartAndVerifyInstructions.push({
+      instruction: await wormhole.methods
+        .writeEncodedVaa({
+          index: VAA_SPLIT_INDEX,
+          data: vaa.subarray(VAA_SPLIT_INDEX),
+        })
+        .accounts({
+          draftVaa: encodedVaaKeypair.publicKey,
+        })
+        .instruction(),
+      signers: [],
+      computeUnits: WRITE_ENCODED_VAA_COMPUTE_BUDGET,
+    });
+  }
+
+  writeSecondPartAndVerifyInstructions.push({
+    instruction: await wormhole.methods
+      .verifyEncodedVaaV1()
+      .accounts({
+        guardianSet: getGuardianSetPda(
+          getGuardianSetIndex(vaa),
+          wormhole.programId,
+        ),
+        draftVaa: encodedVaaKeypair.publicKey,
+      })
+      .instruction(),
+    signers: [],
+    computeUnits: VERIFY_ENCODED_VAA_COMPUTE_BUDGET,
+  });
 
   // Close instructions
   const closeInstructions: InstructionWithEphemeralSigners[] = [

+ 10 - 3
target_chains/stylus/contracts/pyth-receiver/src/lib.rs

@@ -474,13 +474,20 @@ impl PythReceiver {
                                 }
                             }
 
+                            let expo = I32::try_from(price_feed_message.exponent)
+                                .map_err(|_| PythReceiverError::InvalidUpdateData)?;
+                            let price = I64::try_from(price_feed_message.price)
+                                .map_err(|_| PythReceiverError::InvalidUpdateData)?;
+                            let ema_price = I64::try_from(price_feed_message.ema_price)
+                                .map_err(|_| PythReceiverError::InvalidUpdateData)?;
+
                             let price_info_return = (
                                 price_id_fb,
                                 U64::from(publish_time),
-                                I32::from_be_bytes(price_feed_message.exponent.to_be_bytes()),
-                                I64::from_be_bytes(price_feed_message.price.to_be_bytes()),
+                                expo,
+                                price,
                                 U64::from(price_feed_message.conf),
-                                I64::from_be_bytes(price_feed_message.ema_price.to_be_bytes()),
+                                ema_price,
                                 U64::from(price_feed_message.ema_conf),
                             );