@@ -35,6 +35,7 @@ import {
3535 POST_UPDATE_COMPUTE_BUDGET ,
3636 UPDATE_PRICE_FEED_COMPUTE_BUDGET ,
3737 VERIFY_ENCODED_VAA_COMPUTE_BUDGET ,
38+ WRITE_ENCODED_VAA_COMPUTE_BUDGET ,
3839} from "./compute_budget" ;
3940import { Wallet } from "@coral-xyz/anchor" ;
4041import {
@@ -43,6 +44,7 @@ import {
4344 findEncodedVaaAccountsByWriteAuthority ,
4445 getGuardianSetIndex ,
4546 trimSignatures ,
47+ VAA_SPLIT_INDEX ,
4648} from "./vaa" ;
4749import {
4850 TransactionBuilder ,
@@ -194,6 +196,42 @@ export class PythTransactionBuilder extends TransactionBuilder {
194196 this . addInstructions ( postInstructions ) ;
195197 }
196198
199+ /**
200+ * Add instructions to post TWAP updates to the builder.
201+ * Use this function to post fully verified TWAP updates from the present or from the past for your program to consume.
202+ *
203+ * @param twapUpdateDataArray the output of the `@pythnetwork/hermes-client`'s `getLatestTwaps`. This is an array of verifiable price updates.
204+ *
205+ * @example
206+ * ```typescript
207+ * // Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable
208+ * const twapUpdateData = await hermesClient.getLatestTwaps([
209+ * SOL_PRICE_FEED_ID,
210+ * ETH_PRICE_FEED_ID,
211+ * ]);
212+ *
213+ * const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
214+ * await transactionBuilder.addPostTwapUpdates(priceUpdateData);
215+ * console.log("The SOL/USD price update will get posted to:", transactionBuilder.getPriceUpdateAccount(SOL_PRICE_FEED_ID).toBase58())
216+ * await transactionBuilder.addPriceConsumerInstructions(...)
217+ * ```
218+ */
219+ async addPostTwapUpdates ( twapUpdateDataArray : string [ ] ) {
220+ const {
221+ postInstructions,
222+ priceFeedIdToTwapUpdateAccount,
223+ closeInstructions,
224+ } = await this . pythSolanaReceiver . buildPostTwapUpdateInstructions (
225+ twapUpdateDataArray
226+ ) ;
227+ this . closeInstructions . push ( ...closeInstructions ) ;
228+ Object . assign (
229+ this . priceFeedIdToPriceUpdateAccount ,
230+ priceFeedIdToTwapUpdateAccount
231+ ) ;
232+ this . addInstructions ( postInstructions ) ;
233+ }
234+
197235 /**
198236 * Add instructions to update price feed accounts to the builder.
199237 * Price feed accounts are fixed accounts per price feed id that can only be updated with a more recent price.
@@ -317,7 +355,7 @@ export class PythTransactionBuilder extends TransactionBuilder {
317355 this . priceFeedIdToPriceUpdateAccount [ priceFeedId ] ;
318356 if ( ! priceUpdateAccount ) {
319357 throw new Error (
320- `No price update account found for the price feed ID ${ priceFeedId } . Make sure to call addPostPriceUpdates or addPostPartiallyVerifiedPriceUpdates before calling this function.`
358+ `No price update account found for the price feed ID ${ priceFeedId } . Make sure to call addPostPriceUpdates or addPostPartiallyVerifiedPriceUpdates or postTwapUpdates before calling this function.`
321359 ) ;
322360 }
323361 return priceUpdateAccount ;
@@ -596,6 +634,231 @@ export class PythSolanaReceiver {
596634 } ;
597635 }
598636
637+ /**
638+ * Build a series of helper instructions that post TWAP updates to the Pyth Solana Receiver program and another series to close the encoded vaa accounts and the TWAP update accounts.
639+ *
640+ * @param twapUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestTwaps`. This is an array of verifiable price updates.
641+ * @returns `postInstructions`: the instructions to post the TWAP updates, these should be called before consuming the price updates
642+ * @returns `priceFeedIdToTwapUpdateAccount`: this is a map of price feed IDs to Solana address. Given a price feed ID, you can use this map to find the account where `postInstructions` will post the TWAP update.
643+ * @returns `closeInstructions`: the instructions to close the TWAP update accounts, these should be called after consuming the TWAP updates
644+ */
645+ async buildPostTwapUpdateInstructions (
646+ twapUpdateDataArray : string [ ]
647+ ) : Promise < {
648+ postInstructions : InstructionWithEphemeralSigners [ ] ;
649+ priceFeedIdToTwapUpdateAccount : Record < string , PublicKey > ;
650+ closeInstructions : InstructionWithEphemeralSigners [ ] ;
651+ } > {
652+ const postInstructions : InstructionWithEphemeralSigners [ ] = [ ] ;
653+ const priceFeedIdToTwapUpdateAccount : Record < string , PublicKey > = { } ;
654+ const closeInstructions : InstructionWithEphemeralSigners [ ] = [ ] ;
655+
656+ const treasuryId = getRandomTreasuryId ( ) ;
657+
658+ if ( twapUpdateDataArray . length !== 2 ) {
659+ throw new Error (
660+ "twapUpdateDataArray must contain exactly two updates (start and end)"
661+ ) ;
662+ }
663+
664+ const [ startUpdateData , endUpdateData ] = twapUpdateDataArray . map ( ( data ) =>
665+ parseAccumulatorUpdateData ( Buffer . from ( data , "base64" ) )
666+ ) ;
667+
668+ // Validate that the start and end updates contain the same number of price feeds
669+ if ( startUpdateData . updates . length !== endUpdateData . updates . length ) {
670+ throw new Error (
671+ "Start and end updates must contain the same number of price feeds"
672+ ) ;
673+ }
674+
675+ // // Verify the VAAs
676+ // const [startVaa, endVaa] = await Promise.all([
677+ // this.buildPostEncodedVaaInstructions(startUpdateData.vaa),
678+ // this.buildPostEncodedVaaInstructions(endUpdateData.vaa)
679+ // ]);
680+ // postInstructions.push(...startVaa.postInstructions, ...endVaa.postInstructions);
681+ // closeInstructions.push(...startVaa.closeInstructions, ...endVaa.closeInstructions);
682+
683+ // const { encodedVaaAddress: startEncodedVaa } = startVaa;
684+ // const { encodedVaaAddress: endEncodedVaa } = endVaa;
685+
686+ // TRANSACTION 1: Create, init, write initial data for Start VAA
687+ // Create
688+ const trimmedStartVaa = trimSignatures ( startUpdateData . vaa , 13 ) ;
689+ const startEncodedVaaKeypair = new Keypair ( ) ;
690+ postInstructions . push (
691+ await buildEncodedVaaCreateInstruction (
692+ this . wormhole ,
693+ trimmedStartVaa ,
694+ startEncodedVaaKeypair
695+ )
696+ ) ;
697+ // Init
698+ postInstructions . push ( {
699+ instruction : await this . wormhole . methods
700+ . initEncodedVaa ( )
701+ . accounts ( {
702+ encodedVaa : startEncodedVaaKeypair . publicKey ,
703+ } )
704+ . instruction ( ) ,
705+ signers : [ ] ,
706+ computeUnits : INIT_ENCODED_VAA_COMPUTE_BUDGET ,
707+ } ) ;
708+
709+ // Write initial data
710+ postInstructions . push (
711+ ...( await buildWriteEncodedVaaWithSplitInstructions (
712+ this . wormhole ,
713+ trimmedStartVaa ,
714+ startEncodedVaaKeypair . publicKey
715+ ) )
716+ ) ;
717+
718+ // TRANSACTION 2: Create, init, write initial data for End VAA
719+ // Create
720+ const trimmedEndVaa = trimSignatures ( endUpdateData . vaa , 13 ) ;
721+ const endEncodedVaaKeypair = new Keypair ( ) ;
722+ postInstructions . push (
723+ await buildEncodedVaaCreateInstruction (
724+ this . wormhole ,
725+ trimmedEndVaa ,
726+ endEncodedVaaKeypair
727+ )
728+ ) ;
729+ // Init
730+ postInstructions . push ( {
731+ instruction : await this . wormhole . methods
732+ . initEncodedVaa ( )
733+ . accounts ( {
734+ encodedVaa : endEncodedVaaKeypair . publicKey ,
735+ } )
736+ . instruction ( ) ,
737+ signers : [ ] ,
738+ computeUnits : INIT_ENCODED_VAA_COMPUTE_BUDGET ,
739+ } ) ;
740+
741+ // Write initial data
742+ postInstructions . push (
743+ ...( await buildWriteEncodedVaaWithSplitInstructions (
744+ this . wormhole ,
745+ trimmedEndVaa ,
746+ endEncodedVaaKeypair . publicKey
747+ ) )
748+ ) ;
749+
750+ // TRANSACTION 3: Write remaining data and verify for Start & End VAAs
751+ // Write remaining data for start VAA
752+ postInstructions . push ( {
753+ instruction : await this . wormhole . methods
754+ . writeEncodedVaa ( {
755+ index : VAA_SPLIT_INDEX ,
756+ data : trimmedStartVaa . subarray ( VAA_SPLIT_INDEX ) ,
757+ } )
758+ . accounts ( {
759+ draftVaa : startEncodedVaaKeypair . publicKey ,
760+ } )
761+ . instruction ( ) ,
762+ signers : [ ] ,
763+ computeUnits : WRITE_ENCODED_VAA_COMPUTE_BUDGET ,
764+ } ) ;
765+
766+ // Write remaining data for end VAA
767+ postInstructions . push ( {
768+ instruction : await this . wormhole . methods
769+ . writeEncodedVaa ( {
770+ index : VAA_SPLIT_INDEX ,
771+ data : trimmedEndVaa . subarray ( VAA_SPLIT_INDEX ) ,
772+ } )
773+ . accounts ( {
774+ draftVaa : endEncodedVaaKeypair . publicKey ,
775+ } )
776+ . instruction ( ) ,
777+ signers : [ ] ,
778+ computeUnits : WRITE_ENCODED_VAA_COMPUTE_BUDGET ,
779+ } ) ;
780+
781+ // Verify start VAA
782+ const startGuardianSetIndex = getGuardianSetIndex ( trimmedStartVaa ) ;
783+ postInstructions . push ( {
784+ instruction : await this . wormhole . methods
785+ . verifyEncodedVaaV1 ( )
786+ . accounts ( {
787+ guardianSet : getGuardianSetPda (
788+ startGuardianSetIndex ,
789+ this . wormhole . programId
790+ ) ,
791+ draftVaa : startEncodedVaaKeypair . publicKey ,
792+ } )
793+ . instruction ( ) ,
794+ signers : [ ] ,
795+ computeUnits : VERIFY_ENCODED_VAA_COMPUTE_BUDGET ,
796+ } ) ;
797+
798+ // Verify end VAA
799+ const endGuardianSetIndex = getGuardianSetIndex ( trimmedEndVaa ) ;
800+ postInstructions . push ( {
801+ instruction : await this . wormhole . methods
802+ . verifyEncodedVaaV1 ( )
803+ . accounts ( {
804+ guardianSet : getGuardianSetPda (
805+ startGuardianSetIndex ,
806+ this . wormhole . programId
807+ ) ,
808+ draftVaa : startEncodedVaaKeypair . publicKey ,
809+ } )
810+ . accounts ( {
811+ guardianSet : getGuardianSetPda (
812+ startGuardianSetIndex ,
813+ this . wormhole . programId
814+ ) ,
815+ draftVaa : endEncodedVaaKeypair . publicKey ,
816+ } )
817+ . instruction ( ) ,
818+ signers : [ ] ,
819+ computeUnits : VERIFY_ENCODED_VAA_COMPUTE_BUDGET ,
820+ } ) ;
821+
822+ // Post a TWAP update to the receiver contract for each price feed
823+ for ( let i = 0 ; i < startUpdateData . updates . length ; i ++ ) {
824+ const startUpdate = startUpdateData . updates [ i ] ;
825+ const endUpdate = endUpdateData . updates [ i ] ;
826+
827+ const twapUpdateKeypair = new Keypair ( ) ;
828+ postInstructions . push ( {
829+ instruction : await this . receiver . methods
830+ . postTwapUpdate ( {
831+ startMerklePriceUpdate : startUpdate ,
832+ endMerklePriceUpdate : endUpdate ,
833+ treasuryId,
834+ } )
835+ . accounts ( {
836+ startEncodedVaa : startEncodedVaaKeypair . publicKey ,
837+ endEncodedVaa : endEncodedVaaKeypair . publicKey ,
838+ twapUpdateAccount : twapUpdateKeypair . publicKey ,
839+ treasury : getTreasuryPda ( treasuryId , this . receiver . programId ) ,
840+ config : getConfigPda ( this . receiver . programId ) ,
841+ } )
842+ . instruction ( ) ,
843+ signers : [ twapUpdateKeypair ] ,
844+ computeUnits : POST_UPDATE_COMPUTE_BUDGET ,
845+ } ) ;
846+
847+ priceFeedIdToTwapUpdateAccount [
848+ "0x" + parsePriceFeedMessage ( startUpdate . message ) . feedId . toString ( "hex" )
849+ ] = twapUpdateKeypair . publicKey ;
850+ closeInstructions . push (
851+ await this . buildClosePriceUpdateInstruction ( twapUpdateKeypair . publicKey )
852+ ) ;
853+ }
854+
855+ return {
856+ postInstructions,
857+ priceFeedIdToTwapUpdateAccount,
858+ closeInstructions,
859+ } ;
860+ }
861+
599862 /**
600863 * Build a series of helper instructions that update one or many price feed accounts and another series to close the encoded vaa accounts used to update the price feed accounts.
601864 *
0 commit comments