From 8c445100e6d799b0dce0d5a759552b8a191f3aea Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 1 Apr 2024 16:50:17 +0100 Subject: [PATCH 01/28] Checkpoint --- price_pusher/src/index.ts | 2 + price_pusher/src/solana/command.ts | 63 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 price_pusher/src/solana/command.ts diff --git a/price_pusher/src/index.ts b/price_pusher/src/index.ts index eebd248254..3ab3b0374f 100644 --- a/price_pusher/src/index.ts +++ b/price_pusher/src/index.ts @@ -6,6 +6,7 @@ import evm from "./evm/command"; import aptos from "./aptos/command"; import sui from "./sui/command"; import near from "./near/command"; +import solana from "./solana/command"; yargs(hideBin(process.argv)) .config("config") @@ -15,4 +16,5 @@ yargs(hideBin(process.argv)) .command(aptos) .command(sui) .command(near) + .command(solana) .help().argv; diff --git a/price_pusher/src/solana/command.ts b/price_pusher/src/solana/command.ts new file mode 100644 index 0000000000..41b065d2bc --- /dev/null +++ b/price_pusher/src/solana/command.ts @@ -0,0 +1,63 @@ +import { Options } from "yargs"; +import * as options from "../options"; +import { readPriceConfigFile } from "../price-config"; +import { PriceServiceConnection } from "@pythnetwork/price-service-client"; +import { PythPriceListener } from "../pyth-price-listener"; + +export default { + command: "solana", + describe: "run price pusher for solana", + builder: { + endpoint: { + description: "Solana RPC API endpoint", + type: "string", + required: true, + } as Options, + "keypair-file": { + description: "Path to keypair file", + type: "string", + required: true, + } as Options, + ...options.priceConfigFile, + ...options.priceServiceEndpoint, + ...options.pythContractAddress, + ...options.pollingFrequency, + ...options.pushingFrequency, + }, + handler: function (argv: any) { + const { + endpoint, + keypairFile, + priceConfigFile, + priceServiceEndpoint, + pythContractAddress, + pushingFrequency, + pollingFrequency, + } = argv; + + const priceConfigs = readPriceConfigFile(priceConfigFile); + + const priceServiceConnection = new PriceServiceConnection( + priceServiceEndpoint, + { + logger: { + // Log only warnings and errors from the price service client + info: () => undefined, + warn: console.warn, + error: console.error, + debug: () => undefined, + trace: () => undefined, + }, + } + ); + + const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias })); + + const pythListener = new PythPriceListener( + priceServiceConnection, + priceItems + ); + + console.log("hello world"); + }, +}; From cd84738b9189e4095de277c87a924bfecd47f224 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Mon, 1 Apr 2024 18:43:57 +0100 Subject: [PATCH 02/28] Checkpoint --- .../config.solana.testnet.sample.json | 4 + price_pusher/package.json | 1 + price_pusher/src/solana/command.ts | 26 ++- price_pusher/src/solana/solana.ts | 22 +++ .../programs/pyth-push-oracle/src/lib.rs | 4 + .../pyth_solana_receiver_sdk/src/lib.rs | 2 +- .../src/PythSolanaReceiver.ts | 57 ++++++- .../js/pyth_solana_receiver/src/address.ts | 4 + .../src/idl/pyth_push_oracle.ts | 153 ++++++++++++++++++ 9 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 price_pusher/config.solana.testnet.sample.json create mode 100644 price_pusher/src/solana/solana.ts create mode 100644 target_chains/solana/sdk/js/pyth_solana_receiver/src/idl/pyth_push_oracle.ts diff --git a/price_pusher/config.solana.testnet.sample.json b/price_pusher/config.solana.testnet.sample.json new file mode 100644 index 0000000000..0dd526afff --- /dev/null +++ b/price_pusher/config.solana.testnet.sample.json @@ -0,0 +1,4 @@ +{ + "endpoint": "https://api.devnet.solana.com", + "keypair-file": "/keypair" +} diff --git a/price_pusher/package.json b/price_pusher/package.json index 9fcbccc084..11694eefc6 100644 --- a/price_pusher/package.json +++ b/price_pusher/package.json @@ -52,6 +52,7 @@ }, "dependencies": { "@injectivelabs/sdk-ts": "1.10.72", + "@pythnetwork/pyth-solana-receiver": "*", "@mysten/sui.js": "^0.49.1", "@pythnetwork/price-service-client": "*", "@pythnetwork/pyth-sdk-solidity": "*", diff --git a/price_pusher/src/solana/command.ts b/price_pusher/src/solana/command.ts index 41b065d2bc..f42e1c08c4 100644 --- a/price_pusher/src/solana/command.ts +++ b/price_pusher/src/solana/command.ts @@ -3,6 +3,10 @@ import * as options from "../options"; import { readPriceConfigFile } from "../price-config"; import { PriceServiceConnection } from "@pythnetwork/price-service-client"; import { PythPriceListener } from "../pyth-price-listener"; +import { SolanaPriceListener, SolanaPricePusher } from "./solana"; +import { Controller } from "../controller"; +import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver"; +import { KeyPair } from "near-api-js"; export default { command: "solana", @@ -58,6 +62,26 @@ export default { priceItems ); - console.log("hello world"); + // const pythSolanaReceiver = new PythSolanaReceiver( + // {connection : new Connection(endpoint), + // keypair : } + // ) + + const solanaPricePusher = new SolanaPricePusher(); + const solanaPriceListener = new SolanaPriceListener( + "solana", + pollingFrequency, + priceItems + ); + + const controller = new Controller( + priceConfigs, + pythListener, + solanaPriceListener, + solanaPricePusher, + { pushingFrequency } + ); + + controller.start(); }, }; diff --git a/price_pusher/src/solana/solana.ts b/price_pusher/src/solana/solana.ts new file mode 100644 index 0000000000..167bdce9f8 --- /dev/null +++ b/price_pusher/src/solana/solana.ts @@ -0,0 +1,22 @@ +import { ChainPriceListener, IPricePusher, PriceInfo } from "../interface"; + +export class SolanaPriceListener extends ChainPriceListener { + async getOnChainPriceInfo(priceId: string): Promise { + console.log( + `Polled a Solana on chain price for feed ${this.priceIdToAlias.get( + priceId + )} (${priceId}).` + ); + + return undefined; + } +} + +export class SolanaPricePusher implements IPricePusher { + async updatePriceFeed( + priceIds: string[], + pubTimesToPush: number[] + ): Promise { + console.log("successful"); + } +} diff --git a/target_chains/solana/programs/pyth-push-oracle/src/lib.rs b/target_chains/solana/programs/pyth-push-oracle/src/lib.rs index b05c18e8e6..b069fd9078 100644 --- a/target_chains/solana/programs/pyth-push-oracle/src/lib.rs +++ b/target_chains/solana/programs/pyth-push-oracle/src/lib.rs @@ -89,10 +89,14 @@ pub struct UpdatePriceFeed<'info> { #[account(mut)] pub payer: Signer<'info>, pub pyth_solana_receiver: Program<'info, PythSolanaReceiver>, + /// CHECK: Checked by CPI into the Pyth Solana Receiver pub encoded_vaa: AccountInfo<'info>, + /// CHECK: Checked by CPI into the Pyth Solana Receiver pub config: AccountInfo<'info>, + /// CHECK: Checked by CPI into the Pyth Solana Receiver #[account(mut)] pub treasury: AccountInfo<'info>, + /// CHECK: This account's seeds are checked #[account(mut, seeds = [&shard_id.to_le_bytes(), &feed_id], bump)] pub price_feed_account: AccountInfo<'info>, pub system_program: Program<'info, System>, diff --git a/target_chains/solana/pyth_solana_receiver_sdk/src/lib.rs b/target_chains/solana/pyth_solana_receiver_sdk/src/lib.rs index cdd244d1f5..64a9a1099f 100644 --- a/target_chains/solana/pyth_solana_receiver_sdk/src/lib.rs +++ b/target_chains/solana/pyth_solana_receiver_sdk/src/lib.rs @@ -12,4 +12,4 @@ pub mod price_update; declare_id!("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ"); -pub const PYTH_PUSH_ORACLE_ID: Pubkey = pubkey!("F9SP6tBXw9Af7BYauo7Y2R5Es2mpv8FP5aNCXMihp6Za"); +pub const PYTH_PUSH_ORACLE_ID: Pubkey = pubkey!("3a8iuFcGaMHFTX8sagDx55nPWp14fHxzWUbn2Mr4E8NR"); 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 7b368b4331..394ba06f20 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 @@ -1,4 +1,9 @@ -import { AnchorProvider, Program } from "@coral-xyz/anchor"; +import { + AnchorProvider, + IdlAccounts, + IdlTypes, + Program, +} from "@coral-xyz/anchor"; import { Connection, Signer, @@ -14,6 +19,7 @@ import { IDL as WormholeCoreBridgeSolanaIdl, } from "./idl/wormhole_core_bridge_solana"; import { + DEFAULT_PUSH_ORACLE_PROGRAM_ID, DEFAULT_RECEIVER_PROGRAM_ID, DEFAULT_TREASURY_ID, DEFAULT_WORMHOLE_PROGRAM_ID, @@ -43,7 +49,13 @@ import { InstructionWithEphemeralSigners, PriorityFeeConfig, } from "@pythnetwork/solana-utils"; +import { + PythPushOracle, + IDL as PythPushOracleIdl, +} from "./idl/pyth_push_oracle"; +export type PriceUpdateAccount = + IdlAccounts["priceUpdateV2"]; /** * 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 @@ -240,17 +252,20 @@ export class PythSolanaReceiver { readonly provider: AnchorProvider; readonly receiver: Program; readonly wormhole: Program; + readonly pushOracle: Program; constructor({ connection, wallet, wormholeProgramId = DEFAULT_WORMHOLE_PROGRAM_ID, receiverProgramId = DEFAULT_RECEIVER_PROGRAM_ID, + pushOracleProgramId = DEFAULT_PUSH_ORACLE_PROGRAM_ID, }: { connection: Connection; wallet: Wallet; wormholeProgramId?: PublicKey; receiverProgramId?: PublicKey; + pushOracleProgramId?: PublicKey; }) { this.connection = connection; this.wallet = wallet; @@ -267,6 +282,11 @@ export class PythSolanaReceiver { wormholeProgramId, this.provider ); + this.pushOracle = new Program( + PythPushOracleIdl as PythPushOracle, + pushOracleProgramId, + this.provider + ); } /** @@ -531,4 +551,39 @@ export class PythSolanaReceiver { priorityFeeConfig ); } + + async fetchPriceUpdateAccount( + priceUpdateAccount: PublicKey + ): Promise { + return this.receiver.account.priceUpdateV2.fetchNullable( + priceUpdateAccount + ); + } + + async fetchPriceFeedAccount( + shardId: number, + priceFeedId: Buffer + ): Promise { + return this.receiver.account.priceUpdateV2.fetchNullable( + getPriceFeedAccountAddress(0, priceFeedId, this.pushOracle.programId) + ); + } +} + +function getPriceFeedAccountAddress( + shardId: number, + feedId: Buffer, + pushOracleProgramId?: PublicKey +) { + if (feedId.length != 32) { + throw new Error("Feed ID should be 32 bytes long"); + } + const seedBuffer = Buffer.alloc(2 + feedId.length); + seedBuffer.writeUInt16BE(shardId, 0); + seedBuffer.copy(feedId, 2); + + return PublicKey.findProgramAddressSync( + [seedBuffer], + pushOracleProgramId ?? DEFAULT_PUSH_ORACLE_PROGRAM_ID + )[0]; } diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/src/address.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/src/address.ts index 5c99376d83..cb42d3efe0 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/src/address.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/src/address.ts @@ -15,6 +15,10 @@ export const DEFAULT_WORMHOLE_PROGRAM_ID = new PublicKey( "HDwcJBJXjL9FpJ7UBsYBtaDjsBUhuLCUYoz3zr8SWWaQ" ); +export const DEFAULT_PUSH_ORACLE_PROGRAM_ID = new PublicKey( + "3a8iuFcGaMHFTX8sagDx55nPWp14fHxzWUbn2Mr4E8NR" +); + /** * Returns the address of a guardian set account from the Wormhole program. */ diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/src/idl/pyth_push_oracle.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/src/idl/pyth_push_oracle.ts new file mode 100644 index 0000000000..d6df4c72a2 --- /dev/null +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/src/idl/pyth_push_oracle.ts @@ -0,0 +1,153 @@ +export type PythPushOracle = { + version: "0.1.0"; + name: "pyth_push_oracle"; + instructions: [ + { + name: "updatePriceFeed"; + accounts: [ + { + name: "payer"; + isMut: true; + isSigner: true; + }, + { + name: "pythSolanaReceiver"; + isMut: false; + isSigner: false; + }, + { + name: "encodedVaa"; + isMut: false; + isSigner: false; + }, + { + name: "config"; + isMut: false; + isSigner: false; + }, + { + name: "treasury"; + isMut: true; + isSigner: false; + }, + { + name: "priceFeedAccount"; + isMut: true; + isSigner: false; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + } + ]; + args: [ + { + name: "params"; + type: { + defined: "PostUpdateParams"; + }; + }, + { + name: "shardId"; + type: "u16"; + }, + { + name: "feedId"; + type: { + defined: "FeedId"; + }; + } + ]; + } + ]; + errors: [ + { + code: 6000; + name: "UpdatesNotMonotonic"; + msg: "Updates must be monotonically increasing"; + }, + { + code: 6001; + name: "PriceFeedMessageMismatch"; + msg: "Trying to update price feed with the wrong feed id"; + } + ]; +}; + +export const IDL: PythPushOracle = { + version: "0.1.0", + name: "pyth_push_oracle", + instructions: [ + { + name: "updatePriceFeed", + accounts: [ + { + name: "payer", + isMut: true, + isSigner: true, + }, + { + name: "pythSolanaReceiver", + isMut: false, + isSigner: false, + }, + { + name: "encodedVaa", + isMut: false, + isSigner: false, + }, + { + name: "config", + isMut: false, + isSigner: false, + }, + { + name: "treasury", + isMut: true, + isSigner: false, + }, + { + name: "priceFeedAccount", + isMut: true, + isSigner: false, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + ], + args: [ + { + name: "params", + type: { + defined: "PostUpdateParams", + }, + }, + { + name: "shardId", + type: "u16", + }, + { + name: "feedId", + type: { + defined: "FeedId", + }, + }, + ], + }, + ], + errors: [ + { + code: 6000, + name: "UpdatesNotMonotonic", + msg: "Updates must be monotonically increasing", + }, + { + code: 6001, + name: "PriceFeedMessageMismatch", + msg: "Trying to update price feed with the wrong feed id", + }, + ], +}; From 304f8f3fdb3052cbf93dcc0ddb59b53c64db51fa Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 2 Apr 2024 14:32:21 +0100 Subject: [PATCH 03/28] Checkpoint --- price_pusher/src/solana/command.ts | 26 ++++-- price_pusher/src/solana/solana.ts | 53 +++++++++-- .../src/PythSolanaReceiver.ts | 88 +++++++++++++++++++ .../src/idl/pyth_push_oracle.ts | 80 +++++++++++++++++ 4 files changed, 230 insertions(+), 17 deletions(-) diff --git a/price_pusher/src/solana/command.ts b/price_pusher/src/solana/command.ts index f42e1c08c4..0ab62f866d 100644 --- a/price_pusher/src/solana/command.ts +++ b/price_pusher/src/solana/command.ts @@ -6,7 +6,9 @@ import { PythPriceListener } from "../pyth-price-listener"; import { SolanaPriceListener, SolanaPricePusher } from "./solana"; import { Controller } from "../controller"; import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver"; -import { KeyPair } from "near-api-js"; +import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; +import { Keypair, Connection } from "@solana/web3.js"; +import fs from "fs"; export default { command: "solana", @@ -62,16 +64,22 @@ export default { priceItems ); - // const pythSolanaReceiver = new PythSolanaReceiver( - // {connection : new Connection(endpoint), - // keypair : } - // ) + const wallet = new NodeWallet( + Keypair.fromSecretKey( + Uint8Array.from(JSON.parse(fs.readFileSync(keypairFile, "ascii"))) + ) + ); + + const pythSolanaReceiver = new PythSolanaReceiver({ + connection: new Connection(endpoint), + wallet, + }); - const solanaPricePusher = new SolanaPricePusher(); + const solanaPricePusher = new SolanaPricePusher(pythSolanaReceiver); const solanaPriceListener = new SolanaPriceListener( - "solana", - pollingFrequency, - priceItems + pythSolanaReceiver, + priceItems, + { pollingFrequency } ); const controller = new Controller( diff --git a/price_pusher/src/solana/solana.ts b/price_pusher/src/solana/solana.ts index 167bdce9f8..8deb6c8bc1 100644 --- a/price_pusher/src/solana/solana.ts +++ b/price_pusher/src/solana/solana.ts @@ -1,18 +1,55 @@ -import { ChainPriceListener, IPricePusher, PriceInfo } from "../interface"; +import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver"; +import { + ChainPriceListener, + IPricePusher, + PriceInfo, + PriceItem, +} from "../interface"; +import { DurationInSeconds } from "../utils"; export class SolanaPriceListener extends ChainPriceListener { - async getOnChainPriceInfo(priceId: string): Promise { - console.log( - `Polled a Solana on chain price for feed ${this.priceIdToAlias.get( - priceId - )} (${priceId}).` - ); + constructor( + private pythSolanaReceiver: PythSolanaReceiver, + priceItems: PriceItem[], + config: { + pollingFrequency: DurationInSeconds; + } + ) { + super("solana", config.pollingFrequency, priceItems); + } - return undefined; + async getOnChainPriceInfo(priceId: string): Promise { + try { + const priceFeedAccount = + await this.pythSolanaReceiver.fetchPriceFeedAccount( + 0, + Buffer.from(priceId, "hex") + ); + console.log( + `Polled a Solana on chain price for feed ${this.priceIdToAlias.get( + priceId + )} (${priceId}).` + ); + if (priceFeedAccount) { + return { + conf: priceFeedAccount.priceMessage.conf.toString(), + price: priceFeedAccount.priceMessage.price.toString(), + publishTime: priceFeedAccount.priceMessage.publishTime.toNumber(), + }; + } else { + return undefined; + } + } catch (e) { + console.error(`Polling on-chain price for ${priceId} failed. Error:`); + console.error(e); + return undefined; + } } } export class SolanaPricePusher implements IPricePusher { + constructor(private pythSolanaReceiver: PythSolanaReceiver) {} + async updatePriceFeed( priceIds: string[], pubTimesToPush: number[] 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 394ba06f20..07c367d2b7 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 @@ -159,6 +159,22 @@ export class PythTransactionBuilder extends TransactionBuilder { this.addInstructions(postInstructions); } + async addUpdatePriceFeed(priceUpdateDataArray: string[]) { + const { + postInstructions, + priceFeedIdToPriceUpdateAccount, + closeInstructions, + } = await this.pythSolanaReceiver.buildUpdatePriceFeedInstructions( + priceUpdateDataArray + ); + this.closeInstructions.push(...closeInstructions); + Object.assign( + this.priceFeedIdToPriceUpdateAccount, + priceFeedIdToPriceUpdateAccount + ); + this.addInstructions(postInstructions); + } + /** * Add instructions that consume price updates to the builder. * @@ -511,6 +527,78 @@ export class PythSolanaReceiver { }; } + async buildUpdatePriceFeedInstructions( + priceUpdateDataArray: string[] + ): Promise<{ + postInstructions: InstructionWithEphemeralSigners[]; + priceFeedIdToPriceUpdateAccount: Record; + closeInstructions: InstructionWithEphemeralSigners[]; + }> { + const postInstructions: InstructionWithEphemeralSigners[] = []; + const priceFeedIdToPriceUpdateAccount: Record = {}; + 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, + }, + 0, + feedId + ) + .accounts({ + encodedVaa, + priceFeedAccount: getPriceFeedAccountAddress( + 0, + 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(0, feedId, this.pushOracle.programId); + closeInstructions.push( + await this.buildClosePriceUpdateInstruction( + getPriceFeedAccountAddress(0, feedId, this.pushOracle.programId) + ) + ); + } + } + return { + postInstructions, + priceFeedIdToPriceUpdateAccount, + closeInstructions, + }; + } + /** * Build an instruction to close an encoded VAA account, recovering the rent. */ diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/src/idl/pyth_push_oracle.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/src/idl/pyth_push_oracle.ts index d6df4c72a2..f648458941 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/src/idl/pyth_push_oracle.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/src/idl/pyth_push_oracle.ts @@ -61,6 +61,46 @@ export type PythPushOracle = { ]; } ]; + types: [ + { + name: "PostUpdateParams"; + type: { + kind: "struct"; + fields: [ + { + name: "merklePriceUpdate"; + type: { + defined: "MerklePriceUpdate"; + }; + }, + { + name: "treasuryId"; + type: "u8"; + } + ]; + }; + }, + { + name: "MerklePriceUpdate"; + type: { + kind: "struct"; + fields: [ + { + name: "message"; + type: "bytes"; + }, + { + name: "proof"; + type: { + vec: { + array: ["u8", 20]; + }; + }; + } + ]; + }; + } + ]; errors: [ { code: 6000; @@ -138,6 +178,46 @@ export const IDL: PythPushOracle = { ], }, ], + types: [ + { + name: "PostUpdateParams", + type: { + kind: "struct", + fields: [ + { + name: "merklePriceUpdate", + type: { + defined: "MerklePriceUpdate", + }, + }, + { + name: "treasuryId", + type: "u8", + }, + ], + }, + }, + { + name: "MerklePriceUpdate", + type: { + kind: "struct", + fields: [ + { + name: "message", + type: "bytes", + }, + { + name: "proof", + type: { + vec: { + array: ["u8", 20], + }, + }, + }, + ], + }, + }, + ], errors: [ { code: 6000, From bcc092d78dd49f886eed955fa02f24f8e497c136 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 2 Apr 2024 15:52:38 +0100 Subject: [PATCH 04/28] Checkpoint --- price_pusher/src/solana/command.ts | 5 ++- price_pusher/src/solana/solana.ts | 41 ++++++++++++++++++- .../src/PythSolanaReceiver.ts | 7 +--- .../src/idl/pyth_push_oracle.ts | 4 +- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/price_pusher/src/solana/command.ts b/price_pusher/src/solana/command.ts index 0ab62f866d..307eada90d 100644 --- a/price_pusher/src/solana/command.ts +++ b/price_pusher/src/solana/command.ts @@ -75,7 +75,10 @@ export default { wallet, }); - const solanaPricePusher = new SolanaPricePusher(pythSolanaReceiver); + const solanaPricePusher = new SolanaPricePusher( + pythSolanaReceiver, + priceServiceConnection + ); const solanaPriceListener = new SolanaPriceListener( pythSolanaReceiver, priceItems, diff --git a/price_pusher/src/solana/solana.ts b/price_pusher/src/solana/solana.ts index 8deb6c8bc1..a7c99d60d6 100644 --- a/price_pusher/src/solana/solana.ts +++ b/price_pusher/src/solana/solana.ts @@ -6,6 +6,7 @@ import { PriceItem, } from "../interface"; import { DurationInSeconds } from "../utils"; +import { PriceServiceConnection } from "@pythnetwork/price-service-client"; export class SolanaPriceListener extends ChainPriceListener { constructor( @@ -48,12 +49,48 @@ export class SolanaPriceListener extends ChainPriceListener { } export class SolanaPricePusher implements IPricePusher { - constructor(private pythSolanaReceiver: PythSolanaReceiver) {} + constructor( + private pythSolanaReceiver: PythSolanaReceiver, + private priceServiceConnection: PriceServiceConnection + ) {} async updatePriceFeed( priceIds: string[], pubTimesToPush: number[] ): Promise { - console.log("successful"); + if (priceIds.length === 0) { + return; + } + + let priceFeedUpdateData; + try { + priceFeedUpdateData = await this.priceServiceConnection.getLatestVaas( + priceIds + ); + } catch (e: any) { + console.error(new Date(), "getPriceFeedsUpdateData failed:", e); + return; + } + + const transactionBuilder = this.pythSolanaReceiver.newTransactionBuilder({ + closeUpdateAccounts: false, + }); + transactionBuilder.addUpdatePriceFeed(priceFeedUpdateData); + + try { + const transactionHashes = await this.pythSolanaReceiver.provider.sendAll( + await transactionBuilder.buildVersionedTransactions({ + computeUnitPriceMicroLamports: 50000, + }) + ); + console.log( + `Successful. Tx hash: ${ + transactionHashes[transactionHashes.length - 1] + }` + ); + } catch (e: any) { + console.error("Failed pushing"); + return; + } } } 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 07c367d2b7..f5639dda9b 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 @@ -562,7 +562,7 @@ export class PythSolanaReceiver { treasuryId: DEFAULT_TREASURY_ID, }, 0, - feedId + Array.from(feedId) ) .accounts({ encodedVaa, @@ -585,11 +585,6 @@ export class PythSolanaReceiver { priceFeedIdToPriceUpdateAccount[ "0x" + parsePriceFeedMessage(update.message).feedId.toString("hex") ] = getPriceFeedAccountAddress(0, feedId, this.pushOracle.programId); - closeInstructions.push( - await this.buildClosePriceUpdateInstruction( - getPriceFeedAccountAddress(0, feedId, this.pushOracle.programId) - ) - ); } } return { diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/src/idl/pyth_push_oracle.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/src/idl/pyth_push_oracle.ts index f648458941..3ebe124d4a 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/src/idl/pyth_push_oracle.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/src/idl/pyth_push_oracle.ts @@ -55,7 +55,7 @@ export type PythPushOracle = { { name: "feedId"; type: { - defined: "FeedId"; + array: ["u8", 32]; }; } ]; @@ -172,7 +172,7 @@ export const IDL: PythPushOracle = { { name: "feedId", type: { - defined: "FeedId", + array: ["u8", 32], }, }, ], From 5ff2490386974cb461f18907eee4b55040d025d7 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 2 Apr 2024 16:00:52 +0100 Subject: [PATCH 05/28] fix: pusher --- .../sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 f5639dda9b..6d9428d10e 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 @@ -565,6 +565,7 @@ export class PythSolanaReceiver { Array.from(feedId) ) .accounts({ + pythSolanaReceiver: this.receiver.programId, encodedVaa, priceFeedAccount: getPriceFeedAccountAddress( 0, @@ -661,12 +662,11 @@ function getPriceFeedAccountAddress( if (feedId.length != 32) { throw new Error("Feed ID should be 32 bytes long"); } - const seedBuffer = Buffer.alloc(2 + feedId.length); - seedBuffer.writeUInt16BE(shardId, 0); - seedBuffer.copy(feedId, 2); + const shardBuffer = Buffer.alloc(2); + shardBuffer.writeUInt16BE(shardId, 0); return PublicKey.findProgramAddressSync( - [seedBuffer], + [shardBuffer, feedId], pushOracleProgramId ?? DEFAULT_PUSH_ORACLE_PROGRAM_ID )[0]; } From 0eb1fc59eea135f6f10256a43357d3c4dfd8e716 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 2 Apr 2024 16:24:11 +0100 Subject: [PATCH 06/28] Checkpoint --- price_pusher/src/solana/solana.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/price_pusher/src/solana/solana.ts b/price_pusher/src/solana/solana.ts index a7c99d60d6..03f9417cb8 100644 --- a/price_pusher/src/solana/solana.ts +++ b/price_pusher/src/solana/solana.ts @@ -81,7 +81,8 @@ export class SolanaPricePusher implements IPricePusher { const transactionHashes = await this.pythSolanaReceiver.provider.sendAll( await transactionBuilder.buildVersionedTransactions({ computeUnitPriceMicroLamports: 50000, - }) + }), + { skipPreflight: true } ); console.log( `Successful. Tx hash: ${ @@ -90,6 +91,7 @@ export class SolanaPricePusher implements IPricePusher { ); } catch (e: any) { console.error("Failed pushing"); + console.error(e); return; } } From 93cd1e12094eb19eb1af5762675b8d6441c1d818 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 2 Apr 2024 16:40:21 +0100 Subject: [PATCH 07/28] Works --- price_pusher/src/solana/solana.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/price_pusher/src/solana/solana.ts b/price_pusher/src/solana/solana.ts index 03f9417cb8..ac6320f6ae 100644 --- a/price_pusher/src/solana/solana.ts +++ b/price_pusher/src/solana/solana.ts @@ -84,13 +84,9 @@ export class SolanaPricePusher implements IPricePusher { }), { skipPreflight: true } ); - console.log( - `Successful. Tx hash: ${ - transactionHashes[transactionHashes.length - 1] - }` - ); + console.log(`updatePriceFeed succesful`); } catch (e: any) { - console.error("Failed pushing"); + console.error("updatePriceFeed failed"); console.error(e); return; } From 92a47fe7f19d6f5441f5bc2c092f6203d4598116 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 2 Apr 2024 16:56:14 +0100 Subject: [PATCH 08/28] fix: pass pusher program id --- price_pusher/src/solana/command.ts | 2 ++ target_chains/solana/pyth_solana_receiver_sdk/src/lib.rs | 2 +- target_chains/solana/sdk/js/pyth_solana_receiver/src/address.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/price_pusher/src/solana/command.ts b/price_pusher/src/solana/command.ts index 307eada90d..9ab77ce39f 100644 --- a/price_pusher/src/solana/command.ts +++ b/price_pusher/src/solana/command.ts @@ -9,6 +9,7 @@ import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver"; import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; import { Keypair, Connection } from "@solana/web3.js"; import fs from "fs"; +import { PublicKey } from "@solana/web3.js"; export default { command: "solana", @@ -73,6 +74,7 @@ export default { const pythSolanaReceiver = new PythSolanaReceiver({ connection: new Connection(endpoint), wallet, + pushOracleProgramId: new PublicKey(pythContractAddress), }); const solanaPricePusher = new SolanaPricePusher( diff --git a/target_chains/solana/pyth_solana_receiver_sdk/src/lib.rs b/target_chains/solana/pyth_solana_receiver_sdk/src/lib.rs index 64a9a1099f..c0a2ebe4bd 100644 --- a/target_chains/solana/pyth_solana_receiver_sdk/src/lib.rs +++ b/target_chains/solana/pyth_solana_receiver_sdk/src/lib.rs @@ -12,4 +12,4 @@ pub mod price_update; declare_id!("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ"); -pub const PYTH_PUSH_ORACLE_ID: Pubkey = pubkey!("3a8iuFcGaMHFTX8sagDx55nPWp14fHxzWUbn2Mr4E8NR"); +pub const PYTH_PUSH_ORACLE_ID: Pubkey = pubkey!("pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT"); diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/src/address.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/src/address.ts index cb42d3efe0..bcb125b0d0 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/src/address.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/src/address.ts @@ -16,7 +16,7 @@ export const DEFAULT_WORMHOLE_PROGRAM_ID = new PublicKey( ); export const DEFAULT_PUSH_ORACLE_PROGRAM_ID = new PublicKey( - "3a8iuFcGaMHFTX8sagDx55nPWp14fHxzWUbn2Mr4E8NR" + "pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT" ); /** From edc609250ff4d271adf9856ff543718a0833d066 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 2 Apr 2024 17:36:13 +0100 Subject: [PATCH 09/28] Add docs --- .../src/PythSolanaReceiver.ts | 74 ++++++++++++++++--- 1 file changed, 64 insertions(+), 10 deletions(-) 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 6d9428d10e..c2067798fd 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 @@ -1,9 +1,4 @@ -import { - AnchorProvider, - IdlAccounts, - IdlTypes, - Program, -} from "@coral-xyz/anchor"; +import { AnchorProvider, IdlAccounts, Program } from "@coral-xyz/anchor"; import { Connection, Signer, @@ -105,6 +100,18 @@ export class PythTransactionBuilder extends TransactionBuilder { * 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. + * + * @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[]) { const { @@ -159,6 +166,27 @@ export class PythTransactionBuilder extends TransactionBuilder { 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. + * + * 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. + * + * @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[]) { const { postInstructions, @@ -179,7 +207,7 @@ export class PythTransactionBuilder extends TransactionBuilder { * 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. - * 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 * ```typescript @@ -240,8 +268,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 { const priceUpdateAccount = @@ -527,6 +555,14 @@ 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. + * @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[] ): Promise<{ @@ -636,6 +672,11 @@ export class PythSolanaReceiver { ); } + /** + * 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 { @@ -644,6 +685,12 @@ export class PythSolanaReceiver { ); } + /** + * 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 @@ -654,11 +701,18 @@ export class PythSolanaReceiver { } } +/** + * 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"); } From 4a215e21f1958fc1dcd4a56e3d5cca212f6d282e Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 2 Apr 2024 17:36:50 +0100 Subject: [PATCH 10/28] 0.1.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87a4d858a1..78fea76dd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "root", - "version": "0.0.1", + "version": "0.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "root", - "version": "0.0.1", + "version": "0.1.0", "workspaces": [ "express_relay/examples/easy_lend", "express_relay/sdk/js", diff --git a/package.json b/package.json index fe8f7e397e..63b8603b80 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,5 @@ "devDependencies": { "lerna": "^6.4.1" }, - "version": "0.0.1" + "version": "0.1.0" } From 4a995bcc5b0e96dd2f498210f70cf75f230cf6b4 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 2 Apr 2024 17:37:39 +0100 Subject: [PATCH 11/28] Bump npm package --- package-lock.json | 6 ++++-- .../solana/sdk/js/pyth_solana_receiver/package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 78fea76dd4..b2c9664c33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56546,6 +56546,7 @@ "@mysten/sui.js": "^0.49.1", "@pythnetwork/price-service-client": "*", "@pythnetwork/pyth-sdk-solidity": "*", + "@pythnetwork/pyth-solana-receiver": "*", "@pythnetwork/pyth-sui-js": "*", "@truffle/hdwallet-provider": "^2.1.3", "aptos": "^1.8.5", @@ -59498,7 +59499,7 @@ }, "target_chains/solana/sdk/js/pyth_solana_receiver": { "name": "@pythnetwork/pyth-solana-receiver", - "version": "0.3.0", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { "@coral-xyz/anchor": "^0.29.0", @@ -69103,6 +69104,7 @@ "@mysten/sui.js": "^0.49.1", "@pythnetwork/price-service-client": "*", "@pythnetwork/pyth-sdk-solidity": "*", + "@pythnetwork/pyth-solana-receiver": "*", "@pythnetwork/pyth-sui-js": "*", "@truffle/hdwallet-provider": "^2.1.3", "@types/ethereum-protocol": "^1.0.2", @@ -71107,7 +71109,7 @@ "abi_generator": "*", "prettier": "^2.7.1", "prettier-plugin-solidity": "^1.0.0-rc.1", - "solc": "*" + "solc": "^0.8.25" }, "dependencies": { "commander": { 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 121da14ec5..3ebf2e4c53 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.3.0", + "version": "0.4.0", "description": "Pyth solana receiver SDK", "homepage": "https://pyth.network", "main": "lib/index.js", From 29d4bbcf41fbb356e35067a10005326b5431bfb1 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 2 Apr 2024 17:41:48 +0100 Subject: [PATCH 12/28] Go --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b2c9664c33..a65a7dded5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "root", - "version": "0.1.0", + "version": "0.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "root", - "version": "0.1.0", + "version": "0.0.1", "workspaces": [ "express_relay/examples/easy_lend", "express_relay/sdk/js", diff --git a/package.json b/package.json index 63b8603b80..fe8f7e397e 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,5 @@ "devDependencies": { "lerna": "^6.4.1" }, - "version": "0.1.0" + "version": "0.0.1" } From ff2a9d07b98b15ac186cdc43111c82524a93e034 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 2 Apr 2024 18:45:34 +0100 Subject: [PATCH 13/28] Comment --- .../sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c2067798fd..73b7546462 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 @@ -172,7 +172,7 @@ export class PythTransactionBuilder extends TransactionBuilder { * @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates. * * 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. + * 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 From 7aaea45e79ec26b129d158968f22e60c52e0eb19 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 2 Apr 2024 19:29:11 +0100 Subject: [PATCH 14/28] Add customizable shard id --- price_pusher/src/solana/command.ts | 12 +++++++-- price_pusher/src/solana/solana.ts | 15 ++++++----- .../src/PythSolanaReceiver.ts | 26 ++++++++++++++----- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/price_pusher/src/solana/command.ts b/price_pusher/src/solana/command.ts index 9ab77ce39f..74e78e8313 100644 --- a/price_pusher/src/solana/command.ts +++ b/price_pusher/src/solana/command.ts @@ -21,10 +21,15 @@ export default { required: true, } as Options, "keypair-file": { - description: "Path to keypair file", + description: "Path to a keypair file", type: "string", required: true, } as Options, + "shard-id": { + description: "Shard ID", + type: "number", + required: true, + } as Options, ...options.priceConfigFile, ...options.priceServiceEndpoint, ...options.pythContractAddress, @@ -35,6 +40,7 @@ export default { const { endpoint, keypairFile, + shardId, priceConfigFile, priceServiceEndpoint, pythContractAddress, @@ -79,10 +85,12 @@ export default { const solanaPricePusher = new SolanaPricePusher( pythSolanaReceiver, - priceServiceConnection + priceServiceConnection, + shardId ); const solanaPriceListener = new SolanaPriceListener( pythSolanaReceiver, + shardId, priceItems, { pollingFrequency } ); diff --git a/price_pusher/src/solana/solana.ts b/price_pusher/src/solana/solana.ts index ac6320f6ae..f2bce0243e 100644 --- a/price_pusher/src/solana/solana.ts +++ b/price_pusher/src/solana/solana.ts @@ -11,6 +11,7 @@ import { PriceServiceConnection } from "@pythnetwork/price-service-client"; export class SolanaPriceListener extends ChainPriceListener { constructor( private pythSolanaReceiver: PythSolanaReceiver, + private shardId: number, priceItems: PriceItem[], config: { pollingFrequency: DurationInSeconds; @@ -23,7 +24,7 @@ export class SolanaPriceListener extends ChainPriceListener { try { const priceFeedAccount = await this.pythSolanaReceiver.fetchPriceFeedAccount( - 0, + this.shardId, Buffer.from(priceId, "hex") ); console.log( @@ -51,7 +52,8 @@ export class SolanaPriceListener extends ChainPriceListener { export class SolanaPricePusher implements IPricePusher { constructor( private pythSolanaReceiver: PythSolanaReceiver, - private priceServiceConnection: PriceServiceConnection + private priceServiceConnection: PriceServiceConnection, + private shardId: number ) {} async updatePriceFeed( @@ -75,19 +77,18 @@ export class SolanaPricePusher implements IPricePusher { const transactionBuilder = this.pythSolanaReceiver.newTransactionBuilder({ closeUpdateAccounts: false, }); - transactionBuilder.addUpdatePriceFeed(priceFeedUpdateData); + transactionBuilder.addUpdatePriceFeed(priceFeedUpdateData, this.shardId); try { - const transactionHashes = await this.pythSolanaReceiver.provider.sendAll( + await this.pythSolanaReceiver.provider.sendAll( await transactionBuilder.buildVersionedTransactions({ computeUnitPriceMicroLamports: 50000, }), { skipPreflight: true } ); - console.log(`updatePriceFeed succesful`); + console.log(new Date(), "updatePriceFeed successful"); } catch (e: any) { - console.error("updatePriceFeed failed"); - console.error(e); + console.error(new Date(), "updatePriceFeed failed", e); return; } } 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 73b7546462..d067c2cf45 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 @@ -170,6 +170,7 @@ export class PythTransactionBuilder extends TransactionBuilder { * 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. @@ -187,13 +188,14 @@ export class PythTransactionBuilder extends TransactionBuilder { * ... * ``` */ - async addUpdatePriceFeed(priceUpdateDataArray: string[]) { + async addUpdatePriceFeed(priceUpdateDataArray: string[], shardId: number) { const { postInstructions, priceFeedIdToPriceUpdateAccount, closeInstructions, } = await this.pythSolanaReceiver.buildUpdatePriceFeedInstructions( - priceUpdateDataArray + priceUpdateDataArray, + shardId ); this.closeInstructions.push(...closeInstructions); Object.assign( @@ -559,12 +561,14 @@ 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[] + priceUpdateDataArray: string[], + shardId: number ): Promise<{ postInstructions: InstructionWithEphemeralSigners[]; priceFeedIdToPriceUpdateAccount: Record; @@ -597,14 +601,14 @@ export class PythSolanaReceiver { merklePriceUpdate: update, treasuryId: DEFAULT_TREASURY_ID, }, - 0, + shardId, Array.from(feedId) ) .accounts({ pythSolanaReceiver: this.receiver.programId, encodedVaa, priceFeedAccount: getPriceFeedAccountAddress( - 0, + shardId, feedId, this.pushOracle.programId ), @@ -621,7 +625,11 @@ export class PythSolanaReceiver { priceFeedIdToPriceUpdateAccount[ "0x" + parsePriceFeedMessage(update.message).feedId.toString("hex") - ] = getPriceFeedAccountAddress(0, feedId, this.pushOracle.programId); + ] = getPriceFeedAccountAddress( + shardId, + feedId, + this.pushOracle.programId + ); } } return { @@ -696,7 +704,11 @@ export class PythSolanaReceiver { priceFeedId: Buffer ): Promise { return this.receiver.account.priceUpdateV2.fetchNullable( - getPriceFeedAccountAddress(0, priceFeedId, this.pushOracle.programId) + getPriceFeedAccountAddress( + shardId, + priceFeedId, + this.pushOracle.programId + ) ); } } From a47834a20171db7d0ea892d11f2b55f49f5cf450 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Wed, 3 Apr 2024 15:10:22 +0100 Subject: [PATCH 15/28] Allow configurable priority fees --- price_pusher/src/solana/command.ts | 9 ++++++++- price_pusher/src/solana/solana.ts | 8 ++++---- .../js/pyth_solana_receiver/src/PythSolanaReceiver.ts | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/price_pusher/src/solana/command.ts b/price_pusher/src/solana/command.ts index 74e78e8313..430f191ada 100644 --- a/price_pusher/src/solana/command.ts +++ b/price_pusher/src/solana/command.ts @@ -30,6 +30,11 @@ export default { type: "number", required: true, } as Options, + "compute-unit-price-micro-lamports": { + description: "Priority fee per compute unit", + type: "number", + default: 50000, + } as Options, ...options.priceConfigFile, ...options.priceServiceEndpoint, ...options.pythContractAddress, @@ -41,6 +46,7 @@ export default { endpoint, keypairFile, shardId, + computeUnitPriceMicroLamports, priceConfigFile, priceServiceEndpoint, pythContractAddress, @@ -86,7 +92,8 @@ export default { const solanaPricePusher = new SolanaPricePusher( pythSolanaReceiver, priceServiceConnection, - shardId + shardId, + computeUnitPriceMicroLamports ); const solanaPriceListener = new SolanaPriceListener( pythSolanaReceiver, diff --git a/price_pusher/src/solana/solana.ts b/price_pusher/src/solana/solana.ts index f2bce0243e..4f1bfebdd9 100644 --- a/price_pusher/src/solana/solana.ts +++ b/price_pusher/src/solana/solana.ts @@ -53,7 +53,8 @@ export class SolanaPricePusher implements IPricePusher { constructor( private pythSolanaReceiver: PythSolanaReceiver, private priceServiceConnection: PriceServiceConnection, - private shardId: number + private shardId: number, + private computeUnitPriceMicroLamports: number ) {} async updatePriceFeed( @@ -82,9 +83,8 @@ export class SolanaPricePusher implements IPricePusher { try { await this.pythSolanaReceiver.provider.sendAll( await transactionBuilder.buildVersionedTransactions({ - computeUnitPriceMicroLamports: 50000, - }), - { skipPreflight: true } + computeUnitPriceMicroLamports: this.computeUnitPriceMicroLamports, + }) ); console.log(new Date(), "updatePriceFeed successful"); } catch (e: any) { 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 d067c2cf45..3f114e6ea1 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 @@ -729,7 +729,7 @@ function getPriceFeedAccountAddress( throw new Error("Feed ID should be 32 bytes long"); } const shardBuffer = Buffer.alloc(2); - shardBuffer.writeUInt16BE(shardId, 0); + shardBuffer.writeUint16LE(shardId, 0); return PublicKey.findProgramAddressSync( [shardBuffer, feedId], From b8b9c741e5db913a57de1fbd2f82a47c06d79b6d Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Wed, 3 Apr 2024 16:09:03 +0100 Subject: [PATCH 16/28] Update readme --- .../sdk/js/pyth_solana_receiver/README.md | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md index 2bead3142b..549e676eeb 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md @@ -11,7 +11,26 @@ The Pyth Solana Receiver allows users to consume Pyth price updates on a pull ba Price updates get posted into price update accounts, owned by the Receiver contract. Once an update has been posted to a price update account, it can be used by anyone by simply passing the price update account as one of the accounts in a Solana instruction. Price update accounts can be closed by whoever wrote them to recover the rent. -## Example use +## Price update accounts vs price feed accounts + +In the pure pull model, each price update gets posted to an ephemeral account that can be closed after being consumed to reclaim rent. The SDK exposes this functionality with `addPostPriceUpdates` and `addPostPartiallyVerifiedPriceUpdates`. + +Another way of consuming price updates is through the price feed accounts. Price feed accounts are a special type of price update accounts with the following properties: + +- They have a static address that can be derived from a feed id and a shard id (the shard id allows multiple sets of price feed accounts to exist) (the address can be derived using `getPriceFeedAccountAddress`) +- They always contain a price update for the feed id their address is derived from +- The update they contain can only be replaced by a more recent update + +The SDK also allows updating price feed accounts with a more recent update via `addUpdatePriceFeed`. + +## Push model + +Combining price feed accounts with a scheduler service that pushes that periodically updates the price feed account allows using such price feed account as a push oracle. +Assuming the scheduler is running, a downstream app can consume the price updates by simply passing the price feed account as an account in a Solana instruction without needing to worry about updating the price feed account. + +Check out the [Price Pusher]("../../../../../../../price_pusher/") for an example of a price scheduler. + +## Example use (pull model) ```ts import { Connection, PublicKey } from "@solana/web3.js"; From bcafdb5ddddf9c21e047fcce72eece98c48132eb Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Wed, 3 Apr 2024 16:13:14 +0100 Subject: [PATCH 17/28] Update text --- target_chains/solana/sdk/js/pyth_solana_receiver/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md index 549e676eeb..3cbf102519 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md @@ -15,7 +15,7 @@ Price update accounts can be closed by whoever wrote them to recover the rent. In the pure pull model, each price update gets posted to an ephemeral account that can be closed after being consumed to reclaim rent. The SDK exposes this functionality with `addPostPriceUpdates` and `addPostPartiallyVerifiedPriceUpdates`. -Another way of consuming price updates is through the price feed accounts. Price feed accounts are a special type of price update accounts with the following properties: +Another way of consuming price updates is via price feed accounts. Price feed accounts are a special type of price update accounts with the following properties: - They have a static address that can be derived from a feed id and a shard id (the shard id allows multiple sets of price feed accounts to exist) (the address can be derived using `getPriceFeedAccountAddress`) - They always contain a price update for the feed id their address is derived from @@ -25,10 +25,10 @@ The SDK also allows updating price feed accounts with a more recent update via ` ## Push model -Combining price feed accounts with a scheduler service that pushes that periodically updates the price feed account allows using such price feed account as a push oracle. +Combining price feed accounts with a scheduler service that periodically updates the price feed account allows using such price feed account as a push oracle. Assuming the scheduler is running, a downstream app can consume the price updates by simply passing the price feed account as an account in a Solana instruction without needing to worry about updating the price feed account. -Check out the [Price Pusher]("../../../../../../../price_pusher/") for an example of a price scheduler. +Check out the [Price Pusher](../../../../../../../price_pusher/) for an example of a price scheduler. ## Example use (pull model) From bcfe79a189eb757c4d7864d9dd58d748300f3744 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Wed, 3 Apr 2024 16:32:51 +0100 Subject: [PATCH 18/28] readme updates --- .../sdk/js/pyth_solana_receiver/README.md | 2 ++ .../src/PythSolanaReceiver.ts | 23 ++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md index 3cbf102519..90609e9382 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md @@ -38,6 +38,7 @@ import { PriceServiceConnection } from "@pythnetwork/price-service-client"; import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver"; import { MyFirstPythApp, IDL } from "./idl/my_first_pyth_app"; +// Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable const SOL_PRICE_FEED_ID = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; const ETH_PRICE_FEED_ID = @@ -93,6 +94,7 @@ import { PriceServiceConnection } from "@pythnetwork/price-service-client"; import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver"; import { MyFirstPythApp, IDL } from "./idl/my_first_pyth_app"; +// Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable const SOL_PRICE_FEED_ID = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; const ETH_PRICE_FEED_ID = 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 3f114e6ea1..ed7d517cbf 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 @@ -61,12 +61,18 @@ export type PythTransactionBuilderConfig = { /** * A builder class to build transactions that: - * - Post price updates (fully or partially verified) + * - Post price updates (fully or partially verified) or update price feed accounts * - Consume price updates in a consumer program * - (Optionally) Close price update and encoded vaa accounts to recover the rent (`closeUpdateAccounts` in `PythTransactionBuilderConfig`) * + * `addPostPriceUpdates` vs `addUpdatePriceFeed`: + * - `addPostPriceUpdates` is used to post price updates to ephemeral accounts. + * - `addUpdatePriceFeed` is used to post price updates to price feed accounts, they are fixed accounts for each feed id that can only be updated with a more recent price update. Their addesses can be found using `getPriceFeedAccountAddress` + * * @example * ```typescript + * + * // Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable * const priceUpdateData = await priceServiceConnection.getLatestVaas([ * SOL_PRICE_FEED_ID, * ETH_PRICE_FEED_ID, @@ -103,6 +109,7 @@ export class PythTransactionBuilder extends TransactionBuilder { * * @example * ```typescript + * // Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable * const priceUpdateData = await priceServiceConnection.getLatestVaas([ * SOL_PRICE_FEED_ID, * ETH_PRICE_FEED_ID, @@ -139,6 +146,7 @@ export class PythTransactionBuilder extends TransactionBuilder { * * @example * ```typescript + * // Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable * const priceUpdateData = await priceServiceConnection.getLatestVaas([ * SOL_PRICE_FEED_ID, * ETH_PRICE_FEED_ID, @@ -168,15 +176,14 @@ export class PythTransactionBuilder extends TransactionBuilder { /** * Add instructions to update price feed accounts to the builder. + * Price feed accounts are a special type of price update accounts. Instead of being ephemeral accounts, they are fixed accounts for each feed id that can only be updated with a more recent price update. * * @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. + * @param shardId the shard ID of the set of price feed accounts. This shard ID allows for multiple price feed accounts for the same price feed id to exist. * * @example * ```typescript + * // Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable * const priceUpdateData = await priceServiceConnection.getLatestVaas([ * SOL_PRICE_FEED_ID, * ETH_PRICE_FEED_ID, @@ -561,7 +568,7 @@ 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. + * @param shardId the shard ID of the set of price feed accounts. This shard ID allows for multiple price feed accounts for the same price feed id to exist. * @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. @@ -695,7 +702,7 @@ export class PythSolanaReceiver { /** * 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 shardId The shard ID of the set of price feed accounts. This shard ID allows for multiple price feed accounts for the same price feed id to exist. * @param priceFeedId The price feed ID. * @returns The contents of the deserialized price feed account or `null` if the account doesn't exist */ @@ -715,7 +722,7 @@ export class PythSolanaReceiver { /** * 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 shardId The shard ID of the set of price feed accounts. This shard ID allows for multiple price feed accounts for the same price feed id to exist. * @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 From 43417ef712f0dbc4fc9f1496170ba1f50bb122ae Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Wed, 3 Apr 2024 16:43:56 +0100 Subject: [PATCH 19/28] Readme --- .../sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ed7d517cbf..56199043e5 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 @@ -66,8 +66,8 @@ export type PythTransactionBuilderConfig = { * - (Optionally) Close price update and encoded vaa accounts to recover the rent (`closeUpdateAccounts` in `PythTransactionBuilderConfig`) * * `addPostPriceUpdates` vs `addUpdatePriceFeed`: - * - `addPostPriceUpdates` is used to post price updates to ephemeral accounts. - * - `addUpdatePriceFeed` is used to post price updates to price feed accounts, they are fixed accounts for each feed id that can only be updated with a more recent price update. Their addesses can be found using `getPriceFeedAccountAddress` + * - `addPostPriceUpdates` is used to post price updates to ephemeral accounts. Use this to post a price update from the present or from the past for your program to consume. + * - `addUpdatePriceFeed` is used to post price updates to price feed accounts, they are fixed accounts for each feed id that can only be updated with a more recent price update. Their addresses can be found using `getPriceFeedAccountAddress`. Use this to post a recent price update to a shared price feed account that multiple programs can use. * * @example * ```typescript From 7017be6581d6d8a01a4cba855939413cd09fb88f Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Wed, 3 Apr 2024 16:52:03 +0100 Subject: [PATCH 20/28] More text --- target_chains/solana/sdk/js/pyth_solana_receiver/README.md | 4 ++++ .../sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md index 90609e9382..72f2257829 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md @@ -61,6 +61,10 @@ const myFirstPythApp = new Program( const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({}); await transactionBuilder.addPostPriceUpdates(priceUpdateData); +console.log( + "The SOL/USD price update will get posted to:", + transactionBuilder.getPriceUpdateAccount(SOL_PRICE_FEED_ID).toBase58() +); await transactionBuilder.addPriceConsumerInstructions( async ( getPriceUpdateAccount: (priceFeedId: string) => PublicKey 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 56199043e5..8beea5d806 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 @@ -80,6 +80,7 @@ export type PythTransactionBuilderConfig = { * * const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({}); * await transactionBuilder.addPostPriceUpdates(priceUpdateData); + * console.log("The SOL/USD price update will get posted to:", transactionBuilder.getPriceUpdateAccount(SOL_PRICE_FEED_ID).toBase58()) * await transactionBuilder.addPriceConsumerInstructions(...) * * await pythSolanaReceiver.provider.sendAll(await transactionBuilder.buildVersionedTransactions({computeUnitPriceMicroLamports:1000000})) @@ -104,6 +105,7 @@ export class PythTransactionBuilder extends TransactionBuilder { /** * Add instructions to post price updates to the builder. + * Use this function to post fully verified price updates from the present or from the past for your program to consume. * * @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates. * @@ -117,6 +119,7 @@ export class PythTransactionBuilder extends TransactionBuilder { * * const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({}); * await transactionBuilder.addPostPriceUpdates(priceUpdateData); + * console.log("The SOL/USD price update will get posted to:", transactionBuilder.getPriceUpdateAccount(SOL_PRICE_FEED_ID).toBase58()) * await transactionBuilder.addPriceConsumerInstructions(...) * ``` */ @@ -138,6 +141,7 @@ export class PythTransactionBuilder extends TransactionBuilder { /** * Add instructions to post partially verified price updates to the builder. + * Use this function to post partially verified price updates from the present or from the past for your program to consume. * * @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates. * @@ -154,6 +158,7 @@ export class PythTransactionBuilder extends TransactionBuilder { * * const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({}); * await transactionBuilder.addPostPartiallyVerifiedPriceUpdates(priceUpdateData); + * console.log("The SOL/USD price update will get posted to:", transactionBuilder.getPriceUpdateAccount(SOL_PRICE_FEED_ID).toBase58()) * await transactionBuilder.addPriceConsumerInstructions(...) * ... * ``` @@ -177,6 +182,7 @@ export class PythTransactionBuilder extends TransactionBuilder { /** * Add instructions to update price feed accounts to the builder. * Price feed accounts are a special type of price update accounts. Instead of being ephemeral accounts, they are fixed accounts for each feed id that can only be updated with a more recent price update. + * Use this function to post a recent price update to a shared price feed account that multiple programs can use. * * @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 price feed accounts for the same price feed id to exist. From 846ba27cfe21d3758c44a3729985e1b3f723f6c6 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Wed, 3 Apr 2024 16:54:43 +0100 Subject: [PATCH 21/28] More text --- target_chains/solana/sdk/js/pyth_solana_receiver/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md index 72f2257829..036777adb6 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md @@ -17,7 +17,7 @@ In the pure pull model, each price update gets posted to an ephemeral account th Another way of consuming price updates is via price feed accounts. Price feed accounts are a special type of price update accounts with the following properties: -- They have a static address that can be derived from a feed id and a shard id (the shard id allows multiple sets of price feed accounts to exist) (the address can be derived using `getPriceFeedAccountAddress`) +- They have a fixed address that can be derived from a feed id and a shard id (the shard id allows for multiple price feed accounts to exist for each price feed) (the address can be derived using `getPriceFeedAccountAddress`) - They always contain a price update for the feed id their address is derived from - The update they contain can only be replaced by a more recent update From 4c98d59b1371f3017813c591f48f5530af3b1459 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Wed, 3 Apr 2024 17:01:13 +0100 Subject: [PATCH 22/28] Review --- .../sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 8beea5d806..9a56fe56fc 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 @@ -67,7 +67,7 @@ export type PythTransactionBuilderConfig = { * * `addPostPriceUpdates` vs `addUpdatePriceFeed`: * - `addPostPriceUpdates` is used to post price updates to ephemeral accounts. Use this to post a price update from the present or from the past for your program to consume. - * - `addUpdatePriceFeed` is used to post price updates to price feed accounts, they are fixed accounts for each feed id that can only be updated with a more recent price update. Their addresses can be found using `getPriceFeedAccountAddress`. Use this to post a recent price update to a shared price feed account that multiple programs can use. + * - `addUpdatePriceFeed` is used to post price updates to price feed accounts, these are fixed accounts for each feed id that can only be updated with a more recent price update than the one they contain. Their addresses can be found using `getPriceFeedAccountAddress`. Use this to post a recent price update to a shared price feed account. * * @example * ```typescript @@ -181,8 +181,8 @@ export class PythTransactionBuilder extends TransactionBuilder { /** * Add instructions to update price feed accounts to the builder. - * Price feed accounts are a special type of price update accounts. Instead of being ephemeral accounts, they are fixed accounts for each feed id that can only be updated with a more recent price update. - * Use this function to post a recent price update to a shared price feed account that multiple programs can use. + * Price feed accounts are a special type of price update accounts. Instead of being ephemeral accounts, they are fixed accounts for each feed id that can only be updated with a more recent price update than the one they contain. + * Use this function to post a recent price update to a shared price feed account. * * @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 price feed accounts for the same price feed id to exist. From 7e9b1304cd0834574ce2d4352eb7d4001b5aa25e Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Wed, 3 Apr 2024 17:08:24 +0100 Subject: [PATCH 23/28] Text --- .../sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 9a56fe56fc..5654eb628e 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 @@ -67,7 +67,7 @@ export type PythTransactionBuilderConfig = { * * `addPostPriceUpdates` vs `addUpdatePriceFeed`: * - `addPostPriceUpdates` is used to post price updates to ephemeral accounts. Use this to post a price update from the present or from the past for your program to consume. - * - `addUpdatePriceFeed` is used to post price updates to price feed accounts, these are fixed accounts for each feed id that can only be updated with a more recent price update than the one they contain. Their addresses can be found using `getPriceFeedAccountAddress`. Use this to post a recent price update to a shared price feed account. + * - `addUpdatePriceFeed` is used to post price updates to price feed accounts, these are fixed accounts for each feed id that can only be updated with a more recent price update than the one they contain. Their addresses can be found using `getPriceFeedAccountAddress`. Use this to update a shared price feed account with a recent price * * @example * ```typescript @@ -182,7 +182,7 @@ export class PythTransactionBuilder extends TransactionBuilder { /** * Add instructions to update price feed accounts to the builder. * Price feed accounts are a special type of price update accounts. Instead of being ephemeral accounts, they are fixed accounts for each feed id that can only be updated with a more recent price update than the one they contain. - * Use this function to post a recent price update to a shared price feed account. + * Use this function to update a shared price feed account with a recent price * * @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 price feed accounts for the same price feed id to exist. From e80ede3aa6fe93d57ead1047bc4a482b741e2a7f Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Fri, 5 Apr 2024 10:44:32 -0700 Subject: [PATCH 24/28] readme --- .../sdk/js/pyth_solana_receiver/README.md | 249 +++++++++++------- .../examples/post_price_update.ts | 94 +++++++ .../post_price_update_instructions.ts | 80 ++++++ .../examples/update_price_feed.ts | 94 +++++++ .../sdk/js/pyth_solana_receiver/package.json | 2 +- .../src/PythSolanaReceiver.ts | 42 +-- 6 files changed, 444 insertions(+), 117 deletions(-) create mode 100644 target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts create mode 100644 target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update_instructions.ts create mode 100644 target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md index 036777adb6..08d1a50ba9 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md @@ -1,88 +1,142 @@ # Pyth Solana Receiver JS SDK -This is a Javascript SDK to interact with the Pyth Solana Receiver contract whose code lives [here](/target_chains/solana). +[@pythnetwork/pyth-solana-receiver](https://www.npmjs.com/package/@pythnetwork/pyth-solana-receiver) is a Typescript SDK for interacting with the Pyth Solana Receiver contract. +The SDK enables users to construct transactions that post Pyth price updates to the Solana blockchain and use them in downstream applications. -It is available on [npm](https://www.npmjs.com/package/@pythnetwork/pyth-solana-receiver). +The code for the underlying Pyth Solana Receiver program lives [here](/target_chains/solana). -## Pull model +## Installation -The Pyth Solana Receiver allows users to consume Pyth price updates on a pull basis. This means that the user is responsible for submitting the price data on-chain whenever they want to interact with an app that requires a price update. +You can install the package using your favorite typescript version manager -Price updates get posted into price update accounts, owned by the Receiver contract. Once an update has been posted to a price update account, it can be used by anyone by simply passing the price update account as one of the accounts in a Solana instruction. -Price update accounts can be closed by whoever wrote them to recover the rent. +**NPM:** `npm install @pythnetwork/pyth-solana-receiver` -## Price update accounts vs price feed accounts +**Yarn:** `yarn add @pythnetwork/pyth-solana-receiver` -In the pure pull model, each price update gets posted to an ephemeral account that can be closed after being consumed to reclaim rent. The SDK exposes this functionality with `addPostPriceUpdates` and `addPostPartiallyVerifiedPriceUpdates`. +## Preliminaries -Another way of consuming price updates is via price feed accounts. Price feed accounts are a special type of price update accounts with the following properties: +### Accessing Pyth Prices -- They have a fixed address that can be derived from a feed id and a shard id (the shard id allows for multiple price feed accounts to exist for each price feed) (the address can be derived using `getPriceFeedAccountAddress`) -- They always contain a price update for the feed id their address is derived from -- The update they contain can only be replaced by a more recent update +This SDK is designed to be used in combination with a source of Pyth pricing data. +There are two different sources of pricing data that users can choose from. -The SDK also allows updating price feed accounts with a more recent update via `addUpdatePriceFeed`. +- [Hermes](https://docs.pyth.network/price-feeds/pythnet-price-feeds/hermes) is a webservice that provides HTTP and websocket endpoints for retrieving real-time Pyth prices. + The example code below uses the public Hermes instance hosted by the Pyth Data Association at `https://hermes.pyth.network/`. + Hermes is also available from several infrastructure providers [listed here](https://docs.pyth.network/price-feeds/api-instances-and-providers/hermes). + The [Price Service Client](https://github.com/pyth-network/pyth-crosschain/tree/main/price_service/client/js) can be used to access Hermes prices in a convenient way. +- [Benchmarks](https://docs.pyth.network/benchmarks) is a webservice that provides HTTP endpoints for accessing historical Pyth prices. + This service can be used for applications that require prices from specific times in the past. -## Push model +Both of these services return Pyth price updates, which are binary blobs of signed and timestamped prices. +This SDK enables users to post price updates to the Solana blockchain, verify their validity, and consume them in downstream Solana applications. -Combining price feed accounts with a scheduler service that periodically updates the price feed account allows using such price feed account as a push oracle. -Assuming the scheduler is running, a downstream app can consume the price updates by simply passing the price feed account as an account in a Solana instruction without needing to worry about updating the price feed account. +### Price Feed IDs -Check out the [Price Pusher](../../../../../../../price_pusher/) for an example of a price scheduler. +Pyth identifies each pair of assets (e.g., BTC/USD) with a unique price feed id. +The price feed id is a UUID written as a hexadecimal string. +In order to get the price for a specific pair of assets, you will need its corresponding price feed id. +You can look up all available price feed ids [here](https://pyth.network/developers/price-feed-ids). -## Example use (pull model) +### Pyth Solana Receiver -```ts -import { Connection, PublicKey } from "@solana/web3.js"; -import { PriceServiceConnection } from "@pythnetwork/price-service-client"; +The Pyth Solana Receiver provides two different methods for posting and using price updates. + +First, a price update can be written to a **_price update account_**. +Once the account has been written, other programs can read the Pyth price from the account by simply including it in their instruction. +Price update accounts are ephemeral: they have an owner who can overwrite their contents or close the account. +This method for using Pyth prices is a good fit for applications that need to use prices at specific timestamps (e.g., to settle a trade at a time). + +Second, a price update can be written to a **_price feed account_**. +A price feed account is designed to work similarly to a Pyth price feed -- it holds a sequence of price updates that move forward in time. +Applications can therefore store the address of a price feed account and read its contents whenever they need a recent price for the feed. +Price feed accounts have a fixed address derived from the feed id and a shard id. +The shard id allows different applications to use different accounts for the same feed, thereby reducing the impact of solana congestion. +This method of using Pyth prices is a good fit for applications that always want to use the most recent price. +Additionally, the [Price Scheduler](../../../../../../../price_pusher/) can be used to continuously write fresh updates to a price feed account, freeing applications from worrying about writing their own updates. + +This SDK provides methods for working with both types of accounts. + +## Usage + +The `PythSolanaReceiver` class is the main entrypoint for the SDK. +Instantiate it with a Solana web3 `Connection` and anchor `Wallet`: + +```typescript import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver"; -import { MyFirstPythApp, IDL } from "./idl/my_first_pyth_app"; +import { Connection, Keypair, PublicKey } from "@solana/web3.js"; +import { Wallet } from "@coral-xyz/anchor"; -// Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable -const SOL_PRICE_FEED_ID = - "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; -const ETH_PRICE_FEED_ID = - "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"; +const connection = new Connection("https://api.mainnet-beta.solana.com"); +const wallet = new Wallet( + Keypair.fromSecretKey(/* */) +); +const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet }); +``` -const priceServiceConnection = new PriceServiceConnection( - "https://hermes.pyth.network/", - { priceFeedRequestConfig: { binary: true } } +### Post a price update + +Post an update to an ephemeral price update account: + +```ts +// Fetch this from hermes or benchmarks. See Preliminaries section above for more info. +const priceUpdateData = + "UE5BVQEAAAADuAEAAAADDQExWGp7w3s3zDxhkNYnzcK2WalKT3uSQqUwetvCf4PgbFzjCdiowrp8Bq8HO+Q+7MHSuQ0BKS3r6attJvmkVbgFAQJlCXsGZdtF88zjeB6sUBcmpuu/J6Ci7tgHiM6hy81rBD5AcU2AQUnYaHvwkzhQsrme4S3SI/F9fZrjMGrPn2jlAQMWvFXRTa/Ga3Kdur06PgxRk2NiIb/RJ+iwXOb1OBljXCqWdew8BTbtVtJSPxb390O/HVzp1L4m3Lw/645pyattAAaKagQCRrWUZvqDhgRGLRqo0o3AWQJ46JD6AdgG/blL115vvembP/F7AjOuMLrjAWS1SgzJMYd9UbblxWkovS2EAQcx9kqzys5E5cRGXjYxD8WRyTb6G7e6g5eGKIX8cT4UHS72fqrawE+gmn0BWQciThOnSEaP8C/4JWB4qBqZPxMMAQid0Yd8BQNsOvdNNqtE7ETYzqnDKFIN8OHxks6ej2cqXUs605TB+AOZiBtogillICrXBo4PyQuRCacsTjan/NhCAQqdmFKys/qTKCujOWfRfvHSfPNHh2cqDCd8TetgZhj2qXP5Bzah3yoL8mHc1gM62FyRgGPgbjlrsL3f2WPn8W9FAAu0G27GuaEhu6WMqj2LC1M/K6JPENtxLoB+tB9Vhpz6ygAp/Um3W2O6ajKl2H3eXpBNW0VWC80U4T40oHFJWrC4AAwn1Q5XbrxUz5MwqmGRKYlHyNy6XQcG+ZXdhY4JcxU8xB70oLKmVoyLPWUqfquAt23FsaIRiD58vOFAQ/Z+6tr+AQ4icUr89Bdc5QaqzIeCzPUZ7vtXY1P+tOo0uCWdZSRowFq4UCrG+r3gNZlekB/qfcVOI+8MkiZ9S34p0o1JvbpmARB0A/MZSnLRQ3HsFQR0fKtIGhUmP5Teu6B5EG6drvoIFkxunm7a2wVz6iOMPsytvwZwN+0YoC+ReMVTiNAQGxUtARE4/5h2ujquF40DGcoh6/oevKqo2t5qaCpSQ95YvRdCaz7Sl/cZlRsXobmYkuOIk1ENhqmuu4EbG/OK5XeH/2r+ARJgNMjScOHWIbWgTL0xPz2uXGXiDKgkkp7H3InHlM14Ah7qi6yvBYrFmi6DlWhRX+cou4hrqUngyk3TmXXaEsZwAWYQC40AAAAAABrhAfrtrFhR4yubI7X5QRqMK6xKrj7U3XuBHdGnLqSqcQAAAAAC5oR+AUFVV1YAAAAAAAfwdTcAACcQgQ9gmOFJJ6Q9Kc944m+Ad3if+XQCAFUA7w2Lb9os66QdoV1AldHaOSoNL47Qxse8D0z6yMKAtW0AAAAEEnsirAAAAAABBHwT////+AAAAABmEAuNAAAAAGYQC4wAAAAECs5k8AAAAAAA9mHKCoc8xlhwoXfu/sbF3G+SM6vmsaW/kremZS23frVwnt9lUw0F4iILSQxHJXg0en93zIjd2hzhbkb6g6pmxaso8ZcBbxO26bQT21ofP2RlJSREqlL/DcmSOJhH9QTVh9wa8YYqSg1+iE+ikKXnzKSgrDke2U1vl9i2AyrXFMrad6iAlAqIDsqW+qZPX5APSvsdas5AE6KoqhrJxgHXY4GtQZxKKvEQs5EPj/wefL0vgTndN6qkAZ9KPuLVL8TCEfZgKdCNOBGqCer8AFUA/2FJGpMREt3xvYFHzRtkE3X3n1glEm1mVICHRjT9Cs4AAABMntCNVwAAAAANC1wx////+AAAAABmEAuNAAAAAGYQC4wAAABMRtENIAAAAAARjpacCqW6MiwuuCTN37nDR9bes6eLYG8IG4MPoSLbarS61bbZ0MR2iLFPUOIDhdYM4b4LG0+l/tt8LJaCtmi5TrICKPfoRdBRgMbQTR1Xkn+oJEQqXe3kH/IIJ6Yl+seCumnf9Wtw85dJ2m3aGx4zXn12Pwz95hE9nyEnmCrXFMrad6iAlAqIDsqW+qZPX5APSvsdas5AE6KoqhrJxgHXY4GtQZxKKvEQs5EPj/wefL0vgTndN6qkAZ9KPuLVL8TCEfZgKdCNOBGqCer8"; + +// Pass `closeUpdateAccounts: true` to the transaction builder constructor to automatically close the +// price update accounts at the end of the sequence of transactions. +const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({ + closeUpdateAccounts: false, +}); +await transactionBuilder.addPostPriceUpdates(priceUpdateData); + +await transactionBuilder.addPriceConsumerInstructions( + async ( + getPriceUpdateAccount: (priceFeedId: string) => PublicKey + ): Promise => { + // Generate instructions here that use the price updates posted above. + // getPriceUpdateAccount() will give you the account for each price update. + return []; + } ); -const priceUpdateData = await priceServiceConnection.getLatestVaas([ - SOL_PRICE_FEED_ID, - ETH_PRICE_FEED_ID, -]); // Fetch off-chain price update data - -const myFirstPythApp = new Program( - IDL as MyFirstPythApp, - MY_FIRST_PYTH_APP_PROGRAM_ID, - {} + +// Send the instructions in the builder in 1 or more transactions. +// The builder will pack the instructions into transactions automatically. +await pythSolanaReceiver.provider.sendAll( + await transactionBuilder.buildVersionedTransactions({ + computeUnitPriceMicroLamports: 1000000, + }) ); +``` + +The code snippet above will post every price update in `priceUpdateData` to a new ephemeral account. + +See `examples/post_price_update.ts` for a runnable example of posting a price update. + +### Update a price feed account + +Update the price feed account for shard id 1: + +```typescript +// Fetch this from hermes or benchmarks. See Preliminaries section above for more info. +const priceUpdateData = + "UE5BVQEAAAADuAEAAAADDQExWGp7w3s3zDxhkNYnzcK2WalKT3uSQqUwetvCf4PgbFzjCdiowrp8Bq8HO+Q+7MHSuQ0BKS3r6attJvmkVbgFAQJlCXsGZdtF88zjeB6sUBcmpuu/J6Ci7tgHiM6hy81rBD5AcU2AQUnYaHvwkzhQsrme4S3SI/F9fZrjMGrPn2jlAQMWvFXRTa/Ga3Kdur06PgxRk2NiIb/RJ+iwXOb1OBljXCqWdew8BTbtVtJSPxb390O/HVzp1L4m3Lw/645pyattAAaKagQCRrWUZvqDhgRGLRqo0o3AWQJ46JD6AdgG/blL115vvembP/F7AjOuMLrjAWS1SgzJMYd9UbblxWkovS2EAQcx9kqzys5E5cRGXjYxD8WRyTb6G7e6g5eGKIX8cT4UHS72fqrawE+gmn0BWQciThOnSEaP8C/4JWB4qBqZPxMMAQid0Yd8BQNsOvdNNqtE7ETYzqnDKFIN8OHxks6ej2cqXUs605TB+AOZiBtogillICrXBo4PyQuRCacsTjan/NhCAQqdmFKys/qTKCujOWfRfvHSfPNHh2cqDCd8TetgZhj2qXP5Bzah3yoL8mHc1gM62FyRgGPgbjlrsL3f2WPn8W9FAAu0G27GuaEhu6WMqj2LC1M/K6JPENtxLoB+tB9Vhpz6ygAp/Um3W2O6ajKl2H3eXpBNW0VWC80U4T40oHFJWrC4AAwn1Q5XbrxUz5MwqmGRKYlHyNy6XQcG+ZXdhY4JcxU8xB70oLKmVoyLPWUqfquAt23FsaIRiD58vOFAQ/Z+6tr+AQ4icUr89Bdc5QaqzIeCzPUZ7vtXY1P+tOo0uCWdZSRowFq4UCrG+r3gNZlekB/qfcVOI+8MkiZ9S34p0o1JvbpmARB0A/MZSnLRQ3HsFQR0fKtIGhUmP5Teu6B5EG6drvoIFkxunm7a2wVz6iOMPsytvwZwN+0YoC+ReMVTiNAQGxUtARE4/5h2ujquF40DGcoh6/oevKqo2t5qaCpSQ95YvRdCaz7Sl/cZlRsXobmYkuOIk1ENhqmuu4EbG/OK5XeH/2r+ARJgNMjScOHWIbWgTL0xPz2uXGXiDKgkkp7H3InHlM14Ah7qi6yvBYrFmi6DlWhRX+cou4hrqUngyk3TmXXaEsZwAWYQC40AAAAAABrhAfrtrFhR4yubI7X5QRqMK6xKrj7U3XuBHdGnLqSqcQAAAAAC5oR+AUFVV1YAAAAAAAfwdTcAACcQgQ9gmOFJJ6Q9Kc944m+Ad3if+XQCAFUA7w2Lb9os66QdoV1AldHaOSoNL47Qxse8D0z6yMKAtW0AAAAEEnsirAAAAAABBHwT////+AAAAABmEAuNAAAAAGYQC4wAAAAECs5k8AAAAAAA9mHKCoc8xlhwoXfu/sbF3G+SM6vmsaW/kremZS23frVwnt9lUw0F4iILSQxHJXg0en93zIjd2hzhbkb6g6pmxaso8ZcBbxO26bQT21ofP2RlJSREqlL/DcmSOJhH9QTVh9wa8YYqSg1+iE+ikKXnzKSgrDke2U1vl9i2AyrXFMrad6iAlAqIDsqW+qZPX5APSvsdas5AE6KoqhrJxgHXY4GtQZxKKvEQs5EPj/wefL0vgTndN6qkAZ9KPuLVL8TCEfZgKdCNOBGqCer8AFUA/2FJGpMREt3xvYFHzRtkE3X3n1glEm1mVICHRjT9Cs4AAABMntCNVwAAAAANC1wx////+AAAAABmEAuNAAAAAGYQC4wAAABMRtENIAAAAAARjpacCqW6MiwuuCTN37nDR9bes6eLYG8IG4MPoSLbarS61bbZ0MR2iLFPUOIDhdYM4b4LG0+l/tt8LJaCtmi5TrICKPfoRdBRgMbQTR1Xkn+oJEQqXe3kH/IIJ6Yl+seCumnf9Wtw85dJ2m3aGx4zXn12Pwz95hE9nyEnmCrXFMrad6iAlAqIDsqW+qZPX5APSvsdas5AE6KoqhrJxgHXY4GtQZxKKvEQs5EPj/wefL0vgTndN6qkAZ9KPuLVL8TCEfZgKdCNOBGqCer8"; const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({}); -await transactionBuilder.addPostPriceUpdates(priceUpdateData); -console.log( - "The SOL/USD price update will get posted to:", - transactionBuilder.getPriceUpdateAccount(SOL_PRICE_FEED_ID).toBase58() -); +// Update the price feed accounts for the feed ids in priceUpdateData and shard id 1 +await transactionBuilder.addUpdatePriceFeed(priceUpdateData, 1); + await transactionBuilder.addPriceConsumerInstructions( async ( getPriceUpdateAccount: (priceFeedId: string) => PublicKey ): Promise => { - return [ - { - instruction: await myFirstPythApp.methods - .consume() - .accounts({ - solPriceUpdate: getPriceUpdateAccount(SOL_PRICE_FEED_ID), - ethPriceUpdate: getPriceUpdateAccount(ETH_PRICE_FEED_ID), - }) - .instruction(), - signers: [], - }, - ]; + // Generate instructions here that use the price updates posted above. + // getPriceUpdateAccount() will give you the account for each price update. + return []; } ); + +// Send the instructions in the builder in 1 or more transactions. +// The builder will pack the instructions into transactions automatically. await pythSolanaReceiver.provider.sendAll( await transactionBuilder.buildVersionedTransactions({ computeUnitPriceMicroLamports: 1000000, @@ -90,52 +144,49 @@ await pythSolanaReceiver.provider.sendAll( ); ``` -Alternatively you can use the instruction builder methods from `PythSolanaReceiver` : +The code above will update the price feed accounts for the feeds in `priceUpdateData` (in this example, SOL/USD and ETH/USD). +The address of the price feed accounts can be derived automatically from the feed id and the shard id: -```ts -import { PublicKey } from "@solana/web3.js"; -import { PriceServiceConnection } from "@pythnetwork/price-service-client"; -import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver"; -import { MyFirstPythApp, IDL } from "./idl/my_first_pyth_app"; +```typescript +const solUsdPriceFeedAccount = pythSolanaReceiver + .getPriceFeedAccountAddress(SOL_PRICE_FEED_ID, 1) + .toBase58(); +``` -// Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable -const SOL_PRICE_FEED_ID = - "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; -const ETH_PRICE_FEED_ID = - "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"; +Note that the example above uses a shard id of 1. +Changing the shard id to a different value will give you a different account address. -const priceServiceConnection = new PriceServiceConnection( - "https://hermes.pyth.network/", - { priceFeedRequestConfig: { binary: true } } -); -const priceUpdateData = await priceServiceConnection.getLatestVaas([ - SOL_PRICE_FEED_ID, - ETH_PRICE_FEED_ID, -]); // Fetch off-chain price update data +See `examples/update_price_feed.ts` for a runnable example of updating a price feed. -const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet }); -const { postInstructions, closeInstructions, priceFeedIdToPriceUpdateAccount } = - await pythSolanaReceiver.buildPostPriceUpdateInstructions(priceUpdateData); // Get instructions to post the price update data and to close the accounts later +### Partially verified price updates -const myFirstPythApp = new Program( - IDL as MyFirstPythApp, - MY_FIRST_PYTH_APP_PROGRAM_ID, - {} -); -const consumerInstruction: InstructionWithEphemeralSigners = { - instruction: await myFirstPythApp.methods - .consume() - .accounts({ - solPriceUpdate: priceFeedIdToPriceUpdateAccount[SOL_PRICE_FEED_ID], - ethPriceUpdate: priceFeedIdToPriceUpdateAccount[ETH_PRICE_FEED_ID], - }) - .instruction(), - signers: [], -}; - -const transactions = pythSolanaReceiver.batchIntoVersionedTransactions( - [...postInstructions, consumerInstruction, ...closeInstructions], - { computeUnitPriceMicroLamports: 1000000 } -); // Put all the instructions together -await pythSolanaReceiver.provider.sendAll(transactions); +Price updates are relatively large and can take multiple transactions to post on the blockchain. +You can reduce the size of the transaction payload by using `addPostPartiallyVerifiedPriceUpdates` instead of `addPostPriceUpdates`. +This method does sacrifice some security however -- please see the method documentation for more details. + +### Get Instructions + +The `PythTransactionBuilder` class used in the examples above helps craft transactions that update prices and then use them in successive instructions. +However, if you would like to craft your own transactions, `PythSolanaReceiver` exposes several methods for constructing the instructions for working with both price update accounts and price feed accounts. +See `examples/post_price_update_instructions.ts` for an example of how to work with instructions. + +## Examples + +This SDK includes several runnable examples in the `examples/` directory. +You can run these examples by performing the following steps. +First, install and build any necessary typescript dependencies: + +1. Clone the `pyth-crosschain` git repo +2. Run `npm install` in the root of the repo +3. Run `npx lerna run build` anywhere in the repo +4. From the `pyth_solana_receiver` directory, run `npx ts-node examples/.ts` + +The examples require a Solana keypair with SOL to send Solana transactions. +By default, the examples will use the same Solana keypair used by the Solana CLI (at `~/.config/solana/id.json`). +You can override this default by setting the `SOLANA_KEYPAIR` environment variable: + +```bash +export SOLANA_KEYPAIR=/path/to/keypair/id.json ``` + +If you do not have a Solana keypair, you can generate one by downloading and installing the [Solana CLI](https://docs.solanalabs.com/cli/install), the running `solana-keygen new`. diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts new file mode 100644 index 0000000000..b298a6bfd8 --- /dev/null +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts @@ -0,0 +1,94 @@ +import { Connection, Keypair, PublicKey } from "@solana/web3.js"; +import { PriceServiceConnection } from "@pythnetwork/price-service-client"; +import { + InstructionWithEphemeralSigners, + PythSolanaReceiver, +} from "@pythnetwork/pyth-solana-receiver"; +import { Wallet } from "@coral-xyz/anchor"; +import fs from "fs"; + +// Get price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable +const SOL_PRICE_FEED_ID = + "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; +const ETH_PRICE_FEED_ID = + "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"; + +let keypairFile = ""; +if (process.env["SOLANA_KEYPAIR"]) { + keypairFile = process.env["SOLANA_KEYPAIR"]; +} else { + keypairFile = "~/.config/solana/id.json"; +} + +async function main() { + const connection = new Connection("https://api.mainnet-beta.solana.com"); + const keypair = await loadKeypairFromFile(keypairFile); + console.log( + `Sending transactions from account: ${keypair.publicKey.toBase58()}` + ); + const wallet = new Wallet(keypair); + const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet }); + + // Get the price update from hermes + const priceUpdateData = await getPriceUpdateData(); + console.log(`Posting price update: ${priceUpdateData}`); + + // If closeUpdateAccounts = true, the builder will automatically generate instructions to close the ephemeral price update accounts + // at the end of the transaction. Closing the accounts will reclaim their rent. + // The example is using closeUpdateAccounts = false so you can easily look up the price update account in an explorer. + const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({ + closeUpdateAccounts: false, + }); + // Post the price updates to ephemeral accounts, one per price feed. + await transactionBuilder.addPostPriceUpdates(priceUpdateData); + console.log( + "The SOL/USD price update will get posted to:", + transactionBuilder.getPriceUpdateAccount(SOL_PRICE_FEED_ID).toBase58() + ); + + await transactionBuilder.addPriceConsumerInstructions( + async ( + getPriceUpdateAccount: (priceFeedId: string) => PublicKey + ): Promise => { + // You can generate instructions here that use the price updates posted above. + // getPriceUpdateAccount() will give you the account you need. + // These accounts will be packed into transactions by the builder. + return []; + } + ); + + // Send the instructions in the builder in 1 or more transactions. + // The builder will pack the instructions into transactions automatically. + await pythSolanaReceiver.provider.sendAll( + await transactionBuilder.buildVersionedTransactions({ + computeUnitPriceMicroLamports: 1000000, + }) + ); +} + +// Fetch price update data from Hermes +async function getPriceUpdateData() { + const priceServiceConnection = new PriceServiceConnection( + "https://hermes.pyth.network/", + { priceFeedRequestConfig: { binary: true } } + ); + + return await priceServiceConnection.getLatestVaas([ + SOL_PRICE_FEED_ID, + ETH_PRICE_FEED_ID, + ]); +} + +// 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") + ); + return Keypair.fromSecretKey(Uint8Array.from(keypairData)); + } catch (error) { + throw new Error(`Error loading keypair from file: ${error}`); + } +} + +main(); diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update_instructions.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update_instructions.ts new file mode 100644 index 0000000000..9513708bd2 --- /dev/null +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update_instructions.ts @@ -0,0 +1,80 @@ +import { Connection, Keypair, PublicKey } from "@solana/web3.js"; +import { PriceServiceConnection } from "@pythnetwork/price-service-client"; +import { + InstructionWithEphemeralSigners, + PythSolanaReceiver, +} from "@pythnetwork/pyth-solana-receiver"; +import { Wallet } from "@coral-xyz/anchor"; +import fs from "fs"; + +// Get price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable +const SOL_PRICE_FEED_ID = + "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; +const ETH_PRICE_FEED_ID = + "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"; + +let keypairFile = ""; +if (process.env["SOLANA_KEYPAIR"]) { + keypairFile = process.env["SOLANA_KEYPAIR"]; +} else { + keypairFile = "~/.config/solana/id.json"; +} + +async function main() { + const connection = new Connection("https://api.mainnet-beta.solana.com"); + const keypair = await loadKeypairFromFile(keypairFile); + console.log( + `Sending transactions from account: ${keypair.publicKey.toBase58()}` + ); + const wallet = new Wallet(keypair); + const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet }); + + // Get the price update from hermes + const priceUpdateData = await getPriceUpdateData(); + console.log(`Posting price update: ${priceUpdateData}`); + + // Get instructions to post the price update data and to close the accounts later + const { + postInstructions, + closeInstructions, + priceFeedIdToPriceUpdateAccount, + } = await pythSolanaReceiver.buildPostPriceUpdateInstructions( + priceUpdateData + ); + + // Put your instructions here + const consumerInstructions: InstructionWithEphemeralSigners[] = []; + + const transactions = await pythSolanaReceiver.batchIntoVersionedTransactions( + [...postInstructions, ...consumerInstructions, ...closeInstructions], + { computeUnitPriceMicroLamports: 1000000 } + ); // Put all the instructions together + await pythSolanaReceiver.provider.sendAll(transactions); +} + +// Fetch price update data from Hermes +async function getPriceUpdateData() { + const priceServiceConnection = new PriceServiceConnection( + "https://hermes.pyth.network/", + { priceFeedRequestConfig: { binary: true } } + ); + + return await priceServiceConnection.getLatestVaas([ + SOL_PRICE_FEED_ID, + ETH_PRICE_FEED_ID, + ]); +} + +// 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") + ); + return Keypair.fromSecretKey(Uint8Array.from(keypairData)); + } catch (error) { + throw new Error(`Error loading keypair from file: ${error}`); + } +} + +main(); diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts new file mode 100644 index 0000000000..ad93297a34 --- /dev/null +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts @@ -0,0 +1,94 @@ +import { Connection, Keypair, PublicKey } from "@solana/web3.js"; +import { PriceServiceConnection } from "@pythnetwork/price-service-client"; +import { + InstructionWithEphemeralSigners, + PythSolanaReceiver, +} from "@pythnetwork/pyth-solana-receiver"; +import { Wallet } from "@coral-xyz/anchor"; +import fs from "fs"; + +// Get price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable +const SOL_PRICE_FEED_ID = + "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; +const ETH_PRICE_FEED_ID = + "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"; + +let keypairFile = ""; +if (process.env["SOLANA_KEYPAIR"]) { + keypairFile = process.env["SOLANA_KEYPAIR"]; +} else { + keypairFile = "~/.config/solana/id.json"; +} + +async function main() { + const connection = new Connection("https://api.mainnet-beta.solana.com"); + const keypair = await loadKeypairFromFile(keypairFile); + console.log( + `Sending transactions from account: ${keypair.publicKey.toBase58()}` + ); + const wallet = new Wallet(keypair); + const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet }); + + // Get the price update from hermes + const priceUpdateData = await getPriceUpdateData(); + console.log(`Posting price update: ${priceUpdateData}`); + + // The shard indicates which set of price feed accounts you wish to update. + const shardId = 1; + + const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({}); + // Update the price feed accounts for the feed ids in priceUpdateData (in this example, SOL and ETH) and shard id. + await transactionBuilder.addUpdatePriceFeed(priceUpdateData, shardId); + console.log( + "The SOL/USD price update will get posted to:", + pythSolanaReceiver + .getPriceFeedAccountAddress(SOL_PRICE_FEED_ID, shardId) + .toBase58() + ); + + await transactionBuilder.addPriceConsumerInstructions( + async ( + getPriceUpdateAccount: (priceFeedId: string) => PublicKey + ): Promise => { + // You can generate instructions here that use the price updates posted above. + // getPriceUpdateAccount() will give you the account you need. + // These accounts will be packed into transactions by the builder. + return []; + } + ); + + // Send the instructions in the builder in 1 or more transactions. + // The builder will pack the instructions into transactions automatically. + await pythSolanaReceiver.provider.sendAll( + await transactionBuilder.buildVersionedTransactions({ + computeUnitPriceMicroLamports: 1000000, + }) + ); +} + +// Fetch price update data from Hermes +async function getPriceUpdateData() { + const priceServiceConnection = new PriceServiceConnection( + "https://hermes.pyth.network/", + { priceFeedRequestConfig: { binary: true } } + ); + + return await priceServiceConnection.getLatestVaas([ + SOL_PRICE_FEED_ID, + ETH_PRICE_FEED_ID, + ]); +} + +// 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") + ); + return Keypair.fromSecretKey(Uint8Array.from(keypairData)); + } catch (error) { + throw new Error(`Error loading keypair from file: ${error}`); + } +} + +main(); 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 3ebf2e4c53..319e8a8ab9 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.4.0", + "version": "0.5.0", "description": "Pyth solana receiver SDK", "homepage": "https://pyth.network", "main": "lib/index.js", 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 5654eb628e..c63f2fc52d 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 @@ -65,9 +65,12 @@ export type PythTransactionBuilderConfig = { * - Consume price updates in a consumer program * - (Optionally) Close price update and encoded vaa accounts to recover the rent (`closeUpdateAccounts` in `PythTransactionBuilderConfig`) * - * `addPostPriceUpdates` vs `addUpdatePriceFeed`: - * - `addPostPriceUpdates` is used to post price updates to ephemeral accounts. Use this to post a price update from the present or from the past for your program to consume. - * - `addUpdatePriceFeed` is used to post price updates to price feed accounts, these are fixed accounts for each feed id that can only be updated with a more recent price update than the one they contain. Their addresses can be found using `getPriceFeedAccountAddress`. Use this to update a shared price feed account with a recent price + * This class provides methods for working with both price update accounts and price feed accounts. + * Price update accounts are ephemeral accounts containing a single price update, whereas price feed accounts are long-lived + * accounts that always hold price data for a specific feed id. Price feed accounts can be updated to advance the current price. + * Applications should choose which type of account to work with based on their needs. In general, applications that + * want the price at a specific time (e.g., to settle a trade) should use price update accounts, while applications that want + * any recent price should use price feed accounts. * * @example * ```typescript @@ -620,10 +623,9 @@ export class PythSolanaReceiver { .accounts({ pythSolanaReceiver: this.receiver.programId, encodedVaa, - priceFeedAccount: getPriceFeedAccountAddress( + priceFeedAccount: this.getPriceFeedAccountAddress( shardId, - feedId, - this.pushOracle.programId + feedId ), treasury: getTreasuryPda( DEFAULT_TREASURY_ID, @@ -638,11 +640,7 @@ export class PythSolanaReceiver { priceFeedIdToPriceUpdateAccount[ "0x" + parsePriceFeedMessage(update.message).feedId.toString("hex") - ] = getPriceFeedAccountAddress( - shardId, - feedId, - this.pushOracle.programId - ); + ] = this.getPriceFeedAccountAddress(shardId, feedId); } } return { @@ -717,11 +715,21 @@ export class PythSolanaReceiver { priceFeedId: Buffer ): Promise { return this.receiver.account.priceUpdateV2.fetchNullable( - getPriceFeedAccountAddress( - shardId, - priceFeedId, - this.pushOracle.programId - ) + this.getPriceFeedAccountAddress(shardId, priceFeedId) + ); + } + + /** + * 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 price feed accounts for the same price feed id to exist. + * @param priceFeedId The price feed ID. + * @returns The address of the price feed account + */ + getPriceFeedAccountAddress(shardId: number, priceFeedId: Buffer): PublicKey { + return getPriceFeedAccountAddressHelper( + shardId, + priceFeedId, + this.pushOracle.programId ); } } @@ -733,7 +741,7 @@ export class PythSolanaReceiver { * @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( +function getPriceFeedAccountAddressHelper( shardId: number, feedId: Buffer, pushOracleProgramId?: PublicKey From 5e7cd332b7b293e4b80a1ed3bec2c9d88b5c9529 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Fri, 5 Apr 2024 13:34:45 -0700 Subject: [PATCH 25/28] add processed commitment --- .../sdk/js/pyth_solana_receiver/README.md | 2 +- .../examples/post_price_update.ts | 8 +++++--- .../examples/post_price_update_instructions.ts | 9 ++++++--- .../examples/update_price_feed.ts | 10 ++++++---- .../src/PythSolanaReceiver.ts | 17 ++++++++++++++--- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md index 08d1a50ba9..64c10455ad 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md @@ -149,7 +149,7 @@ The address of the price feed accounts can be derived automatically from the fee ```typescript const solUsdPriceFeedAccount = pythSolanaReceiver - .getPriceFeedAccountAddress(SOL_PRICE_FEED_ID, 1) + .getPriceFeedAccountAddress(1, SOL_PRICE_FEED_ID) .toBase58(); ``` diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts index b298a6bfd8..074970e12c 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts @@ -6,6 +6,7 @@ import { } from "@pythnetwork/pyth-solana-receiver"; import { Wallet } from "@coral-xyz/anchor"; import fs from "fs"; +import os from "os"; // Get price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable const SOL_PRICE_FEED_ID = @@ -17,11 +18,11 @@ let keypairFile = ""; if (process.env["SOLANA_KEYPAIR"]) { keypairFile = process.env["SOLANA_KEYPAIR"]; } else { - keypairFile = "~/.config/solana/id.json"; + keypairFile = `${os.homedir()}/.config/solana/id.json`; } async function main() { - const connection = new Connection("https://api.mainnet-beta.solana.com"); + const connection = new Connection("https://api.devnet.solana.com"); const keypair = await loadKeypairFromFile(keypairFile); console.log( `Sending transactions from account: ${keypair.publicKey.toBase58()}` @@ -62,7 +63,8 @@ async function main() { await pythSolanaReceiver.provider.sendAll( await transactionBuilder.buildVersionedTransactions({ computeUnitPriceMicroLamports: 1000000, - }) + }), + { preflightCommitment: "processed" } ); } diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update_instructions.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update_instructions.ts index 9513708bd2..454ce1a60f 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update_instructions.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update_instructions.ts @@ -6,6 +6,7 @@ import { } from "@pythnetwork/pyth-solana-receiver"; import { Wallet } from "@coral-xyz/anchor"; import fs from "fs"; +import os from "os"; // Get price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable const SOL_PRICE_FEED_ID = @@ -17,11 +18,11 @@ let keypairFile = ""; if (process.env["SOLANA_KEYPAIR"]) { keypairFile = process.env["SOLANA_KEYPAIR"]; } else { - keypairFile = "~/.config/solana/id.json"; + keypairFile = `${os.homedir()}/.config/solana/id.json`; } async function main() { - const connection = new Connection("https://api.mainnet-beta.solana.com"); + const connection = new Connection("https://api.devnet.solana.com"); const keypair = await loadKeypairFromFile(keypairFile); console.log( `Sending transactions from account: ${keypair.publicKey.toBase58()}` @@ -49,7 +50,9 @@ async function main() { [...postInstructions, ...consumerInstructions, ...closeInstructions], { computeUnitPriceMicroLamports: 1000000 } ); // Put all the instructions together - await pythSolanaReceiver.provider.sendAll(transactions); + await pythSolanaReceiver.provider.sendAll(transactions, { + preflightCommitment: "processed", + }); } // Fetch price update data from Hermes diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts index ad93297a34..5c10b95370 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts @@ -6,6 +6,7 @@ import { } from "@pythnetwork/pyth-solana-receiver"; import { Wallet } from "@coral-xyz/anchor"; import fs from "fs"; +import os from "os"; // Get price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable const SOL_PRICE_FEED_ID = @@ -17,11 +18,11 @@ let keypairFile = ""; if (process.env["SOLANA_KEYPAIR"]) { keypairFile = process.env["SOLANA_KEYPAIR"]; } else { - keypairFile = "~/.config/solana/id.json"; + keypairFile = `${os.homedir()}/.config/solana/id.json`; } async function main() { - const connection = new Connection("https://api.mainnet-beta.solana.com"); + const connection = new Connection("https://api.devnet.solana.com"); const keypair = await loadKeypairFromFile(keypairFile); console.log( `Sending transactions from account: ${keypair.publicKey.toBase58()}` @@ -42,7 +43,7 @@ async function main() { console.log( "The SOL/USD price update will get posted to:", pythSolanaReceiver - .getPriceFeedAccountAddress(SOL_PRICE_FEED_ID, shardId) + .getPriceFeedAccountAddress(shardId, SOL_PRICE_FEED_ID) .toBase58() ); @@ -62,7 +63,8 @@ async function main() { await pythSolanaReceiver.provider.sendAll( await transactionBuilder.buildVersionedTransactions({ computeUnitPriceMicroLamports: 1000000, - }) + }), + { preflightCommitment: "processed" } ); } 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 c63f2fc52d..e8d69d5c6a 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 @@ -725,8 +725,19 @@ export class PythSolanaReceiver { * @param priceFeedId The price feed ID. * @returns The address of the price feed account */ - getPriceFeedAccountAddress(shardId: number, priceFeedId: Buffer): PublicKey { - return getPriceFeedAccountAddressHelper( + getPriceFeedAccountAddress( + shardId: number, + priceFeedId: Buffer | string + ): PublicKey { + if (typeof priceFeedId == "string") { + if (priceFeedId.startsWith("0x")) { + priceFeedId = Buffer.from(priceFeedId.slice(2), "hex"); + } else { + priceFeedId = Buffer.from(priceFeedId, "hex"); + } + } + + return getPriceFeedAccountForProgram( shardId, priceFeedId, this.pushOracle.programId @@ -741,7 +752,7 @@ export class PythSolanaReceiver { * @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 getPriceFeedAccountAddressHelper( +function getPriceFeedAccountForProgram( shardId: number, feedId: Buffer, pushOracleProgramId?: PublicKey From bd054f35e9815f1b279d656363179358b00c8aa4 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Fri, 5 Apr 2024 13:37:59 -0700 Subject: [PATCH 26/28] fix comment --- .../sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 e8d69d5c6a..d68c1ca2a0 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 @@ -184,8 +184,7 @@ export class PythTransactionBuilder extends TransactionBuilder { /** * Add instructions to update price feed accounts to the builder. - * Price feed accounts are a special type of price update accounts. Instead of being ephemeral accounts, they are fixed accounts for each feed id that can only be updated with a more recent price update than the one they contain. - * Use this function to update a shared price feed account with a recent price + * Price feed accounts are fixed accounts per price feed id that can only be updated with a more recent price. * * @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 price feed accounts for the same price feed id to exist. From 395205733e7a0ac6b9a214b9d94cef2c3598d9c9 Mon Sep 17 00:00:00 2001 From: Jayant Krishnamurthy Date: Fri, 5 Apr 2024 13:40:03 -0700 Subject: [PATCH 27/28] whoops --- .../src/PythSolanaReceiver.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 d68c1ca2a0..83484004fb 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 @@ -728,14 +728,6 @@ export class PythSolanaReceiver { shardId: number, priceFeedId: Buffer | string ): PublicKey { - if (typeof priceFeedId == "string") { - if (priceFeedId.startsWith("0x")) { - priceFeedId = Buffer.from(priceFeedId.slice(2), "hex"); - } else { - priceFeedId = Buffer.from(priceFeedId, "hex"); - } - } - return getPriceFeedAccountForProgram( shardId, priceFeedId, @@ -753,17 +745,25 @@ export class PythSolanaReceiver { */ function getPriceFeedAccountForProgram( shardId: number, - feedId: Buffer, + priceFeedId: Buffer | string, pushOracleProgramId?: PublicKey ): PublicKey { - if (feedId.length != 32) { + if (typeof priceFeedId == "string") { + if (priceFeedId.startsWith("0x")) { + priceFeedId = Buffer.from(priceFeedId.slice(2), "hex"); + } else { + priceFeedId = Buffer.from(priceFeedId, "hex"); + } + } + + if (priceFeedId.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], + [shardBuffer, priceFeedId], pushOracleProgramId ?? DEFAULT_PUSH_ORACLE_PROGRAM_ID )[0]; } From 5af54bf39090f29018d27fee23e3d51efbb645a4 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Tue, 9 Apr 2024 13:41:57 +0100 Subject: [PATCH 28/28] Set compute units to a more reasonable value --- .../solana/sdk/js/pyth_solana_receiver/README.md | 4 ++-- .../js/pyth_solana_receiver/examples/post_price_update.ts | 2 +- .../examples/post_price_update_instructions.ts | 8 ++++++-- .../js/pyth_solana_receiver/examples/update_price_feed.ts | 2 +- .../sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md index 64c10455ad..9b3821f6f4 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/README.md +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/README.md @@ -103,7 +103,7 @@ await transactionBuilder.addPriceConsumerInstructions( // The builder will pack the instructions into transactions automatically. await pythSolanaReceiver.provider.sendAll( await transactionBuilder.buildVersionedTransactions({ - computeUnitPriceMicroLamports: 1000000, + computeUnitPriceMicroLamports: 100000, }) ); ``` @@ -139,7 +139,7 @@ await transactionBuilder.addPriceConsumerInstructions( // The builder will pack the instructions into transactions automatically. await pythSolanaReceiver.provider.sendAll( await transactionBuilder.buildVersionedTransactions({ - computeUnitPriceMicroLamports: 1000000, + computeUnitPriceMicroLamports: 100000, }) ); ``` diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts index 074970e12c..1a97baa48e 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update.ts @@ -62,7 +62,7 @@ async function main() { // The builder will pack the instructions into transactions automatically. await pythSolanaReceiver.provider.sendAll( await transactionBuilder.buildVersionedTransactions({ - computeUnitPriceMicroLamports: 1000000, + computeUnitPriceMicroLamports: 100000, }), { preflightCommitment: "processed" } ); diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update_instructions.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update_instructions.ts index 454ce1a60f..72d8e03b38 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update_instructions.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/post_price_update_instructions.ts @@ -1,4 +1,4 @@ -import { Connection, Keypair, PublicKey } from "@solana/web3.js"; +import { Connection, Keypair } from "@solana/web3.js"; import { PriceServiceConnection } from "@pythnetwork/price-service-client"; import { InstructionWithEphemeralSigners, @@ -42,13 +42,17 @@ async function main() { } = await pythSolanaReceiver.buildPostPriceUpdateInstructions( priceUpdateData ); + console.log( + "The SOL/USD price update will get posted to:", + priceFeedIdToPriceUpdateAccount[SOL_PRICE_FEED_ID].toBase58() + ); // Put your instructions here const consumerInstructions: InstructionWithEphemeralSigners[] = []; const transactions = await pythSolanaReceiver.batchIntoVersionedTransactions( [...postInstructions, ...consumerInstructions, ...closeInstructions], - { computeUnitPriceMicroLamports: 1000000 } + { computeUnitPriceMicroLamports: 100000 } ); // Put all the instructions together await pythSolanaReceiver.provider.sendAll(transactions, { preflightCommitment: "processed", diff --git a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts index 5c10b95370..135d5ec7b6 100644 --- a/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts +++ b/target_chains/solana/sdk/js/pyth_solana_receiver/examples/update_price_feed.ts @@ -62,7 +62,7 @@ async function main() { // The builder will pack the instructions into transactions automatically. await pythSolanaReceiver.provider.sendAll( await transactionBuilder.buildVersionedTransactions({ - computeUnitPriceMicroLamports: 1000000, + computeUnitPriceMicroLamports: 100000, }), { preflightCommitment: "processed" } ); 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 83484004fb..bcc0753af2 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 @@ -86,7 +86,7 @@ export type PythTransactionBuilderConfig = { * console.log("The SOL/USD price update will get posted to:", transactionBuilder.getPriceUpdateAccount(SOL_PRICE_FEED_ID).toBase58()) * await transactionBuilder.addPriceConsumerInstructions(...) * - * await pythSolanaReceiver.provider.sendAll(await transactionBuilder.buildVersionedTransactions({computeUnitPriceMicroLamports:1000000})) + * await pythSolanaReceiver.provider.sendAll(await transactionBuilder.buildVersionedTransactions({computeUnitPriceMicroLamports:100000})) * ``` */ export class PythTransactionBuilder extends TransactionBuilder {