-
Notifications
You must be signed in to change notification settings - Fork 222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: transaction builder #1356
Changes from 73 commits
2ebde4b
c086dcf
d32169f
b49e084
fb0d06f
d08e92a
d73e808
3113026
4b645cc
191fa01
9172fad
063de56
b905266
81a3307
6a2bdbc
dc687d1
0045908
d8757d5
b854868
97f8bd0
a5f0cae
8328496
a0f95ea
131d8ce
f3fc9ef
db73a1f
520e5fb
eedd806
42c7cd0
07756b1
16c1f86
ef052fa
160c011
2f7697c
89eb8e8
0e362c5
99b76b6
117a166
2e044b0
66bdbc7
0068d5f
dcdd39f
9a13bb6
3a2562c
6b2d09b
ecd2c4d
4b0094a
5926166
6290cab
b92e8ce
00554e9
a5c0fc4
afcfcf3
0825092
8e7d416
c05b409
2e047f8
40adb68
0e68936
7ae5c93
09e4bc3
8bf4dbd
64f2daf
d0b6323
3d7abc1
9165bbb
01e050f
e14604e
380bddc
2b36cf3
15eb363
42e9ef5
89870ba
4ba58f5
3401d22
d7f9e44
f53f529
1365322
e0d8f8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,12 +39,150 @@ import { | |
PriorityFeeConfig, | ||
} from "@pythnetwork/solana-utils"; | ||
|
||
/** | ||
* A builder class to build transactions that: | ||
* - Post price updates (fully or partially verified) | ||
* - Consume price updates in a consumer program | ||
* - (Optionally) Close price update and encoded vaa accounts to recover the rent | ||
* | ||
* @example | ||
* ```typescript | ||
* const priceUpdateData = await priceServiceConnection.getLatestVaas([ | ||
* SOL_PRICE_FEED_ID, | ||
* ETH_PRICE_FEED_ID, | ||
* ]); | ||
* | ||
* const transactionBuilder = pythSolanaReceiver.newTransactionBuilder(); | ||
* await transactionBuilder.withPostPriceUpdates(priceUpdateData); | ||
* await transactionBuilder.withPriceConsumerInstructions(...) | ||
* transactionBuilder.withCloseInstructions(); | ||
* | ||
* await pythSolanaReceiver.provider.sendAll(await transactionBuilder.getVersionedTransactions({computeUnitPriceMicroLamports:1000000})) | ||
* ``` | ||
*/ | ||
export class PythTransactionBuilder extends TransactionBuilder { | ||
readonly pythSolanaReceiver: PythSolanaReceiver; | ||
private closeInstructions: InstructionWithEphemeralSigners[]; | ||
private priceFeedIdToPriceUpdateAccount: Record<string, PublicKey>; | ||
|
||
constructor(pythSolanaReceiver: PythSolanaReceiver) { | ||
super(pythSolanaReceiver.wallet.publicKey, pythSolanaReceiver.connection); | ||
this.pythSolanaReceiver = pythSolanaReceiver; | ||
this.closeInstructions = []; | ||
this.priceFeedIdToPriceUpdateAccount = {}; | ||
} | ||
|
||
/** | ||
* 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. | ||
*/ | ||
async withPostPriceUpdates(priceUpdateDataArray: string[]) { | ||
const { | ||
postInstructions, | ||
priceFeedIdToPriceUpdateAccount, | ||
closeInstructions, | ||
} = await this.pythSolanaReceiver.buildPostPriceUpdateInstructions( | ||
priceUpdateDataArray | ||
); | ||
this.closeInstructions.push(...closeInstructions); | ||
this.priceFeedIdToPriceUpdateAccount = { | ||
...this.priceFeedIdToPriceUpdateAccount, | ||
...priceFeedIdToPriceUpdateAccount, | ||
}; | ||
this.addInstructions(postInstructions); | ||
} | ||
|
||
/** | ||
* Add instructions to post partially verified 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. | ||
* | ||
* Partially verified price updates are price updates where not all the guardian signatures have been verified. By default this methods checks `DEFAULT_REDUCED_GUARDIAN_SET_SIZE` signatures when posting the VAA. | ||
* If you are a on-chain program developer, make sure you understand the risks of consuming partially verified price updates here: {@link https://github.com/pyth-network/pyth-crosschain/blob/main/target_chains/solana/pyth_solana_receiver_state/src/price_update.rs}. | ||
* | ||
* @example | ||
* ```typescript | ||
* const priceUpdateData = await priceServiceConnection.getLatestVaas([ | ||
* SOL_PRICE_FEED_ID, | ||
* ETH_PRICE_FEED_ID, | ||
* ]); | ||
* | ||
* const transactionBuilder = pythSolanaReceiver.newTransactionBuilder(); | ||
* await transactionBuilder.withPostPartiallyVerifiedPriceUpdates(priceUpdateData); | ||
* await transactionBuilder.withPriceConsumerInstructions(...) | ||
* ... | ||
* ``` | ||
*/ | ||
async withPostPartiallyVerifiedPriceUpdates(priceUpdateDataArray: string[]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. very nit: normally There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah i agree with this. generally with builders there's always a question of "does this method mutate the internal state, or does it return a new builder". The |
||
const { | ||
postInstructions, | ||
priceFeedIdToPriceUpdateAccount, | ||
closeInstructions, | ||
} = await this.pythSolanaReceiver.buildPostPriceUpdateAtomicInstructions( | ||
priceUpdateDataArray | ||
); | ||
this.closeInstructions.push(...closeInstructions); | ||
this.priceFeedIdToPriceUpdateAccount = { | ||
...this.priceFeedIdToPriceUpdateAccount, | ||
...priceFeedIdToPriceUpdateAccount, | ||
}; | ||
this.addInstructions(postInstructions); | ||
} | ||
|
||
/** | ||
* Add instructions that consume price updates to the builder. | ||
* | ||
* @param getInstructions a function that given a map 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. | ||
* | ||
* @example | ||
* ```typescript | ||
* ... | ||
* await transactionBuilder.withPostPriceUpdates(priceUpdateData); | ||
* await transactionBuilder.withPriceConsumerInstructions( | ||
* async ( | ||
* priceFeedIdToPriceAccount: Record<string, PublicKey> | ||
* ): Promise<InstructionWithEphemeralSigners[]> => { | ||
* return [ | ||
* { | ||
* instruction: await myFirstPythApp.methods | ||
* .consume() | ||
* .accounts({ | ||
* solPriceUpdate: priceFeedIdToPriceUpdateAccount[SOL_PRICE_FEED_ID], | ||
* ethPriceUpdate: priceFeedIdToPriceUpdateAccount[ETH_PRICE_FEED_ID], | ||
* }) | ||
* .instruction(), | ||
* signers: [], | ||
* }, | ||
* ]; | ||
* } | ||
* ); | ||
* transactionBuilder.withCloseInstructions(); | ||
* ``` | ||
*/ | ||
async withPriceConsumerInstructions( | ||
getInstructions: ( | ||
priceFeedIdToPriceUpdateAccount: Record<string, PublicKey> | ||
) => Promise<InstructionWithEphemeralSigners[]> | ||
) { | ||
this.addInstructions( | ||
await getInstructions(this.priceFeedIdToPriceUpdateAccount) | ||
); | ||
} | ||
|
||
/** | ||
* Add instructions to close the encoded vaa accounts and the price update accounts created by `withPostPriceUpdates` and `withPostPartiallyVerifiedPriceUpdates` to reclaim rent. | ||
*/ | ||
withCloseInstructions() { | ||
this.addInstructions(this.closeInstructions); | ||
} | ||
guibescos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* A class to interact with the Pyth Solana Receiver program. | ||
* | ||
* This class provides helpful methods to: | ||
* - Post price updates from Pythnet to the Pyth Solana Receiver program | ||
* - Consume price updates in a consumer program | ||
* This class provides helpful methods to build instructions to interact with the Pyth Solana Receiver program: | ||
* - Post price updates (fully or partially verified) | ||
* - Close price update and encoded vaa accounts to recover rent | ||
*/ | ||
export class PythSolanaReceiver { | ||
|
@@ -83,65 +221,10 @@ export class PythSolanaReceiver { | |
} | ||
|
||
/** | ||
* Build a series of transactions that post price updates to the Pyth Solana Receiver program, consume them in a consumer program and close the encoded vaa accounts and price update accounts. | ||
* @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates. | ||
* @param getInstructions a function that given a map of price feed IDs to price update accounts, returns a series of instructions to consume the price updates in a consumer program. This function is a way for the user to indicate which accounts in their instruction need to be "replaced" with price update accounts. | ||
* @param priorityFeeConfig a configuration for the compute unit price to use for the transactions. | ||
* @returns an array of transactions and their corresponding ephemeral signers | ||
* Get a new transaction builder to build transactions that interact with the Pyth Solana Receiver program and consume price updates | ||
*/ | ||
async withPriceUpdate( | ||
priceUpdateDataArray: string[], | ||
getInstructions: ( | ||
priceFeedIdToPriceUpdateAccount: Record<string, PublicKey> | ||
) => Promise<InstructionWithEphemeralSigners[]>, | ||
priorityFeeConfig?: PriorityFeeConfig | ||
): Promise<{ tx: VersionedTransaction; signers: Signer[] }[]> { | ||
const { | ||
postInstructions, | ||
priceFeedIdToPriceUpdateAccount: priceFeedIdToPriceUpdateAccount, | ||
closeInstructions, | ||
} = await this.buildPostPriceUpdateInstructions(priceUpdateDataArray); | ||
return this.batchIntoVersionedTransactions( | ||
[ | ||
...postInstructions, | ||
...(await getInstructions(priceFeedIdToPriceUpdateAccount)), | ||
...closeInstructions, | ||
], | ||
priorityFeeConfig ?? {} | ||
); | ||
} | ||
|
||
/** | ||
* Build a series of transactions that post partially verified price updates to the Pyth Solana Receiver program, consume them in a consumer program and close the price update accounts. | ||
* | ||
* Partially verified price updates are price updates where not all the guardian signatures have been verified. By default this methods checks `DEFAULT_REDUCED_GUARDIAN_SET_SIZE` signatures when posting the VAA. | ||
* If you are a on-chain program developer, make sure you understand the risks of consuming partially verified price updates here: {@link https://github.com/pyth-network/pyth-crosschain/blob/main/target_chains/solana/pyth_solana_receiver_state/src/price_update.rs}. | ||
* | ||
* @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates. | ||
* @param getInstructions a function that given a map of price feed IDs to price update accounts, returns a series of instructions to consume the price updates in a consumer program. This function is a way for the user to indicate which accounts in their instruction need to be "replaced" with price update accounts. | ||
* @param priorityFeeConfig a configuration for the compute unit price to use for the transactions. | ||
* @returns an array of transactions and their corresponding ephemeral signers | ||
*/ | ||
async withPartiallyVerifiedPriceUpdate( | ||
priceUpdateDataArray: string[], | ||
getInstructions: ( | ||
priceFeedIdToPriceUpdateAccount: Record<string, PublicKey> | ||
) => Promise<InstructionWithEphemeralSigners[]>, | ||
priorityFeeConfig?: PriorityFeeConfig | ||
): Promise<{ tx: VersionedTransaction; signers: Signer[] }[]> { | ||
const { | ||
postInstructions, | ||
priceFeedIdToPriceUpdateAccount, | ||
closeInstructions, | ||
} = await this.buildPostPriceUpdateAtomicInstructions(priceUpdateDataArray); | ||
return this.batchIntoVersionedTransactions( | ||
[ | ||
...postInstructions, | ||
...(await getInstructions(priceFeedIdToPriceUpdateAccount)), | ||
...closeInstructions, | ||
], | ||
priorityFeeConfig ?? {} | ||
); | ||
newTransactionBuilder(): PythTransactionBuilder { | ||
return new PythTransactionBuilder(this); | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we enforce that
withPostPriceUpdates
,withPriceConsumerInstructions
andwithCloseInstructions
need to be called sequentially and each one only once?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be good to expose them. If you enforce it via different types (or markers from below) the auto complete will also guide people to use it correctly. If you have an intermediate step you can have
Builder & pricesMap: Record<String, String>
exposed and people can just calladdInstruction
oraddInstructions
without lambda function.Look at here or here to see how you can impose order using some type markers. Feel free to ignore it if you think it gets complex.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some of constraints here aren't strictly necessary right? Like in theory I could call
withPriceConsumerInstructions
multiple times adding different sets of instructions and the code will work (?)My inclination would be to avoid adding constraints on usage that aren't necessary for correctness -- you're just making the builder less flexible. see also the comment below about
withCloseInstructions
which I think helps resolve the one correctness constraint that exists.