aptos.ts 8.3 KB

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