11import {
22 AddressLookupTableAccount ,
33 BlockhashWithExpiryBlockHeight ,
4+ ComputeBudgetProgram ,
45 Connection ,
6+ Keypair ,
57 PublicKey ,
68 SystemProgram ,
79 Transaction ,
@@ -18,6 +20,7 @@ import {
1820} from "@solana/spl-token" ;
1921import BN from "bn.js" ;
2022import BigNumber from "bignumber.js" ;
23+ import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver" ;
2124import {
2225 Obligation ,
2326 OBLIGATION_SIZE ,
@@ -45,6 +48,10 @@ import {
4548import { POSITION_LIMIT } from "./constants" ;
4649import { EnvironmentType , PoolType , ReserveType } from "./types" ;
4750import { getProgramId , U64_MAX , WAD } from "./constants" ;
51+ import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet" ;
52+ import { PriceServiceConnection } from "@pythnetwork/price-service-client" ;
53+ import { AnchorProvider , Program } from "@coral-xyz/anchor-30" ;
54+ import { CrossbarClient , loadLookupTables , PullFeed , SB_ON_DEMAND_PID } from "@switchboard-xyz/on-demand" ;
4855
4956const SOL_PADDING_FOR_INTEREST = "1000000" ;
5057
@@ -86,6 +93,9 @@ export class SolendActionCore {
8693
8794 hostAta ?: PublicKey ;
8895
96+ // TODO: potentially don't need to keep signers
97+ pullPriceTxns : Array < VersionedTransaction > ;
98+
8999 setupIxs : Array < TransactionInstruction > ;
90100
91101 lendingIxs : Array < TransactionInstruction > ;
@@ -136,6 +146,7 @@ export class SolendActionCore {
136146 this . obligationAddress = obligationAddress ;
137147 this . userTokenAccountAddress = userTokenAccountAddress ;
138148 this . userCollateralAccountAddress = userCollateralAccountAddress ;
149+ this . pullPriceTxns = [ ] as Array < VersionedTransaction > ;
139150 this . setupIxs = [ ] ;
140151 this . lendingIxs = [ ] ;
141152 this . cleanupIxs = [ ] ;
@@ -562,15 +573,17 @@ export class SolendActionCore {
562573 return txns ;
563574 }
564575
565- async getTransactions ( blockhash : BlockhashWithExpiryBlockHeight ) {
576+ async getTransactions ( blockhash : BlockhashWithExpiryBlockHeight , tipAmount ?: 9000 ) {
566577 const txns : {
567578 preLendingTxn : VersionedTransaction | null ;
568579 lendingTxn : VersionedTransaction | null ;
569580 postLendingTxn : VersionedTransaction | null ;
581+ pullPriceTxns : VersionedTransaction [ ] | null
570582 } = {
571583 preLendingTxn : null ,
572584 lendingTxn : null ,
573585 postLendingTxn : null ,
586+ pullPriceTxns : null ,
574587 } ;
575588
576589 if ( this . preTxnIxs . length ) {
@@ -591,6 +604,7 @@ export class SolendActionCore {
591604 ...this . setupIxs ,
592605 ...this . lendingIxs ,
593606 ...this . cleanupIxs ,
607+ ...this .
594608 ] ,
595609 } ) . compileToV0Message (
596610 this . lookupTableAccount ? [ this . lookupTableAccount ] : [ ]
@@ -607,6 +621,10 @@ export class SolendActionCore {
607621 ) ;
608622 }
609623
624+ if ( this . pullPriceTxns . length ) {
625+ txns . pullPriceTxns = this . pullPriceTxns ;
626+ }
627+
610628 return txns ;
611629 }
612630
@@ -831,6 +849,95 @@ export class SolendActionCore {
831849 }
832850 }
833851
852+ private async buildPullPriceTxns ( oracleKeys : Array < string > ) {
853+ const oracleAccounts = await this . connection . getMultipleAccountsInfo ( oracleKeys . map ( ( o ) => new PublicKey ( o ) ) , 'processed' )
854+ const priceServiceConnection = new PriceServiceConnection ( "https://hermes.pyth.network" ) ;
855+ const pythSolanaReceiver = new PythSolanaReceiver ( {
856+ connection : this . connection ,
857+ wallet : new NodeWallet ( Keypair . fromSeed ( new Uint8Array ( 32 ) . fill ( 1 ) ) )
858+ } ) ;
859+ const transactionBuilder = pythSolanaReceiver . newTransactionBuilder ( {
860+ closeUpdateAccounts : true ,
861+ } ) ;
862+
863+ const provider = new AnchorProvider ( this . connection , new NodeWallet ( Keypair . fromSeed ( new Uint8Array ( 32 ) . fill ( 1 ) ) ) , { } ) ;
864+ const idl = ( await Program . fetchIdl ( SB_ON_DEMAND_PID , provider ) ) ! ;
865+ const sbod = new Program ( idl , provider ) ;
866+
867+ const pythPulledOracles = oracleAccounts . filter ( o => o ?. owner . toBase58 ( ) === pythSolanaReceiver . receiver . programId . toBase58 ( ) ) ;
868+ if ( pythPulledOracles . length ) {
869+ const shuffledPriceIds = pythPulledOracles
870+ . map ( ( pythOracleData , index ) => {
871+ if ( ! pythOracleData ) {
872+ throw new Error ( `Could not find oracle data at index ${ index } ` ) ;
873+ }
874+ const priceUpdate = pythSolanaReceiver . receiver . account . priceUpdateV2 . coder . accounts . decode (
875+ 'priceUpdateV2' ,
876+ pythOracleData . data ,
877+ ) ;
878+
879+ return { key : Math . random ( ) , priceFeedId : priceUpdate . priceMessage . feedId } ;
880+ } )
881+ . sort ( ( a , b ) => a . key - b . key )
882+ . map ( ( x ) => x . priceFeedId ) ;
883+
884+ let priceFeedUpdateData ;
885+ priceFeedUpdateData = await priceServiceConnection . getLatestVaas (
886+ shuffledPriceIds
887+ ) ;
888+
889+ await transactionBuilder . addUpdatePriceFeed (
890+ priceFeedUpdateData ,
891+ 0 // shardId of 0
892+ ) ;
893+
894+ const transactionsWithSigners = await transactionBuilder . buildVersionedTransactions ( {
895+ tightComputeBudget : true ,
896+ } ) ;
897+
898+ for ( const transaction of transactionsWithSigners ) {
899+ const signers = transaction . signers ;
900+ let tx = transaction . tx ;
901+ if ( signers ) {
902+ tx . sign ( signers ) ;
903+ this . pullPriceTxns . push ( tx ) ;
904+ }
905+ }
906+ }
907+
908+ const sbPulledOracles = oracleKeys . filter ( ( _o , index ) => oracleAccounts [ index ] ?. owner . toBase58 ( ) === sbod . programId . toBase58 ( ) )
909+ if ( sbPulledOracles . length ) {
910+ const feedAccounts = sbPulledOracles . map ( ( oracleKey ) => new PullFeed ( sbod as any , oracleKey ) ) ;
911+ const crossbar = new CrossbarClient ( "https://crossbar.switchboard.xyz" ) ;
912+
913+ // Responses is Array<[pullIx, responses, success]>
914+ const responses = await Promise . all ( feedAccounts . map ( ( feedAccount ) => feedAccount . fetchUpdateIx ( { numSignatures : 1 , crossbarClient : crossbar } ) ) ) ;
915+ const oracles = responses . flatMap ( ( x ) => x [ 1 ] . map ( y => y . oracle ) ) ;
916+ const lookupTables = await loadLookupTables ( [ ...oracles , ...feedAccounts ] ) ;
917+
918+ const priorityFeeIx = ComputeBudgetProgram . setComputeUnitPrice ( {
919+ microLamports : 100_000 ,
920+ } ) ;
921+
922+ // Get the latest context
923+ const {
924+ value : { blockhash } ,
925+ } = await this . connection . getLatestBlockhashAndContext ( ) ;
926+
927+ // Get Transaction Message
928+ const message = new TransactionMessage ( {
929+ payerKey : this . publicKey ,
930+ recentBlockhash : blockhash ,
931+ instructions : [ priorityFeeIx , ...responses . map ( r => r [ 0 ] ! ) ] ,
932+ } ) . compileToV0Message ( lookupTables ) ;
933+
934+ // Get Versioned Transaction
935+ const vtx = new VersionedTransaction ( message ) ;
936+
937+ this . pullPriceTxns . push ( vtx ) ;
938+ }
939+ }
940+
834941 private async addRefreshIxs ( action : ActionType ) {
835942 // Union of addresses
836943 const allReserveAddresses = Array . from ( new Set ( [
@@ -840,6 +947,8 @@ export class SolendActionCore {
840947 ] ) ,
841948 ) ;
842949
950+ await this . buildPullPriceTxns ( allReserveAddresses ) ;
951+
843952 allReserveAddresses . forEach ( ( reserveAddress ) => {
844953 const reserveInfo = this . pool . reserves . find (
845954 ( reserve ) => reserve . address === reserveAddress
0 commit comments