Procházet zdrojové kódy

[contract-manager] More utility scripts and docs (#1222)

* Remove mainnet condition in check proposal

Testnet contracts can also listen to mainnet governance so this check does not make
sense anymore

* Simple script for executing vaas

* Add script for evm upgrades

* Script to list evm contracts with their versions

* More docs
Amin Moghaddam před 1 rokem
rodič
revize
084f2018a6

+ 2 - 0
contract_manager/.gitignore

@@ -1 +1,3 @@
 lib/
+.cache*
+docs

+ 18 - 0
contract_manager/README.md

@@ -7,3 +7,21 @@ It has the following structure:
 - `store` contains all the necessary information for registered chains and deployed contracts
 - `scripts` contains utility scripts to interact with the contract manager and accomplish common tasks
 - `src` contains the contract manager code
+
+# Main Entities
+
+Contract Manager has base classes which you can use to interact with the following entities:
+
+- Chain
+- PythContract
+- WormholeContract
+
+Each of these entities has a specialized class for each supported chain (EVM/Cosmos/Aptos/Sui).
+
+# Docs
+
+You can generate the docs by running `npx typedoc src/index.ts` from this directory. Open the docs by opening `docs/index.html` in your browser.
+
+# Scripts
+
+You can run the scripts by executing `npx ts-node scripts/<script_name>.ts` from this directory.

+ 4 - 3
contract_manager/package.json

@@ -20,18 +20,19 @@
     "url": "git+https://github.com/pyth-network/pyth-crosschain.git"
   },
   "dependencies": {
-    "@mysten/sui.js": "^0.37.1",
     "@certusone/wormhole-sdk": "^0.9.8",
+    "@injectivelabs/networks": "1.0.68",
+    "@mysten/sui.js": "^0.37.1",
     "@pythnetwork/cosmwasm-deploy-tools": "*",
     "@pythnetwork/price-service-client": "*",
     "@pythnetwork/pyth-sui-js": "*",
-    "@injectivelabs/networks": "1.0.68",
     "aptos": "^1.5.0",
     "bs58": "^5.0.0",
     "ts-node": "^10.9.1",
     "typescript": "^4.9.3"
   },
   "devDependencies": {
-    "prettier": "^2.6.2"
+    "prettier": "^2.6.2",
+    "typedoc": "^0.25.7"
   }
 }

+ 0 - 1
contract_manager/scripts/check_proposal.ts

