Skip to content

Commit 277599b

Browse files
authored
[xc-admin] Add pythnet relayer (#511)
* Add pythnet relayer * Bugfix * Reset clusters * Revert cluster change * Add comment
1 parent 5900550 commit 277599b

File tree

5 files changed

+235
-0
lines changed

5 files changed

+235
-0
lines changed

governance/xc-admin/package-lock.json

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "crank-pythnet-relayer",
3+
"version": "0.0.0",
4+
"description": "A crank to relay pyth governance actions to pythnet",
5+
"author": "",
6+
"homepage": "https://github.com/pyth-network/pyth-crosschain",
7+
"license": "ISC",
8+
"main": "src/index.ts",
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/pyth-network/pyth-crosschain.git"
12+
},
13+
"bugs": {
14+
"url": "https://github.com/pyth-network/pyth-crosschain/issues"
15+
},
16+
"scripts": {
17+
"build": "tsc",
18+
"format": "prettier --write \"src/**/*.ts\""
19+
},
20+
"dependencies": {
21+
"@certusone/wormhole-sdk": "^0.9.9",
22+
"@coral-xyz/anchor": "^0.26.0",
23+
"@pythnetwork/client": "^2.9.0",
24+
"@solana/web3.js": "^1.73.0",
25+
"@sqds/mesh": "^1.0.6",
26+
"ts-node": "^10.9.1",
27+
"xc-admin-common": "*"
28+
}
29+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { ParsedVaa, parseVaa, postVaaSolana } from "@certusone/wormhole-sdk";
2+
import { signTransactionFactory } from "@certusone/wormhole-sdk/lib/cjs/solana";
3+
import {
4+
derivePostedVaaKey,
5+
getPostedVaa,
6+
} from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
7+
import { AnchorProvider, BN, Program } from "@coral-xyz/anchor";
8+
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
9+
import {
10+
getPythClusterApiUrl,
11+
PythCluster,
12+
} from "@pythnetwork/client/lib/cluster";
13+
import {
14+
AccountMeta,
15+
Commitment,
16+
Connection,
17+
Keypair,
18+
PublicKey,
19+
} from "@solana/web3.js";
20+
import * as fs from "fs";
21+
import {
22+
decodeGovernancePayload,
23+
ExecutePostedVaa,
24+
WORMHOLE_ADDRESS,
25+
WORMHOLE_API_ENDPOINT,
26+
} from "xc-admin-common";
27+
28+
export function envOrErr(env: string): string {
29+
const val = process.env[env];
30+
if (!val) {
31+
throw new Error(`environment variable "${env}" must be set`);
32+
}
33+
return String(process.env[env]);
34+
}
35+
36+
const REMOTE_EXECUTOR_ADDRESS = new PublicKey(
37+
"exe6S3AxPVNmy46L4Nj6HrnnAVQUhwyYzMSNcnRn3qq"
38+
);
39+
40+
const CLAIM_RECORD_SEED = "CLAIM_RECORD";
41+
const EXECUTOR_KEY_SEED = "EXECUTOR_KEY";
42+
const CLUSTER: PythCluster = envOrErr("CLUSTER") as PythCluster;
43+
const COMMITMENT: Commitment =
44+
(process.env.COMMITMENT as Commitment) ?? "confirmed";
45+
const OFFSET: number = Number(process.env.OFFSET) ?? -1;
46+
const EMITTER: PublicKey = new PublicKey(envOrErr("EMITTER"));
47+
const KEYPAIR: Keypair = Keypair.fromSecretKey(
48+
Uint8Array.from(JSON.parse(fs.readFileSync(envOrErr("WALLET"), "ascii")))
49+
);
50+
51+
async function run() {
52+
const provider = new AnchorProvider(
53+
new Connection(getPythClusterApiUrl(CLUSTER), COMMITMENT),
54+
new NodeWallet(KEYPAIR),
55+
{
56+
commitment: COMMITMENT,
57+
preflightCommitment: COMMITMENT,
58+
}
59+
);
60+
61+
const remoteExecutor = await Program.at(REMOTE_EXECUTOR_ADDRESS, provider);
62+
63+
const claimRecordAddress: PublicKey = PublicKey.findProgramAddressSync(
64+
[Buffer.from(CLAIM_RECORD_SEED), EMITTER.toBuffer()],
65+
remoteExecutor.programId
66+
)[0];
67+
const executorKey: PublicKey = PublicKey.findProgramAddressSync(
68+
[Buffer.from(EXECUTOR_KEY_SEED), EMITTER.toBuffer()],
69+
remoteExecutor.programId
70+
)[0];
71+
const claimRecord = await remoteExecutor.account.claimRecord.fetchNullable(
72+
claimRecordAddress
73+
);
74+
let lastSequenceNumber: number = claimRecord
75+
? (claimRecord.sequence as BN).toNumber()
76+
: -1;
77+
lastSequenceNumber = Math.max(lastSequenceNumber, OFFSET);
78+
const wormholeApi = WORMHOLE_API_ENDPOINT[CLUSTER];
79+
80+
while (true) {
81+
lastSequenceNumber += 1;
82+
console.log(`Trying sequence number : ${lastSequenceNumber}`);
83+
84+
const response = await (
85+
await fetch(
86+
`${wormholeApi}/v1/signed_vaa/1/${EMITTER.toBuffer().toString(
87+
"hex"
88+
)}/${lastSequenceNumber}`
89+
)
90+
).json();
91+
92+
if (response.vaaBytes) {
93+
const vaa = parseVaa(Buffer.from(response.vaaBytes, "base64"));
94+
const governancePayload = decodeGovernancePayload(vaa.payload);
95+
96+
if (
97+
governancePayload instanceof ExecutePostedVaa &&
98+
governancePayload.targetChainId == "pythnet"
99+
) {
100+
console.log(`Found VAA ${lastSequenceNumber}, relaying ...`);
101+
await postVaaSolana(
102+
provider.connection,
103+
signTransactionFactory(KEYPAIR),
104+
WORMHOLE_ADDRESS[CLUSTER]!,
105+
provider.wallet.publicKey,
106+
Buffer.from(response.vaaBytes, "base64"),
107+
{ commitment: COMMITMENT }
108+
);
109+
110+
let extraAccountMetas: AccountMeta[] = [
111+
{ pubkey: executorKey, isSigner: false, isWritable: true },
112+
];
113+
for (const ix of governancePayload.instructions) {
114+
extraAccountMetas.push({
115+
pubkey: ix.programId,
116+
isSigner: false,
117+
isWritable: false,
118+
});
119+
extraAccountMetas.push(
120+
...ix.keys.filter((acc) => {
121+
return !acc.pubkey.equals(executorKey);
122+
})
123+
);
124+
}
125+
126+
await remoteExecutor.methods
127+
.executePostedVaa()
128+
.accounts({
129+
claimRecord: claimRecordAddress,
130+
postedVaa: derivePostedVaaKey(WORMHOLE_ADDRESS[CLUSTER]!, vaa.hash),
131+
})
132+
.remainingAccounts(extraAccountMetas)
133+
.rpc();
134+
}
135+
} else if (response.code == 5) {
136+
console.log(`Wormhole API failure`);
137+
console.log(
138+
`${wormholeApi}/v1/signed_vaa/1/${EMITTER.toBuffer().toString(
139+
"hex"
140+
)}/${lastSequenceNumber}`
141+
);
142+
break;
143+
} else {
144+
throw new Error("Could not connect to wormhole api");
145+
}
146+
}
147+
}
148+
149+
(async () => {
150+
await run();
151+
})();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"declaration": true,
4+
"target": "es2016",
5+
"module": "commonjs",
6+
"outDir": "lib",
7+
"esModuleInterop": true,
8+
"forceConsistentCasingInFileNames": true,
9+
"strict": true,
10+
"skipLibCheck": true,
11+
"resolveJsonModule": true,
12+
"noErrorTruncation": true
13+
},
14+
"include": ["src/**/*.ts"],
15+
"exclude": ["src/__tests__/"]
16+
}

governance/xc-admin/packages/xc-admin-common/src/wormhole.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,13 @@ export const WORMHOLE_ADDRESS: Record<PythCluster, PublicKey | undefined> = {
99
localnet: new PublicKey("Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"),
1010
testnet: undefined,
1111
};
12+
13+
// Source : https://book.wormhole.com/reference/rpcnodes.html
14+
export const WORMHOLE_API_ENDPOINT: Record<PythCluster, string | undefined> = {
15+
"mainnet-beta": "https://wormhole-v2-mainnet-api.certus.one",
16+
pythtest: "https://wormhole-v2-testnet-api.certus.one",
17+
devnet: "https://wormhole-v2-testnet-api.certus.one",
18+
pythnet: "https://wormhole-v2-mainnet-api.certus.one",
19+
localnet: undefined,
20+
testnet: undefined,
21+
};

0 commit comments

Comments
 (0)