sui.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. import { Chain, SuiChain } from "../chains";
  2. import { DataSource } from "@pythnetwork/xc-admin-common";
  3. import { WormholeContract } from "./wormhole";
  4. import { PriceFeedContract, PrivateKey, TxResult } from "../base";
  5. import { SuiPythClient } from "@pythnetwork/pyth-sui-js";
  6. import { SUI_CLOCK_OBJECT_ID } from "@mysten/sui/utils";
  7. import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
  8. import { Transaction } from "@mysten/sui/transactions";
  9. import { uint8ArrayToBCS } from "@certusone/wormhole-sdk/lib/cjs/sui";
  10. type ObjectId = string;
  11. export class SuiPriceFeedContract extends PriceFeedContract {
  12. static type = "SuiPriceFeedContract";
  13. private client: SuiPythClient;
  14. /**
  15. * Given the ids of the pyth state and wormhole state, create a new SuiPriceFeedContract
  16. * The package ids are derived based on the state ids
  17. *
  18. * @param chain the chain which this contract is deployed on
  19. * @param stateId id of the pyth state for the deployed contract
  20. * @param wormholeStateId id of the wormhole state for the wormhole contract that pyth binds to
  21. */
  22. constructor(
  23. public chain: SuiChain,
  24. public stateId: string,
  25. public wormholeStateId: string
  26. ) {
  27. super();
  28. this.client = new SuiPythClient(
  29. this.getProvider(),
  30. this.stateId,
  31. this.wormholeStateId
  32. );
  33. }
  34. static fromJson(
  35. chain: Chain,
  36. parsed: { type: string; stateId: string; wormholeStateId: string }
  37. ): SuiPriceFeedContract {
  38. if (parsed.type !== SuiPriceFeedContract.type)
  39. throw new Error("Invalid type");
  40. if (!(chain instanceof SuiChain))
  41. throw new Error(`Wrong chain type ${chain}`);
  42. return new SuiPriceFeedContract(
  43. chain,
  44. parsed.stateId,
  45. parsed.wormholeStateId
  46. );
  47. }
  48. getType(): string {
  49. return SuiPriceFeedContract.type;
  50. }
  51. getChain(): SuiChain {
  52. return this.chain;
  53. }
  54. toJson() {
  55. return {
  56. chain: this.chain.getId(),
  57. stateId: this.stateId,
  58. wormholeStateId: this.wormholeStateId,
  59. type: SuiPriceFeedContract.type,
  60. };
  61. }
  62. /**
  63. * Given a objectId, returns the id for the package that the object belongs to.
  64. * @param objectId
  65. */
  66. async getPackageId(objectId: ObjectId): Promise<ObjectId> {
  67. return this.client.getPackageId(objectId);
  68. }
  69. async getPythPackageId(): Promise<ObjectId> {
  70. return await this.getPackageId(this.stateId);
  71. }
  72. async getWormholePackageId(): Promise<ObjectId> {
  73. return await this.getPackageId(this.wormholeStateId);
  74. }
  75. getId(): string {
  76. return `${this.chain.getId()}_${this.stateId}`;
  77. }
  78. private async parsePrice(priceInfo: {
  79. type: string;
  80. fields: {
  81. expo: { fields: { magnitude: string; negative: boolean } };
  82. price: { fields: { magnitude: string; negative: boolean } };
  83. conf: string;
  84. timestamp: string;
  85. };
  86. }) {
  87. let expo = priceInfo.fields.expo.fields.magnitude;
  88. if (priceInfo.fields.expo.fields.negative) expo = "-" + expo;
  89. let price = priceInfo.fields.price.fields.magnitude;
  90. if (priceInfo.fields.price.fields.negative) price = "-" + price;
  91. return {
  92. conf: priceInfo.fields.conf,
  93. publishTime: priceInfo.fields.timestamp,
  94. expo,
  95. price,
  96. };
  97. }
  98. async getPriceFeed(feedId: string) {
  99. const provider = this.getProvider();
  100. const priceInfoObjectId = await this.client.getPriceFeedObjectId(feedId);
  101. if (!priceInfoObjectId) return undefined;
  102. const priceInfo = await provider.getObject({
  103. id: priceInfoObjectId,
  104. options: { showContent: true },
  105. });
  106. if (!priceInfo.data || !priceInfo.data.content) {
  107. throw new Error(
  108. `Price feed ID ${priceInfoObjectId} in price table but object not found!!`
  109. );
  110. }
  111. if (priceInfo.data.content.dataType !== "moveObject") {
  112. throw new Error(
  113. `Expected ${priceInfoObjectId} to be a moveObject (PriceInfoObject)`
  114. );
  115. }
  116. return {
  117. emaPrice: await this.parsePrice(
  118. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  119. // @ts-ignore
  120. priceInfo.data.content.fields.price_info.fields.price_feed.fields
  121. .ema_price
  122. ),
  123. price: await this.parsePrice(
  124. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  125. // @ts-ignore
  126. priceInfo.data.content.fields.price_info.fields.price_feed.fields.price
  127. ),
  128. };
  129. }
  130. /**
  131. * Given a signed VAA, execute the migration instruction on the pyth contract.
  132. * The payload of the VAA can be obtained from the `getUpgradePackagePayload` method.
  133. * @param vaa
  134. * @param keypair used to sign the transaction
  135. */
  136. async executeMigrateInstruction(vaa: Buffer, keypair: Ed25519Keypair) {
  137. const tx = new Transaction();
  138. const packageId = await this.getPythPackageId();
  139. const verificationReceipt = await this.getVaaVerificationReceipt(
  140. tx,
  141. packageId,
  142. vaa
  143. );
  144. tx.moveCall({
  145. target: `${packageId}::migrate::migrate`,
  146. arguments: [tx.object(this.stateId), verificationReceipt],
  147. });
  148. return this.executeTransaction(tx, keypair);
  149. }
  150. async executeUpdatePriceFeed(): Promise<TxResult> {
  151. // We need the feed ids to be able to execute the transaction
  152. // it may be possible to get them from the VAA but in batch transactions,
  153. // it is also possible to hava fewer feeds that user wants to update compared to
  154. // what exists in the VAA.
  155. throw new Error("Use executeUpdatePriceFeedWithFeeds instead");
  156. }
  157. async executeUpdatePriceFeedWithFeeds(
  158. senderPrivateKey: string,
  159. vaas: Buffer[],
  160. feedIds: string[]
  161. ): Promise<TxResult> {
  162. const tx = new Transaction();
  163. await this.client.updatePriceFeeds(tx, vaas, feedIds);
  164. const keypair = Ed25519Keypair.fromSecretKey(
  165. new Uint8Array(Buffer.from(senderPrivateKey, "hex"))
  166. );
  167. const result = await this.executeTransaction(tx, keypair);
  168. return { id: result.digest, info: result };
  169. }
  170. async executeCreatePriceFeed(
  171. senderPrivateKey: string,
  172. vaas: Buffer[]
  173. ): Promise<TxResult> {
  174. const tx = new Transaction();
  175. await this.client.createPriceFeed(tx, vaas);
  176. const keypair = Ed25519Keypair.fromSecretKey(
  177. new Uint8Array(Buffer.from(senderPrivateKey, "hex"))
  178. );
  179. const result = await this.executeTransaction(tx, keypair);
  180. return { id: result.digest, info: result };
  181. }
  182. async executeGovernanceInstruction(
  183. senderPrivateKey: PrivateKey,
  184. vaa: Buffer
  185. ): Promise<TxResult> {
  186. const keypair = Ed25519Keypair.fromSecretKey(
  187. new Uint8Array(Buffer.from(senderPrivateKey, "hex"))
  188. );
  189. const tx = new Transaction();
  190. const packageId = await this.getPythPackageId();
  191. const verificationReceipt = await this.getVaaVerificationReceipt(
  192. tx,
  193. packageId,
  194. vaa
  195. );
  196. tx.moveCall({
  197. target: `${packageId}::governance::execute_governance_instruction`,
  198. arguments: [tx.object(this.stateId), verificationReceipt],
  199. });
  200. const result = await this.executeTransaction(tx, keypair);
  201. return { id: result.digest, info: result };
  202. }
  203. async executeUpgradeInstruction(
  204. vaa: Buffer,
  205. keypair: Ed25519Keypair,
  206. modules: number[][],
  207. dependencies: string[]
  208. ) {
  209. const tx = new Transaction();
  210. const packageId = await this.getPythPackageId();
  211. const verificationReceipt = await this.getVaaVerificationReceipt(
  212. tx,
  213. packageId,
  214. vaa
  215. );
  216. const [upgradeTicket] = tx.moveCall({
  217. target: `${packageId}::contract_upgrade::authorize_upgrade`,
  218. arguments: [tx.object(this.stateId), verificationReceipt],
  219. });
  220. const [upgradeReceipt] = tx.upgrade({
  221. modules,
  222. dependencies,
  223. package: packageId,
  224. ticket: upgradeTicket,
  225. });
  226. tx.moveCall({
  227. target: `${packageId}::contract_upgrade::commit_upgrade`,
  228. arguments: [tx.object(this.stateId), upgradeReceipt],
  229. });
  230. const result = await this.executeTransaction(tx, keypair);
  231. return { id: result.digest, info: result };
  232. }
  233. /**
  234. * Utility function to get the verification receipt object for a VAA that can be
  235. * used to authorize a governance instruction.
  236. * @param tx
  237. * @param packageId pyth package id
  238. * @param vaa
  239. * @private
  240. */
  241. async getVaaVerificationReceipt(
  242. tx: Transaction,
  243. packageId: string,
  244. vaa: Buffer
  245. ) {
  246. const wormholePackageId = await this.getWormholePackageId();
  247. const [verifiedVAA] = tx.moveCall({
  248. target: `${wormholePackageId}::vaa::parse_and_verify`,
  249. arguments: [
  250. tx.object(this.wormholeStateId),
  251. tx.pure.arguments(Array.from(vaa)),
  252. tx.object(SUI_CLOCK_OBJECT_ID),
  253. ],
  254. });
  255. const [verificationReceipt] = tx.moveCall({
  256. target: `${packageId}::governance::verify_vaa`,
  257. arguments: [tx.object(this.stateId), verifiedVAA],
  258. });
  259. return verificationReceipt;
  260. }
  261. /**
  262. * Given a transaction block and a keypair, sign and execute it
  263. * Sets the gas budget to 2x the estimated gas cost
  264. * @param tx
  265. * @param keypair
  266. * @private
  267. */
  268. private async executeTransaction(tx: Transaction, keypair: Ed25519Keypair) {
  269. const provider = this.getProvider();
  270. tx.setSender(keypair.toSuiAddress());
  271. const dryRun = await provider.dryRunTransactionBlock({
  272. transactionBlock: await tx.build({ client: provider }),
  273. });
  274. tx.setGasBudget(BigInt(dryRun.input.gasData.budget.toString()) * BigInt(2));
  275. return provider.signAndExecuteTransaction({
  276. signer: keypair,
  277. transaction: tx,
  278. options: {
  279. showEffects: true,
  280. showEvents: true,
  281. },
  282. });
  283. }
  284. async getValidTimePeriod() {
  285. const fields = await this.getStateFields();
  286. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  287. // @ts-ignore
  288. return Number(fields.stale_price_threshold);
  289. }
  290. async getDataSources(): Promise<DataSource[]> {
  291. const provider = this.getProvider();
  292. const result = await provider.getDynamicFieldObject({
  293. parentId: this.stateId,
  294. name: {
  295. type: "vector<u8>",
  296. value: "data_sources",
  297. },
  298. });
  299. if (!result.data || !result.data.content) {
  300. throw new Error(
  301. "Data Sources not found, contract may not be initialized"
  302. );
  303. }
  304. if (result.data.content.dataType !== "moveObject") {
  305. throw new Error("Data Sources type mismatch");
  306. }
  307. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  308. // @ts-ignore
  309. return result.data.content.fields.value.fields.keys.map(
  310. ({
  311. fields,
  312. }: {
  313. fields: {
  314. emitter_address: { fields: { value: { fields: { data: string } } } };
  315. emitter_chain: string;
  316. };
  317. }) => {
  318. return {
  319. emitterChain: Number(fields.emitter_chain),
  320. emitterAddress: Buffer.from(
  321. fields.emitter_address.fields.value.fields.data
  322. ).toString("hex"),
  323. };
  324. }
  325. );
  326. }
  327. async getGovernanceDataSource(): Promise<DataSource> {
  328. const fields = await this.getStateFields();
  329. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  330. // @ts-ignore
  331. const governanceFields = fields.governance_data_source.fields;
  332. const chainId = governanceFields.emitter_chain;
  333. const emitterAddress =
  334. governanceFields.emitter_address.fields.value.fields.data;
  335. return {
  336. emitterChain: Number(chainId),
  337. emitterAddress: Buffer.from(emitterAddress).toString("hex"),
  338. };
  339. }
  340. async getBaseUpdateFee() {
  341. const fields = await this.getStateFields();
  342. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  343. // @ts-ignore
  344. return { amount: fields.base_update_fee };
  345. }
  346. async getLastExecutedGovernanceSequence() {
  347. const fields = await this.getStateFields();
  348. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  349. // @ts-ignore
  350. return Number(fields.last_executed_governance_sequence);
  351. }
  352. getProvider() {
  353. return this.chain.getProvider();
  354. }
  355. private async getStateFields() {
  356. const provider = this.getProvider();
  357. const result = await provider.getObject({
  358. id: this.stateId,
  359. options: { showContent: true },
  360. });
  361. if (
  362. !result.data ||
  363. !result.data.content ||
  364. result.data.content.dataType !== "moveObject"
  365. )
  366. throw new Error("Unable to fetch pyth state object");
  367. return result.data.content.fields;
  368. }
  369. }
  370. export class SuiWormholeContract extends WormholeContract {
  371. public static type = "SuiWormholeContract";
  372. private client: SuiPythClient;
  373. getId(): string {
  374. return `${this.chain.getId()}_${this.stateId}`;
  375. }
  376. getType(): string {
  377. return SuiWormholeContract.type;
  378. }
  379. toJson() {
  380. return {
  381. chain: this.chain.getId(),
  382. stateId: this.stateId,
  383. type: SuiWormholeContract.type,
  384. };
  385. }
  386. static fromJson(
  387. chain: Chain,
  388. parsed: {
  389. type: string;
  390. stateId: string;
  391. }
  392. ): SuiWormholeContract {
  393. if (parsed.type !== SuiWormholeContract.type)
  394. throw new Error("Invalid type");
  395. if (!(chain instanceof SuiChain))
  396. throw new Error(`Wrong chain type ${chain}`);
  397. return new SuiWormholeContract(chain, parsed.stateId);
  398. }
  399. constructor(public chain: SuiChain, public stateId: string) {
  400. super();
  401. this.client = new SuiPythClient(
  402. this.chain.getProvider(),
  403. // HACK:
  404. // We're using the SuiPythClient to work with the Wormhole contract
  405. // so there is no Pyth contract here, passing empty string to type-
  406. // check.
  407. "",
  408. this.stateId
  409. );
  410. }
  411. async getCurrentGuardianSetIndex(): Promise<number> {
  412. const data = await this.getStateFields();
  413. return Number(data.guardian_set_index);
  414. }
  415. // There doesn't seem to be a way to get a value out of any function call
  416. // via a Sui transaction due to the linear nature of the language, this is
  417. // enforced at the TransactionBlock level by only allowing you to receive
  418. // receipts.
  419. async getChainId(): Promise<number> {
  420. return this.chain.getWormholeChainId();
  421. }
  422. // NOTE: There's no way to getChain() on the main interface, should update
  423. // that interface.
  424. public getChain(): SuiChain {
  425. return this.chain;
  426. }
  427. async getGuardianSet(): Promise<string[]> {
  428. const data = await this.getStateFields();
  429. const guardian_sets = data.guardian_sets;
  430. return guardian_sets;
  431. }
  432. async upgradeGuardianSets(
  433. senderPrivateKey: PrivateKey,
  434. vaa: Buffer
  435. ): Promise<TxResult> {
  436. const tx = new Transaction();
  437. const coreObjectId = this.stateId;
  438. const corePackageId = await this.client.getWormholePackageId();
  439. const [verifiedVaa] = tx.moveCall({
  440. target: `${corePackageId}::vaa::parse_and_verify`,
  441. arguments: [
  442. tx.object(coreObjectId),
  443. tx.pure(uint8ArrayToBCS(new Uint8Array(vaa))),
  444. tx.object(SUI_CLOCK_OBJECT_ID),
  445. ],
  446. });
  447. const [decreeTicket] = tx.moveCall({
  448. target: `${corePackageId}::update_guardian_set::authorize_governance`,
  449. arguments: [tx.object(coreObjectId)],
  450. });
  451. const [decreeReceipt] = tx.moveCall({
  452. target: `${corePackageId}::governance_message::verify_vaa`,
  453. arguments: [tx.object(coreObjectId), verifiedVaa, decreeTicket],
  454. typeArguments: [
  455. `${corePackageId}::update_guardian_set::GovernanceWitness`,
  456. ],
  457. });
  458. tx.moveCall({
  459. target: `${corePackageId}::update_guardian_set::update_guardian_set`,
  460. arguments: [
  461. tx.object(coreObjectId),
  462. decreeReceipt,
  463. tx.object(SUI_CLOCK_OBJECT_ID),
  464. ],
  465. });
  466. const keypair = Ed25519Keypair.fromSecretKey(
  467. new Uint8Array(Buffer.from(senderPrivateKey, "hex"))
  468. );
  469. const result = await this.executeTransaction(tx, keypair);
  470. return { id: result.digest, info: result };
  471. }
  472. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  473. private async getStateFields(): Promise<any> {
  474. const provider = this.chain.getProvider();
  475. const result = await provider.getObject({
  476. id: this.stateId,
  477. options: { showContent: true },
  478. });
  479. if (
  480. !result.data ||
  481. !result.data.content ||
  482. result.data.content.dataType !== "moveObject"
  483. )
  484. throw new Error("Unable to fetch pyth state object");
  485. return result.data.content.fields;
  486. }
  487. /**
  488. * Given a transaction block and a keypair, sign and execute it
  489. * Sets the gas budget to 2x the estimated gas cost
  490. * @param tx
  491. * @param keypair
  492. * @private
  493. */
  494. private async executeTransaction(tx: Transaction, keypair: Ed25519Keypair) {
  495. const provider = this.chain.getProvider();
  496. tx.setSender(keypair.toSuiAddress());
  497. const dryRun = await provider.dryRunTransactionBlock({
  498. transactionBlock: await tx.build({ client: provider }),
  499. });
  500. tx.setGasBudget(BigInt(dryRun.input.gasData.budget.toString()) * BigInt(2));
  501. return provider.signAndExecuteTransaction({
  502. signer: keypair,
  503. transaction: tx,
  504. options: {
  505. showEffects: true,
  506. showEvents: true,
  507. },
  508. });
  509. }
  510. }