@@ -63,7 +63,6 @@ async function main() {
         for (const chain of Object.values(DefaultStore.chains)) {
           if (
             chain instanceof EvmChain &&
-            chain.isMainnet() === (cluster === "mainnet-beta") &&
             chain.wormholeChainName ===
               instruction.governanceAction.targetChainId
           ) {

+ 88 - 0
contract_manager/scripts/execute_vaas.ts

@@ -0,0 +1,88 @@
+import yargs from "yargs";
+import { hideBin } from "yargs/helpers";
+import { DefaultStore } from "../src/store";
+import { SubmittedWormholeMessage, Vault } from "../src/governance";
+import { parseVaa } from "@certusone/wormhole-sdk";
+import { decodeGovernancePayload } from "xc_admin_common";
+import { executeVaa } from "../src/executor";
+import { toPrivateKey } from "../src";
+
+const parser = yargs(hideBin(process.argv))
+  .usage(
+    "Tries to execute all vaas on a vault.\n" +
+      "Useful for batch upgrades.\n" +
+      "Usage: $0 --vault <mainnet|devnet> --private-key <private-key> --offset <offset> [--dryrun]"
+  )
+  .options({
+    vault: {
+      type: "string",
+      default: "mainnet",
+      choices: ["mainnet", "devnet"],
+      desc: "Which vault to use for fetching VAAs",
+    },
+    "private-key": {
+      type: "string",
+      demandOption: true,
+      desc: "Private key to sign the transactions executing the governance VAAs. Hex format, without 0x prefix.",
+    },
+    offset: {
+      type: "number",
+      demandOption: true,
+      desc: "Offset to use from the last executed sequence number",
+    },
+    dryrun: {
+      type: "boolean",
+      default: false,
+      desc: "Whether to execute the VAAs or just print them",
+    },
+  });
+
+async function main() {
+  const argv = await parser.argv;
+  let vault: Vault;
+  if (argv.vault === "mainnet") {
+    vault =
+      DefaultStore.vaults[
+        "mainnet-beta_FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj"
+      ];
+  } else {
+    vault =
+      DefaultStore.vaults[
+        "devnet_6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3"
+      ];
+  }
+  console.log("Executing VAAs for vault", vault.getId());
+  console.log(
+    "Executing VAAs for emitter",
+    (await vault.getEmitter()).toBase58()
+  );
+  const lastSequenceNumber = await vault.getLastSequenceNumber();
+  const startSequenceNumber = lastSequenceNumber - argv.offset;
+  console.log(
+    `Going from sequence number ${startSequenceNumber} to ${lastSequenceNumber}`
+  );
+  for (
+    let seqNumber = startSequenceNumber;
+    seqNumber <= lastSequenceNumber;
+    seqNumber++
+  ) {
+    const submittedWormholeMessage = new SubmittedWormholeMessage(
+      await vault.getEmitter(),
+      seqNumber,
+      vault.cluster
+    );
+    const vaa = await submittedWormholeMessage.fetchVaa();
+    const decodedAction = decodeGovernancePayload(parseVaa(vaa).payload);
+    if (!decodedAction) {
+      console.log("Skipping unknown action for vaa ", seqNumber);
+      continue;
+    }
+    console.log("Executing vaa", seqNumber);
+    console.log(decodedAction);
+    if (!argv.dryrun) {
+      await executeVaa(toPrivateKey(argv["private-key"]), vaa);
+    }
+  }
+}
+
+main();

+ 42 - 0
contract_manager/scripts/list_evm_contracts.ts

@@ -0,0 +1,42 @@
+import yargs from "yargs";
+import { hideBin } from "yargs/helpers";
+import {
+  AptosContract,
+  CosmWasmContract,
+  DefaultStore,
+  EvmContract,
+} from "../src";
+
+const parser = yargs(hideBin(process.argv))
+  .usage("Usage: $0")
+  .options({
+    testnet: {
+      type: "boolean",
+      default: false,
+      desc: "Fetch testnet contract fees instead of mainnet",
+    },
+  });
+
+async function main() {
+  const argv = await parser.argv;
+  const entries = [];
+  for (const contract of Object.values(DefaultStore.contracts)) {
+    if (contract.getChain().isMainnet() === argv.testnet) continue;
+    if (contract instanceof EvmContract) {
+      try {
+        const version = await contract.getVersion();
+        entries.push({
+          chain: contract.getChain().getId(),
+          contract: contract.address,
+          version: version,
+        });
+        console.log(`Fetched version for ${contract.getId()}`);
+      } catch (e) {
+        console.error(`Error fetching version for ${contract.getId()}`, e);
+      }
+    }
+  }
+  console.table(entries);
+}
+
+main();

+ 127 - 0
contract_manager/scripts/upgrade_evm_contracts.ts

@@ -0,0 +1,127 @@
+import yargs from "yargs";
+import { hideBin } from "yargs/helpers";
+import { DefaultStore, EvmChain, loadHotWallet, toPrivateKey } from "../src";
+import { existsSync, readFileSync, writeFileSync } from "fs";
+
+const CACHE_FILE = ".cache-upgrade-evm";
+
+const parser = yargs(hideBin(process.argv))
+  .usage(
+    "Deploys a new PythUpgradable contract to a set of chains and creates a governance proposal for it.\n" +
+      `Uses a cache file (${CACHE_FILE}) to avoid deploying contracts twice\n` +
+      "Usage: $0 --chain <chain_1> --chain <chain_2> --private-key <private_key> --ops-key-path <ops_key_path> --std-output <std_output>"
+  )
+  .options({
+    testnet: {
+      type: "boolean",
+      default: false,
+      desc: "Upgrade testnet contracts instead of mainnet",
+    },
+    "all-chains": {
+      type: "boolean",
+      default: false,
+      desc: "Upgrade the contract on all chains. Use with --testnet flag to upgrade all testnet contracts",
+    },
+    chain: {
+      type: "array",
+      string: true,
+      desc: "Chains to upgrade the contract on",
+    },
+    "private-key": {
+      type: "string",
+      demandOption: true,
+      desc: "Private key to use for the deployment",
+    },
+    "ops-key-path": {
+      type: "string",
+      demandOption: true,
+      desc: "Path to the private key of the proposer to use for the operations multisig governance proposal",
+    },
+    "std-output": {
+      type: "string",
+      demandOption: true,
+      desc: "Path to the standard JSON output of the pyth contract (build artifact)",
+    },
+  });
+
+async function run_if_not_cached(
+  cache_key: string,
+  fn: () => Promise<string>
+): Promise<string> {
+  const cache = existsSync(CACHE_FILE)
+    ? JSON.parse(readFileSync(CACHE_FILE, "utf8"))
+    : {};
+  if (cache[cache_key]) {
+    return cache[cache_key];
+  }
+  const result = await fn();
+  cache[cache_key] = result;
+  writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
+  return result;
+}
+
+async function main() {
+  const argv = await parser.argv;
+  const selectedChains: EvmChain[] = [];
+
+  if (argv.allChains && argv.chain)
+    throw new Error("Cannot use both --all-chains and --chain");
+  if (!argv.allChains && !argv.chain)
+    throw new Error("Must use either --all-chains or --chain");
+  for (const chain of Object.values(DefaultStore.chains)) {
+    if (!(chain instanceof EvmChain)) continue;
+    if (
+      (argv.allChains && chain.isMainnet() !== argv.testnet) ||
+      argv.chain?.includes(chain.getId())
+    )
+      selectedChains.push(chain);
+  }
+  if (argv.chain && selectedChains.length !== argv.chain.length)
+    throw new Error(
+      `Some chains were not found ${selectedChains
+        .map((chain) => chain.getId())
+        .toString()}`
+    );
+  for (const chain of selectedChains) {
+    if (chain.isMainnet() != selectedChains[0].isMainnet())
+      throw new Error("All chains must be either mainnet or testnet");
+  }
+
+  const vault =
+    DefaultStore.vaults[
+      "mainnet-beta_FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj"
+    ];
+
+  console.log("Using cache file", CACHE_FILE);
+  console.log(
+    "Upgrading on chains",
+    selectedChains.map((c) => c.getId())
+  );
+
+  const payloads: Buffer[] = [];
+  for (const chain of selectedChains) {
+    const artifact = JSON.parse(readFileSync(argv["std-output"], "utf8"));
+    console.log("Deploying contract to", chain.getId());
+    const address = await run_if_not_cached(`deploy-${chain.getId()}`, () => {
+      return chain.deploy(
+        toPrivateKey(argv["private-key"]),
+        artifact["abi"],
+        artifact["bytecode"],
+        []
+      );
+    });
+    console.log(`Deployed contract at ${address} on ${chain.getId()}`);
+    payloads.push(
+      chain.generateGovernanceUpgradePayload(address.replace("0x", ""))
+    );
+  }
+
+  console.log("Using vault at for proposal", vault.getId());
+  const wallet = await loadHotWallet(argv["ops-key-path"]);
+  console.log("Using wallet ", wallet.publicKey.toBase58());
+  await vault.connect(wallet);
+  const proposal = await vault.proposeWormholeMessage(payloads);
+  console.log("Proposal address", proposal.address.toBase58());
+}
+
+main();

+ 9 - 1
contract_manager/src/executor.ts

@@ -23,8 +23,16 @@ export async function executeVaa(senderPrivateKey: PrivateKey, vaa: Buffer) {
           parsedVaa.emitterAddress.toString("hex") &&
         governanceSource.emitterChain === parsedVaa.emitterChain
       ) {
-        // TODO: check governance sequence number as well
+        const lastExecutedSequence =
+          await contract.getLastExecutedGovernanceSequence();
+        if (lastExecutedSequence >= parsedVaa.sequence) {
+          console.log(
+            `Skipping on contract ${contract.getId()} as it was already executed`
+          );
+          continue;
+        }
         await contract.executeGovernanceInstruction(senderPrivateKey, vaa);
+        console.log(`Executed on contract ${contract.getId()}`);
       }
     }
   }

+ 52 - 5
contract_manager/src/governance.ts

@@ -200,13 +200,16 @@ export class WormholeEmitter {
   }
 }
 
