Initial version of contract manager sdk (#943)
* Initial version of governance sdk * Add more functionality to Sui contract manager and migrate variable naming to camelCase * Refactor sui functions * Add prettier * Add SuiAuthorizeUpgradeContractInstruction for governance * Update cosmwasm deploy tools entry point and expose some classes * Remove console.logs from CosmWasm * Refactor storage logic and add sui docs * Use relative path for default path of store * More documentation and minor fixes * Rename package * Add EVM classes * Implement getters for data sources * Use Google naming convention for abbreviations More info here: https://google.github.io/styleguide/tsguide.html#identifiers-abbreviations * Change package license * More comments and documentation * Store code proxy function in CosmWasm
This commit is contained in:
parent
a77ee78d13
commit
66e5f186b2
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "@pythnetwork/pyth-contract-manager",
|
||||
"version": "1.0.0",
|
||||
"description": "Set of tools to manage pyth contracts",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"shell": "ts-node ./src/shell.ts"
|
||||
},
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/pyth-network/pyth-crosschain.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pythnetwork/cosmwasm-deploy-tools": "*",
|
||||
"@pythnetwork/price-service-client": "*",
|
||||
"@pythnetwork/xc-governance-sdk": "*",
|
||||
"@certusone/wormhole-sdk": "^0.9.8",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.6.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { DataSource, HexString32Bytes } from "@pythnetwork/xc-governance-sdk";
|
||||
|
||||
export abstract class Storable {
|
||||
/**
|
||||
* Returns the unique identifier for this object
|
||||
*/
|
||||
abstract getId(): string;
|
||||
|
||||
/**
|
||||
* Returns the type of this object. This is used to reconstruct the object and should match
|
||||
* the static field type in the class responsible for constructing this object.
|
||||
*/
|
||||
abstract getType(): string;
|
||||
|
||||
/**
|
||||
* Returns a JSON representation of this object. It should be possible to
|
||||
* reconstruct the object from the JSON using the fromJson method.
|
||||
*/
|
||||
abstract toJson(): any;
|
||||
}
|
||||
|
||||
export abstract class Contract extends Storable {
|
||||
/**
|
||||
* Returns the time period in seconds that stale data is considered valid for.
|
||||
*/
|
||||
abstract getValidTimePeriod(): Promise<number>;
|
||||
|
||||
/**
|
||||
* Returns an array of data sources that this contract accepts price feed messages from
|
||||
*/
|
||||
abstract getDataSources(): Promise<DataSource[]>;
|
||||
|
||||
/**
|
||||
* Returns the single data source that this contract accepts governance messages from
|
||||
*/
|
||||
abstract getGovernanceDataSource(): Promise<DataSource>;
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import { readdirSync, readFileSync, writeFileSync } from "fs";
|
||||
import { Storable } from "./base";
|
||||
|
||||
export abstract class Chain extends Storable {
|
||||
protected constructor(public id: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return this.id;
|
||||
}
|
||||
}
|
||||
|
||||
export class CosmWasmChain extends Chain {
|
||||
static type: string = "CosmWasmChain";
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
public querierEndpoint: string,
|
||||
public executorEndpoint: string,
|
||||
public gasPrice: string,
|
||||
public prefix: string,
|
||||
public feeDenom: string
|
||||
) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
static fromJson(parsed: any): CosmWasmChain {
|
||||
if (parsed.type !== CosmWasmChain.type) throw new Error("Invalid type");
|
||||
return new CosmWasmChain(
|
||||
parsed.id,
|
||||
parsed.querierEndpoint,
|
||||
parsed.executorEndpoint,
|
||||
parsed.gasPrice,
|
||||
parsed.prefix,
|
||||
parsed.feeDenom
|
||||
);
|
||||
}
|
||||
|
||||
toJson(): any {
|
||||
return {
|
||||
querierEndpoint: this.querierEndpoint,
|
||||
executorEndpoint: this.executorEndpoint,
|
||||
id: this.id,
|
||||
gasPrice: this.gasPrice,
|
||||
prefix: this.prefix,
|
||||
feeDenom: this.feeDenom,
|
||||
type: CosmWasmChain.type,
|
||||
};
|
||||
}
|
||||
|
||||
getType(): string {
|
||||
return CosmWasmChain.type;
|
||||
}
|
||||
}
|
||||
|
||||
export class SuiChain extends Chain {
|
||||
static type: string = "SuiChain";
|
||||
|
||||
constructor(id: string, public rpcUrl: string) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
static fromJson(parsed: any): SuiChain {
|
||||
if (parsed.type !== SuiChain.type) throw new Error("Invalid type");
|
||||
return new SuiChain(parsed.id, parsed.rpcUrl);
|
||||
}
|
||||
|
||||
toJson(): any {
|
||||
return {
|
||||
id: this.id,
|
||||
rpcUrl: this.rpcUrl,
|
||||
type: SuiChain.type,
|
||||
};
|
||||
}
|
||||
|
||||
getType(): string {
|
||||
return SuiChain.type;
|
||||
}
|
||||
}
|
||||
|
||||
export class EVMChain extends Chain {
|
||||
static type: string = "EVMChain";
|
||||
|
||||
constructor(id: string, public rpcUrl: string) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
static fromJson(parsed: any): SuiChain {
|
||||
if (parsed.type !== EVMChain.type) throw new Error("Invalid type");
|
||||
return new EVMChain(parsed.id, parsed.rpcUrl);
|
||||
}
|
||||
|
||||
toJson(): any {
|
||||
return {
|
||||
id: this.id,
|
||||
rpcUrl: this.rpcUrl,
|
||||
type: EVMChain.type,
|
||||
};
|
||||
}
|
||||
|
||||
getType(): string {
|
||||
return EVMChain.type;
|
||||
}
|
||||
}
|
||||
|
||||
export const Chains: Record<string, Chain> = {};
|
|
@ -0,0 +1,344 @@
|
|||
import { Chains, CosmWasmChain } from "./chains";
|
||||
import { readFileSync } from "fs";
|
||||
import { getPythConfig } from "@pythnetwork/cosmwasm-deploy-tools/lib/configs";
|
||||
import {
|
||||
CHAINS,
|
||||
DataSource,
|
||||
HexString32Bytes,
|
||||
SetFeeInstruction,
|
||||
} from "@pythnetwork/xc-governance-sdk";
|
||||
import { DeploymentType } from "@pythnetwork/cosmwasm-deploy-tools/lib/helper";
|
||||
import {
|
||||
CosmwasmExecutor,
|
||||
PythWrapperExecutor,
|
||||
PythWrapperQuerier,
|
||||
} from "@pythnetwork/cosmwasm-deploy-tools";
|
||||
import {
|
||||
ContractInfoResponse,
|
||||
CosmwasmQuerier,
|
||||
} from "@pythnetwork/cosmwasm-deploy-tools/lib/chains-manager/chain-querier";
|
||||
import { PriceServiceConnection } from "@pythnetwork/price-service-client";
|
||||
import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";
|
||||
import { Contract } from "./base";
|
||||
|
||||
/**
|
||||
* Variables here need to be snake case to match the on-chain contract configs
|
||||
*/
|
||||
namespace CosmWasmContract {
|
||||
export interface WormholeSource {
|
||||
emitter: string;
|
||||
chain_id: number;
|
||||
}
|
||||
|
||||
export interface DeploymentConfig {
|
||||
data_sources: WormholeSource[];
|
||||
governance_source: WormholeSource;
|
||||
wormhole_contract: string;
|
||||
governance_source_index: number;
|
||||
governance_sequence_number: number;
|
||||
chain_id: number;
|
||||
valid_time_period_secs: number;
|
||||
fee: { amount: string; denom: string };
|
||||
}
|
||||
}
|
||||
|
||||
export class CosmWasmContract extends Contract {
|
||||
async getDataSources(): Promise<DataSource[]> {
|
||||
const config = await this.getConfig();
|
||||
return config.config_v1.data_sources.map(({ emitter, chain_id }: any) => {
|
||||
return new DataSource(
|
||||
Number(chain_id),
|
||||
new HexString32Bytes(Buffer.from(emitter, "base64").toString("hex"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async getGovernanceDataSource(): Promise<DataSource> {
|
||||
const config = await this.getConfig();
|
||||
const { emitter: emitterAddress, chain_id: chainId } =
|
||||
config.config_v1.governance_source;
|
||||
return new DataSource(
|
||||
Number(chainId),
|
||||
new HexString32Bytes(
|
||||
Buffer.from(emitterAddress, "base64").toString("hex")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
static type = "CosmWasmContract";
|
||||
|
||||
constructor(public chain: CosmWasmChain, public address: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
static fromJson(parsed: any): CosmWasmContract {
|
||||
if (parsed.type !== CosmWasmContract.type) throw new Error("Invalid type");
|
||||
if (!Chains[parsed.chain])
|
||||
throw new Error(`Chain ${parsed.chain} not found`);
|
||||
return new CosmWasmContract(
|
||||
Chains[parsed.chain] as CosmWasmChain,
|
||||
parsed.address
|
||||
);
|
||||
}
|
||||
|
||||
getType(): string {
|
||||
return CosmWasmContract.type;
|
||||
}
|
||||
|
||||
//TODO : make deploymentType enum stable | edge
|
||||
static getDeploymentConfig(
|
||||
chain: CosmWasmChain,
|
||||
deploymentType: string,
|
||||
wormholeContract: string
|
||||
): CosmWasmContract.DeploymentConfig {
|
||||
return getPythConfig({
|
||||
feeDenom: chain.feeDenom,
|
||||
wormholeChainId: CHAINS[chain.getId() as keyof typeof CHAINS],
|
||||
wormholeContract,
|
||||
deploymentType: deploymentType as DeploymentType,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the wasm code on the specified chain using the provided mnemonic as the signer
|
||||
* You can find the wasm artifacts from the repo releases
|
||||
* @param chain chain to store the code on
|
||||
* @param mnemonic mnemonic to use for signing the transaction
|
||||
* @param wasmPath path in your local filesystem to the wasm artifact
|
||||
*/
|
||||
static async storeCode(
|
||||
chain: CosmWasmChain,
|
||||
mnemonic: string,
|
||||
wasmPath: string
|
||||
) {
|
||||
const contractBytes = readFileSync(wasmPath);
|
||||
let executor = this.getExecutor(chain, mnemonic);
|
||||
return executor.storeCode({ contractBytes });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploys a new contract to the specified chain using the uploaded wasm code codeId
|
||||
* @param chain chain to deploy to
|
||||
* @param codeId codeId of the uploaded wasm code. You can get this from the storeCode result
|
||||
* @param config deployment config for initializing the contract (data sources, governance source, etc)
|
||||
* @param mnemonic mnemonic to use for signing the transaction
|
||||
*/
|
||||
static async initialize(
|
||||
chain: CosmWasmChain,
|
||||
codeId: number,
|
||||
config: CosmWasmContract.DeploymentConfig,
|
||||
mnemonic: string
|
||||
): Promise<CosmWasmContract> {
|
||||
let executor = this.getExecutor(chain, mnemonic);
|
||||
let result = await executor.instantiateContract({
|
||||
codeId: codeId,
|
||||
instMsg: config,
|
||||
label: "pyth",
|
||||
});
|
||||
await executor.updateContractAdmin({
|
||||
newAdminAddr: result.contractAddr,
|
||||
contractAddr: result.contractAddr,
|
||||
});
|
||||
return new CosmWasmContract(chain, result.contractAddr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the wasm code and initializes a new contract to the specified chain.
|
||||
* Use this method if you are deploying to a new chain, or you want a fresh contract in
|
||||
* a testnet environment. Uses the default deployment configurations for governance, data sources,
|
||||
* valid time period, etc. You can manually run the storeCode and initialize methods if you want
|
||||
* more control over the deployment process.
|
||||
* @param chain
|
||||
* @param wormholeContract
|
||||
* @param mnemonic
|
||||
* @param wasmPath
|
||||
*/
|
||||
static async deploy(
|
||||
chain: CosmWasmChain,
|
||||
wormholeContract: string,
|
||||
mnemonic: string,
|
||||
wasmPath: string
|
||||
): Promise<CosmWasmContract> {
|
||||
let config = this.getDeploymentConfig(chain, "edge", wormholeContract);
|
||||
const { codeId } = await this.storeCode(chain, mnemonic, wasmPath);
|
||||
return this.initialize(chain, codeId, config, mnemonic);
|
||||
}
|
||||
|
||||
private static getExecutor(chain: CosmWasmChain, mnemonic: string) {
|
||||
// TODO: logic for injective
|
||||
return new CosmwasmExecutor(
|
||||
chain.executorEndpoint,
|
||||
mnemonic,
|
||||
chain.prefix,
|
||||
chain.gasPrice + chain.feeDenom
|
||||
);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return `${this.chain.getId()}_${this.address}`;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
chain: this.chain.id,
|
||||
address: this.address,
|
||||
type: CosmWasmContract.type,
|
||||
};
|
||||
}
|
||||
|
||||
async getQuerier(): Promise<PythWrapperQuerier> {
|
||||
const chainQuerier = await CosmwasmQuerier.connect(
|
||||
this.chain.querierEndpoint
|
||||
);
|
||||
const pythQuerier = new PythWrapperQuerier(chainQuerier);
|
||||
return pythQuerier;
|
||||
}
|
||||
|
||||
async getCodeId(): Promise<number> {
|
||||
let result = await this.getWasmContractInfo();
|
||||
return result.codeId;
|
||||
}
|
||||
|
||||
async getWasmContractInfo(): Promise<ContractInfoResponse> {
|
||||
const chainQuerier = await CosmwasmQuerier.connect(
|
||||
this.chain.querierEndpoint
|
||||
);
|
||||
return chainQuerier.getContractInfo({ contractAddr: this.address });
|
||||
}
|
||||
|
||||
async getConfig() {
|
||||
const chainQuerier = await CosmwasmQuerier.connect(
|
||||
this.chain.querierEndpoint
|
||||
);
|
||||
let allStates = (await chainQuerier.getAllContractState({
|
||||
contractAddr: this.address,
|
||||
})) as any;
|
||||
let config = {
|
||||
config_v1: JSON.parse(allStates["\x00\tconfig_v1"]),
|
||||
contract_version: JSON.parse(allStates["\x00\x10contract_version"]),
|
||||
};
|
||||
return config;
|
||||
}
|
||||
|
||||
// TODO: function for uploading the code and getting the code id
|
||||
// TODO: function for upgrading the contract
|
||||
// TODO: Cleanup and more strict linter to convert let to const
|
||||
|
||||
async getPriceFeed(feedId: string): Promise<any> {
|
||||
let querier = await this.getQuerier();
|
||||
return querier.getPriceFeed(this.address, feedId);
|
||||
}
|
||||
|
||||
equalDataSources(
|
||||
dataSources1: CosmWasmContract.WormholeSource[],
|
||||
dataSources2: CosmWasmContract.WormholeSource[]
|
||||
): boolean {
|
||||
if (dataSources1.length !== dataSources2.length) return false;
|
||||
for (let i = 0; i < dataSources1.length; i++) {
|
||||
let found = false;
|
||||
for (let j = 0; j < dataSources2.length; j++) {
|
||||
if (
|
||||
dataSources1[i].emitter === dataSources2[j].emitter &&
|
||||
dataSources1[i].chain_id === dataSources2[j].chain_id
|
||||
) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async getDeploymentType(): Promise<string> {
|
||||
let config = await this.getConfig();
|
||||
let wormholeContract = config.config_v1.wormhole_contract;
|
||||
let stableConfig = getPythConfig({
|
||||
feeDenom: this.chain.feeDenom,
|
||||
wormholeChainId: CHAINS[this.chain.getId() as keyof typeof CHAINS],
|
||||
wormholeContract,
|
||||
deploymentType: "stable",
|
||||
});
|
||||
let edgeConfig = getPythConfig({
|
||||
feeDenom: this.chain.feeDenom,
|
||||
wormholeChainId: CHAINS[this.chain.getId() as keyof typeof CHAINS],
|
||||
wormholeContract,
|
||||
deploymentType: "edge",
|
||||
});
|
||||
if (
|
||||
this.equalDataSources(
|
||||
config.config_v1.data_sources,
|
||||
stableConfig.data_sources
|
||||
)
|
||||
)
|
||||
return "stable";
|
||||
else if (
|
||||
this.equalDataSources(
|
||||
config.config_v1.data_sources,
|
||||
edgeConfig.data_sources
|
||||
)
|
||||
)
|
||||
return "edge";
|
||||
else return "unknown";
|
||||
}
|
||||
|
||||
async executeUpdatePriceFeed(feedId: string, mnemonic: string) {
|
||||
const deploymentType = await this.getDeploymentType();
|
||||
const priceServiceConnection = new PriceServiceConnection(
|
||||
deploymentType === "stable"
|
||||
? "https://xc-mainnet.pyth.network"
|
||||
: "https://xc-testnet.pyth.network"
|
||||
);
|
||||
|
||||
const vaas = await priceServiceConnection.getLatestVaas([feedId]);
|
||||
const fund = await this.getUpdateFee(vaas);
|
||||
let executor = new CosmwasmExecutor(
|
||||
this.chain.executorEndpoint,
|
||||
mnemonic,
|
||||
this.chain.prefix,
|
||||
this.chain.gasPrice + this.chain.feeDenom
|
||||
);
|
||||
let pythExecutor = new PythWrapperExecutor(executor);
|
||||
return pythExecutor.executeUpdatePriceFeeds({
|
||||
contractAddr: this.address,
|
||||
vaas,
|
||||
fund,
|
||||
});
|
||||
}
|
||||
|
||||
async executeGovernanceInstruction(mnemonic: string, vaa: string) {
|
||||
let executor = new CosmwasmExecutor(
|
||||
this.chain.executorEndpoint,
|
||||
mnemonic,
|
||||
this.chain.prefix,
|
||||
this.chain.gasPrice + this.chain.feeDenom
|
||||
);
|
||||
let pythExecutor = new PythWrapperExecutor(executor);
|
||||
return pythExecutor.executeGovernanceInstruction({
|
||||
contractAddr: this.address,
|
||||
vaa,
|
||||
});
|
||||
}
|
||||
|
||||
async getUpdateFee(msgs: string[]): Promise<any> {
|
||||
let querier = await this.getQuerier();
|
||||
return querier.getUpdateFee(this.address, msgs);
|
||||
}
|
||||
|
||||
getSetUpdateFeePayload(fee: number): Buffer {
|
||||
return new SetFeeInstruction(
|
||||
CHAINS[this.chain.getId() as keyof typeof CHAINS],
|
||||
BigInt(fee),
|
||||
BigInt(0)
|
||||
).serialize();
|
||||
}
|
||||
|
||||
async getValidTimePeriod() {
|
||||
let client = await CosmWasmClient.connect(this.chain.querierEndpoint);
|
||||
let result = await client.queryContractSmart(
|
||||
this.address,
|
||||
"get_valid_time_period"
|
||||
);
|
||||
return Number(result.secs + result.nanos * 1e-9);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,273 @@
|
|||
import { readFileSync } from "fs";
|
||||
|
||||
import {
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
SYSVAR_CLOCK_PUBKEY,
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
Transaction,
|
||||
TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
|
||||
import { BN } from "bn.js";
|
||||
import { getPythClusterApiUrl } from "@pythnetwork/client/lib/cluster";
|
||||
import SquadsMesh, { getTxPDA } from "@sqds/mesh";
|
||||
import { AnchorProvider, Wallet } from "@coral-xyz/anchor/dist/cjs/provider";
|
||||
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
|
||||
import { WORMHOLE_ADDRESS, WORMHOLE_API_ENDPOINT } from "xc_admin_common";
|
||||
import {
|
||||
createWormholeProgramInterface,
|
||||
deriveEmitterSequenceKey,
|
||||
deriveFeeCollectorKey,
|
||||
deriveWormholeBridgeDataKey,
|
||||
} from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
|
||||
import { Contract, Storable } from "./base";
|
||||
|
||||
export const Contracts: Record<string, Contract> = {};
|
||||
|
||||
export class SubmittedWormholeMessage {
|
||||
constructor(
|
||||
public emitter: PublicKey,
|
||||
public sequenceNumber: number,
|
||||
public cluster: string
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Tries to fetch the VAA from the wormhole bridge API waiting for a certain amount of time
|
||||
* before giving up and throwing an error
|
||||
* @param waitingSeconds how long to wait before giving up
|
||||
*/
|
||||
async fetchVaa(waitingSeconds: number = 1): Promise<Buffer> {
|
||||
let rpcUrl =
|
||||
WORMHOLE_API_ENDPOINT[this.cluster as keyof typeof WORMHOLE_API_ENDPOINT];
|
||||
|
||||
let startTime = Date.now();
|
||||
while (Date.now() - startTime < waitingSeconds * 1000) {
|
||||
const response = await fetch(
|
||||
`${rpcUrl}/v1/signed_vaa/1/${this.emitter.toBuffer().toString("hex")}/${
|
||||
this.sequenceNumber
|
||||
}`
|
||||
);
|
||||
if (response.status === 404) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
continue;
|
||||
}
|
||||
const { vaaBytes } = await response.json();
|
||||
return Buffer.from(vaaBytes, "base64");
|
||||
}
|
||||
throw new Error("VAA not found, maybe too soon to fetch?");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple emitter that can send messages to the wormhole bridge
|
||||
* This can be used instead of multisig as a simple way to send messages
|
||||
* and debug contracts deployed on testing networks
|
||||
* You need to set your pyth contract data source / governance source address to this emitter
|
||||
*/
|
||||
export class WormholeEmitter {
|
||||
cluster: string;
|
||||
wallet: Wallet;
|
||||
|
||||
constructor(cluster: string, wallet: Wallet) {
|
||||
this.cluster = cluster;
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
async sendMessage(payload: Buffer) {
|
||||
const provider = new AnchorProvider(
|
||||
new Connection(getPythClusterApiUrl(this.cluster as any), "confirmed"),
|
||||
this.wallet,
|
||||
{
|
||||
commitment: "confirmed",
|
||||
preflightCommitment: "confirmed",
|
||||
}
|
||||
);
|
||||
let wormholeAddress =
|
||||
WORMHOLE_ADDRESS[this.cluster as keyof typeof WORMHOLE_ADDRESS]!;
|
||||
let kp = Keypair.generate();
|
||||
let feeCollector = deriveFeeCollectorKey(wormholeAddress);
|
||||
let emitter = this.wallet.publicKey;
|
||||
let accounts = {
|
||||
bridge: deriveWormholeBridgeDataKey(wormholeAddress),
|
||||
message: kp.publicKey,
|
||||
emitter: emitter,
|
||||
sequence: deriveEmitterSequenceKey(emitter, wormholeAddress),
|
||||
payer: emitter,
|
||||
feeCollector,
|
||||
clock: SYSVAR_CLOCK_PUBKEY,
|
||||
rent: SYSVAR_RENT_PUBKEY,
|
||||
systemProgram: SystemProgram.programId,
|
||||
};
|
||||
const wormholeProgram = createWormholeProgramInterface(
|
||||
wormholeAddress,
|
||||
provider
|
||||
);
|
||||
const transaction = new Transaction();
|
||||
transaction.add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: emitter,
|
||||
toPubkey: feeCollector,
|
||||
lamports: 1000,
|
||||
})
|
||||
);
|
||||
transaction.add(
|
||||
await wormholeProgram.methods
|
||||
.postMessage(0, payload, 0)
|
||||
.accounts(accounts)
|
||||
.instruction()
|
||||
);
|
||||
const txSig = await provider.sendAndConfirm(transaction, [kp]);
|
||||
const txDetails = await provider.connection.getParsedTransaction(txSig);
|
||||
const sequenceLogPrefix = "Sequence: ";
|
||||
const txLog = txDetails?.meta?.logMessages?.find((s) =>
|
||||
s.includes(sequenceLogPrefix)
|
||||
);
|
||||
|
||||
const sequenceNumber = Number(
|
||||
txLog?.substring(
|
||||
txLog.indexOf(sequenceLogPrefix) + sequenceLogPrefix.length
|
||||
)
|
||||
);
|
||||
return new SubmittedWormholeMessage(emitter, sequenceNumber, this.cluster);
|
||||
}
|
||||
}
|
||||
|
||||
export class Vault extends Storable {
|
||||
static type: string = "vault";
|
||||
key: PublicKey;
|
||||
squad?: SquadsMesh;
|
||||
cluster: string;
|
||||
|
||||
constructor(key: string, cluster: string) {
|
||||
super();
|
||||
this.key = new PublicKey(key);
|
||||
this.cluster = cluster;
|
||||
}
|
||||
|
||||
getType(): string {
|
||||
return Vault.type;
|
||||
}
|
||||
|
||||
static from(path: string): Vault {
|
||||
let parsed = JSON.parse(readFileSync(path, "utf-8"));
|
||||
if (parsed.type !== Vault.type) throw new Error("Invalid type");
|
||||
return new Vault(parsed.key, parsed.cluster);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return `${this.cluster}_${this.key.toString()}`;
|
||||
}
|
||||
|
||||
toJson(): any {
|
||||
return {
|
||||
key: this.key.toString(),
|
||||
cluster: this.cluster,
|
||||
type: Vault.type,
|
||||
};
|
||||
}
|
||||
|
||||
public connect(wallet: Wallet): void {
|
||||
this.squad = SquadsMesh.endpoint(
|
||||
getPythClusterApiUrl(this.cluster as any), // TODO Fix any
|
||||
wallet
|
||||
);
|
||||
}
|
||||
|
||||
public async createProposalIx(
|
||||
proposalIndex: number
|
||||
): Promise<[TransactionInstruction, PublicKey]> {
|
||||
const squad = this.getSquadOrThrow();
|
||||
const msAccount = await squad.getMultisig(this.key);
|
||||
|
||||
const ix = await squad.buildCreateTransaction(
|
||||
msAccount.publicKey,
|
||||
msAccount.authorityIndex,
|
||||
proposalIndex
|
||||
);
|
||||
|
||||
const newProposalAddress = getTxPDA(
|
||||
this.key,
|
||||
new BN(proposalIndex),
|
||||
squad.multisigProgramId
|
||||
)[0];
|
||||
|
||||
return [ix, newProposalAddress];
|
||||
}
|
||||
|
||||
public async activateProposalIx(
|
||||
proposalAddress: PublicKey
|
||||
): Promise<TransactionInstruction> {
|
||||
const squad = this.getSquadOrThrow();
|
||||
return await squad.buildActivateTransaction(this.key, proposalAddress);
|
||||
}
|
||||
|
||||
public async approveProposalIx(
|
||||
proposalAddress: PublicKey
|
||||
): Promise<TransactionInstruction> {
|
||||
const squad = this.getSquadOrThrow();
|
||||
return await squad.buildApproveTransaction(this.key, proposalAddress);
|
||||
}
|
||||
|
||||
getSquadOrThrow(): SquadsMesh {
|
||||
if (!this.squad) throw new Error("Please connect a wallet to the vault");
|
||||
return this.squad;
|
||||
}
|
||||
|
||||
public async proposeWormholeMessage(payload: Buffer): Promise<any> {
|
||||
const squad = this.getSquadOrThrow();
|
||||
const msAccount = await squad.getMultisig(this.key);
|
||||
|
||||
let ixToSend: TransactionInstruction[] = [];
|
||||
const [proposalIx, newProposalAddress] = await this.createProposalIx(
|
||||
msAccount.transactionIndex + 1
|
||||
);
|
||||
|
||||
const proposalIndex = msAccount.transactionIndex + 1;
|
||||
ixToSend.push(proposalIx);
|
||||
return ixToSend;
|
||||
// const instructionToPropose = await getPostMessageInstruction(
|
||||
// squad,
|
||||
// this.key,
|
||||
// newProposalAddress,
|
||||
// 1,
|
||||
// this.wormholeAddress()!,
|
||||
// payload
|
||||
// );
|
||||
// ixToSend.push(
|
||||
// await squad.buildAddInstruction(
|
||||
// this.key,
|
||||
// newProposalAddress,
|
||||
// instructionToPropose.instruction,
|
||||
// 1,
|
||||
// instructionToPropose.authorityIndex,
|
||||
// instructionToPropose.authorityBump,
|
||||
// instructionToPropose.authorityType
|
||||
// )
|
||||
// );
|
||||
// ixToSend.push(await this.activateProposalIx(newProposalAddress));
|
||||
// ixToSend.push(await this.approveProposalIx(newProposalAddress));
|
||||
|
||||
// const txToSend = batchIntoTransactions(ixToSend);
|
||||
// for (let i = 0; i < txToSend.length; i += SIZE_OF_SIGNED_BATCH) {
|
||||
// await this.getAnchorProvider().sendAll(
|
||||
// txToSend.slice(i, i + SIZE_OF_SIGNED_BATCH).map((tx) => {
|
||||
// return { tx, signers: [] };
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
// return newProposalAddress;
|
||||
}
|
||||
}
|
||||
|
||||
export const Vaults: Record<string, Vault> = {};
|
||||
|
||||
export async function loadHotWallet(wallet: string): Promise<Wallet> {
|
||||
return new NodeWallet(
|
||||
Keypair.fromSecretKey(
|
||||
Uint8Array.from(JSON.parse(readFileSync(wallet, "ascii")))
|
||||
)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
import Web3 from "web3"; //TODO: decide on using web3 or ethers.js
|
||||
import PythInterfaceAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
|
||||
import { Contract } from "./base";
|
||||
import { Chains, EVMChain } from "./chains";
|
||||
import { DataSource, HexString32Bytes } from "@pythnetwork/xc-governance-sdk";
|
||||
|
||||
export class EVMContract extends Contract {
|
||||
static type = "EVMContract";
|
||||
|
||||
constructor(public chain: EVMChain, public address: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
static fromJson(parsed: any): EVMContract {
|
||||
if (parsed.type !== EVMContract.type) throw new Error("Invalid type");
|
||||
if (!Chains[parsed.chain])
|
||||
throw new Error(`Chain ${parsed.chain} not found`);
|
||||
return new EVMContract(Chains[parsed.chain] as EVMChain, parsed.address);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return `${this.chain.getId()}_${this.address}`;
|
||||
}
|
||||
|
||||
getType(): string {
|
||||
return EVMContract.type;
|
||||
}
|
||||
|
||||
getContract() {
|
||||
const web3 = new Web3(this.chain.rpcUrl);
|
||||
const pythContract = new web3.eth.Contract(
|
||||
[
|
||||
{
|
||||
inputs: [],
|
||||
name: "governanceDataSource",
|
||||
outputs: [
|
||||
{
|
||||
components: [
|
||||
{
|
||||
internalType: "uint16",
|
||||
name: "chainId",
|
||||
type: "uint16",
|
||||
},
|
||||
{
|
||||
internalType: "bytes32",
|
||||
name: "emitterAddress",
|
||||
type: "bytes32",
|
||||
},
|
||||
],
|
||||
internalType: "struct PythInternalStructs.DataSource",
|
||||
name: "",
|
||||
type: "tuple",
|
||||
},
|
||||
],
|
||||
stateMutability: "view",
|
||||
type: "function",
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: "validDataSources",
|
||||
outputs: [
|
||||
{
|
||||
components: [
|
||||
{
|
||||
internalType: "uint16",
|
||||
name: "chainId",
|
||||
type: "uint16",
|
||||
},
|
||||
{
|
||||
internalType: "bytes32",
|
||||
name: "emitterAddress",
|
||||
type: "bytes32",
|
||||
},
|
||||
],
|
||||
internalType: "struct PythInternalStructs.DataSource[]",
|
||||
name: "",
|
||||
type: "tuple[]",
|
||||
},
|
||||
],
|
||||
stateMutability: "view",
|
||||
type: "function",
|
||||
constant: true,
|
||||
},
|
||||
...PythInterfaceAbi,
|
||||
] as any,
|
||||
this.address
|
||||
);
|
||||
return pythContract;
|
||||
}
|
||||
|
||||
async getPriceFeed(feedId: string) {
|
||||
const pythContract = this.getContract();
|
||||
const [price, conf, expo, publishTime] = await pythContract.methods
|
||||
.getPriceUnsafe(feedId)
|
||||
.call();
|
||||
return { price, conf, expo, publishTime };
|
||||
}
|
||||
|
||||
async getValidTimePeriod() {
|
||||
const pythContract = this.getContract();
|
||||
const result = await pythContract.methods.getValidTimePeriod().call();
|
||||
return Number(result);
|
||||
}
|
||||
|
||||
async getDataSources(): Promise<DataSource[]> {
|
||||
const pythContract = this.getContract();
|
||||
const result = await pythContract.methods.validDataSources().call();
|
||||
return result.map(({ chainId, emitterAddress }: any) => {
|
||||
return new DataSource(
|
||||
Number(chainId),
|
||||
new HexString32Bytes(emitterAddress)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async getGovernanceDataSource(): Promise<DataSource> {
|
||||
const pythContract = this.getContract();
|
||||
const [chainId, emitterAddress] = await pythContract.methods
|
||||
.governanceDataSource()
|
||||
.call();
|
||||
return new DataSource(
|
||||
Number(chainId),
|
||||
new HexString32Bytes(emitterAddress)
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
chain: this.chain.id,
|
||||
address: this.address,
|
||||
type: EVMContract.type,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import * as tsNode from "ts-node";
|
||||
|
||||
const repl = tsNode.createRepl();
|
||||
const service = tsNode.create({ ...repl.evalAwarePartialHost });
|
||||
repl.setService(service);
|
||||
repl.start();
|
||||
repl.evalCode(
|
||||
"import { Contracts, Vaults, loadHotWallet } from './src/entities';" +
|
||||
"import { Chains,SuiChain,CosmWasmChain } from './src/chains';" +
|
||||
"import { SuiContract } from './src/sui';" +
|
||||
"import { CosmWasmContract } from './src/cosmwasm';" +
|
||||
"import { DefaultStore } from './src/store';" +
|
||||
"DefaultStore"
|
||||
);
|
||||
|
||||
// import * as repl from 'node:repl';
|
||||
// import { CosmWasmChain, Chains, ChainContracts } from './entities';
|
||||
// // import { CHAINS_NETWORK_CONFIG } from './chains-manager/chains';
|
||||
|
||||
// const replServer = repl.start('Pyth shell> ')
|
||||
// // const mnemonic = "salon myth guide analyst umbrella load arm first roast pelican stuff satoshi";
|
||||
|
||||
// replServer.context.CosmWasmChain = CosmWasmChain;
|
||||
// replServer.context.Chains = Chains;
|
||||
// replServer.context.ChainContracts = ChainContracts;
|
|
@ -0,0 +1,105 @@
|
|||
import { Chain, CosmWasmChain, SuiChain, Chains, EVMChain } from "./chains";
|
||||
import { CosmWasmContract } from "./cosmwasm";
|
||||
import { SuiContract } from "./sui";
|
||||
import { Contract } from "./base";
|
||||
import {
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
mkdirSync,
|
||||
existsSync,
|
||||
statSync,
|
||||
} from "fs";
|
||||
import { Contracts } from "./entities";
|
||||
import { EVMContract } from "./evm";
|
||||
|
||||
class Store {
|
||||
static Chains: Record<string, Chain> = {};
|
||||
static Contracts: Record<string, CosmWasmContract | SuiContract> = {};
|
||||
|
||||
constructor(public path: string) {
|
||||
this.loadAllChains();
|
||||
this.loadAllContracts();
|
||||
}
|
||||
|
||||
save(obj: any) {
|
||||
let dir, file, content;
|
||||
if (obj instanceof Contract) {
|
||||
let contract = obj;
|
||||
dir = `${this.path}/contracts/${contract.getType()}`;
|
||||
file = contract.getId();
|
||||
content = contract.toJson();
|
||||
} else if (obj instanceof Chain) {
|
||||
let chain = obj;
|
||||
dir = `${this.path}/chains/${chain.getType()}`;
|
||||
file = chain.getId();
|
||||
content = chain.toJson();
|
||||
} else {
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
writeFileSync(
|
||||
`${dir}/${file}.json`,
|
||||
JSON.stringify(content, undefined, 2) + "\n"
|
||||
);
|
||||
}
|
||||
|
||||
getJSONFiles(path: string) {
|
||||
const walk = function (dir: string) {
|
||||
let results: string[] = [];
|
||||
const list = readdirSync(dir);
|
||||
list.forEach(function (file) {
|
||||
file = dir + "/" + file;
|
||||
const stat = statSync(file);
|
||||
if (stat && stat.isDirectory()) {
|
||||
// Recurse into a subdirectory
|
||||
results = results.concat(walk(file));
|
||||
} else {
|
||||
// Is a file
|
||||
results.push(file);
|
||||
}
|
||||
});
|
||||
return results;
|
||||
};
|
||||
return walk(path).filter((file) => file.endsWith(".json"));
|
||||
}
|
||||
|
||||
loadAllChains() {
|
||||
let allChainClasses = {
|
||||
[CosmWasmChain.type]: CosmWasmChain,
|
||||
[SuiChain.type]: SuiChain,
|
||||
[EVMChain.type]: EVMChain,
|
||||
};
|
||||
|
||||
this.getJSONFiles(`${this.path}/chains/`).forEach((jsonFile) => {
|
||||
let parsed = JSON.parse(readFileSync(jsonFile, "utf-8"));
|
||||
if (allChainClasses[parsed.type] === undefined) return;
|
||||
let chain = allChainClasses[parsed.type].fromJson(parsed);
|
||||
if (Chains[chain.getId()])
|
||||
throw new Error(`Multiple chains with id ${chain.getId()} found`);
|
||||
Chains[chain.getId()] = chain;
|
||||
});
|
||||
}
|
||||
|
||||
loadAllContracts() {
|
||||
let allContractClasses = {
|
||||
[CosmWasmContract.type]: CosmWasmContract,
|
||||
[SuiContract.type]: SuiContract,
|
||||
[EVMContract.type]: EVMContract,
|
||||
};
|
||||
this.getJSONFiles(`${this.path}/contracts/`).forEach((jsonFile) => {
|
||||
let parsed = JSON.parse(readFileSync(jsonFile, "utf-8"));
|
||||
if (allContractClasses[parsed.type] === undefined) return;
|
||||
let chainContract = allContractClasses[parsed.type].fromJson(parsed);
|
||||
if (Contracts[chainContract.getId()])
|
||||
throw new Error(
|
||||
`Multiple contracts with id ${chainContract.getId()} found`
|
||||
);
|
||||
Contracts[chainContract.getId()] = chainContract;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const DefaultStore = new Store(`${__dirname}/../store`);
|
|
@ -0,0 +1,381 @@
|
|||
import {
|
||||
RawSigner,
|
||||
SUI_CLOCK_OBJECT_ID,
|
||||
TransactionBlock,
|
||||
JsonRpcProvider,
|
||||
Ed25519Keypair,
|
||||
Connection,
|
||||
ObjectId,
|
||||
} from "@mysten/sui.js";
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { Chains, SuiChain } from "./chains";
|
||||
import {
|
||||
CHAINS,
|
||||
DataSource,
|
||||
HexString32Bytes,
|
||||
SetFeeInstruction,
|
||||
SuiAuthorizeUpgradeContractInstruction,
|
||||
} from "@pythnetwork/xc-governance-sdk";
|
||||
import { BufferBuilder } from "@pythnetwork/xc-governance-sdk/lib/serialize";
|
||||
import { Contract } from "./base";
|
||||
|
||||
export class SuiContract extends Contract {
|
||||
static type = "SuiContract";
|
||||
|
||||
/**
|
||||
* Given the ids of the pyth state and wormhole state, create a new SuiContract
|
||||
* The package ids are derived based on the state ids
|
||||
*
|
||||
* @param chain the chain which this contract is deployed on
|
||||
* @param stateId id of the pyth state for the deployed contract
|
||||
* @param wormholeStateId id of the wormhole state for the wormhole contract that pyth binds to
|
||||
*/
|
||||
constructor(
|
||||
public chain: SuiChain,
|
||||
public stateId: string,
|
||||
public wormholeStateId: string
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
static fromJson(parsed: any): SuiContract {
|
||||
if (parsed.type !== SuiContract.type) throw new Error("Invalid type");
|
||||
if (!Chains[parsed.chain])
|
||||
throw new Error(`Chain ${parsed.chain} not found`);
|
||||
return new SuiContract(
|
||||
Chains[parsed.chain] as SuiChain,
|
||||
parsed.stateId,
|
||||
parsed.wormholeStateId
|
||||
);
|
||||
}
|
||||
|
||||
getType(): string {
|
||||
return SuiContract.type;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
chain: this.chain.id,
|
||||
stateId: this.stateId,
|
||||
wormholeStateId: this.wormholeStateId,
|
||||
type: SuiContract.type,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a objectId, returns the id for the package that the object belongs to.
|
||||
* @param objectId
|
||||
*/
|
||||
async getPackageId(objectId: ObjectId): Promise<ObjectId> {
|
||||
const provider = this.getProvider();
|
||||
const state = await provider
|
||||
.getObject({
|
||||
id: objectId,
|
||||
options: {
|
||||
showContent: true,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data?.content?.dataType == "moveObject") {
|
||||
return result.data.content.fields;
|
||||
}
|
||||
|
||||
throw new Error("not move object");
|
||||
});
|
||||
|
||||
if ("upgrade_cap" in state) {
|
||||
return state.upgrade_cap.fields.package;
|
||||
}
|
||||
|
||||
throw new Error("upgrade_cap not found");
|
||||
}
|
||||
|
||||
async getPythPackageId(): Promise<ObjectId> {
|
||||
return await this.getPackageId(this.stateId);
|
||||
}
|
||||
|
||||
async getWormholePackageId(): Promise<ObjectId> {
|
||||
return await this.getPackageId(this.wormholeStateId);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return `${this.chain.getId()}_${this.stateId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the price table object id for the current state id
|
||||
*/
|
||||
async getPriceTableId(): Promise<ObjectId> {
|
||||
const provider = this.getProvider();
|
||||
let result = await provider.getDynamicFieldObject({
|
||||
parentId: this.stateId,
|
||||
name: {
|
||||
type: "vector<u8>",
|
||||
value: "price_info",
|
||||
},
|
||||
});
|
||||
if (!result.data) {
|
||||
throw new Error("Price Table not found, contract may not be initialized");
|
||||
}
|
||||
return result.data.objectId;
|
||||
}
|
||||
|
||||
async getPriceFeed(feedId: string) {
|
||||
const tableId = await this.getPriceTableId();
|
||||
const provider = this.getProvider();
|
||||
let result = await provider.getDynamicFieldObject({
|
||||
parentId: tableId,
|
||||
name: {
|
||||
type: `${await this.getPythPackageId()}::price_identifier::PriceIdentifier`,
|
||||
value: {
|
||||
bytes: Array.from(Buffer.from(feedId, "hex")),
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result.data || !result.data.content) {
|
||||
throw new Error("Price feed not found");
|
||||
}
|
||||
if (result.data.content.dataType !== "moveObject") {
|
||||
throw new Error("Price feed type mismatch");
|
||||
}
|
||||
let priceInfoObjectId = result.data.content.fields.value;
|
||||
let priceInfo = await provider.getObject({
|
||||
id: priceInfoObjectId,
|
||||
options: { showContent: true },
|
||||
});
|
||||
if (!priceInfo.data || !priceInfo.data.content) {
|
||||
throw new Error(
|
||||
`Price feed ID ${priceInfoObjectId} in price table but object not found!!`
|
||||
);
|
||||
}
|
||||
if (priceInfo.data.content.dataType !== "moveObject") {
|
||||
throw new Error(
|
||||
`Expected ${priceInfoObjectId} to be a moveObject (PriceInfoObject)`
|
||||
);
|
||||
}
|
||||
return priceInfo.data.content.fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a signed VAA, execute the migration instruction on the pyth contract.
|
||||
* The payload of the VAA can be obtained from the `getUpgradePackagePayload` method.
|
||||
* @param vaa
|
||||
* @param keypair used to sign the transaction
|
||||
*/
|
||||
async executeMigrateInstruction(vaa: Buffer, keypair: Ed25519Keypair) {
|
||||
const tx = new TransactionBlock();
|
||||
const packageId = await this.getPythPackageId();
|
||||
let decreeReceipt = await this.getVaaDecreeReceipt(tx, packageId, vaa);
|
||||
|
||||
tx.moveCall({
|
||||
target: `${packageId}::migrate::migrate`,
|
||||
arguments: [tx.object(this.stateId), decreeReceipt],
|
||||
});
|
||||
|
||||
return this.executeTransaction(tx, keypair);
|
||||
}
|
||||
|
||||
getUpgradePackagePayload(digest: string): Buffer {
|
||||
let setFee = new SuiAuthorizeUpgradeContractInstruction(
|
||||
CHAINS["sui"],
|
||||
new HexString32Bytes(digest)
|
||||
).serialize();
|
||||
return this.wrapWithWormholeGovernancePayload(0, setFee);
|
||||
}
|
||||
|
||||
getSetUpdateFeePayload(fee: number): Buffer {
|
||||
let setFee = new SetFeeInstruction(
|
||||
CHAINS["sui"],
|
||||
BigInt(fee),
|
||||
BigInt(0)
|
||||
).serialize();
|
||||
return this.wrapWithWormholeGovernancePayload(3, setFee);
|
||||
}
|
||||
|
||||
async executeGovernanceInstruction(vaa: Buffer, keypair: Ed25519Keypair) {
|
||||
const tx = new TransactionBlock();
|
||||
const packageId = await this.getPythPackageId();
|
||||
let decreeReceipt = await this.getVaaDecreeReceipt(tx, packageId, vaa);
|
||||
|
||||
tx.moveCall({
|
||||
target: `${packageId}::governance::execute_governance_instruction`,
|
||||
arguments: [tx.object(this.stateId), decreeReceipt],
|
||||
});
|
||||
|
||||
return this.executeTransaction(tx, keypair);
|
||||
}
|
||||
|
||||
async executeUpgradeInstruction(
|
||||
vaa: Buffer,
|
||||
keypair: Ed25519Keypair,
|
||||
modules: number[][],
|
||||
dependencies: string[]
|
||||
) {
|
||||
const tx = new TransactionBlock();
|
||||
const packageId = await this.getPythPackageId();
|
||||
let decreeReceipt = await this.getVaaDecreeReceipt(tx, packageId, vaa);
|
||||
|
||||
const [upgradeTicket] = tx.moveCall({
|
||||
target: `${packageId}::contract_upgrade::authorize_upgrade`,
|
||||
arguments: [tx.object(this.stateId), decreeReceipt],
|
||||
});
|
||||
|
||||
const [upgradeReceipt] = tx.upgrade({
|
||||
modules,
|
||||
dependencies,
|
||||
packageId: packageId,
|
||||
ticket: upgradeTicket,
|
||||
});
|
||||
|
||||
tx.moveCall({
|
||||
target: `${packageId}::contract_upgrade::commit_upgrade`,
|
||||
arguments: [tx.object(this.stateId), upgradeReceipt],
|
||||
});
|
||||
return this.executeTransaction(tx, keypair);
|
||||
}
|
||||
|
||||
private wrapWithWormholeGovernancePayload(
|
||||
actionVariant: number,
|
||||
payload: Buffer
|
||||
): Buffer {
|
||||
const builder = new BufferBuilder();
|
||||
builder.addBuffer(
|
||||
Buffer.from(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"hex"
|
||||
)
|
||||
);
|
||||
builder.addUint8(actionVariant);
|
||||
builder.addUint16(CHAINS["sui"]); // should always be sui (21) no matter devnet or testnet
|
||||
builder.addBuffer(payload);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to get the decree receipt object for a VAA that can be
|
||||
* used to authorize a governance instruction.
|
||||
* @param tx
|
||||
* @param packageId pyth package id
|
||||
* @param vaa
|
||||
* @private
|
||||
*/
|
||||
private async getVaaDecreeReceipt(
|
||||
tx: TransactionBlock,
|
||||
packageId: string,
|
||||
vaa: Buffer
|
||||
) {
|
||||
const wormholePackageId = await this.getWormholePackageId();
|
||||
let [decreeTicket] = tx.moveCall({
|
||||
target: `${packageId}::set_update_fee::authorize_governance`,
|
||||
arguments: [tx.object(this.stateId), tx.pure(false)],
|
||||
});
|
||||
|
||||
let [verifiedVAA] = tx.moveCall({
|
||||
target: `${wormholePackageId}::vaa::parse_and_verify`,
|
||||
arguments: [
|
||||
tx.object(this.wormholeStateId),
|
||||
tx.pure(Array.from(vaa)),
|
||||
tx.object(SUI_CLOCK_OBJECT_ID),
|
||||
],
|
||||
});
|
||||
|
||||
let [decreeReceipt] = tx.moveCall({
|
||||
target: `${wormholePackageId}::governance_message::verify_vaa`,
|
||||
arguments: [tx.object(this.wormholeStateId), verifiedVAA, decreeTicket],
|
||||
typeArguments: [`${packageId}::governance_witness::GovernanceWitness`],
|
||||
});
|
||||
return decreeReceipt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a transaction block and a keypair, sign and execute it
|
||||
* Sets the gas budget to 2x the estimated gas cost
|
||||
* @param tx
|
||||
* @param keypair
|
||||
* @private
|
||||
*/
|
||||
private async executeTransaction(
|
||||
tx: TransactionBlock,
|
||||
keypair: Ed25519Keypair
|
||||
) {
|
||||
const provider = this.getProvider();
|
||||
let txBlock = {
|
||||
transactionBlock: tx,
|
||||
options: {
|
||||
showEffects: true,
|
||||
showEvents: true,
|
||||
},
|
||||
};
|
||||
const wallet = new RawSigner(keypair, provider);
|
||||
let gasCost = await wallet.getGasCostEstimation(txBlock);
|
||||
tx.setGasBudget(gasCost * BigInt(2));
|
||||
return wallet.signAndExecuteTransactionBlock(txBlock);
|
||||
}
|
||||
|
||||
async getValidTimePeriod() {
|
||||
const fields = await this.getStateFields();
|
||||
return Number(fields.stale_price_threshold);
|
||||
}
|
||||
|
||||
async getDataSources(): Promise<DataSource[]> {
|
||||
const provider = this.getProvider();
|
||||
let result = await provider.getDynamicFieldObject({
|
||||
parentId: this.stateId,
|
||||
name: {
|
||||
type: "vector<u8>",
|
||||
value: "data_sources",
|
||||
},
|
||||
});
|
||||
if (!result.data || !result.data.content) {
|
||||
throw new Error(
|
||||
"Data Sources not found, contract may not be initialized"
|
||||
);
|
||||
}
|
||||
if (result.data.content.dataType !== "moveObject") {
|
||||
throw new Error("Data Sources type mismatch");
|
||||
}
|
||||
return result.data.content.fields.value.fields.keys.map(
|
||||
({ fields }: any) => {
|
||||
return new DataSource(
|
||||
Number(fields.emitter_chain),
|
||||
new HexString32Bytes(
|
||||
Buffer.from(
|
||||
fields.emitter_address.fields.value.fields.data
|
||||
).toString("hex")
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async getGovernanceDataSource(): Promise<DataSource> {
|
||||
const fields = await this.getStateFields();
|
||||
const governanceFields = fields.governance_data_source.fields;
|
||||
const chainId = governanceFields.emitter_chain;
|
||||
const emitterAddress =
|
||||
governanceFields.emitter_address.fields.value.fields.data;
|
||||
return new DataSource(
|
||||
Number(chainId),
|
||||
new HexString32Bytes(Buffer.from(emitterAddress).toString("hex"))
|
||||
);
|
||||
}
|
||||
|
||||
private getProvider() {
|
||||
return new JsonRpcProvider(new Connection({ fullnode: this.chain.rpcUrl }));
|
||||
}
|
||||
|
||||
private async getStateFields() {
|
||||
const provider = this.getProvider();
|
||||
const result = await provider.getObject({
|
||||
id: this.stateId,
|
||||
options: { showContent: true },
|
||||
});
|
||||
if (
|
||||
!result.data ||
|
||||
!result.data.content ||
|
||||
result.data.content.dataType !== "moveObject"
|
||||
)
|
||||
throw new Error("Unable to fetch pyth state object");
|
||||
return result.data.content.fields;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import {
|
||||
Vault,
|
||||
Contracts,
|
||||
Vaults,
|
||||
loadHotWallet,
|
||||
WormholeEmitter,
|
||||
} from "./entities";
|
||||
import { SuiContract } from "./sui";
|
||||
import { CosmWasmContract } from "./cosmwasm";
|
||||
import { Ed25519Keypair, RawSigner } from "@mysten/sui.js";
|
||||
import { DefaultStore } from "./store";
|
||||
import { Chains } from "./chains";
|
||||
|
||||
async function test() {
|
||||
// Deploy the same cosmwasm code with different config
|
||||
|
||||
// let c = Contracts.osmosis_testnet_5_osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3 as CosmWasmContract;
|
||||
// let old_conf = await c.getConfig();
|
||||
// let config = CosmWasmContract.getDeploymentConfig(c.chain, 'edge', old_conf.config_v1.wormhole_contract);
|
||||
// console.log(config);
|
||||
// config.governance_source.emitter = wallet.publicKey.toBuffer().toString('base64');
|
||||
// let mnemonic = 'FILLME'
|
||||
// console.log(await CosmWasmContract.deploy(c.chain, await c.getCodeId(), config, mnemonic));
|
||||
|
||||
let s = DefaultStore;
|
||||
Object.values(Contracts).forEach((c) => {
|
||||
console.log(c);
|
||||
s.save(c);
|
||||
});
|
||||
|
||||
Object.values(Chains).forEach((c) => {
|
||||
console.log(c);
|
||||
s.save(c);
|
||||
});
|
||||
|
||||
// Execute some governance instruction on sui contract
|
||||
|
||||
// let c = Contracts.sui_testnet_0x651dcb84d579fcdf51f15d79eb28f7e10b416c9202b6a156495bb1a4aecd55ea as SuiContract
|
||||
// let wallet = await loadHotWallet('/tmp/priv.json');
|
||||
// let emitter = new WormholeEmitter("devnet", wallet);
|
||||
// let proposal = c.setUpdateFee(200);
|
||||
// let submittedWormholeMessage = await emitter.sendMessage(proposal);
|
||||
// let vaa = await submittedWormholeMessage.fetchVAA(10);
|
||||
// const keypair = Ed25519Keypair.fromSecretKey(Buffer.from('FILLME', "hex"));
|
||||
// await c.executeGovernanceInstruction(vaa);
|
||||
}
|
||||
|
||||
test();
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"querierEndpoint": "https://rpc.uni.junonetwork.io/",
|
||||
"executorEndpoint": "https://rpc.uni.junonetwork.io/",
|
||||
"id": "juno_testnet",
|
||||
"gasPrice": "0.025",
|
||||
"prefix": "juno",
|
||||
"feeDenom": "ujunox",
|
||||
"type": "CosmWasmChain"
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"querierEndpoint": "https://rpc-kralum.neutron-1.neutron.org",
|
||||
"executorEndpoint": "https://rpc-kralum.neutron-1.neutron.org",
|
||||
"id": "neutron",
|
||||
"gasPrice": "0.025",
|
||||
"prefix": "neutron",
|
||||
"feeDenom": "untrn",
|
||||
"type": "CosmWasmChain"
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"querierEndpoint": "https://rpc.pion.rs-testnet.polypore.xyz/",
|
||||
"executorEndpoint": "https://rpc.pion.rs-testnet.polypore.xyz/",
|
||||
"id": "neutron_testnet_pion_1",
|
||||
"gasPrice": "0.05",
|
||||
"prefix": "neutron",
|
||||
"feeDenom": "untrn",
|
||||
"type": "CosmWasmChain"
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"querierEndpoint": "https://rpc.osmotest5.osmosis.zone/",
|
||||
"executorEndpoint": "https://rpc.osmotest5.osmosis.zone/",
|
||||
"id": "osmosis_testnet_5",
|
||||
"gasPrice": "0.025",
|
||||
"prefix": "osmo",
|
||||
"feeDenom": "uosmo",
|
||||
"type": "CosmWasmChain"
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"querierEndpoint": "https://sei-rpc.polkachu.com",
|
||||
"executorEndpoint": "https://sei-rpc.polkachu.com",
|
||||
"id": "sei_pacific_1",
|
||||
"gasPrice": "0.025",
|
||||
"prefix": "sei",
|
||||
"feeDenom": "usei",
|
||||
"type": "CosmWasmChain"
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"querierEndpoint": "https://rpc.atlantic-2.seinetwork.io/",
|
||||
"executorEndpoint": "https://rpc.atlantic-2.seinetwork.io/",
|
||||
"id": "sei_testnet_atlantic_2",
|
||||
"gasPrice": "0.01",
|
||||
"prefix": "sei",
|
||||
"feeDenom": "usei",
|
||||
"type": "CosmWasmChain"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "arbitrum_testnet",
|
||||
"rpcUrl": "https://goerli-rollup.arbitrum.io/rpc",
|
||||
"type": "EVMChain"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "cronos",
|
||||
"rpcUrl": "https://cronosrpc-1.xstaking.sg",
|
||||
"type": "EVMChain"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "cronos_testnet",
|
||||
"rpcUrl": "https://evm-t3.cronos.org",
|
||||
"type": "EVMChain"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "sui_devnet",
|
||||
"rpcUrl": "https://fullnode.devnet.sui.io:443",
|
||||
"type": "SuiChain"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "sui_mainnet",
|
||||
"rpcUrl": "https://fullnode.mainnet.sui.io:443",
|
||||
"type": "SuiChain"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "sui_testnet",
|
||||
"rpcUrl": "https://fullnode.testnet.sui.io:443",
|
||||
"type": "SuiChain"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"chain": "juno_testnet",
|
||||
"address": "juno1h93q9kwlnfml2gum4zj54al9w4jdmuhtzrh6vhycnemsqlqv9l9snnznxs",
|
||||
"type": "CosmWasmContract"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"chain": "neutron",
|
||||
"address": "neutron1m2emc93m9gpwgsrsf2vylv9xvgqh654630v7dfrhrkmr5slly53spg85wv",
|
||||
"type": "CosmWasmContract"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"chain": "neutron_testnet_pion_1",
|
||||
"address": "neutron1xxmcu6wxgawjlajx8jalyk9cxsudnygjg0tvjesfyurh4utvtpes5wmpjp",
|
||||
"type": "CosmWasmContract"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"chain": "osmosis_testnet_5",
|
||||
"address": "osmo1lltupx02sj99suakmuk4sr4ppqf34ajedaxut3ukjwkv6469erwqtpg9t3",
|
||||
"type": "CosmWasmContract"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"chain": "sei_testnet_atlantic_2",
|
||||
"address": "sei1w2rxq6eckak47s25crxlhmq96fzjwdtjgdwavn56ggc0qvxvw7rqczxyfy",
|
||||
"type": "CosmWasmContract"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"chain": "cronos",
|
||||
"address": "0xe0d0e68297772dd5a1f1d99897c581e2082dba5b",
|
||||
"type": "EVMContract"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"chain": "cronos_testnet",
|
||||
"address": "0xFF125F377F9F7631a05f4B01CeD32a6A2ab843C7",
|
||||
"type": "EVMContract"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"chain": "sui_mainnet",
|
||||
"stateId": "0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f",
|
||||
"wormholeStateId": "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c",
|
||||
"type": "SuiContract"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"chain": "sui_testnet",
|
||||
"stateId": "0xb3142a723792001caafc601b7c6fe38f09f3684e360b56d8d90fc574e71e75f3",
|
||||
"wormholeStateId": "0xebba4cc4d614f7a7cdbe883acc76d1cc767922bc96778e7b68be0d15fce27c02",
|
||||
"type": "SuiContract"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"chain": "sui_testnet",
|
||||
"stateId": "0xe8c2ddcd5b10e8ed98e53b12fcf8f0f6fd9315f810ae61fa4001858851f21c88",
|
||||
"wormholeStateId": "0xebba4cc4d614f7a7cdbe883acc76d1cc767922bc96778e7b68be0d15fce27c02",
|
||||
"type": "SuiContract"
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"rootDir": "src/",
|
||||
"outDir": "./lib"
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
export {
|
||||
DataSource,
|
||||
AptosAuthorizeUpgradeContractInstruction,
|
||||
SuiAuthorizeUpgradeContractInstruction,
|
||||
EthereumUpgradeContractInstruction,
|
||||
EthereumSetWormholeAddress,
|
||||
HexString20Bytes,
|
||||
|
|
|
@ -111,6 +111,16 @@ export class AptosAuthorizeUpgradeContractInstruction extends TargetInstruction
|
|||
}
|
||||
}
|
||||
|
||||
export class SuiAuthorizeUpgradeContractInstruction extends TargetInstruction {
|
||||
constructor(targetChainId: ChainId, private digest: HexString32Bytes) {
|
||||
super(TargetAction.UpgradeContract, targetChainId);
|
||||
}
|
||||
|
||||
protected serializePayload(): Buffer {
|
||||
return this.digest.serialize();
|
||||
}
|
||||
}
|
||||
|
||||
export class EthereumUpgradeContractInstruction extends TargetInstruction {
|
||||
constructor(targetChainId: ChainId, private address: HexString20Bytes) {
|
||||
super(TargetAction.UpgradeContract, targetChainId);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "@pythnetwork/cosmwasm-deploy-tools",
|
||||
"version": "1.1.0",
|
||||
"description": "",
|
||||
"main": "deploy-pyth-bridge.ts",
|
||||
"main": "./src/index.ts",
|
||||
"private": "true",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./cosmwasm";
|
||||
export * from "./injective";
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./deployer";
|
||||
export * from "./chains-manager";
|
||||
export * from "./pyth-wrapper";
|
|
@ -1,16 +0,0 @@
|
|||
[
|
||||
{
|
||||
"contractName": "Migrations",
|
||||
"address": "0x1c6Cd107fB71768FBc46F8B6180Eec155C03eEb5"
|
||||
},
|
||||
{
|
||||
"contractName": "WormholeReceiver",
|
||||
"address": "0x15D35b8985e350f783fe3d95401401E194ff1E6f",
|
||||
"transactionHash": "0x1c33d9b6971f7337e0e2ea390affe18fe90709dcb803712f6d8bb4a008705fb7"
|
||||
},
|
||||
{
|
||||
"contractName": "PythUpgradable",
|
||||
"address": "0xBAEA4A1A2Eaa4E9bb78f2303C213Da152933170E",
|
||||
"transactionHash": "0x507d747b3c978794cc880a201c009d37367f66925b930c0cebc30c493c9d31eb"
|
||||
}
|
||||
]
|
Loading…
Reference in New Issue