terra.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import { fromUint8Array } from "js-base64";
  2. import {
  3. LCDClient,
  4. LCDClientConfig,
  5. MnemonicKey,
  6. MsgExecuteContract,
  7. } from "@terra-money/terra.js";
  8. import { hexToUint8Array } from "@certusone/wormhole-sdk";
  9. import axios from "axios";
  10. import { logger } from "../helpers";
  11. import { Relay, RelayResult, RelayRetcode, PriceId } from "./iface";
  12. export const TERRA_GAS_PRICES_URL = "https://fcd.terra.dev/v1/txs/gas_prices";
  13. export class TerraRelay implements Relay {
  14. readonly nodeUrl: string;
  15. readonly terraChainId: string;
  16. readonly walletPrivateKey: string;
  17. readonly coin: string;
  18. readonly contractAddress: string;
  19. readonly lcdConfig: LCDClientConfig;
  20. constructor(cfg: {
  21. nodeUrl: string;
  22. terraChainId: string;
  23. walletPrivateKey: string;
  24. coin: string;
  25. contractAddress: string;
  26. }) {
  27. this.nodeUrl = cfg.nodeUrl;
  28. this.terraChainId = cfg.terraChainId;
  29. this.walletPrivateKey = cfg.walletPrivateKey;
  30. this.coin = cfg.coin;
  31. this.contractAddress = cfg.contractAddress;
  32. this.lcdConfig = {
  33. URL: this.nodeUrl,
  34. chainID: this.terraChainId,
  35. };
  36. logger.info(
  37. "Terra connection parameters: url: [" +
  38. this.nodeUrl +
  39. "], terraChainId: [" +
  40. this.terraChainId +
  41. "], coin: [" +
  42. this.coin +
  43. "], contractAddress: [" +
  44. this.contractAddress +
  45. "]"
  46. );
  47. }
  48. async relay(signedVAAs: Array<string>) {
  49. let terraRes;
  50. try {
  51. logger.debug("relaying " + signedVAAs.length + " messages to terra");
  52. logger.debug("TIME: connecting to terra");
  53. const lcdClient = new LCDClient(this.lcdConfig);
  54. const mk = new MnemonicKey({
  55. mnemonic: this.walletPrivateKey,
  56. });
  57. const wallet = lcdClient.wallet(mk);
  58. logger.debug("TIME: creating messages");
  59. let msgs = new Array<MsgExecuteContract>();
  60. for (let idx = 0; idx < signedVAAs.length; ++idx) {
  61. const msg = new MsgExecuteContract(
  62. wallet.key.accAddress,
  63. this.contractAddress,
  64. {
  65. update_price_feeds: {
  66. data: Buffer.from(signedVAAs[idx], "hex").toString("base64"),
  67. },
  68. }
  69. );
  70. msgs.push(msg);
  71. }
  72. let gasPrices;
  73. try {
  74. gasPrices = await axios
  75. .get(TERRA_GAS_PRICES_URL)
  76. .then((result) => result.data);
  77. } catch (e: any) {
  78. logger.warn(e);
  79. logger.warn(e.stack);
  80. logger.warn(
  81. "Couldn't fetch gas price and fee estimate. Using default values"
  82. );
  83. }
  84. const tx = await wallet.createAndSignTx({
  85. msgs: msgs,
  86. memo: "P2T",
  87. feeDenoms: [this.coin],
  88. gasPrices,
  89. });
  90. logger.debug("TIME: sending msg");
  91. terraRes = await lcdClient.tx.broadcastSync(tx);
  92. logger.debug(
  93. `TIME:submitted to terra: terraRes: ${JSON.stringify(terraRes)}`
  94. );
  95. // Act on known Terra errors
  96. if (terraRes.raw_log) {
  97. if (terraRes.raw_log.search("VaaAlreadyExecuted") >= 0) {
  98. logger.error(
  99. "Already Executed:",
  100. terraRes.txhash
  101. ? terraRes.txhash
  102. : "<INTERNAL: no txhash for AlreadyExecuted>"
  103. );
  104. return new RelayResult(RelayRetcode.AlreadyExecuted, []);
  105. } else if (terraRes.raw_log.search("insufficient funds") >= 0) {
  106. logger.error(
  107. "relay failed due to insufficient funds: ",
  108. JSON.stringify(terraRes)
  109. );
  110. return new RelayResult(RelayRetcode.InsufficientFunds, []);
  111. } else if (terraRes.raw_log.search("failed") >= 0) {
  112. logger.error(
  113. "relay seems to have failed: ",
  114. JSON.stringify(terraRes)
  115. );
  116. return new RelayResult(RelayRetcode.Fail, []);
  117. }
  118. } else {
  119. logger.warn("No logs were found, result: ", JSON.stringify(terraRes));
  120. }
  121. // Base case, no errors were detected and no exceptions were thrown
  122. if (terraRes.txhash) {
  123. return new RelayResult(RelayRetcode.Success, [terraRes.txhash]);
  124. }
  125. } catch (e: any) {
  126. // Act on known Terra exceptions
  127. logger.error(e);
  128. logger.error(e.stack);
  129. if (
  130. e.message &&
  131. e.message.search("timeout") >= 0 &&
  132. e.message.search("exceeded") >= 0
  133. ) {
  134. logger.error("relay timed out: %o", e);
  135. return new RelayResult(RelayRetcode.Timeout, []);
  136. } else if (
  137. e.response?.data?.error &&
  138. e.response.data.error.search("VaaAlreadyExecuted") >= 0
  139. ) {
  140. logger.error("VAA Already Executed");
  141. logger.error(e.response.data.error);
  142. return new RelayResult(RelayRetcode.AlreadyExecuted, []);
  143. } else if (
  144. e.response?.data?.message &&
  145. e.response.data.message.search("account sequence mismatch") >= 0
  146. ) {
  147. logger.error("Account sequence mismatch");
  148. logger.error(e.response.data.message);
  149. return new RelayResult(RelayRetcode.SeqNumMismatch, []);
  150. } else {
  151. logger.error("Unknown error:");
  152. logger.error(e.toString());
  153. return new RelayResult(RelayRetcode.Fail, []);
  154. }
  155. }
  156. logger.error("INTERNAL: Terra relay() logic failed to produce a result");
  157. return new RelayResult(RelayRetcode.Fail, []);
  158. }
  159. async query(priceId: PriceId) {
  160. logger.info("Querying terra for price info for priceId [" + priceId + "]");
  161. const lcdClient = new LCDClient(this.lcdConfig);
  162. const mk = new MnemonicKey({
  163. mnemonic: this.walletPrivateKey,
  164. });
  165. return await lcdClient.wasm.contractQuery(this.contractAddress, {
  166. price_feed: {
  167. id: priceId,
  168. },
  169. });
  170. }
  171. async getPayerInfo(): Promise<{ address: string; balance: bigint }> {
  172. const lcdClient = new LCDClient(this.lcdConfig);
  173. const mk = new MnemonicKey({
  174. mnemonic: this.walletPrivateKey,
  175. });
  176. const wallet = lcdClient.wallet(mk);
  177. let balance: number = NaN;
  178. try {
  179. logger.debug("querying wallet balance");
  180. let coins: any;
  181. let pagnation: any;
  182. [coins, pagnation] = await lcdClient.bank.balance(wallet.key.accAddress);
  183. logger.debug("wallet query returned: %o", coins);
  184. if (coins) {
  185. let coin = coins.get(this.coin);
  186. if (coin) {
  187. balance = parseInt(coin.toData().amount);
  188. } else {
  189. logger.error(
  190. "failed to query coin balance, coin [" +
  191. this.coin +
  192. "] is not in the wallet, coins: %o",
  193. coins
  194. );
  195. }
  196. } else {
  197. logger.error("failed to query coin balance!");
  198. }
  199. } catch (e) {
  200. logger.error("failed to query coin balance: %o", e);
  201. }
  202. return { address: wallet.key.accAddress, balance: BigInt(balance) };
  203. }
  204. }