소스 검색

Pyth terra bridge: add contract deployment script (#88)

* Add pyth deployment script

- Also updates build.sh to build pyth completely
- Add a readme for deployment guide
Ali Behjati 3 년 전
부모
커밋
7d41fb1ea1
5개의 변경된 파일341개의 추가작업 그리고 2개의 파일을 삭제
  1. 1 0
      terra/.gitignore
  2. 88 0
      terra/README.pyth.md
  3. 2 1
      terra/build.sh
  4. 248 0
      terra/tools/deploy-pyth-bridge.js
  5. 2 1
      terra/tools/package.json

+ 1 - 0
terra/.gitignore

@@ -0,0 +1 @@
+artifacts/

+ 88 - 0
terra/README.pyth.md

@@ -0,0 +1,88 @@
+# Intro
+
+Deploying a contract in terra consists of two steps:
+1. Uploading the code. This step will give you a code id.
+2. Optionally create a new contract or migrate an existing one:
+    1. Creating a new contract which has an address with a code id as its program.
+    2. Migrating an existing contract code id to the new code id.
+
+This script can do both steps at the same time. Read below for the details.
+
+# Uploading the code
+
+First build the contracts:
+
+``` sh
+bash build.sh
+```
+
+This command will builds and saves all the contracts in the `artifact` directory.
+
+Then, for example, to deploy `pyth_bridge.wasm`, run in the `tools` directory:
+
+``` sh
+npm ci # Do it only once to install required packages
+npm run deploy-pyth -- --network testnet --artifact ../artifacts/pyth_bridge.wasm --mnemonic "..."
+```
+
+which will print something along the lines of:
+
+``` sh
+Storing WASM: ../artifacts/pyth_bridge.wasm (367689 bytes)
+Deploy fee:  88446uluna
+Code ID:  2435
+```
+
+If you do not pass any additional arguments to the script it will only upload the code and returns the code id. If you want to create a 
+new contract or upgrade an existing contract you should pass more arguments that are described below.
+
+# Instantiating new contract
+If you want instantiate a new contract after your deployment pass `--instantiate` argument to the above command.
+It will upload the code and with the resulting code id instantiates a new pyth contract:
+
+``` sh
+npm run deploy-pyth -- --network testnet --artifact ../artifacts/pyth_bridge.wasm --mnemonic "..." --instantiate
+```
+
+If successful, the output should look like:
+```
+Storing WASM: ../artifacts/pyth_bridge.wasm (183749 bytes)
+Deploy fee:  44682uluna
+Code ID:  53199
+Instantiating a contract
+Sleeping for 10 seconds for store transaction to finalize.
+Instantiated Pyth Bridge at terra123456789yelw23uh22nadqlyjvtl7s5527er97 (0x0000000000000000000000001234567896267ee5479752a7d683e49317ff4294)
+Deployed pyth contract at terra123456789yelw23uh22nadqlyjvtl7s5527er97
+```
+
+# Migrating existing contract
+If you want to upgrade an existing contract pass `--migrate --contract terra123456xyzqwe..` arguments to the above command.
+It will upload the code and with the resulting code id migrates the existing contract to the new one:
+
+``` sh
+npm run deploy-pyth -- --network testnet --artifact ../artifacts/pyth_bridge.wasm --mnemonic "..." --migrate --contract "terra123..."
+```
+
+If successful, the output should look like:
+```
+Storing WASM: ../artifacts/pyth_bridge.wasm (183749 bytes)
+Deploy fee:  44682uluna
+Code ID:  53227
+Sleeping for 10 seconds for store transaction to finalize.
+Migrating contract terra1rhjej5gkyelw23uh22nadqlyjvtl7s5527er97 to 53227
+Contract terra1rhjej5gkyelw23uh22nadqlyjvtl7s5527er97 code_id successfully updated to 53227
+```
+
+# Notes
+
+You might encounter gateway timeout or account sequence mismatch in errors. In is good to double check with terra finder as sometimes
+transactions succeed despite being timed out.
+
+If that happens in the middle of an instantiation or migration. You can avoid re-uploading the code and use the resulting Code Id 
+by passing `--code-id <codeId>` instead of `--artifact` and it will only do the instantiation/migration part.
+
+An example is:
+
+``` sh
+npm run deploy-pyth -- --network testnet --code-id 50123 --mnemonic "..." --migrate --contract "terra123..."
+```

+ 2 - 1
terra/build.sh

@@ -1,6 +1,7 @@
 #!/usr/bin/env bash
 
 docker run --rm -v "$(pwd)":/code \
+  -v $(cd ../third_party; pwd):/third_party \
   --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
   --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
-  cosmwasm/workspace-optimizer:0.12.1
+  cosmwasm/workspace-optimizer:0.12.5

+ 248 - 0
terra/tools/deploy-pyth-bridge.js

@@ -0,0 +1,248 @@
+import { LCDClient, MnemonicKey } from "@terra-money/terra.js";
+import {
+  MsgInstantiateContract,
+  MsgMigrateContract,
+  MsgStoreCode,
+} from "@terra-money/terra.js";
+import { readFileSync } from "fs";
+import { Bech32, toHex } from "@cosmjs/encoding";
+import { zeroPad } from "ethers/lib/utils.js";
+import axios from "axios";
+import yargs from "yargs";
+import {hideBin} from "yargs/helpers";
+import assert from "assert";
+
+export const TERRA_GAS_PRICES_URL = "https://fcd.terra.dev/v1/txs/gas_prices";
+
+const argv = yargs(hideBin(process.argv))
+  .option('network', {
+    description: 'Which network to deploy to',
+    choices: ['mainnet', 'testnet'],
+    required: true
+  })
+  .option('artifact', {
+    description: 'Path to Pyth artifact',
+    type: 'string',
+    required: false
+  })
+  .option('mnemonic', {
+    description: 'Mnemonic (private key)',
+    type: 'string',
+    required: true
+  })
+  .option('instantiate', {
+    description: 'Instantiate contract if set (default: disabled)',
+    type: 'boolean',
+    default: false,
+    required: false
+  })
+  .option('migrate', {
+    description: 'Migrate an existing contract if set (default: disabled)',
+    type: 'boolean',
+    default: false,
+    required: false
+  })
+  .option('contract', {
+    description: 'Contract address, used only for migration',
+    type: 'string',
+    required: false,
+    default: ''
+  })
+  .option('code-id', {
+    description: 'Code Id, if provided this will be used for migrate/instantiate and no code will be uploaded',
+    type: 'number',
+    requred: false
+  })
+  .help()
+  .alias('help', 'h').argv;
+
+const artifact = argv.artifact;
+
+/* Set up terra client & wallet. It won't fail because inputs are validated with yargs */
+
+const CONFIG = {
+  mainnet: {
+    terraHost: {
+      URL: "https://lcd.terra.dev",
+      chainID: "columbus-5",
+      name: "mainnet",
+    },
+    wormholeContract: "terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5",
+    pythEmitterAddress: "6bb14509a612f01fbbc4cffeebd4bbfb492a86df717ebe92eb6df432a3f00a25"
+  },
+  testnet: {
+    terraHost: {
+      URL: "https://bombay-lcd.terra.dev",
+      chainID: "bombay-12",
+      name: "testnet",
+    },
+    wormholeContract: "terra1pd65m0q9tl3v8znnz5f5ltsfegyzah7g42cx5v",
+    pythEmitterAddress: "f346195ac02f37d60d4db8ffa6ef74cb1be3550047543a4a9ee9acf4d78697b0"
+  }
+}
+
+const terraHost = CONFIG[argv.network].terraHost;
+const wormholeContract = CONFIG[argv.network].wormholeContract;
+const pythEmitterAddress = CONFIG[argv.network].pythEmitterAddress;
+  
+const lcd = new LCDClient(terraHost);
+
+const feeDenoms = ["uluna"];
+
+const gasPrices = await axios
+  .get(TERRA_GAS_PRICES_URL)
+  .then((result) => result.data);
+
+const wallet = lcd.wallet(
+  new MnemonicKey({
+    mnemonic: argv.mnemonic
+  })
+);
+
+/* Deploy artifacts */
+
+var codeId; 
+
+if (argv.codeId !== undefined) {
+  codeId = argv.codeId;
+} else {
+  if (argv.artifact === undefined) {
+    console.error("Artifact is not provided. Please at least provide artifact or code id");
+    process.exit(1);
+  }
+
+  const contract_bytes = readFileSync(artifact);
+  console.log(`Storing WASM: ${artifact} (${contract_bytes.length} bytes)`);
+
+  const store_code = new MsgStoreCode(
+    wallet.key.accAddress,
+    contract_bytes.toString("base64")
+  );
+
+  const feeEstimate = await lcd.tx.estimateFee(
+    wallet.key.accAddress,
+    [store_code],
+    {
+      feeDenoms,
+      gasPrices,
+    }
+  );
+
+  console.log("Deploy fee: ", feeEstimate.amount.toString());
+
+  const tx = await wallet.createAndSignTx({
+    msgs: [store_code],
+    feeDenoms,
+    gasPrices,
+    fee: feeEstimate,
+  });
+
+  const rs = await lcd.tx.broadcast(tx);
+
+  try {
+    const ci = /"code_id","value":"([^"]+)/gm.exec(rs.raw_log)[1];
+    codeId = parseInt(ci);
+  } catch(e) {
+    console.error("Encountered an error in parsing deploy code result. Printing raw log")
+    console.error(rs.raw_log);
+    throw(e);
+  }
+
+  console.log("Code ID: ", codeId);
+
+  if (argv.instantiate || argv.migrate) {
+    console.log("Sleeping for 10 seconds for store transaction to finalize.");
+    await sleep(10000);
+  }
+}
+
+if (argv.instantiate) {
+  console.log("Instantiating a contract");
+
+  async function instantiate(codeId, inst_msg) {
+    var address;
+    await wallet
+      .createAndSignTx({
+        msgs: [
+          new MsgInstantiateContract(
+            wallet.key.accAddress,
+            wallet.key.accAddress,
+            codeId,
+            inst_msg
+          ),
+        ],
+      })
+      .then((tx) => lcd.tx.broadcast(tx))
+      .then((rs) => {
+        try {
+          address = /"contract_address","value":"([^"]+)/gm.exec(rs.raw_log)[1];
+        } catch (e) { 
+          console.error("Encountered an error in parsing instantiation result. Printing raw log")
+          console.error(rs.raw_log);
+          throw(e);
+        }
+      });
+    console.log(`Instantiated Pyth Bridge at ${address} (${convert_terra_address_to_hex(address)})`);
+    return address;
+  }
+
+  const pythChain = 1;
+
+  const contractAddress = await instantiate(codeId, {
+    wormhole_contract: wormholeContract,
+    pyth_emitter: Buffer.from(pythEmitterAddress, "hex").toString(
+      "base64"
+    ),
+    pyth_emitter_chain: pythChain,
+  });
+
+  console.log(`Deployed pyth contract at ${contractAddress}`);
+}
+
+if (argv.migrate) {
+  if (argv.contract === '') {
+    console.error("Contract address is not provided. Provide it using --contract");
+    process.exit(1);
+  }
+
+  console.log(`Migrating contract ${argv.contract} to ${codeId}`);
+
+  const tx = await wallet.createAndSignTx({
+    msgs: [
+      new MsgMigrateContract(
+        wallet.key.accAddress,
+        argv.contract,
+        codeId,
+        {
+          "action": ""
+        },
+        { uluna: 1000 }
+      ),
+    ],
+    feeDenoms,
+    gasPrices,
+  });
+  
+  const rs = await lcd.tx.broadcast(tx);
+  var resultCodeId;
+  try {
+    resultCodeId = /"code_id","value":"([^"]+)/gm.exec(rs.raw_log)[1];
+    assert.equal(codeId, resultCodeId)
+  } catch (e) {
+    console.error("Encountered an error in parsing migration result. Printing raw log")
+    console.error(rs.raw_log);
+    throw(e);
+  }
+
+  console.log(`Contract ${argv.contract} code_id successfully updated to ${resultCodeId}`);
+}
+
+// Terra addresses are "human-readable", but for cross-chain registrations, we
+// want the "canonical" version
+function convert_terra_address_to_hex(human_addr) {
+  return "0x" + toHex(zeroPad(Bech32.decode(human_addr).data, 32));
+}
+
+function sleep(ms) {
+  return new Promise(resolve => setTimeout(resolve, ms));
+}

+ 2 - 1
terra/tools/package.json

@@ -5,7 +5,8 @@
   "main": "deploy.js",
   "type": "module",
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "deploy-pyth": "node deploy-pyth-bridge.js"
   },
   "author": "",
   "license": "ISC",