Skip to content

Commit 31ab162

Browse files
authored
[xc admin] Pyth parser (#477)
* Pyth parser * Bump pyth-client-js
1 parent 8ef49e6 commit 31ab162

File tree

7 files changed

+226
-19
lines changed

7 files changed

+226
-19
lines changed

xc-admin/package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

xc-admin/packages/xc-admin-common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
},
2121
"dependencies": {
2222
"@certusone/wormhole-sdk": "^0.9.8",
23-
"@pythnetwork/client": "^2.9.0",
23+
"@pythnetwork/client": "^2.10.0",
2424
"@solana/buffer-layout": "^4.0.1",
2525
"@solana/web3.js": "^1.73.0",
2626
"@sqds/mesh": "^1.0.6",
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { AnchorProvider, Wallet } from "@project-serum/anchor";
2+
import { pythOracleProgram } from "@pythnetwork/client";
3+
import {
4+
getPythClusterApiUrl,
5+
getPythProgramKeyForCluster,
6+
PythCluster,
7+
} from "@pythnetwork/client/lib/cluster";
8+
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
9+
import { MultisigInstructionProgram, MultisigParser } from "..";
10+
import { PythMultisigInstruction } from "../multisig_transaction/PythMultisigInstruction";
11+
12+
test("Pyth multisig instruction parse: create price account", (done) => {
13+
jest.setTimeout(60000);
14+
15+
const cluster: PythCluster = "devnet";
16+
const pythProgram = pythOracleProgram(
17+
getPythProgramKeyForCluster(cluster),
18+
new AnchorProvider(
19+
new Connection(getPythClusterApiUrl(cluster)),
20+
new Wallet(new Keypair()),
21+
AnchorProvider.defaultOptions()
22+
)
23+
);
24+
const parser = MultisigParser.fromCluster(cluster);
25+
26+
pythProgram.methods
27+
.addPrice(-8, 1)
28+
.accounts({
29+
fundingAccount: PublicKey.unique(),
30+
productAccount: PublicKey.unique(),
31+
priceAccount: PublicKey.unique(),
32+
})
33+
.instruction()
34+
.then((instruction) => {
35+
const parsedInstruction = parser.parseInstruction(instruction);
36+
37+
if (parsedInstruction instanceof PythMultisigInstruction) {
38+
expect(parsedInstruction.program).toBe(
39+
MultisigInstructionProgram.PythOracle
40+
);
41+
expect(parsedInstruction.name).toBe("addPrice");
42+
expect(
43+
parsedInstruction.accounts.named["fundingAccount"].pubkey.equals(
44+
instruction.keys[0].pubkey
45+
)
46+
).toBeTruthy();
47+
expect(
48+
parsedInstruction.accounts.named["fundingAccount"].isSigner
49+
).toBe(instruction.keys[0].isSigner);
50+
expect(
51+
parsedInstruction.accounts.named["fundingAccount"].isWritable
52+
).toBe(instruction.keys[0].isWritable);
53+
console.log(parsedInstruction.accounts.named["productAccount"]);
54+
expect(
55+
parsedInstruction.accounts.named["productAccount"].pubkey.equals(
56+
instruction.keys[1].pubkey
57+
)
58+
).toBeTruthy();
59+
expect(
60+
parsedInstruction.accounts.named["productAccount"].isSigner
61+
).toBe(instruction.keys[1].isSigner);
62+
expect(
63+
parsedInstruction.accounts.named["productAccount"].isWritable
64+
).toBe(instruction.keys[1].isWritable);
65+
expect(
66+
parsedInstruction.accounts.named["priceAccount"].pubkey.equals(
67+
instruction.keys[2].pubkey
68+
)
69+
).toBeTruthy();
70+
expect(parsedInstruction.accounts.named["priceAccount"].isSigner).toBe(
71+
instruction.keys[2].isSigner
72+
);
73+
expect(
74+
parsedInstruction.accounts.named["priceAccount"].isWritable
75+
).toBe(instruction.keys[2].isWritable);
76+
expect(
77+
parsedInstruction.accounts.named["permissionsAccount"].pubkey.equals(
78+
instruction.keys[3].pubkey
79+
)
80+
).toBeTruthy();
81+
expect(
82+
parsedInstruction.accounts.named["permissionsAccount"].isSigner
83+
).toBe(instruction.keys[3].isSigner);
84+
expect(
85+
parsedInstruction.accounts.named["permissionsAccount"].isWritable
86+
).toBe(instruction.keys[3].isWritable);
87+
expect(parsedInstruction.accounts.remaining.length).toBe(0);
88+
89+
expect(parsedInstruction.args.expo).toBe(-8);
90+
expect(parsedInstruction.args.pType).toBe(1);
91+
done();
92+
} else {
93+
done("Not instance of PythMultisigInstruction");
94+
}
95+
});
96+
});
97+
98+
test("Pyth multisig instruction parse: set minimum publishers", (done) => {
99+
jest.setTimeout(60000);
100+
101+
const cluster: PythCluster = "devnet";
102+
const pythProgram = pythOracleProgram(
103+
getPythProgramKeyForCluster(cluster),
104+
new AnchorProvider(
105+
new Connection(getPythClusterApiUrl(cluster)),
106+
new Wallet(new Keypair()),
107+
AnchorProvider.defaultOptions()
108+
)
109+
);
110+
const parser = MultisigParser.fromCluster(cluster);
111+
112+
pythProgram.methods
113+
.setMinPub(25, [0, 0, 0])
114+
.accounts({
115+
fundingAccount: PublicKey.unique(),
116+
priceAccount: PublicKey.unique(),
117+
})
118+
.instruction()
119+
.then((instruction) => {
120+
const parsedInstruction = parser.parseInstruction(instruction);
121+
122+
if (parsedInstruction instanceof PythMultisigInstruction) {
123+
expect(parsedInstruction.program).toBe(
124+
MultisigInstructionProgram.PythOracle
125+
);
126+
expect(parsedInstruction.name).toBe("setMinPub");
127+
expect(
128+
parsedInstruction.accounts.named["fundingAccount"].pubkey.equals(
129+
instruction.keys[0].pubkey
130+
)
131+
).toBeTruthy();
132+
expect(
133+
parsedInstruction.accounts.named["fundingAccount"].isSigner
134+
).toBe(instruction.keys[0].isSigner);
135+
expect(
136+
parsedInstruction.accounts.named["fundingAccount"].isWritable
137+
).toBe(instruction.keys[0].isWritable);
138+
expect(
139+
parsedInstruction.accounts.named["priceAccount"].pubkey.equals(
140+
instruction.keys[1].pubkey
141+
)
142+
).toBeTruthy();
143+
expect(parsedInstruction.accounts.named["priceAccount"].isSigner).toBe(
144+
instruction.keys[1].isSigner
145+
);
146+
expect(
147+
parsedInstruction.accounts.named["priceAccount"].isWritable
148+
).toBe(instruction.keys[1].isWritable);
149+
expect(
150+
parsedInstruction.accounts.named["permissionsAccount"].pubkey.equals(
151+
instruction.keys[2].pubkey
152+
)
153+
).toBeTruthy();
154+
expect(
155+
parsedInstruction.accounts.named["permissionsAccount"].isSigner
156+
).toBe(instruction.keys[2].isSigner);
157+
expect(
158+
parsedInstruction.accounts.named["permissionsAccount"].isWritable
159+
).toBe(instruction.keys[2].isWritable);
160+
expect(parsedInstruction.accounts.remaining.length).toBe(0);
161+
expect(parsedInstruction.args.minPub).toBe(25);
162+
done();
163+
} else {
164+
done("Not instance of PythMultisigInstruction");
165+
}
166+
});
167+
});

xc-admin/packages/xc-admin-common/src/__tests__/WormholeMultisigInstruction.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,6 @@ test("Wormhole multisig instruction parse: send message without governance paylo
4646
.instruction()
4747
.then((instruction) => {
4848
const parsedInstruction = parser.parseInstruction(instruction);
49-
expect(
50-
parsedInstruction instanceof WormholeMultisigInstruction
51-
).toBeTruthy();
5249
if (parsedInstruction instanceof WormholeMultisigInstruction) {
5350
expect(parsedInstruction.program).toBe(
5451
MultisigInstructionProgram.WormholeBridge
@@ -161,7 +158,7 @@ test("Wormhole multisig instruction parse: send message without governance paylo
161158
expect(parsedInstruction.args.targetChain).toBeUndefined();
162159
done();
163160
} else {
164-
done("Not instance of WormholeInstruction");
161+
done("Not instance of WormholeMultisigInstruction");
165162
}
166163
});
167164
});
@@ -354,7 +351,7 @@ test("Wormhole multisig instruction parse: send message with governance payload"
354351
done("Not instance of ExecutePostedVaa");
355352
}
356353
} else {
357-
done("Not instance of WormholeInstruction");
354+
done("Not instance of WormholeMultisigInstruction");
358355
}
359356
});
360357
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { MultisigInstruction, MultisigInstructionProgram } from ".";
2+
import { AnchorAccounts, resolveAccountNames } from "./anchor";
3+
import { pythIdl, pythOracleCoder } from "@pythnetwork/client";
4+
import { TransactionInstruction } from "@solana/web3.js";
5+
import { Idl } from "@coral-xyz/anchor";
6+
7+
export class PythMultisigInstruction implements MultisigInstruction {
8+
readonly program = MultisigInstructionProgram.PythOracle;
9+
readonly name: string;
10+
readonly args: { [key: string]: any };
11+
readonly accounts: AnchorAccounts;
12+
13+
constructor(
14+
name: string,
15+
args: { [key: string]: any },
16+
accounts: AnchorAccounts
17+
) {
18+
this.name = name;
19+
this.args = args;
20+
this.accounts = accounts;
21+
}
22+
23+
static fromTransactionInstruction(
24+
instruction: TransactionInstruction
25+
): PythMultisigInstruction {
26+
const pythInstructionCoder = pythOracleCoder().instruction;
27+
28+
const deserializedData = pythInstructionCoder.decode(instruction.data);
29+
30+
if (deserializedData) {
31+
return new PythMultisigInstruction(
32+
deserializedData.name,
33+
deserializedData.data,
34+
resolveAccountNames(pythIdl as Idl, deserializedData.name, instruction)
35+
);
36+
} else {
37+
return new PythMultisigInstruction(
38+
"Unrecognized instruction",
39+
{},
40+
{ named: {}, remaining: instruction.keys }
41+
);
42+
}
43+
}
44+
}

