diff --git a/CHANGELOG.md b/CHANGELOG.md index b0d3aaecc..d8a34157a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features -- program: add oracle id for wen ([#1129](https://github.com/drift-labs/protocol-v2/pull/1129)) - program: track fuel ([#1048](https://github.com/drift-labs/protocol-v2/pull/1048)) +- program: add post multi pyth oracle updates atomic ([#1133](https://github.com/drift-labs/protocol-v2/pull/1133)) - program: track fuel for if staking ([#1127](https://github.com/drift-labs/protocol-v2/pull/1127)) - program: validate fee structure ([#1075](https://github.com/drift-labs/protocol-v2/pull/1075)) - program: check 5 min oracle twap divergence in trigger order ([#1116](https://github.com/drift-labs/protocol-v2/pull/1116)) @@ -123,13 +123,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixes - program: set default ContractTier to HighlySpeculative ([#1013](https://github.com/drift-labs/protocol-v2/pull/1013)) -- program: avoid dust borrows not being transferred +- program: avoid dust borrows not being transferred ### Breaking ## [2.77.0] - 2024-04-13 ### Features + - program: lax funding rate update oracle validity criteria ([#1009](https://github.com/drift-labs/protocol-v2/pull/1009)) ### Fixes @@ -157,10 +158,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features - program: add fee adjustment to spot market ([#987](https://github.com/drift-labs/protocol-v2/pull/987)) -- program: allow multiple makers to be passed into for spot fills ([#946](https://github.com/drift-labs/protocol-v2/pull/946)) -- ts-sdk: add fn to get admin ix ([#980](https://github.com/drift-labs/protocol-v2/pull/980)) -- program: add invariant check boolean for attempt settle revenue to insurance ([#937](https://github.com/drift-labs/protocol-v2/pull/937)) -- program: improve best bid/ask estimate in mark twap update ([#975](https://github.com/drift-labs/protocol-v2/pull/975)) +- program: allow multiple makers to be passed into for spot fills ([#946](https://github.com/drift-labs/protocol-v2/pull/946)) +- ts-sdk: add fn to get admin ix ([#980](https://github.com/drift-labs/protocol-v2/pull/980)) +- program: add invariant check boolean for attempt settle revenue to insurance ([#937](https://github.com/drift-labs/protocol-v2/pull/937)) +- program: improve best bid/ask estimate in mark twap update ([#975](https://github.com/drift-labs/protocol-v2/pull/975)) - program: add optional margin calculations for drift-rs ([#978](https://github.com/drift-labs/protocol-v2/pull/978)) ### Fixes @@ -171,12 +172,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features -- program: add 'highly speculative' contract tier enum 4 ([#968](https://github.com/drift-labs/protocol-v2/pull/968)) -- program: expand initialize market parameters ([#969](https://github.com/drift-labs/protocol-v2/pull/969)) +- program: add 'highly speculative' contract tier enum 4 ([#968](https://github.com/drift-labs/protocol-v2/pull/968)) +- program: expand initialize market parameters ([#969](https://github.com/drift-labs/protocol-v2/pull/969)) ### Fixes -- program: fix checking isolated tier in add_perp_lp_shares ([#965](https://github.com/drift-labs/protocol-v2/pull/965)) +- program: fix checking isolated tier in add_perp_lp_shares ([#965](https://github.com/drift-labs/protocol-v2/pull/965)) ### Breaking diff --git a/Cargo.lock b/Cargo.lock index b101b6dcf..59c3c71d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1690,7 +1690,7 @@ checksum = "44de48029c54ec1ca570786b5baeb906b0fc2409c8e0145585e287ee7a526c72" [[package]] name = "pyth-solana-receiver-sdk" version = "0.3.0" -source = "git+https://github.com/drift-labs/pyth-crosschain?rev=04cf2c#04cf2c01642a2ce0a0f620652034755d3e81d576" +source = "git+https://github.com/drift-labs/pyth-crosschain?rev=3e8a24ecd0bcf22b787313e2020f4186bb22c729#3e8a24ecd0bcf22b787313e2020f4186bb22c729" dependencies = [ "anchor-lang", "hex", @@ -1701,7 +1701,7 @@ dependencies = [ [[package]] name = "pythnet-sdk" version = "2.1.0" -source = "git+https://github.com/drift-labs/pyth-crosschain?rev=04cf2c#04cf2c01642a2ce0a0f620652034755d3e81d576" +source = "git+https://github.com/drift-labs/pyth-crosschain?rev=3e8a24ecd0bcf22b787313e2020f4186bb22c729#3e8a24ecd0bcf22b787313e2020f4186bb22c729" dependencies = [ "anchor-lang", "bincode", diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index bbbe4708b..88a73b389 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -22,8 +22,8 @@ anchor-lang = "0.29.0" solana-program = "1.16" anchor-spl = "0.29.0" pyth-client = "0.2.2" -pythnet-sdk = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "04cf2c"} -pyth-solana-receiver-sdk = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "04cf2c"} +pythnet-sdk = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "3e8a24ecd0bcf22b787313e2020f4186bb22c729"} +pyth-solana-receiver-sdk = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "3e8a24ecd0bcf22b787313e2020f4186bb22c729"} bytemuck = { version = "1.4.0" } borsh = "0.10.3" num-traits = "0.2" diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index 9917460fc..c8ed01214 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -557,6 +557,12 @@ pub enum ErrorCode { OracleWrongWriteAuthority, #[msg("Oracle vaa owner must be wormhole program")] OracleWrongVaaOwner, + #[msg("Multi updates must have 2 or fewer accounts passed in remaining accounts")] + OracleTooManyPriceAccountUpdates, + #[msg("Don't have the same remaining accounts number and merkle price updates left")] + OracleMismatchedVaaAndPriceUpdates, + #[msg("Remaining account passed is not a valid pda")] + OracleBadRemainingAccountPublicKey, } #[macro_export] diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 5188e43a0..b46181654 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -3826,7 +3826,7 @@ pub fn handle_initialize_pyth_pull_oracle( let signer_seeds = &[&seeds[..]]; let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds); - pyth_solana_receiver_sdk::cpi::init_price_update(cpi_context)?; + pyth_solana_receiver_sdk::cpi::init_price_update(cpi_context, feed_id)?; Ok(()) } diff --git a/programs/drift/src/instructions/pyth_pull_oracle.rs b/programs/drift/src/instructions/pyth_pull_oracle.rs index 1c13626cf..a2e6f2f8f 100644 --- a/programs/drift/src/instructions/pyth_pull_oracle.rs +++ b/programs/drift/src/instructions/pyth_pull_oracle.rs @@ -1,11 +1,12 @@ use crate::error::ErrorCode; use crate::ids::{drift_oracle_receiver_program, wormhole_program}; +use crate::validate; use anchor_lang::prelude::*; use pyth_solana_receiver_sdk::{ cpi::accounts::{PostUpdate, PostUpdateAtomic}, price_update::PriceUpdateV2, program::PythSolanaReceiver, - PostUpdateAtomicParams, PostUpdateParams, + PostMultiUpdatesAtomicParams, PostUpdateAtomicParams, PostUpdateParams, }; use pythnet_sdk::{ messages::Message, @@ -51,7 +52,7 @@ pub fn handle_update_pyth_pull_oracle( let price_feed_account = PriceUpdateV2::try_deserialize(&mut &price_feed_account_data[..])?; - require!( + validate!( price_feed_account.price_message.feed_id == feed_id, ErrorCode::OraclePriceFeedMessageMismatch ); @@ -96,7 +97,7 @@ pub fn handle_post_pyth_pull_oracle_update_atomic( let price_feed_account = PriceUpdateV2::try_deserialize(&mut &price_feed_account_data[..])?; - require!( + validate!( price_feed_account.price_message.feed_id == feed_id, ErrorCode::OraclePriceFeedMessageMismatch ); @@ -105,6 +106,75 @@ pub fn handle_post_pyth_pull_oracle_update_atomic( Ok(()) } +pub fn handle_post_multi_pyth_pull_oracle_updates_atomic<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, PostPythPullMultiOracleUpdatesAtomic<'info>>, + params: Vec, +) -> Result<()> { + let remaining_accounts = ctx.remaining_accounts; + validate!( + remaining_accounts.len() <= 2, + ErrorCode::OracleTooManyPriceAccountUpdates + ); + let update_param = PostMultiUpdatesAtomicParams::deserialize(&mut ¶ms[..]).unwrap(); + let vaa = update_param.vaa; + let merkle_price_updates = update_param.merkle_price_updates; + + validate!( + remaining_accounts.len() == merkle_price_updates.len(), + ErrorCode::OracleMismatchedVaaAndPriceUpdates + ); + + for (account, merkle_price_update) in remaining_accounts.iter().zip(merkle_price_updates.iter()) + { + let cpi_program = ctx.accounts.pyth_solana_receiver.to_account_info().clone(); + let cpi_accounts = PostUpdateAtomic { + payer: ctx.accounts.keeper.to_account_info().clone(), + guardian_set: ctx.accounts.guardian_set.to_account_info().clone(), + price_update_account: account.clone(), + write_authority: account.clone(), + }; + + let price_feed_account_data = account.try_borrow_data()?; + let price_feed_account = PriceUpdateV2::try_deserialize(&mut &price_feed_account_data[..])?; + let feed_id = price_feed_account.price_message.feed_id; + + // Verify the pda + let (pda, bump) = Pubkey::find_program_address( + &[PTYH_PRICE_FEED_SEED_PREFIX, feed_id.as_ref()], + &crate::ID, + ); + require_keys_eq!( + *account.key, + pda, + ErrorCode::OracleBadRemainingAccountPublicKey + ); + + let seeds = &[PTYH_PRICE_FEED_SEED_PREFIX, feed_id.as_ref(), &[bump]]; + + let signer_seeds = &[&seeds[..]]; + let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds); + + // Get the timestamp of the price currently stored in the price feed account. + let current_timestamp = get_timestamp_from_price_feed_account(account)?; + let next_timestamp = get_timestamp_from_price_update_message(&merkle_price_update.message)?; + + drop(price_feed_account_data); + drop(price_feed_account); + + if next_timestamp > current_timestamp { + pyth_solana_receiver_sdk::cpi::post_update_atomic( + cpi_context, + PostUpdateAtomicParams { + merkle_price_update: merkle_price_update.clone(), + vaa: vaa.clone(), + }, + )?; + } + } + + Ok(()) +} + pub fn get_timestamp_from_price_feed_account(price_feed_account: &AccountInfo) -> Result { if price_feed_account.data_is_empty() { Ok(0) @@ -158,3 +228,14 @@ pub struct PostPythPullOracleUpdateAtomic<'info> { #[account(mut, owner = drift_oracle_receiver_program::id(), seeds = [PTYH_PRICE_FEED_SEED_PREFIX, &feed_id], bump)] pub price_feed: AccountInfo<'info>, } + +#[derive(Accounts)] +pub struct PostPythPullMultiOracleUpdatesAtomic<'info> { + #[account(mut)] + pub keeper: Signer<'info>, + pub pyth_solana_receiver: Program<'info, PythSolanaReceiver>, + /// CHECK: We can't use AccountVariant:: here because its owner is hardcoded as the "official" Wormhole program + #[account( + owner = wormhole_program::id() @ ErrorCode::OracleWrongGuardianSetOwner)] + pub guardian_set: AccountInfo<'info>, +} diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index fc723a061..b6da9598b 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -599,6 +599,13 @@ pub mod drift { handle_post_pyth_pull_oracle_update_atomic(ctx, feed_id, params) } + pub fn post_multi_pyth_pull_oracle_updates_atomic<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, PostPythPullMultiOracleUpdatesAtomic<'info>>, + params: Vec, + ) -> Result<()> { + handle_post_multi_pyth_pull_oracle_updates_atomic(ctx, params) + } + // Admin Instructions pub fn initialize(ctx: Context) -> Result<()> { diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index d978b76b8..2868f89de 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -132,7 +132,6 @@ import { isOracleValid, trimVaaSignatures } from './math/oracles'; import { TxHandler } from './tx/txHandler'; import { wormholeCoreBridgeIdl, - pythSolanaReceiverIdl, DEFAULT_RECEIVER_PROGRAM_ID, } from '@pythnetwork/pyth-solana-receiver'; import { parseAccumulatorUpdateData } from '@pythnetwork/price-service-sdk'; @@ -145,6 +144,7 @@ import { WormholeCoreBridgeSolana } from '@pythnetwork/pyth-solana-receiver/lib/ import { PythSolanaReceiver } from '@pythnetwork/pyth-solana-receiver/lib/idl/pyth_solana_receiver'; import { getFeedIdUint8Array, trimFeedId } from './util/pythPullOracleUtils'; import { isVersionedTransaction } from './tx/utils'; +import pythSolanaReceiverIdl from './idl/pyth_solana_receiver.json'; type RemainingAccountParams = { userAccounts: UserAccount[]; @@ -6913,7 +6913,7 @@ export class DriftClient { public getReceiverProgram(): Program { if (this.receiverProgram === undefined) { this.receiverProgram = new Program( - pythSolanaReceiverIdl, + pythSolanaReceiverIdl as PythSolanaReceiver, DEFAULT_RECEIVER_PROGRAM_ID, this.provider ); @@ -6935,12 +6935,25 @@ export class DriftClient { return txSig; } + public async postMultiPythPullOracleUpdatesAtomic( + vaaString: string, + feedIds: string[] + ): Promise { + const postIxs = await this.getPostPythPullOracleUpdateAtomicIxs( + vaaString, + feedIds + ); + const tx = await this.buildTransaction(postIxs); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + public async getPostPythPullOracleUpdateAtomicIxs( vaaString: string, - feedId: string, + feedIds: string | string[], numSignatures = 2 ): Promise { - feedId = trimFeedId(feedId); const accumulatorUpdateData = parseAccumulatorUpdateData( Buffer.from(vaaString, 'base64') ); @@ -6955,23 +6968,61 @@ export class DriftClient { ); const postIxs: TransactionInstruction[] = []; - for (const update of accumulatorUpdateData.updates) { + if (accumulatorUpdateData.updates.length > 1) { + const encodedParams = this.getReceiverProgram().coder.types.encode( + 'PostMultiUpdatesAtomicParams', + { + vaa: trimmedVaa, + merklePriceUpdates: accumulatorUpdateData.updates, + } + ); + const feedIdsToUse: string[] = + typeof feedIds === 'string' ? [feedIds] : feedIds; + const pubkeys = feedIdsToUse.map((feedId) => { + return getPythPullOraclePublicKey( + this.program.programId, + getFeedIdUint8Array(feedId) + ); + }); + + const remainingAccounts: Array = pubkeys.map((pubkey) => { + return { + pubkey, + isSigner: false, + isWritable: true, + }; + }); + postIxs.push( + this.program.instruction.postMultiPythPullOracleUpdatesAtomic( + encodedParams, + { + accounts: { + keeper: this.wallet.publicKey, + pythSolanaReceiver: DRIFT_ORACLE_RECEIVER_ID, + guardianSet, + }, + remainingAccounts, + } + ) + ); + } else { + let feedIdToUse = typeof feedIds === 'string' ? feedIds : feedIds[0]; + feedIdToUse = trimFeedId(feedIdToUse); postIxs.push( await this.getSinglePostPythPullOracleAtomicIx( { vaa: trimmedVaa, - merklePriceUpdate: update, + merklePriceUpdate: accumulatorUpdateData.updates[0], }, - feedId, + feedIdToUse, guardianSet ) ); } - return postIxs; } - public async getSinglePostPythPullOracleAtomicIx( + private async getSinglePostPythPullOracleAtomicIx( params: { vaa: Buffer; merklePriceUpdate: { @@ -7083,7 +7134,7 @@ export class DriftClient { ); } - public async getBuildEncodedVaaIxs( + private async getBuildEncodedVaaIxs( vaa: Buffer, guardianSet: PublicKey ): Promise<[TransactionInstruction[], Keypair]> { diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index f6fb6a114..d13de8ea2 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -2663,6 +2663,32 @@ } ] }, + { + "name": "postMultiPythPullOracleUpdatesAtomic", + "accounts": [ + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "pythSolanaReceiver", + "isMut": false, + "isSigner": false + }, + { + "name": "guardianSet", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "params", + "type": "bytes" + } + ] + }, { "name": "initialize", "accounts": [ @@ -12512,6 +12538,21 @@ "code": 6275, "name": "OracleWrongVaaOwner", "msg": "Oracle vaa owner must be wormhole program" + }, + { + "code": 6276, + "name": "OracleTooManyPriceAccountUpdates", + "msg": "Multi updates must have 2 or fewer accounts passed in remaining accounts" + }, + { + "code": 6277, + "name": "OracleMismatchedVaaAndPriceUpdates", + "msg": "Don't have the same remaining accounts number and merkle price updates left" + }, + { + "code": 6278, + "name": "OracleBadRemainingAccountPublicKey", + "msg": "Remaining account passed is not a valid pda" } ], "metadata": { diff --git a/sdk/src/idl/pyth_solana_receiver.json b/sdk/src/idl/pyth_solana_receiver.json new file mode 100644 index 000000000..bbf20dfc4 --- /dev/null +++ b/sdk/src/idl/pyth_solana_receiver.json @@ -0,0 +1,628 @@ +{ + "version": "0.1.0", + "name": "pyth_solana_receiver", + "instructions": [ + { + "name": "initialize", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "initialConfig", + "type": { + "defined": "Config" + } + } + ] + }, + { + "name": "requestGovernanceAuthorityTransfer", + "accounts": [ + { + "name": "payer", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "targetGovernanceAuthority", + "type": "publicKey" + } + ] + }, + { + "name": "acceptGovernanceAuthorityTransfer", + "accounts": [ + { + "name": "payer", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setDataSources", + "accounts": [ + { + "name": "payer", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "validDataSources", + "type": { + "vec": { + "defined": "DataSource" + } + } + } + ] + }, + { + "name": "setFee", + "accounts": [ + { + "name": "payer", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "singleUpdateFeeInLamports", + "type": "u64" + } + ] + }, + { + "name": "setWormholeAddress", + "accounts": [ + { + "name": "payer", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "wormhole", + "type": "publicKey" + } + ] + }, + { + "name": "setMinimumSignatures", + "accounts": [ + { + "name": "payer", + "isMut": false, + "isSigner": true + }, + { + "name": "config", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "minimumSignatures", + "type": "u8" + } + ] + }, + { + "name": "postUpdateAtomic", + "docs": [ + "Post a price update using a VAA and a MerklePriceUpdate.", + "This function allows you to post a price update in a single transaction.", + "Compared to post_update, it is less secure since you won't be able to verify all guardian signatures if you use this function because of transaction size limitations.", + "Typically, you can fit 5 guardian signatures in a transaction that uses this." + ], + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "guardianSet", + "isMut": false, + "isSigner": false, + "docs": [ + "Instead we do the same steps in deserialize_guardian_set_checked." + ] + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "treasury", + "isMut": true, + "isSigner": false + }, + { + "name": "priceUpdateAccount", + "isMut": true, + "isSigner": true, + "docs": [ + "The contraint is such that either the price_update_account is uninitialized or the payer is the write_authority.", + "Pubkey::default() is the SystemProgram on Solana and it can't sign so it's impossible that price_update_account.write_authority == Pubkey::default() once the account is initialized" + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "writeAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "PostUpdateAtomicParams" + } + } + ] + }, + { + "name": "postUpdate", + "docs": [ + "Post a price update using an encoded_vaa account and a MerklePriceUpdate calldata.", + "This should be called after the client has already verified the Vaa via the Wormhole contract.", + "Check out target_chains/solana/cli/src/main.rs for an example of how to do this." + ], + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "encodedVaa", + "isMut": false, + "isSigner": false + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "treasury", + "isMut": true, + "isSigner": false + }, + { + "name": "priceUpdateAccount", + "isMut": true, + "isSigner": true, + "docs": [ + "The contraint is such that either the price_update_account is uninitialized or the payer is the write_authority.", + "Pubkey::default() is the SystemProgram on Solana and it can't sign so it's impossible that price_update_account.write_authority == Pubkey::default() once the account is initialized" + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "writeAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": "PostUpdateParams" + } + } + ] + }, + { + "name": "reclaimRent", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "priceUpdateAccount", + "isMut": true, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "Config", + "type": { + "kind": "struct", + "fields": [ + { + "name": "governanceAuthority", + "type": "publicKey" + }, + { + "name": "targetGovernanceAuthority", + "type": { + "option": "publicKey" + } + }, + { + "name": "wormhole", + "type": "publicKey" + }, + { + "name": "validDataSources", + "type": { + "vec": { + "defined": "DataSource" + } + } + }, + { + "name": "singleUpdateFeeInLamports", + "type": "u64" + }, + { + "name": "minimumSignatures", + "type": "u8" + } + ] + } + }, + { + "name": "priceUpdateV2", + "type": { + "kind": "struct", + "fields": [ + { + "name": "writeAuthority", + "type": "publicKey" + }, + { + "name": "verificationLevel", + "type": { + "defined": "VerificationLevel" + } + }, + { + "name": "priceMessage", + "type": { + "defined": "PriceFeedMessage" + } + }, + { + "name": "postedSlot", + "type": "u64" + } + ] + } + } + ], + "types": [ + { + "name": "PriceFeedMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "feedId", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "price", + "type": "i64" + }, + { + "name": "conf", + "type": "u64" + }, + { + "name": "exponent", + "type": "i32" + }, + { + "name": "publishTime", + "type": "i64" + }, + { + "name": "prevPublishTime", + "type": "i64" + }, + { + "name": "emaPrice", + "type": "i64" + }, + { + "name": "emaConf", + "type": "u64" + } + ] + } + }, + { + "name": "MerklePriceUpdate", + "type": { + "kind": "struct", + "fields": [ + { + "name": "message", + "type": "bytes" + }, + { + "name": "proof", + "type": { + "vec": { + "array": ["u8", 20] + } + } + } + ] + } + }, + { + "name": "DataSource", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chain", + "type": "u16" + }, + { + "name": "emitter", + "type": "publicKey" + } + ] + } + }, + { + "name": "PostMultiUpdatesAtomicParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vaa", + "type": "bytes" + }, + { + "name": "merklePriceUpdates", + "type": { + "vec": { + "defined": "MerklePriceUpdate" + } + } + } + ] + } + }, + { + "name": "PostUpdateAtomicParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vaa", + "type": "bytes" + }, + { + "name": "merklePriceUpdate", + "type": { + "defined": "MerklePriceUpdate" + } + } + ] + } + }, + { + "name": "PostUpdateParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "merklePriceUpdate", + "type": { + "defined": "MerklePriceUpdate" + } + } + ] + } + }, + { + "name": "VerificationLevel", + "docs": [ + "* This enum represents how many guardian signatures were checked for a Pythnet price update\n * If full, guardian quorum has been attained\n * If partial, at least config.minimum signatures have been verified, but in the case config.minimum_signatures changes in the future we also include the number of signatures that were checked" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "Partial", + "fields": [ + { + "name": "numSignatures", + "type": "u8" + } + ] + }, + { + "name": "Full" + } + ] + } + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidWormholeMessage", + "msg": "Received an invalid wormhole message" + }, + { + "code": 6001, + "name": "DeserializeMessageFailed", + "msg": "An error occurred when deserializing the message" + }, + { + "code": 6002, + "name": "InvalidPriceUpdate", + "msg": "Received an invalid price update" + }, + { + "code": 6003, + "name": "UnsupportedMessageType", + "msg": "This type of message is not supported currently" + }, + { + "code": 6004, + "name": "InvalidDataSource", + "msg": "The tuple emitter chain, emitter doesn't match one of the valid data sources." + }, + { + "code": 6005, + "name": "InsufficientFunds", + "msg": "Funds are insufficient to pay the receiving fee" + }, + { + "code": 6006, + "name": "WrongWriteAuthority", + "msg": "This signer can't write to price update account" + }, + { + "code": 6007, + "name": "WrongVaaOwner", + "msg": "The posted VAA account has the wrong owner." + }, + { + "code": 6008, + "name": "DeserializeVaaFailed", + "msg": "An error occurred when deserializing the VAA." + }, + { + "code": 6009, + "name": "InsufficientGuardianSignatures", + "msg": "The number of guardian signatures is below the minimum" + }, + { + "code": 6010, + "name": "InvalidVaaVersion", + "msg": "Invalid VAA version" + }, + { + "code": 6011, + "name": "GuardianSetMismatch", + "msg": "Guardian set version in the VAA doesn't match the guardian set passed" + }, + { + "code": 6012, + "name": "InvalidGuardianOrder", + "msg": "Guardian signature indices must be increasing" + }, + { + "code": 6013, + "name": "InvalidGuardianIndex", + "msg": "Guardian index exceeds the number of guardians in the set" + }, + { + "code": 6014, + "name": "InvalidSignature", + "msg": "A VAA signature is invalid" + }, + { + "code": 6015, + "name": "InvalidGuardianKeyRecovery", + "msg": "The recovered guardian public key doesn't match the guardian set" + }, + { + "code": 6016, + "name": "WrongGuardianSetOwner", + "msg": "The guardian set account is owned by the wrong program" + }, + { + "code": 6017, + "name": "InvalidGuardianSetPda", + "msg": "The Guardian Set account doesn't match the PDA derivation" + }, + { + "code": 6018, + "name": "GuardianSetExpired", + "msg": "The Guardian Set is expired" + }, + { + "code": 6019, + "name": "GovernanceAuthorityMismatch", + "msg": "The signer is not authorized to perform this governance action" + }, + { + "code": 6020, + "name": "TargetGovernanceAuthorityMismatch", + "msg": "The signer is not authorized to accept the governance authority" + }, + { + "code": 6021, + "name": "NonexistentGovernanceAuthorityTransferRequest", + "msg": "The governance authority needs to request a transfer first" + } + ] +} diff --git a/tests/fixtures/pyth_solana_receiver.so b/tests/fixtures/pyth_solana_receiver.so index 71e32fc9d..09a0a3297 100755 Binary files a/tests/fixtures/pyth_solana_receiver.so and b/tests/fixtures/pyth_solana_receiver.so differ diff --git a/tests/pythPull.ts b/tests/pythPull.ts index eecf900c5..5118651af 100644 --- a/tests/pythPull.ts +++ b/tests/pythPull.ts @@ -38,6 +38,9 @@ describe('pyth pull oracles', () => { const feedId = '0x2f2d17abbc1e781bd87b4a5d52c8b2856886f5c482fa3593cebf6795040ab0b6'; + const feedId2 = + '0x8ac0c70fff57e9aefdf5edf44b51d62c2d433653cbb2cf5cc06bb115af04d221'; + let feedAddress: PublicKey; before(async () => { @@ -116,11 +119,24 @@ describe('pyth pull oracles', () => { const txsig = await driftClient.initializePythPullOracle(feedId); console.log(txsig); + + const txsig2 = await driftClient.initializePythPullOracle(feedId2); + + console.log(txsig2); }); it('post atomic', async () => { const vaa = - 'UE5BVQEAAAADuAEAAAAEDQBCMgHU9FQcIQcDeFlahVuIjFTV3Ga+h+mLNrjNtGudAVhCNf7nJQPI7+N+x5o9B52zFhydj5NfeiDVGyTTcgmBAQKIAWC+ENn58snD+mQy/n62kpDJKXgnRQsa34HzoqqGihWeG5E2ZuFsf3CRv8vAqi7OLnHvAUr0Iyh+ZqOC63HhAQOYwX+xZDyah05YVSJ8WRpcvGb5/ILnQBtaE+hLBhsQtQqzN+dnGPva5uHiU9HV4MheEacJgris2qbSQKXQI2QPAAS5TlKWIBEf61jOB4nUwywXTD8s4S71SnuMNzDb7EmgLzVn56Xi2+BHluI3mH70DLrdFeKtdN7/VWa8rHX/exAnAQai3i3ofNfIkakObv7GP0DVN6tqCetbt57oP5Ioer0Fo3rfNPTZfpeqixhu6Yg0TdjCTavB3S3pQD4r1BeFccagAQvsV3AkXvUWwspj30bGc+/yZKTaSwRkFsAdgGXGCVS/J0V40eGhqvx+EIuZlQnnWthtA83PELrOQ56WU7UivnFSAAwMd3AjpNMGEnrnsvupYSX6GUq5q8zff85LYJikJ3miQxLfc77QepaTmubOI/iTAtUbocy1cS7h7paqXR9NMf2AAA1sKdfW8d1tFsr0zJoEwBjCSMWnRIpiT/tOa4sKPnzF1zN6G0F1sEauCFMuIqKpgHUN0BZEiytiSEK8Xu4yVuuVAQ6s27zlTsMMy+Ku0pfFiVefhhJwdI8IdWLHIG0NaIJjHVYbVPA26kwBkpz2AMlcYM+bs5bELlAcStv5PKC5U2+nAA/TcNr+b77ui5+OBoIrnqL4k+5Q0ZNm58KQml+aDBwzuiCNm1um+RdZLbsYAtERItJ3o/2DV5mxK453KupXNrB8ARD/27EL0Csf7fWQlZKIDZPebny5jdW8LSLPqG0yU0/xCVaXjnyA7CktfW1N2aUP6SaRU7yk/z9iKoFv5tE+Ti+EARHZHKSGL3Q9e6dshpquRBo0kGQ0mihXeUuEEXFbrrS8UjnjcWoS/liligFYFyN9jE/JDBamEeXV9ZHl9tHYYQKgABLhScEt52h+pdTAeHH/kECTj5IeTRqcklzgD3dmVNymdGE4eXcjmvFxh7hXLSbSx/EpILAyzHRaKfe03N/1oFpNAGZ90fcAAAAAABrhAfrtrFhR4yubI7X5QRqMK6xKrj7U3XuBHdGnLqSqcQAAAAAD6d6rAUFVV1YAAAAAAAj0UkIAACcQILdMyJ7aEHG/UrkKF0N32va/y3ABAFUALy0Xq7weeBvYe0pdUsiyhWiG9cSC+jWTzr9nlQQKsLYAAAAAAHjmhQAAAAAAAHZV////+AAAAABmfdH3AAAAAGZ90fcAAAAAAHkdDQAAAAAAAI4BCp6vPuZHQJAkw6QVYM8r5LckP6y7M0O90/+uxk99a+3XUEcOu2o4iF2pu2Pfy+g3/hULrluX11aWCwkhjQIT8U3BApQtpkDlT0sGXKZCcaGmyuFVsVxcgoH6zxzEZ35ibCXKjFZgnfbSCyzF299o4VxAWBd0WA8XXTX6QpfKc3y8xgQ1tPn6OO2466XWD6ywYkZe7a2n52XCC6Vq0iG+cCY2G9eoZEPULNWAcamTIu6Xzbm+zTcv6ULsnbjhPAQ/8kcbpmYQPKRY'; + 'UE5BVQEAAAADuAEAAAAEDQCGP2Fjz2LFIX48Qqm/paFzO/iEtFgH5sC1FHhNroyIC2fuzsISzz9IHbvBrlknA0UvM8r9UHSvsAwaqzquhzFsAALnhRblTgAMLanjq38YctnwqDsdV39WviJ0QAnWgRn+a2i4ljPkbVQl1+MM47qcsua4+1L6jo8i3LPMivVx+HQgAQRRADMkx28oGLPnNZnaZp8wtsxckjtx1GvXi+l9d89Yu1DJnYEGkVF4TzZSKtIQe+5OoUPAaIpnEauGVe0AEeh7AAYzopa5UFEUji5zKjTRRUxlQWAWDq3LuGMeV7osio6L/jyD0jMxpYMR0r/dqIdUi2a4GP0uF15k9yFMkANh7OCRAAgb/WNBqaYjLjZsqRqEFKbG3ZVR2vS5ph7EO6D8TKP2gg3Mk3Mhyr2O21KAboy061vKPbHTjX1w3dq8ipnz6EacAQpOpdNfjx1LhMu7ZaMfSM6IFqHOZnQqGxQOQHwh5lAd50Vj8LVu3rng211UchelGHtROvhN1IapTkVSEhD0dbeeAQs+IYIUBk8EahKpPnD0hk6E2d8ge3gKDcgakWgDhRMunArMASyVWkWw0N3p9FvOobXg4V4L5Tim6L1AhHf5Rj0YAAxsygUAwlhGQPEThxT72eY0HVbi8C1LATsBXrW6jksUNTllCqWWbRwgwDSlgibrk05BKtO1pjFCjkWRZZ+TCvrsAA05LnYl0RwpRYUs31y5Lbk8mZHrFDj02MkTC05rGcjVzmddlNcj5/IIp8Hc44GJFZ4XZO3kx7jW3vuF6RQm6RPmAA6xLKcvzZllJT8kxn/LI4AYUuCIOVyLMG/kVodeXWkOKSrkXr0SNwMFsLfl9xvPk2dCa7SyicGwMTUfKP4P8cyeAQ9Q5G4EDpPCq/A0J3luHRoCnSDpCuCu4zTzESAmRe80aSwDl7tN4wSn369Nu4iD6JSyUx/y3bHF7BgvlyGfQYHjABCZpnivKtKFNYpaLR627OKG//Vv3zol7gdCoMOXRcIxLhwSuhn5QlVHgeoOrHiLtOBlTzpz4bwa8btRxvU43pCgABK2TIKVKUnv5OyTjkQh8N5IMpaRK83UH3hpvsJKejNpJQK2zR/WfCkrYjy6pYQfhenZYHi4GCMQ0ALSh9cojaDlAGaVh0wAAAAAABrhAfrtrFhR4yubI7X5QRqMK6xKrj7U3XuBHdGnLqSqcQAAAAAEHJOLAUFVV1YAAAAAAAknGqgAACcQdoC/4vcjI21wyoVC1q3FUZH0FpwBAFUALy0Xq7weeBvYe0pdUsiyhWiG9cSC+jWTzr9nlQQKsLYAAAAAAICwNAAAAAAAAS4k////+AAAAABmlYdMAAAAAGaVh0sAAAAAAH6C+QAAAAAAAPFsCz2Sz2/hyAOSwCA+M8lRiOs+jGuZp6wcFR4rTAFuR2bAYNycVYQFeCxlkQJrEKDSba6FxQXgPZ7wBb/43EHuHHKQaaGb3NVsxnHFnLHefZDbF235q+aRnadgJfm6gqckqb0IczoHBaSuyrVYfSEbPuyNjXE7V++G/OwwVrwQOWqD6ti/nzLgnQ+qCVpEBto25YvZQzkmfYMKg1tJepxs/Sbgyx2fayAJtK8pRlJIixSTRbLiQX408KCq/ElVNzOSqt6Aw1KrAg81sLzKSjMEqnhbdFxgSzqncj8kPFw='; await driftClient.postPythPullOracleUpdateAtomic(vaa, feedId); }); + + it('post multi atomic', async () => { + const vaa2 = + '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'; + await driftClient.postMultiPythPullOracleUpdatesAtomic(vaa2, [ + feedId, + feedId2, + ]); + }); });