1
1
import {
2
2
AddressLookupTableAccount ,
3
3
BlockhashWithExpiryBlockHeight ,
4
+ ComputeBudgetProgram ,
4
5
Connection ,
6
+ Keypair ,
5
7
PublicKey ,
6
8
SystemProgram ,
7
9
Transaction ,
@@ -18,6 +20,7 @@ import {
18
20
} from "@solana/spl-token" ;
19
21
import BN from "bn.js" ;
20
22
import BigNumber from "bignumber.js" ;
23
+ import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver" ;
21
24
import {
22
25
Obligation ,
23
26
OBLIGATION_SIZE ,
@@ -45,6 +48,10 @@ import {
45
48
import { POSITION_LIMIT } from "./constants" ;
46
49
import { EnvironmentType , PoolType , ReserveType } from "./types" ;
47
50
import { 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" ;
48
55
49
56
const SOL_PADDING_FOR_INTEREST = "1000000" ;
50
57
@@ -86,6 +93,9 @@ export class SolendActionCore {
86
93
87
94
hostAta ?: PublicKey ;
88
95
96
+ // TODO: potentially don't need to keep signers
97
+ pullPriceTxns : Array < VersionedTransaction > ;
98
+
89
99
setupIxs : Array < TransactionInstruction > ;
90
100
91
101
lendingIxs : Array < TransactionInstruction > ;
@@ -136,6 +146,7 @@ export class SolendActionCore {
136
146
this . obligationAddress = obligationAddress ;
137
147
this . userTokenAccountAddress = userTokenAccountAddress ;
138
148
this . userCollateralAccountAddress = userCollateralAccountAddress ;
149
+ this . pullPriceTxns = [ ] as Array < VersionedTransaction > ;
139
150
this . setupIxs = [ ] ;
140
151
this . lendingIxs = [ ] ;
141
152
this . cleanupIxs = [ ] ;
@@ -562,15 +573,17 @@ export class SolendActionCore {
562
573
return txns ;
563
574
}
564
575
565
- async getTransactions ( blockhash : BlockhashWithExpiryBlockHeight ) {
576
+ async getTransactions ( blockhash : BlockhashWithExpiryBlockHeight , tipAmount ?: 9000 ) {
566
577
const txns : {
567
578
preLendingTxn : VersionedTransaction | null ;
568
579
lendingTxn : VersionedTransaction | null ;
569
580
postLendingTxn : VersionedTransaction | null ;
581
+ pullPriceTxns : VersionedTransaction [ ] | null
570
582
} = {
571
583
preLendingTxn : null ,
572
584
lendingTxn : null ,
573
585
postLendingTxn : null ,
586
+ pullPriceTxns : null ,
574
587
} ;
575
588
576
589
if ( this . preTxnIxs . length ) {
@@ -591,6 +604,7 @@ export class SolendActionCore {
591
604
...this . setupIxs ,
592
605
...this . lendingIxs ,
593
606
...this . cleanupIxs ,
607
+ ...this .
594
608
] ,
595
609
} ) . compileToV0Message (
596
610
this . lookupTableAccount ? [ this . lookupTableAccount ] : [ ]
@@ -607,6 +621,10 @@ export class SolendActionCore {
607
621
) ;
608
622
}
609
623
624
+ if ( this . pullPriceTxns . length ) {
625
+ txns . pullPriceTxns = this . pullPriceTxns ;
626
+ }
627
+
610
628
return txns ;
611
629
}
612
630
@@ -831,6 +849,95 @@ export class SolendActionCore {
831
849
}
832
850
}
833
851
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
+
834
941
private async addRefreshIxs ( action : ActionType ) {
835
942
// Union of addresses
836
943
const allReserveAddresses = Array . from ( new Set ( [
@@ -840,6 +947,8 @@ export class SolendActionCore {
840
947
] ) ,
841
948
) ;
842
949
950
+ await this . buildPullPriceTxns ( allReserveAddresses ) ;
951
+
843
952
allReserveAddresses . forEach ( ( reserveAddress ) => {
844
953
const reserveInfo = this . pool . reserves . find (
845
954
( reserve ) => reserve . address === reserveAddress
0 commit comments