Browse Source

p2w-terra-relay: Add a query() EVM call and Tilt boilerplate

commit-id:f97d0c16
Stan Drozd 3 years ago
parent
commit
4776ff30c7

+ 50 - 0
devnet/p2w-terra-relay.yaml

@@ -45,6 +45,11 @@ spec:
               protocol: TCP
           tcpSocket:
             port: 2000
+          command:
+            - node
+            - lib/index.js
+            - "--"
+            - "--terra"
           env:
             - name: SPY_SERVICE_HOST
               value: spy:7072
@@ -78,3 +83,48 @@ spec:
               value: '1'
             - name: LOG_LEVEL
               value: debug
+        - name: p2w-evm-relay
+          image: p2w-terra-relay
+          ports:
+            - containerPort: 8082
+              name: prometheus
+              protocol: TCP
+            - containerPort: 4201
+              name: rest-api
+              protocol: TCP
+          tcpSocket:
+            port: 2001
+          command:
+            - node
+            - lib/index.js
+            - "--"
+            - "--evm"
+          env:
+            - name: SPY_SERVICE_HOST
+              value: spy:7072
+            - name: SPY_SERVICE_FILTERS
+              value: '[{"chain_id":1,"emitter_address":"71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b"}]'
+            - name: EVM_NODE_WEBSOCKET_URL
+              value: "ws://eth-devnet:8545"
+            - name: EVM_WALLET_MNEMONIC_FILE
+              value: "./devnet_mnemonic.txt"
+            - name: EVM_HDWALLET_PATH
+              value: "m/44'/60'/0'/0/1" # Use account with idx 1
+            - name: EVM_PYTH_CONTRACT_ADDRESS
+              value: "0x0E696947A06550DEf604e82C26fd9E493e576337"
+            - name: REST_PORT
+              value: '4201'
+            - name: PROM_PORT
+              value: '8082'
+            - name: BAL_QUERY_INTERVAL
+              value: '60000'
+            - name: READINESS_PORT
+              value: '2001'
+            - name: RETRY_MAX_ATTEMPTS
+              value: '4'
+            - name: RETRY_DELAY_IN_MS
+              value: '250'
+            - name: MAX_MSGS_PER_BATCH
+              value: '1'
+            - name: LOG_LEVEL
+              value: debug

+ 1 - 2
third_party/pyth/p2w-terra-relay/Dockerfile.pyth_relay

@@ -22,7 +22,6 @@ RUN npm ci && npm run build && npm cache clean --force
 
 RUN mkdir -p /app/pyth_relay/logs
 RUN addgroup -S pyth -g 10001 && adduser -S pyth -G pyth -u 10001
+RUN cp ${P2W_BASE_PATH}/ethereum/devnet_mnemonic.txt .
 RUN chown -R pyth:pyth .
 USER pyth
-
-CMD [ "node", "lib/index.js" ]

+ 53 - 37
third_party/pyth/p2w-terra-relay/src/helpers.ts

@@ -147,7 +147,8 @@ In version 2 prices are sent in batch with the following structure:
 */
 
 export const PYTH_PRICE_ATTESTATION_MIN_LENGTH: number = 150;
-export const PYTH_BATCH_PRICE_ATTESTATION_MIN_LENGTH: number = 11 + PYTH_PRICE_ATTESTATION_MIN_LENGTH;
+export const PYTH_BATCH_PRICE_ATTESTATION_MIN_LENGTH: number =
+  11 + PYTH_PRICE_ATTESTATION_MIN_LENGTH;
 
 export type PythRational = {
   value: BigInt;
@@ -174,12 +175,12 @@ export type PythPriceAttestation = {
 
 export type PythBatchPriceAttestation = {
   magic: number;
-  version: number; 
+  version: number;
   payloadId: number;
   nAttestations: number;
   attestationSize: number;
   priceAttestations: PythPriceAttestation[];
-}
+};
 
 export const PYTH_MAGIC: number = 0x50325748;
 
@@ -225,14 +226,18 @@ export function parsePythPriceAttestation(arr: Buffer): PythPriceAttestation {
   };
 }
 
-export function parsePythBatchPriceAttestation(arr: Buffer): PythBatchPriceAttestation {
+export function parsePythBatchPriceAttestation(
+  arr: Buffer
+): PythBatchPriceAttestation {
   if (!isPyth(arr)) {
-    throw new Error("Cannot parse payload. Header mismatch: This is not a Pyth 2 Wormhole message");
+    throw new Error(
+      "Cannot parse payload. Header mismatch: This is not a Pyth 2 Wormhole message"
+    );
   }
 
   if (arr.length < PYTH_BATCH_PRICE_ATTESTATION_MIN_LENGTH) {
     throw new Error(
-        "Cannot parse payload. Payload length is wrong: length: " +
+      "Cannot parse payload. Payload length is wrong: length: " +
         arr.length +
         ", expected length to be at least:" +
         PYTH_BATCH_PRICE_ATTESTATION_MIN_LENGTH
@@ -251,45 +256,56 @@ export function parsePythBatchPriceAttestation(arr: Buffer): PythBatchPriceAttes
     );
   }
 
-  let priceAttestations: PythPriceAttestation[] = []
+  let priceAttestations: PythPriceAttestation[] = [];
 
   let offset = 11;
   for (let i = 0; i < nAttestations; i += 1) {
-    priceAttestations.push(parsePythPriceAttestation(arr.subarray(offset, offset + attestationSize)));
+    priceAttestations.push(
+      parsePythPriceAttestation(arr.subarray(offset, offset + attestationSize))
+    );
     offset += attestationSize;
   }
 
   return {
-      magic,
-      version,
-      payloadId,
-      nAttestations,
-      attestationSize,
-      priceAttestations
-    }
+    magic,
+    version,
+    payloadId,
+    nAttestations,
+    attestationSize,
+    priceAttestations,
+  };
 }
 
 // Returns a hash of all priceIds within the batch, it can be used to identify whether there is a
 // new batch with exact same symbols (and ignore the old one)
