Skip to content

Commit

Permalink
pull oracles
Browse files Browse the repository at this point in the history
  • Loading branch information
0xodia committed Jul 17, 2024
1 parent 8f9454a commit 832b29c
Show file tree
Hide file tree
Showing 4 changed files with 415 additions and 14 deletions.
120 changes: 120 additions & 0 deletions solend-sdk/__tests__/oracle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {
ComputeBudgetProgram,
Connection,
Keypair,
PublicKey,
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
import { parseObligation } from "../src";
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
import { PythSolanaReceiver, pythSolanaReceiverIdl } from "@pythnetwork/pyth-solana-receiver";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { AnchorProvider, Program } from "@coral-xyz/anchor-30";
import { CrossbarClient, loadLookupTables, PullFeed, SB_ON_DEMAND_PID } from "@switchboard-xyz/on-demand";

jest.setTimeout(50_000);

describe("check", function () {
it("pulls switchboard oracle", async function () {
const connection = new Connection("https://api.mainnet-beta.solana.com");
const testKey = []
if (testKey.length === 0) {
throw Error('Best tested with a throwaway mainnet test account.')
}

const provider = new AnchorProvider(connection, new NodeWallet(Keypair.fromSecretKey(new Uint8Array(
testKey
))), {});
const idl = (await Program.fetchIdl(SB_ON_DEMAND_PID, provider))!;
const sbod = new Program(idl, provider);

const sbPulledOracles = [
'2F9M59yYc28WMrAymNWceaBEk8ZmDAjUAKULp8seAJF3',
'AZcoqpWhMJUaKEDUfKsfzCr3Y96gSQwv43KSQ6KpeyQ1'
];

const feedAccounts = sbPulledOracles.map((oracleKey) => new PullFeed(sbod as any, oracleKey));
const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");

// Responses is Array<[pullIx, responses, success]>
const responses = await Promise.all(feedAccounts.map((feedAccount) => feedAccount.fetchUpdateIx({ numSignatures: 1, crossbarClient: crossbar })));
const oracles = responses.flatMap((x) => x[1].map(y => y.oracle));
const lookupTables = await loadLookupTables([...oracles, ...feedAccounts]);

// Get the latest context
const {
value: { blockhash },
} = await connection.getLatestBlockhashAndContext();

// Get Transaction Message
const message = new TransactionMessage({
payerKey: provider.publicKey,
recentBlockhash: blockhash,
instructions: [...responses.map(r => r[0]!)],
}).compileToV0Message(lookupTables);

// Get Versioned Transaction
const vtx = new VersionedTransaction(message);
provider.wallet.signAllTransactions([vtx]);
const sig = await connection.sendRawTransaction(vtx.serialize(), {skipPreflight: true});
await connection.confirmTransaction(sig, 'confirmed');
});

it("pulls pyth oracles", async function () {
const connection = new Connection("https://api.mainnet-beta.solana.com");
const testKey = []
if (testKey.length === 0) {
throw Error('Best tested with a throwaway mainnet test account.')
}
const priceServiceConnection = new PriceServiceConnection("https://hermes.pyth.network");
const pythSolanaReceiver = new PythSolanaReceiver({
connection: connection,
wallet: new NodeWallet(Keypair.fromSecretKey(new Uint8Array(
testKey
)))
});
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({
closeUpdateAccounts: true,
});

let priceFeedUpdateData;
priceFeedUpdateData = await priceServiceConnection.getLatestVaas(
[
'0x93c3def9b169f49eed14c9d73ed0e942c666cf0e1290657ec82038ebb792c2a8', // BLZE
'0xf2fc1dfcf51867abfa70874c929e920edc649e4997cbac88f280094df8c72bcd', // EUROE
]
);

await transactionBuilder.addUpdatePriceFeed(
priceFeedUpdateData,
0 // shardId of 0
);

const transactionsWithSigners = await transactionBuilder.buildVersionedTransactions({
tightComputeBudget: true,
});

const pullPriceTxns = [] as Array<VersionedTransaction>;

for (const transaction of transactionsWithSigners) {
const signers = transaction.signers;
let tx = transaction.tx;

if (signers) {
tx.sign(signers);
pullPriceTxns.push(tx);
}
}

pythSolanaReceiver.wallet.signAllTransactions(pullPriceTxns)

for (const tx of pullPriceTxns) {
const serializedTransaction = tx.serialize();
const sig = await connection.sendRawTransaction(serializedTransaction, {skipPreflight: true});
await connection.confirmTransaction(sig, 'confirmed');
}

});
});

5 changes: 4 additions & 1 deletion solend-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solendprotocol/solend-sdk",
"version": "0.10.9",
"version": "0.10.11",
"private": true,
"main": "src/index.ts",
"module": "src/index.ts",
Expand All @@ -21,10 +21,13 @@
"dependencies": {
"@project-serum/anchor": "^0.24.2",
"@pythnetwork/client": "^2.12.0",
"@pythnetwork/price-service-client": "^1.9.0",
"@pythnetwork/pyth-solana-receiver": "^0.8.0",
"@solana/buffer-layout": "=4.0.1",
"@solana/spl-token": "^0.3.7",
"@solana/web3.js": "=1.92.3",
"@solflare-wallet/utl-sdk": "^1.4.0",
"@switchboard-xyz/on-demand": "^1.1.39",
"@switchboard-xyz/sbv2-lite": "^0.2.4",
"axios": "^0.24.0",
"bignumber.js": "^9.0.2",
Expand Down
111 changes: 110 additions & 1 deletion solend-sdk/src/core/actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
AddressLookupTableAccount,
BlockhashWithExpiryBlockHeight,
ComputeBudgetProgram,
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
Expand All @@ -18,6 +20,7 @@ import {
} from "@solana/spl-token";
import BN from "bn.js";
import BigNumber from "bignumber.js";
import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver";
import {
Obligation,
OBLIGATION_SIZE,
Expand Down Expand Up @@ -45,6 +48,10 @@ import {
import { POSITION_LIMIT } from "./constants";
import { EnvironmentType, PoolType, ReserveType } from "./types";
import { getProgramId, U64_MAX, WAD } from "./constants";
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
import { AnchorProvider, Program } from "@coral-xyz/anchor-30";
import { CrossbarClient, loadLookupTables, PullFeed, SB_ON_DEMAND_PID } from "@switchboard-xyz/on-demand";

const SOL_PADDING_FOR_INTEREST = "1000000";

Expand Down Expand Up @@ -86,6 +93,9 @@ export class SolendActionCore {

hostAta?: PublicKey;

// TODO: potentially don't need to keep signers
pullPriceTxns: Array<VersionedTransaction>;

setupIxs: Array<TransactionInstruction>;

lendingIxs: Array<TransactionInstruction>;
Expand Down Expand Up @@ -136,6 +146,7 @@ export class SolendActionCore {
this.obligationAddress = obligationAddress;
this.userTokenAccountAddress = userTokenAccountAddress;
this.userCollateralAccountAddress = userCollateralAccountAddress;
this.pullPriceTxns = [] as Array<VersionedTransaction>;
this.setupIxs = [];
this.lendingIxs = [];
this.cleanupIxs = [];
Expand Down Expand Up @@ -562,15 +573,17 @@ export class SolendActionCore {
return txns;
}

async getTransactions(blockhash: BlockhashWithExpiryBlockHeight) {
async getTransactions(blockhash: BlockhashWithExpiryBlockHeight, tipAmount?: 9000 ) {
const txns: {
preLendingTxn: VersionedTransaction | null;
lendingTxn: VersionedTransaction | null;
postLendingTxn: VersionedTransaction | null;
pullPriceTxns: VersionedTransaction[] | null
} = {
preLendingTxn: null,
lendingTxn: null,
postLendingTxn: null,
pullPriceTxns: null,
};

if (this.preTxnIxs.length) {
Expand All @@ -591,6 +604,7 @@ export class SolendActionCore {
...this.setupIxs,
...this.lendingIxs,
...this.cleanupIxs,
...this.
],
}).compileToV0Message(
this.lookupTableAccount ? [this.lookupTableAccount] : []
Expand All @@ -607,6 +621,10 @@ export class SolendActionCore {
);
}

if (this.pullPriceTxns.length) {
txns.pullPriceTxns = this.pullPriceTxns;
}

return txns;
}

Expand Down Expand Up @@ -831,6 +849,95 @@ export class SolendActionCore {
}
}

private async buildPullPriceTxns(oracleKeys: Array<string>) {
const oracleAccounts = await this.connection.getMultipleAccountsInfo(oracleKeys.map((o) => new PublicKey(o)), 'processed')
const priceServiceConnection = new PriceServiceConnection("https://hermes.pyth.network");
const pythSolanaReceiver = new PythSolanaReceiver({
connection: this.connection,
wallet: new NodeWallet(Keypair.fromSeed(new Uint8Array(32).fill(1)))
});
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({
closeUpdateAccounts: true,
});

const provider = new AnchorProvider(this.connection, new NodeWallet(Keypair.fromSeed(new Uint8Array(32).fill(1))), {});
const idl = (await Program.fetchIdl(SB_ON_DEMAND_PID, provider))!;
const sbod = new Program(idl, provider);

const pythPulledOracles = oracleAccounts.filter(o => o?.owner.toBase58() === pythSolanaReceiver.receiver.programId.toBase58());
if (pythPulledOracles.length) {
const shuffledPriceIds = pythPulledOracles
.map((pythOracleData, index) => {
if (!pythOracleData) {
throw new Error(`Could not find oracle data at index ${index}`);
}
const priceUpdate = pythSolanaReceiver.receiver.account.priceUpdateV2.coder.accounts.decode(
'priceUpdateV2',
pythOracleData.data,
);

return { key: Math.random() , priceFeedId: priceUpdate.priceMessage.feedId };
})
.sort((a, b) => a.key - b.key)
.map((x) => x.priceFeedId);

let priceFeedUpdateData;
priceFeedUpdateData = await priceServiceConnection.getLatestVaas(
shuffledPriceIds
);

await transactionBuilder.addUpdatePriceFeed(
priceFeedUpdateData,
0 // shardId of 0
);

const transactionsWithSigners = await transactionBuilder.buildVersionedTransactions({
tightComputeBudget: true,
});

for (const transaction of transactionsWithSigners) {
const signers = transaction.signers;
let tx = transaction.tx;
if (signers) {
tx.sign(signers);
this.pullPriceTxns.push(tx);
}
}
}

const sbPulledOracles = oracleKeys.filter((_o, index) => oracleAccounts[index]?.owner.toBase58() === sbod.programId.toBase58())
if (sbPulledOracles.length) {
const feedAccounts = sbPulledOracles.map((oracleKey) => new PullFeed(sbod as any, oracleKey));
const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");

// Responses is Array<[pullIx, responses, success]>
const responses = await Promise.all(feedAccounts.map((feedAccount) => feedAccount.fetchUpdateIx({ numSignatures: 1, crossbarClient: crossbar })));
const oracles = responses.flatMap((x) => x[1].map(y => y.oracle));
const lookupTables = await loadLookupTables([...oracles, ...feedAccounts]);

const priorityFeeIx = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: 100_000,
});

// Get the latest context
const {
value: { blockhash },
} = await this.connection.getLatestBlockhashAndContext();

// Get Transaction Message
const message = new TransactionMessage({
payerKey: this.publicKey,
recentBlockhash: blockhash,
instructions: [priorityFeeIx, ...responses.map(r => r[0]!)],
}).compileToV0Message(lookupTables);

// Get Versioned Transaction
const vtx = new VersionedTransaction(message);

this.pullPriceTxns.push(vtx);
}
}

private async addRefreshIxs(action: ActionType) {
// Union of addresses
const allReserveAddresses = Array.from(new Set([
Expand All @@ -840,6 +947,8 @@ export class SolendActionCore {
]),
);

await this.buildPullPriceTxns(allReserveAddresses);

allReserveAddresses.forEach((reserveAddress) => {
const reserveInfo = this.pool.reserves.find(
(reserve) => reserve.address === reserveAddress
Expand Down
Loading

0 comments on commit 832b29c

Please sign in to comment.