Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
@@ -0,0 +1,40 @@
import { PublicKey } from "@solana/web3.js";
import {
createPriceStoreInstruction,
parsePriceStoreInstruction,
PriceStoreInstruction,
} from "../price_store";

test("Price store instruction parse: roundtrip", (done) => {
const items: PriceStoreInstruction[] = [
{
type: "Initialize",
data: {
payerKey: new PublicKey("Fe9vtgwRhbMSUsAjwUzupzRoJKofyyk1Rz8ZUrPmGHMr"),
authorityKey: new PublicKey(
"D9rnZSLjdYboFGDGHk5Qre2yBS8HYbc6374Zm6AeC1PB"
),
},
},
{
type: "InitializePublisher",
data: {
authorityKey: new PublicKey(
"D9rnZSLjdYboFGDGHk5Qre2yBS8HYbc6374Zm6AeC1PB"
),
publisherKey: new PublicKey(
"EXAyN9UVu1x163PQkVzyNm4YunNkMGu5Ry7ntoyyQGTe"
),
bufferKey: new PublicKey(
"7q6SS575jGDjE8bWsx4PiLVqS7cHJhjJBhysvRoP53WJ"
),
},
},
];
for (const data of items) {
const instruction = createPriceStoreInstruction(data);
const parsed = parsePriceStoreInstruction(instruction);
expect(parsed).toStrictEqual(data);
}
done();
});
27 changes: 27 additions & 0 deletions governance/xc_admin/packages/xc_admin_common/src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ import {
TransactionBuilder,
PriorityFeeConfig,
} from "@pythnetwork/solana-utils";
import {
findDetermisticPublisherBufferAddress,
PRICE_STORE_BUFFER_SPACE,
PRICE_STORE_PROGRAM_ID,
PriceStoreMultisigInstruction,
} from "./price_store";

