|
@@ -1,4 +1,4 @@
|
|
|
-import { AnchorProvider, Program } from "@coral-xyz/anchor";
|
|
|
|
|
|
|
+import { AnchorProvider, IdlAccounts, Program } from "@coral-xyz/anchor";
|
|
|
import {
|
|
import {
|
|
|
Connection,
|
|
Connection,
|
|
|
Signer,
|
|
Signer,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
IDL as WormholeCoreBridgeSolanaIdl,
|
|
IDL as WormholeCoreBridgeSolanaIdl,
|
|
|
} from "./idl/wormhole_core_bridge_solana";
|
|
} from "./idl/wormhole_core_bridge_solana";
|
|
|
import {
|
|
import {
|
|
|
|
|
+ DEFAULT_PUSH_ORACLE_PROGRAM_ID,
|
|
|
DEFAULT_RECEIVER_PROGRAM_ID,
|
|
DEFAULT_RECEIVER_PROGRAM_ID,
|
|
|
DEFAULT_TREASURY_ID,
|
|
DEFAULT_TREASURY_ID,
|
|
|
DEFAULT_WORMHOLE_PROGRAM_ID,
|
|
DEFAULT_WORMHOLE_PROGRAM_ID,
|
|
@@ -43,7 +44,13 @@ import {
|
|
|
InstructionWithEphemeralSigners,
|
|
InstructionWithEphemeralSigners,
|
|
|
PriorityFeeConfig,
|
|
PriorityFeeConfig,
|
|
|
} from "@pythnetwork/solana-utils";
|
|
} from "@pythnetwork/solana-utils";
|
|
|
|
|
+import {
|
|
|
|
|
+ PythPushOracle,
|
|
|
|
|
+ IDL as PythPushOracleIdl,
|
|
|
|
|
+} from "./idl/pyth_push_oracle";
|
|
|
|
|
|
|
|
|
|
+export type PriceUpdateAccount =
|
|
|
|
|
+ IdlAccounts<PythSolanaReceiverProgram>["priceUpdateV2"];
|
|
|
/**
|
|
/**
|
|
|
* Configuration for the PythTransactionBuilder
|
|
* Configuration for the PythTransactionBuilder
|
|
|
* @property closeUpdateAccounts (default: true) if true, the builder will add instructions to close the price update accounts and the encoded vaa accounts to recover the rent
|
|
* @property closeUpdateAccounts (default: true) if true, the builder will add instructions to close the price update accounts and the encoded vaa accounts to recover the rent
|
|
@@ -93,6 +100,18 @@ export class PythTransactionBuilder extends TransactionBuilder {
|
|
|
* Add instructions to post price updates to the builder.
|
|
* Add instructions to post price updates to the builder.
|
|
|
*
|
|
*
|
|
|
* @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
|
|
* @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @example
|
|
|
|
|
+ * ```typescript
|
|
|
|
|
+ * const priceUpdateData = await priceServiceConnection.getLatestVaas([
|
|
|
|
|
+ * SOL_PRICE_FEED_ID,
|
|
|
|
|
+ * ETH_PRICE_FEED_ID,
|
|
|
|
|
+ * ]);
|
|
|
|
|
+ *
|
|
|
|
|
+ * const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
|
|
|
|
|
+ * await transactionBuilder.addPostPriceUpdates(priceUpdateData);
|
|
|
|
|
+ * await transactionBuilder.addPriceConsumerInstructions(...)
|
|
|
|
|
+ * ```
|
|
|
*/
|
|
*/
|
|
|
async addPostPriceUpdates(priceUpdateDataArray: string[]) {
|
|
async addPostPriceUpdates(priceUpdateDataArray: string[]) {
|
|
|
const {
|
|
const {
|
|
@@ -147,11 +166,50 @@ export class PythTransactionBuilder extends TransactionBuilder {
|
|
|
this.addInstructions(postInstructions);
|
|
this.addInstructions(postInstructions);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Add instructions to update price feed accounts to the builder.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
|
|
|
|
|
+ * @param shardId the shard ID of the set of price feed accounts. This shard ID allows for multiple sets of price feed accounts to be managed by the same program.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Price feed accounts are a special type of price update accounts.
|
|
|
|
|
+ * Instead of using ephemeral addresses, they are PDAs of the Pyth Push Oracle program derived from the feed ID. They can only be updated with a more recent price update.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @example
|
|
|
|
|
+ * ```typescript
|
|
|
|
|
+ * const priceUpdateData = await priceServiceConnection.getLatestVaas([
|
|
|
|
|
+ * SOL_PRICE_FEED_ID,
|
|
|
|
|
+ * ETH_PRICE_FEED_ID,
|
|
|
|
|
+ * ]);
|
|
|
|
|
+ *
|
|
|
|
|
+ * const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
|
|
|
|
|
+ * await transactionBuilder.addUpdatePriceFeed(priceUpdateData);
|
|
|
|
|
+ * await transactionBuilder.addPriceConsumerInstructions(...)
|
|
|
|
|
+ * ...
|
|
|
|
|
+ * ```
|
|
|
|
|
+ */
|
|
|
|
|
+ async addUpdatePriceFeed(priceUpdateDataArray: string[], shardId: number) {
|
|
|
|
|
+ const {
|
|
|
|
|
+ postInstructions,
|
|
|
|
|
+ priceFeedIdToPriceUpdateAccount,
|
|
|
|
|
+ closeInstructions,
|
|
|
|
|
+ } = await this.pythSolanaReceiver.buildUpdatePriceFeedInstructions(
|
|
|
|
|
+ priceUpdateDataArray,
|
|
|
|
|
+ shardId
|
|
|
|
|
+ );
|
|
|
|
|
+ this.closeInstructions.push(...closeInstructions);
|
|
|
|
|
+ Object.assign(
|
|
|
|
|
+ this.priceFeedIdToPriceUpdateAccount,
|
|
|
|
|
+ priceFeedIdToPriceUpdateAccount
|
|
|
|
|
+ );
|
|
|
|
|
+ this.addInstructions(postInstructions);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Add instructions that consume price updates to the builder.
|
|
* Add instructions that consume price updates to the builder.
|
|
|
*
|
|
*
|
|
|
* @param getInstructions a function that given a mapping of price feed IDs to price update accounts, generates a series of instructions. Price updates get posted to ephemeral accounts and this function allows the user to indicate which accounts in their instruction need to be "replaced" with each price update account.
|
|
* @param getInstructions a function that given a mapping of price feed IDs to price update accounts, generates a series of instructions. Price updates get posted to ephemeral accounts and this function allows the user to indicate which accounts in their instruction need to be "replaced" with each price update account.
|
|
|
- * If multiple price updates for the same price feed id are posted with the same builder, the account corresponding to the last update to get posted will be used.
|
|
|
|
|
|
|
+ * If multiple price updates for the same price feed ID are posted with the same builder, the account corresponding to the last update to get posted will be used.
|
|
|
*
|
|
*
|
|
|
* @example
|
|
* @example
|
|
|
* ```typescript
|
|
* ```typescript
|
|
@@ -212,8 +270,8 @@ export class PythTransactionBuilder extends TransactionBuilder {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * This method is used to retrieve the address of the price update account where the price update for a given price feed id will be posted.
|
|
|
|
|
- * If multiple price updates for the same price feed id will be posted with the same builder, the address of the account corresponding to the last update to get posted will be returned.
|
|
|
|
|
|
|
+ * This method is used to retrieve the address of the price update account where the price update for a given price feed ID will be posted.
|
|
|
|
|
+ * If multiple price updates for the same price feed ID will be posted with the same builder, the address of the account corresponding to the last update to get posted will be returned.
|
|
|
* */
|
|
* */
|
|
|
getPriceUpdateAccount(priceFeedId: string): PublicKey {
|
|
getPriceUpdateAccount(priceFeedId: string): PublicKey {
|
|
|
const priceUpdateAccount =
|
|
const priceUpdateAccount =
|
|
@@ -240,17 +298,20 @@ export class PythSolanaReceiver {
|
|
|
readonly provider: AnchorProvider;
|
|
readonly provider: AnchorProvider;
|
|
|
readonly receiver: Program<PythSolanaReceiverProgram>;
|
|
readonly receiver: Program<PythSolanaReceiverProgram>;
|
|
|
readonly wormhole: Program<WormholeCoreBridgeSolana>;
|
|
readonly wormhole: Program<WormholeCoreBridgeSolana>;
|
|
|
|
|
+ readonly pushOracle: Program<PythPushOracle>;
|
|
|
|
|
|
|
|
constructor({
|
|
constructor({
|
|
|
connection,
|
|
connection,
|
|
|
wallet,
|
|
wallet,
|
|
|
wormholeProgramId = DEFAULT_WORMHOLE_PROGRAM_ID,
|
|
wormholeProgramId = DEFAULT_WORMHOLE_PROGRAM_ID,
|
|
|
receiverProgramId = DEFAULT_RECEIVER_PROGRAM_ID,
|
|
receiverProgramId = DEFAULT_RECEIVER_PROGRAM_ID,
|
|
|
|
|
+ pushOracleProgramId = DEFAULT_PUSH_ORACLE_PROGRAM_ID,
|
|
|
}: {
|
|
}: {
|
|
|
connection: Connection;
|
|
connection: Connection;
|
|
|
wallet: Wallet;
|
|
wallet: Wallet;
|
|
|
wormholeProgramId?: PublicKey;
|
|
wormholeProgramId?: PublicKey;
|
|
|
receiverProgramId?: PublicKey;
|
|
receiverProgramId?: PublicKey;
|
|
|
|
|
+ pushOracleProgramId?: PublicKey;
|
|
|
}) {
|
|
}) {
|
|
|
this.connection = connection;
|
|
this.connection = connection;
|
|
|
this.wallet = wallet;
|
|
this.wallet = wallet;
|
|
@@ -267,6 +328,11 @@ export class PythSolanaReceiver {
|
|
|
wormholeProgramId,
|
|
wormholeProgramId,
|
|
|
this.provider
|
|
this.provider
|
|
|
);
|
|
);
|
|
|
|
|
+ this.pushOracle = new Program<PythPushOracle>(
|
|
|
|
|
+ PythPushOracleIdl as PythPushOracle,
|
|
|
|
|
+ pushOracleProgramId,
|
|
|
|
|
+ this.provider
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -491,6 +557,88 @@ export class PythSolanaReceiver {
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Build a series of helper instructions that update one or many price feed accounts and another series to close the encoded vaa accounts used to update the price feed accounts.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
|
|
|
|
|
+ * @param shardId the shard ID of the set of price feed accounts. This shard ID allows for multiple sets of price feed accounts to be managed by the same program.
|
|
|
|
|
+ * @returns `postInstructions`: the instructions to update the price feed accounts. If the price feed accounts don't contain a recent update, these should be called before consuming the price updates.
|
|
|
|
|
+ * @returns `priceFeedIdToPriceUpdateAccount`: this is a map of price feed IDs to Solana address. Given a price feed ID, you can use this map to find the account where `postInstructions` will post the price update. Note that since price feed accounts are PDAs, the address of the account can also be found with `getPriceFeedAccountAddress`.
|
|
|
|
|
+ * @returns `closeInstructions`: the instructions to close the encoded VAA accounts that were used to update the price feed accounts.
|
|
|
|
|
+ */
|
|
|
|
|
+ async buildUpdatePriceFeedInstructions(
|
|
|
|
|
+ priceUpdateDataArray: string[],
|
|
|
|
|
+ shardId: number
|
|
|
|
|
+ ): Promise<{
|
|
|
|
|
+ postInstructions: InstructionWithEphemeralSigners[];
|
|
|
|
|
+ priceFeedIdToPriceUpdateAccount: Record<string, PublicKey>;
|
|
|
|
|
+ closeInstructions: InstructionWithEphemeralSigners[];
|
|
|
|
|
+ }> {
|
|
|
|
|
+ const postInstructions: InstructionWithEphemeralSigners[] = [];
|
|
|
|
|
+ const priceFeedIdToPriceUpdateAccount: Record<string, PublicKey> = {};
|
|
|
|
|
+ const closeInstructions: InstructionWithEphemeralSigners[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ for (const priceUpdateData of priceUpdateDataArray) {
|
|
|
|
|
+ const accumulatorUpdateData = parseAccumulatorUpdateData(
|
|
|
|
|
+ Buffer.from(priceUpdateData, "base64")
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const {
|
|
|
|
|
+ postInstructions: postEncodedVaaInstructions,
|
|
|
|
|
+ encodedVaaAddress: encodedVaa,
|
|
|
|
|
+ closeInstructions: postEncodedVaacloseInstructions,
|
|
|
|
|
+ } = await this.buildPostEncodedVaaInstructions(accumulatorUpdateData.vaa);
|
|
|
|
|
+ postInstructions.push(...postEncodedVaaInstructions);
|
|
|
|
|
+ closeInstructions.push(...postEncodedVaacloseInstructions);
|
|
|
|
|
+
|
|
|
|
|
+ for (const update of accumulatorUpdateData.updates) {
|
|
|
|
|
+ const feedId = parsePriceFeedMessage(update.message).feedId;
|
|
|
|
|
+
|
|
|
|
|
+ postInstructions.push({
|
|
|
|
|
+ instruction: await this.pushOracle.methods
|
|
|
|
|
+ .updatePriceFeed(
|
|
|
|
|
+ {
|
|
|
|
|
+ merklePriceUpdate: update,
|
|
|
|
|
+ treasuryId: DEFAULT_TREASURY_ID,
|
|
|
|
|
+ },
|
|
|
|
|
+ shardId,
|
|
|
|
|
+ Array.from(feedId)
|
|
|
|
|
+ )
|
|
|
|
|
+ .accounts({
|
|
|
|
|
+ pythSolanaReceiver: this.receiver.programId,
|
|
|
|
|
+ encodedVaa,
|
|
|
|
|
+ priceFeedAccount: getPriceFeedAccountAddress(
|
|
|
|
|
+ shardId,
|
|
|
|
|
+ feedId,
|
|
|
|
|
+ this.pushOracle.programId
|
|
|
|
|
+ ),
|
|
|
|
|
+ treasury: getTreasuryPda(
|
|
|
|
|
+ DEFAULT_TREASURY_ID,
|
|
|
|
|
+ this.receiver.programId
|
|
|
|
|
+ ),
|
|
|
|
|
+ config: getConfigPda(this.receiver.programId),
|
|
|
|
|
+ })
|
|
|
|
|
+ .instruction(),
|
|
|
|
|
+ signers: [],
|
|
|
|
|
+ computeUnits: POST_UPDATE_COMPUTE_BUDGET,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ priceFeedIdToPriceUpdateAccount[
|
|
|
|
|
+ "0x" + parsePriceFeedMessage(update.message).feedId.toString("hex")
|
|
|
|
|
+ ] = getPriceFeedAccountAddress(
|
|
|
|
|
+ shardId,
|
|
|
|
|
+ feedId,
|
|
|
|
|
+ this.pushOracle.programId
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ postInstructions,
|
|
|
|
|
+ priceFeedIdToPriceUpdateAccount,
|
|
|
|
|
+ closeInstructions,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Build an instruction to close an encoded VAA account, recovering the rent.
|
|
* Build an instruction to close an encoded VAA account, recovering the rent.
|
|
|
*/
|
|
*/
|
|
@@ -531,4 +679,60 @@ export class PythSolanaReceiver {
|
|
|
priorityFeeConfig
|
|
priorityFeeConfig
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Fetch the contents of a price update account
|
|
|
|
|
+ * @param priceUpdateAccount The address of the price update account
|
|
|
|
|
+ * @returns The contents of the deserialized price update account or `null` if the account doesn't exist
|
|
|
|
|
+ */
|
|
|
|
|
+ async fetchPriceUpdateAccount(
|
|
|
|
|
+ priceUpdateAccount: PublicKey
|
|
|
|
|
+ ): Promise<PriceUpdateAccount | null> {
|
|
|
|
|
+ return this.receiver.account.priceUpdateV2.fetchNullable(
|
|
|
|
|
+ priceUpdateAccount
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Fetch the contents of a price feed account
|
|
|
|
|
+ * @param shardId The shard ID of the set of price feed accounts. This shard ID allows for multiple sets of price feed accounts to be managed by the same program.
|
|
|
|
|
+ * @param priceFeedId The price feed ID.
|
|
|
|
|
+ * @returns The contents of the deserialized price feed account or `null` if the account doesn't exist
|
|
|
|
|
+ */
|
|
|
|
|
+ async fetchPriceFeedAccount(
|
|
|
|
|
+ shardId: number,
|
|
|
|
|
+ priceFeedId: Buffer
|
|
|
|
|
+ ): Promise<PriceUpdateAccount | null> {
|
|
|
|
|
+ return this.receiver.account.priceUpdateV2.fetchNullable(
|
|
|
|
|
+ getPriceFeedAccountAddress(
|
|
|
|
|
+ shardId,
|
|
|
|
|
+ priceFeedId,
|
|
|
|
|
+ this.pushOracle.programId
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Derive the address of a price feed account
|
|
|
|
|
+ * @param shardId The shard ID of the set of price feed accounts. This shard ID allows for multiple sets of price feed accounts to be managed by the same program.
|
|
|
|
|
+ * @param priceFeedId The price feed ID.
|
|
|
|
|
+ * @param pushOracleProgramId The program ID of the Pyth Push Oracle program. If not provided, the default deployment will be used.
|
|
|
|
|
+ * @returns The address of the price feed account
|
|
|
|
|
+ */
|
|
|
|
|
+function getPriceFeedAccountAddress(
|
|
|
|
|
+ shardId: number,
|
|
|
|
|
+ feedId: Buffer,
|
|
|
|
|
+ pushOracleProgramId?: PublicKey
|
|
|
|
|
+): PublicKey {
|
|
|
|
|
+ if (feedId.length != 32) {
|
|
|
|
|
+ throw new Error("Feed ID should be 32 bytes long");
|
|
|
|
|
+ }
|
|
|
|
|
+ const shardBuffer = Buffer.alloc(2);
|
|
|
|
|
+ shardBuffer.writeUint16LE(shardId, 0);
|
|
|
|
|
+
|
|
|
|
|
+ return PublicKey.findProgramAddressSync(
|
|
|
|
|
+ [shardBuffer, feedId],
|
|
|
|
|
+ pushOracleProgramId ?? DEFAULT_PUSH_ORACLE_PROGRAM_ID
|
|
|
|
|
+ )[0];
|
|
|
}
|
|
}
|