Parcourir la source

[xc-admin] Add pythnet relayer (#511)

* Add pythnet relayer

* Bugfix

* Reset clusters

* Revert cluster change

* Add comment
guibescos il y a 2 ans
Parent
commit
277599b458

+ 29 - 0
governance/xc-admin/package-lock.json

@@ -12038,6 +12038,10 @@
       "resolved": "packages/crank-executor",
       "link": true
     },
+    "node_modules/crank-pythnet-relayer": {
+      "resolved": "packages/crank-pythnet-relayer",
+      "link": true
+    },
     "node_modules/crc": {
       "version": "3.8.0",
       "license": "MIT",
@@ -25229,6 +25233,19 @@
         "xc-admin-common": "*"
       }
     },
+    "packages/crank-pythnet-relayer": {
+      "version": "0.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "@certusone/wormhole-sdk": "^0.9.9",
+        "@coral-xyz/anchor": "^0.26.0",
+        "@pythnetwork/client": "^2.9.0",
+        "@solana/web3.js": "^1.73.0",
+        "@sqds/mesh": "^1.0.6",
+        "ts-node": "^10.9.1",
+        "xc-admin-common": "*"
+      }
+    },
     "packages/xc-admin-cli": {
       "version": "0.0.0",
       "license": "ISC",
@@ -33261,6 +33278,18 @@
         "xc-admin-common": "*"
       }
     },
