cosmwasm.ts 12 KB

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