Procházet zdrojové kódy

[xc admin] Pyth parser (#477)

* Pyth parser

* Bump pyth-client-js
guibescos před 2 roky
rodič
revize
31ab162168

+ 8 - 8
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",

+ 1 - 1
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",

+ 167 - 0
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");
+      }
+    });
+});

+ 2 - 5
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");
       }
     });
 });

+ 44 - 0
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 }
+      );
+    }
+  }
+}

+ 3 - 5
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);
     }

+ 1 - 0
xc-admin/packages/xc-admin-common/src/wormhole.ts

@@ -6,5 +6,6 @@ export const WORMHOLE_ADDRESS: Record<PythCluster, PublicKey | undefined> = {
   pythtest: new PublicKey("EUrRARh92Cdc54xrDn6qzaqjA77NRrCcfbr8kPwoTL4z"),
   devnet: new PublicKey("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5"),
   pythnet: new PublicKey("H3fxXJ86ADW2PNuDDmZJg6mzTtPxkYCpNuQUTgmJ7AjU"),
+  localnet: new PublicKey("gMYYig2utAxVoXnM9UhtTWrt8e7x2SVBZqsWZJeT5Gw"),
   testnet: undefined,
 };