aptos.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import { PriceFeedContract, PriceFeed, PrivateKey, TxResult } from "../base";
  2. import { ApiError, BCS, CoinClient, TxnBuilderTypes } from "aptos";
  3. import { AptosChain, Chain } from "../chains";
  4. import { DataSource } from "xc_admin_common";
  5. import { WormholeContract } from "./wormhole";
  6. type WormholeState = {
  7. chain_id: { number: string };
  8. guardian_set_index: { number: string };
  9. guardian_sets: { handle: string };
  10. };
  11. type GuardianSet = {
  12. guardians: { address: { bytes: string } }[];
  13. expiration_time: { number: string };
  14. index: { number: string };
  15. };
  16. export class WormholeAptosContract extends WormholeContract {
  17. constructor(public chain: AptosChain, public address: string) {
  18. super();
  19. }
  20. async getState(): Promise<WormholeState> {
  21. const client = this.chain.getClient();
  22. const resources = await client.getAccountResources(this.address);
  23. const type = "WormholeState";
  24. for (const resource of resources) {
  25. if (resource.type === `${this.address}::state::${type}`) {
  26. return resource.data as WormholeState;
  27. }
  28. }
  29. throw new Error(`${type} resource not found in account ${this.address}`);
  30. }
  31. async getCurrentGuardianSetIndex(): Promise<number> {
  32. const data = await this.getState();
  33. return Number(data.guardian_set_index.number);
  34. }
  35. async getChainId(): Promise<number> {
  36. const data = await this.getState();
  37. return Number(data.chain_id.number);
  38. }
  39. async getGuardianSet(): Promise<string[]> {
  40. const data = await this.getState();
  41. const client = this.chain.getClient();
  42. const result = (await client.getTableItem(data.guardian_sets.handle, {
  43. key_type: `u64`,
  44. value_type: `${this.address}::structs::GuardianSet`,
  45. key: data.guardian_set_index.number.toString(),
  46. })) as GuardianSet;
  47. return result.guardians.map((guardian) => guardian.address.bytes);
  48. }
  49. async upgradeGuardianSets(
  50. senderPrivateKey: PrivateKey,
  51. vaa: Buffer
  52. ): Promise<TxResult> {
  53. const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
  54. TxnBuilderTypes.EntryFunction.natural(
  55. `${this.address}::guardian_set_upgrade`,
  56. "submit_vaa_entry",
  57. [],
  58. [BCS.bcsSerializeBytes(vaa)]
  59. )
  60. );
  61. return this.chain.sendTransaction(senderPrivateKey, txPayload);
  62. }
  63. }
  64. export class AptosPriceFeedContract extends PriceFeedContract {
  65. static type = "AptosPriceFeedContract";
  66. /**
  67. * Given the ids of the pyth state and wormhole state, create a new AptosContract
  68. * The package ids are derived based on the state ids
  69. *
  70. * @param chain the chain which this contract is deployed on
  71. * @param stateId id of the pyth state for the deployed contract
  72. * @param wormholeStateId id of the wormhole state for the wormhole contract that pyth binds to
  73. */
  74. constructor(
  75. public chain: AptosChain,
  76. public stateId: string,
  77. public wormholeStateId: string
  78. ) {
  79. super();
  80. }
  81. static fromJson(
  82. chain: Chain,
  83. parsed: { type: string; stateId: string; wormholeStateId: string }
  84. ): AptosPriceFeedContract {
  85. if (parsed.type !== AptosPriceFeedContract.type)
  86. throw new Error("Invalid type");
  87. if (!(chain instanceof AptosChain))
  88. throw new Error(`Wrong chain type ${chain}`);
  89. return new AptosPriceFeedContract(
  90. chain,
  91. parsed.stateId,
  92. parsed.wormholeStateId
  93. );
  94. }
  95. async executeGovernanceInstruction(
  96. senderPrivateKey: PrivateKey,
  97. vaa: Buffer
  98. ): Promise<TxResult> {
  99. const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
  100. TxnBuilderTypes.EntryFunction.natural(
  101. `${this.stateId}::governance`,
  102. "execute_governance_instruction",
  103. [],
  104. [BCS.bcsSerializeBytes(vaa)]
  105. )
  106. );
  107. return this.chain.sendTransaction(senderPrivateKey, txPayload);
  108. }
  109. public getWormholeContract(): WormholeAptosContract {
  110. return new WormholeAptosContract(this.chain, this.wormholeStateId);
  111. }
  112. async executeUpdatePriceFeed(
  113. senderPrivateKey: PrivateKey,
  114. vaas: Buffer[]
  115. ): Promise<TxResult> {
  116. const txPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
  117. TxnBuilderTypes.EntryFunction.natural(
  118. `${this.stateId}::pyth`,
  119. "update_price_feeds_with_funder",
  120. [],
  121. [BCS.serializeVectorWithFunc(vaas, "serializeBytes")]
  122. )
  123. );
  124. return this.chain.sendTransaction(senderPrivateKey, txPayload);
  125. }
  126. getStateResources() {
  127. const client = this.chain.getClient();
  128. return client.getAccountResources(this.stateId);
  129. }
  130. /**
  131. * Returns the first occurrence of a resource with the given type in the pyth package state
  132. * @param type
  133. */
  134. async findResource(type: string) {
  135. const resources = await this.getStateResources();
  136. for (const resource of resources) {
  137. if (resource.type === `${this.stateId}::state::${type}`) {
  138. return resource.data;
  139. }
  140. }
  141. throw new Error(`${type} resource not found in state ${this.stateId}`);
  142. }
  143. async getBaseUpdateFee() {
  144. const data = (await this.findResource("BaseUpdateFee")) as { fee: string };
  145. return { amount: data.fee };
  146. }
  147. getChain(): AptosChain {
  148. return this.chain;
  149. }
  150. private parsePrice(priceInfo: {
  151. expo: { magnitude: string; negative: boolean };
  152. price: { magnitude: string; negative: boolean };
  153. conf: string;
  154. timestamp: string;
  155. }) {
  156. let expo = priceInfo.expo.magnitude;
  157. if (priceInfo.expo.negative) expo = "-" + expo;
  158. let price = priceInfo.price.magnitude;
  159. if (priceInfo.price.negative) price = "-" + price;
  160. return {
  161. conf: priceInfo.conf,
  162. publishTime: priceInfo.timestamp,
  163. expo,
  164. price,
  165. };
  166. }
  167. async getPriceFeed(feedId: string): Promise<PriceFeed | undefined> {
  168. const client = this.chain.getClient();
  169. const res = (await this.findResource("LatestPriceInfo")) as {
  170. info: { handle: string };
  171. };
  172. const handle = res.info.handle;
  173. try {
  174. const priceItemRes = await client.getTableItem(handle, {
  175. key_type: `${this.stateId}::price_identifier::PriceIdentifier`,
  176. value_type: `${this.stateId}::price_info::PriceInfo`,
  177. key: {
  178. bytes: feedId,
  179. },
  180. });
  181. return {
  182. price: this.parsePrice(priceItemRes.price_feed.price),
  183. emaPrice: this.parsePrice(priceItemRes.price_feed.ema_price),
  184. };
  185. } catch (e) {
  186. if (e instanceof ApiError && e.errorCode === "table_item_not_found")
  187. return undefined;
  188. throw e;
  189. }
  190. }
  191. async getDataSources(): Promise<DataSource[]> {
  192. const data = (await this.findResource("DataSources")) as {
  193. sources: {
  194. keys: {
  195. emitter_chain: string;
  196. emitter_address: { external_address: string };
  197. }[];
  198. };
  199. };
  200. return data.sources.keys.map((source) => {
  201. return {
  202. emitterChain: Number(source.emitter_chain),
  203. emitterAddress: source.emitter_address.external_address.replace(
  204. "0x",
  205. ""
  206. ),
  207. };
  208. });
  209. }
  210. async getGovernanceDataSource(): Promise<DataSource> {
  211. const data = (await this.findResource("GovernanceDataSource")) as {
  212. source: {
  213. emitter_chain: string;
  214. emitter_address: { external_address: string };
  215. };
  216. };
  217. return {
  218. emitterChain: Number(data.source.emitter_chain),
  219. emitterAddress: data.source.emitter_address.external_address.replace(
  220. "0x",
  221. ""
  222. ),
  223. };
  224. }
  225. async getLastExecutedGovernanceSequence() {
  226. const data = (await this.findResource(
  227. "LastExecutedGovernanceSequence"
  228. )) as { sequence: string };
  229. return Number(data.sequence);
  230. }
  231. getId(): string {
  232. return `${this.chain.getId()}_${this.stateId}`;
  233. }
  234. getType(): string {
  235. return AptosPriceFeedContract.type;
  236. }
  237. async getTotalFee(): Promise<bigint> {
  238. const client = new CoinClient(this.chain.getClient());
  239. return await client.checkBalance(this.stateId);
  240. }
  241. async getValidTimePeriod() {
  242. const data = (await this.findResource("StalePriceThreshold")) as {
  243. threshold_secs: string;
  244. };
  245. return Number(data.threshold_secs);
  246. }
  247. toJson() {
  248. return {
  249. chain: this.chain.getId(),
  250. stateId: this.stateId,
  251. wormholeStateId: this.wormholeStateId,
  252. type: AptosPriceFeedContract.type,
  253. };
  254. }
  255. }