@pythnetwork/pyth-solana-receiver
Version:
Pyth solana receiver SDK
271 lines (270 loc) • 11.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VAA_SPLIT_INDEX = exports.VAA_START = exports.VAA_SIGNATURE_SIZE = exports.DEFAULT_REDUCED_GUARDIAN_SET_SIZE = void 0;
exports.getGuardianSetIndex = getGuardianSetIndex;
exports.trimSignatures = trimSignatures;
exports.buildPostEncodedVaaInstructions = buildPostEncodedVaaInstructions;
exports.buildPostEncodedVaasForTwapInstructions = buildPostEncodedVaasForTwapInstructions;
exports.buildCloseEncodedVaaInstruction = buildCloseEncodedVaaInstruction;
exports.buildEncodedVaaCreateInstruction = buildEncodedVaaCreateInstruction;
exports.findEncodedVaaAccountsByWriteAuthority = findEncodedVaaAccountsByWriteAuthority;
const web3_js_1 = require("@solana/web3.js");
const compute_budget_1 = require("./compute_budget");
const sha256_1 = require("@noble/hashes/sha256");
const bytes_1 = require("@coral-xyz/anchor/dist/cjs/utils/bytes");
const address_1 = require("./address");
/**
* Get the index of the guardian set that signed a VAA
*/
function getGuardianSetIndex(vaa) {
return vaa.readUInt32BE(1);
}
/**
* The default number of signatures to keep in a VAA when using `trimSignatures`.
* This number was chosen as the maximum number of signatures so that the VAA's contents can be posted in a single Solana transaction.
*/
exports.DEFAULT_REDUCED_GUARDIAN_SET_SIZE = 5;
/**
* The size of a guardian signature in a VAA.
*
* It is 66 bytes long, the first byte is the guardian index and the next 65 bytes are the signature (including a recovery id).
*/
exports.VAA_SIGNATURE_SIZE = 66;
/**
* The start of the VAA bytes in an encoded VAA account. Before this offset, the account contains a header.
*/
exports.VAA_START = 46;
/**
* Writing the VAA to an encoded VAA account is done in 2 instructions.
*
* The first one writes the first `VAA_SPLIT_INDEX` bytes and the second one writes the rest.
*
* This number was chosen as the biggest number such that one can still call `createInstruction`,
* `initEncodedVaa` and `writeEncodedVaa` in a single Solana transaction, while using an address lookup table.
* This way, the packing of the instructions to post an encoded vaa is more efficient.
*/
exports.VAA_SPLIT_INDEX = 721;
/**
* Trim the number of signatures of a VAA.
*
* @returns the same VAA as the input, but with `n` signatures instead of the original number of signatures.
*
* A Wormhole VAA typically has a number of signatures equal to two thirds of the number of guardians. However,
* this function is useful to make VAAs smaller to post their contents in a single Solana transaction.
*/
function trimSignatures(vaa, n = exports.DEFAULT_REDUCED_GUARDIAN_SET_SIZE) {
const currentNumSignatures = vaa[5];
if (n > currentNumSignatures) {
throw new Error("Resulting VAA can't have more signatures than the original VAA");
}
const trimmedVaa = Buffer.concat([
vaa.subarray(0, 6 + n * exports.VAA_SIGNATURE_SIZE),
vaa.subarray(6 + currentNumSignatures * exports.VAA_SIGNATURE_SIZE),
]);
trimmedVaa[5] = n;
return trimmedVaa;
}
// Core function to generate VAA instruction groups
async function generateVaaInstructionGroups(wormhole, vaa) {
const encodedVaaKeypair = new web3_js_1.Keypair();
// Create and init instructions
const initInstructions = [
await buildEncodedVaaCreateInstruction(wormhole, vaa, encodedVaaKeypair),
{
instruction: await wormhole.methods
.initEncodedVaa()
.accounts({
encodedVaa: encodedVaaKeypair.publicKey,
})
.instruction(),
signers: [],
computeUnits: compute_budget_1.INIT_ENCODED_VAA_COMPUTE_BUDGET,
},
];
// First write instruction
const writeFirstPartInstructions = [
{
instruction: await wormhole.methods
.writeEncodedVaa({
index: 0,
data: vaa.subarray(0, exports.VAA_SPLIT_INDEX),
})
.accounts({
draftVaa: encodedVaaKeypair.publicKey,
})
.instruction(),
signers: [],
computeUnits: compute_budget_1.WRITE_ENCODED_VAA_COMPUTE_BUDGET,
},
];
// Second write and verify instructions
const writeSecondPartAndVerifyInstructions = [];
// The second write instruction is only needed if there are more bytes past the split index in the VAA
if (vaa.length > exports.VAA_SPLIT_INDEX) {
writeSecondPartAndVerifyInstructions.push({
instruction: await wormhole.methods
.writeEncodedVaa({
index: exports.VAA_SPLIT_INDEX,
data: vaa.subarray(exports.VAA_SPLIT_INDEX),
})
.accounts({
draftVaa: encodedVaaKeypair.publicKey,
})
.instruction(),
signers: [],
computeUnits: compute_budget_1.WRITE_ENCODED_VAA_COMPUTE_BUDGET,
});
}
writeSecondPartAndVerifyInstructions.push({
instruction: await wormhole.methods
.verifyEncodedVaaV1()
.accounts({
guardianSet: (0, address_1.getGuardianSetPda)(getGuardianSetIndex(vaa), wormhole.programId),
draftVaa: encodedVaaKeypair.publicKey,
})
.instruction(),
signers: [],
computeUnits: compute_budget_1.VERIFY_ENCODED_VAA_COMPUTE_BUDGET,
});
// Close instructions
const closeInstructions = [
{
instruction: await wormhole.methods
.closeEncodedVaa()
.accounts({ encodedVaa: encodedVaaKeypair.publicKey })
.instruction(),
signers: [],
computeUnits: compute_budget_1.CLOSE_ENCODED_VAA_COMPUTE_BUDGET,
},
];
return {
initInstructions,
writeFirstPartInstructions,
writeSecondPartAndVerifyInstructions,
closeInstructions,
encodedVaaAddress: encodedVaaKeypair.publicKey,
};
}
/**
* Build instructions to post a single VAA to the Wormhole program.
* The instructions can be packed efficiently into 2 transactions:
* - TX1: Create, init the encoded VAA account and write the first part of the VAA
* - TX2: Write the second part of the VAA and verify it
*
* @param wormhole - The Wormhole program instance
* @param vaa - The VAA buffer to post
* @returns {Object} Result containing:
* - encodedVaaAddress: Public key of the encoded VAA account
* - postInstructions: Instructions to post and verify the VAA
* - closeInstructions: Instructions to close the encoded VAA account and recover rent
*/
async function buildPostEncodedVaaInstructions(wormhole, vaa) {
const groups = await generateVaaInstructionGroups(wormhole, vaa);
// Pack instructions for optimal 2-transaction pattern:
// TX1: init + first write
// TX2: second write + verify
return {
encodedVaaAddress: groups.encodedVaaAddress,
postInstructions: [
...groups.initInstructions,
...groups.writeFirstPartInstructions,
...groups.writeSecondPartAndVerifyInstructions,
],
closeInstructions: groups.closeInstructions,
};
}
/**
* Build instructions to post two VAAs for TWAP (Time-Weighted Average Price) calculations,
* optimized for 3 transactions. This is specifically designed for posting start and end
* accumulator update VAAs efficiently.
* The instructions are packed into 3 transactions:
* - TX1: Initialize and write first part of start VAA
* - TX2: Initialize and write first part of end VAA
* - TX3: Write second part and verify both VAAs
*
* @param wormhole - The Wormhole program instance
* @param startUpdateData - Accumulator update data containing the start VAA
* @param endUpdateData - Accumulator update data containing the end VAA
* @returns {Object} Result containing:
* - startEncodedVaaAddress: Public key of the start VAA account
* - endEncodedVaaAddress: Public key of the end VAA account
* - postInstructions: Instructions to post and verify both VAAs
* - closeInstructions: Instructions to close both encoded VAA accounts
*/
async function buildPostEncodedVaasForTwapInstructions(wormhole, startUpdateData, endUpdateData) {
const startGroups = await generateVaaInstructionGroups(wormhole, startUpdateData.vaa);
const endGroups = await generateVaaInstructionGroups(wormhole, endUpdateData.vaa);
// Pack instructions for optimal 3-transaction pattern:
// TX1: start VAA init + first write
// TX2: end VAA init + first write
// TX3: both VAAs second write + verify
const postInstructions = [
// TX1
...startGroups.initInstructions,
...startGroups.writeFirstPartInstructions,
// TX2
...endGroups.initInstructions,
...endGroups.writeFirstPartInstructions,
// TX3
...startGroups.writeSecondPartAndVerifyInstructions,
...endGroups.writeSecondPartAndVerifyInstructions,
];
return {
startEncodedVaaAddress: startGroups.encodedVaaAddress,
endEncodedVaaAddress: endGroups.encodedVaaAddress,
postInstructions,
closeInstructions: [
...startGroups.closeInstructions,
...endGroups.closeInstructions,
],
};
}
/**
* Build an instruction to close an encoded VAA account, recovering the rent.
*/
async function buildCloseEncodedVaaInstruction(wormhole, encodedVaa) {
const instruction = await wormhole.methods
.closeEncodedVaa()
.accounts({ encodedVaa })
.instruction();
return {
instruction,
signers: [],
computeUnits: compute_budget_1.CLOSE_ENCODED_VAA_COMPUTE_BUDGET,
};
}
/**
* Build an instruction to create an encoded VAA account.
*
* This is the first step to post a VAA to the Wormhole program.
*/
async function buildEncodedVaaCreateInstruction(wormhole, vaa, encodedVaaKeypair) {
const encodedVaaSize = vaa.length + exports.VAA_START;
return {
instruction: await wormhole.account.encodedVaa.createInstruction(encodedVaaKeypair, encodedVaaSize),
signers: [encodedVaaKeypair],
};
}
/**
* Find all the encoded VAA accounts that have a given write authority
* @returns a list of the public keys of the encoded VAA accounts
*/
async function findEncodedVaaAccountsByWriteAuthority(connection, writeAuthority, wormholeProgramId) {
const result = await connection.getProgramAccounts(wormholeProgramId, {
filters: [
{
memcmp: {
offset: 0,
bytes: bytes_1.bs58.encode(Buffer.from((0, sha256_1.sha256)("account:EncodedVaa").slice(0, 8))),
},
},
{
memcmp: {
offset: 8 + 1,
bytes: bytes_1.bs58.encode(writeAuthority.toBuffer()),
},
},
],
});
return result.map((account) => new web3_js_1.PublicKey(account.pubkey));
}