pyth-crosschain/contract_manager/src/contracts/cosmwasm.ts

394 lines
12 KiB
TypeScript

import { Chain, CosmWasmChain } from "../chains";
import { readFileSync } from "fs";
import {
ContractInfoResponse,
CosmwasmQuerier,
DeploymentType,
getPythConfig,
Price,
PythWrapperExecutor,
PythWrapperQuerier,
} from "@pythnetwork/cosmwasm-deploy-tools";
import { Coin } from "@cosmjs/stargate";
import { CHAINS, DataSource } from "xc_admin_common";
import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { Contract, PrivateKey, TxResult } from "../base";
import { WormholeContract } from "./wormhole";
/**
* Variables here need to be snake case to match the on-chain contract configs
*/
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 WormholeCosmWasmContract extends WormholeContract {
constructor(public chain: CosmWasmChain, public address: string) {
super();
}
async getConfig() {
const chainQuerier = await CosmwasmQuerier.connect(this.chain.endpoint);
return (await chainQuerier.getAllContractState({
contractAddr: this.address,
})) as Record<string, string>;
}
async getCurrentGuardianSetIndex(): Promise<number> {
const config = await this.getConfig();
return JSON.parse(config["\x00\x06config"])["guardian_set_index"];
}
async getGuardianSet(): Promise<string[]> {
const config = await this.getConfig();
const guardianSetIndex = JSON.parse(config["\x00\x06config"])[
"guardian_set_index"
];
let key = "\x00\fguardian_set";
//append guardianSetIndex as 4 bytes to key string
key += Buffer.from(guardianSetIndex.toString(16).padStart(8, "0"), "hex");
const guardianSet = JSON.parse(config[key])["addresses"];
return guardianSet.map((entry: { bytes: string }) =>
Buffer.from(entry.bytes, "base64").toString("hex")
);
}
async upgradeGuardianSets(
senderPrivateKey: PrivateKey,
vaa: Buffer
): Promise<TxResult> {
const executor = await this.chain.getExecutor(senderPrivateKey);
const result = await executor.executeContract({
contractAddr: this.address,
msg: {
submit_v_a_a: { vaa: vaa.toString("base64") },
},
});
return { id: result.txHash, info: result };
}
}
export class CosmWasmContract extends Contract {
async getDataSources(): Promise<DataSource[]> {
const config = await this.getConfig();
return config.config_v1.data_sources.map(
({ emitter, chain_id }: { emitter: string; chain_id: string }) => {
return {
emitterChain: Number(chain_id),
emitterAddress: 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 {
emitterChain: Number(chainId),
emitterAddress: Buffer.from(emitterAddress, "base64").toString("hex"),
};
}
static type = "CosmWasmContract";
constructor(public chain: CosmWasmChain, public address: string) {
super();
}
static fromJson(
chain: Chain,
parsed: { type: string; address: string }
): CosmWasmContract {
if (parsed.type !== CosmWasmContract.type) throw new Error("Invalid type");
if (!(chain instanceof CosmWasmChain))
throw new Error(`Wrong chain type ${chain}`);
return new CosmWasmContract(chain, parsed.address);
}
getType(): string {
return CosmWasmContract.type;
}
static getDeploymentConfig(
chain: CosmWasmChain,
deploymentType: DeploymentType,
wormholeContract: string
): DeploymentConfig {
return getPythConfig({
feeDenom: chain.feeDenom,
wormholeChainId: CHAINS[chain.wormholeChainName],
wormholeContract,
deploymentType: deploymentType,
});
}
/**
* Stores the wasm code on the specified chain using the provided private key as the signer
* You can find the wasm artifacts from the repo releases
* @param chain chain to store the code on
* @param privateKey private key to use for signing the transaction in hex format without 0x prefix
* @param wasmPath path in your local filesystem to the wasm artifact
*/
static async storeCode(
chain: CosmWasmChain,
privateKey: PrivateKey,
wasmPath: string
) {
const contractBytes = readFileSync(wasmPath);
const executor = await chain.getExecutor(privateKey);
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 privateKey private key to use for signing the transaction in hex format without 0x prefix
*/
static async initialize(
chain: CosmWasmChain,
codeId: number,
config: DeploymentConfig,
privateKey: PrivateKey
): Promise<CosmWasmContract> {
const executor = await chain.getExecutor(privateKey);
const 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 privateKey private key to use for signing the transaction in hex format without 0x prefix
* @param wasmPath
*/
static async deploy(
chain: CosmWasmChain,
wormholeContract: string,
privateKey: PrivateKey,
wasmPath: string
): Promise<CosmWasmContract> {
const config = this.getDeploymentConfig(chain, "beta", wormholeContract);
const { codeId } = await this.storeCode(chain, privateKey, wasmPath);
return this.initialize(chain, codeId, config, privateKey);
}
getId(): string {
return `${this.chain.getId()}_${this.address}`;
}
toJson() {
return {
chain: this.chain.getId(),
address: this.address,
type: CosmWasmContract.type,
};
}
async getQuerier(): Promise<PythWrapperQuerier> {
const chainQuerier = await CosmwasmQuerier.connect(this.chain.endpoint);
const pythQuerier = new PythWrapperQuerier(chainQuerier);
return pythQuerier;
}
async getCodeId(): Promise<number> {
const result = await this.getWasmContractInfo();
return result.codeId;
}
async getWasmContractInfo(): Promise<ContractInfoResponse> {
const chainQuerier = await CosmwasmQuerier.connect(this.chain.endpoint);
return chainQuerier.getContractInfo({ contractAddr: this.address });
}
async getConfig() {
const chainQuerier = await CosmwasmQuerier.connect(this.chain.endpoint);
const allStates = (await chainQuerier.getAllContractState({
contractAddr: this.address,
})) as Record<string, string>;
const config = {
config_v1: JSON.parse(allStates["\x00\tconfig_v1"]),
contract_version: JSON.parse(allStates["\x00\x10contract_version"]),
};
return config;
}
async getLastExecutedGovernanceSequence() {
const config = await this.getConfig();
return Number(config.config_v1.governance_sequence_number);
}
// TODO: function for upgrading the contract
// TODO: Cleanup and more strict linter to convert let to const
private parsePrice(priceInfo: Price) {
return {
conf: priceInfo.conf.toString(),
publishTime: priceInfo.publish_time.toString(),
expo: priceInfo.expo.toString(),
price: priceInfo.price.toString(),
};
}
async getPriceFeed(feedId: string) {
const querier = await this.getQuerier();
try {
const response = await querier.getPriceFeed(this.address, feedId);
return {
price: this.parsePrice(response.price),
emaPrice: this.parsePrice(response.ema_price),
};
} catch (e) {
return undefined;
}
}
equalDataSources(
dataSources1: WormholeSource[],
dataSources2: 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> {
const config = await this.getConfig();
const wormholeContract = config.config_v1.wormhole_contract;
const stableConfig = getPythConfig({
feeDenom: this.chain.feeDenom,
wormholeChainId: CHAINS[this.chain.getId() as keyof typeof CHAINS],
wormholeContract,
deploymentType: "stable",
});
const betaConfig = getPythConfig({
feeDenom: this.chain.feeDenom,
wormholeChainId: CHAINS[this.chain.getId() as keyof typeof CHAINS],
wormholeContract,
deploymentType: "beta",
});
if (
this.equalDataSources(
config.config_v1.data_sources,
stableConfig.data_sources
)
)
return "stable";
else if (
this.equalDataSources(
config.config_v1.data_sources,
betaConfig.data_sources
)
)
return "beta";
else return "unknown";
}
async executeUpdatePriceFeed(senderPrivateKey: PrivateKey, vaas: Buffer[]) {
const base64Vaas = vaas.map((v) => v.toString("base64"));
const fund = await this.getUpdateFee(base64Vaas);
const executor = await this.chain.getExecutor(senderPrivateKey);
const pythExecutor = new PythWrapperExecutor(executor);
const result = await pythExecutor.executeUpdatePriceFeeds({
contractAddr: this.address,
vaas: base64Vaas,
fund,
});
return { id: result.txHash, info: result };
}
async executeGovernanceInstruction(privateKey: PrivateKey, vaa: Buffer) {
const executor = await this.chain.getExecutor(privateKey);
const pythExecutor = new PythWrapperExecutor(executor);
const result = await pythExecutor.executeGovernanceInstruction({
contractAddr: this.address,
vaa: vaa.toString("base64"),
});
return { id: result.txHash, info: result };
}
async getWormholeContract(): Promise<WormholeCosmWasmContract> {
const config = await this.getConfig();
const wormholeAddress = config.config_v1.wormhole_contract;
return new WormholeCosmWasmContract(this.chain, wormholeAddress);
}
async getUpdateFee(msgs: string[]): Promise<Coin> {
const querier = await this.getQuerier();
return querier.getUpdateFee(this.address, msgs);
}
async getBaseUpdateFee(): Promise<{ amount: string; denom: string }> {
const config = await this.getConfig();
return config.config_v1.fee;
}
async getVersion(): Promise<string> {
const config = await this.getConfig();
return config.contract_version;
}
getChain(): CosmWasmChain {
return this.chain;
}
async getTotalFee(): Promise<bigint> {
const client = await CosmWasmClient.connect(this.chain.endpoint);
const coin = await client.getBalance(
this.address,
this.getChain().feeDenom
);
return BigInt(coin.amount);
}
async getValidTimePeriod() {
const client = await CosmWasmClient.connect(this.chain.endpoint);
const result = await client.queryContractSmart(
this.address,
"get_valid_time_period"
);
return Number(result.secs + result.nanos * 1e-9);
}
}