-export class WormholeMultiSigTransaction {
+export class WormholeMultisigProposal {
   constructor(
     public address: PublicKey,
     public squad: SquadsMesh,
     public cluster: PythCluster
   ) {}
 
+  /**
+   * Gets the current state of the proposal which can be "active", "draft", "executed", etc.
+   */
   async getState() {
     const proposal = await this.squad.getTransaction(this.address);
     // Converts the status object to a string e.g
@@ -214,6 +217,10 @@ export class WormholeMultiSigTransaction {
     return Object.keys(proposal.status)[0];
   }
 
+  /**
+   * Executes the proposal and returns the wormhole messages that were sent
+   * The proposal must be already approved.
+   */
   async execute(): Promise<SubmittedWormholeMessage[]> {
     const proposal = await this.squad.getTransaction(this.address);
     const signatures = await executeProposal(
@@ -239,6 +246,10 @@ export class WormholeMultiSigTransaction {
   }
 }
 
+/**
+ * A vault represents a pyth multisig governance realm which exists in solana mainnet or testnet.
+ * It can be used for proposals to send wormhole messages to the wormhole bridge.
+ */
 export class Vault extends Storable {
   static type = "vault";
   key: PublicKey;
@@ -276,6 +287,11 @@ export class Vault extends Storable {
     };
   }
 
+  /**
+   * Connects the vault to a wallet that can be used to submit proposals
+   * The wallet should be a multisig signer of the vault
+   * @param wallet
+   */
   public connect(wallet: Wallet): void {
     this.squad = SquadsMesh.endpoint(
       getPythClusterApiUrl(this.cluster),
@@ -288,6 +304,9 @@ export class Vault extends Storable {
     return this.squad;
   }
 
+  /**
+   * Gets the emitter address of the vault
+   */
   public async getEmitter() {
     const squad = SquadsMesh.endpoint(
       getPythClusterApiUrl(this.cluster),
@@ -296,10 +315,33 @@ export class Vault extends Storable {
     return squad.getAuthorityPDA(this.key, 1);
   }
 
+  /**
+   * Gets the last sequence number of the vault emitter
+   * This is used to determine the sequence number of the next wormhole message
+   * Fetches the sequence number from the wormholescan API
+   * @returns the last sequence number
+   */
+  public async getLastSequenceNumber(): Promise<number> {
+    const rpcUrl = WORMHOLE_API_ENDPOINT[this.cluster];
+    const emitter = await this.getEmitter();
+    const response = await fetch(
+      `${rpcUrl}/api/v1/vaas/1/${emitter.toBase58()}`
+    );
+    const { data } = await response.json();
+    return data[0].sequence;
+  }
+
+  /**
+   * Proposes sending an array of wormhole messages to the wormhole bridge
+   * Requires a wallet to be connected to the vault
+   *
+   * @param payloads the payloads to send to the wormhole bridge
+   * @param proposalAddress if specified, will continue an existing proposal
+   */
   public async proposeWormholeMessage(
     payloads: Buffer[],
     proposalAddress?: PublicKey
-  ): Promise<WormholeMultiSigTransaction> {
+  ): Promise<WormholeMultisigProposal> {
     const squad = this.getSquadOrThrow();
     const multisigVault = new MultisigVault(
       squad.wallet,
@@ -313,14 +355,19 @@ export class Vault extends Storable {
         squad.wallet.publicKey,
         proposalAddress
       );
-    return new WormholeMultiSigTransaction(txAccount, squad, this.cluster);
+    return new WormholeMultisigProposal(txAccount, squad, this.cluster);
   }
 }
 
-export async function loadHotWallet(wallet: string): Promise<Wallet> {
+/**
+ * Loads a solana wallet from a file. The file should contain the secret key in array of integers format
+ * This wallet can be used to connect to a vault and submit proposals
+ * @param walletPath path to the wallet file
+ */
+export async function loadHotWallet(walletPath: string): Promise<Wallet> {
   return new NodeWallet(
     Keypair.fromSecretKey(
-      Uint8Array.from(JSON.parse(readFileSync(wallet, "ascii")))
+      Uint8Array.from(JSON.parse(readFileSync(walletPath, "ascii")))
     )
   );
 }

+ 3 - 0
contract_manager/src/store.ts

@@ -146,4 +146,7 @@ export class Store {
   }
 }
 
+/**
+ * DefaultStore loads all the contracts and chains from the store directory and provides a single point of access to them.
+ */
 export const DefaultStore = new Store(`${__dirname}/../store`);

+ 170 - 1
package-lock.json

@@ -51,7 +51,8 @@
         "typescript": "^4.9.3"
       },
       "devDependencies": {
-        "prettier": "^2.6.2"
+        "prettier": "^2.6.2",
+        "typedoc": "^0.25.7"
       }
     },
     "contract_manager/node_modules/@certusone/wormhole-sdk": {
@@ -20839,6 +20840,12 @@
         "node": ">=8"
       }
     },
+    "node_modules/ansi-sequence-parser": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz",
+      "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==",
+      "dev": true
+    },
     "node_modules/ansi-styles": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -36503,6 +36510,12 @@
       "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz",
       "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA=="
     },
+    "node_modules/lunr": {
+      "version": "2.3.9",
+      "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
+      "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
+      "dev": true
+    },
     "node_modules/lz-string": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -36605,6 +36618,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/marked": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
+      "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
+      "dev": true,
+      "bin": {
+        "marked": "bin/marked.js"
+      },
+      "engines": {
+        "node": ">= 12"
+      }
+    },
     "node_modules/mcl-wasm": {
       "version": "0.7.9",
       "resolved": "https://registry.npmjs.org/mcl-wasm/-/mcl-wasm-0.7.9.tgz",
@@ -47697,6 +47722,18 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/shiki": {
+      "version": "0.14.7",
+      "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz",
+      "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==",
+      "dev": true,
+      "dependencies": {
+        "ansi-sequence-parser": "^1.1.0",
+        "jsonc-parser": "^3.2.0",
+        "vscode-oniguruma": "^1.7.0",
+        "vscode-textmate": "^8.0.0"
+      }
+    },
     "node_modules/shx": {
       "version": "0.3.4",
       "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz",
@@ -51734,6 +51771,51 @@
         "is-typedarray": "^1.0.0"
       }
     },
+    "node_modules/typedoc": {
+      "version": "0.25.7",
+      "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.7.tgz",
+      "integrity": "sha512-m6A6JjQRg39p2ZVRIN3NKXgrN8vzlHhOS+r9ymUYtcUP/TIQPvWSq7YgE5ZjASfv5Vd5BW5xrir6Gm2XNNcOow==",
+      "dev": true,
+      "dependencies": {
+        "lunr": "^2.3.9",
+        "marked": "^4.3.0",
+        "minimatch": "^9.0.3",
+        "shiki": "^0.14.7"
+      },
+      "bin": {
+        "typedoc": "bin/typedoc"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "peerDependencies": {
+        "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x"
+      }
+    },
+    "node_modules/typedoc/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/typedoc/node_modules/minimatch": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/typeforce": {
       "version": "1.18.0",
       "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
@@ -52372,6 +52454,18 @@
       "resolved": "https://registry.npmjs.org/vlq/-/vlq-2.0.4.tgz",
       "integrity": "sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA=="
     },
+    "node_modules/vscode-oniguruma": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
+      "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==",
+      "dev": true
+    },
+    "node_modules/vscode-textmate": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz",
+      "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==",
+      "dev": true
+    },
     "node_modules/vuvuzela": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/vuvuzela/-/vuvuzela-1.0.3.tgz",