-export function getBatchAttestationHashKey(batchAttestation: PythBatchPriceAttestation): string {
-  const priceIds: string[] = batchAttestation.priceAttestations.map((priceAttestation) => priceAttestation.priceId);
+export function getBatchAttestationHashKey(
+  batchAttestation: PythBatchPriceAttestation
+): string {
+  const priceIds: string[] = batchAttestation.priceAttestations.map(
+    (priceAttestation) => priceAttestation.priceId
+  );
   priceIds.sort();
 
-  return priceIds.join('#');
+  return priceIds.join("#");
 }
 
-export function getBatchSummary(batchAttestation: PythBatchPriceAttestation): string {
+export function getBatchSummary(
+  batchAttestation: PythBatchPriceAttestation
+): string {
   let abstractRepresentation = {
-    "num_attestations": batchAttestation.nAttestations,
-    "prices": batchAttestation.priceAttestations.map((priceAttestation) => {
-        return {
-          "price_id": priceAttestation.priceId,
-          "price": computePrice(priceAttestation.price, priceAttestation.exponent),
-          "conf": computePrice(priceAttestation.confidenceInterval, priceAttestation.exponent)
-        }
-    })
-  }
-  return JSON.stringify(abstractRepresentation)
+    num_attestations: batchAttestation.nAttestations,
+    prices: batchAttestation.priceAttestations.map((priceAttestation) => {
+      return {
+        price_id: priceAttestation.priceId,
+        price: computePrice(priceAttestation.price, priceAttestation.exponent),
+        conf: computePrice(
+          priceAttestation.confidenceInterval,
+          priceAttestation.exponent
+        ),
+      };
+    }),
+  };
+  return JSON.stringify(abstractRepresentation);
 }
 
 ////////////////////////////////// Start of Other Helpful Stuff //////////////////////////////////////
@@ -304,13 +320,13 @@ export function computePrice(rawPrice: BigInt, expo: number): number {
 
 // Shorthand for optional/mandatory envs
 export function envOrErr(env: string, defaultValue?: string): string {
-    let val = process.env[env];
-    if (!val) {
-	if (!defaultValue) {
-	    throw `environment variable "${env}" must be set`;
-	} else {
-	    return defaultValue;
-	}
+  let val = process.env[env];
+  if (!val) {
+    if (!defaultValue) {
+      throw `environment variable "${env}" must be set`;
+    } else {
+      return defaultValue;
     }
-    return String(process.env[env]);
+  }
+  return String(process.env[env]);
 }

+ 31 - 9
third_party/pyth/p2w-terra-relay/src/index.ts

@@ -1,5 +1,6 @@
 import { setDefaultWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
 
+import * as fs from "fs";
 import * as listen from "./listen";
 import * as worker from "./worker";
 import * as rest from "./rest";
@@ -9,6 +10,7 @@ import { PromHelper } from "./promHelpers";
 
 import { Relay } from "./relay/iface";
 import { TerraRelay } from "./relay/terra";
+import { EvmRelay } from "./relay/evm";
 
 let configFile: string = ".env";
 if (process.env.PYTH_RELAY_CONFIG) {
@@ -25,26 +27,46 @@ helpers.initLogger();
 
 let error: boolean = false;
 let listenOnly: boolean = false;
-let relayImpl: Relay;
+let relayImpl: Relay | null = null;
 for (let idx = 0; idx < process.argv.length; ++idx) {
   if (process.argv[idx] === "--listen_only") {
     logger.info("running in listen only mode, will not relay anything!");
     listenOnly = true;
+  } else if (process.argv[idx] === "--terra" && !relayImpl) {
+    relayImpl = new TerraRelay({
+      nodeUrl: helpers.envOrErr("TERRA_NODE_URL"),
+      terraChainId: helpers.envOrErr("TERRA_CHAIN_ID"),
+      walletPrivateKey: helpers.envOrErr("TERRA_PRIVATE_KEY"),
+      coin: helpers.envOrErr("TERRA_COIN"),
+      contractAddress: helpers.envOrErr("TERRA_PYTH_CONTRACT_ADDRESS"),
+    });
+    logger.info("Relaying to Terra");
+  } else if (process.argv[idx] === "--evm" && !relayImpl) {
+    relayImpl = new EvmRelay({
+      rpcWsUrl: helpers.envOrErr("EVM_NODE_WEBSOCKET_URL"),
+      payerWalletMnemonic: fs
+        .readFileSync(helpers.envOrErr("EVM_WALLET_MNEMONIC_FILE"))
+        .toString("utf-8")
+        .trim(),
+      payerHDWalletPath: helpers.envOrErr(
+        "EVM_HDWALLET_PATH",
+        "m/44'/60'/0'/0"
+      ), // ETH mainnet default
+      p2wContractAddress: helpers.envOrErr("EVM_PYTH_CONTRACT_ADDRESS"),
+    });
+    logger.info("Relaying to EVM.");
   }
 }
 
-relayImpl = new TerraRelay({
-  nodeUrl: helpers.envOrErr("TERRA_NODE_URL"),
-  terraChainId: helpers.envOrErr("TERRA_CHAIN_ID"),
-  walletPrivateKey: helpers.envOrErr("TERRA_PRIVATE_KEY"),
-  coin: helpers.envOrErr("TERRA_COIN"),
-  contractAddress: helpers.envOrErr("TERRA_PYTH_CONTRACT_ADDRESS"),
-});
+if (!relayImpl) {
+  logger.error("No relay implementation specified");
+  error = true;
+}
 
 if (
   !error &&
   listen.init(listenOnly) &&
-  worker.init(!listenOnly, relayImpl) &&
+  worker.init(!listenOnly, relayImpl as any) &&
   rest.init(!listenOnly)
 ) {
   // Start the Prometheus client with the app name and http port

+ 56 - 6
third_party/pyth/p2w-terra-relay/src/relay/evm.ts

@@ -11,30 +11,80 @@ export class EvmRelay implements Relay {
   payerWallet: ethers.Wallet;
   p2wContract: PythImplementation;
   async relay(signedVAAs: Array<string>): Promise<RelayResult> {
-    logger.warn("EvmRelay.relay(): TODO(2021-03-22)");
-    return new RelayResult(RelayRetcode.Fail, []);
+    let batchCount = signedVAAs.length;
+
+    // Schedule all received batches in parallel
+    let txs = [];
+    for (let i = 0; i < signedVAAs.length; ++i) {
+      let tx = await this.p2wContract
+        .attestPriceBatch("0x" + signedVAAs[i], { gasLimit: 1000000 })
+        .then(async (pending) => {
+          try {
+            let receipt = await pending.wait();
+            logger.info(`Batch ${i + 1}/${batchCount} tx OK`);
+            return new RelayResult(RelayRetcode.Success, [
+              receipt.transactionHash,
+            ]);
+          } catch (e: any) {
+            logger.error(
+              `Batch ${i + 1}/${batchCount} tx failed: ${
+                e.code
+              }, failed tx hash ${e.transactionHash}`
+            );
+            logger.error(
+              `Batch ${i + 1}/${batchCount} failure details: ${JSON.stringify(
+                e
+              )}`
+            );
+          }
+          return new RelayResult(RelayRetcode.Fail, []);
+        });
+
+      txs.push(tx);
+    }
+
+    logger.info(`scheduled ${txs.length} EVM transaction(s)`);
+
+    let results = await Promise.all(txs);
+
+    let ok = true;
+    let txHashes: Array<string> = [];
+    for (let res of results) {
+      if (res.is_ok()) {
+        txHashes.concat(res.txHashes);
+      } else {
+        ok = false;
+      }
+    }
+
+    // TODO(2021-03-23): Make error reporting for caller more granular (Array<RelayResult>, retries etc.)
+    if (ok) {
+      return new RelayResult(RelayRetcode.Success, txHashes);
+    } else {
+      return new RelayResult(RelayRetcode.Fail, []);
+    }
   }
   async query(priceId: PriceId): Promise<any> {
-    logger.warn("EvmRelay.relay(): TODO(2021-03-22)");
+    logger.warn("EvmRelay.query(): TODO(2021-03-22)");
     return new RelayResult(RelayRetcode.Fail, []);
   }
   async getPayerInfo(): Promise<{ address: string; balance: bigint }> {
     return {
       address: this.payerWallet.address,
-      balance: BigInt(`$(await this.payerWallet.getBalance())`),
+      balance: BigInt(`${await this.payerWallet.getBalance()}`),
     };
   }
 
   constructor(cfg: {
     rpcWsUrl: string;
     payerWalletMnemonic: string;
-    payerWalletHDPath: string;
+    payerHDWalletPath: string;
     p2wContractAddress: string;
   }) {
     let provider = new ethers.providers.WebSocketProvider(cfg.rpcWsUrl);
     let wallet = ethers.Wallet.fromMnemonic(
       cfg.payerWalletMnemonic,
-      cfg.payerWalletHDPath
+      cfg.payerHDWalletPath
     );
 
     this.payerWallet = new ethers.Wallet(wallet.privateKey, provider);