From 31ab162168122007d852b083099fda11fcdd21e9 Mon Sep 17 00:00:00 2001 From: guibescos <59208140+guibescos@users.noreply.github.com> Date: Thu, 12 Jan 2023 11:53:20 -0600 Subject: [PATCH] [xc admin] Pyth parser (#477) * Pyth parser * Bump pyth-client-js --- xc-admin/package-lock.json | 16 +- .../packages/xc-admin-common/package.json | 2 +- .../__tests__/PythMultisigInstruction.test.ts | 167 ++++++++++++++++++ .../WormholeMultisigInstruction.test.ts | 7 +- .../PythMultisigInstruction.ts | 44 +++++ .../src/multisig_transaction/index.ts | 8 +- .../packages/xc-admin-common/src/wormhole.ts | 1 + 7 files changed, 226 insertions(+), 19 deletions(-) create mode 100644 xc-admin/packages/xc-admin-common/src/__tests__/PythMultisigInstruction.test.ts create mode 100644 xc-admin/packages/xc-admin-common/src/multisig_transaction/PythMultisigInstruction.ts diff --git a/xc-admin/package-lock.json b/xc-admin/package-lock.json index d13e1158..ca1e3144 100644 --- a/xc-admin/package-lock.json +++ b/xc-admin/package-lock.json @@ -3748,9 +3748,9 @@ "license": "BSD-3-Clause" }, "node_modules/@pythnetwork/client": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@pythnetwork/client/-/client-2.9.0.tgz", - "integrity": "sha512-2CyDmTwPWW+JCQgRKUpwMR/31oiLWH6I3GA0h2ZXIcbt/hWxcr5TXyKlWuyi+l+jh73WWh88gq8NXLoIgRcvkA==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@pythnetwork/client/-/client-2.10.0.tgz", + "integrity": "sha512-dmj8dAj8K7rhSpaDUIjLGKYs+64kM1LR/V1ht/IShg6Zu0GNJY522+0K0EBcmbjzs3GaHua873DlcvQJlU5iHw==", "dependencies": { "buffer": "^6.0.1" }, @@ -12759,7 +12759,7 @@ "license": "ISC", "dependencies": { "@certusone/wormhole-sdk": "^0.9.8", - "@pythnetwork/client": "^2.9.0", + "@pythnetwork/client": "^2.10.0", "@solana/buffer-layout": "^4.0.1", "@solana/web3.js": "^1.73.0", "@sqds/mesh": "^1.0.6", @@ -15331,9 +15331,9 @@ "version": "1.1.0" }, "@pythnetwork/client": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@pythnetwork/client/-/client-2.9.0.tgz", - "integrity": "sha512-2CyDmTwPWW+JCQgRKUpwMR/31oiLWH6I3GA0h2ZXIcbt/hWxcr5TXyKlWuyi+l+jh73WWh88gq8NXLoIgRcvkA==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@pythnetwork/client/-/client-2.10.0.tgz", + "integrity": "sha512-dmj8dAj8K7rhSpaDUIjLGKYs+64kM1LR/V1ht/IShg6Zu0GNJY522+0K0EBcmbjzs3GaHua873DlcvQJlU5iHw==", "requires": { "buffer": "^6.0.1" }, @@ -21192,7 +21192,7 @@ "version": "file:packages/xc-admin-common", "requires": { "@certusone/wormhole-sdk": "^0.9.8", - "@pythnetwork/client": "*", + "@pythnetwork/client": "^2.10.0", "@solana/buffer-layout": "^4.0.1", "@solana/web3.js": "^1.73.0", "@sqds/mesh": "^1.0.6", diff --git a/xc-admin/packages/xc-admin-common/package.json b/xc-admin/packages/xc-admin-common/package.json index 2ad26eb7..d500eaf3 100644 --- a/xc-admin/packages/xc-admin-common/package.json +++ b/xc-admin/packages/xc-admin-common/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@certusone/wormhole-sdk": "^0.9.8", - "@pythnetwork/client": "^2.9.0", + "@pythnetwork/client": "^2.10.0", "@solana/buffer-layout": "^4.0.1", "@solana/web3.js": "^1.73.0", "@sqds/mesh": "^1.0.6", diff --git a/xc-admin/packages/xc-admin-common/src/__tests__/PythMultisigInstruction.test.ts b/xc-admin/packages/xc-admin-common/src/__tests__/PythMultisigInstruction.test.ts new file mode 100644 index 00000000..31dc5569 --- /dev/null +++ b/xc-admin/packages/xc-admin-common/src/__tests__/PythMultisigInstruction.test.ts @@ -0,0 +1,167 @@ +import { AnchorProvider, Wallet } from "@project-serum/anchor"; +import { pythOracleProgram } from "@pythnetwork/client"; +import { + getPythClusterApiUrl, + getPythProgramKeyForCluster, + PythCluster, +} from "@pythnetwork/client/lib/cluster"; +import { Connection, Keypair, PublicKey } from "@solana/web3.js"; +import { MultisigInstructionProgram, MultisigParser } from ".."; +import { PythMultisigInstruction } from "../multisig_transaction/PythMultisigInstruction"; + +test("Pyth multisig instruction parse: create price account", (done) => { + jest.setTimeout(60000); + + const cluster: PythCluster = "devnet"; + const pythProgram = pythOracleProgram( + getPythProgramKeyForCluster(cluster), + new AnchorProvider( + new Connection(getPythClusterApiUrl(cluster)), + new Wallet(new Keypair()), + AnchorProvider.defaultOptions() + ) + ); + const parser = MultisigParser.fromCluster(cluster); + + pythProgram.methods + .addPrice(-8, 1) + .accounts({ + fundingAccount: PublicKey.unique(), + productAccount: PublicKey.unique(), + priceAccount: PublicKey.unique(), + }) + .instruction() + .then((instruction) => { + const parsedInstruction = parser.parseInstruction(instruction); + + if (parsedInstruction instanceof PythMultisigInstruction) { + expect(parsedInstruction.program).toBe( + MultisigInstructionProgram.PythOracle + ); + expect(parsedInstruction.name).toBe("addPrice"); + expect( + parsedInstruction.accounts.named["fundingAccount"].pubkey.equals( + instruction.keys[0].pubkey + ) + ).toBeTruthy(); + expect( + parsedInstruction.accounts.named["fundingAccount"].isSigner + ).toBe(instruction.keys[0].isSigner); + expect( + parsedInstruction.accounts.named["fundingAccount"].isWritable + ).toBe(instruction.keys[0].isWritable); + console.log(parsedInstruction.accounts.named["productAccount"]); + expect( + parsedInstruction.accounts.named["productAccount"].pubkey.equals( + instruction.keys[1].pubkey + ) + ).toBeTruthy(); + expect( + parsedInstruction.accounts.named["productAccount"].isSigner + ).toBe(instruction.keys[1].isSigner); + expect( + parsedInstruction.accounts.named["productAccount"].isWritable + ).toBe(instruction.keys[1].isWritable); + expect( + parsedInstruction.accounts.named["priceAccount"].pubkey.equals( + instruction.keys[2].pubkey + ) + ).toBeTruthy(); + expect(parsedInstruction.accounts.named["priceAccount"].isSigner).toBe( + instruction.keys[2].isSigner + ); + expect( + parsedInstruction.accounts.named["priceAccount"].isWritable + ).toBe(instruction.keys[2].isWritable); + expect( + parsedInstruction.accounts.named["permissionsAccount"].pubkey.equals( + instruction.keys[3].pubkey + ) + ).toBeTruthy(); + expect( + parsedInstruction.accounts.named["permissionsAccount"].isSigner + ).toBe(instruction.keys[3].isSigner); + expect( + parsedInstruction.accounts.named["permissionsAccount"].isWritable + ).toBe(instruction.keys[3].isWritable); + expect(parsedInstruction.accounts.remaining.length).toBe(0); + + expect(parsedInstruction.args.expo).toBe(-8); + expect(parsedInstruction.args.pType).toBe(1); + done(); + } else { + done("Not instance of PythMultisigInstruction"); + } + }); +}); + +test("Pyth multisig instruction parse: set minimum publishers", (done) => { + jest.setTimeout(60000); + + const cluster: PythCluster = "devnet"; + const pythProgram = pythOracleProgram( + getPythProgramKeyForCluster(cluster), + new AnchorProvider( + new Connection(getPythClusterApiUrl(cluster)), + new Wallet(new Keypair()), + AnchorProvider.defaultOptions() + ) + ); + const parser = MultisigParser.fromCluster(cluster); + + pythProgram.methods + .setMinPub(25, [0, 0, 0]) + .accounts({ + fundingAccount: PublicKey.unique(), + priceAccount: PublicKey.unique(), + }) + .instruction() + .then((instruction) => { + const parsedInstruction = parser.parseInstruction(instruction); + + if (parsedInstruction instanceof PythMultisigInstruction) { + expect(parsedInstruction.program).toBe( + MultisigInstructionProgram.PythOracle + ); + expect(parsedInstruction.name).toBe("setMinPub"); + expect( + parsedInstruction.accounts.named["fundingAccount"].pubkey.equals( + instruction.keys[0].pubkey + ) + ).toBeTruthy(); + expect( + parsedInstruction.accounts.named["fundingAccount"].isSigner + ).toBe(instruction.keys[0].isSigner); + expect( + parsedInstruction.accounts.named["fundingAccount"].isWritable + ).toBe(instruction.keys[0].isWritable); + expect( + parsedInstruction.accounts.named["priceAccount"].pubkey.equals( + instruction.keys[1].pubkey + ) + ).toBeTruthy(); + expect(parsedInstruction.accounts.named["priceAccount"].isSigner).toBe( + instruction.keys[1].isSigner + ); + expect( + parsedInstruction.accounts.named["priceAccount"].isWritable + ).toBe(instruction.keys[1].isWritable); + expect( + parsedInstruction.accounts.named["permissionsAccount"].pubkey.equals( + instruction.keys[2].pubkey + ) + ).toBeTruthy(); + expect( + parsedInstruction.accounts.named["permissionsAccount"].isSigner + ).toBe(instruction.keys[2].isSigner); + expect( + parsedInstruction.accounts.named["permissionsAccount"].isWritable + ).toBe(instruction.keys[2].isWritable); + expect(parsedInstruction.accounts.remaining.length).toBe(0); + expect(parsedInstruction.args.minPub).toBe(25); + done(); + } else { + done("Not instance of PythMultisigInstruction"); + } + }); +}); diff --git a/xc-admin/packages/xc-admin-common/src/__tests__/WormholeMultisigInstruction.test.ts b/xc-admin/packages/xc-admin-common/src/__tests__/WormholeMultisigInstruction.test.ts index e0860e6d..eddb07c8 100644 --- a/xc-admin/packages/xc-admin-common/src/__tests__/WormholeMultisigInstruction.test.ts +++ b/xc-admin/packages/xc-admin-common/src/__tests__/WormholeMultisigInstruction.test.ts @@ -46,9 +46,6 @@ test("Wormhole multisig instruction parse: send message without governance paylo .instruction() .then((instruction) => { const parsedInstruction = parser.parseInstruction(instruction); - expect( - parsedInstruction instanceof WormholeMultisigInstruction - ).toBeTruthy(); if (parsedInstruction instanceof WormholeMultisigInstruction) { expect(parsedInstruction.program).toBe( MultisigInstructionProgram.WormholeBridge @@ -161,7 +158,7 @@ test("Wormhole multisig instruction parse: send message without governance paylo expect(parsedInstruction.args.targetChain).toBeUndefined(); done(); } else { - done("Not instance of WormholeInstruction"); + done("Not instance of WormholeMultisigInstruction"); } }); }); @@ -354,7 +351,7 @@ test("Wormhole multisig instruction parse: send message with governance payload" done("Not instance of ExecutePostedVaa"); } } else { - done("Not instance of WormholeInstruction"); + done("Not instance of WormholeMultisigInstruction"); } }); }); diff --git a/xc-admin/packages/xc-admin-common/src/multisig_transaction/PythMultisigInstruction.ts b/xc-admin/packages/xc-admin-common/src/multisig_transaction/PythMultisigInstruction.ts new file mode 100644 index 00000000..dc774b08 --- /dev/null +++ b/xc-admin/packages/xc-admin-common/src/multisig_transaction/PythMultisigInstruction.ts @@ -0,0 +1,44 @@ +import { MultisigInstruction, MultisigInstructionProgram } from "."; +import { AnchorAccounts, resolveAccountNames } from "./anchor"; +import { pythIdl, pythOracleCoder } from "@pythnetwork/client"; +import { TransactionInstruction } from "@solana/web3.js"; +import { Idl } from "@coral-xyz/anchor"; + +export class PythMultisigInstruction implements MultisigInstruction { + readonly program = MultisigInstructionProgram.PythOracle; + 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 + ): PythMultisigInstruction { + const pythInstructionCoder = pythOracleCoder().instruction; + + const deserializedData = pythInstructionCoder.decode(instruction.data); + + if (deserializedData) { + return new PythMultisigInstruction( + deserializedData.name, + deserializedData.data, + resolveAccountNames(pythIdl as Idl, deserializedData.name, instruction) + ); + } else { + return new PythMultisigInstruction( + "Unrecognized instruction", + {}, + { named: {}, remaining: instruction.keys } + ); + } + } +} diff --git a/xc-admin/packages/xc-admin-common/src/multisig_transaction/index.ts b/xc-admin/packages/xc-admin-common/src/multisig_transaction/index.ts index 1f3fe62a..11db79a3 100644 --- a/xc-admin/packages/xc-admin-common/src/multisig_transaction/index.ts +++ b/xc-admin/packages/xc-admin-common/src/multisig_transaction/index.ts @@ -4,6 +4,7 @@ import { } from "@pythnetwork/client/lib/cluster"; import { PublicKey, TransactionInstruction } from "@solana/web3.js"; import { WORMHOLE_ADDRESS } from "../wormhole"; +import { PythMultisigInstruction } from "./PythMultisigInstruction"; import { WormholeMultisigInstruction } from "./WormholeMultisigInstruction"; export enum MultisigInstructionProgram { @@ -30,11 +31,6 @@ export class UnrecognizedProgram implements MultisigInstruction { return new UnrecognizedProgram(instruction); } } - -export class PythMultisigInstruction implements MultisigInstruction { - readonly program = MultisigInstructionProgram.PythOracle; -} - export class MultisigParser { readonly pythOracleAddress: PublicKey; readonly wormholeBridgeAddress: PublicKey | undefined; @@ -61,6 +57,8 @@ export class MultisigParser { return WormholeMultisigInstruction.fromTransactionInstruction( instruction ); + } else if (instruction.programId.equals(this.pythOracleAddress)) { + return PythMultisigInstruction.fromTransactionInstruction(instruction); } else { return UnrecognizedProgram.fromTransactionInstruction(instruction); } diff --git a/xc-admin/packages/xc-admin-common/src/wormhole.ts b/xc-admin/packages/xc-admin-common/src/wormhole.ts index 6be1a469..79d953d2 100644 --- a/xc-admin/packages/xc-admin-common/src/wormhole.ts +++ b/xc-admin/packages/xc-admin-common/src/wormhole.ts @@ -6,5 +6,6 @@ export const WORMHOLE_ADDRESS: Record = { pythtest: new PublicKey("EUrRARh92Cdc54xrDn6qzaqjA77NRrCcfbr8kPwoTL4z"), devnet: new PublicKey("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5"), pythnet: new PublicKey("H3fxXJ86ADW2PNuDDmZJg6mzTtPxkYCpNuQUTgmJ7AjU"), + localnet: new PublicKey("gMYYig2utAxVoXnM9UhtTWrt8e7x2SVBZqsWZJeT5Gw"), testnet: undefined, };