@@ -76899,6 +76993,12 @@
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
     },
+    "ansi-sequence-parser": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz",
+      "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==",
+      "dev": true
+    },
     "ansi-styles": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -79635,6 +79735,7 @@
         "bs58": "^5.0.0",
         "prettier": "^2.6.2",
         "ts-node": "^10.9.1",
+        "typedoc": "^0.25.7",
         "typescript": "^4.9.3"
       },
       "dependencies": {
@@ -89790,6 +89891,12 @@
       "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz",
       "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA=="
     },
+    "lunr": {
+      "version": "2.3.9",
+      "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
+      "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
+      "dev": true
+    },
     "lz-string": {
       "version": "1.5.0",
       "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -89867,6 +89974,12 @@
         "object-visit": "^1.0.0"
       }
     },
+    "marked": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
+      "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
+      "dev": true
+    },
     "mcl-wasm": {
       "version": "0.7.9",
       "resolved": "https://registry.npmjs.org/mcl-wasm/-/mcl-wasm-0.7.9.tgz",
@@ -98959,6 +99072,18 @@
         }
       }
     },
+    "shiki": {
+      "version": "0.14.7",
+      "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz",
+      "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==",
+      "dev": true,
+      "requires": {
+        "ansi-sequence-parser": "^1.1.0",
+        "jsonc-parser": "^3.2.0",
+        "vscode-oniguruma": "^1.7.0",
+        "vscode-textmate": "^8.0.0"
+      }
+    },
     "shx": {
       "version": "0.3.4",
       "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz",
@@ -102108,6 +102233,38 @@
         "is-typedarray": "^1.0.0"
       }
     },
+    "typedoc": {
+      "version": "0.25.7",
+      "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.7.tgz",
+      "integrity": "sha512-m6A6JjQRg39p2ZVRIN3NKXgrN8vzlHhOS+r9ymUYtcUP/TIQPvWSq7YgE5ZjASfv5Vd5BW5xrir6Gm2XNNcOow==",
+      "dev": true,
+      "requires": {
+        "lunr": "^2.3.9",
+        "marked": "^4.3.0",
+        "minimatch": "^9.0.3",
+        "shiki": "^0.14.7"
+      },
+      "dependencies": {
+        "brace-expansion": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0"
+          }
+        },
+        "minimatch": {
+          "version": "9.0.3",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+          "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^2.0.1"
+          }
+        }
+      }
+    },
     "typeforce": {
       "version": "1.18.0",
       "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
@@ -102613,6 +102770,18 @@
       "resolved": "https://registry.npmjs.org/vlq/-/vlq-2.0.4.tgz",
       "integrity": "sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA=="
     },
+    "vscode-oniguruma": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
+      "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==",
+      "dev": true
+    },
+    "vscode-textmate": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz",
+      "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==",
+      "dev": true
+    },
     "vuvuzela": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/vuvuzela/-/vuvuzela-1.0.3.tgz",