xc-admin/packages/xc-admin-common/src/multisig_transaction/index.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
} from "@pythnetwork/client/lib/cluster";
55
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
66
import { WORMHOLE_ADDRESS } from "../wormhole";
7+
import { PythMultisigInstruction } from "./PythMultisigInstruction";
78
import { WormholeMultisigInstruction } from "./WormholeMultisigInstruction";
89

910
export enum MultisigInstructionProgram {
@@ -30,11 +31,6 @@ export class UnrecognizedProgram implements MultisigInstruction {
3031
return new UnrecognizedProgram(instruction);
3132
}
3233
}
33-
34-
export class PythMultisigInstruction implements MultisigInstruction {
35-
readonly program = MultisigInstructionProgram.PythOracle;
36-
}
37-
3834
export class MultisigParser {
3935
readonly pythOracleAddress: PublicKey;
4036
readonly wormholeBridgeAddress: PublicKey | undefined;
@@ -61,6 +57,8 @@ export class MultisigParser {
6157
return WormholeMultisigInstruction.fromTransactionInstruction(
6258
instruction
6359
);
60+
} else if (instruction.programId.equals(this.pythOracleAddress)) {
61+
return PythMultisigInstruction.fromTransactionInstruction(instruction);
6462
} else {
6563
return UnrecognizedProgram.fromTransactionInstruction(instruction);
6664
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export const WORMHOLE_ADDRESS: Record<PythCluster, PublicKey | undefined> = {
66
pythtest: new PublicKey("EUrRARh92Cdc54xrDn6qzaqjA77NRrCcfbr8kPwoTL4z"),
77
devnet: new PublicKey("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5"),
88
pythnet: new PublicKey("H3fxXJ86ADW2PNuDDmZJg6mzTtPxkYCpNuQUTgmJ7AjU"),
9+
localnet: new PublicKey("gMYYig2utAxVoXnM9UhtTWrt8e7x2SVBZqsWZJeT5Gw"),
910
testnet: undefined,
1011
};

0 commit comments

Comments
 (0)