Skip to content

Commit 8458dc4

Browse files
committed
feat: add postTwapUpdates function
1 parent 4451b45 commit 8458dc4

File tree

2 files changed

+268
-1
lines changed

2 files changed

+268
-1
lines changed

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

Lines changed: 264 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
POST_UPDATE_COMPUTE_BUDGET,
3636
UPDATE_PRICE_FEED_COMPUTE_BUDGET,
3737
VERIFY_ENCODED_VAA_COMPUTE_BUDGET,
38+
WRITE_ENCODED_VAA_COMPUTE_BUDGET,
3839
} from "./compute_budget";
3940
import { Wallet } from "@coral-xyz/anchor";
4041
import {
@@ -43,6 +44,7 @@ import {
4344
findEncodedVaaAccountsByWriteAuthority,
4445
getGuardianSetIndex,
4546
trimSignatures,
47+
VAA_SPLIT_INDEX,
4648
} from "./vaa";
4749
import {
4850
TransactionBuilder,
@@ -194,6 +196,42 @@ export class PythTransactionBuilder extends TransactionBuilder {
194196
this.addInstructions(postInstructions);
195197
}
196198

199+
/**
200+
* Add instructions to post TWAP updates to the builder.
201+
* Use this function to post fully verified TWAP updates from the present or from the past for your program to consume.
202+
*
203+
* @param twapUpdateDataArray the output of the `@pythnetwork/hermes-client`'s `getLatestTwaps`. This is an array of verifiable price updates.
204+
*
205+
* @example
206+
* ```typescript
207+
* // Get the price feed ids from https://pyth.network/developers/price-feed-ids#pyth-evm-stable
208+
* const twapUpdateData = await hermesClient.getLatestTwaps([
209+
* SOL_PRICE_FEED_ID,
210+
* ETH_PRICE_FEED_ID,
211+
* ]);
212+
*
213+
* const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({});
214+
* await transactionBuilder.addPostTwapUpdates(priceUpdateData);
215+
* console.log("The SOL/USD price update will get posted to:", transactionBuilder.getPriceUpdateAccount(SOL_PRICE_FEED_ID).toBase58())
216+
* await transactionBuilder.addPriceConsumerInstructions(...)
217+
* ```
218+
*/
219+
async addPostTwapUpdates(twapUpdateDataArray: string[]) {
220+
const {
221+
postInstructions,
222+
priceFeedIdToTwapUpdateAccount,
223+
closeInstructions,
224+
} = await this.pythSolanaReceiver.buildPostTwapUpdateInstructions(
225+
twapUpdateDataArray
226+
);
227+
this.closeInstructions.push(...closeInstructions);
228+
Object.assign(
229+
this.priceFeedIdToPriceUpdateAccount,
230+
priceFeedIdToTwapUpdateAccount
231+
);
232+
this.addInstructions(postInstructions);
233+
}
234+
197235
/**
198236
* Add instructions to update price feed accounts to the builder.
199237
* Price feed accounts are fixed accounts per price feed id that can only be updated with a more recent price.
@@ -317,7 +355,7 @@ export class PythTransactionBuilder extends TransactionBuilder {
317355
this.priceFeedIdToPriceUpdateAccount[priceFeedId];
318356
if (!priceUpdateAccount) {
319357
throw new Error(
320-
`No price update account found for the price feed ID ${priceFeedId}. Make sure to call addPostPriceUpdates or addPostPartiallyVerifiedPriceUpdates before calling this function.`
358+
`No price update account found for the price feed ID ${priceFeedId}. Make sure to call addPostPriceUpdates or addPostPartiallyVerifiedPriceUpdates or postTwapUpdates before calling this function.`
321359
);
322360
}
323361
return priceUpdateAccount;
@@ -596,6 +634,231 @@ export class PythSolanaReceiver {
596634
};
597635
}
598636

637+
/**
638+
* Build a series of helper instructions that post TWAP updates to the Pyth Solana Receiver program and another series to close the encoded vaa accounts and the TWAP update accounts.
639+
*
640+
* @param twapUpdateDataArray the output of the `@pythnetwork/price-service-client`'s `PriceServiceConnection.getLatestTwaps`. This is an array of verifiable price updates.
641+
* @returns `postInstructions`: the instructions to post the TWAP updates, these should be called before consuming the price updates
642+
* @returns `priceFeedIdToTwapUpdateAccount`: 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 TWAP update.
643+
* @returns `closeInstructions`: the instructions to close the TWAP update accounts, these should be called after consuming the TWAP updates
644+
*/
645+
async buildPostTwapUpdateInstructions(
646+
twapUpdateDataArray: string[]
647+
): Promise<{
648+
postInstructions: InstructionWithEphemeralSigners[];
649+
priceFeedIdToTwapUpdateAccount: Record<string, PublicKey>;
650+
closeInstructions: InstructionWithEphemeralSigners[];
651+
}> {
652+
const postInstructions: InstructionWithEphemeralSigners[] = [];
653+
const priceFeedIdToTwapUpdateAccount: Record<string, PublicKey> = {};
654+
const closeInstructions: InstructionWithEphemeralSigners[] = [];
655+
656+
const treasuryId = getRandomTreasuryId();
657+
658+
if (twapUpdateDataArray.length !== 2) {
659+
throw new Error(
660+
"twapUpdateDataArray must contain exactly two updates (start and end)"
661+
);
662+
}
663+
664+
const [startUpdateData, endUpdateData] = twapUpdateDataArray.map((data) =>
665+
parseAccumulatorUpdateData(Buffer.from(data, "base64"))
666+
);
667+
668+
// Validate that the start and end updates contain the same number of price feeds
669+
if (startUpdateData.updates.length !== endUpdateData.updates.length) {
670+
throw new Error(
671+
"Start and end updates must contain the same number of price feeds"
672+
);
673+
}
674+
675+
// // Verify the VAAs
676+
// const [startVaa, endVaa] = await Promise.all([
677+
// this.buildPostEncodedVaaInstructions(startUpdateData.vaa),
678+
// this.buildPostEncodedVaaInstructions(endUpdateData.vaa)
679+
// ]);
680+
// postInstructions.push(...startVaa.postInstructions, ...endVaa.postInstructions);
681+
// closeInstructions.push(...startVaa.closeInstructions, ...endVaa.closeInstructions);
682+
683+
// const { encodedVaaAddress: startEncodedVaa } = startVaa;
684+
// const { encodedVaaAddress: endEncodedVaa } = endVaa;
685+
686+
// TRANSACTION 1: Create, init, write initial data for Start VAA
687+
// Create
688+
const trimmedStartVaa = trimSignatures(startUpdateData.vaa, 13);
689+
const startEncodedVaaKeypair = new Keypair();
690+
postInstructions.push(
691+
await buildEncodedVaaCreateInstruction(
692+
this.wormhole,
693+
trimmedStartVaa,
694+
startEncodedVaaKeypair
695+
)
696+
);
697+
// Init
698+
postInstructions.push({
699+
instruction: await this.wormhole.methods
700+
.initEncodedVaa()
701+
.accounts({
702+
encodedVaa: startEncodedVaaKeypair.publicKey,
703+
})
704+
.instruction(),
705+
signers: [],
706+
computeUnits: INIT_ENCODED_VAA_COMPUTE_BUDGET,
707+
});
708+
709+
// Write initial data
710+
postInstructions.push(
711+
...(await buildWriteEncodedVaaWithSplitInstructions(
712+
this.wormhole,
713+
trimmedStartVaa,
714+
startEncodedVaaKeypair.publicKey
715+
))
716+
);
717+
718+
// TRANSACTION 2: Create, init, write initial data for End VAA
719+
// Create
720+
const trimmedEndVaa = trimSignatures(endUpdateData.vaa, 13);
721+
const endEncodedVaaKeypair = new Keypair();
722+
postInstructions.push(
723+
await buildEncodedVaaCreateInstruction(
724+
this.wormhole,
725+
trimmedEndVaa,
726+
endEncodedVaaKeypair
727+
)
728+
);
729+
// Init
730+
postInstructions.push({
731+
instruction: await this.wormhole.methods
732+
.initEncodedVaa()
733+
.accounts({
734+
encodedVaa: endEncodedVaaKeypair.publicKey,
735+
})
736+
.instruction(),
737+
signers: [],
738+
computeUnits: INIT_ENCODED_VAA_COMPUTE_BUDGET,
739+
});
740+
741+
// Write initial data
742+
postInstructions.push(
743+
...(await buildWriteEncodedVaaWithSplitInstructions(
744+
this.wormhole,
745+
trimmedEndVaa,
746+
endEncodedVaaKeypair.publicKey
747+
))
748+
);
749+
750+
// TRANSACTION 3: Write remaining data and verify for Start & End VAAs
751+
// Write remaining data for start VAA
752+
postInstructions.push({
753+
instruction: await this.wormhole.methods
754+
.writeEncodedVaa({
755+
index: VAA_SPLIT_INDEX,
756+
data: trimmedStartVaa.subarray(VAA_SPLIT_INDEX),
757+
})
758+
.accounts({
759+
draftVaa: startEncodedVaaKeypair.publicKey,
760+
})
761+
.instruction(),
762+
signers: [],
763+
computeUnits: WRITE_ENCODED_VAA_COMPUTE_BUDGET,
764+
});
765+
766+
// Write remaining data for end VAA
767+
postInstructions.push({
768+
instruction: await this.wormhole.methods
769+
.writeEncodedVaa({
770+
index: VAA_SPLIT_INDEX,
771+
data: trimmedEndVaa.subarray(VAA_SPLIT_INDEX),
772+
})
773+
.accounts({
774+
draftVaa: endEncodedVaaKeypair.publicKey,
775+
})
776+
.instruction(),
777+
signers: [],
778+
computeUnits: WRITE_ENCODED_VAA_COMPUTE_BUDGET,
779+
});
780+
781+
// Verify start VAA
782+
const startGuardianSetIndex = getGuardianSetIndex(trimmedStartVaa);
783+
postInstructions.push({
784+
instruction: await this.wormhole.methods
785+
.verifyEncodedVaaV1()
786+
.accounts({
787+
guardianSet: getGuardianSetPda(
788+
startGuardianSetIndex,
789+
this.wormhole.programId
790+
),
791+
draftVaa: startEncodedVaaKeypair.publicKey,
792+
})
793+
.instruction(),
794+
signers: [],
795+
computeUnits: VERIFY_ENCODED_VAA_COMPUTE_BUDGET,
796+
});
797+
798+
// Verify end VAA
799+
const endGuardianSetIndex = getGuardianSetIndex(trimmedEndVaa);
800+
postInstructions.push({
801+
instruction: await this.wormhole.methods
802+
.verifyEncodedVaaV1()
803+
.accounts({
804+
guardianSet: getGuardianSetPda(
805+
startGuardianSetIndex,
806+
this.wormhole.programId
807+
),
808+
draftVaa: startEncodedVaaKeypair.publicKey,
809+
})
810+
.accounts({
811+
guardianSet: getGuardianSetPda(
812+
startGuardianSetIndex,
813+
this.wormhole.programId
814+
),
815+
draftVaa: endEncodedVaaKeypair.publicKey,
816+
})
817+
.instruction(),
818+
signers: [],
819+
computeUnits: VERIFY_ENCODED_VAA_COMPUTE_BUDGET,
820+
});
821+
822+
// Post a TWAP update to the receiver contract for each price feed
823+
for (let i = 0; i < startUpdateData.updates.length; i++) {
824+
const startUpdate = startUpdateData.updates[i];
825+
const endUpdate = endUpdateData.updates[i];
826+
827+
const twapUpdateKeypair = new Keypair();
828+
postInstructions.push({
829+
instruction: await this.receiver.methods
830+
.postTwapUpdate({
831+
startMerklePriceUpdate: startUpdate,
832+
endMerklePriceUpdate: endUpdate,
833+
treasuryId,
834+
})
835+
.accounts({
836+
startEncodedVaa: startEncodedVaaKeypair.publicKey,
837+
endEncodedVaa: endEncodedVaaKeypair.publicKey,
838+
twapUpdateAccount: twapUpdateKeypair.publicKey,
839+
treasury: getTreasuryPda(treasuryId, this.receiver.programId),
840+
config: getConfigPda(this.receiver.programId),
841+
})
842+
.instruction(),
843+
signers: [twapUpdateKeypair],
844+
computeUnits: POST_UPDATE_COMPUTE_BUDGET,
845+
});
846+
847+
priceFeedIdToTwapUpdateAccount[
848+
"0x" + parsePriceFeedMessage(startUpdate.message).feedId.toString("hex")
849+
] = twapUpdateKeypair.publicKey;
850+
closeInstructions.push(
851+
await this.buildClosePriceUpdateInstruction(twapUpdateKeypair.publicKey)
852+
);
853+
}
854+
855+
return {
856+
postInstructions,
857+
priceFeedIdToTwapUpdateAccount,
858+
closeInstructions,
859+
};
860+
}
861+
599862
/**
600863
* 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.
601864
*

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export const POST_UPDATE_ATOMIC_COMPUTE_BUDGET = 170000;
1010
* A hard-coded budget for the compute units required for the `postUpdate` instruction in the Pyth Solana Receiver program.
1111
*/
1212
export const POST_UPDATE_COMPUTE_BUDGET = 35000;
13+
/**
14+
* A hard-coded budget for the compute units required for the `postUpdate` instruction in the Pyth Solana Receiver program.
15+
*/
16+
export const POST_TWAP_UPDATE_COMPUTE_BUDGET = 100_000;
1317
/**
1418
* A hard-coded budget for the compute units required for the `updatePriceFeed` instruction in the Pyth Push Oracle program.
1519
*/

0 commit comments

Comments
 (0)