/**
* Returns the instruction to pay the fee for a wormhole postMessage instruction
Expand Down Expand Up @@ -134,6 +140,27 @@ export async function executeProposal(
} else {
throw Error("Product account not found");
}
} else if (
parsedInstruction instanceof PriceStoreMultisigInstruction &&
parsedInstruction.name == "InitializePublisher"
) {
const [bufferKey, bufferSeed] =
await findDetermisticPublisherBufferAddress(
parsedInstruction.args.publisherKey
);
transaction.add(
SystemProgram.createAccountWithSeed({
fromPubkey: squad.wallet.publicKey,
basePubkey: squad.wallet.publicKey,
newAccountPubkey: bufferKey,
seed: bufferSeed,
space: PRICE_STORE_BUFFER_SPACE,
lamports: await squad.connection.getMinimumBalanceForRentExemption(
PRICE_STORE_BUFFER_SPACE
),
programId: PRICE_STORE_PROGRAM_ID,
})
);
}

TransactionBuilder.addPriorityFee(transaction, priorityFeeConfig);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import { BPF_UPGRADABLE_LOADER } from "../bpf_upgradable_loader";
import { AnchorAccounts } from "./anchor";
import { SolanaStakingMultisigInstruction } from "./SolanaStakingMultisigInstruction";
import { DEFAULT_RECEIVER_PROGRAM_ID } from "@pythnetwork/pyth-solana-receiver";
import {
PRICE_STORE_PROGRAM_ID,
PriceStoreMultisigInstruction,
} from "../price_store";

export const UNRECOGNIZED_INSTRUCTION = "unrecognizedInstruction";
export enum MultisigInstructionProgram {
Expand All @@ -36,6 +40,7 @@ export enum MultisigInstructionProgram {
SolanaStakingProgram,
SolanaReceiver,
UnrecognizedProgram,
PythPriceStore,
}

export function getProgramName(program: MultisigInstructionProgram) {
Expand All @@ -58,6 +63,8 @@ export function getProgramName(program: MultisigInstructionProgram) {
return "Pyth Staking Program";
case MultisigInstructionProgram.SolanaReceiver:
return "Pyth Solana Receiver";
case MultisigInstructionProgram.PythPriceStore:
return "Pyth Price Store";
case MultisigInstructionProgram.UnrecognizedProgram:
return "Unknown";
}
Expand Down Expand Up @@ -99,18 +106,22 @@ export class UnrecognizedProgram implements MultisigInstruction {
export class MultisigParser {
readonly pythOracleAddress: PublicKey;
readonly wormholeBridgeAddress: PublicKey | undefined;
readonly pythPriceStoreAddress: PublicKey | undefined;

constructor(
pythOracleAddress: PublicKey,
wormholeBridgeAddress: PublicKey | undefined
wormholeBridgeAddress: PublicKey | undefined,
pythPriceStoreAddress: PublicKey | undefined
) {
this.pythOracleAddress = pythOracleAddress;
this.wormholeBridgeAddress = wormholeBridgeAddress;
this.pythPriceStoreAddress = pythPriceStoreAddress;
}
static fromCluster(cluster: PythCluster): MultisigParser {
return new MultisigParser(
getPythProgramKeyForCluster(cluster),
WORMHOLE_ADDRESS[cluster]
WORMHOLE_ADDRESS[cluster],
PRICE_STORE_PROGRAM_ID
);
}

Expand All @@ -124,6 +135,13 @@ export class MultisigParser {
);
} else if (instruction.programId.equals(this.pythOracleAddress)) {
return PythMultisigInstruction.fromTransactionInstruction(instruction);
} else if (
this.pythPriceStoreAddress &&
instruction.programId.equals(this.pythPriceStoreAddress)
) {
return PriceStoreMultisigInstruction.fromTransactionInstruction(
instruction
);
} else if (
instruction.programId.equals(MESSAGE_BUFFER_PROGRAM_ID) ||
instruction.programId.equals(MESH_PROGRAM_ID) ||
Expand Down
236 changes: 236 additions & 0 deletions governance/xc_admin/packages/xc_admin_common/src/price_store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import {
MAX_SEED_LENGTH,
PublicKey,
SystemProgram,
TransactionInstruction,
} from "@solana/web3.js";
import {
MultisigInstruction,
MultisigInstructionProgram,
UNRECOGNIZED_INSTRUCTION,
} from "./multisig_transaction";
import { AnchorAccounts } from "./multisig_transaction/anchor";
import { PRICE_FEED_OPS_KEY } from "./multisig";

export const PRICE_STORE_PROGRAM_ID: PublicKey = new PublicKey(
"3m6sv6HGqEbuyLV84mD7rJn4MAC9LhUa1y1AUNVqcPfr"
);

export type PriceStoreInitializeInstruction = {
payerKey: PublicKey;
authorityKey: PublicKey;
};

export type PriceStoreInitializePublisherInstruction = {
authorityKey: PublicKey;
publisherKey: PublicKey;
bufferKey: PublicKey;
};

// No need to support SubmitPrices instruction.
export type PriceStoreInstruction =
| {
type: "Initialize";
data: PriceStoreInitializeInstruction;
}
| {
type: "InitializePublisher";
data: PriceStoreInitializePublisherInstruction;
};

enum InstructionId {
Initialize = 0,
SubmitPrices = 1,
InitializePublisher = 2,
}

export function createPriceStoreInstruction(
data: PriceStoreInstruction
): TransactionInstruction {
if (data.type == "Initialize") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i recommend using switch here, but if not triple equals (===)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to use switch.

const [configKey, configBump] = PublicKey.findProgramAddressSync(
[Buffer.from("CONFIG")],
PRICE_STORE_PROGRAM_ID
);
const instructionData = Buffer.concat([
Buffer.from([InstructionId.Initialize, configBump]),
data.data.authorityKey.toBuffer(),
]);

return new TransactionInstruction({
keys: [
{
pubkey: data.data.payerKey,
isSigner: true,
isWritable: true,
},
{
pubkey: configKey,
isSigner: false,
isWritable: true,
},
{
pubkey: SystemProgram.programId,
isSigner: false,
isWritable: false,
},
],
programId: PRICE_STORE_PROGRAM_ID,
data: instructionData,
});
} else if (data.type == "InitializePublisher") {
const [configKey, configBump] = PublicKey.findProgramAddressSync(
[Buffer.from("CONFIG")],
PRICE_STORE_PROGRAM_ID
);
const [publisherConfigKey, publisherConfigBump] =
PublicKey.findProgramAddressSync(
[Buffer.from("PUBLISHER_CONFIG"), data.data.publisherKey.toBuffer()],
PRICE_STORE_PROGRAM_ID
);
const instructionData = Buffer.concat([
Buffer.from([
InstructionId.InitializePublisher,
configBump,
publisherConfigBump,
]),
data.data.publisherKey.toBuffer(),
]);
return new TransactionInstruction({
keys: [
{
pubkey: data.data.authorityKey,
isSigner: true,
isWritable: true,
},
{
pubkey: configKey,
isSigner: false,
isWritable: false,
},
{
pubkey: publisherConfigKey,
isSigner: false,
isWritable: true,
},
{
pubkey: data.data.bufferKey,
isSigner: false,
isWritable: true,
},
{
pubkey: SystemProgram.programId,
isSigner: false,
isWritable: false,
},
],
programId: PRICE_STORE_PROGRAM_ID,
data: instructionData,
});
}
// No need to support SubmitPrices instruction.
throw new Error("invalid type");
}

export function parsePriceStoreInstruction(
instruction: TransactionInstruction
): PriceStoreInstruction {
if (instruction.programId != PRICE_STORE_PROGRAM_ID) {
throw new Error("program ID mismatch");
}
if (instruction.data.length < 1) {
throw new Error("instruction data is too short");
}
const instructionId = instruction.data.readInt8(0);
if (instructionId == InstructionId.Initialize) {
if (instruction.data.length < 34) {
throw new Error("instruction data is too short");
}
const authorityKey = new PublicKey(instruction.data.subarray(2, 34));
if (instruction.keys.length != 3) {
throw new Error("invalid number of accounts");
}
return {
type: "Initialize",
data: {
payerKey: instruction.keys[0].pubkey,
authorityKey,
},
};
} else if (instructionId == InstructionId.InitializePublisher) {
if (instruction.data.length < 35) {
throw new Error("instruction data is too short");
}
const publisherKey = new PublicKey(instruction.data.subarray(3, 35));
if (instruction.keys.length != 5) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

de we need to check remaining accounts to be valid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a check that accounts and instruction data match the expected values.

throw new Error("invalid number of accounts");
}
return {
type: "InitializePublisher",
data: {
authorityKey: instruction.keys[0].pubkey,
bufferKey: instruction.keys[3].pubkey,
publisherKey,
},
};
} else if (instructionId == InstructionId.SubmitPrices) {
throw new Error("SubmitPrices instruction is not supported");
} else {
throw new Error("unrecognized instruction id");
}
}

export class PriceStoreMultisigInstruction implements MultisigInstruction {
readonly program = MultisigInstructionProgram.PythPriceStore;
readonly name: string;
readonly args: { [key: string]: any };
readonly accounts: AnchorAccounts;

constructor(
name: string,
args: { [key: string]: any },
accounts: AnchorAccounts
) {
this.name = name;
this.args = args;
this.accounts = accounts;
}

static fromTransactionInstruction(
instruction: TransactionInstruction
): PriceStoreMultisigInstruction {
let result;
try {
result = parsePriceStoreInstruction(instruction);
} catch (e) {
return new PriceStoreMultisigInstruction(
UNRECOGNIZED_INSTRUCTION,
{ data: instruction.data, error: (e as Error).toString() },
{ named: {}, remaining: instruction.keys }
);
}

return new PriceStoreMultisigInstruction(result.type, result.data, {
named: {},
remaining: instruction.keys,
});
}
}

export async function findDetermisticPublisherBufferAddress(
publisher: PublicKey
): Promise<[PublicKey, string]> {
const seedPrefix = "Buffer";
const seed =
seedPrefix +
publisher.toBase58().substring(0, MAX_SEED_LENGTH - seedPrefix.length);
const address: PublicKey = await PublicKey.createWithSeed(
PRICE_FEED_OPS_KEY,
seed,
PRICE_STORE_PROGRAM_ID
);
return [address, seed];
}

// Recommended buffer size, enough to hold 5000 prices.
export const PRICE_STORE_BUFFER_SPACE = 100048;
Loading