Pārlūkot izejas kodu

[price-pusher] support for aptos (#815)

* aptos price listener

* price pusher aptos

* add comment

* update package lock

* remove eslint disable comments

* bump version

* npm i at root

* update readme

* address feedback

* update readme

* json fix
Dev Kalra 2 gadi atpakaļ
vecāks
revīzija
3fc996d6f9

+ 115 - 112
package-lock.json

@@ -11514,6 +11514,10 @@
       "resolved": "target_chains/cosmwasm/tools",
       "link": true
     },
+    "node_modules/@pythnetwork/eth-oracle-swap-example-frontend": {
+      "resolved": "target_chains/ethereum/examples/oracle_swap/app",
+      "link": true
+    },
     "node_modules/@pythnetwork/price-pusher": {
       "resolved": "price_pusher",
       "link": true
@@ -21067,9 +21071,9 @@
       "dev": true
     },
     "node_modules/aptos": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/aptos/-/aptos-1.6.0.tgz",
-      "integrity": "sha512-5khjDwrDeNMDBFRcZAmETW20D+V2AqTdsgqkh6bvvl70BtRXdkitN0saM05gf1rK3atnO9PyUKO8iRaBDG5qtA==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/aptos/-/aptos-1.8.5.tgz",
+      "integrity": "sha512-iQxliWesNHjGQ5YYXCyss9eg4+bDGQWqAZa73vprqGQ9tungK0cRjUI2fmnp63Ed6UG6rurHrL+b0ckbZAOZZQ==",
       "dependencies": {
         "@noble/hashes": "1.1.3",
         "@scure/bip39": "1.1.0",
@@ -50989,10 +50993,6 @@
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
       "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
     },
-    "node_modules/@pythnetwork/eth-oracle-swap-example-frontend": {
-      "resolved": "target_chains/ethereum/examples/oracle_swap/app",
-      "link": true
-    },
     "node_modules/traverse-chain": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz",
@@ -55110,13 +55110,14 @@
     },
     "price_pusher": {
       "name": "@pythnetwork/price-pusher",
-      "version": "5.0.0",
+      "version": "5.1.0",
       "license": "Apache-2.0",
       "dependencies": {
         "@injectivelabs/sdk-ts": "1.10.72",
         "@pythnetwork/price-service-client": "*",
         "@pythnetwork/pyth-sdk-solidity": "*",
         "@truffle/hdwallet-provider": "^2.1.3",
+        "aptos": "^1.8.5",
         "joi": "^17.6.0",
         "web3": "^1.8.1",
         "web3-eth-contract": "^1.8.1",
@@ -57475,6 +57476,7 @@
       }
     },
     "target_chains/ethereum/examples/oracle_swap/app": {
+      "name": "@pythnetwork/eth-oracle-swap-example-frontend",
       "version": "0.1.0",
       "dependencies": {
         "@pythnetwork/pyth-evm-js": "*",
@@ -66433,6 +66435,107 @@
         }
       }
     },
