Skip to content

Commit

Permalink
feat(xc-admin): add price store instructions and executor support
Browse files Browse the repository at this point in the history
  • Loading branch information
Riateche committed Sep 12, 2024
1 parent ad20151 commit a58f9e0
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 2 deletions.
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") {
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) {
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;

0 comments on commit a58f9e0

Please sign in to comment.