diff --git a/package-lock.json b/package-lock.json index 949c378327..744497922c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56741,7 +56741,7 @@ }, "price_pusher": { "name": "@pythnetwork/price-pusher", - "version": "6.5.0", + "version": "6.6.0", "license": "Apache-2.0", "dependencies": { "@injectivelabs/sdk-ts": "1.10.72", @@ -59703,10 +59703,11 @@ }, "target_chains/solana/sdk/js/pyth_solana_receiver": { "name": "@pythnetwork/pyth-solana-receiver", - "version": "0.6.0", + "version": "0.7.0", "license": "Apache-2.0", "dependencies": { "@coral-xyz/anchor": "^0.29.0", + "@noble/hashes": "^1.4.0", "@pythnetwork/price-service-sdk": ">=1.6.0", "@pythnetwork/solana-utils": "*", "@solana/web3.js": "^1.90.0" @@ -59763,9 +59764,9 @@ } }, "target_chains/solana/sdk/js/pyth_solana_receiver/node_modules/@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "engines": { "node": ">= 16" }, @@ -59786,7 +59787,7 @@ }, "target_chains/solana/sdk/js/solana_utils": { "name": "@pythnetwork/solana-utils", - "version": "0.3.0", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { "@coral-xyz/anchor": "^0.29.0", @@ -71442,6 +71443,7 @@ "version": "file:target_chains/solana/sdk/js/pyth_solana_receiver", "requires": { "@coral-xyz/anchor": "^0.29.0", + "@noble/hashes": "^1.4.0", "@pythnetwork/price-service-sdk": ">=1.6.0", "@pythnetwork/solana-utils": "*", "@solana/web3.js": "^1.90.0", @@ -71487,9 +71489,9 @@ } }, "@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" }, "camelcase": { "version": "6.3.0", diff --git a/price_pusher/package.json b/price_pusher/package.json index 9bbd757055..38164b7bf2 100644 --- a/price_pusher/package.json +++ b/price_pusher/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/price-pusher", - "version": "6.5.0", + "version": "6.6.0", "description": "Pyth Price Pusher", "homepage": "https://pyth.network", "main": "lib/index.js", diff --git a/price_pusher/src/solana/solana.ts b/price_pusher/src/solana/solana.ts index 8433fe9340..eff1cccd06 100644 --- a/price_pusher/src/solana/solana.ts +++ b/price_pusher/src/solana/solana.ts @@ -146,6 +146,7 @@ export class SolanaPricePusherJito implements IPricePusher { priceFeedUpdateData, this.shardId ); + await transactionBuilder.addClosePreviousEncodedVaasInstructions(); const transactions = await transactionBuilder.buildVersionedTransactions({ jitoTipLamports: this.jitoTipLamports, diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/package.json b/target_chains/solana/sdk/js/pyth_solana_receiver/package.json index cafa1585d4..fa31a42ee2 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/package.json +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/pyth-solana-receiver", - "version": "0.6.0", + "version": "0.7.0", "description": "Pyth solana receiver SDK", "homepage": "https://pyth.network", "main": "lib/index.js", @@ -43,6 +43,7 @@ }, "dependencies": { "@coral-xyz/anchor": "^0.29.0", + "@noble/hashes": "^1.4.0", "@pythnetwork/price-service-sdk": ">=1.6.0", "@pythnetwork/solana-utils": "*", "@solana/web3.js": "^1.90.0" diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts index e2f0e891e0..d1bffe3eff 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts @@ -28,6 +28,7 @@ import { parsePriceFeedMessage, } from "@pythnetwork/price-service-sdk"; import { + CLOSE_ENCODED_VAA_COMPUTE_BUDGET, INIT_ENCODED_VAA_COMPUTE_BUDGET, POST_UPDATE_ATOMIC_COMPUTE_BUDGET, POST_UPDATE_COMPUTE_BUDGET, @@ -38,8 +39,8 @@ import { Wallet } from "@coral-xyz/anchor"; import { buildEncodedVaaCreateInstruction, buildWriteEncodedVaaWithSplitInstructions, + findEncodedVaaAccountsByWriteAuthority, getGuardianSetIndex, - overrideGuardianSet, trimSignatures, } from "./vaa"; import { @@ -263,6 +264,18 @@ export class PythTransactionBuilder extends TransactionBuilder { ); } + /** Add instructions to close encoded VAA accounts from previous actions. + * If you have previously used the PythTransactionBuilder with closeUpdateAccounts set to false or if you posted encoded VAAs but the transaction to close them did not land on-chain, your wallet might own many encoded VAA accounts. + * The rent cost for these accounts is 0.008 SOL per encoded VAA account. You can recover this rent calling this function when building a set of transactions. + */ + async addClosePreviousEncodedVaasInstructions(maxInstructions = 40) { + this.addInstructions( + await this.pythSolanaReceiver.buildClosePreviousEncodedVaasInstructions( + maxInstructions + ) + ); + } + /** * Returns all the added instructions batched into versioned transactions, plus for each transaction the ephemeral signers that need to sign it */ @@ -447,7 +460,6 @@ export class PythSolanaReceiver { encodedVaaAddress: PublicKey; closeInstructions: InstructionWithEphemeralSigners[]; }> { - vaa = overrideGuardianSet(vaa); // Short term fix Wormhole officially server guardian set 4 vaas const postInstructions: InstructionWithEphemeralSigners[] = []; const closeInstructions: InstructionWithEphemeralSigners[] = []; const encodedVaaKeypair = new Keypair(); @@ -664,7 +676,25 @@ export class PythSolanaReceiver { .closeEncodedVaa() .accounts({ encodedVaa }) .instruction(); - return { instruction, signers: [] }; + return { + instruction, + signers: [], + computeUnits: CLOSE_ENCODED_VAA_COMPUTE_BUDGET, + }; + } + + /** + * Build aset of instructions to close all the existing encoded VAA accounts owned by this PythSolanaReceiver's wallet + */ + async buildClosePreviousEncodedVaasInstructions( + maxInstructions: number + ): Promise { + const encodedVaas = await this.findOwnedEncodedVaaAccounts(); + const instructions = []; + for (const encodedVaa of encodedVaas) { + instructions.push(await this.buildCloseEncodedVaaInstruction(encodedVaa)); + } + return instructions.slice(0, maxInstructions); } /** @@ -739,6 +769,18 @@ export class PythSolanaReceiver { this.pushOracle.programId ); } + + /** + * Find all the encoded VAA accounts owned by this PythSolanaReceiver's wallet + * @returns a list of the public keys of the encoded VAA accounts + */ + async findOwnedEncodedVaaAccounts() { + return await findEncodedVaaAccountsByWriteAuthority( + this.receiver.provider.connection, + this.wallet.publicKey, + this.wormhole.programId + ); + } } /** diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/src/compute_budget.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/src/compute_budget.ts index e7eabd1b61..ff9398c13c 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/src/compute_budget.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/src/compute_budget.ts @@ -22,3 +22,7 @@ export const INIT_ENCODED_VAA_COMPUTE_BUDGET = 3000; * A hard-coded budget for the compute units required for the `writeEncodedVaa` instruction in the Wormhole program. */ export const WRITE_ENCODED_VAA_COMPUTE_BUDGET = 3000; +/** + * A hard-coded budget for the compute units required for the `closeEncodedVaa` instruction in the Wormhole program. + */ +export const CLOSE_ENCODED_VAA_COMPUTE_BUDGET = 30000; diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/src/index.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/src/index.ts index be05849880..36aa3f8491 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/src/index.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/src/index.ts @@ -6,7 +6,6 @@ export { TransactionBuilder, InstructionWithEphemeralSigners, } from "@pythnetwork/solana-utils"; - export { getConfigPda, DEFAULT_RECEIVER_PROGRAM_ID, diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/src/vaa.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/src/vaa.ts index d54b295251..8594c822c3 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/src/vaa.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/src/vaa.ts @@ -1,9 +1,10 @@ -import { Keypair, PublicKey } from "@solana/web3.js"; +import { Connection, Keypair, PublicKey } from "@solana/web3.js"; import { WormholeCoreBridgeSolana } from "./idl/wormhole_core_bridge_solana"; import { Program } from "@coral-xyz/anchor"; import { InstructionWithEphemeralSigners } from "@pythnetwork/solana-utils"; import { WRITE_ENCODED_VAA_COMPUTE_BUDGET } from "./compute_budget"; - +import { sha256 } from "@noble/hashes/sha256"; +import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes"; /** * Get the index of the guardian set that signed a VAA */ @@ -52,18 +53,6 @@ export function trimSignatures( return trimmedVaa; } -export const PREVIOUS_GUARDIAN_SET_INDEX = 4; -export const CURRENT_GUARDIAN_SET_INDEX = 4; -export function overrideGuardianSet(vaa: Buffer): Buffer { - const guardianSetIndex = getGuardianSetIndex(vaa); - - if (guardianSetIndex <= 3) { - vaa.writeUint32BE(CURRENT_GUARDIAN_SET_INDEX, 1); - } - - return vaa; -} - /** * The start of the VAA bytes in an encoded VAA account. Before this offset, the account contains a header. */ @@ -97,7 +86,7 @@ export async function buildEncodedVaaCreateInstruction( * This number was chosen as the biggest number such that one can still call `createInstruction`, `initEncodedVaa` and `writeEncodedVaa` in a single Solana transaction. * This way, the packing of the instructions to post an encoded vaa is more efficient. */ -export const VAA_SPLIT_INDEX = 792; +export const VAA_SPLIT_INDEX = 755; /** * Build a set of instructions to write a VAA to an encoded VAA account @@ -141,3 +130,33 @@ export async function buildWriteEncodedVaaWithSplitInstructions( }, ]; } + +/** + * Find all the encoded VAA accounts that have a given write authority + * @returns a list of the public keys of the encoded VAA accounts + */ +export async function findEncodedVaaAccountsByWriteAuthority( + connection: Connection, + writeAuthority: PublicKey, + wormholeProgramId: PublicKey +): Promise { + const result = await connection.getProgramAccounts(wormholeProgramId, { + filters: [ + { + memcmp: { + offset: 0, + bytes: bs58.encode( + Buffer.from(sha256("account:EncodedVaa").slice(0, 8)) + ), + }, + }, + { + memcmp: { + offset: 8 + 1, + bytes: bs58.encode(writeAuthority.toBuffer()), + }, + }, + ], + }); + return result.map((account) => new PublicKey(account.pubkey)); +} diff --git a/target_chains/solana/sdk/js/solana_utils/benchmarks/jito_benchmark.ts b/target_chains/solana/sdk/js/solana_utils/benchmarks/jito_benchmark.ts deleted file mode 100644 index 8bb6d4d423..0000000000 --- a/target_chains/solana/sdk/js/solana_utils/benchmarks/jito_benchmark.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Connection, Keypair } from "@solana/web3.js"; -import { PriceServiceConnection } from "@pythnetwork/price-service-client"; -import { sendTransactionsJito } from ".."; -import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver"; -import { Wallet } from "@coral-xyz/anchor"; -import fs from "fs"; -import os from "os"; -import { - SearcherClient, - searcherClient, -} from "jito-ts/dist/sdk/block-engine/searcher"; - -// Get price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable -const SOL_PRICE_FEED_ID = - "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; - -let keypairFile = ""; -if (process.env["SOLANA_KEYPAIR"]) { - keypairFile = process.env["SOLANA_KEYPAIR"]; -} else { - keypairFile = `${os.homedir()}/.config/solana/id.json`; -} - -const jitoKeypairFile = `${os.homedir()}/.config/solana/jito.json`; - -async function main() { - const connection = new Connection("http://api.mainnet-beta.solana.com"); - const keypair = await loadKeypairFromFile(keypairFile); - const jitoKeypair = await loadKeypairFromFile(jitoKeypairFile); - console.log( - `Sending transactions from account: ${keypair.publicKey.toBase58()}` - ); - const wallet = new Wallet(keypair); - const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet }); - - const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({ - closeUpdateAccounts: true, - }); - const priceUpdateData = await getPriceUpdateData(); - await transactionBuilder.addUpdatePriceFeed(priceUpdateData, 1); - - const c = searcherClient("mainnet.block-engine.jito.wtf", jitoKeypair); - - const transactions = await transactionBuilder.buildVersionedTransactions({ - tightComputeBudget: true, - jitoTipLamports: 100000, - }); - - await sendTransactionsJito(transactions, c, wallet); - - onBundleResult(c); -} - -export const onBundleResult = (c: SearcherClient) => { - c.onBundleResult( - (result) => { - console.log("received bundle result:", result); - }, - (e) => { - throw e; - } - ); -}; - -// Load a solana keypair from an id.json file -async function loadKeypairFromFile(filePath: string): Promise { - try { - const keypairData = JSON.parse( - await fs.promises.readFile(filePath, "utf8") - ); - console.log(keypairData); - return Keypair.fromSecretKey(Uint8Array.from(keypairData)); - } catch (error) { - throw new Error(`Error loading keypair from file: ${error}`); - } -} -main(); - -async function getPriceUpdateData() { - const priceServiceConnection = new PriceServiceConnection( - "https://hermes.pyth.network/", - { priceFeedRequestConfig: { binary: true } } - ); - - return await priceServiceConnection.getLatestVaas([SOL_PRICE_FEED_ID]); -} diff --git a/target_chains/solana/sdk/js/solana_utils/package.json b/target_chains/solana/sdk/js/solana_utils/package.json index a28cfe8587..898e7fa087 100644 --- a/target_chains/solana/sdk/js/solana_utils/package.json +++ b/target_chains/solana/sdk/js/solana_utils/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/solana-utils", - "version": "0.3.0", + "version": "0.4.0", "description": "Utility functions for Solana", "homepage": "https://pyth.network", "main": "lib/index.js", diff --git a/target_chains/solana/sdk/js/solana_utils/src/jito.ts b/target_chains/solana/sdk/js/solana_utils/src/jito.ts index a0991f624b..fa7b1844ad 100644 --- a/target_chains/solana/sdk/js/solana_utils/src/jito.ts +++ b/target_chains/solana/sdk/js/solana_utils/src/jito.ts @@ -1,5 +1,11 @@ import { Wallet } from "@coral-xyz/anchor"; -import { PublicKey, Signer, VersionedTransaction } from "@solana/web3.js"; +import { + PublicKey, + Signer, + SystemProgram, + TransactionInstruction, + VersionedTransaction, +} from "@solana/web3.js"; import bs58 from "bs58"; import { SearcherClient } from "jito-ts/dist/sdk/block-engine/searcher"; import { Bundle } from "jito-ts/dist/sdk/block-engine/types"; @@ -20,6 +26,17 @@ export function getRandomTipAccount(): PublicKey { return new PublicKey(TIP_ACCOUNTS[randomInt]); } +export function buildJitoTipInstruction( + payer: PublicKey, + lamports: number +): TransactionInstruction { + return SystemProgram.transfer({ + fromPubkey: payer, + toPubkey: getRandomTipAccount(), + lamports, + }); +} + export async function sendTransactionsJito( transactions: { tx: VersionedTransaction; diff --git a/target_chains/solana/sdk/js/solana_utils/src/transaction.ts b/target_chains/solana/sdk/js/solana_utils/src/transaction.ts index 1077b4c179..9b9229bec0 100644 --- a/target_chains/solana/sdk/js/solana_utils/src/transaction.ts +++ b/target_chains/solana/sdk/js/solana_utils/src/transaction.ts @@ -6,19 +6,19 @@ import { PACKET_DATA_SIZE, PublicKey, Signer, - SystemProgram, Transaction, TransactionInstruction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js"; import bs58 from "bs58"; -import { TIP_ACCOUNTS, getRandomTipAccount } from "./jito"; +import { buildJitoTipInstruction } from "./jito"; /** * If the transaction doesn't contain a `setComputeUnitLimit` instruction, the default compute budget is 200,000 units per instruction. */ export const DEFAULT_COMPUTE_BUDGET_UNITS = 200000; + /** * The maximum size of a Solana transaction, leaving some room for the compute budget instructions. */ @@ -178,33 +178,36 @@ export class TransactionBuilder { signers: signers, computeUnits: computeUnits ?? 0, }); - } else if ( - getSizeOfTransaction( + } else { + const sizeWithComputeUnits = getSizeOfTransaction( [ ...this.transactionInstructions[ this.transactionInstructions.length - 1 ].instructions, instruction, + buildJitoTipInstruction(this.payer, 1), + ComputeBudgetProgram.setComputeUnitLimit({ units: 1 }), ], true, this.addressLookupTable - ) <= PACKET_DATA_SIZE_WITH_ROOM_FOR_COMPUTE_BUDGET - ) { - this.transactionInstructions[ - this.transactionInstructions.length - 1 - ].instructions.push(instruction); - this.transactionInstructions[ - this.transactionInstructions.length - 1 - ].signers.push(...signers); - this.transactionInstructions[ - this.transactionInstructions.length - 1 - ].computeUnits += computeUnits ?? 0; - } else - this.transactionInstructions.push({ - instructions: [instruction], - signers: signers, - computeUnits: computeUnits ?? 0, - }); + ); + if (sizeWithComputeUnits <= PACKET_DATA_SIZE) { + this.transactionInstructions[ + this.transactionInstructions.length - 1 + ].instructions.push(instruction); + this.transactionInstructions[ + this.transactionInstructions.length - 1 + ].signers.push(...signers); + this.transactionInstructions[ + this.transactionInstructions.length - 1 + ].computeUnits += computeUnits ?? 0; + } else + this.transactionInstructions.push({ + instructions: [instruction], + signers: signers, + computeUnits: computeUnits ?? 0, + }); + } } /** @@ -249,16 +252,9 @@ export class TransactionBuilder { }) ); } - if ( - args.jitoTipLamports && - index % jitoBundleSize === jitoBundleSize - 1 - ) { + if (args.jitoTipLamports && index % jitoBundleSize === 0) { instructionsWithComputeBudget.push( - SystemProgram.transfer({ - fromPubkey: this.payer, - toPubkey: getRandomTipAccount(), - lamports: args.jitoTipLamports, - }) + buildJitoTipInstruction(this.payer, args.jitoTipLamports) ); } @@ -307,16 +303,9 @@ export class TransactionBuilder { }) ); } - if ( - args.jitoTipLamports && - index % jitoBundleSize === jitoBundleSize - 1 - ) { + if (args.jitoTipLamports && index % jitoBundleSize === 0) { instructionsWithComputeBudget.push( - SystemProgram.transfer({ - fromPubkey: this.payer, - toPubkey: getRandomTipAccount(), - lamports: args.jitoTipLamports, - }) + buildJitoTipInstruction(this.payer, args.jitoTipLamports) ); }