Skip to content

Commit 832b29c

Browse files
committed
pull oracles
1 parent 8f9454a commit 832b29c

File tree

4 files changed

+415
-14
lines changed

4 files changed

+415
-14
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import {
2+
ComputeBudgetProgram,
3+
Connection,
4+
Keypair,
5+
PublicKey,
6+
TransactionMessage,
7+
VersionedTransaction,
8+
} from "@solana/web3.js";
9+
import { parseObligation } from "../src";
10+
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
11+
import { PythSolanaReceiver, pythSolanaReceiverIdl } from "@pythnetwork/pyth-solana-receiver";
12+
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
13+
import { AnchorProvider, Program } from "@coral-xyz/anchor-30";
14+
import { CrossbarClient, loadLookupTables, PullFeed, SB_ON_DEMAND_PID } from "@switchboard-xyz/on-demand";
15+
16+
jest.setTimeout(50_000);
17+
18+
describe("check", function () {
19+
it("pulls switchboard oracle", async function () {
20+
const connection = new Connection("https://api.mainnet-beta.solana.com");
21+
const testKey = []
22+
if (testKey.length === 0) {
23+
throw Error('Best tested with a throwaway mainnet test account.')
24+
}
25+
26+
const provider = new AnchorProvider(connection, new NodeWallet(Keypair.fromSecretKey(new Uint8Array(
27+
testKey
28+
))), {});
29+
const idl = (await Program.fetchIdl(SB_ON_DEMAND_PID, provider))!;
30+
const sbod = new Program(idl, provider);
31+
32+
const sbPulledOracles = [
33+
'2F9M59yYc28WMrAymNWceaBEk8ZmDAjUAKULp8seAJF3',
34+
'AZcoqpWhMJUaKEDUfKsfzCr3Y96gSQwv43KSQ6KpeyQ1'
35+
];
36+
37+
const feedAccounts = sbPulledOracles.map((oracleKey) => new PullFeed(sbod as any, oracleKey));
38+
const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");
39+
40+
// Responses is Array<[pullIx, responses, success]>
41+
const responses = await Promise.all(feedAccounts.map((feedAccount) => feedAccount.fetchUpdateIx({ numSignatures: 1, crossbarClient: crossbar })));
42+
const oracles = responses.flatMap((x) => x[1].map(y => y.oracle));
43+
const lookupTables = await loadLookupTables([...oracles, ...feedAccounts]);
44+
45+
// Get the latest context
46+
const {
47+
value: { blockhash },
48+
} = await connection.getLatestBlockhashAndContext();
49+
50+
// Get Transaction Message
51+
const message = new TransactionMessage({
52+
payerKey: provider.publicKey,
53+
recentBlockhash: blockhash,
54+
instructions: [...responses.map(r => r[0]!)],
55+
}).compileToV0Message(lookupTables);
56+
57+
// Get Versioned Transaction
58+
const vtx = new VersionedTransaction(message);
59+
provider.wallet.signAllTransactions([vtx]);
60+
const sig = await connection.sendRawTransaction(vtx.serialize(), {skipPreflight: true});
61+
await connection.confirmTransaction(sig, 'confirmed');
62+
});
63+
64+
it("pulls pyth oracles", async function () {
65+
const connection = new Connection("https://api.mainnet-beta.solana.com");
66+
const testKey = []
67+
if (testKey.length === 0) {
68+
throw Error('Best tested with a throwaway mainnet test account.')
69+
}
70+
const priceServiceConnection = new PriceServiceConnection("https://hermes.pyth.network");
71+
const pythSolanaReceiver = new PythSolanaReceiver({
72+
connection: connection,
73+
wallet: new NodeWallet(Keypair.fromSecretKey(new Uint8Array(
74+
testKey
75+
)))
76+
});
77+
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({
78+
closeUpdateAccounts: true,
79+
});
80+
81+
let priceFeedUpdateData;
82+
priceFeedUpdateData = await priceServiceConnection.getLatestVaas(
83+
[
84+
'0x93c3def9b169f49eed14c9d73ed0e942c666cf0e1290657ec82038ebb792c2a8', // BLZE
85+
'0xf2fc1dfcf51867abfa70874c929e920edc649e4997cbac88f280094df8c72bcd', // EUROE
86+
]
87+
);
88+
89+
await transactionBuilder.addUpdatePriceFeed(
90+
priceFeedUpdateData,
91+
0 // shardId of 0
92+
);
93+
94+
const transactionsWithSigners = await transactionBuilder.buildVersionedTransactions({
95+
tightComputeBudget: true,
96+
});
97+
98+
const pullPriceTxns = [] as Array<VersionedTransaction>;
99+
100+
for (const transaction of transactionsWithSigners) {
101+
const signers = transaction.signers;
102+
let tx = transaction.tx;
103+
104+
if (signers) {
105+
tx.sign(signers);
106+
pullPriceTxns.push(tx);
107+
}
108+
}
109+
110+
pythSolanaReceiver.wallet.signAllTransactions(pullPriceTxns)
111+
112+
for (const tx of pullPriceTxns) {
113+
const serializedTransaction = tx.serialize();
114+
const sig = await connection.sendRawTransaction(serializedTransaction, {skipPreflight: true});
115+
await connection.confirmTransaction(sig, 'confirmed');
116+
}
117+
118+
});
119+
});
120+

solend-sdk/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@solendprotocol/solend-sdk",
3-
"version": "0.10.9",
3+
"version": "0.10.11",
44
"private": true,
55
"main": "src/index.ts",
66
"module": "src/index.ts",
@@ -21,10 +21,13 @@
2121
"dependencies": {
2222
"@project-serum/anchor": "^0.24.2",
2323
"@pythnetwork/client": "^2.12.0",
24+
"@pythnetwork/price-service-client": "^1.9.0",
25+
"@pythnetwork/pyth-solana-receiver": "^0.8.0",
2426
"@solana/buffer-layout": "=4.0.1",
2527
"@solana/spl-token": "^0.3.7",
2628
"@solana/web3.js": "=1.92.3",
2729
"@solflare-wallet/utl-sdk": "^1.4.0",
30+
"@switchboard-xyz/on-demand": "^1.1.39",
2831
"@switchboard-xyz/sbv2-lite": "^0.2.4",
2932
"axios": "^0.24.0",
3033
"bignumber.js": "^9.0.2",

solend-sdk/src/core/actions.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {
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";
1921
import BN from "bn.js";
2022
import BigNumber from "bignumber.js";
23+
import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver";
2124
import {
2225
Obligation,
2326
OBLIGATION_SIZE,
@@ -45,6 +48,10 @@ import {
4548
import { POSITION_LIMIT } from "./constants";
4649
import { EnvironmentType, PoolType, ReserveType } from "./types";
4750
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";
4855

4956
const 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

Comments
 (0)