cosmwasm.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import { Chain, CosmWasmChain } from "../chains";
  2. import { readFileSync } from "fs";
  3. import {
  4. ContractInfoResponse,
  5. CosmwasmQuerier,
  6. Price,
  7. PythWrapperExecutor,
  8. PythWrapperQuerier,
  9. } from "@pythnetwork/cosmwasm-deploy-tools";
  10. import { Coin } from "@cosmjs/stargate";
  11. import { DataSource } from "xc_admin_common";
  12. import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";
  13. import {
  14. PriceFeedContract,
  15. getDefaultDeploymentConfig,
  16. PrivateKey,
  17. TxResult,
  18. } from "../base";
  19. import { WormholeContract } from "./wormhole";
  20. import { TokenQty } from "../token";
  21. /**
  22. * Variables here need to be snake case to match the on-chain contract configs
  23. */
  24. export interface WormholeSource {
  25. emitter: string;
  26. chain_id: number;
  27. }
  28. export interface DeploymentConfig {
  29. data_sources: WormholeSource[];
  30. governance_source: WormholeSource;
  31. wormhole_contract: string;
  32. governance_source_index: number;
  33. governance_sequence_number: number;
  34. chain_id: number;
  35. valid_time_period_secs: number;
  36. fee: { amount: string; denom: string };
  37. }
  38. export class WormholeCosmWasmContract extends WormholeContract {
  39. constructor(public chain: CosmWasmChain, public address: string) {
  40. super();
  41. }
  42. async getConfig() {
  43. const chainQuerier = await CosmwasmQuerier.connect(this.chain.endpoint);
  44. return (await chainQuerier.getAllContractState({
  45. contractAddr: this.address,
  46. })) as Record<string, string>;
  47. }
  48. async getCurrentGuardianSetIndex(): Promise<number> {
  49. const config = await this.getConfig();
  50. return JSON.parse(config["\x00\x06config"])["guardian_set_index"];
  51. }
  52. async getChainId(): Promise<number> {
  53. const config = await this.getConfig();
  54. return JSON.parse(config["\x00\x06config"])["chain_id"];
  55. }
  56. async getGuardianSet(): Promise<string[]> {
  57. const config = await this.getConfig();
  58. const guardianSetIndex = JSON.parse(config["\x00\x06config"])[
  59. "guardian_set_index"
  60. ];
  61. let key = "\x00\fguardian_set";
  62. //append guardianSetIndex as 4 bytes to key string
  63. key += Buffer.from(guardianSetIndex.toString(16).padStart(8, "0"), "hex");
  64. const guardianSet = JSON.parse(config[key])["addresses"];
  65. return guardianSet.map((entry: { bytes: string }) =>
  66. Buffer.from(entry.bytes, "base64").toString("hex")
  67. );
  68. }
  69. async upgradeGuardianSets(
  70. senderPrivateKey: PrivateKey,
  71. vaa: Buffer
  72. ): Promise<TxResult> {
  73. const executor = await this.chain.getExecutor(senderPrivateKey);
  74. const result = await executor.executeContract({
  75. contractAddr: this.address,
  76. msg: {
  77. submit_v_a_a: { vaa: vaa.toString("base64") },
  78. },
  79. });
  80. return { id: result.txHash, info: result };
  81. }
  82. }
  83. export class CosmWasmPriceFeedContract extends PriceFeedContract {
  84. static type = "CosmWasmPriceFeedContract";
  85. async getDataSources(): Promise<DataSource[]> {
  86. const config = await this.getConfig();
  87. return config.config_v1.data_sources.map(
  88. ({ emitter, chain_id }: WormholeSource) => {
  89. return {
  90. emitterChain: Number(chain_id),
  91. emitterAddress: Buffer.from(emitter, "base64").toString("hex"),
  92. };
  93. }
  94. );
  95. }
  96. async getGovernanceDataSource(): Promise<DataSource> {
  97. const config = await this.getConfig();
  98. const { emitter: emitterAddress, chain_id: chainId } =
  99. config.config_v1.governance_source;
  100. return {
  101. emitterChain: Number(chainId),
  102. emitterAddress: Buffer.from(emitterAddress, "base64").toString("hex"),
  103. };
  104. }
  105. constructor(public chain: CosmWasmChain, public address: string) {
  106. super();
  107. }
  108. static fromJson(
  109. chain: Chain,
  110. parsed: { type: string; address: string }
  111. ): CosmWasmPriceFeedContract {
  112. if (parsed.type !== CosmWasmPriceFeedContract.type)
  113. throw new Error("Invalid type");
  114. if (!(chain instanceof CosmWasmChain))
  115. throw new Error(`Wrong chain type ${chain}`);
  116. return new CosmWasmPriceFeedContract(chain, parsed.address);
  117. }
  118. getType(): string {
  119. return CosmWasmPriceFeedContract.type;
  120. }
  121. /**
  122. * Stores the wasm code on the specified chain using the provided private key as the signer
  123. * You can find the wasm artifacts from the repo releases
  124. * @param chain chain to store the code on
  125. * @param privateKey private key to use for signing the transaction in hex format without 0x prefix
  126. * @param wasmPath path in your local filesystem to the wasm artifact
  127. */
  128. static async storeCode(
  129. chain: CosmWasmChain,
  130. privateKey: PrivateKey,
  131. wasmPath: string
  132. ) {
  133. const contractBytes = readFileSync(wasmPath);
  134. const executor = await chain.getExecutor(privateKey);
  135. return executor.storeCode({ contractBytes });
  136. }
  137. /**
  138. * Deploys a new contract to the specified chain using the uploaded wasm code codeId
  139. * @param chain chain to deploy to
  140. * @param codeId codeId of the uploaded wasm code. You can get this from the storeCode result
  141. * @param config deployment config for initializing the contract (data sources, governance source, etc)
  142. * @param privateKey private key to use for signing the transaction in hex format without 0x prefix
  143. */
  144. static async initialize(
  145. chain: CosmWasmChain,
  146. codeId: number,
  147. config: DeploymentConfig,
  148. privateKey: PrivateKey
  149. ): Promise<CosmWasmPriceFeedContract> {
  150. const executor = await chain.getExecutor(privateKey);
  151. const result = await executor.instantiateContract({
  152. codeId: codeId,
  153. instMsg: config,
  154. label: "pyth",
  155. });
  156. await executor.updateContractAdmin({
  157. newAdminAddr: result.contractAddr,
  158. contractAddr: result.contractAddr,
  159. });
  160. return new CosmWasmPriceFeedContract(chain, result.contractAddr);
  161. }
  162. getId(): string {
  163. return `${this.chain.getId()}_${this.address}`;
  164. }
  165. toJson() {
  166. return {
  167. chain: this.chain.getId(),
  168. address: this.address,
  169. type: CosmWasmPriceFeedContract.type,
  170. };
  171. }
  172. async getQuerier(): Promise<PythWrapperQuerier> {
  173. const chainQuerier = await CosmwasmQuerier.connect(this.chain.endpoint);
  174. const pythQuerier = new PythWrapperQuerier(chainQuerier);
  175. return pythQuerier;
  176. }
  177. async getCodeId(): Promise<number> {
  178. const result = await this.getWasmContractInfo();
  179. return result.codeId;
  180. }
  181. async getWasmContractInfo(): Promise<ContractInfoResponse> {
  182. const chainQuerier = await CosmwasmQuerier.connect(this.chain.endpoint);
  183. return chainQuerier.getContractInfo({ contractAddr: this.address });
  184. }
  185. async getConfig() {
  186. const chainQuerier = await CosmwasmQuerier.connect(this.chain.endpoint);
  187. const allStates = (await chainQuerier.getAllContractState({
  188. contractAddr: this.address,
  189. })) as Record<string, string>;
  190. const config = {
  191. config_v1: JSON.parse(allStates["\x00\tconfig_v1"]),
  192. contract_version: allStates["\x00\x10contract_version"]
  193. ? JSON.parse(allStates["\x00\x10contract_version"])
  194. : undefined,
  195. };
  196. return config;
  197. }
  198. async getLastExecutedGovernanceSequence() {
  199. const config = await this.getConfig();
  200. return Number(config.config_v1.governance_sequence_number);
  201. }
  202. private parsePrice(priceInfo: Price) {
  203. return {
  204. conf: priceInfo.conf.toString(),
  205. publishTime: priceInfo.publish_time.toString(),
  206. expo: priceInfo.expo.toString(),
  207. price: priceInfo.price.toString(),
  208. };
  209. }
  210. async getPriceFeed(feedId: string) {
  211. const querier = await this.getQuerier();
  212. try {
  213. const response = await querier.getPriceFeed(this.address, feedId);
  214. return {
  215. price: this.parsePrice(response.price),
  216. emaPrice: this.parsePrice(response.ema_price),
  217. };
  218. } catch (e) {
  219. return undefined;
  220. }
  221. }
  222. equalDataSources(
  223. dataSources1: WormholeSource[],
  224. dataSources2: WormholeSource[]
  225. ): boolean {
  226. if (dataSources1.length !== dataSources2.length) return false;
  227. for (let i = 0; i < dataSources1.length; i++) {
  228. let found = false;
  229. for (let j = 0; j < dataSources2.length; j++) {
  230. if (
  231. dataSources1[i].emitter === dataSources2[j].emitter &&
  232. dataSources1[i].chain_id === dataSources2[j].chain_id
  233. ) {
  234. found = true;
  235. break;
  236. }
  237. }
  238. if (!found) return false;
  239. }
  240. return true;
  241. }
  242. async getDeploymentType(): Promise<string> {
  243. const config = await this.getConfig();
  244. const convertDataSource = (source: DataSource) => {
  245. return {
  246. emitter: Buffer.from(source.emitterAddress, "hex").toString("base64"),
  247. chain_id: source.emitterChain,
  248. };
  249. };
  250. const stableDataSources =
  251. getDefaultDeploymentConfig("stable").dataSources.map(convertDataSource);
  252. const betaDataSources =
  253. getDefaultDeploymentConfig("beta").dataSources.map(convertDataSource);
  254. if (this.equalDataSources(config.config_v1.data_sources, stableDataSources))
  255. return "stable";
  256. else if (
  257. this.equalDataSources(config.config_v1.data_sources, betaDataSources)
  258. )
  259. return "beta";
  260. else return "unknown";
  261. }
  262. async executeUpdatePriceFeed(senderPrivateKey: PrivateKey, vaas: Buffer[]) {
  263. const base64Vaas = vaas.map((v) => v.toString("base64"));
  264. const fund = await this.getUpdateFee(base64Vaas);
  265. const executor = await this.chain.getExecutor(senderPrivateKey);
  266. const pythExecutor = new PythWrapperExecutor(executor);
  267. const result = await pythExecutor.executeUpdatePriceFeeds({
  268. contractAddr: this.address,
  269. vaas: base64Vaas,
  270. fund,
  271. });
  272. return { id: result.txHash, info: result };
  273. }
  274. async executeGovernanceInstruction(privateKey: PrivateKey, vaa: Buffer) {
  275. const executor = await this.chain.getExecutor(privateKey);
  276. const pythExecutor = new PythWrapperExecutor(executor);
  277. const result = await pythExecutor.executeGovernanceInstruction({
  278. contractAddr: this.address,
  279. vaa: vaa.toString("base64"),
  280. });
  281. return { id: result.txHash, info: result };
  282. }
  283. async getWormholeContract(): Promise<WormholeCosmWasmContract> {
  284. const config = await this.getConfig();
  285. const wormholeAddress = config.config_v1.wormhole_contract;
  286. return new WormholeCosmWasmContract(this.chain, wormholeAddress);
  287. }
  288. async getUpdateFee(msgs: string[]): Promise<Coin> {
  289. const querier = await this.getQuerier();
  290. return querier.getUpdateFee(this.address, msgs);
  291. }
  292. async getBaseUpdateFee(): Promise<{ amount: string; denom: string }> {
  293. const config = await this.getConfig();
  294. return config.config_v1.fee;
  295. }
  296. async getVersion(): Promise<string> {
  297. const config = await this.getConfig();
  298. return config.contract_version;
  299. }
  300. getChain(): CosmWasmChain {
  301. return this.chain;
  302. }
  303. async getTotalFee(): Promise<TokenQty> {
  304. const client = await CosmWasmClient.connect(this.chain.endpoint);
  305. const coin = await client.getBalance(
  306. this.address,
  307. this.getChain().feeDenom
  308. );
  309. return {
  310. amount: BigInt(coin.amount),
  311. denom: this.chain.getNativeToken(),
  312. };
  313. }
  314. async getValidTimePeriod() {
  315. const client = await CosmWasmClient.connect(this.chain.endpoint);
  316. const result = await client.queryContractSmart(
  317. this.address,
  318. "get_valid_time_period"
  319. );
  320. return Number(result.secs + result.nanos * 1e-9);
  321. }
  322. }