+    "crank-pythnet-relayer": {
+      "version": "file:packages/crank-pythnet-relayer",
+      "requires": {
+        "@certusone/wormhole-sdk": "^0.9.9",
+        "@coral-xyz/anchor": "^0.26.0",
+        "@pythnetwork/client": "^2.9.0",
+        "@solana/web3.js": "^1.73.0",
+        "@sqds/mesh": "^1.0.6",
+        "ts-node": "^10.9.1",
+        "xc-admin-common": "*"
+      }
+    },
     "crc": {
       "version": "3.8.0",
       "requires": {

+ 29 - 0
governance/xc-admin/packages/crank-pythnet-relayer/package.json

@@ -0,0 +1,29 @@
+{
+  "name": "crank-pythnet-relayer",
+  "version": "0.0.0",
+  "description": "A crank to relay pyth governance actions to pythnet",
+  "author": "",
+  "homepage": "https://github.com/pyth-network/pyth-crosschain",
+  "license": "ISC",
+  "main": "src/index.ts",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/pyth-network/pyth-crosschain.git"
+  },
+  "bugs": {
+    "url": "https://github.com/pyth-network/pyth-crosschain/issues"
+  },
+  "scripts": {
+    "build": "tsc",
+    "format": "prettier --write \"src/**/*.ts\""
+  },
+  "dependencies": {
+    "@certusone/wormhole-sdk": "^0.9.9",
+    "@coral-xyz/anchor": "^0.26.0",
+    "@pythnetwork/client": "^2.9.0",
+    "@solana/web3.js": "^1.73.0",
+    "@sqds/mesh": "^1.0.6",
+    "ts-node": "^10.9.1",
+    "xc-admin-common": "*"
+  }
+}

+ 151 - 0
governance/xc-admin/packages/crank-pythnet-relayer/src/index.ts

@@ -0,0 +1,151 @@
+import { ParsedVaa, parseVaa, postVaaSolana } from "@certusone/wormhole-sdk";
+import { signTransactionFactory } from "@certusone/wormhole-sdk/lib/cjs/solana";
+import {
+  derivePostedVaaKey,
+  getPostedVaa,
+} from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
+import { AnchorProvider, BN, Program } from "@coral-xyz/anchor";
+import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
+import {
+  getPythClusterApiUrl,
+  PythCluster,
+} from "@pythnetwork/client/lib/cluster";
+import {
+  AccountMeta,
+  Commitment,
+  Connection,
+  Keypair,
+  PublicKey,
+} from "@solana/web3.js";
+import * as fs from "fs";
+import {
+  decodeGovernancePayload,
+  ExecutePostedVaa,
+  WORMHOLE_ADDRESS,
+  WORMHOLE_API_ENDPOINT,
+} from "xc-admin-common";
+
+export function envOrErr(env: string): string {
+  const val = process.env[env];
+  if (!val) {
+    throw new Error(`environment variable "${env}" must be set`);
+  }
+  return String(process.env[env]);
+}
+
+const REMOTE_EXECUTOR_ADDRESS = new PublicKey(
+  "exe6S3AxPVNmy46L4Nj6HrnnAVQUhwyYzMSNcnRn3qq"
+);
+
+const CLAIM_RECORD_SEED = "CLAIM_RECORD";
+const EXECUTOR_KEY_SEED = "EXECUTOR_KEY";
+const CLUSTER: PythCluster = envOrErr("CLUSTER") as PythCluster;
+const COMMITMENT: Commitment =
+  (process.env.COMMITMENT as Commitment) ?? "confirmed";
+const OFFSET: number = Number(process.env.OFFSET) ?? -1;
+const EMITTER: PublicKey = new PublicKey(envOrErr("EMITTER"));
+const KEYPAIR: Keypair = Keypair.fromSecretKey(
+  Uint8Array.from(JSON.parse(fs.readFileSync(envOrErr("WALLET"), "ascii")))
+);
+
+async function run() {
+  const provider = new AnchorProvider(
+    new Connection(getPythClusterApiUrl(CLUSTER), COMMITMENT),
+    new NodeWallet(KEYPAIR),
+    {
+      commitment: COMMITMENT,
+      preflightCommitment: COMMITMENT,
+    }
+  );
+
+  const remoteExecutor = await Program.at(REMOTE_EXECUTOR_ADDRESS, provider);
+
+  const claimRecordAddress: PublicKey = PublicKey.findProgramAddressSync(
+    [Buffer.from(CLAIM_RECORD_SEED), EMITTER.toBuffer()],
+    remoteExecutor.programId
+  )[0];
+  const executorKey: PublicKey = PublicKey.findProgramAddressSync(
+    [Buffer.from(EXECUTOR_KEY_SEED), EMITTER.toBuffer()],
+    remoteExecutor.programId
+  )[0];
+  const claimRecord = await remoteExecutor.account.claimRecord.fetchNullable(
+    claimRecordAddress
+  );
+  let lastSequenceNumber: number = claimRecord
+    ? (claimRecord.sequence as BN).toNumber()
+    : -1;
+  lastSequenceNumber = Math.max(lastSequenceNumber, OFFSET);
+  const wormholeApi = WORMHOLE_API_ENDPOINT[CLUSTER];
+
+  while (true) {
+    lastSequenceNumber += 1;
+    console.log(`Trying sequence number : ${lastSequenceNumber}`);
+
+    const response = await (
+      await fetch(
+        `${wormholeApi}/v1/signed_vaa/1/${EMITTER.toBuffer().toString(
+          "hex"
+        )}/${lastSequenceNumber}`
+      )
+    ).json();
+
+    if (response.vaaBytes) {
+      const vaa = parseVaa(Buffer.from(response.vaaBytes, "base64"));
+      const governancePayload = decodeGovernancePayload(vaa.payload);
+
+      if (
+        governancePayload instanceof ExecutePostedVaa &&
+        governancePayload.targetChainId == "pythnet"
+      ) {
+        console.log(`Found VAA ${lastSequenceNumber}, relaying ...`);
+        await postVaaSolana(
+          provider.connection,
+          signTransactionFactory(KEYPAIR),
+          WORMHOLE_ADDRESS[CLUSTER]!,
+          provider.wallet.publicKey,
+          Buffer.from(response.vaaBytes, "base64"),
+          { commitment: COMMITMENT }
+        );
+
+        let extraAccountMetas: AccountMeta[] = [
+          { pubkey: executorKey, isSigner: false, isWritable: true },
+        ];
+        for (const ix of governancePayload.instructions) {
+          extraAccountMetas.push({
+            pubkey: ix.programId,
+            isSigner: false,
+            isWritable: false,
+          });
+          extraAccountMetas.push(
+            ...ix.keys.filter((acc) => {
+              return !acc.pubkey.equals(executorKey);
+            })
+          );
+        }
+
+        await remoteExecutor.methods
+          .executePostedVaa()
+          .accounts({
+            claimRecord: claimRecordAddress,
+            postedVaa: derivePostedVaaKey(WORMHOLE_ADDRESS[CLUSTER]!, vaa.hash),
+          })
+          .remainingAccounts(extraAccountMetas)
+          .rpc();
+      }
+    } else if (response.code == 5) {
+      console.log(`Wormhole API failure`);
+      console.log(
+        `${wormholeApi}/v1/signed_vaa/1/${EMITTER.toBuffer().toString(
+          "hex"
+        )}/${lastSequenceNumber}`
+      );
+      break;
+    } else {
+      throw new Error("Could not connect to wormhole api");
+    }
+  }
+}
+
+(async () => {
+  await run();
+})();

+ 16 - 0
governance/xc-admin/packages/crank-pythnet-relayer/tsconfig.json

@@ -0,0 +1,16 @@
+{
+  "compilerOptions": {
+    "declaration": true,
+    "target": "es2016",
+    "module": "commonjs",
+    "outDir": "lib",
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true,
+    "strict": true,
+    "skipLibCheck": true,
+    "resolveJsonModule": true,
+    "noErrorTruncation": true
+  },
+  "include": ["src/**/*.ts"],
+  "exclude": ["src/__tests__/"]
+}

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

@@ -9,3 +9,13 @@ export const WORMHOLE_ADDRESS: Record<PythCluster, PublicKey | undefined> = {
   localnet: new PublicKey("Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"),
   testnet: undefined,
 };
+
+// Source : https://book.wormhole.com/reference/rpcnodes.html
+export const WORMHOLE_API_ENDPOINT: Record<PythCluster, string | undefined> = {
+  "mainnet-beta": "https://wormhole-v2-mainnet-api.certus.one",
+  pythtest: "https://wormhole-v2-testnet-api.certus.one",
+  devnet: "https://wormhole-v2-testnet-api.certus.one",
+  pythnet: "https://wormhole-v2-mainnet-api.certus.one",
+  localnet: undefined,
+  testnet: undefined,
+};