Procházet zdrojové kódy

[contract_manager] Add logic for tracking fee denominations and dollar values (#1394)

* tokens

* progress

* progress

* progress

* infra for storing tokens and using them in fee calculations

* precommit

* cleanup

* cleanup

* fix
Jayant Krishnamurthy před 1 rokem
rodič
revize
0f7a9cc334

+ 2 - 1
contract_manager/package.json

@@ -21,8 +21,8 @@
     "url": "git+https://github.com/pyth-network/pyth-crosschain.git"
   },
   "dependencies": {
-    "@coral-xyz/anchor": "^0.29.0",
     "@certusone/wormhole-sdk": "^0.9.8",
+    "@coral-xyz/anchor": "^0.29.0",
     "@injectivelabs/networks": "1.0.68",
     "@mysten/sui.js": "^0.49.1",
     "@pythnetwork/cosmwasm-deploy-tools": "*",
@@ -31,6 +31,7 @@
     "@pythnetwork/pyth-sui-js": "*",
     "@types/yargs": "^17.0.32",
     "aptos": "^1.5.0",
+    "axios": "^0.24.0",
     "bs58": "^5.0.0",
     "ts-node": "^10.9.1",
     "typescript": "^5.3.3"

+ 27 - 1
contract_manager/scripts/fetch_fees.ts

@@ -19,6 +19,18 @@ const parser = yargs(hideBin(process.argv))
 
 async function main() {
   const argv = await parser.argv;
+
+  const prices: Record<string, number> = {};
+  for (const token of Object.values(DefaultStore.tokens)) {
+    const price = await token.getPriceForMinUnit();
+    // We're going to ignore the value of tokens that aren't configured
+    // in the store -- these are likely not worth much anyway.
+    if (price !== undefined) {
+      prices[token.id] = price;
+    }
+  }
+
+  let totalFeeUsd = 0;
   for (const contract of Object.values(DefaultStore.contracts)) {
     if (contract.getChain().isMainnet() === argv.testnet) continue;
     if (
@@ -27,12 +39,26 @@ async function main() {
       contract instanceof CosmWasmPriceFeedContract
     ) {
       try {
-        console.log(`${contract.getId()} ${await contract.getTotalFee()}`);
+        const fee = await contract.getTotalFee();
+        let feeUsd = 0;
+        if (fee.denom !== undefined && prices[fee.denom] !== undefined) {
+          feeUsd = Number(fee.amount) * prices[fee.denom];
+          totalFeeUsd += feeUsd;
+          console.log(
+            `${contract.getId()} ${fee.amount} ${fee.denom} ($${feeUsd})`
+          );
+        } else {
+          console.log(
+            `${contract.getId()} ${fee.amount} ${fee.denom} ($ value unknown)`
+          );
+        }
       } catch (e) {
         console.error(`Error fetching fees for ${contract.getId()}`, e);
       }
     }
   }
+
+  console.log(`Total fees in USD: $${totalFeeUsd}`);
 }
 
 main();

+ 23 - 7
contract_manager/src/chains.ts

@@ -23,10 +23,12 @@ import { Network } from "@injectivelabs/networks";
 import { SuiClient } from "@mysten/sui.js/client";
 import { Ed25519Keypair } from "@mysten/sui.js/keypairs/ed25519";
 import { TransactionObject } from "web3/eth/types";
+import { TokenId } from "./token";
 
 export type ChainConfig = Record<string, string> & {
   mainnet: boolean;
   id: string;
+  nativeToken: TokenId;
 };
 export abstract class Chain extends Storable {
   public wormholeChainName: ChainName;
@@ -37,12 +39,14 @@ export abstract class Chain extends Storable {
    * @param mainnet whether this chain is mainnet or testnet/devnet
    * @param wormholeChainName the name of the wormhole chain that this chain is associated with.
    * Note that pyth has included additional chain names and ids to the wormhole spec.
+   * @param nativeToken the id of the token used to pay gas on this chain
    * @protected
    */
   protected constructor(
     protected id: string,
     protected mainnet: boolean,
-    wormholeChainName: string
+    wormholeChainName: string,
+    protected nativeToken: TokenId | undefined
   ) {
     super();
     this.wormholeChainName = wormholeChainName as ChainName;
@@ -65,6 +69,10 @@ export abstract class Chain extends Storable {
     return this.mainnet;
   }
 
+  public getNativeToken(): TokenId | undefined {
+    return this.nativeToken;
+  }
+
   /**
    * Returns the payload for a governance SetFee instruction for contracts deployed on this chain
    * @param fee the new fee to set
@@ -125,7 +133,7 @@ export abstract class Chain extends Storable {
 export class GlobalChain extends Chain {
   static type = "GlobalChain";
   constructor() {
-    super("global", true, "unset");
+    super("global", true, "unset", undefined);
   }
 
   generateGovernanceUpgradePayload(): Buffer {
@@ -163,12 +171,13 @@ export class CosmWasmChain extends Chain {
     id: string,
     mainnet: boolean,
     wormholeChainName: string,
+    nativeToken: TokenId | undefined,
     public endpoint: string,
     public gasPrice: string,
     public prefix: string,
     public feeDenom: string
   ) {
-    super(id, mainnet, wormholeChainName);
+    super(id, mainnet, wormholeChainName, nativeToken);
   }
 
   static fromJson(parsed: ChainConfig): CosmWasmChain {
@@ -180,7 +189,8 @@ export class CosmWasmChain extends Chain {
       parsed.endpoint,
       parsed.gasPrice,
       parsed.prefix,
-      parsed.feeDenom
+      parsed.feeDenom,
+      parsed.nativeToken
     );
   }
 
@@ -248,9 +258,10 @@ export class SuiChain extends Chain {
     id: string,
     mainnet: boolean,
     wormholeChainName: string,
+    nativeToken: TokenId | undefined,
     public rpcUrl: string
   ) {
-    super(id, mainnet, wormholeChainName);
+    super(id, mainnet, wormholeChainName, nativeToken);
   }
 
   static fromJson(parsed: ChainConfig): SuiChain {
@@ -259,6 +270,7 @@ export class SuiChain extends Chain {
       parsed.id,
       parsed.mainnet,
       parsed.wormholeChainName,
+      parsed.nativeToken,
       parsed.rpcUrl
     );
   }
@@ -314,11 +326,12 @@ export class EvmChain extends Chain {
   constructor(
     id: string,
     mainnet: boolean,
+    nativeToken: TokenId | undefined,
     private rpcUrl: string,
     private networkId: number
   ) {
     // On EVM networks we use the chain id as the wormhole chain name
-    super(id, mainnet, id);
+    super(id, mainnet, id, nativeToken);
   }
 
   static fromJson(parsed: ChainConfig & { networkId: number }): EvmChain {
@@ -326,6 +339,7 @@ export class EvmChain extends Chain {
     return new EvmChain(
       parsed.id,
       parsed.mainnet,
+      parsed.nativeToken,
       parsed.rpcUrl,
       parsed.networkId
     );
@@ -468,9 +482,10 @@ export class AptosChain extends Chain {
     id: string,
     mainnet: boolean,
     wormholeChainName: string,
+    nativeToken: TokenId | undefined,
     public rpcUrl: string
   ) {
-    super(id, mainnet, wormholeChainName);
+    super(id, mainnet, wormholeChainName, nativeToken);
   }
 
   getClient(): AptosClient {
@@ -508,6 +523,7 @@ export class AptosChain extends Chain {
       parsed.id,
       parsed.mainnet,
       parsed.wormholeChainName,
+      parsed.nativeToken,
       parsed.rpcUrl
     );
   }

+ 12 - 3
contract_manager/src/contracts/aptos.ts

@@ -3,6 +3,7 @@ import { ApiError, BCS, CoinClient, TxnBuilderTypes } from "aptos";
 import { AptosChain, Chain } from "../chains";
 import { DataSource } from "xc_admin_common";
 import { WormholeContract } from "./wormhole";
+import { TokenQty } from "../token";
 
 type WormholeState = {
   chain_id: { number: string };
@@ -91,7 +92,11 @@ export class AptosPriceFeedContract extends PriceFeedContract {
 
   static fromJson(
     chain: Chain,
-    parsed: { type: string; stateId: string; wormholeStateId: string }
+    parsed: {
+      type: string;
+      stateId: string;
+      wormholeStateId: string;
+    }
   ): AptosPriceFeedContract {
     if (parsed.type !== AptosPriceFeedContract.type)
       throw new Error("Invalid type");
@@ -260,9 +265,13 @@ export class AptosPriceFeedContract extends PriceFeedContract {
     return AptosPriceFeedContract.type;
   }
 
-  async getTotalFee(): Promise<bigint> {
+  async getTotalFee(): Promise<TokenQty> {
     const client = new CoinClient(this.chain.getClient());
-    return await client.checkBalance(this.stateId);
+    const amount = await client.checkBalance(this.stateId);
+    return {
+      amount,
+      denom: this.chain.getNativeToken(),
+    };
   }
 
   async getValidTimePeriod() {

+ 6 - 2
contract_manager/src/contracts/cosmwasm.ts

@@ -17,6 +17,7 @@ import {
   TxResult,
 } from "../base";
 import { WormholeContract } from "./wormhole";
+import { TokenQty } from "../token";
 
 /**
  * Variables here need to be snake case to match the on-chain contract configs
@@ -332,13 +333,16 @@ export class CosmWasmPriceFeedContract extends PriceFeedContract {
     return this.chain;
   }
 
-  async getTotalFee(): Promise<bigint> {
+  async getTotalFee(): Promise<TokenQty> {
     const client = await CosmWasmClient.connect(this.chain.endpoint);
     const coin = await client.getBalance(
       this.address,
       this.getChain().feeDenom
     );
-    return BigInt(coin.amount);
+    return {
+      amount: BigInt(coin.amount),
+      denom: this.chain.getNativeToken(),
+    };
   }
 
   async getValidTimePeriod() {

+ 7 - 2
contract_manager/src/contracts/evm.ts

@@ -5,6 +5,7 @@ import { PriceFeedContract, PrivateKey, Storable } from "../base";
 import { Chain, EvmChain } from "../chains";
 import { DataSource, EvmExecute } from "xc_admin_common";
 import { WormholeContract } from "./wormhole";
+import { TokenQty } from "../token";
 
 // Just to make sure tx gas limit is enough
 const EXTENDED_ENTROPY_ABI = [
@@ -724,9 +725,13 @@ export class EvmPriceFeedContract extends PriceFeedContract {
     return Web3.utils.keccak256(strippedCode);
   }
 
-  async getTotalFee(): Promise<bigint> {
+  async getTotalFee(): Promise<TokenQty> {
     const web3 = new Web3(this.chain.getRpcUrl());
-    return BigInt(await web3.eth.getBalance(this.address));
+    const amount = BigInt(await web3.eth.getBalance(this.address));
+    return {
+      amount,
+      denom: this.chain.getNativeToken(),
+    };
   }
 
   async getLastExecutedGovernanceSequence() {

+ 17 - 0
contract_manager/src/store.ts

@@ -13,6 +13,7 @@ import {
   EvmPriceFeedContract,
   SuiPriceFeedContract,
 } from "./contracts";
+import { Token } from "./token";
 import { PriceFeedContract, Storable } from "./base";
 import { parse, stringify } from "yaml";
 import { readdirSync, readFileSync, statSync, writeFileSync } from "fs";
@@ -22,11 +23,13 @@ export class Store {
   public chains: Record<string, Chain> = { global: new GlobalChain() };
   public contracts: Record<string, PriceFeedContract> = {};
   public entropy_contracts: Record<string, EvmEntropyContract> = {};
+  public tokens: Record<string, Token> = {};
   public vaults: Record<string, Vault> = {};
 
   constructor(public path: string) {
     this.loadAllChains();
     this.loadAllContracts();
+    this.loadAllTokens();
     this.loadAllVaults();
   }
 
@@ -143,6 +146,20 @@ export class Store {
     });
   }
 
+  loadAllTokens() {
+    this.getYamlFiles(`${this.path}/tokens/`).forEach((yamlFile) => {
+      const parsedArray = parse(readFileSync(yamlFile, "utf-8"));
+      for (const parsed of parsedArray) {
+        if (parsed.type !== Token.type) return;
+
+        const token = Token.fromJson(parsed);
+        if (this.tokens[token.getId()])
+          throw new Error(`Multiple tokens with id ${token.getId()} found`);
+        this.tokens[token.getId()] = token;
+      }
+    });
+  }
+
   loadAllVaults() {
     this.getYamlFiles(`${this.path}/vaults/`).forEach((yamlFile) => {
       const parsedArray = parse(readFileSync(yamlFile, "utf-8"));

+ 81 - 0
contract_manager/src/token.ts

@@ -0,0 +1,81 @@
+import axios from "axios";
+import { KeyValueConfig, Storable } from "./base";
+
+export type TokenId = string;
+/**
+ * A quantity of a token, represented as an integer number of the minimum denomination of the token.
+ * This can also represent a quantity of an unknown token (represented by an undefined denom).
+ */
+export type TokenQty = {
+  amount: bigint;
+  denom: TokenId | undefined;
+};
+
+/**
+ * A token represents a cryptocurrency like ETH or BTC.
+ * The main use of this class is to calculate the dollar value of accrued fees.
+ */
+export class Token extends Storable {
+  static type = "token";
+
+  public constructor(
+    public id: TokenId,
+    // The hexadecimal pyth id of the tokens X/USD price feed
+    // (get this from hermes or the Pyth docs page)
+    public pythId: string | undefined,
+    public decimals: number
+  ) {
+    super();
+  }
+
+  getId(): TokenId {
+    return this.id;
+  }
+
+  getType(): string {
+    return Token.type;
+  }
+
+  /**
+   * Get the dollar value of 1 token. Returns undefined for tokens that do
+   * not have a configured pricing method.
+   */
+  async getPrice(): Promise<number | undefined> {
+    if (this.pythId) {
+      const url = `https://hermes.pyth.network/v2/updates/price/latest?ids%5B%5D=${this.pythId}&parsed=true`;
+      const response = await axios.get(url);
+      const price = response.data.parsed[0].price;
+
+      // Note that this conversion can lose some precision.
+      // We don't really care about that in this application.
+      return parseInt(price.price) * Math.pow(10, price.expo);
+    } else {
+      // We may support other pricing methodologies in the future but whatever.
+      return undefined;
+    }
+  }
+
+  /**
+   * Get the dollar value of the minimum representable quantity of this token.
+   * E.g., for ETH, this method will return the dollar value of 1 wei.
+   */
+  async getPriceForMinUnit(): Promise<number | undefined> {
+    const price = await this.getPrice();
+    return price ? price / Math.pow(10, this.decimals) : undefined;
+  }
+
+  toJson(): KeyValueConfig {
+    return {
+      id: this.id,
+      ...(this.pythId !== undefined ? { pythId: this.pythId } : {}),
+    };
+  }
+
+  static fromJson(parsed: {
+    id: string;
+    pythId?: string;
+    decimals: number;
+  }): Token {
+    return new Token(parsed.id, parsed.pythId, parsed.decimals);
+  }
+}

+ 1 - 0
contract_manager/store/chains/AptosChains.yaml

@@ -8,6 +8,7 @@
   mainnet: true
   rpcUrl: https://fullnode.mainnet.aptoslabs.com/v1
   type: AptosChain
+  nativeToken: APT
 - id: movement_move_devnet
   wormholeChainName: movement_move_devnet
   mainnet: false

+ 16 - 0
contract_manager/store/chains/EvmChains.yaml

@@ -13,6 +13,7 @@
   rpcUrl: https://evmos-evm.publicnode.com
   networkId: 9001
   type: EvmChain
+  nativeToken: EVMOS
 - id: canto
   mainnet: true
   rpcUrl: https://canto.slingshot.finance
@@ -68,6 +69,7 @@
   rpcUrl: https://rpc.gnosischain.com
   networkId: 100
   type: EvmChain
+  nativeToken: DAI
 - id: fantom_testnet
   mainnet: false
   rpcUrl: https://fantom-testnet.blastapi.io/$ENV_BLAST_API_KEY
@@ -83,6 +85,7 @@
   rpcUrl: https://rpc.ankr.com/fantom
   networkId: 250
   type: EvmChain
+  nativeToken: FTM
 - id: mumbai
   mainnet: false
   rpcUrl: https://polygon-testnet.blastapi.io/$ENV_BLAST_API_KEY
@@ -108,6 +111,7 @@
   rpcUrl: https://rpc.mantle.xyz/
   networkId: 5000
   type: EvmChain
+  nativeToken: MNT
 - id: kava_testnet
   mainnet: false
   rpcUrl: https://evm.testnet.kava.io
@@ -128,6 +132,7 @@
   rpcUrl: https://eth-mainnet.blastapi.io/$ENV_BLAST_API_KEY
   networkId: 1
   type: EvmChain
+  nativeToken: ETH
 - id: bsc_testnet
   mainnet: false
   rpcUrl: https://rpc.ankr.com/bsc_testnet_chapel
@@ -143,6 +148,7 @@
   rpcUrl: https://mainnet.aurora.dev
   networkId: 1313161554
   type: EvmChain
+  nativeToken: NEAR
 - id: bsc
   mainnet: true
   rpcUrl: https://rpc.ankr.com/bsc
@@ -178,6 +184,7 @@
   rpcUrl: https://polygon-rpc.com
   networkId: 137
   type: EvmChain
+  nativeToken: MATIC
 - id: wemix_testnet
   mainnet: false
   rpcUrl: https://api.test.wemix.com
@@ -188,11 +195,13 @@
   rpcUrl: https://rpc-mainnet.kcc.network
   networkId: 321
   type: EvmChain
+  nativeToken: KCS
 - id: polygon_zkevm
   mainnet: true
   rpcUrl: https://zkevm-rpc.com
   networkId: 1101
   type: EvmChain
+  nativeToken: ETH
 - id: celo_alfajores_testnet
   mainnet: false
   rpcUrl: https://alfajores-forno.celo-testnet.org
@@ -208,21 +217,25 @@
   rpcUrl: https://zksync2-mainnet.zksync.io
   networkId: 324
   type: EvmChain
+  nativeToken: ETH
 - id: base
   mainnet: true
   rpcUrl: https://developer-access-mainnet.base.org/
   networkId: 8453
   type: EvmChain
+  nativeToken: ETH
 - id: arbitrum
   mainnet: true
   rpcUrl: https://arb1.arbitrum.io/rpc
   networkId: 42161
   type: EvmChain
+  nativeToken: ETH
 - id: optimism
   mainnet: true
   rpcUrl: https://rpc.ankr.com/optimism
   networkId: 10
   type: EvmChain
+  nativeToken: ETH
 - id: kcc_testnet
   mainnet: false
   rpcUrl: https://rpc-testnet.kcc.network
@@ -243,6 +256,7 @@
   rpcUrl: https://linea.rpc.thirdweb.com
   networkId: 59144
   type: EvmChain
+  nativeToken: ETH
 - id: shimmer_testnet
   mainnet: false
   rpcUrl: https://json-rpc.evm.testnet.shimmer.network
@@ -373,6 +387,7 @@
   rpcUrl: https://rpc.coredao.org
   networkId: 1116
   type: EvmChain
+  nativeToken: CORE
 - id: tomochain
   mainnet: true
   rpcUrl: https://rpc.tomochain.com
@@ -443,6 +458,7 @@
   rpcUrl: https://mainnet.hashio.io/api
   networkId: 295
   type: EvmChain
+  nativeToken: HBAR
 - id: filecoin_calibration
   mainnet: false
   rpcUrl: https://rpc.ankr.com/filecoin_testnet

+ 44 - 0
contract_manager/store/tokens/Tokens.yaml

@@ -0,0 +1,44 @@
+- id: ETH
+  pythId: ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace
+  decimals: 18
+  type: token
+- id: APT
+  pythId: 03ae4db29ed4ae33d323568895aa00337e658e348b37509f5372ae51f0af00d5
+  decimals: 8
+  type: token
+- id: EVMOS
+  pythId: c19405e4c8bdcbf2a66c37ae05a27d385c8309e9d648ed20dc6ee717e7d30e17
+  decimals: 18
+  type: token
+- id: MATIC
+  pythId: 5de33a9112c2b700b8d30b8a3402c103578ccfa2765696471cc672bd5cf6ac52
+  decimals: 18
+  type: token
+- id: NEAR
+  pythId: c415de8d2eba7db216527dff4b60e8f3a5311c740dadb233e13e12547e226750
+  decimals: 18
+  type: token
+- id: FTM
+  pythId: 5c6c0d2386e3352356c3ab84434fafb5ea067ac2678a38a338c4a69ddc4bdb0c
+  decimals: 18
+  type: token
+- id: DAI
+  pythId: b0948a5e5313200c632b51bb5ca32f6de0d36e9950a942d19751e833f70dabfd
+  decimals: 18
+  type: token
+- id: KCS
+  pythId: c8acad81438490d4ebcac23b3e93f31cdbcb893fcba746ea1c66b89684faae2f
+  decimals: 18
+  type: token
+- id: MNT
+  pythId: 4e3037c822d852d79af3ac80e35eb420ee3b870dca49f9344a38ef4773fb0585
+  decimals: 18
+  type: token
+- id: HBAR
+  pythId: 3728e591097635310e6341af53db8b7ee42da9b3a8d918f9463ce9cca886dfbd
+  decimals: 8
+  type: token
+- id: CORE
+  pythId: 9b4503710cc8c53f75c30e6e4fda1a7064680ef2e0ee97acd2e3a7c37b3c830c
+  decimals: 18
+  type: token

+ 16 - 14
package-lock.json

@@ -55,6 +55,7 @@
         "@pythnetwork/pyth-sui-js": "*",
         "@types/yargs": "^17.0.32",
         "aptos": "^1.5.0",
+        "axios": "^0.24.0",
         "bs58": "^5.0.0",
         "ts-node": "^10.9.1",
         "typescript": "^5.3.3"
@@ -23808,11 +23809,11 @@
       }
     },
     "node_modules/axios": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
-      "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
+      "version": "1.6.8",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
+      "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
       "dependencies": {
-        "follow-redirects": "^1.15.0",
+        "follow-redirects": "^1.15.6",
         "form-data": "^4.0.0",
         "proxy-from-env": "^1.1.0"
       }
@@ -31326,9 +31327,9 @@
       }
     },
     "node_modules/follow-redirects": {
-      "version": "1.15.2",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
-      "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+      "version": "1.15.6",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+      "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
       "funding": [
         {
           "type": "individual",
@@ -79140,11 +79141,11 @@
       "integrity": "sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg=="
     },
     "axios": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
-      "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
+      "version": "1.6.8",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
+      "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
       "requires": {
-        "follow-redirects": "^1.15.0",
+        "follow-redirects": "^1.15.6",
         "form-data": "^4.0.0",
         "proxy-from-env": "^1.1.0"
       }
@@ -81094,6 +81095,7 @@
         "@pythnetwork/pyth-sui-js": "*",
         "@types/yargs": "^17.0.32",
         "aptos": "^1.5.0",
+        "axios": "^0.24.0",
         "bs58": "^5.0.0",
         "prettier": "^2.6.2",
         "ts-node": "^10.9.1",
@@ -86038,9 +86040,9 @@
       "peer": true
     },
     "follow-redirects": {
-      "version": "1.15.2",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
-      "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
+      "version": "1.15.6",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+      "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
     },
     "for-each": {
       "version": "0.3.3",