+    "@pythnetwork/eth-oracle-swap-example-frontend": {
+      "version": "file:target_chains/ethereum/examples/oracle_swap/app",
+      "requires": {
+        "@pythnetwork/pyth-evm-js": "*",
+        "@pythnetwork/pyth-sdk-solidity": "*",
+        "@testing-library/jest-dom": "^5.16.5",
+        "@testing-library/react": "^13.4.0",
+        "@testing-library/user-event": "^13.5.0",
+        "@types/jest": "^27.5.2",
+        "@types/node": "^16.11.64",
+        "@types/react": "^18.0.21",
+        "@types/react-dom": "^18.0.6",
+        "buffer": "^6.0.3",
+        "ethers": "^5.7.2",
+        "metamask-react": "^2.4.0",
+        "prettier": "^2.7.1",
+        "react": "^18.2.0",
+        "react-dom": "^18.2.0",
+        "react-scripts": "5.0.1",
+        "typescript": "^4.8.4",
+        "web-vitals": "^2.1.4",
+        "web3": "^1.8.0"
+      },
+      "dependencies": {
+        "@types/jest": {
+          "version": "27.5.2",
+          "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz",
+          "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==",
+          "requires": {
+            "jest-matcher-utils": "^27.0.0",
+            "pretty-format": "^27.0.0"
+          }
+        },
+        "@types/node": {
+          "version": "16.18.28",
+          "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.28.tgz",
+          "integrity": "sha512-SNMfiPqsiPoYfmyi+2qnDO4nZyMIOCab/CW+Slcml0lhIzkOizYzWtt/A7tgB3TSitd+YJKi8fSC2Cpm/VCp7A=="
+        },
+        "ansi-styles": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+          "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="
+        },
+        "buffer": {
+          "version": "6.0.3",
+          "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+          "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+          "requires": {
+            "base64-js": "^1.3.1",
+            "ieee754": "^1.2.1"
+          }
+        },
+        "diff-sequences": {
+          "version": "27.5.1",
+          "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
+          "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ=="
+        },
+        "jest-diff": {
+          "version": "27.5.1",
+          "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",
+          "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==",
+          "requires": {
+            "chalk": "^4.0.0",
+            "diff-sequences": "^27.5.1",
+            "jest-get-type": "^27.5.1",
+            "pretty-format": "^27.5.1"
+          }
+        },
+        "jest-get-type": {
+          "version": "27.5.1",
+          "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz",
+          "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw=="
+        },
+        "jest-matcher-utils": {
+          "version": "27.5.1",
+          "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz",
+          "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==",
+          "requires": {
+            "chalk": "^4.0.0",
+            "jest-diff": "^27.5.1",
+            "jest-get-type": "^27.5.1",
+            "pretty-format": "^27.5.1"
+          }
+        },
+        "pretty-format": {
+          "version": "27.5.1",
+          "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+          "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+          "requires": {
+            "ansi-regex": "^5.0.1",
+            "ansi-styles": "^5.0.0",
+            "react-is": "^17.0.1"
+          }
+        },
+        "react-is": {
+          "version": "17.0.2",
+          "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+          "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
+        }
+      }
+    },
     "@pythnetwork/price-pusher": {
       "version": "file:price_pusher",
       "requires": {
@@ -66445,6 +66548,7 @@
         "@types/yargs": "^17.0.10",
         "@typescript-eslint/eslint-plugin": "^5.20.0",
         "@typescript-eslint/parser": "^5.20.0",
+        "aptos": "^1.8.5",
         "eslint": "^8.13.0",
         "jest": "^27.5.1",
         "joi": "^17.6.0",
@@ -77292,9 +77396,9 @@
       "dev": true
     },
     "aptos": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/aptos/-/aptos-1.6.0.tgz",
-      "integrity": "sha512-5khjDwrDeNMDBFRcZAmETW20D+V2AqTdsgqkh6bvvl70BtRXdkitN0saM05gf1rK3atnO9PyUKO8iRaBDG5qtA==",
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/aptos/-/aptos-1.8.5.tgz",
+      "integrity": "sha512-iQxliWesNHjGQ5YYXCyss9eg4+bDGQWqAZa73vprqGQ9tungK0cRjUI2fmnp63Ed6UG6rurHrL+b0ckbZAOZZQ==",
       "requires": {
         "@noble/hashes": "1.1.3",
         "@scure/bip39": "1.1.0",
@@ -101582,107 +101686,6 @@
       "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
       "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
     },
-    "@pythnetwork/eth-oracle-swap-example-frontend": {
-      "version": "file:target_chains/ethereum/examples/oracle_swap/app",
-      "requires": {
-        "@pythnetwork/pyth-evm-js": "*",
-        "@pythnetwork/pyth-sdk-solidity": "*",
-        "@testing-library/jest-dom": "^5.16.5",
-        "@testing-library/react": "^13.4.0",
-        "@testing-library/user-event": "^13.5.0",
-        "@types/jest": "^27.5.2",
-        "@types/node": "^16.11.64",
-        "@types/react": "^18.0.21",
-        "@types/react-dom": "^18.0.6",
-        "buffer": "^6.0.3",
-        "ethers": "^5.7.2",
-        "metamask-react": "^2.4.0",
-        "prettier": "^2.7.1",
-        "react": "^18.2.0",
-        "react-dom": "^18.2.0",
-        "react-scripts": "5.0.1",
-        "typescript": "^4.8.4",
-        "web-vitals": "^2.1.4",
-        "web3": "^1.8.0"
-      },
-      "dependencies": {
-        "@types/jest": {
-          "version": "27.5.2",
-          "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz",
-          "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==",
-          "requires": {
-            "jest-matcher-utils": "^27.0.0",
-            "pretty-format": "^27.0.0"
-          }
-        },
-        "@types/node": {
-          "version": "16.18.28",
-          "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.28.tgz",
-          "integrity": "sha512-SNMfiPqsiPoYfmyi+2qnDO4nZyMIOCab/CW+Slcml0lhIzkOizYzWtt/A7tgB3TSitd+YJKi8fSC2Cpm/VCp7A=="
-        },
-        "ansi-styles": {
-          "version": "5.2.0",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
-          "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="
-        },
-        "buffer": {
-          "version": "6.0.3",
-          "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
-          "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
-          "requires": {
-            "base64-js": "^1.3.1",
-            "ieee754": "^1.2.1"
-          }
-        },
-        "diff-sequences": {
-          "version": "27.5.1",
-          "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz",
-          "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ=="
-        },
-        "jest-diff": {
-          "version": "27.5.1",
-          "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz",
-          "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==",
-          "requires": {
-            "chalk": "^4.0.0",
-            "diff-sequences": "^27.5.1",
-            "jest-get-type": "^27.5.1",
-            "pretty-format": "^27.5.1"
-          }
-        },
-        "jest-get-type": {
-          "version": "27.5.1",
-          "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz",
-          "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw=="
-        },
-        "jest-matcher-utils": {
-          "version": "27.5.1",
-          "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz",
-          "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==",
-          "requires": {
-            "chalk": "^4.0.0",
-            "jest-diff": "^27.5.1",
-            "jest-get-type": "^27.5.1",
-            "pretty-format": "^27.5.1"
-          }
-        },
-        "pretty-format": {
-          "version": "27.5.1",
-          "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
-          "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
-          "requires": {
-            "ansi-regex": "^5.0.1",
-            "ansi-styles": "^5.0.0",
-            "react-is": "^17.0.1"
-          }
-        },
-        "react-is": {
-          "version": "17.0.2",
-          "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
-          "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
-        }
-      }
-    },
     "traverse-chain": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz",

+ 8 - 0
price_pusher/README.md

@@ -71,6 +71,14 @@ npm run start -- injective --grpc-endpoint https://grpc-endpoint.com \
     [--pushing-frequency 10] \
     [--polling-frequency 5] \
 
+# For Aptos
+npm run start -- aptos --endpoint https://fullnode.testnet.aptoslabs.com/v1 \
+    --pyth-contract-address 0x7e783b349d3e89cf5931af376ebeadbfab855b3fa239b7ada8f5a92fbea6b387 --price-service-endpoint "https://xc-testnet.pyth.network" \
+    --price-config-file "./price-config.testnet.sample.yaml" \
+    --mnemonic-file "path/to/mnemonic.txt" \
+    [--pushing-frequency 10] \
+    [--polling-frequency 5] \
+
 
 # Or, run the price pusher docker image instead of building from the source
 docker run public.ecr.aws/pyth-network/xc-price-pusher:v<version> -- <above-arguments>

+ 7 - 0
price_pusher/config.aptos.testnet.sample.json

@@ -0,0 +1,7 @@
+{
+  "endpoint": "https://fullnode.testnet.aptoslabs.com/v1",
+  "pyth-contract-address": "0x7e783b349d3e89cf5931af376ebeadbfab855b3fa239b7ada8f5a92fbea6b387",
+  "price-service-endpoint": "https://xc-testnet.pyth.network",
+  "mnemonic-file": "./mnemonic",
+  "price-config-file": "./price-config.testnet.sample.yaml"
+}

+ 2 - 1
price_pusher/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/price-pusher",
-  "version": "5.0.0",
+  "version": "5.1.0",
   "description": "Pyth Price Pusher",
   "homepage": "https://pyth.network",
   "main": "lib/index.js",
@@ -55,6 +55,7 @@
     "@pythnetwork/price-service-client": "*",
     "@pythnetwork/pyth-sdk-solidity": "*",
     "@truffle/hdwallet-provider": "^2.1.3",
+    "aptos": "^1.8.5",
     "joi": "^17.6.0",
     "web3": "^1.8.1",
     "web3-eth-contract": "^1.8.1",

+ 190 - 0
price_pusher/src/aptos/aptos.ts

@@ -0,0 +1,190 @@
+import {
+  ChainPriceListener,
+  IPricePusher,
+  PriceInfo,
+  PriceItem,
+} from "../interface";
+import { AptosAccount, AptosClient, TxnBuilderTypes } from "aptos";
+import { DurationInSeconds } from "../utils";
+import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import { PushAttempt } from "../common";
+
+export class AptosPriceListener extends ChainPriceListener {
+  constructor(
+    private pythModule: string,
+    private endpoint: string,
+    priceItems: PriceItem[],
+    config: {
+      pollingFrequency: DurationInSeconds;
+    }
+  ) {
+    super("aptos", config.pollingFrequency, priceItems);
+  }
+
+  async getOnChainPriceInfo(priceId: string): Promise<PriceInfo | undefined> {
+    try {
+      const client = new AptosClient(this.endpoint);
+
+      const res = await client.getAccountResource(
+        this.pythModule,
+        `${this.pythModule}::state::LatestPriceInfo`
+      );
+
+      // This depends upon the pyth contract storage on Aptos and should not be undefined.
+      // If undefined, there has been some change and we would need to update accordingly.
+      const handle = (res.data as any).info.handle;
+
+      const priceItemRes = await client.getTableItem(handle, {
+        key_type: `${this.pythModule}::price_identifier::PriceIdentifier`,
+        value_type: `${this.pythModule}::price_info::PriceInfo`,
+        key: {
+          bytes: priceId,
+        },
+      });
+
+      const multiplier =
+        priceItemRes.price_feed.price.price.negative === true ? -1 : 1;
+      const price =
+        multiplier * Number(priceItemRes.price_feed.price.price.magnitude);
+
+      console.log(
+        `Polled an Aptos on-chain price for feed ${this.priceIdToAlias.get(
+          priceId
+        )} (${priceId}).`
+      );
+
+      return {
+        price: price.toString(),
+        conf: priceItemRes.price_feed.price.conf,
+        publishTime: Number(priceItemRes.price_feed.price.timestamp),
+      };
+    } catch (e) {
+      console.error(
+        `Polling Aptos on-chain price for ${priceId} failed. Error:`
+      );
+      console.error(e);
+      return undefined;
+    }
+  }
+}
+
+export class AptosPricePusher implements IPricePusher {
+  private lastPushAttempt: PushAttempt | undefined;
+
+  private readonly accountHDPath = "m/44'/637'/0'/0'/0'";
+  constructor(
+    private priceServiceConnection: PriceServiceConnection,
+    private pythContractAddress: string,
+    private endpoint: string,
+    private mnemonic: string,
+    private overrideGasPriceMultiplier: number
+  ) {}
+
+  /**
+   * Gets price update data which then can be submitted to the Pyth contract to update the prices.
+   * This will throw an axios error if there is a network problem or the price service returns a non-ok response (e.g: Invalid price ids)
+   *
+   * @param priceIds Array of hex-encoded price ids.
+   * @returns Array of price update data.
+   */
+  async getPriceFeedsUpdateData(priceIds: string[]): Promise<number[][]> {
+    // Fetch the latest price feed update VAAs from the price service
+    const latestVaas = await this.priceServiceConnection.getLatestVaas(
+      priceIds
+    );
+    return latestVaas.map((vaa) => Array.from(Buffer.from(vaa, "base64")));
+  }
+
+  async updatePriceFeed(
+    priceIds: string[],
+    pubTimesToPush: number[]
+  ): Promise<void> {
+    if (priceIds.length === 0) {
+      return;
+    }
+
+    if (priceIds.length !== pubTimesToPush.length)
+      throw new Error("Invalid arguments");
+
+    let priceFeedUpdateData;
+    try {
+      // get the latest VAAs for updatePriceFeed and then push them
+      priceFeedUpdateData = await this.getPriceFeedsUpdateData(priceIds);
+    } catch (e) {
+      console.error("Error fetching the latest vaas to push");
+      console.error(e);
+      return;
+    }
+
+    try {
+      const account = AptosAccount.fromDerivePath(
+        this.accountHDPath,
+        this.mnemonic
+      );
+      const client = new AptosClient(this.endpoint);
+
+      const rawTx = await client.generateTransaction(account.address(), {
+        function: `${this.pythContractAddress}::pyth::update_price_feeds_if_fresh_with_funder`,
+        type_arguments: [],
+        arguments: [
+          priceFeedUpdateData,
+          priceIds.map((priceId) => Buffer.from(priceId, "hex")),
+          pubTimesToPush,
+        ],
+      });
+
+      const simulation = await client.simulateTransaction(account, rawTx, {
+        estimateGasUnitPrice: true,
+        estimateMaxGasAmount: true,
+        estimatePrioritizedGasUnitPrice: true,
+      });
+
+      // Transactions on Aptos can be prioritized by paying a higher gas unit price.
+      // We are storing the gas unit price paid for the last transaction.
+      // If that transaction is not added to the block, we are increasing the gas unit price
+      // by multiplying the old gas unit price with `this.overrideGasPriceMultiplier`.
+      // After which we are sending a transaction with the same sequence number as the last
+      // transaction. Since they have the same sequence number only one of them will be added to
+      // the block and we won't be paying fees twice.
+      let gasUnitPrice = Number(simulation[0].gas_unit_price);
+      if (
+        this.lastPushAttempt !== undefined &&
+        Number(simulation[0].sequence_number) === this.lastPushAttempt.nonce
+      ) {
+        const newGasUnitPrice = Number(
+          this.lastPushAttempt.gasPrice * this.overrideGasPriceMultiplier
+        );
+        if (gasUnitPrice < newGasUnitPrice) gasUnitPrice = newGasUnitPrice;
+      }
+
+      const gasUsed = Number(simulation[0].gas_used) * 1.5;
+      const maxGasAmount = Number(gasUnitPrice * gasUsed);
+
+      const rawTxWithFee = new TxnBuilderTypes.RawTransaction(
+        rawTx.sender,
+        rawTx.sequence_number,
+        rawTx.payload,
+        BigInt(maxGasAmount.toFixed()),
+        BigInt(gasUnitPrice.toFixed()),
+        rawTx.expiration_timestamp_secs,
+        rawTx.chain_id
+      );
+
+      const signedTx = await client.signTransaction(account, rawTxWithFee);
+      const pendingTx = await client.submitTransaction(signedTx);
+
+      console.log("Succesfully broadcasted txHash:", pendingTx.hash);
+
+      // Update lastAttempt
+      this.lastPushAttempt = {
+        nonce: Number(pendingTx.sequence_number),
+        gasPrice: gasUnitPrice,
+      };
+      return;
+    } catch (e: any) {
+      console.error("Error executing messages");
+      console.log(e);
+      return;
+    }
+  }
+}

+ 96 - 0
price_pusher/src/aptos/command.ts

@@ -0,0 +1,96 @@
+import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import * as options from "../options";
+import { readPriceConfigFile } from "../price-config";
+import fs from "fs";
+import { PythPriceListener } from "../pyth-price-listener";
+import { Controller } from "../controller";
+import { Options } from "yargs";
+import { AptosPriceListener, AptosPricePusher } from "./aptos";
+
+export default {
+  command: "aptos",
+  describe: "run price pusher for aptos",
+  builder: {
+    endpoint: {
+      description:
+        "RPC endpoint endpoint URL for aptos. The pusher will periodically" +
+        "poll for updates. The polling interval is configurable via the " +
+        "`polling-frequency` command-line argument.",
+      type: "string",
+      required: true,
+    } as Options,
+    "override-gas-price-multiplier": {
+      description:
+        "Multiply the gas price by this number if the transaction is not landing to override it. Default 2",
+      type: "number",
+      required: false,
+      default: 2,
+    } as Options,
+    ...options.priceConfigFile,
+    ...options.priceServiceEndpoint,
+    ...options.mnemonicFile,
+    ...options.pythContractAddress,
+    ...options.pollingFrequency,
+    ...options.pushingFrequency,
+  },
+  handler: function (argv: any) {
+    // FIXME: type checks for this
+    const {
+      endpoint,
+      priceConfigFile,
+      priceServiceEndpoint,
+      mnemonicFile,
+      pythContractAddress,
+      pushingFrequency,
+      pollingFrequency,
+      overrideGasPriceMultiplier,
+    } = argv;
+
+    const priceConfigs = readPriceConfigFile(priceConfigFile);
+    const priceServiceConnection = new PriceServiceConnection(
+      priceServiceEndpoint,
+      {
+        logger: {
+          // Log only warnings and errors from the price service client
+          info: () => undefined,
+          warn: console.warn,
+          error: console.error,
+          debug: () => undefined,
+          trace: () => undefined,
+        },
+      }
+    );
+    const mnemonic = fs.readFileSync(mnemonicFile, "utf-8").trim();
+
+    const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+
+    const pythListener = new PythPriceListener(
+      priceServiceConnection,
+      priceItems
+    );
+
+    const aptosListener = new AptosPriceListener(
+      pythContractAddress,
+      endpoint,
+      priceItems,
+      { pollingFrequency }
+    );
+    const aptosPusher = new AptosPricePusher(
+      priceServiceConnection,
+      pythContractAddress,
+      endpoint,
+      mnemonic,
+      overrideGasPriceMultiplier
+    );
+
+    const controller = new Controller(
+      priceConfigs,
+      pythListener,
+      aptosListener,
+      aptosPusher,
+      { pushingFrequency }
+    );
+
+    controller.start();
+  },
+};

+ 4 - 0
price_pusher/src/common.ts

@@ -0,0 +1,4 @@
+export type PushAttempt = {
+  nonce: number;
+  gasPrice: number;
+};

+ 1 - 5
price_pusher/src/evm/evm.ts

@@ -18,6 +18,7 @@ import {
 } from "@pythnetwork/price-service-client";
 import { CustomGasStation } from "./custom-gas-station";
 import { Provider } from "web3/providers";
+import { PushAttempt } from "../common";
 
 export class EvmPriceListener extends ChainPriceListener {
   private pythContractFactory: PythContractFactory;
@@ -117,11 +118,6 @@ export class EvmPriceListener extends ChainPriceListener {
   }
 }
 
-type PushAttempt = {
-  nonce: number;
-  gasPrice: number;
-};
-
 export class EvmPricePusher implements IPricePusher {
   private customGasStation?: CustomGasStation;
   private pythContract: Contract;

+ 2 - 0
price_pusher/src/index.ts

@@ -3,10 +3,12 @@ import yargs from "yargs";
 import { hideBin } from "yargs/helpers";
 import injective from "./injective/command";
 import evm from "./evm/command";
+import aptos from "./aptos/command";
 
 yargs(hideBin(process.argv))
   .config("config")
   .global("config")
   .command(evm)
   .command(injective)
+  .command(aptos)
   .help().argv;