فهرست منبع

feat: support closing vaas (#1460)

* feat: support closing vaas

* Go

* Max out

* Cleanup

* Refactor, add comments

* Add max

* Remove script

* bump solana utils

* Revert "Fix: guardian set (#1459)"

This reverts commit d9c85d8f9ddfb9e9fd985475f6d28b08670611c6.

* Update compute budget

* Go

* Restore

* Bump
guibescos 1 سال پیش
والد
کامیت
8d92ad9931

+ 11 - 9
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",

+ 1 - 1
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",

+ 1 - 0
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,

+ 2 - 1
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"

+ 45 - 3
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<InstructionWithEphemeralSigners[]> {
+    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
+    );
+  }
 }
 
 /**

+ 4 - 0
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;

+ 0 - 1
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,

+ 34 - 15
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<PublicKey[]> {
+  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));
+}

+ 0 - 86
target_chains/solana/sdk/js/solana_utils/benchmarks/jito_benchmark.ts

@@ -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<Keypair> {
-  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]);
-}

+ 1 - 1
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",

+ 18 - 1
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;

+ 28 - 39
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)
           );
         }