Skip to content
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

Merged
merged 79 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
2ebde4b
Do it
guibescos Feb 26, 2024
c086dcf
Remove some duplicate code
guibescos Feb 26, 2024
d32169f
Cleanup
guibescos Feb 26, 2024
b49e084
Cleanup
guibescos Feb 26, 2024
fb0d06f
Cleanup import
guibescos Feb 26, 2024
d08e92a
Correct description
guibescos Feb 26, 2024
d73e808
Fix path
guibescos Feb 26, 2024
3113026
Cleanup deps
guibescos Feb 26, 2024
4b645cc
Unique
guibescos Feb 26, 2024
191fa01
Works
guibescos Feb 26, 2024
9172fad
Continue
guibescos Feb 26, 2024
063de56
Lint
guibescos Feb 26, 2024
b905266
Lint config
guibescos Feb 26, 2024
81a3307
Fix ci
guibescos Feb 26, 2024
6a2bdbc
Checkpoint
guibescos Feb 20, 2024
dc687d1
Checkpoint
guibescos Feb 20, 2024
0045908
Gitignore
guibescos Feb 20, 2024
d8757d5
Cleanup
guibescos Feb 20, 2024
b854868
Cleanup
guibescos Feb 20, 2024
97f8bd0
Continue building the sdk
guibescos Feb 20, 2024
a5f0cae
build function
guibescos Feb 21, 2024
8328496
Remove files
guibescos Feb 21, 2024
a0f95ea
Remove files
guibescos Feb 21, 2024
131d8ce
Rename
guibescos Feb 21, 2024
f3fc9ef
Refactor : make transaction builder
guibescos Feb 23, 2024
db73a1f
Make commitment
guibescos Feb 23, 2024
520e5fb
Move
guibescos Feb 26, 2024
eedd806
Progress
guibescos Feb 27, 2024
42c7cd0
Checkpoint
guibescos Feb 27, 2024
07756b1
Ephemeral signers 2
guibescos Feb 27, 2024
16c1f86
Checkpoint
guibescos Feb 27, 2024
ef052fa
Checkpoint
guibescos Feb 27, 2024
160c011
Fix bug
guibescos Feb 27, 2024
2f7697c
Cleanup idls
guibescos Feb 28, 2024
89eb8e8
Compute units
guibescos Feb 28, 2024
0e362c5
Make program addresses configurable
guibescos Feb 28, 2024
99b76b6
Handle arrays
guibescos Feb 28, 2024
117a166
Handle arrays
guibescos Feb 28, 2024
2e044b0
Move PythSolanaReceiver
guibescos Feb 28, 2024
66bdbc7
Cleanup constants
guibescos Feb 28, 2024
0068d5f
Contants
guibescos Feb 28, 2024
dcdd39f
Refactor constants
guibescos Feb 28, 2024
9a13bb6
Gitignore refactor
guibescos Feb 28, 2024
3a2562c
package lock
guibescos Feb 28, 2024
6b2d09b
Cleanup idl
guibescos Feb 28, 2024
ecd2c4d
Add useful static
guibescos Feb 28, 2024
4b0094a
Add useful static
guibescos Feb 28, 2024
5926166
Add useful static
guibescos Feb 28, 2024
6290cab
Lint
guibescos Feb 28, 2024
b92e8ce
Add lint config
guibescos Feb 28, 2024
00554e9
Docs
guibescos Feb 29, 2024
a5c0fc4
Comments
guibescos Feb 29, 2024
afcfcf3
Docs
guibescos Feb 29, 2024
0825092
Merged
guibescos Feb 29, 2024
8e7d416
Don't touch this
guibescos Feb 29, 2024
c05b409
Readme
guibescos Feb 29, 2024
2e047f8
Readme
guibescos Feb 29, 2024
40adb68
Cleanup
guibescos Feb 29, 2024
0e68936
Readme
guibescos Feb 29, 2024
7ae5c93
Fix
guibescos Feb 29, 2024
09e4bc3
Merge branch 'main' into solana/js-sdk
guibescos Mar 13, 2024
8bf4dbd
address readme comments
guibescos Mar 13, 2024
64f2daf
from pyth, not pythnet
guibescos Mar 13, 2024
d0b6323
Add a couple more comments
guibescos Mar 13, 2024
3d7abc1
Rename cleanup to close
guibescos Mar 13, 2024
9165bbb
Go go go
guibescos Mar 13, 2024
01e050f
Gogogo
guibescos Mar 13, 2024
e14604e
Merge
guibescos Mar 13, 2024
380bddc
Go
guibescos Mar 13, 2024
2b36cf3
Fix readme
guibescos Mar 13, 2024
15eb363
Improve readme
guibescos Mar 13, 2024
42e9ef5
Nit
guibescos Mar 13, 2024
89870ba
Nits
guibescos Mar 13, 2024
4ba58f5
Refactor withClose
guibescos Mar 14, 2024
3401d22
Update comments
guibescos Mar 14, 2024
d7f9e44
Cleanup
guibescos Mar 14, 2024
f53f529
First rename
guibescos Mar 14, 2024
1365322
Rename 2
guibescos Mar 14, 2024
e0d8f8e
Improve error message
guibescos Mar 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ const Proposal = ({
squads.connection
)
builder.addInstruction({ instruction, signers: [] })
const versionedTxs = await builder.getVersionedTransactions(
const versionedTxs = await builder.buildVersionedTransactions(
DEFAULT_PRIORITY_FEE_CONFIG
)
await sendTransactions(
Expand Down
67 changes: 49 additions & 18 deletions target_chains/solana/sdk/js/pyth_solana_receiver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,59 @@ Price update accounts can be closed by whoever wrote them to recover the rent.
## Example use

```ts
import { Connection, 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';


const SOL_PRICE_FEED_ID = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"
const ETH_PRICE_FEED_ID = "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"
import { Connection, 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";

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
const SOL_PRICE_FEED_ID =
"0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d";
const ETH_PRICE_FEED_ID =
"0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace";

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

const myFirstPythApp = new Program<MyFirstPythApp>(IDL as MyFirstPythApp, , PublicKey.unique(), {})
const getInstructions = async (priceFeedIdToPriceUpdateAccount: Record<string, PublicKey>) => { return [{ instruction: await myFirstApp.methods.consume().accounts({ solPriceUpdate: priceFeedIdToPriceUpdateAccount[SOL_PRICE_FEED_ID], ethPriceUpdate: priceFeedIdToPriceUpdateAccount[ETH_PRICE_FEED_ID] }).instruction(), signers: [] }] };
const myFirstPythApp = new Program<MyFirstPythApp>(
IDL as MyFirstPythApp,
MY_FIRST_PYTH_APP_PROGRAM_ID,
{}
);

const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet });
const transactions = await pythSolanaReceiver.withPriceUpdate(priceUpdateData, getInstructions, {})
await pythSolanaReceiver.provider.sendAll(transactions);
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
await transactionBuilder.addPostPriceUpdates(priceUpdateData);
await transactionBuilder.addPriceConsumerInstructions(
async (
getPriceUpdateAccount: (priceFeedId: string) => PublicKey
): Promise<InstructionWithEphemeralSigners[]> => {
return [
{
instruction: await myFirstPythApp.methods
.consume()
.accounts({
solPriceUpdate: getPriceUpdateAccount(SOL_PRICE_FEED_ID),
ethPriceUpdate: getPriceUpdateAccount(ETH_PRICE_FEED_ID),
})
.instruction(),
signers: [],
},
];
}
);
await pythSolanaReceiver.provider.sendAll(
await transactionBuilder.buildVersionedTransactions({
computeUnitPriceMicroLamports: 1000000,
})
);
```

Or, alternatively:
Alternatively you can use the instruction builder methods from `PythSolanaReceiver` :

```ts
import { PublicKey } from "@solana/web3.js";
Expand Down Expand Up @@ -61,7 +92,7 @@ const { postInstructions, closeInstructions, priceFeedIdToPriceUpdateAccount } =

const myFirstPythApp = new Program<MyFirstPythApp>(
IDL as MyFirstPythApp,
PublicKey.unique(),
MY_FIRST_PYTH_APP_PROGRAM_ID,
{}
);
const consumerInstruction: InstructionWithEphemeralSigners = {
Expand All @@ -77,7 +108,7 @@ const consumerInstruction: InstructionWithEphemeralSigners = {

const transactions = pythSolanaReceiver.batchIntoVersionedTransactions(
[...postInstructions, consumerInstruction, ...closeInstructions],
{}
{ computeUnitPriceMicroLamports: 1000000 }
); // Put all the instructions together
await pythSolanaReceiver.provider.sendAll(transactions);
```
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { AnchorProvider, Program } from "@coral-xyz/anchor";
import { Connection, Signer, VersionedTransaction } from "@solana/web3.js";
import {
Connection,
Signer,
Transaction,
VersionedTransaction,
} from "@solana/web3.js";
import {
PythSolanaReceiver as PythSolanaReceiverProgram,
IDL as Idl,
Expand Down Expand Up @@ -39,12 +44,194 @@ import {
PriorityFeeConfig,
} from "@pythnetwork/solana-utils";

/**
* 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
*/
export type PythTransactionBuilderConfig = {
closeUpdateAccounts?: boolean;
};

/**
* 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 (`closeUpdateAccounts` in `PythTransactionBuilderConfig`)
*
* @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(...)
*
* await pythSolanaReceiver.provider.sendAll(await transactionBuilder.buildVersionedTransactions({computeUnitPriceMicroLamports:1000000}))
* ```
*/
export class PythTransactionBuilder extends TransactionBuilder {
readonly pythSolanaReceiver: PythSolanaReceiver;
readonly closeInstructions: InstructionWithEphemeralSigners[];
readonly priceFeedIdToPriceUpdateAccount: Record<string, PublicKey>;
readonly closeUpdateAccounts: boolean;

constructor(
pythSolanaReceiver: PythSolanaReceiver,
config: PythTransactionBuilderConfig
) {
super(pythSolanaReceiver.wallet.publicKey, pythSolanaReceiver.connection);
this.pythSolanaReceiver = pythSolanaReceiver;
this.closeInstructions = [];
this.priceFeedIdToPriceUpdateAccount = {};
this.closeUpdateAccounts = config.closeUpdateAccounts ?? true;
}

/**
* 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 addPostPriceUpdates(priceUpdateDataArray: string[]) {
const {
postInstructions,
priceFeedIdToPriceUpdateAccount,
closeInstructions,
} = await this.pythSolanaReceiver.buildPostPriceUpdateInstructions(
priceUpdateDataArray
);
this.closeInstructions.push(...closeInstructions);
Object.assign(
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.addPostPartiallyVerifiedPriceUpdates(priceUpdateData);
* await transactionBuilder.addPriceConsumerInstructions(...)
* ...
* ```
*/
async addPostPartiallyVerifiedPriceUpdates(priceUpdateDataArray: string[]) {
const {
postInstructions,
priceFeedIdToPriceUpdateAccount,
closeInstructions,
} = await this.pythSolanaReceiver.buildPostPriceUpdateAtomicInstructions(
priceUpdateDataArray
);
this.closeInstructions.push(...closeInstructions);
Object.assign(
this.priceFeedIdToPriceUpdateAccount,
priceFeedIdToPriceUpdateAccount
);
this.addInstructions(postInstructions);
}

/**
* 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.
*
* @example
* ```typescript
* ...
* await transactionBuilder.addPostPriceUpdates(priceUpdateData);
* await transactionBuilder.addPriceConsumerInstructions(
* async (
* getPriceUpdateAccount: ( priceFeedId: string) => PublicKey
* ): Promise<InstructionWithEphemeralSigners[]> => {
* return [
* {
* instruction: await myFirstPythApp.methods
* .consume()
* .accounts({
* solPriceUpdate: getPriceUpdateAccount(SOL_PRICE_FEED_ID),
* ethPriceUpdate: getPriceUpdateAccount(ETH_PRICE_FEED_ID),
* })
* .instruction(),
* signers: [],
* },
* ];
* }
* );
* ```
*/
async addPriceConsumerInstructions(
getInstructions: (
getPriceUpdateAccount: (priceFeedId: string) => PublicKey
) => Promise<InstructionWithEphemeralSigners[]>
) {
this.addInstructions(
await getInstructions(this.getPriceUpdateAccount.bind(this))
);
}

/**
* Returns all the added instructions batched into versioned transactions, plus for each transaction the ephemeral signers that need to sign it
*/
async buildVersionedTransactions(
args: PriorityFeeConfig
): Promise<{ tx: VersionedTransaction; signers: Signer[] }[]> {
if (this.closeUpdateAccounts) {
this.addInstructions(this.closeInstructions);
}
return super.buildVersionedTransactions(args);
}

/**
* Returns all the added instructions batched into transactions, plus for each transaction the ephemeral signers that need to sign it
*/
buildLegacyTransactions(
args: PriorityFeeConfig
): { tx: Transaction; signers: Signer[] }[] {
if (this.closeUpdateAccounts) {
this.addInstructions(this.closeInstructions);
}
return super.buildLegacyTransactions(args);
}

/**
* 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 =
this.priceFeedIdToPriceUpdateAccount[priceFeedId];
if (!priceUpdateAccount) {
throw new Error(
`No price update account found for the price feed ID ${priceFeedId}. Make sure to call addPostPriceUpdates or addPostPartiallyVerifiedPriceUpdates before calling this function.`
);
}
return priceUpdateAccount;
}
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 {
Expand Down Expand Up @@ -83,65 +270,12 @@ 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(
config: PythTransactionBuilderConfig
): PythTransactionBuilder {
return new PythTransactionBuilder(this, config);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export { PythSolanaReceiver } from "./PythSolanaReceiver";
export {
PythSolanaReceiver,
PythTransactionBuilder,
} from "./PythSolanaReceiver";
export {
TransactionBuilder,
InstructionWithEphemeralSigners,
Expand Down
Loading
Loading