-
Notifications
You must be signed in to change notification settings - Fork 213
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(xc-admin): add price store instructions and executor support
- Loading branch information
Showing
4 changed files
with
323 additions
and
2 deletions.
There are no files selected for viewing
40 changes: 40 additions & 0 deletions
40
...ance/xc_admin/packages/xc_admin_common/src/__tests__/PriceStoreProgramInstruction.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
236 changes: 236 additions & 0 deletions
236
governance/xc_admin/packages/xc_admin_common/src/price_store.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |