Skip to content

Commit 8b1c29e

Browse files
authored
feat: transaction builder (#1356)
* Do it * Remove some duplicate code * Cleanup * Cleanup * Cleanup import * Correct description * Fix path * Cleanup deps * Unique * Works * Continue * Lint * Lint config * Fix ci * Checkpoint * Checkpoint * Gitignore * Cleanup * Cleanup * Continue building the sdk * build function * Remove files * Remove files * Rename * Refactor : make transaction builder * Make commitment * Move * Progress * Checkpoint * Ephemeral signers 2 * Checkpoint * Checkpoint * Fix bug * Cleanup idls * Compute units * Make program addresses configurable * Handle arrays * Handle arrays * Move PythSolanaReceiver * Cleanup constants * Contants * Refactor constants * Gitignore refactor * package lock * Cleanup idl * Add useful static * Add useful static * Add useful static * Lint * Add lint config * Docs * Comments * Docs * Don't touch this * Readme * Readme * Cleanup * Readme * Fix * address readme comments * from pyth, not pythnet * Add a couple more comments * Rename cleanup to close * Go go go * Gogogo * Go * Fix readme * Improve readme * Nit * Nits * Refactor withClose * Update comments * Cleanup * First rename * Rename 2 * Improve error message
1 parent 4534741 commit 8b1c29e

File tree

5 files changed

+254
-86
lines changed

5 files changed

+254
-86
lines changed

governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ const Proposal = ({
401401
squads.connection
402402
)
403403
builder.addInstruction({ instruction, signers: [] })
404-
const versionedTxs = await builder.getVersionedTransactions(
404+
const versionedTxs = await builder.buildVersionedTransactions(
405405
DEFAULT_PRIORITY_FEE_CONFIG
406406
)
407407
await sendTransactions(

target_chains/solana/sdk/js/pyth_solana_receiver/README.md

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,59 @@ Price update accounts can be closed by whoever wrote them to recover the rent.
1212
## Example use
1313

1414
```ts
15-
import { Connection, PublicKey } from '@solana/web3.js';
16-
import { PriceServiceConnection } from '@pythnetwork/price-service-client';
17-
import { PythSolanaReceiver } from '@pythnetwork/pyth-solana-receiver';
18-
import { MyFirstPythApp, IDL } from './idl/my_first_pyth_app';
19-
20-
21-
const SOL_PRICE_FEED_ID = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"
22-
const ETH_PRICE_FEED_ID = "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"
15+
import { Connection, PublicKey } from "@solana/web3.js";
16+
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
17+
import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver";
18+
import { MyFirstPythApp, IDL } from "./idl/my_first_pyth_app";
2319

24-
const priceServiceConnection = new PriceServiceConnection("https://hermes.pyth.network/", { priceFeedRequestConfig: { binary: true } });
25-
const priceUpdateData = await priceServiceConnection.getLatestVaas([SOL_PRICE_FEED_ID, ETH_PRICE_FEED_ID]); // Fetch off-chain price update data
20+
const SOL_PRICE_FEED_ID =
21+
"0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d";
22+
const ETH_PRICE_FEED_ID =
23+
"0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace";
2624

25+
const priceServiceConnection = new PriceServiceConnection(
26+
"https://hermes.pyth.network/",
27+
{ priceFeedRequestConfig: { binary: true } }
28+
);
29+
const priceUpdateData = await priceServiceConnection.getLatestVaas([
30+
SOL_PRICE_FEED_ID,
31+
ETH_PRICE_FEED_ID,
32+
]); // Fetch off-chain price update data
2733

28-
const myFirstPythApp = new Program<MyFirstPythApp>(IDL as MyFirstPythApp, , PublicKey.unique(), {})
29-
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: [] }] };
34+
const myFirstPythApp = new Program<MyFirstPythApp>(
35+
IDL as MyFirstPythApp,
36+
MY_FIRST_PYTH_APP_PROGRAM_ID,
37+
{}
38+
);
3039

31-
const pythSolanaReceiver = new PythSolanaReceiver({ connection, wallet });
32-
const transactions = await pythSolanaReceiver.withPriceUpdate(priceUpdateData, getInstructions, {})
33-
await pythSolanaReceiver.provider.sendAll(transactions);
40+
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
41+
await transactionBuilder.addPostPriceUpdates(priceUpdateData);
42+
await transactionBuilder.addPriceConsumerInstructions(
43+
async (
44+
getPriceUpdateAccount: (priceFeedId: string) => PublicKey
45+
): Promise<InstructionWithEphemeralSigners[]> => {
46+
return [
47+
{
48+
instruction: await myFirstPythApp.methods
49+
.consume()
50+
.accounts({
51+
solPriceUpdate: getPriceUpdateAccount(SOL_PRICE_FEED_ID),
52+
ethPriceUpdate: getPriceUpdateAccount(ETH_PRICE_FEED_ID),
53+
})
54+
.instruction(),
55+
signers: [],
56+
},
57+
];
58+
}
59+
);
60+
await pythSolanaReceiver.provider.sendAll(
61+
await transactionBuilder.buildVersionedTransactions({
62+
computeUnitPriceMicroLamports: 1000000,
63+
})
64+
);
3465
```
3566

36-
Or, alternatively:
67+
Alternatively you can use the instruction builder methods from `PythSolanaReceiver` :
3768

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

6293
const myFirstPythApp = new Program<MyFirstPythApp>(
6394
IDL as MyFirstPythApp,
64-
PublicKey.unique(),
95+
MY_FIRST_PYTH_APP_PROGRAM_ID,
6596
{}
6697
);
6798
const consumerInstruction: InstructionWithEphemeralSigners = {
@@ -77,7 +108,7 @@ const consumerInstruction: InstructionWithEphemeralSigners = {
77108

78109
const transactions = pythSolanaReceiver.batchIntoVersionedTransactions(
79110
[...postInstructions, consumerInstruction, ...closeInstructions],
80-
{}
111+
{ computeUnitPriceMicroLamports: 1000000 }
81112
); // Put all the instructions together
82113
await pythSolanaReceiver.provider.sendAll(transactions);
83114
```

target_chains/solana/sdk/js/pyth_solana_receiver/src/PythSolanaReceiver.ts

Lines changed: 196 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { AnchorProvider, Program } from "@coral-xyz/anchor";
2-
import { Connection, Signer, VersionedTransaction } from "@solana/web3.js";
2+
import {
3+
Connection,
4+
Signer,
5+
Transaction,
6+
VersionedTransaction,
7+
} from "@solana/web3.js";
38
import {
49
PythSolanaReceiver as PythSolanaReceiverProgram,
510
IDL as Idl,
@@ -39,12 +44,194 @@ import {
3944
PriorityFeeConfig,
4045
} from "@pythnetwork/solana-utils";
4146

47+
/**
48+
* Configuration for the PythTransactionBuilder
49+
* @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
50+
*/
51+
export type PythTransactionBuilderConfig = {
52+
closeUpdateAccounts?: boolean;
53+
};
54+
55+
/**
56+
* A builder class to build transactions that:
57+
* - Post price updates (fully or partially verified)
58+
* - Consume price updates in a consumer program
59+
* - (Optionally) Close price update and encoded vaa accounts to recover the rent (`closeUpdateAccounts` in `PythTransactionBuilderConfig`)
60+
*
61+
* @example
62+
* ```typescript
63+
* const priceUpdateData = await priceServiceConnection.getLatestVaas([
64+
* SOL_PRICE_FEED_ID,
65+
* ETH_PRICE_FEED_ID,
66+
* ]);
67+
*
68+
* const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
69+
* await transactionBuilder.addPostPriceUpdates(priceUpdateData);
70+
* await transactionBuilder.addPriceConsumerInstructions(...)
71+
*
72+
* await pythSolanaReceiver.provider.sendAll(await transactionBuilder.buildVersionedTransactions({computeUnitPriceMicroLamports:1000000}))
73+
* ```
74+
*/
75+
export class PythTransactionBuilder extends TransactionBuilder {
76+
readonly pythSolanaReceiver: PythSolanaReceiver;
77+
readonly closeInstructions: InstructionWithEphemeralSigners[];
78+
readonly priceFeedIdToPriceUpdateAccount: Record<string, PublicKey>;
79+
readonly closeUpdateAccounts: boolean;
80+
81+
constructor(
82+
pythSolanaReceiver: PythSolanaReceiver,
83+
config: PythTransactionBuilderConfig
84+
) {
85+
super(pythSolanaReceiver.wallet.publicKey, pythSolanaReceiver.connection);
86+
this.pythSolanaReceiver = pythSolanaReceiver;
87+
this.closeInstructions = [];
88+
this.priceFeedIdToPriceUpdateAccount = {};
89+
this.closeUpdateAccounts = config.closeUpdateAccounts ?? true;
90+
}
91+
92+
/**
93+
* Add instructions to post price updates to the builder.
94+
*
95+
* @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
96+
*/
97+
async addPostPriceUpdates(priceUpdateDataArray: string[]) {
98+
const {
99+
postInstructions,
100+
priceFeedIdToPriceUpdateAccount,
101+
closeInstructions,
102+
} = await this.pythSolanaReceiver.buildPostPriceUpdateInstructions(
103+
priceUpdateDataArray
104+
);
105+
this.closeInstructions.push(...closeInstructions);
106+
Object.assign(
107+
this.priceFeedIdToPriceUpdateAccount,
108+
priceFeedIdToPriceUpdateAccount
109+
);
110+
this.addInstructions(postInstructions);
111+
}
112+
113+
/**
114+
* Add instructions to post partially verified price updates to the builder.
115+
*
116+
* @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
117+
*
118+
* 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.
119+
* 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}.
120+
*
121+
* @example
122+
* ```typescript
123+
* const priceUpdateData = await priceServiceConnection.getLatestVaas([
124+
* SOL_PRICE_FEED_ID,
125+
* ETH_PRICE_FEED_ID,
126+
* ]);
127+
*
128+
* const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
129+
* await transactionBuilder.addPostPartiallyVerifiedPriceUpdates(priceUpdateData);
130+
* await transactionBuilder.addPriceConsumerInstructions(...)
131+
* ...
132+
* ```
133+
*/
134+
async addPostPartiallyVerifiedPriceUpdates(priceUpdateDataArray: string[]) {
135+
const {
136+
postInstructions,
137+
priceFeedIdToPriceUpdateAccount,
138+
closeInstructions,
139+
} = await this.pythSolanaReceiver.buildPostPriceUpdateAtomicInstructions(
140+
priceUpdateDataArray
141+
);
142+
this.closeInstructions.push(...closeInstructions);
143+
Object.assign(
144+
this.priceFeedIdToPriceUpdateAccount,
145+
priceFeedIdToPriceUpdateAccount
146+
);
147+
this.addInstructions(postInstructions);
148+
}
149+
150+
/**
151+
* Add instructions that consume price updates to the builder.
152+
*
153+
* @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.
154+
* 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.
155+
*
156+
* @example
157+
* ```typescript
158+
* ...
159+
* await transactionBuilder.addPostPriceUpdates(priceUpdateData);
160+
* await transactionBuilder.addPriceConsumerInstructions(
161+
* async (
162+
* getPriceUpdateAccount: ( priceFeedId: string) => PublicKey
163+
* ): Promise<InstructionWithEphemeralSigners[]> => {
164+
* return [
165+
* {
166+
* instruction: await myFirstPythApp.methods
167+
* .consume()
168+
* .accounts({
169+
* solPriceUpdate: getPriceUpdateAccount(SOL_PRICE_FEED_ID),
170+
* ethPriceUpdate: getPriceUpdateAccount(ETH_PRICE_FEED_ID),
171+
* })
172+
* .instruction(),
173+
* signers: [],
174+
* },
175+
* ];
176+
* }
177+
* );
178+
* ```
179+
*/
180+
async addPriceConsumerInstructions(
181+
getInstructions: (
182+
getPriceUpdateAccount: (priceFeedId: string) => PublicKey
183+
) => Promise<InstructionWithEphemeralSigners[]>
184+
) {
185+
this.addInstructions(
186+
await getInstructions(this.getPriceUpdateAccount.bind(this))
187+
);
188+
}
189+
190+
/**
191+
* Returns all the added instructions batched into versioned transactions, plus for each transaction the ephemeral signers that need to sign it
192+
*/
193+
async buildVersionedTransactions(
194+
args: PriorityFeeConfig
195+
): Promise<{ tx: VersionedTransaction; signers: Signer[] }[]> {
196+
if (this.closeUpdateAccounts) {
197+
this.addInstructions(this.closeInstructions);
198+
}
199+
return super.buildVersionedTransactions(args);
200+
}
201+
202+
/**
203+
* Returns all the added instructions batched into transactions, plus for each transaction the ephemeral signers that need to sign it
204+
*/
205+
buildLegacyTransactions(
206+
args: PriorityFeeConfig
207+
): { tx: Transaction; signers: Signer[] }[] {
208+
if (this.closeUpdateAccounts) {
209+
this.addInstructions(this.closeInstructions);
210+
}
211+
return super.buildLegacyTransactions(args);
212+
}
213+
214+
/**
215+
* 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.
216+
* 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.
217+
* */
218+
getPriceUpdateAccount(priceFeedId: string): PublicKey {
219+
const priceUpdateAccount =
220+
this.priceFeedIdToPriceUpdateAccount[priceFeedId];
221+
if (!priceUpdateAccount) {
222+
throw new Error(
223+
`No price update account found for the price feed ID ${priceFeedId}. Make sure to call addPostPriceUpdates or addPostPartiallyVerifiedPriceUpdates before calling this function.`
224+
);
225+
}
226+
return priceUpdateAccount;
227+
}
228+
}
229+
42230
/**
43231
* A class to interact with the Pyth Solana Receiver program.
44232
*
45-
* This class provides helpful methods to:
46-
* - Post price updates from Pythnet to the Pyth Solana Receiver program
47-
* - Consume price updates in a consumer program
233+
* This class provides helpful methods to build instructions to interact with the Pyth Solana Receiver program:
234+
* - Post price updates (fully or partially verified)
48235
* - Close price update and encoded vaa accounts to recover rent
49236
*/
50237
export class PythSolanaReceiver {
@@ -83,65 +270,12 @@ export class PythSolanaReceiver {
83270
}
84271

85272
/**
86-
* 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.
87-
* @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
88-
* @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.
89-
* @param priorityFeeConfig a configuration for the compute unit price to use for the transactions.
90-
* @returns an array of transactions and their corresponding ephemeral signers
273+
* Get a new transaction builder to build transactions that interact with the Pyth Solana Receiver program and consume price updates
91274
*/
92-
async withPriceUpdate(
93-
priceUpdateDataArray: string[],
94-
getInstructions: (
95-
priceFeedIdToPriceUpdateAccount: Record<string, PublicKey>
96-
) => Promise<InstructionWithEphemeralSigners[]>,
97-
priorityFeeConfig?: PriorityFeeConfig
98-
): Promise<{ tx: VersionedTransaction; signers: Signer[] }[]> {
99-
const {
100-
postInstructions,
101-
priceFeedIdToPriceUpdateAccount: priceFeedIdToPriceUpdateAccount,
102-
closeInstructions,
103-
} = await this.buildPostPriceUpdateInstructions(priceUpdateDataArray);
104-
return this.batchIntoVersionedTransactions(
105-
[
106-
...postInstructions,
107-
...(await getInstructions(priceFeedIdToPriceUpdateAccount)),
108-
...closeInstructions,
109-
],
110-
priorityFeeConfig ?? {}
111-
);
112-
}
113-
114-
/**
115-
* 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.
116-
*
117-
* 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.
118-
* 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}.
119-
*
120-
* @param priceUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestVaas`. This is an array of verifiable price updates.
121-
* @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.
122-
* @param priorityFeeConfig a configuration for the compute unit price to use for the transactions.
123-
* @returns an array of transactions and their corresponding ephemeral signers
124-
*/
125-
async withPartiallyVerifiedPriceUpdate(
126-
priceUpdateDataArray: string[],
127-
getInstructions: (
128-
priceFeedIdToPriceUpdateAccount: Record<string, PublicKey>
129-
) => Promise<InstructionWithEphemeralSigners[]>,
130-
priorityFeeConfig?: PriorityFeeConfig
131-
): Promise<{ tx: VersionedTransaction; signers: Signer[] }[]> {
132-
const {
133-
postInstructions,
134-
priceFeedIdToPriceUpdateAccount,
135-
closeInstructions,
136-
} = await this.buildPostPriceUpdateAtomicInstructions(priceUpdateDataArray);
137-
return this.batchIntoVersionedTransactions(
138-
[
139-
...postInstructions,
140-
...(await getInstructions(priceFeedIdToPriceUpdateAccount)),
141-
...closeInstructions,
142-
],
143-
priorityFeeConfig ?? {}
144-
);
275+
newTransactionBuilder(
276+
config: PythTransactionBuilderConfig
277+
): PythTransactionBuilder {
278+
return new PythTransactionBuilder(this, config);
145279
}
146280

147281
/**

target_chains/solana/sdk/js/pyth_solana_receiver/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
export { PythSolanaReceiver } from "./PythSolanaReceiver";
1+
export {
2+
PythSolanaReceiver,
3+
PythTransactionBuilder,
4+
} from "./PythSolanaReceiver";
25
export {
36
TransactionBuilder,
47
InstructionWithEphemeralSigners,

0 commit comments

Comments
 (0)