Browse Source

Changes without eth since diff

Ali Behjati 2 năm trước cách đây
mục cha
commit
a68e4d31f9
100 tập tin đã thay đổi với 5353 bổ sung6194 xóa
  1. 30 0
      .github/workflows/aptos-contract.yml
  2. 44 0
      .github/workflows/hermes-image-push.yml
  3. 43 0
      .github/workflows/message-buffer-idl-check.yaml
  4. 1 1
      .github/workflows/pre-commit.yml
  5. 1 1
      .github/workflows/publish-pyth-sdk-cw.yml
  6. 3 2
      .github/workflows/remote-executor.yml
  7. 30 0
      .github/workflows/sui-contract.yml
  8. 9 1
      .github/workflows/xc-admin-frontend-image-push.yaml
  9. 1 1
      .gitignore
  10. 26 13
      .pre-commit-config.yaml
  11. 10 0
      README.md
  12. 2 1
      SECURITY.md
  13. 1 2
      governance/multisig_wh_message_builder/tsconfig.json
  14. 80 14
      governance/remote_executor/Cargo.lock
  15. 6 0
      governance/remote_executor/Cargo.toml
  16. 3 2
      governance/remote_executor/cli/Cargo.toml
  17. 22 38
      governance/remote_executor/cli/src/main.rs
  18. 4 5
      governance/remote_executor/programs/remote-executor/Cargo.toml
  19. 2 1
      governance/remote_executor/programs/remote-executor/src/lib.rs
  20. 1 1
      governance/remote_executor/programs/remote-executor/src/state/governance_payload.rs
  21. 11 6
      governance/remote_executor/programs/remote-executor/src/tests/executor_simulator.rs
  22. 2 0
      governance/remote_executor/rust-toolchain.toml
  23. 2 1
      governance/xc_admin/Dockerfile
  24. 1 2
      governance/xc_admin/packages/crank_executor/tsconfig.json
  25. 1 2
      governance/xc_admin/packages/crank_pythnet_relayer/tsconfig.json
  26. 30 0
      governance/xc_admin/packages/proposer_server/package.json
  27. 93 0
      governance/xc_admin/packages/proposer_server/src/index.ts
  28. 9 0
      governance/xc_admin/packages/proposer_server/tsconfig.json
  29. 2 1
      governance/xc_admin/packages/xc_admin_cli/package.json
  30. 178 0
      governance/xc_admin/packages/xc_admin_cli/src/cli.ts
  31. 118 164
      governance/xc_admin/packages/xc_admin_cli/src/index.ts
  32. 1 2
      governance/xc_admin/packages/xc_admin_cli/tsconfig.json
  33. 4 0
      governance/xc_admin/packages/xc_admin_common/package.json
  34. 235 0
      governance/xc_admin/packages/xc_admin_common/src/__tests__/MessageBufferMultisigInstruction.test.ts
  35. 1 1
      governance/xc_admin/packages/xc_admin_common/src/__tests__/PythMultisigInstruction.test.ts
  36. 1 1
      governance/xc_admin/packages/xc_admin_common/src/__tests__/TransactionSize.test.ts
  37. 1 1
      governance/xc_admin/packages/xc_admin_common/src/__tests__/WormholeMultisigInstruction.test.ts
  38. 139 0
      governance/xc_admin/packages/xc_admin_common/src/contracts/Contract.ts
  39. 98 0
      governance/xc_admin/packages/xc_admin_common/src/contracts/EvmPythUpgradable.ts
  40. 43 0
      governance/xc_admin/packages/xc_admin_common/src/contracts/EvmWormholeReceiver.ts
  41. 58 0
      governance/xc_admin/packages/xc_admin_common/src/contracts/config.ts
  42. 4 0
      governance/xc_admin/packages/xc_admin_common/src/contracts/index.ts
  43. 2 0
      governance/xc_admin/packages/xc_admin_common/src/index.ts
  44. 41 0
      governance/xc_admin/packages/xc_admin_common/src/message_buffer.ts
  45. 55 0
      governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/MessageBufferMultisigInstruction.ts
  46. 8 0
      governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/index.ts
  47. 284 173
      governance/xc_admin/packages/xc_admin_common/src/propose.ts
  48. 1 2
      governance/xc_admin/packages/xc_admin_common/tsconfig.json
  49. 3 0
      governance/xc_admin/packages/xc_admin_frontend/Dockerfile
  50. 25 16
      governance/xc_admin/packages/xc_admin_frontend/components/PermissionDepermissionKey.tsx
  51. 120 44
      governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx
  52. 47 6
      governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx
  53. 9 6
      governance/xc_admin/packages/xc_admin_frontend/components/tabs/UpdatePermissions.tsx
  54. 1 0
      governance/xc_admin/packages/xc_admin_frontend/next.config.js
  55. 3 1
      governance/xc_admin/packages/xc_admin_frontend/package.json
  56. 7 14
      governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx
  57. 2 2
      governance/xc_admin/packages/xc_admin_frontend/tsconfig.json
  58. 24 0
      governance/xc_governance_sdk_js/src/chains.ts
  59. 1 0
      governance/xc_governance_sdk_js/src/index.ts
  60. 11 0
      governance/xc_governance_sdk_js/src/instructions.ts
  61. 1 2
      governance/xc_governance_sdk_js/tsconfig.json
  62. 1 1
      hermes/.gitignore
  63. 443 262
      hermes/Cargo.lock
  64. 55 46
      hermes/Cargo.toml
  65. 31 0
      hermes/Dockerfile
  66. 32 18
      hermes/build.rs
  67. 1 1
      hermes/shell.nix
  68. 32 23
      hermes/src/api.rs
  69. 99 39
      hermes/src/api/rest.rs
  70. 33 13
      hermes/src/api/types.rs
  71. 88 62
      hermes/src/api/ws.rs
  72. 23 31
      hermes/src/config.rs
  73. 29 82
      hermes/src/main.rs
  74. 1 1
      hermes/src/network.rs
  75. 72 2
      hermes/src/network/p2p.go
  76. 56 19
      hermes/src/network/p2p.rs
  77. 310 0
      hermes/src/network/pythnet.rs
  78. 262 43
      hermes/src/store.rs
  79. 1 1
      hermes/src/store/proof.rs
  80. 0 175
      hermes/src/store/proof/batch_vaa.rs
  81. 135 0
      hermes/src/store/proof/wormhole_merkle.rs
  82. 219 14
      hermes/src/store/storage.rs
  83. 0 106
      hermes/src/store/storage/local_cache.rs
  84. 862 0
      hermes/src/store/storage/local_storage.rs
  85. 61 0
      hermes/src/store/types.rs
  86. 133 0
      hermes/src/store/wormhole.rs
  87. 0 1755
      message_buffer/Cargo.lock
  88. 0 64
      message_buffer/programs/message_buffer/src/instructions/delete_buffer.rs
  89. 0 54
      message_buffer/programs/message_buffer/src/instructions/put_all.rs
  90. 0 111
      message_buffer/programs/message_buffer/src/instructions/resize_buffer.rs
  91. 0 11
      message_buffer/programs/message_buffer/src/macros.rs
  92. 0 2316
      message_buffer/yarn.lock
  93. 303 408
      package-lock.json
  94. 4 0
      package.json
  95. 36 2
      price_pusher/README.md
  96. 7 0
      price_pusher/config.aptos.testnet.sample.json
  97. 11 0
      price_pusher/config.sui.mainnet.sample.json
  98. 11 0
      price_pusher/config.sui.testnet.sample.json
  99. 3 2
      price_pusher/docker-compose.mainnet.sample.yaml
  100. 3 2
      price_pusher/docker-compose.testnet.sample.yaml

+ 30 - 0
.github/workflows/aptos-contract.yml

@@ -0,0 +1,30 @@
+on:
+  pull_request:
+    paths:
+      - target_chains/aptos/contracts/**
+  push:
+    branches:
+      - main
+    paths:
+      - target_chains/aptos/contracts/**
+
+name: Aptos Contract
+
+jobs:
+  aptos-tests:
+    name: Aptos tests
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        working-directory: target_chains/aptos/contracts/
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Download CLI
+        run: wget https://github.com/aptos-labs/aptos-core/releases/download/aptos-cli-v1.0.4/aptos-cli-1.0.4-Ubuntu-22.04-x86_64.zip
+
+      - name: Unzip CLI
+        run: unzip aptos-cli-1.0.4-Ubuntu-22.04-x86_64.zip
+
+      - name: Run tests
+        run: ./aptos move test

+ 44 - 0
.github/workflows/hermes-image-push.yml

@@ -0,0 +1,44 @@
+name: Build and Push Hermes Image
+on:
+  push:
+    tags:
+      - hermes-v*
+  workflow_dispatch:
+    inputs:
+      dispatch_description:
+        description: "Dispatch description"
+        required: true
+        type: string
+permissions:
+  contents: read
+  id-token: write
+jobs:
+  hermes-image:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set image tag to version of the git tag
+        if: ${{ startsWith(github.ref, 'refs/tags/hermes-v') }}
+        run: |
+          PREFIX="refs/tags/hermes-"
+          VERSION="${GITHUB_REF:${#PREFIX}}"
+          echo "IMAGE_TAG=${VERSION}" >> "${GITHUB_ENV}"
+      - name: Set image tag to the git commit hash
+        if: ${{ !startsWith(github.ref, 'refs/tags/hermes-v') }}
+        run: |
+          echo "IMAGE_TAG=${{ github.sha }}" >> "${GITHUB_ENV}"
+      - uses: aws-actions/configure-aws-credentials@8a84b07f2009032ade05a88a28750d733cc30db1
+        with:
+          role-to-assume: arn:aws:iam::192824654885:role/github-actions-ecr
+          aws-region: eu-west-2
+      - uses: docker/login-action@v2
+        with:
+          registry: public.ecr.aws
+        env:
+          AWS_REGION: us-east-1
+      - run: |
+          DOCKER_BUILDKIT=1 docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f hermes/Dockerfile .
+          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
+        env:
+          ECR_REGISTRY: public.ecr.aws
+          ECR_REPOSITORY: pyth-network/hermes

+ 43 - 0
.github/workflows/message-buffer-idl-check.yaml

@@ -0,0 +1,43 @@
+name: Message Buffer IDL Check
+on:
+  pull_request:
+    paths:
+      - pythnet/message_buffer/**
+  push:
+    branches:
+      - main
+    paths:
+      - pythnet/message_buffer/**
+jobs:
+  abi-check:
+    name: Check Message Buffer IDL files are up to date
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        working-directory: pythnet/message_buffer
+    steps:
+      - name: Checkout sources
+        uses: actions/checkout@v3
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: nightly-2023-03-01
+          components: rustfmt, clippy
+      - name: Install Solana
+        run: |
+          sh -c "$(curl -sSfL https://release.solana.com/v1.14.18/install)"
+          echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
+      - name: Install Anchor
+        run: |
+          cargo install --git https://github.com/coral-xyz/anchor --tag v0.27.0 anchor-cli --locked
+      - name: Build and generate IDLs
+        run: anchor build
+      - name: Copy anchor target files
+        run: cp ./target/idl/message_buffer.json idl/ && cp ./target/types/message_buffer.ts idl/
+      - name: Run prettier (to avoid pre-commit failures)
+        run: |
+          npx prettier --write "./idl/*"
+      - name: Check IDL changes
+        # Fails if the IDL files are not up to date. Please use anchor build to regenerate the IDL files for
+        # the current version of the contract and update idl directory.
+        run: git diff --exit-code idl/*

+ 1 - 1
.github/workflows/pre-commit.yml

@@ -14,6 +14,6 @@ jobs:
       - uses: actions-rs/toolchain@v1
         with:
           profile: minimal
-          toolchain: nightly
+          toolchain: nightly-2023-03-01
           components: rustfmt, clippy
       - uses: pre-commit/action@v2.0.3

+ 1 - 1
.github/workflows/publish-pyth-sdk-cw.yml

@@ -16,4 +16,4 @@ jobs:
       - run: cargo publish --token ${CARGO_REGISTRY_TOKEN}
         env:
           CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
-        working-directory: "target_chains/cosmwasm/pyth-sdk-cw"
+        working-directory: "target_chains/cosmwasm/sdk/rust"

+ 3 - 2
.github/workflows/remote-executor.yml

@@ -15,11 +15,12 @@ jobs:
       - uses: actions-rs/toolchain@v1
         with:
           profile: minimal
-          toolchain: nightly
+          toolchain: 1.66.1
           components: rustfmt, clippy
+          override: true
       - name: Install Solana
         run: |
-          sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
+          sh -c "$(curl -sSfL https://release.solana.com/v1.14.18/install)"
           echo "/home/runner/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
       - name: Run executor tests
         run: cargo test-bpf --manifest-path ./governance/remote_executor/Cargo.toml

+ 30 - 0
.github/workflows/sui-contract.yml

@@ -0,0 +1,30 @@
+on:
+  pull_request:
+    paths:
+      - target_chains/sui/contracts/**
+  push:
+    branches:
+      - main
+    paths:
+      - target_chains/sui/contracts/**
+
+name: Sui Contracts
+
+jobs:
+  sui-tests:
+    name: Sui tests
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        working-directory: target_chains/sui/contracts/
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Update rust
+        run: rustup update stable
+
+      - name: Install Sui CLI
+        run: cargo install --locked --git https://github.com/MystenLabs/sui.git --rev 09b2081498366df936abae26eea4b2d5cafb2788 sui
+
+      - name: Run tests
+        run: sui move test

+ 9 - 1
.github/workflows/xc-admin-frontend-image-push.yaml

@@ -1,5 +1,6 @@
 name: Build and Push Cross Chain Admin Frontend
 on:
+  pull_request:
   push:
     branches: [main]
     paths: [governance/xc_admin/**]
@@ -22,9 +23,16 @@ jobs:
           aws-region: eu-west-2
       - uses: aws-actions/amazon-ecr-login@v1
         id: ecr_login
-      - run: |
+      - name: Build docker image
+        run: |
           DOCKER_BUILDKIT=1 docker build -t lerna -f tilt_devnet/docker_images/Dockerfile.lerna .
           DOCKER_BUILDKIT=1 docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f governance/xc_admin/packages/xc_admin_frontend/Dockerfile .
+        env:
+          ECR_REGISTRY: ${{ steps.ecr_login.outputs.registry }}
+          ECR_REPOSITORY: xc-admin-frontend
+      - name: Push docker image
+        if: github.ref == 'refs/heads/main'
+        run: |
           docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
         env:
           ECR_REGISTRY: ${{ steps.ecr_login.outputs.registry }}

+ 1 - 1
.gitignore

@@ -17,4 +17,4 @@ tsconfig.tsbuildinfo
 *~
 *mnemonic*
 .envrc
-.DS_Store
+*/*.sui.log*

+ 26 - 13
.pre-commit-config.yaml

@@ -21,27 +21,27 @@ repos:
       - id: cargo-fmt-remote-executor
         name: Cargo format for remote executor
         language: "rust"
-        entry: cargo +nightly fmt --manifest-path ./governance/remote_executor/Cargo.toml --all -- --config-path rustfmt.toml
+        entry: cargo +nightly-2023-03-01 fmt --manifest-path ./governance/remote_executor/Cargo.toml --all -- --config-path rustfmt.toml
         pass_filenames: false
         files: governance/remote_executor
       - id: cargo-clippy-remote-executor
         name: Cargo clippy for remote executor
         language: "rust"
-        entry: cargo +nightly clippy --manifest-path ./governance/remote_executor/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
+        entry: cargo +nightly-2023-03-01 clippy --manifest-path ./governance/remote_executor/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
         pass_filenames: false
         files: governance/remote_executor
       # Hooks for the attester
       - id: cargo-fmt-attester
         name: Cargo format for attester
         language: "rust"
-        entry: cargo +nightly fmt --manifest-path ./wormhole_attester/Cargo.toml --all -- --config-path rustfmt.toml
+        entry: cargo +nightly-2023-03-01 fmt --manifest-path ./wormhole_attester/Cargo.toml --all -- --config-path rustfmt.toml
         pass_filenames: false
         files: wormhole_attester
       - id: cargo-clippy-attester
         name: Cargo clippy for attester
         language: "rust"
         entry: |
-          bash -c 'EMITTER_ADDRESS=0 BRIDGE_ADDRESS=0 cargo +nightly clippy --manifest-path \
+          bash -c 'EMITTER_ADDRESS=0 BRIDGE_ADDRESS=0 cargo +nightly-2023-03-01 clippy --manifest-path \
             ./wormhole_attester/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings'
         pass_filenames: false
         files: wormhole_attester
@@ -49,45 +49,58 @@ repos:
       - id: cargo-fmt-cosmwasm
         name: Cargo format for cosmwasm contract
         language: "rust"
-        entry: cargo +nightly fmt --manifest-path ./target_chains/cosmwasm/Cargo.toml --all -- --config-path rustfmt.toml
+        entry: cargo +nightly-2023-03-01 fmt --manifest-path ./target_chains/cosmwasm/Cargo.toml --all -- --config-path rustfmt.toml
         pass_filenames: false
         files: target_chains/cosmwasm
       - id: cargo-clippy-cosmwasm
         name: Cargo clippy for cosmwasm contract
         language: "rust"
-        entry: cargo +nightly clippy --manifest-path ./target_chains/cosmwasm/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
+        entry: cargo +nightly-2023-03-01 clippy --manifest-path ./target_chains/cosmwasm/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
         pass_filenames: false
         files: target_chains/cosmwasm
       # Hooks for Hermes
       - id: cargo-fmt-hermes
         name: Cargo format for Pyth Hermes
         language: "rust"
-        entry: cargo +nightly fmt --manifest-path ./hermes/Cargo.toml --all -- --config-path rustfmt.toml
+        entry: cargo +nightly-2023-03-01 fmt --manifest-path ./hermes/Cargo.toml --all -- --config-path rustfmt.toml
         pass_filenames: false
         files: hermes
       # Hooks for message buffer contract
       - id: cargo-fmt-message-buffer
         name: Cargo format for message buffer contract
         language: "rust"
-        entry: cargo +nightly fmt --manifest-path ./message_buffer/Cargo.toml --all -- --config-path rustfmt.toml
+        entry: cargo +nightly-2023-03-01 fmt --manifest-path ./pythnet/message_buffer/Cargo.toml --all -- --config-path rustfmt.toml
         pass_filenames: false
-        files: message_buffer
+        files: pythnet/message_buffer
       - id: cargo-clippy-message-buffer
         name: Cargo clippy for message buffer contract
         language: "rust"
-        entry: cargo +nightly clippy --manifest-path ./message_buffer/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
+        entry: cargo +nightly-2023-03-01 clippy --manifest-path ./pythnet/message_buffer/Cargo.toml --tests --fix --allow-dirty --allow-staged --features test-bpf -- -D warnings
         pass_filenames: false
-        files: message_buffer
+        files: pythnet/message_buffer
+      # Hooks for pythnet_sdk
+      - id: cargo-fmt-pythnet-sdk
+        name: Cargo format for pythnet SDK
+        language: "rust"
+        entry: cargo +nightly-2023-03-01 fmt --manifest-path ./pythnet/pythnet_sdk/Cargo.toml --all -- --config-path rustfmt.toml
+        pass_filenames: false
+        files: pythnet/pythnet_sdk
+      - id: cargo-clippy-pythnet-sdk
+        name: Cargo clippy for pythnet SDK
+        language: "rust"
+        entry: cargo +nightly-2023-03-01 clippy --manifest-path ./pythnet/pythnet_sdk/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
+        pass_filenames: false
+        files: pythnet/pythnet_sdk
       # Hooks for solana receiver contract
       - id: cargo-fmt-solana-receiver
         name: Cargo format for solana target chain contract
         language: "rust"
-        entry: cargo +nightly fmt --manifest-path ./target_chains/solana/Cargo.toml --all -- --config-path rustfmt.toml
+        entry: cargo +nightly-2023-03-01 fmt --manifest-path ./target_chains/solana/Cargo.toml --all -- --config-path rustfmt.toml
         pass_filenames: false
         files: target_chains/solana
       - id: cargo-clippy-solana-receiver
         name: Cargo clippy for solana target chain contract
         language: "rust"
-        entry: cargo +nightly clippy --manifest-path ./target_chains/solana/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
+        entry: cargo +nightly-2023-03-01 clippy --manifest-path ./target_chains/solana/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
         pass_filenames: false
         files: target_chains/solana

+ 10 - 0
README.md

@@ -45,6 +45,16 @@ and examples for your blockchain runtime in the `target_chains` directory.
 
 ## Development
 
+### Pull requests
+
+Use the following format for naming the pull requests:
+
+[component] PR description
+
+For example:
+
+[hermes] Add storage tests
+
 ### Releases
 
 The repository has a CI workflow that will release javascript packages whose version number has changed.

+ 2 - 1
SECURITY.md

@@ -6,8 +6,9 @@ Pyth operates a self hosted [bug bounty program](https://pyth.network/bounty) to
 
 - **Scopes**
   - [Pyth Oracle](https://github.com/pyth-network/pyth-client/tree/main/program)
-  - [Pyth Crosschain Ethereum](https://github.com/pyth-network/pyth-crosschain/tree/main/target_chains/ethereum/contracts/pyth)
+  - [Pyth Crosschain Ethereum](https://github.com/pyth-network/pyth-crosschain/tree/main/target_chains/ethereum/contracts/contracts/pyth)
   - [Pyth Crosschain Aptos](https://github.com/pyth-network/pyth-crosschain/tree/main/target_chains/aptos/contracts)
+  - [Pyth Crosschain Sui](https://github.com/pyth-network/pyth-crosschain/tree/main/target_chains/sui/contracts)
   - [Pyth Governance](https://github.com/pyth-network/governance/tree/master/staking/programs/staking)
 - **Rewards**
   - Critical: Up to $500,000

+ 1 - 2
governance/multisig_wh_message_builder/tsconfig.json

@@ -4,7 +4,6 @@
   "exclude": ["node_modules", "**/__tests__/*"],
   "compilerOptions": {
     "rootDir": "src/",
-    "outDir": "./lib",
-    "skipLibCheck": true
+    "outDir": "./lib"
   }
 }

+ 80 - 14
governance/remote_executor/Cargo.lock

@@ -587,6 +587,18 @@ dependencies = [
  "regex-automata",
 ]
 
+[[package]]
+name = "bstr"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5"
+dependencies = [
+ "memchr",
+ "once_cell",
+ "regex-automata",
+ "serde",
+]
+
 [[package]]
 name = "bumpalo"
 version = "3.11.0"
@@ -1127,6 +1139,12 @@ dependencies = [
  "syn 0.15.44",
 ]
 
+[[package]]
+name = "dyn-clone"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
+
 [[package]]
 name = "ed25519"
 version = "1.5.2"
@@ -2796,10 +2814,11 @@ dependencies = [
  "bincode",
  "boolinator",
  "rand 0.8.5",
+ "serde_wormhole",
  "solana-program-test",
  "solana-sdk",
  "tokio",
- "wormhole-core",
+ "wormhole-sdk",
  "wormhole-solana",
 ]
 
@@ -2813,11 +2832,12 @@ dependencies = [
  "clap 3.2.22",
  "hex",
  "remote-executor",
+ "serde_wormhole",
  "shellexpand",
  "solana-client",
  "solana-program",
  "solana-sdk",
- "wormhole-core",
+ "wormhole-sdk",
  "wormhole-solana",
 ]
 
@@ -3001,6 +3021,30 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "schemars"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f"
+dependencies = [
+ "dyn-clone",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c"
+dependencies = [
+ "proc-macro2 1.0.43",
+ "quote 1.0.21",
+ "serde_derive_internals",
+ "syn 1.0.100",
+]
+
 [[package]]
 name = "scopeguard"
 version = "1.1.0"
@@ -3095,6 +3139,17 @@ dependencies = [
  "syn 1.0.100",
 ]
 
+[[package]]
+name = "serde_derive_internals"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c"
+dependencies = [
+ "proc-macro2 1.0.43",
+ "quote 1.0.21",
+ "syn 1.0.100",
+]
+
 [[package]]
 name = "serde_json"
 version = "1.0.85"
@@ -3118,6 +3173,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_wormhole"
+version = "0.1.0"
+source = "git+https://github.com/wormhole-foundation/wormhole?tag=v2.17.1#3e423a75180f9da69263279e9ffce47b1858ae78"
+dependencies = [
+ "base64 0.13.0",
+ "itoa",
+ "serde",
+ "serde_bytes",
+ "thiserror",
+]
+
 [[package]]
 name = "serde_yaml"
 version = "0.8.26"
@@ -5025,17 +5092,15 @@ dependencies = [
 ]
 
 [[package]]
-name = "wormhole-core"
+name = "wormhole-sdk"
 version = "0.1.0"
-source = "git+https://github.com/guibescos/wormhole?branch=reisen/sdk-solana#61bb2fb691a8df0aa0e42a21632e43b392ffa90f"
+source = "git+https://github.com/wormhole-foundation/wormhole?tag=v2.17.1#3e423a75180f9da69263279e9ffce47b1858ae78"
 dependencies = [
- "borsh",
- "bstr",
- "byteorder",
- "hex",
- "hex-literal",
- "nom",
- "primitive-types",
+ "anyhow",
+ "bstr 1.5.0",
+ "schemars",
+ "serde",
+ "serde_wormhole",
  "sha3 0.10.5",
  "thiserror",
 ]
@@ -5043,19 +5108,20 @@ dependencies = [
 [[package]]
 name = "wormhole-solana"
 version = "0.1.0"
-source = "git+https://github.com/guibescos/wormhole?branch=reisen/sdk-solana#61bb2fb691a8df0aa0e42a21632e43b392ffa90f"
+source = "git+https://github.com/guibescos/wormhole-solana?rev=f14b3b54c1e37e1aaf8c2ac2a5e236832ffdb3c2#f14b3b54c1e37e1aaf8c2ac2a5e236832ffdb3c2"
 dependencies = [
  "borsh",
- "bstr",
+ "bstr 0.2.17",
  "byteorder",
  "hex",
  "hex-literal",
  "nom",
  "primitive-types",
+ "serde_wormhole",
  "sha3 0.10.5",
  "solana-program",
  "thiserror",
- "wormhole-core",
+ "wormhole-sdk",
 ]
 
 [[package]]

+ 6 - 0
governance/remote_executor/Cargo.toml

@@ -3,3 +3,9 @@ members = [
     "programs/*",
     "cli/"
 ]
+
+[profile.release]
+overflow-checks = true
+
+[patch.crates-io]
+serde_wormhole = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1" }

+ 3 - 2
governance/remote_executor/cli/Cargo.toml

@@ -13,6 +13,7 @@ anchor-client = "0.25.0"
 shellexpand = "2.1.2"
 anyhow = "1.0.65"
 base64 = "0.13.0"
-wormhole-solana = { git = "https://github.com/guibescos/wormhole", branch = "reisen/sdk-solana"}
-wormhole-core = { git = "https://github.com/guibescos/wormhole", branch = "reisen/sdk-solana"}
+wormhole-solana = { git = "https://github.com/guibescos/wormhole-solana", rev="f14b3b54c1e37e1aaf8c2ac2a5e236832ffdb3c2"}
+wormhole-sdk = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1" }
+serde_wormhole = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1"}
 hex = "0.4.3"

+ 22 - 38
governance/remote_executor/cli/src/main.rs

@@ -1,4 +1,13 @@
 #![deny(warnings)]
+
+use {
+    serde_wormhole::RawMessage,
+    wormhole_sdk::vaa::{
+        Body,
+        Header,
+        Vaa,
+    },
+};
 pub mod cli;
 use {
     anchor_client::{
@@ -43,14 +52,10 @@ use {
             Keypair,
         },
         signer::Signer,
-        system_instruction::{
-            self,
-            transfer,
-        },
+        system_instruction::{self,},
         transaction::Transaction,
     },
     std::str::FromStr,
-    wormhole::VAA,
     wormhole_solana::{
         instructions::{
             post_message,
@@ -89,31 +94,10 @@ fn main() -> Result<()> {
 
             let signature_set_keypair = Keypair::new();
 
-            let vaa = VAA::from_bytes(vaa_bytes.clone())?;
-
-            // RENT HACK STARTS HERE
-            let signature_set_size = 4 + 19 + 32 + 4;
-            let posted_vaa_size = 3 + 1 + 1 + 4 + 32 + 4 + 4 + 8 + 2 + 32 + 4 + vaa.payload.len();
-            let posted_vaa_key = PostedVAA::key(&wormhole, vaa.digest().unwrap().hash);
-
-            process_transaction(
-                &rpc_client,
-                vec![
-                    transfer(
-                        &payer.pubkey(),
-                        &signature_set_keypair.pubkey(),
-                        rpc_client.get_minimum_balance_for_rent_exemption(signature_set_size)?,
-                    ),
-                    transfer(
-                        &payer.pubkey(),
-                        &posted_vaa_key,
-                        rpc_client.get_minimum_balance_for_rent_exemption(posted_vaa_size)?,
-                    ),
-                ],
-                &vec![&payer],
-            )?;
+            let vaa: Vaa<&RawMessage> = serde_wormhole::from_slice(&vaa_bytes)?;
+            let (header, body): (Header, Body<&RawMessage>) = vaa.into();
 
-            // RENT HACK ENDS HERE
+            let posted_vaa_key = PostedVAA::key(&wormhole, body.digest().unwrap().hash);
 
             // First verify VAA
             let verify_txs = verify_signatures_txs(
@@ -131,15 +115,15 @@ fn main() -> Result<()> {
 
             // Post VAA
             let post_vaa_data = PostVAAData {
-                version:            vaa.version,
-                guardian_set_index: vaa.guardian_set_index,
-                timestamp:          vaa.timestamp,
-                nonce:              vaa.nonce,
-                emitter_chain:      vaa.emitter_chain.into(),
-                emitter_address:    vaa.emitter_address,
-                sequence:           vaa.sequence,
-                consistency_level:  vaa.consistency_level,
-                payload:            vaa.payload,
+                version:            header.version,
+                guardian_set_index: header.guardian_set_index,
+                timestamp:          body.timestamp,
+                nonce:              body.nonce,
+                emitter_chain:      body.emitter_chain.into(),
+                emitter_address:    body.emitter_address.0,
+                sequence:           body.sequence,
+                consistency_level:  body.consistency_level,
+                payload:            body.payload.to_vec(),
             };
 
             process_transaction(

+ 4 - 5
governance/remote_executor/programs/remote-executor/Cargo.toml

@@ -16,16 +16,15 @@ cpi = ["no-entrypoint"]
 default = []
 pythtest = []
 
-[profile.release]
-overflow-checks = true
-
 [dependencies]
 anchor-lang = {version = "0.25.0", features = ["init-if-needed"]}
-wormhole-solana = { git = "https://github.com/guibescos/wormhole", branch = "reisen/sdk-solana"}
-wormhole-core = { git = "https://github.com/guibescos/wormhole", branch = "reisen/sdk-solana"}
+wormhole-solana = { git = "https://github.com/guibescos/wormhole-solana", rev="f14b3b54c1e37e1aaf8c2ac2a5e236832ffdb3c2"}
+wormhole-sdk = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1" }
+serde_wormhole = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1"}
 boolinator = "2.4.0"
 
 [dev-dependencies]
+
 solana-program-test = "=1.10.31"
 tokio = "1.14.1"
 solana-sdk = "=1.10.31"

+ 2 - 1
governance/remote_executor/programs/remote-executor/src/lib.rs

@@ -1,5 +1,6 @@
 #![deny(warnings)]
 #![allow(clippy::result_unit_err)]
+#![allow(clippy::result_large_err)]
 
 use {
     anchor_lang::{
@@ -12,7 +13,7 @@ use {
         claim_record::ClaimRecord,
         posted_vaa::AnchorVaa,
     },
-    wormhole::Chain::{
+    wormhole_sdk::Chain::{
         self,
         Solana,
     },

+ 1 - 1
governance/remote_executor/programs/remote-executor/src/state/governance_payload.rs

@@ -10,7 +10,7 @@ use {
         mem::size_of,
         ops::Deref,
     },
-    wormhole::Chain,
+    wormhole_sdk::Chain,
 };
 
 pub const MAGIC_NUMBER: u32 = 0x4d475450; // Reverse order of the solidity contract because borsh uses little endian numbers (the solidity contract uses 0x5054474d)

+ 11 - 6
governance/remote_executor/programs/remote-executor/src/tests/executor_simulator.rs

@@ -31,7 +31,6 @@ use {
         ToAccountMetas,
     },
     solana_program_test::{
-        find_file,
         read_file,
         BanksClient,
         BanksClientError,
@@ -54,8 +53,11 @@ use {
             TransactionError,
         },
     },
-    std::collections::HashMap,
-    wormhole::Chain,
+    std::{
+        collections::HashMap,
+        path::Path,
+    },
+    wormhole_sdk::Chain,
     wormhole_solana::VAA,
 };
 
@@ -81,9 +83,12 @@ pub enum VaaAttack {
 impl ExecutorBench {
     /// Deploys the executor program as upgradable
     pub fn new() -> ExecutorBench {
-        let mut bpf_data = read_file(find_file("remote_executor.so").unwrap_or_else(|| {
-            panic!("Unable to locate {}", "remote_executor.so");
-        }));
+        let mut bpf_data = read_file(
+            std::env::current_dir()
+                .unwrap()
+                .join(Path::new("../../target/deploy/remote_executor.so")),
+        );
+
 
         let mut program_test = ProgramTest::default();
         let program_key = crate::id();

+ 2 - 0
governance/remote_executor/rust-toolchain.toml

@@ -0,0 +1,2 @@
+[toolchain]
+channel = "1.66.1"

+ 2 - 1
governance/xc_admin/Dockerfile

@@ -7,8 +7,9 @@ WORKDIR /home/node/
 USER 1000
 
 COPY --chown=1000:1000 governance/xc_admin governance/xc_admin
+COPY --chown=1000:1000 pythnet/message_buffer pythnet/message_buffer
 
-RUN npx lerna run build --scope="{crank_executor,crank_pythnet_relayer}" --include-dependencies
+RUN npx lerna run build --scope="{crank_executor,crank_pythnet_relayer,proposer_server}" --include-dependencies
 
 WORKDIR /home/node/governance/xc_admin
 

+ 1 - 2
governance/xc_admin/packages/crank_executor/tsconfig.json

@@ -4,7 +4,6 @@
   "exclude": ["node_modules", "**/__tests__/*"],
   "compilerOptions": {
     "rootDir": "src/",
-    "outDir": "./lib",
-    "skipLibCheck": true
+    "outDir": "./lib"
   }
 }

+ 1 - 2
governance/xc_admin/packages/crank_pythnet_relayer/tsconfig.json

@@ -4,7 +4,6 @@
   "exclude": ["node_modules", "**/__tests__/*"],
   "compilerOptions": {
     "rootDir": "src/",
-    "outDir": "./lib",
-    "skipLibCheck": true
+    "outDir": "./lib"
   }
 }

+ 30 - 0
governance/xc_admin/packages/proposer_server/package.json

@@ -0,0 +1,30 @@
+{
+  "name": "proposer_server",
+  "version": "0.0.0",
+  "description": "A server that proposes the instructions that it receives to the multisig",
+  "private": "true",
+  "author": "",
+  "homepage": "https://github.com/pyth-network/pyth-crosschain",
+  "license": "ISC",
+  "main": "src/index.ts",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/pyth-network/pyth-crosschain.git"
+  },
+  "bugs": {
+    "url": "https://github.com/pyth-network/pyth-crosschain/issues"
+  },
+  "scripts": {
+    "build": "tsc",
+    "format": "prettier --write \"src/**/*.ts\""
+  },
+  "dependencies": {
+    "@coral-xyz/anchor": "^0.27.0",
+    "@pythnetwork/client": "^2.17.0",
+    "@solana/web3.js": "^1.76.0",
+    "@sqds/mesh": "^1.0.6",
+    "cors": "^2.8.5",
+    "ts-node": "^10.9.1",
+    "xc_admin_common": "*"
+  }
+}

+ 93 - 0
governance/xc_admin/packages/proposer_server/src/index.ts

@@ -0,0 +1,93 @@
+import express, { Request, Response } from "express";
+import cors from "cors";
+import {
+  Cluster,
+  Connection,
+  Keypair,
+  PublicKey,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import {
+  envOrErr,
+  getMultisigCluster,
+  MultisigVault,
+  PRICE_FEED_MULTISIG,
+} from "xc_admin_common";
+import * as fs from "fs";
+import { getPythClusterApiUrl, PythCluster } from "@pythnetwork/client";
+import SquadsMesh from "@sqds/mesh";
+import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
+
+const PORT: number = Number(process.env.PORT ?? "4000");
+const KEYPAIR: Keypair = Keypair.fromSecretKey(
+  Uint8Array.from(JSON.parse(fs.readFileSync(envOrErr("WALLET"), "ascii")))
+);
+const MAINNET_RPC: string =
+  process.env.MAINNET_RPC ?? getPythClusterApiUrl("mainnet-beta");
+const DEVNET_RPC: string =
+  process.env.DEVNET_RPC ?? getPythClusterApiUrl("devnet");
+const TESTNET_RPC: string =
+  process.env.TESTNET_RPC ?? getPythClusterApiUrl("testnet");
+const LOCALNET_RPC: string =
+  process.env.LOCALNET_RPC ?? getPythClusterApiUrl("localnet");
+
+const RPC_URLS: Record<Cluster | "localnet", string> = {
+  "mainnet-beta": MAINNET_RPC,
+  devnet: DEVNET_RPC,
+  testnet: TESTNET_RPC,
+  localnet: LOCALNET_RPC,
+};
+
+const app = express();
+
+app.use(cors());
+app.use(express.json({ limit: "50mb" }));
+app.use(express.urlencoded({ extended: true, limit: "50mb" }));
+
+app.post("/api/propose", async (req: Request, res: Response) => {
+  try {
+    const instructions: TransactionInstruction[] = req.body.instructions.map(
+      (ix: any) =>
+        new TransactionInstruction({
+          data: Buffer.from(ix.data),
+          programId: new PublicKey(ix.programId),
+          keys: ix.keys.map((key: any) => {
+            return {
+              isSigner: key.isSigner,
+              isWritable: key.isWritable,
+              pubkey: new PublicKey(key.pubkey),
+            };
+          }),
+        })
+    );
+
+    const cluster: PythCluster = req.body.cluster;
+
+    const wallet = new NodeWallet(KEYPAIR);
+    const proposeSquads: SquadsMesh = new SquadsMesh({
+      connection: new Connection(RPC_URLS[getMultisigCluster(cluster)]),
+      wallet,
+    });
+
+    const vault = new MultisigVault(
+      wallet,
+      getMultisigCluster(cluster),
+      proposeSquads,
+      PRICE_FEED_MULTISIG[getMultisigCluster(cluster)]
+    );
+
+    // preserve the existing API by returning only the first pubkey
+    const proposalPubkey = (
+      await vault.proposeInstructions(instructions, cluster)
+    )[0];
+    res.status(200).json({ proposalPubkey: proposalPubkey });
+  } catch (error) {
+    if (error instanceof Error) {
+      res.status(500).json(error.message);
+    } else {
+      res.status(500).json("An unknown error occurred");
+    }
+  }
+});
+
+app.listen(PORT);

+ 9 - 0
governance/xc_admin/packages/proposer_server/tsconfig.json

@@ -0,0 +1,9 @@
+{
+  "extends": "../../../../tsconfig.base.json",
+  "include": ["src"],
+  "exclude": ["node_modules", "**/__tests__/*"],
+  "compilerOptions": {
+    "rootDir": "src/",
+    "outDir": "./lib"
+  }
+}

+ 2 - 1
governance/xc_admin/packages/xc_admin_cli/package.json

@@ -16,7 +16,8 @@
   },
   "scripts": {
     "build": "tsc",
-    "format": "prettier --write \"src/**/*.ts\""
+    "format": "prettier --write \"src/**/*.ts\"",
+    "cli": "npm run build && node lib/cli.js"
   },
   "dependencies": {
     "@coral-xyz/anchor": "^0.26.0",

+ 178 - 0
governance/xc_admin/packages/xc_admin_cli/src/cli.ts

@@ -0,0 +1,178 @@
+import { program } from "commander";
+import { loadContractConfig, ContractType, SyncOp } from "xc_admin_common";
+import * as fs from "fs";
+
+// TODO: extract this configuration to a file
+const contractsConfig = [
+  {
+    type: ContractType.EvmPythUpgradable,
+    networkId: "arbitrum",
+    address: "0xff1a0f4744e8582DF1aE09D5611b887B6a12925C",
+  },
+  {
+    type: ContractType.EvmWormholeReceiver,
+    networkId: "canto",
+    address: "0x87047526937246727E4869C5f76A347160e08672",
+  },
+  {
+    type: ContractType.EvmPythUpgradable,
+    networkId: "canto",
+    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
+  },
+  {
+    type: ContractType.EvmPythUpgradable,
+    networkId: "avalanche",
+    address: "0x4305FB66699C3B2702D4d05CF36551390A4c69C6",
+  },
+];
+
+const networksConfig = {
+  evm: {
+    optimism_goerli: {
+      url: `https://rpc.ankr.com/optimism_testnet`,
+    },
+    arbitrum: {
+      url: "https://arb1.arbitrum.io/rpc",
+    },
+    avalanche: {
+      url: "https://api.avax.network/ext/bc/C/rpc",
+    },
+    canto: {
+      url: "https://canto.gravitychain.io",
+    },
+  },
+};
+
+// TODO: we will need configuration of this stuff to decide which multisig to run.
+const multisigs = [
+  {
+    name: "",
+    wormholeNetwork: "mainnet",
+  },
+];
+
+program
+  .name("pyth_governance")
+  .description("CLI for governing Pyth contracts")
+  .version("0.1.0");
+
+program
+  .command("get")
+  .description("Find Pyth contracts matching the given search criteria")
+  .option("-n, --network <network-id>", "Find contracts on the given network")
+  .option("-a, --address <address>", "Find contracts with the given address")
+  .option("-t, --type <type-id>", "Find contracts of the given type")
+  .action(async (options: any) => {
+    const contracts = loadContractConfig(contractsConfig, networksConfig);
+
+    console.log(JSON.stringify(options));
+
+    const matches = [];
+    for (const contract of contracts) {
+      if (
+        (options.network === undefined ||
+          contract.networkId == options.network) &&
+        (options.address === undefined ||
+          contract.getAddress() == options.address) &&
+        (options.type === undefined || contract.type == options.type)
+      ) {
+        matches.push(contract);
+      }
+    }
+
+    for (const contract of matches) {
+      const state = await contract.getState();
+      console.log({
+        networkId: contract.networkId,
+        address: contract.getAddress(),
+        type: contract.type,
+        state: state,
+      });
+    }
+  });
+
+class Cache {
+  private path: string;
+
+  constructor(path: string) {
+    this.path = path;
+  }
+
+  private opFilePath(op: SyncOp): string {
+    return `${this.path}/${op.id()}.json`;
+  }
+
+  public readOpCache(op: SyncOp): Record<string, any> {
+    const path = this.opFilePath(op);
+    if (fs.existsSync(path)) {
+      return JSON.parse(fs.readFileSync(path).toString("utf-8"));
+    } else {
+      return {};
+    }
+  }
+
+  public writeOpCache(op: SyncOp, cache: Record<string, any>) {
+    fs.writeFileSync(this.opFilePath(op), JSON.stringify(cache));
+  }
+
+  public deleteCache(op: SyncOp) {
+    fs.rmSync(this.opFilePath(op));
+  }
+}
+
+program
+  .command("set")
+  .description("Set a configuration parameter for one or more Pyth contracts")
+  .option("-n, --network <network-id>", "Find contracts on the given network")
+  .option("-a, --address <address>", "Find contracts with the given address")
+  .option("-t, --type <type-id>", "Find contracts of the given type")
+  .argument("<fields...>", "Fields to set on the given contracts")
+  .action(async (fields, options: any, command) => {
+    const contracts = loadContractConfig(contractsConfig, networksConfig);
+
+    console.log(JSON.stringify(fields));
+    console.log(JSON.stringify(options));
+
+    const setters = fields.map((value: string) => value.split("="));
+
+    const matches = [];
+    for (const contract of contracts) {
+      if (
+        (options.network === undefined ||
+          contract.networkId == options.network) &&
+        (options.address === undefined ||
+          contract.getAddress() == options.address) &&
+        (options.type === undefined || contract.type == options.type)
+      ) {
+        matches.push(contract);
+      }
+    }
+
+    const ops = [];
+    for (const contract of matches) {
+      const state = await contract.getState();
+      // TODO: make a decent format for this
+      for (const [field, value] of setters) {
+        state[field] = value;
+      }
+
+      ops.push(...(await contract.sync(state)));
+    }
+
+    // TODO: extract constant
+    const cacheDir = "cache";
+    fs.mkdirSync(cacheDir, { recursive: true });
+    const cache = new Cache(cacheDir);
+
+    for (const op of ops) {
+      const opCache = cache.readOpCache(op);
+      const isDone = await op.run(opCache);
+      if (isDone) {
+        cache.deleteCache(op);
+      } else {
+        cache.writeOpCache(op, opCache);
+      }
+    }
+  });
+
+program.parse();

+ 118 - 164
governance/xc_admin/packages/xc_admin_cli/src/index.ts

@@ -7,6 +7,7 @@ import {
   AccountMeta,
   SystemProgram,
   LAMPORTS_PER_SOL,
+  Connection,
 } from "@solana/web3.js";
 import { program } from "commander";
 import {
@@ -22,13 +23,9 @@ import {
   BPF_UPGRADABLE_LOADER,
   getMultisigCluster,
   getProposalInstructions,
-  isRemoteCluster,
-  mapKey,
   MultisigParser,
   PROGRAM_AUTHORITY_ESCROW,
-  proposeArbitraryPayload,
-  proposeInstructions,
-  WORMHOLE_ADDRESS,
+  MultisigVault,
 } from "xc_admin_common";
 import { pythOracleProgram } from "@pythnetwork/client";
 import { Wallet } from "@coral-xyz/anchor/dist/cjs/provider";
@@ -56,6 +53,26 @@ export async function loadHotWalletOrLedger(
   }
 }
 
+async function loadVaultFromOptions(options: any): Promise<MultisigVault> {
+  const wallet = await loadHotWalletOrLedger(
+    options.wallet,
+    options.ledgerDerivationAccount,
+    options.ledgerDerivationChange
+  );
+  // This is the cluster where we want to perform the action
+  const cluster: PythCluster = options.cluster;
+  // This is the cluster where the multisig lives that can perform actions on ^
+  const multisigCluster = getMultisigCluster(cluster);
+  const vault: PublicKey = new PublicKey(options.vault);
+
+  const squad = SquadsMesh.endpoint(
+    getPythClusterApiUrl(multisigCluster),
+    wallet
+  );
+
+  return new MultisigVault(wallet, multisigCluster, squad, vault);
+}
+
 const multisigCommand = (name: string, description: string) =>
   program
     .command(name)
@@ -97,44 +114,21 @@ multisigCommand(
   )
 
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
-    const cluster: PythCluster = options.cluster;
+    const vault = await loadVaultFromOptions(options);
+    const targetCluster: PythCluster = options.cluster;
+
     const programId: PublicKey = new PublicKey(options.programId);
     const current: PublicKey = new PublicKey(options.current);
-    const vault: PublicKey = new PublicKey(options.vault);
-
-    const isRemote = isRemoteCluster(cluster);
-    const squad = SquadsMesh.endpoint(
-      getPythClusterApiUrl(getMultisigCluster(cluster)),
-      wallet
-    );
-    const msAccount = await squad.getMultisig(vault);
-    const vaultAuthority = squad.getAuthorityPDA(
-      msAccount.publicKey,
-      msAccount.authorityIndex
-    );
 
     const programAuthorityEscrowIdl = await Program.fetchIdl(
       PROGRAM_AUTHORITY_ESCROW,
-      new AnchorProvider(
-        squad.connection,
-        squad.wallet,
-        AnchorProvider.defaultOptions()
-      )
+      vault.getAnchorProvider()
     );
 
     const programAuthorityEscrow = new Program(
       programAuthorityEscrowIdl!,
       PROGRAM_AUTHORITY_ESCROW,
-      new AnchorProvider(
-        squad.connection,
-        squad.wallet,
-        AnchorProvider.defaultOptions()
-      )
+      vault.getAnchorProvider()
     );
     const programDataAccount = PublicKey.findProgramAddressSync(
       [programId.toBuffer()],
@@ -145,20 +139,14 @@ multisigCommand(
       .accept()
       .accounts({
         currentAuthority: current,
-        newAuthority: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
+        newAuthority: await vault.getVaultAuthorityPDA(targetCluster),
         programAccount: programId,
         programDataAccount,
         bpfUpgradableLoader: BPF_UPGRADABLE_LOADER,
       })
       .instruction();
 
-    await proposeInstructions(
-      squad,
-      vault,
-      [proposalInstruction],
-      isRemote,
-      WORMHOLE_ADDRESS[getMultisigCluster(cluster)]
-    );
+    await vault.proposeInstructions([proposalInstruction], targetCluster);
   });
 
 multisigCommand("upgrade-program", "Upgrade a program from a buffer")
@@ -169,26 +157,10 @@ multisigCommand("upgrade-program", "Upgrade a program from a buffer")
   .requiredOption("-b, --buffer <pubkey>", "buffer account")
 
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
+    const vault = await loadVaultFromOptions(options);
     const cluster: PythCluster = options.cluster;
     const programId: PublicKey = new PublicKey(options.programId);
     const buffer: PublicKey = new PublicKey(options.buffer);
-    const vault: PublicKey = new PublicKey(options.vault);
-
-    const isRemote = isRemoteCluster(cluster);
-    const squad = SquadsMesh.endpoint(
-      getPythClusterApiUrl(getMultisigCluster(cluster)),
-      wallet
-    );
-    const msAccount = await squad.getMultisig(vault);
-    const vaultAuthority = squad.getAuthorityPDA(
-      msAccount.publicKey,
-      msAccount.authorityIndex
-    );
 
     const programDataAccount = PublicKey.findProgramAddressSync(
       [programId.toBuffer()],
@@ -204,24 +176,18 @@ multisigCommand("upgrade-program", "Upgrade a program from a buffer")
         { pubkey: programDataAccount, isSigner: false, isWritable: true },
         { pubkey: programId, isSigner: false, isWritable: true },
         { pubkey: buffer, isSigner: false, isWritable: true },
-        { pubkey: wallet.publicKey, isSigner: false, isWritable: true },
+        { pubkey: vault.wallet.publicKey, isSigner: false, isWritable: true },
         { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
         { pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
         {
-          pubkey: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
+          pubkey: await vault.getVaultAuthorityPDA(cluster),
           isSigner: true,
           isWritable: false,
         },
       ],
     };
 
-    await proposeInstructions(
-      squad,
-      vault,
-      [proposalInstruction],
-      isRemote,
-      WORMHOLE_ADDRESS[getMultisigCluster(cluster)]
-    );
+    await vault.proposeInstructions([proposalInstruction], cluster);
   });
 
 multisigCommand(
@@ -231,36 +197,22 @@ multisigCommand(
   .requiredOption("-p, --price <pubkey>", "Price account to modify")
   .requiredOption("-e, --exponent <number>", "New exponent")
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
+    const vault = await loadVaultFromOptions(options);
     const cluster: PythCluster = options.cluster;
-    const vault: PublicKey = new PublicKey(options.vault);
     const priceAccount: PublicKey = new PublicKey(options.price);
     const exponent = options.exponent;
-    const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
-
-    const msAccount = await squad.getMultisig(vault);
-    const vaultAuthority = squad.getAuthorityPDA(
-      msAccount.publicKey,
-      msAccount.authorityIndex
-    );
 
-    const provider = new AnchorProvider(
-      squad.connection,
-      wallet,
-      AnchorProvider.defaultOptions()
-    );
     const proposalInstruction: TransactionInstruction = await pythOracleProgram(
       getPythProgramKeyForCluster(cluster),
-      provider
+      vault.getAnchorProvider()
     )
       .methods.setExponent(exponent, 1)
-      .accounts({ fundingAccount: vaultAuthority, priceAccount })
+      .accounts({
+        fundingAccount: await vault.getVaultAuthorityPDA(cluster),
+        priceAccount,
+      })
       .instruction();
-    await proposeInstructions(squad, vault, [proposalInstruction], false);
+    await vault.proposeInstructions([proposalInstruction], cluster);
   });
 
 program
@@ -305,16 +257,9 @@ multisigCommand("approve", "Approve a transaction sitting in the multisig")
     "address of the outstanding transaction"
   )
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
+    const vault = await loadVaultFromOptions(options);
     const transaction: PublicKey = new PublicKey(options.transaction);
-    const cluster: PythCluster = options.cluster;
-
-    const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
-    await squad.approveTransaction(transaction);
+    await vault.squad.approveTransaction(transaction);
   });
 
 multisigCommand("propose-token-transfer", "Propose token transfer")
@@ -326,34 +271,23 @@ multisigCommand("propose-token-transfer", "Propose token transfer")
     "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" // default value is solana mainnet USDC SPL
   )
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
+    const vault = await loadVaultFromOptions(options);
 
     const cluster: PythCluster = options.cluster;
+    const connection = new Connection(getPythClusterApiUrl(cluster)); // from cluster
     const destination: PublicKey = new PublicKey(options.destination);
     const mint: PublicKey = new PublicKey(options.mint);
-    const vault: PublicKey = new PublicKey(options.vault);
     const amount: number = options.amount;
 
-    const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
-    const msAccount = await squad.getMultisig(vault);
-    const vaultAuthority = squad.getAuthorityPDA(
-      msAccount.publicKey,
-      msAccount.authorityIndex
-    );
-
     const mintAccount = await getMint(
-      squad.connection,
+      connection,
       mint,
       undefined,
       TOKEN_PROGRAM_ID
     );
     const sourceTokenAccount = await getAssociatedTokenAddress(
       mint,
-      vaultAuthority,
+      await vault.getVaultAuthorityPDA(cluster),
       true
     );
     const destinationTokenAccount = await getAssociatedTokenAddress(
@@ -365,79 +299,43 @@ multisigCommand("propose-token-transfer", "Propose token transfer")
       createTransferInstruction(
         sourceTokenAccount,
         destinationTokenAccount,
-        vaultAuthority,
+        await vault.getVaultAuthorityPDA(cluster),
         BigInt(amount) * BigInt(10) ** BigInt(mintAccount.decimals)
       );
 
-    await proposeInstructions(squad, vault, [proposalInstruction], false);
+    await vault.proposeInstructions([proposalInstruction], cluster);
   });
 
 multisigCommand("propose-sol-transfer", "Propose sol transfer")
   .requiredOption("-a, --amount <number>", "amount in sol")
   .requiredOption("-d, --destination <pubkey>", "destination address")
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
+    const vault = await loadVaultFromOptions(options);
 
     const cluster: PythCluster = options.cluster;
-    const isRemote = isRemoteCluster(cluster);
     const destination: PublicKey = new PublicKey(options.destination);
-    const vault: PublicKey = new PublicKey(options.vault);
     const amount: number = options.amount;
 
-    const squad = SquadsMesh.endpoint(
-      getPythClusterApiUrl(getMultisigCluster(cluster)),
-      wallet
-    );
-    const msAccount = await squad.getMultisig(vault);
-    const vaultAuthority = squad.getAuthorityPDA(
-      msAccount.publicKey,
-      msAccount.authorityIndex
-    );
-
     const proposalInstruction: TransactionInstruction = SystemProgram.transfer({
-      fromPubkey: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
+      fromPubkey: await vault.getVaultAuthorityPDA(cluster),
       toPubkey: destination,
       lamports: amount * LAMPORTS_PER_SOL,
     });
 
-    await proposeInstructions(
-      squad,
-      vault,
-      [proposalInstruction],
-      isRemote,
-      WORMHOLE_ADDRESS[getMultisigCluster(cluster)]
-    );
+    await vault.proposeInstructions([proposalInstruction], cluster);
   });
 
 multisigCommand("propose-arbitrary-payload", "Propose arbitrary payload")
   .option("-p, --payload <hex-string>", "Wormhole VAA payload")
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
-
-    const cluster: PythCluster = options.cluster;
-    const vault: PublicKey = new PublicKey(options.vault);
-
-    const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
+    const vault = await loadVaultFromOptions(options);
 
     let payload = options.payload;
     if (payload.startsWith("0x")) {
       payload = payload.substring(2);
     }
 
-    await proposeArbitraryPayload(
-      squad,
-      vault,
-      Buffer.from(payload, "hex"),
-      WORMHOLE_ADDRESS[cluster]!
-    );
+    await vault.proposeWormholeMessage(Buffer.from(payload, "hex"));
   });
 
 /**
@@ -449,17 +347,73 @@ multisigCommand("activate", "Activate a transaction sitting in the multisig")
     "address of the draft transaction"
   )
   .action(async (options: any) => {
-    const wallet = await loadHotWalletOrLedger(
-      options.wallet,
-      options.ledgerDerivationAccount,
-      options.ledgerDerivationChange
-    );
-
+    const vault = await loadVaultFromOptions(options);
     const transaction: PublicKey = new PublicKey(options.transaction);
-    const cluster: PythCluster = options.cluster;
+    await vault.squad.activateTransaction(transaction);
+  });
 
-    const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
-    await squad.activateTransaction(transaction);
+multisigCommand("add-and-delete", "Change the roster of the multisig")
+  .option(
+    "-a, --add <comma_separated_members>",
+    "addresses to add to the multisig"
+  )
+  .option(
+    "-r, --remove <comma_separated_members>",
+    "addresses to remove from the multisig"
+  )
+  .requiredOption(
+    "-t, --target-vaults <comma_separated_vaults>",
+    "the vault whose roster we want to change"
+  )
+  .action(async (options: any) => {
+    const vault: MultisigVault = await loadVaultFromOptions(options);
+
+    const targetVaults: PublicKey[] = options.targetVaults
+      ? options.targetVaults.split(",").map((m: string) => new PublicKey(m))
+      : [];
+
+    let proposalInstructions: TransactionInstruction[] = [];
+
+    const membersToAdd: PublicKey[] = options.add
+      ? options.add.split(",").map((m: string) => new PublicKey(m))
+      : [];
+
+    for (const member of membersToAdd) {
+      for (const targetVault of targetVaults) {
+        proposalInstructions.push(await vault.addMemberIx(member, targetVault));
+      }
+    }
+
+    const membersToRemove: PublicKey[] = options.remove
+      ? options.remove.split(",").map((m: string) => new PublicKey(m))
+      : [];
+
+    for (const member of membersToRemove) {
+      for (const targetVault of targetVaults) {
+        proposalInstructions.push(
+          await vault.removeMemberIx(member, targetVault)
+        );
+      }
+    }
+
+    vault.proposeInstructions(proposalInstructions, options.cluster);
+  });
+
+/**
+ * READ THIS BEFORE USING THIS COMMAND
+ * This command exists because of a bug in mesh where
+ * roster change proposals executed through executeInstruction don't work.
+ * It is equivalent to executing proposals through the mesh UI.
+ * It might not work for some types of proposals that require the crank to
+ * execute them.
+ * https://github.com/Squads-Protocol/squads-mpl/pull/32
+ */
+multisigCommand("execute-add-and-delete", "Execute a roster change proposal")
+  .requiredOption("-t, --transaction <pubkey>", "address of the proposal")
+  .action(async (options: any) => {
+    const vault: MultisigVault = await loadVaultFromOptions(options);
+    const proposal = new PublicKey(options.transaction);
+    await vault.squad.executeTransaction(proposal);
   });
 
 program.parse();

+ 1 - 2
governance/xc_admin/packages/xc_admin_cli/tsconfig.json

@@ -4,7 +4,6 @@
   "exclude": ["node_modules", "**/__tests__/*"],
   "compilerOptions": {
     "rootDir": "src/",
-    "outDir": "./lib",
-    "skipLibCheck": true
+    "outDir": "./lib"
   }
 }

+ 4 - 0
governance/xc_admin/packages/xc_admin_common/package.json

@@ -21,10 +21,14 @@
   },
   "dependencies": {
     "@certusone/wormhole-sdk": "^0.9.8",
+    "@coral-xyz/anchor": "^0.26.0",
     "@pythnetwork/client": "^2.17.0",
+    "@pythnetwork/pyth-sdk-solidity": "*",
+    "@pythnetwork/xc-governance-sdk": "*",
     "@solana/buffer-layout": "^4.0.1",
     "@solana/web3.js": "^1.73.0",
     "@sqds/mesh": "^1.0.6",
+    "ethers": "^5.7.2",
     "lodash": "^4.17.21",
     "typescript": "^4.9.4"
   },

+ 235 - 0
governance/xc_admin/packages/xc_admin_common/src/__tests__/MessageBufferMultisigInstruction.test.ts

@@ -0,0 +1,235 @@
+import { AnchorProvider, Wallet, Program, Idl } from "@coral-xyz/anchor";
+import {
+  getPythClusterApiUrl,
+  PythCluster,
+} from "@pythnetwork/client/lib/cluster";
+import { Connection, Keypair, PublicKey } from "@solana/web3.js";
+import {
+  MessageBufferMultisigInstruction,
+  MESSAGE_BUFFER_PROGRAM_ID,
+  MultisigInstructionProgram,
+  MultisigParser,
+} from "..";
+import messageBuffer from "message_buffer/idl/message_buffer.json";
+import { MessageBuffer } from "message_buffer/idl/message_buffer";
+
+test("Message buffer multisig instruction parse: create buffer", (done) => {
+  jest.setTimeout(60000);
+
+  const cluster: PythCluster = "pythtest-crosschain";
+
+  const messageBufferProgram = new Program(
+    messageBuffer as Idl,
+    new PublicKey(MESSAGE_BUFFER_PROGRAM_ID),
+    new AnchorProvider(
+      new Connection(getPythClusterApiUrl(cluster)),
+      new Wallet(new Keypair()),
+      AnchorProvider.defaultOptions()
+    )
+  ) as unknown as Program<MessageBuffer>;
+
+  const parser = MultisigParser.fromCluster(cluster);
+
+  const allowedProgramAuth = PublicKey.unique();
+  const baseAccountKey = PublicKey.unique();
+
+  messageBufferProgram.methods
+    .createBuffer(allowedProgramAuth, baseAccountKey, 100)
+    .accounts({
+      admin: PublicKey.unique(),
+      payer: PublicKey.unique(),
+    })
+    .remainingAccounts([
+      {
+        pubkey: PublicKey.unique(),
+        isSigner: false,
+        isWritable: true,
+      },
+    ])
+    .instruction()
+    .then((instruction) => {
+      const parsedInstruction = parser.parseInstruction(instruction);
+
+      if (parsedInstruction instanceof MessageBufferMultisigInstruction) {
+        expect(parsedInstruction.program).toBe(
+          MultisigInstructionProgram.MessageBuffer
+        );
+        expect(parsedInstruction.name).toBe("createBuffer");
+
+        expect(
+          parsedInstruction.accounts.named["whitelist"].pubkey.equals(
+            instruction.keys[0].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["whitelist"].isSigner).toBe(
+          instruction.keys[0].isSigner
+        );
+        expect(parsedInstruction.accounts.named["whitelist"].isWritable).toBe(
+          instruction.keys[0].isWritable
+        );
+
+        expect(
+          parsedInstruction.accounts.named["admin"].pubkey.equals(
+            instruction.keys[1].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["admin"].isSigner).toBe(
+          instruction.keys[1].isSigner
+        );
+        expect(parsedInstruction.accounts.named["admin"].isWritable).toBe(
+          instruction.keys[1].isWritable
+        );
+
+        expect(
+          parsedInstruction.accounts.named["payer"].pubkey.equals(
+            instruction.keys[2].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["payer"].isSigner).toBe(
+          instruction.keys[2].isSigner
+        );
+        expect(parsedInstruction.accounts.named["payer"].isWritable).toBe(
+          instruction.keys[2].isWritable
+        );
+
+        expect(
+          parsedInstruction.accounts.named["systemProgram"].pubkey.equals(
+            instruction.keys[3].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["systemProgram"].isSigner).toBe(
+          instruction.keys[3].isSigner
+        );
+        expect(
+          parsedInstruction.accounts.named["systemProgram"].isWritable
+        ).toBe(instruction.keys[3].isWritable);
+
+        expect(parsedInstruction.accounts.remaining.length).toBe(1);
+
+        expect(
+          parsedInstruction.accounts.remaining[0].pubkey.equals(
+            instruction.keys[4].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.remaining[0].isSigner).toBe(
+          instruction.keys[4].isSigner
+        );
+        expect(parsedInstruction.accounts.remaining[0].isWritable).toBe(
+          instruction.keys[4].isWritable
+        );
+
+        expect(
+          parsedInstruction.args.allowedProgramAuth.equals(allowedProgramAuth)
+        ).toBeTruthy();
+        expect(
+          parsedInstruction.args.baseAccountKey.equals(baseAccountKey)
+        ).toBeTruthy();
+        expect(parsedInstruction.args.targetSize).toBe(100);
+
+        done();
+      } else {
+        done("Not instance of MessageBufferMultisigInstruction");
+      }
+    });
+});
+
+test("Message buffer multisig instruction parse: delete buffer", (done) => {
+  jest.setTimeout(60000);
+
+  const cluster: PythCluster = "pythtest-crosschain";
+
+  const messageBufferProgram = new Program(
+    messageBuffer as Idl,
+    new PublicKey(MESSAGE_BUFFER_PROGRAM_ID),
+    new AnchorProvider(
+      new Connection(getPythClusterApiUrl(cluster)),
+      new Wallet(new Keypair()),
+      AnchorProvider.defaultOptions()
+    )
+  ) as unknown as Program<MessageBuffer>;
+
+  const parser = MultisigParser.fromCluster(cluster);
+
+  const allowedProgramAuth = PublicKey.unique();
+  const baseAccountKey = PublicKey.unique();
+
+  messageBufferProgram.methods
+    .deleteBuffer(allowedProgramAuth, baseAccountKey)
+    .accounts({
+      admin: PublicKey.unique(),
+      payer: PublicKey.unique(),
+      messageBuffer: PublicKey.unique(),
+    })
+    .instruction()
+    .then((instruction) => {
+      const parsedInstruction = parser.parseInstruction(instruction);
+
+      if (parsedInstruction instanceof MessageBufferMultisigInstruction) {
+        expect(parsedInstruction.program).toBe(
+          MultisigInstructionProgram.MessageBuffer
+        );
+        expect(parsedInstruction.name).toBe("deleteBuffer");
+
+        expect(
+          parsedInstruction.accounts.named["whitelist"].pubkey.equals(
+            instruction.keys[0].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["whitelist"].isSigner).toBe(
+          instruction.keys[0].isSigner
+        );
+        expect(parsedInstruction.accounts.named["whitelist"].isWritable).toBe(
+          instruction.keys[0].isWritable
+        );
+
+        expect(
+          parsedInstruction.accounts.named["admin"].pubkey.equals(
+            instruction.keys[1].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["admin"].isSigner).toBe(
+          instruction.keys[1].isSigner
+        );
+        expect(parsedInstruction.accounts.named["admin"].isWritable).toBe(
+          instruction.keys[1].isWritable
+        );
+
+        expect(
+          parsedInstruction.accounts.named["payer"].pubkey.equals(
+            instruction.keys[2].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["payer"].isSigner).toBe(
+          instruction.keys[2].isSigner
+        );
+        expect(parsedInstruction.accounts.named["payer"].isWritable).toBe(
+          instruction.keys[2].isWritable
+        );
+
+        expect(
+          parsedInstruction.accounts.named["messageBuffer"].pubkey.equals(
+            instruction.keys[3].pubkey
+          )
+        ).toBeTruthy();
+        expect(parsedInstruction.accounts.named["messageBuffer"].isSigner).toBe(
+          instruction.keys[3].isSigner
+        );
+        expect(
+          parsedInstruction.accounts.named["messageBuffer"].isWritable
+        ).toBe(instruction.keys[3].isWritable);
+
+        expect(parsedInstruction.accounts.remaining.length).toBe(0);
+
+        expect(
+          parsedInstruction.args.allowedProgramAuth.equals(allowedProgramAuth)
+        ).toBeTruthy();
+        expect(
+          parsedInstruction.args.baseAccountKey.equals(baseAccountKey)
+        ).toBeTruthy();
+
+        done();
+      } else {
+        done("Not instance of MessageBufferMultisigInstruction");
+      }
+    });
+});

+ 1 - 1
governance/xc_admin/packages/xc_admin_common/src/__tests__/PythMultisigInstruction.test.ts

@@ -1,4 +1,4 @@
-import { AnchorProvider, Wallet } from "@project-serum/anchor";
+import { AnchorProvider, Wallet } from "@coral-xyz/anchor";
 import { pythOracleProgram } from "@pythnetwork/client";
 import {
   getPythClusterApiUrl,

+ 1 - 1
governance/xc_admin/packages/xc_admin_common/src/__tests__/TransactionSize.test.ts

@@ -1,4 +1,4 @@
-import { AnchorProvider, Wallet } from "@project-serum/anchor";
+import { AnchorProvider, Wallet } from "@coral-xyz/anchor";
 import { pythOracleProgram } from "@pythnetwork/client";
 import {
   getPythClusterApiUrl,

+ 1 - 1
governance/xc_admin/packages/xc_admin_common/src/__tests__/WormholeMultisigInstruction.test.ts

@@ -1,5 +1,5 @@
 import { createWormholeProgramInterface } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
-import { AnchorProvider, Wallet } from "@project-serum/anchor";
+import { AnchorProvider, Wallet } from "@coral-xyz/anchor";
 import {
   getPythClusterApiUrl,
   PythCluster,

+ 139 - 0
governance/xc_admin/packages/xc_admin_common/src/contracts/Contract.ts

@@ -0,0 +1,139 @@
+import { ChainId, Instruction } from "@pythnetwork/xc-governance-sdk";
+import { ethers } from "ethers";
+
+export enum ContractType {
+  Oracle,
+  EvmPythUpgradable,
+  EvmWormholeReceiver,
+}
+
+/**
+ * A unique identifier for a blockchain. Note that we cannot use ChainId for this, as ChainId currently reuses
+ * some ids across mainnet / testnet chains (e.g., ethereum goerli has the same id as ethereum mainnet).
+ */
+export type NetworkId = string;
+
+/** A unique identifier for message senders across all wormhole networks. */
+export interface WormholeAddress {
+  emitter: string;
+  chainId: ChainId;
+  // which network this sender is on
+  network: WormholeNetwork;
+}
+export type WormholeNetwork = "mainnet" | "testnet";
+
+/**
+ * A Contract is the basic unit of on-chain state that is managed by xc_admin.
+ * Each contracts lives at a specific address of a specific network, and has a type
+ * representing which of several known contract types (evm target chain, wormhole receiver, etc)
+ * that it is.
+ *
+ * Contracts further expose a state representing values that can be modified by governance.
+ * The fields of the state object vary depending on what type of contract this is.
+ * Finally, contracts expose a sync method that generates the needed operations to bring the on-chain state
+ * in sync with a provided desired state.
+ */
+export interface Contract<State> {
+  type: ContractType;
+  networkId: NetworkId;
+  /** The address of the contract. The address may be written in different formats for different networks. */
+  getAddress(): string;
+
+  /** Get the on-chain state of all governance-controllable fields of this contract. */
+  getState(): Promise<State>;
+
+  /** Generate a set of operations that, if executed, will update the on-chain contract state to be `target`. */
+  sync(target: State): Promise<SyncOp[]>;
+}
+
+/**
+ * An idempotent synchronization operation to update on-chain state. The operation may depend on
+ * external approvals or actions to complete, in which case the operation will pause and need to
+ * be resumed later.
+ */
+export interface SyncOp {
+  /**
+   * A unique identifier for this operation. The id represents the content of the operation (e.g., "sets the X
+   * field to Y on contract Z"), so can be used to identify the "same" operation across multiple runs of this program.
+   */
+  id(): string;
+  /**
+   * Run this operation from a previous state (recorded in cache). The operation can modify cache
+   * to record progress, then returns true if the operation has completed. If this function returns false,
+   * it is waiting on an external operation to complete (e.g., a multisig transaction to be approved).
+   * Re-run this function again once that operation is completed to continue making progress.
+   *
+   * The caller of this function is responsible for preserving the contents of `cache` between calls to
+   * this function.
+   */
+  run(cache: Record<string, any>): Promise<boolean>;
+}
+
+export class SendGovernanceInstruction implements SyncOp {
+  private instruction: Instruction;
+  private sender: WormholeAddress;
+  // function to submit the signed VAA to the target chain contract
+  private submitVaa: (vaa: string) => Promise<boolean>;
+
+  constructor(
+    instruction: Instruction,
+    from: WormholeAddress,
+    submitVaa: (vaa: string) => Promise<boolean>
+  ) {
+    this.instruction = instruction;
+    this.sender = from;
+    this.submitVaa = submitVaa;
+  }
+
+  public id(): string {
+    // TODO: use a more understandable identifier (also this may not be unique)
+    return ethers.utils.sha256(this.instruction.serialize());
+  }
+
+  public async run(cache: Record<string, any>): Promise<boolean> {
+    // FIXME: this implementation is temporary. replace with something like the commented out code below.
+    if (cache["multisigTx"] === undefined) {
+      cache["multisigTx"] = "fooooo";
+      return false;
+    }
+
+    if (cache["vaa"] === undefined) {
+      return false;
+    }
+
+    // VAA is guaranteed to be defined here
+    const vaa = cache["vaa"];
+
+    // assertVaaPayloadEquals(vaa, payload);
+
+    return await this.submitVaa(vaa);
+  }
+
+  /*
+  public async run(cache: Record<string,any>): Promise<boolean> {
+    if (cache["multisigTx"] === undefined) {
+      // Have not yet submitted this operation to the multisig.
+      const payload = this.instruction.serialize();
+      const txKey = vault.sendWormholeInstruction(payload);
+      cache["multisigTx"] = txKey;
+      return false;
+    }
+
+    if (cache["vaa"] === undefined) {
+      const vaa = await executeMultisigTxAndGetVaa(txKey, payloadHex);
+      if (vaa === undefined) {
+        return false;
+      }
+      cache["vaa"] = vaa;
+    }
+
+    // VAA is guaranteed to be defined here
+    const vaa = cache["vaa"];
+
+    assertVaaPayloadEquals(vaa, payload);
+
+    // await proxy.executeGovernanceInstruction("0x" + vaa);
+    await submitVaa(vaa);
+  }
+   */
+}

+ 98 - 0
governance/xc_admin/packages/xc_admin_common/src/contracts/EvmPythUpgradable.ts

@@ -0,0 +1,98 @@
+import {
+  Contract,
+  ContractType,
+  NetworkId,
+  SendGovernanceInstruction,
+  SyncOp,
+  WormholeAddress,
+  WormholeNetwork,
+} from "./Contract";
+import {
+  ChainId,
+  SetValidPeriodInstruction,
+} from "@pythnetwork/xc-governance-sdk";
+import { ethers } from "ethers";
+
+export class EvmPythUpgradable implements Contract<EvmPythUpgradableState> {
+  public type = ContractType.EvmPythUpgradable;
+  public networkId;
+  private address;
+
+  private contract: ethers.Contract;
+
+  constructor(
+    networkId: NetworkId,
+    address: string,
+    contract: ethers.Contract
+  ) {
+    this.networkId = networkId;
+    this.address = address;
+    this.contract = contract;
+  }
+
+  public getAddress() {
+    return this.address;
+  }
+
+  // TODO: these getters will need the full PythUpgradable ABI
+  public async getAuthority(): Promise<WormholeAddress> {
+    // FIXME: read from data sources
+    return {
+      emitter: "123454",
+      chainId: 1,
+      network: "mainnet",
+    };
+  }
+
+  // get the chainId that identifies this contract
+  public async getChainId(): Promise<ChainId> {
+    // FIXME: read from data sources
+    return 23;
+  }
+
+  public async getState(): Promise<EvmPythUpgradableState> {
+    const bytecodeSha = ethers.utils.sha256(
+      (await this.contract.provider.getCode(this.contract.address)) as string
+    );
+    const validTimePeriod =
+      (await this.contract.getValidTimePeriod()) as bigint;
+    return {
+      bytecodeSha,
+      validTimePeriod: validTimePeriod.toString(),
+    };
+  }
+
+  public async sync(target: EvmPythUpgradableState): Promise<SyncOp[]> {
+    const myState = await this.getState();
+    const authority = await this.getAuthority();
+    const myChainId = await this.getChainId();
+    const whInstructions = [];
+
+    if (myState.validTimePeriod !== target.validTimePeriod) {
+      whInstructions.push(
+        new SetValidPeriodInstruction(myChainId, BigInt(target.validTimePeriod))
+      );
+    }
+
+    return whInstructions.map(
+      (value) =>
+        new SendGovernanceInstruction(
+          value,
+          authority,
+          this.submitGovernanceVaa
+        )
+    );
+  }
+
+  public async submitGovernanceVaa(vaa: string): Promise<boolean> {
+    // FIXME: also needs the full PythUpgradable ABI
+    // await this.contract.executeGovernanceInstruction("0x" + vaa)
+    return true;
+  }
+}
+
+export interface EvmPythUpgradableState {
+  bytecodeSha: string;
+  // bigint serialized as a string
+  validTimePeriod: string;
+}

+ 43 - 0
governance/xc_admin/packages/xc_admin_common/src/contracts/EvmWormholeReceiver.ts

@@ -0,0 +1,43 @@
+import { ethers } from "ethers";
+import { Contract, ContractType, NetworkId, SyncOp } from "./Contract";
+
+export class EvmWormholeReceiver implements Contract<EvmWormholeReceiverState> {
+  public type = ContractType.EvmWormholeReceiver;
+  public networkId;
+  private address;
+
+  private contract: ethers.Contract;
+
+  constructor(
+    networkId: NetworkId,
+    address: string,
+    contract: ethers.Contract
+  ) {
+    this.networkId = networkId;
+    this.address = address;
+    this.contract = contract;
+  }
+
+  public getAddress() {
+    return this.address;
+  }
+
+  public async getState(): Promise<EvmWormholeReceiverState> {
+    const bytecodeSha = ethers.utils.sha256(
+      (await this.contract.provider.getCode(this.contract.address)) as string
+    );
+
+    return {
+      bytecodeSha,
+    };
+  }
+
+  public async sync(target: EvmWormholeReceiverState): Promise<SyncOp[]> {
+    // TODO
+    return [];
+  }
+}
+
+export interface EvmWormholeReceiverState {
+  bytecodeSha: string;
+}

+ 58 - 0
governance/xc_admin/packages/xc_admin_common/src/contracts/config.ts

@@ -0,0 +1,58 @@
+import { ethers } from "ethers";
+import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
+import { Contract, ContractType, NetworkId } from "./Contract";
+import { EvmPythUpgradable } from "./EvmPythUpgradable";
+import { EvmWormholeReceiver } from "./EvmWormholeReceiver";
+
+export function getEvmProvider(
+  networkId: NetworkId,
+  networksConfig: any
+): ethers.providers.Provider {
+  const networkConfig = networksConfig["evm"][networkId]!;
+  return ethers.getDefaultProvider(networkConfig.url);
+}
+
+export function loadContractConfig(
+  contractsConfig: any,
+  networksConfig: any
+): Contract<any>[] {
+  const contracts = [];
+  for (const contractConfig of contractsConfig) {
+    contracts.push(fromConfig(contractConfig, networksConfig));
+  }
+  return contracts;
+}
+
+function fromConfig(contractConfig: any, networksConfig: any): Contract<any> {
+  switch (contractConfig.type) {
+    case ContractType.EvmPythUpgradable: {
+      const ethersContract = new ethers.Contract(
+        contractConfig.address,
+        PythAbi,
+        getEvmProvider(contractConfig.networkId, networksConfig)
+      );
+
+      return new EvmPythUpgradable(
+        contractConfig.networkId,
+        contractConfig.address,
+        ethersContract
+      );
+    }
+    case ContractType.EvmWormholeReceiver: {
+      const ethersContract = new ethers.Contract(
+        contractConfig.address,
+        // TODO: pass in an appropriate ABI here
+        [],
+        getEvmProvider(contractConfig.networkId, networksConfig)
+      );
+
+      return new EvmWormholeReceiver(
+        contractConfig.networkId,
+        contractConfig.address,
+        ethersContract
+      );
+    }
+    default:
+      throw new Error(`unknown contract type: ${contractConfig.type}`);
+  }
+}

+ 4 - 0
governance/xc_admin/packages/xc_admin_common/src/contracts/index.ts

@@ -0,0 +1,4 @@
+export * from "./config";
+export * from "./Contract";
+export * from "./EvmPythUpgradable";
+export * from "./EvmWormholeReceiver";

+ 2 - 0
governance/xc_admin/packages/xc_admin_common/src/index.ts

@@ -8,3 +8,5 @@ export * from "./remote_executor";
 export * from "./bpf_upgradable_loader";
 export * from "./deterministic_oracle_accounts";
 export * from "./cranks";
+export * from "./message_buffer";
+export * from "./contracts";

+ 41 - 0
governance/xc_admin/packages/xc_admin_common/src/message_buffer.ts

@@ -0,0 +1,41 @@
+import { getPythProgramKeyForCluster, PythCluster } from "@pythnetwork/client";
+import { PublicKey } from "@solana/web3.js";
+
+/**
+ * Address of the message buffer program.
+ */
+export const MESSAGE_BUFFER_PROGRAM_ID: PublicKey = new PublicKey(
+  "7Vbmv1jt4vyuqBZcpYPpnVhrqVe5e6ZPb6JxDcffRHUM"
+);
+
+export const MESSAGE_BUFFER_BUFFER_SIZE = 2048;
+
+export function isMessageBufferAvailable(cluster: PythCluster): boolean {
+  return cluster === "pythtest-crosschain" || cluster === "pythnet";
+}
+
+export function getPythOracleMessageBufferCpiAuth(
+  cluster: PythCluster
+): PublicKey {
+  const pythOracleProgramId = getPythProgramKeyForCluster(cluster);
+  return PublicKey.findProgramAddressSync(
+    [Buffer.from("upd_price_write"), MESSAGE_BUFFER_PROGRAM_ID.toBuffer()],
+    pythOracleProgramId
+  )[0];
+}
+
+// TODO: We can remove this when createBuffer takes message buffer account
+// as a named account because Anchor can automatically find the address.
+export function getMessageBufferAddressForPrice(
+  cluster: PythCluster,
+  priceAccount: PublicKey
+): PublicKey {
+  return PublicKey.findProgramAddressSync(
+    [
+      getPythOracleMessageBufferCpiAuth(cluster).toBuffer(),
+      Buffer.from("message"),
+      priceAccount.toBuffer(),
+    ],
+    MESSAGE_BUFFER_PROGRAM_ID
+  )[0];
+}

+ 55 - 0
governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/MessageBufferMultisigInstruction.ts

@@ -0,0 +1,55 @@
+import {
+  MultisigInstruction,
+  MultisigInstructionProgram,
+  UNRECOGNIZED_INSTRUCTION,
+} from ".";
+import { AnchorAccounts, resolveAccountNames } from "./anchor";
+import messageBuffer from "message_buffer/idl/message_buffer.json";
+import { TransactionInstruction } from "@solana/web3.js";
+import { Idl, BorshCoder } from "@coral-xyz/anchor";
+
+export class MessageBufferMultisigInstruction implements MultisigInstruction {
+  readonly program = MultisigInstructionProgram.MessageBuffer;
+  readonly name: string;
+  readonly args: { [key: string]: any };
+  readonly accounts: AnchorAccounts;
+
+  constructor(
+    name: string,
+    args: { [key: string]: any },
+    accounts: AnchorAccounts
+  ) {
+    this.name = name;
+    this.args = args;
+    this.accounts = accounts;
+  }
+
+  static fromTransactionInstruction(
+    instruction: TransactionInstruction
+  ): MessageBufferMultisigInstruction {
+    const messageBufferInstructionCoder = new BorshCoder(messageBuffer as Idl)
+      .instruction;
+
+    const deserializedData = messageBufferInstructionCoder.decode(
+      instruction.data
+    );
+
+    if (deserializedData) {
+      return new MessageBufferMultisigInstruction(
+        deserializedData.name,
+        deserializedData.data,
+        resolveAccountNames(
+          messageBuffer as Idl,
+          deserializedData.name,
+          instruction
+        )
+      );
+    } else {
+      return new MessageBufferMultisigInstruction(
+        UNRECOGNIZED_INSTRUCTION,
+        { data: instruction.data },
+        { named: {}, remaining: instruction.keys }
+      );
+    }
+  }
+}

+ 8 - 0
governance/xc_admin/packages/xc_admin_common/src/multisig_transaction/index.ts

@@ -3,7 +3,9 @@ import {
   PythCluster,
 } from "@pythnetwork/client/lib/cluster";
 import { PublicKey, TransactionInstruction } from "@solana/web3.js";
+import { MESSAGE_BUFFER_PROGRAM_ID } from "../message_buffer";
 import { WORMHOLE_ADDRESS } from "../wormhole";
+import { MessageBufferMultisigInstruction } from "./MessageBufferMultisigInstruction";
 import { PythMultisigInstruction } from "./PythMultisigInstruction";
 import { WormholeMultisigInstruction } from "./WormholeMultisigInstruction";
 
@@ -11,6 +13,7 @@ export const UNRECOGNIZED_INSTRUCTION = "unrecognizedInstruction";
 export enum MultisigInstructionProgram {
   PythOracle,
   WormholeBridge,
+  MessageBuffer,
   UnrecognizedProgram,
 }
 
@@ -60,6 +63,10 @@ export class MultisigParser {
       );
     } else if (instruction.programId.equals(this.pythOracleAddress)) {
       return PythMultisigInstruction.fromTransactionInstruction(instruction);
+    } else if (instruction.programId.equals(MESSAGE_BUFFER_PROGRAM_ID)) {
+      return MessageBufferMultisigInstruction.fromTransactionInstruction(
+        instruction
+      );
     } else {
       return UnrecognizedProgram.fromTransactionInstruction(instruction);
     }
@@ -68,3 +75,4 @@ export class MultisigParser {
 
 export { WormholeMultisigInstruction } from "./WormholeMultisigInstruction";
 export { PythMultisigInstruction } from "./PythMultisigInstruction";
+export { MessageBufferMultisigInstruction } from "./MessageBufferMultisigInstruction";

+ 284 - 173
governance/xc_admin/packages/xc_admin_common/src/propose.ts

@@ -1,4 +1,3 @@
-import Squads, { getIxAuthorityPDA, getTxPDA } from "@sqds/mesh";
 import {
   PublicKey,
   Transaction,
@@ -7,9 +6,10 @@ import {
   SYSVAR_CLOCK_PUBKEY,
   SystemProgram,
   PACKET_DATA_SIZE,
+  ConfirmOptions,
 } from "@solana/web3.js";
 import { BN } from "bn.js";
-import { AnchorProvider } from "@project-serum/anchor";
+import { AnchorProvider } from "@coral-xyz/anchor";
 import {
   createWormholeProgramInterface,
   deriveWormholeBridgeDataKey,
@@ -18,6 +18,12 @@ import {
 } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
 import { ExecutePostedVaa } from "./governance_payload/ExecutePostedVaa";
 import { getOpsKey, PRICE_FEED_OPS_KEY } from "./multisig";
+import { PythCluster } from "@pythnetwork/client/lib/cluster";
+import { Wallet } from "@coral-xyz/anchor/dist/cjs/provider";
+import SquadsMesh, { getIxAuthorityPDA, getTxPDA } from "@sqds/mesh";
+import { MultisigAccount } from "@sqds/mesh/lib/types";
+import { mapKey } from "./remote_executor";
+import { WORMHOLE_ADDRESS } from "./wormhole";
 
 export const MAX_EXECUTOR_PAYLOAD_SIZE = PACKET_DATA_SIZE - 687; // Bigger payloads won't fit in one addInstruction call when adding to the proposal
 export const SIZE_OF_SIGNED_BATCH = 30;
@@ -30,199 +36,304 @@ type SquadInstruction = {
   authorityType?: string;
 };
 
-export async function proposeArbitraryPayload(
-  squad: Squads,
-  vault: PublicKey,
-  payload: Buffer,
-  wormholeAddress: PublicKey
-): Promise<PublicKey> {
-  const msAccount = await squad.getMultisig(vault);
+/**
+ * A multisig vault can sign arbitrary instructions with various vault-controlled PDAs, if the multisig approves.
+ * This of course allows the vault to interact with programs on the same blockchain, but a vault also has two
+ * other significant capabilities:
+ * 1. It can execute arbitrary transactions on other blockchains that have the Remote Executor program.
+ *    This allows e.g., a vault on solana mainnet to control programs deployed on PythNet.
+ * 2. It can send wormhole messages from the vault authority. This allows the vault to control programs
+ *    on other chains using Pyth governance messages.
+ */
+export class MultisigVault {
+  public wallet: Wallet;
+  /// The cluster that this multisig lives on
+  public cluster: PythCluster;
+  public squad: SquadsMesh;
+  public vault: PublicKey;
+
+  constructor(
+    wallet: Wallet,
+    cluster: PythCluster,
+    squad: SquadsMesh,
+    vault: PublicKey
+  ) {
+    this.wallet = wallet;
+    this.cluster = cluster;
+    this.squad = squad;
+    this.vault = vault;
+  }
+
+  public async getMultisigAccount(): Promise<MultisigAccount> {
+    return this.squad.getMultisig(this.vault);
+  }
+
+  /**
+   * Get the PDA that the vault can sign for on `cluster`. If `cluster` is remote, this PDA
+   * is the PDA of the remote executor program representing the vault's Wormhole emitter address.
+   * @param cluster
+   */
+  public async getVaultAuthorityPDA(cluster?: PythCluster): Promise<PublicKey> {
+    const msAccount = await this.getMultisigAccount();
+    const localAuthorityPDA = await this.squad.getAuthorityPDA(
+      msAccount.publicKey,
+      msAccount.authorityIndex
+    );
+
+    if (cluster === undefined || cluster === this.cluster) {
+      return localAuthorityPDA;
+    } else {
+      return mapKey(localAuthorityPDA);
+    }
+  }
+
+  public wormholeAddress(): PublicKey | undefined {
+    // TODO: we should configure the wormhole address as a vault parameter.
+    return WORMHOLE_ADDRESS[this.cluster];
+  }
+
+  // TODO: does this need a cluster argument?
+  public async getAuthorityPDA(authorityIndex: number = 1): Promise<PublicKey> {
+    return this.squad.getAuthorityPDA(this.vault, authorityIndex);
+  }
+
+  // NOTE: this function probably doesn't belong on this class, but it makes it easier to refactor so we'll leave
+  // it here for now.
+  public getAnchorProvider(opts?: ConfirmOptions): AnchorProvider {
+    if (opts === undefined) {
+      opts = AnchorProvider.defaultOptions();
+    }
+
+    return new AnchorProvider(this.squad.connection, this.squad.wallet, opts);
+  }
+
+  // Convenience wrappers around squads methods
+
+  public async createProposalIx(
+    proposalIndex: number
+  ): Promise<[TransactionInstruction, PublicKey]> {
+    const msAccount = await this.squad.getMultisig(this.vault);
 
-  let ixToSend: TransactionInstruction[] = [];
-  const proposalIndex = msAccount.transactionIndex + 1;
-  ixToSend.push(
-    await squad.buildCreateTransaction(
+    const ix = await this.squad.buildCreateTransaction(
       msAccount.publicKey,
       msAccount.authorityIndex,
       proposalIndex
-    )
-  );
+    );
 
-  const newProposalAddress = getTxPDA(
-    vault,
-    new BN(proposalIndex),
-    squad.multisigProgramId
-  )[0];
+    const newProposalAddress = getTxPDA(
+      this.vault,
+      new BN(proposalIndex),
+      this.squad.multisigProgramId
+    )[0];
 
-  const instructionToPropose = await getPostMessageInstruction(
-    squad,
-    vault,
-    newProposalAddress,
-    1,
-    wormholeAddress,
-    payload
-  );
-  ixToSend.push(
-    await squad.buildAddInstruction(
-      vault,
+    return [ix, newProposalAddress];
+  }
+
+  public async activateProposalIx(
+    proposalAddress: PublicKey
+  ): Promise<TransactionInstruction> {
+    return await this.squad.buildActivateTransaction(
+      this.vault,
+      proposalAddress
+    );
+  }
+
+  public async approveProposalIx(
+    proposalAddress: PublicKey
+  ): Promise<TransactionInstruction> {
+    return await this.squad.buildApproveTransaction(
+      this.vault,
+      proposalAddress
+    );
+  }
+
+  public async addMemberIx(
+    member: PublicKey,
+    targetVault: PublicKey
+  ): Promise<TransactionInstruction> {
+    return await this.squad.buildAddMember(
+      targetVault,
+      await this.getAuthorityPDA(),
+      member
+    );
+  }
+
+  public async removeMemberIx(
+    member: PublicKey,
+    targetVault: PublicKey
+  ): Promise<TransactionInstruction> {
+    return await this.squad.buildRemoveMember(
+      targetVault,
+      await this.getAuthorityPDA(),
+      member
+    );
+  }
+
+  // Propose instructions
+
+  /**
+   * Propose submitting `payload` as a wormhole message. If the proposal is approved, the sent message
+   * will have `this.getVaultAuthorityPda()` as its emitter address.
+   * @param payload the bytes to send as the wormhole message's payload.
+   * @returns the newly created proposal's public key
+   */
+  public async proposeWormholeMessage(payload: Buffer): Promise<PublicKey> {
+    const msAccount = await this.getMultisigAccount();
+
+    let ixToSend: TransactionInstruction[] = [];
+    const [proposalIx, newProposalAddress] = await this.createProposalIx(
+      msAccount.transactionIndex + 1
+    );
+
+    const proposalIndex = msAccount.transactionIndex + 1;
+    ixToSend.push(proposalIx);
+
+    const instructionToPropose = await getPostMessageInstruction(
+      this.squad,
+      this.vault,
       newProposalAddress,
-      instructionToPropose.instruction,
       1,
-      instructionToPropose.authorityIndex,
-      instructionToPropose.authorityBump,
-      instructionToPropose.authorityType
-    )
-  );
-  ixToSend.push(
-    await squad.buildActivateTransaction(vault, newProposalAddress)
-  );
-  ixToSend.push(await squad.buildApproveTransaction(vault, newProposalAddress));
-
-  const txToSend = batchIntoTransactions(ixToSend);
-
-  for (let i = 0; i < txToSend.length; i += SIZE_OF_SIGNED_BATCH) {
-    await new AnchorProvider(
-      squad.connection,
-      squad.wallet,
-      AnchorProvider.defaultOptions()
-    ).sendAll(
-      txToSend.slice(i, i + SIZE_OF_SIGNED_BATCH).map((tx) => {
-        return { tx, signers: [] };
-      })
+      this.wormholeAddress()!,
+      payload
     );
-  }
-  return newProposalAddress;
-}
+    ixToSend.push(
+      await this.squad.buildAddInstruction(
+        this.vault,
+        newProposalAddress,
+        instructionToPropose.instruction,
+        1,
+        instructionToPropose.authorityIndex,
+        instructionToPropose.authorityBump,
+        instructionToPropose.authorityType
+      )
+    );
+    ixToSend.push(await this.activateProposalIx(newProposalAddress));
+    ixToSend.push(await this.approveProposalIx(newProposalAddress));
 
-/**
- * Propose an array of `TransactionInstructions` as a proposal
- * @param squad Squads client
- * @param vault vault public key (the id of the multisig where these instructions should be proposed)
- * @param instructions instructions that will be proposed
- * @param remote whether the instructions should be executed in the chain of the multisig or remotely on Pythnet
- * @returns the newly created proposal's pubkey
- */
-export async function proposeInstructions(
-  squad: Squads,
-  vault: PublicKey,
-  instructions: TransactionInstruction[],
-  remote: boolean,
-  wormholeAddress?: PublicKey
-): Promise<PublicKey> {
-  const msAccount = await squad.getMultisig(vault);
-  const newProposals = [];
-
-  let ixToSend: TransactionInstruction[] = [];
-  if (remote) {
-    if (!wormholeAddress) {
-      throw new Error("Need wormhole address");
+    const txToSend = batchIntoTransactions(ixToSend);
+    for (let i = 0; i < txToSend.length; i += SIZE_OF_SIGNED_BATCH) {
+      await this.getAnchorProvider().sendAll(
+        txToSend.slice(i, i + SIZE_OF_SIGNED_BATCH).map((tx) => {
+          return { tx, signers: [] };
+        })
+      );
     }
-    const batches = batchIntoExecutorPayload(instructions);
-
-    for (let j = 0; j < batches.length; j += MAX_INSTRUCTIONS_PER_PROPOSAL) {
-      const proposalIndex =
-        msAccount.transactionIndex + 1 + j / MAX_INSTRUCTIONS_PER_PROPOSAL;
-      ixToSend.push(
-        await squad.buildCreateTransaction(
-          msAccount.publicKey,
-          msAccount.authorityIndex,
+    return newProposalAddress;
+  }
+
+  /**
+   * Propose an array of `TransactionInstructions` as one or more proposals
+   * @param instructions instructions that will be proposed
+   * @param targetCluster the cluster where the instructions should be executed. If the cluster is not the
+   * same as the one this multisig is on, execution will use wormhole and the remote executor program.
+   * @returns the newly created proposals' public keys
+   */
+  public async proposeInstructions(
+    instructions: TransactionInstruction[],
+    targetCluster?: PythCluster
+  ): Promise<PublicKey[]> {
+    const msAccount = await this.getMultisigAccount();
+    const newProposals = [];
+
+    const remote = targetCluster != this.cluster;
+
+    let ixToSend: TransactionInstruction[] = [];
+    if (remote) {
+      if (!this.wormholeAddress()) {
+        throw new Error("Need wormhole address");
+      }
+      const batches = batchIntoExecutorPayload(instructions);
+
+      for (let j = 0; j < batches.length; j += MAX_INSTRUCTIONS_PER_PROPOSAL) {
+        const proposalIndex =
+          msAccount.transactionIndex + 1 + j / MAX_INSTRUCTIONS_PER_PROPOSAL;
+        const [proposalIx, newProposalAddress] = await this.createProposalIx(
           proposalIndex
-        )
-      );
-      const newProposalAddress = getTxPDA(
-        vault,
-        new BN(proposalIndex),
-        squad.multisigProgramId
-      )[0];
-      newProposals.push(newProposalAddress);
-
-      for (const [i, batch] of batches
-        .slice(j, j + MAX_INSTRUCTIONS_PER_PROPOSAL)
-        .entries()) {
-        const squadIx = await wrapAsRemoteInstruction(
-          squad,
-          vault,
-          newProposalAddress,
-          batch,
-          i + 1,
-          wormholeAddress
         );
-        ixToSend.push(
-          await squad.buildAddInstruction(
-            vault,
+        ixToSend.push(proposalIx);
+        newProposals.push(newProposalAddress);
+
+        for (const [i, batch] of batches
+          .slice(j, j + MAX_INSTRUCTIONS_PER_PROPOSAL)
+          .entries()) {
+          const squadIx = await wrapAsRemoteInstruction(
+            this.squad,
+            this.vault,
             newProposalAddress,
-            squadIx.instruction,
+            batch,
             i + 1,
-            squadIx.authorityIndex,
-            squadIx.authorityBump,
-            squadIx.authorityType
-          )
-        );
+            this.wormholeAddress()!
+          );
+          ixToSend.push(
+            await this.squad.buildAddInstruction(
+              this.vault,
+              newProposalAddress,
+              squadIx.instruction,
+              i + 1,
+              squadIx.authorityIndex,
+              squadIx.authorityBump,
+              squadIx.authorityType
+            )
+          );
+        }
+        ixToSend.push(await this.activateProposalIx(newProposalAddress));
+        ixToSend.push(await this.approveProposalIx(newProposalAddress));
       }
-      ixToSend.push(
-        await squad.buildActivateTransaction(vault, newProposalAddress)
-      );
-      ixToSend.push(
-        await squad.buildApproveTransaction(vault, newProposalAddress)
-      );
-    }
-  } else {
-    for (
-      let j = 0;
-      j < instructions.length;
-      j += MAX_INSTRUCTIONS_PER_PROPOSAL
-    ) {
-      const proposalIndex =
-        msAccount.transactionIndex + 1 + j / MAX_INSTRUCTIONS_PER_PROPOSAL;
-      ixToSend.push(
-        await squad.buildCreateTransaction(
-          msAccount.publicKey,
-          msAccount.authorityIndex,
+    } else {
+      for (
+        let j = 0;
+        j < instructions.length;
+        j += MAX_INSTRUCTIONS_PER_PROPOSAL
+      ) {
+        const proposalIndex =
+          msAccount.transactionIndex + 1 + j / MAX_INSTRUCTIONS_PER_PROPOSAL;
+        const [proposalIx, newProposalAddress] = await this.createProposalIx(
           proposalIndex
-        )
-      );
-      const newProposalAddress = getTxPDA(
-        vault,
-        new BN(proposalIndex),
-        squad.multisigProgramId
-      )[0];
-      newProposals.push(newProposalAddress);
-
-      for (let [i, instruction] of instructions
-        .slice(j, j + MAX_INSTRUCTIONS_PER_PROPOSAL)
-        .entries()) {
+        );
+        ixToSend.push(proposalIx);
+        newProposals.push(newProposalAddress);
+
+        for (let [i, instruction] of instructions
+          .slice(j, j + MAX_INSTRUCTIONS_PER_PROPOSAL)
+          .entries()) {
+          ixToSend.push(
+            await this.squad.buildAddInstruction(
+              this.vault,
+              newProposalAddress,
+              instruction,
+              i + 1
+            )
+          );
+        }
         ixToSend.push(
-          await squad.buildAddInstruction(
-            vault,
-            newProposalAddress,
-            instruction,
-            i + 1
+          await this.squad.buildActivateTransaction(
+            this.vault,
+            newProposalAddress
+          )
+        );
+        ixToSend.push(
+          await this.squad.buildApproveTransaction(
+            this.vault,
+            newProposalAddress
           )
         );
       }
-      ixToSend.push(
-        await squad.buildActivateTransaction(vault, newProposalAddress)
-      );
-      ixToSend.push(
-        await squad.buildApproveTransaction(vault, newProposalAddress)
-      );
     }
-  }
 
-  const txToSend = batchIntoTransactions(ixToSend);
-
-  for (let i = 0; i < txToSend.length; i += SIZE_OF_SIGNED_BATCH) {
-    await new AnchorProvider(
-      squad.connection,
-      squad.wallet,
-      AnchorProvider.defaultOptions()
-    ).sendAll(
-      txToSend.slice(i, i + SIZE_OF_SIGNED_BATCH).map((tx) => {
-        return { tx, signers: [] };
-      })
-    );
+    const txToSend = batchIntoTransactions(ixToSend);
+
+    for (let i = 0; i < txToSend.length; i += SIZE_OF_SIGNED_BATCH) {
+      await this.getAnchorProvider({
+        preflightCommitment: "processed",
+        commitment: "confirmed",
+      }).sendAll(
+        txToSend.slice(i, i + SIZE_OF_SIGNED_BATCH).map((tx) => {
+          return { tx, signers: [] };
+        })
+      );
+    }
+    return newProposals;
   }
-  return newProposals[0];
 }
 
 /**
@@ -348,7 +459,7 @@ export function getSizeOfCompressedU16(n: number) {
  * @returns an instruction to be proposed
  */
 export async function wrapAsRemoteInstruction(
-  squad: Squads,
+  squad: SquadsMesh,
   vault: PublicKey,
   proposalAddress: PublicKey,
   instructions: TransactionInstruction[],
@@ -376,7 +487,7 @@ export async function wrapAsRemoteInstruction(
  * @param payload the payload to be posted
  */
 async function getPostMessageInstruction(
-  squad: Squads,
+  squad: SquadsMesh,
   vault: PublicKey,
   proposalAddress: PublicKey,
   instructionIndex: number,

+ 1 - 2
governance/xc_admin/packages/xc_admin_common/tsconfig.json

@@ -4,7 +4,6 @@
   "exclude": ["node_modules", "**/__tests__/*"],
   "compilerOptions": {
     "rootDir": "src/",
-    "outDir": "./lib",
-    "skipLibCheck": true
+    "outDir": "./lib"
   }
 }

+ 3 - 0
governance/xc_admin/packages/xc_admin_frontend/Dockerfile

@@ -7,6 +7,9 @@ WORKDIR /home/node/
 USER 1000
 
 COPY --chown=1000:1000 governance/xc_admin governance/xc_admin
+COPY --chown=1000:1000 pythnet/message_buffer pythnet/message_buffer
+COPY --chown=1000:1000 target_chains/ethereum/sdk/solidity target_chains/ethereum/sdk/solidity
+COPY --chown=1000:1000 governance/xc_governance_sdk_js governance/xc_governance_sdk_js
 
 ENV NODE_ENV production
 ENV NEXT_TELEMETRY_DISABLED 1

+ 25 - 16
governance/xc_admin/packages/xc_admin_frontend/components/PermissionDepermissionKey.tsx

@@ -6,6 +6,7 @@ import { useWallet } from '@solana/wallet-adapter-react'
 import { WalletModalButton } from '@solana/wallet-adapter-react-ui'
 import { Cluster, PublicKey, TransactionInstruction } from '@solana/web3.js'
 import SquadsMesh from '@sqds/mesh'
+import axios from 'axios'
 import { Fragment, useContext, useEffect, useState } from 'react'
 import toast from 'react-hot-toast'
 import {
@@ -13,7 +14,6 @@ import {
   isRemoteCluster,
   mapKey,
   PRICE_FEED_MULTISIG,
-  proposeInstructions,
   WORMHOLE_ADDRESS,
 } from 'xc_admin_common'
 import { ClusterContext } from '../contexts/ClusterContext'
@@ -30,10 +30,12 @@ const PermissionDepermissionKey = ({
   isPermission,
   pythProgramClient,
   squads,
+  proposerServerUrl,
 }: {
   isPermission: boolean
   pythProgramClient?: Program<PythOracle>
   squads?: SquadsMesh
+  proposerServerUrl: string
 }) => {
   const [publisherKey, setPublisherKey] = useState(
     'JTmFx5zX9mM94itfk2nQcJnQQDPjcv4UPD7SYj6xDCV'
@@ -77,39 +79,46 @@ const PermissionDepermissionKey = ({
       const fundingAccount = isRemote
         ? mapKey(multisigAuthority)
         : multisigAuthority
-      priceAccounts.map((priceAccount) => {
-        isPermission
-          ? pythProgramClient.methods
+
+      for (const priceAccount of priceAccounts) {
+        if (isPermission) {
+          instructions.push(
+            await pythProgramClient.methods
               .addPublisher(new PublicKey(publisherKey))
               .accounts({
                 fundingAccount,
                 priceAccount: priceAccount,
               })
               .instruction()
-              .then((instruction) => instructions.push(instruction))
-          : pythProgramClient.methods
+          )
+        } else {
+          instructions.push(
+            await pythProgramClient.methods
               .delPublisher(new PublicKey(publisherKey))
               .accounts({
                 fundingAccount,
                 priceAccount: priceAccount,
               })
               .instruction()
-              .then((instruction) => instructions.push(instruction))
-      })
+          )
+        }
+      }
       setIsSubmitButtonLoading(true)
       try {
-        const proposalPubkey = await proposeInstructions(
-          squads,
-          PRICE_FEED_MULTISIG[getMultisigCluster(cluster)],
+        const response = await axios.post(proposerServerUrl + '/api/propose', {
           instructions,
-          isRemote,
-          wormholeAddress
-        )
+          cluster,
+        })
+        const { proposalPubkey } = response.data
         toast.success(`Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`)
         setIsSubmitButtonLoading(false)
         closeModal()
-      } catch (e: any) {
-        toast.error(capitalizeFirstLetter(e.message))
+      } catch (error: any) {
+        if (error.response) {
+          toast.error(capitalizeFirstLetter(error.response.data))
+        } else {
+          toast.error(capitalizeFirstLetter(error.message))
+        }
         setIsSubmitButtonLoading(false)
       }
     }

+ 120 - 44
governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx

@@ -1,18 +1,26 @@
-import { AnchorProvider, Program } from '@coral-xyz/anchor'
+import { AnchorProvider, Idl, Program } from '@coral-xyz/anchor'
 import { AccountType, getPythProgramKeyForCluster } from '@pythnetwork/client'
 import { PythOracle, pythOracleProgram } from '@pythnetwork/client/lib/anchor'
 import { useWallet } from '@solana/wallet-adapter-react'
 import { Cluster, PublicKey, TransactionInstruction } from '@solana/web3.js'
+import messageBuffer from 'message_buffer/idl/message_buffer.json'
+import { MessageBuffer } from 'message_buffer/idl/message_buffer'
+import axios from 'axios'
 import { useCallback, useContext, useEffect, useState } from 'react'
 import toast from 'react-hot-toast'
 import {
   findDetermisticAccountAddress,
   getMultisigCluster,
+  getPythOracleMessageBufferCpiAuth,
+  isMessageBufferAvailable,
   isRemoteCluster,
   mapKey,
+  MESSAGE_BUFFER_PROGRAM_ID,
+  MESSAGE_BUFFER_BUFFER_SIZE,
   PRICE_FEED_MULTISIG,
-  proposeInstructions,
   WORMHOLE_ADDRESS,
+  PRICE_FEED_OPS_KEY,
+  getMessageBufferAddressForPrice,
 } from 'xc_admin_common'
 import { ClusterContext } from '../../contexts/ClusterContext'
 import { useMultisigContext } from '../../contexts/MultisigContext'
@@ -24,7 +32,7 @@ import Spinner from '../common/Spinner'
 import Loadbar from '../loaders/Loadbar'
 import PermissionDepermissionKey from '../PermissionDepermissionKey'
 
-const General = () => {
+const General = ({ proposerServerUrl }: { proposerServerUrl: string }) => {
   const [data, setData] = useState<any>({})
   const [dataChanges, setDataChanges] = useState<Record<string, any>>()
   const [existingSymbols, setExistingSymbols] = useState<Set<string>>(new Set())
@@ -41,6 +49,9 @@ const General = () => {
   const [pythProgramClient, setPythProgramClient] =
     useState<Program<PythOracle>>()
 
+  const [messageBufferClient, setMessageBufferClient] =
+    useState<Program<MessageBuffer>>()
+
   const openModal = () => {
     setIsModalOpen(true)
   }
@@ -322,19 +333,44 @@ const General = () => {
               .instruction()
           )
 
+          if (isMessageBufferAvailable(cluster) && messageBufferClient) {
+            // create create buffer instruction for the price account
+            instructions.push(
+              await messageBufferClient.methods
+                .createBuffer(
+                  getPythOracleMessageBufferCpiAuth(cluster),
+                  priceAccountKey,
+                  MESSAGE_BUFFER_BUFFER_SIZE
+                )
+                .accounts({
+                  admin: fundingAccount,
+                  payer: PRICE_FEED_OPS_KEY,
+                })
+                .remainingAccounts([
+                  {
+                    pubkey: getMessageBufferAddressForPrice(
+                      cluster,
+                      priceAccountKey
+                    ),
+                    isSigner: false,
+                    isWritable: true,
+                  },
+                ])
+                .instruction()
+            )
+          }
+
           // create add publisher instruction if there are any publishers
-          if (newChanges.priceAccounts[0].publishers.length > 0) {
-            newChanges.priceAccounts[0].publishers.forEach(
-              (publisherKey: string) => {
-                pythProgramClient.methods
-                  .addPublisher(new PublicKey(publisherKey))
-                  .accounts({
-                    fundingAccount,
-                    priceAccount: priceAccountKey,
-                  })
-                  .instruction()
-                  .then((instruction) => instructions.push(instruction))
-              }
+
+          for (let publisherKey of newChanges.priceAccounts[0].publishers) {
+            instructions.push(
+              await pythProgramClient.methods
+                .addPublisher(new PublicKey(publisherKey))
+                .accounts({
+                  fundingAccount,
+                  priceAccount: priceAccountKey,
+                })
+                .instruction()
             )
           }
 
@@ -351,6 +387,8 @@ const General = () => {
             )
           }
         } else if (!newChanges) {
+          const priceAccount = new PublicKey(prev.priceAccounts[0].address)
+
           // if new is undefined, it means that the symbol is deleted
           // create delete price account instruction
           instructions.push(
@@ -359,10 +397,11 @@ const General = () => {
               .accounts({
                 fundingAccount,
                 productAccount: new PublicKey(prev.address),
-                priceAccount: new PublicKey(prev.priceAccounts[0].address),
+                priceAccount,
               })
               .instruction()
           )
+
           // create delete product account instruction
           instructions.push(
             await pythProgramClient.methods
@@ -374,6 +413,26 @@ const General = () => {
               })
               .instruction()
           )
+
+          if (isMessageBufferAvailable(cluster) && messageBufferClient) {
+            // create delete buffer instruction for the price buffer
+            instructions.push(
+              await messageBufferClient.methods
+                .deleteBuffer(
+                  getPythOracleMessageBufferCpiAuth(cluster),
+                  priceAccount
+                )
+                .accounts({
+                  admin: fundingAccount,
+                  payer: PRICE_FEED_OPS_KEY,
+                  messageBuffer: getMessageBufferAddressForPrice(
+                    cluster,
+                    priceAccount
+                  ),
+                })
+                .instruction()
+            )
+          }
         } else {
           // check if metadata has changed
           if (
@@ -437,45 +496,50 @@ const General = () => {
           )
 
           // add instructions to remove publishers
-          publisherKeysToRemove.forEach((publisherKey: string) => {
-            pythProgramClient.methods
-              .delPublisher(new PublicKey(publisherKey))
-              .accounts({
-                fundingAccount,
-                priceAccount: new PublicKey(prev.priceAccounts[0].address),
-              })
-              .instruction()
-              .then((instruction) => instructions.push(instruction))
-          })
+
+          for (let publisherKey of publisherKeysToRemove) {
+            instructions.push(
+              await pythProgramClient.methods
+                .delPublisher(new PublicKey(publisherKey))
+                .accounts({
+                  fundingAccount,
+                  priceAccount: new PublicKey(prev.priceAccounts[0].address),
+                })
+                .instruction()
+            )
+          }
 
           // add instructions to add new publishers
-          publisherKeysToAdd.forEach((publisherKey: string) => {
-            pythProgramClient.methods
-              .addPublisher(new PublicKey(publisherKey))
-              .accounts({
-                fundingAccount,
-                priceAccount: new PublicKey(prev.priceAccounts[0].address),
-              })
-              .instruction()
-              .then((instruction) => instructions.push(instruction))
-          })
+          for (let publisherKey of publisherKeysToAdd) {
+            instructions.push(
+              await pythProgramClient.methods
+                .addPublisher(new PublicKey(publisherKey))
+                .accounts({
+                  fundingAccount,
+                  priceAccount: new PublicKey(prev.priceAccounts[0].address),
+                })
+                .instruction()
+            )
+          }
         }
       }
 
       setIsSendProposalButtonLoading(true)
       try {
-        const proposalPubkey = await proposeInstructions(
-          proposeSquads,
-          PRICE_FEED_MULTISIG[getMultisigCluster(cluster)],
+        const response = await axios.post(proposerServerUrl + '/api/propose', {
           instructions,
-          isRemote,
-          wormholeAddress
-        )
+          cluster,
+        })
+        const { proposalPubkey } = response.data
         toast.success(`Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`)
         setIsSendProposalButtonLoading(false)
         closeModal()
-      } catch (e: any) {
-        toast.error(capitalizeFirstLetter(e.message))
+      } catch (error: any) {
+        if (error.response) {
+          toast.error(capitalizeFirstLetter(error.response.data))
+        } else {
+          toast.error(capitalizeFirstLetter(error.message))
+        }
         setIsSendProposalButtonLoading(false)
       }
     }
@@ -737,6 +801,16 @@ const General = () => {
       setPythProgramClient(
         pythOracleProgram(getPythProgramKeyForCluster(cluster), provider)
       )
+
+      if (isMessageBufferAvailable(cluster)) {
+        setMessageBufferClient(
+          new Program(
+            messageBuffer as Idl,
+            new PublicKey(MESSAGE_BUFFER_PROGRAM_ID),
+            provider
+          ) as unknown as Program<MessageBuffer>
+        )
+      }
     }
   }, [connection, connected, cluster, proposeSquads])
 
@@ -764,11 +838,13 @@ const General = () => {
             isPermission={true}
             pythProgramClient={pythProgramClient}
             squads={proposeSquads}
+            proposerServerUrl={proposerServerUrl}
           />
           <PermissionDepermissionKey
             isPermission={false}
             pythProgramClient={pythProgramClient}
             squads={proposeSquads}
+            proposerServerUrl={proposerServerUrl}
           />
         </div>
         <div className="relative mt-6">

+ 47 - 6
governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx

@@ -17,9 +17,11 @@ import {
   getMultisigCluster,
   getProposals,
   MultisigInstruction,
+  MultisigInstructionProgram,
   MultisigParser,
   PRICE_FEED_MULTISIG,
   PythMultisigInstruction,
+  MessageBufferMultisigInstruction,
   UnrecognizedProgram,
   WormholeMultisigInstruction,
 } from 'xc_admin_common'
@@ -788,9 +790,12 @@ const Proposal = ({
                                 {parsedInstruction instanceof
                                 PythMultisigInstruction
                                   ? 'Pyth Oracle'
-                                  : innerInstruction instanceof
+                                  : parsedInstruction instanceof
                                     WormholeMultisigInstruction
                                   ? 'Wormhole'
+                                  : parsedInstruction instanceof
+                                    MessageBufferMultisigInstruction
+                                  ? 'Message Buffer'
                                   : 'Unknown'}
                               </div>
                             </div>
@@ -803,7 +808,9 @@ const Proposal = ({
                                 {parsedInstruction instanceof
                                   PythMultisigInstruction ||
                                 parsedInstruction instanceof
-                                  WormholeMultisigInstruction
+                                  WormholeMultisigInstruction ||
+                                parsedInstruction instanceof
+                                  MessageBufferMultisigInstruction
                                   ? parsedInstruction.name
                                   : 'Unknown'}
                               </div>
@@ -816,7 +823,9 @@ const Proposal = ({
                               {parsedInstruction instanceof
                                 PythMultisigInstruction ||
                               parsedInstruction instanceof
-                                WormholeMultisigInstruction ? (
+                                WormholeMultisigInstruction ||
+                              parsedInstruction instanceof
+                                MessageBufferMultisigInstruction ? (
                                 Object.keys(parsedInstruction.args).length >
                                 0 ? (
                                   <div className="col-span-4 mt-2 bg-[#444157] p-4 lg:col-span-3 lg:mt-0">
@@ -906,7 +915,9 @@ const Proposal = ({
                             {parsedInstruction instanceof
                               PythMultisigInstruction ||
                             parsedInstruction instanceof
-                              WormholeMultisigInstruction ? (
+                              WormholeMultisigInstruction ||
+                            parsedInstruction instanceof
+                              MessageBufferMultisigInstruction ? (
                               <div
                                 key={`${index}_accounts`}
                                 className="grid grid-cols-4 justify-between"
@@ -983,9 +994,36 @@ const Proposal = ({
                                         ) : null}
                                       </>
                                     ))}
+                                    {parsedInstruction.accounts.remaining.map(
+                                      (accountMeta, index) => (
+                                        <>
+                                          <div
+                                            key="rem-{index}"
+                                            className="flex justify-between border-t border-beige-300 py-3"
+                                          >
+                                            <div className="max-w-[80px] break-words sm:max-w-none sm:break-normal">
+                                              Remaining {index + 1}
+                                            </div>
+                                            <div className="space-y-2 sm:flex sm:space-y-0 sm:space-x-2">
+                                              <div className="flex items-center space-x-2 sm:ml-2">
+                                                {accountMeta.isSigner ? (
+                                                  <SignerTag />
+                                                ) : null}
+                                                {accountMeta.isWritable ? (
+                                                  <WritableTag />
+                                                ) : null}
+                                              </div>
+                                              <CopyPubkey
+                                                pubkey={accountMeta.pubkey.toBase58()}
+                                              />
+                                            </div>
+                                          </div>
+                                        </>
+                                      )
+                                    )}
                                   </div>
                                 ) : (
-                                  <div>No arguments</div>
+                                  <div>No accounts</div>
                                 )}
                               </div>
                             ) : parsedInstruction instanceof
@@ -1125,7 +1163,10 @@ const Proposals = ({
                       keys: remoteIx.keys as AccountMeta[],
                     })
                   return (
-                    parsedRemoteInstruction instanceof PythMultisigInstruction
+                    parsedRemoteInstruction instanceof
+                      PythMultisigInstruction ||
+                    parsedRemoteInstruction instanceof
+                      MessageBufferMultisigInstruction
                   )
                 }) &&
                 ix.governanceAction.targetChainId === 'pythnet')

+ 9 - 6
governance/xc_admin/packages/xc_admin_frontend/components/tabs/UpdatePermissions.tsx

@@ -21,9 +21,9 @@ import {
   getMultisigCluster,
   isRemoteCluster,
   mapKey,
-  proposeInstructions,
   WORMHOLE_ADDRESS,
   UPGRADE_MULTISIG,
+  MultisigVault,
 } from 'xc_admin_common'
 import { ClusterContext } from '../../contexts/ClusterContext'
 import { useMultisigContext } from '../../contexts/MultisigContext'
@@ -266,13 +266,16 @@ const UpdatePermissions = () => {
           if (!isMultisigLoading) {
             setIsSendProposalButtonLoading(true)
             try {
-              const proposalPubkey = await proposeInstructions(
+              const vault = new MultisigVault(
+                proposeSquads.wallet,
+                getMultisigCluster(cluster),
                 proposeSquads,
-                UPGRADE_MULTISIG[getMultisigCluster(cluster)],
-                [instruction],
-                isRemoteCluster(cluster),
-                WORMHOLE_ADDRESS[getMultisigCluster(cluster)]
+                UPGRADE_MULTISIG[getMultisigCluster(cluster)]
               )
+
+              const proposalPubkey = (
+                await vault.proposeInstructions([instruction], cluster)
+              )[0]
               toast.success(
                 `Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`
               )

+ 1 - 0
governance/xc_admin/packages/xc_admin_frontend/next.config.js

@@ -5,6 +5,7 @@ const nextConfig = {
     externalDir: true,
   },
   webpack(config) {
+    config.experiments = { asyncWebAssembly: true }
     config.resolve.fallback = { fs: false }
     const fileLoaderRule = config.module.rules.find(
       (rule) => rule.test && rule.test.test('.svg')

+ 3 - 1
governance/xc_admin/packages/xc_admin_frontend/package.json

@@ -25,6 +25,7 @@
     "@types/node": "^18.11.18",
     "@types/react": "18.0.26",
     "@types/react-dom": "18.0.10",
+    "axios": "^1.4.0",
     "copy-to-clipboard": "^3.3.3",
     "gsap": "^3.11.4",
     "next": "12.2.5",
@@ -34,7 +35,8 @@
     "react-hot-toast": "^2.4.0",
     "typescript": "4.9.4",
     "use-debounce": "^9.0.2",
-    "xc_admin_common": "*"
+    "xc_admin_common": "*",
+    "message_buffer": "*"
   },
   "devDependencies": {
     "@svgr/webpack": "^6.3.1",

+ 7 - 14
governance/xc_admin/packages/xc_admin_frontend/pages/index.tsx

@@ -17,11 +17,6 @@ import { StatusFilterProvider } from '../contexts/StatusFilterContext'
 import { classNames } from '../utils/classNames'
 
 export const getServerSideProps: GetServerSideProps = async () => {
-  const KEYPAIR_BASE_PATH = process.env.KEYPAIR_BASE_PATH || ''
-  const OPS_WALLET = fs.existsSync(`${KEYPAIR_BASE_PATH}/ops-key`)
-    ? JSON.parse(fs.readFileSync(`${KEYPAIR_BASE_PATH}/ops-key`, 'ascii'))
-    : null
-
   const MAPPINGS_BASE_PATH = process.env.MAPPINGS_BASE_PATH || ''
   const PUBLISHER_PYTHNET_MAPPING_PATH = `${MAPPINGS_BASE_PATH}/publishers-pythnet.json`
   const PUBLISHER_PYTHTEST_MAPPING_PATH = `${MAPPINGS_BASE_PATH}/publishers-pythtest.json`
@@ -51,9 +46,11 @@ export const getServerSideProps: GetServerSideProps = async () => {
       )
     : {}
 
+  const proposerServerUrl =
+    process.env.PROPOSER_SERVER_URL || 'http://localhost:4000'
   return {
     props: {
-      OPS_WALLET,
+      proposerServerUrl,
       publisherKeyToNameMapping,
       multisigSignerKeyToNameMapping,
     },
@@ -81,22 +78,18 @@ const TAB_INFO = {
 const DEFAULT_TAB = 'general'
 
 const Home: NextPage<{
-  OPS_WALLET: number[] | null
   publisherKeyToNameMapping: Record<string, Record<string, string>>
   multisigSignerKeyToNameMapping: Record<string, string>
+  proposerServerUrl: string
 }> = ({
-  OPS_WALLET,
   publisherKeyToNameMapping,
   multisigSignerKeyToNameMapping,
+  proposerServerUrl,
 }) => {
   const [currentTabIndex, setCurrentTabIndex] = useState(0)
   const tabInfoArray = Object.values(TAB_INFO)
   const anchorWallet = useAnchorWallet()
-  const wallet = OPS_WALLET
-    ? (new NodeWallet(
-        Keypair.fromSecretKey(Uint8Array.from(OPS_WALLET))
-      ) as Wallet)
-    : (anchorWallet as Wallet)
+  const wallet = anchorWallet as Wallet
 
   const router = useRouter()
 
@@ -158,7 +151,7 @@ const Home: NextPage<{
           </div>
           {tabInfoArray[currentTabIndex].queryString ===
           TAB_INFO.General.queryString ? (
-            <General />
+            <General proposerServerUrl={proposerServerUrl} />
           ) : tabInfoArray[currentTabIndex].queryString ===
             TAB_INFO.UpdatePermissions.queryString ? (
             <UpdatePermissions />

+ 2 - 2
governance/xc_admin/packages/xc_admin_frontend/tsconfig.json

@@ -3,7 +3,6 @@
     "target": "es2020",
     "lib": ["dom", "dom.iterable", "esnext"],
     "allowJs": true,
-    "skipLibCheck": true,
     "strict": true,
     "forceConsistentCasingInFileNames": true,
     "noEmit": true,
@@ -13,7 +12,8 @@
     "resolveJsonModule": true,
     "isolatedModules": true,
     "jsx": "preserve",
-    "incremental": true
+    "incremental": true,
+    "skipLibCheck": true
   },
   "include": [
     "next-env.d.ts",

+ 24 - 0
governance/xc_governance_sdk_js/src/chains.ts

@@ -1,6 +1,19 @@
 import { CHAINS as WORMHOLE_CHAINS } from "@certusone/wormhole-sdk";
 
 export { CHAINS as WORMHOLE_CHAINS } from "@certusone/wormhole-sdk";
+// GUIDELINES to add a chain
+// PYTH will have:
+// 1. Mainnet Deployment - which will have pyth mainnet governance and data sources
+// 2. Testnet Stable Deployment - which will also have pyth mainnet governance and data sources
+// 3. Testnet Edge Deployment - which will have pyth testnet governance and data sources.
+// Different chains will have different chain ids i.e., mainnet and testnet will have different chain ids.
+// Though stable and edge contracts on testnet will share the same chain id. They are governed by different
+// sources hence there is no chance of collision.
+
+// If there is already a chain id in wormhole sdk. Use that for Mainnet
+// Else add a chain id for mainnet too.
+// Add an id for the testnet
+// Currently we are deploying this for cosmos chains only. But this will be for all the chains in future.
 export const RECEIVER_CHAINS = {
   cronos: 60001,
   kcc: 60002,
@@ -14,6 +27,17 @@ export const RECEIVER_CHAINS = {
   meter: 60010,
   mantle: 60011,
   conflux_espace: 60012,
+  injective_testnet: 60013,
+  osmosis: 60014,
+  osmosis_testnet_4: 60015,
+  osmosis_testnet_5: 60016,
+  sei_pacific_1: 60017,
+  sei_testnet_atlantic_2: 60018,
+  neutron: 60019,
+  neutron_testnet_pion_1: 60020,
+  juno: 60020,
+  juno_testnet: 60021,
+  kava: 60022,
 };
 
 // If there is any overlapping value the receiver chain will replace the wormhole

+ 1 - 0
governance/xc_governance_sdk_js/src/index.ts

@@ -2,6 +2,7 @@ export {
   DataSource,
   AptosAuthorizeUpgradeContractInstruction,
   EthereumUpgradeContractInstruction,
+  EthereumSetWormholeAddress,
   HexString20Bytes,
   HexString32Bytes,
   SetDataSourcesInstruction,

+ 11 - 0
governance/xc_governance_sdk_js/src/instructions.ts

@@ -14,6 +14,7 @@ enum TargetAction {
   SetFee,
   SetValidPeriod,
   RequestGovernanceDataSourceTransfer,
+  SetWormholeAddress,
 }
 
 abstract class HexString implements Serializable {
@@ -194,3 +195,13 @@ export class RequestGovernanceDataSourceTransferInstruction extends TargetInstru
       .build();
   }
 }
+
+export class EthereumSetWormholeAddress extends TargetInstruction {
+  constructor(targetChainId: ChainId, private address: HexString20Bytes) {
+    super(TargetAction.SetWormholeAddress, targetChainId);
+  }
+
+  protected serializePayload(): Buffer {
+    return this.address.serialize();
+  }
+}

+ 1 - 2
governance/xc_governance_sdk_js/tsconfig.json

@@ -4,7 +4,6 @@
   "exclude": ["node_modules", "**/__tests__/*"],
   "compilerOptions": {
     "rootDir": "src/",
-    "outDir": "./lib",
-    "skipLibCheck": true
+    "outDir": "./lib"
   }
 }

+ 1 - 1
hermes/.gitignore

@@ -7,4 +7,4 @@ src/network/p2p.proto
 tools/
 
 # Ignore Wormhole cloned repo
-wormhole/
+wormhole*/

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 443 - 262
hermes/Cargo.lock


+ 55 - 46
hermes/Cargo.toml

@@ -1,62 +1,71 @@
 [package]
-name                           = "hermes"
-version                        = "0.1.0"
-edition                        = "2021"
+name                   = "hermes"
+version                = "0.1.4"
+edition                = "2021"
 
 [dependencies]
-axum                           = { version = "0.6.9", features = ["json", "ws", "macros"] }
-axum-extra                     = { version = "0.7.2", features = ["query"] }
-axum-macros                    = { version = "0.3.4" }
-anyhow                         = { version = "1.0.69" }
-base64                         = { version = "0.21.0" }
-borsh                          = { version = "0.9.0" }
-bs58                           = { version = "0.4.0" }
-dashmap                        = { version = "5.4.0" }
-der                            = { version = "0.7.0" }
-derive_more                    = { version = "0.99.17" }
-env_logger                     = { version = "0.10.0" }
-futures                        = { version = "0.3.26" }
-hex                            = { version = "0.4.3" }
-rand                           = { version = "0.8.5" }
-reqwest                        = { version = "0.11.14", features = ["blocking", "json"] }
-ring                           = { version = "0.16.20" }
-rusqlite                       = { version = "0.28.0", features = ["bundled"] }
-lazy_static                    = { version = "1.4.0" }
-libc                           = { version = "0.2.140" }
-pyth-sdk                       = { version = "0.7.0" }
-secp256k1                      = { version = "0.26.0", features = ["rand", "recovery", "serde"] }
-serde                          = { version = "1.0.152", features = ["derive"] }
-serde_arrays                   = { version = "0.1.0" }
-serde_cbor                     = { version = "0.11.2" }
-serde_json                     = { version = "1.0.93" }
-sha256                         = { version = "1.1.2" }
-structopt                      = { version = "0.3.26" }
-tokio                          = { version = "1.26.0", features = ["full"] }
-typescript-type-def            = { version = "0.5.5" }
-log                            = { version = "0.4.17" }
-
-# Parse Wormhole VAAs from our own patch. TODO: Replace with released version when wormhole releases it
-wormhole-core                  = { git = "https://github.com/guibescos/wormhole", branch = "reisen/sdk-solana"}
-
-# Parse Wormhole attester price attestations.
-pyth-wormhole-attester-sdk     = { path = "../wormhole_attester/sdk/rust/", version = "0.1.2" }
+anyhow                 = { version = "1.0.69" }
+async-trait            = { version = "0.1.68" }
+axum                   = { version = "0.6.9", features = ["json", "ws", "macros"] }
+axum-macros            = { version = "0.3.4" }
+base64                 = { version = "0.21.0" }
+borsh                  = { version = "0.9.0" }
+byteorder              = { version = "1.4.3" }
+dashmap                = { version = "5.4.0" }
+derive_more            = { version = "0.99.17" }
+env_logger             = { version = "0.10.0" }
+futures                = { version = "0.3.28" }
+hex                    = { version = "0.4.3" }
+humantime              = { version = "2.1.0" }
+lazy_static            = { version = "1.4.0" }
+libc                   = { version = "0.2.140" }
 
 # Setup LibP2P. Unfortunately the dependencies required by libp2p are shared
 # with the dependencies required by solana's geyser plugin. This means that we
 # would have to use the same version of libp2p as solana. Luckily we don't need
 # to do this yet but it's something to keep in mind.
-libp2p                         = { version = "0.51.1", features = [
-    "dns",
+libp2p                 = { version = "0.42.2", features = [
     "gossipsub",
     "identify",
-    "macros",
     "mplex",
     "noise",
-    "quic",
     "secp256k1",
-    "tcp",
-    "tls",
-    "tokio",
     "websocket",
     "yamux",
 ]}
+
+log                    = { version = "0.4.17" }
+prometheus-client      = { version = "0.21.1" }
+pyth-sdk               = { version = "0.7.0" }
+
+# Parse Wormhole attester price attestations.
+pythnet-sdk            = { path = "../pythnet/pythnet_sdk/", version = "2.0.0", features = ["strum"] }
+
+rand                   = { version = "0.8.5" }
+reqwest                = { version = "0.11.14", features = ["blocking", "json"] }
+secp256k1              = { version = "0.26.0", features = ["rand", "recovery", "serde"] }
+serde                  = { version = "1.0.152", features = ["derive"] }
+serde_json             = { version = "1.0.93" }
+serde_qs               = { version = "0.12.0", features = ["axum"] }
+serde_wormhole         = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1" }
+sha3                   = { version = "0.10.4" }
+
+# We around bound to this version because of pyth-oracle
+solana-client          = { version = "=1.13.3" }
+solana-sdk             = { version = "=1.13.3" }
+solana-account-decoder = { version = "=1.13.3" }
+
+structopt              = { version = "0.3.26" }
+strum                  = { version = "0.24.1", features = ["derive"] }
+tokio                  = { version = "1.26.0", features = ["full"] }
+tower-http             = { version = "0.4.0", features = ["cors"] }
+wormhole-sdk           = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1" }
+
+[patch.crates-io]
+serde_wormhole         = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1" }
+
+[profile.release]
+panic                  = 'abort'
+
+[profile.dev]
+panic                  = 'abort'

+ 31 - 0
hermes/Dockerfile

@@ -0,0 +1,31 @@
+FROM docker.io/golang:1.20.4@sha256:6dd5c5f8936d7d4487802fb10a77f31b1776740be0fc17ada1acb74ac958f7be AS build
+
+# Install OS packages
+RUN apt-get update && apt-get install --yes \
+    build-essential curl clang libssl-dev
+
+# Install Rust
+RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --quiet --no-modify-path
+ENV PATH="/root/.cargo/bin:${PATH}"
+
+# Install Solana
+RUN sh -c "$(curl -sSfL https://release.solana.com/v1.14.17/install)"
+ENV PATH="/root/.local/share/solana/install/active_release/bin:$PATH"
+
+# Set default toolchain
+RUN rustup default nightly-2023-01-15
+
+# Build
+WORKDIR /src
+COPY hermes hermes
+COPY pythnet/pythnet_sdk pythnet/pythnet_sdk
+
+
+WORKDIR /src/hermes
+
+RUN --mount=type=cache,target=/root/.cargo/registry cargo build --release
+
+FROM docker.io/golang:1.20.4@sha256:6dd5c5f8936d7d4487802fb10a77f31b1776740be0fc17ada1acb74ac958f7be
+
+# Copy artifacts from other images
+COPY --from=build /src/hermes/target/release/hermes /usr/local/bin/

+ 32 - 18
hermes/build.rs

@@ -1,63 +1,79 @@
 use std::{
     env,
     path::PathBuf,
-    process::Command,
+    process::{
+        Command,
+        Stdio,
+    },
 };
 
 fn main() {
     let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
     let out_var = env::var("OUT_DIR").unwrap();
 
-    // Clone the Wormhole repository, which we need to access the protobuf definitions for Wormhole
-    // P2P message types.
+    // Download the Wormhole repository at a certain tag, which we need to access the protobuf definitions
+    // for Wormhole P2P message types.
     //
-    // TODO: This is ugly and costly, and requires git. Instead of this we should have our own tool
+    // TODO: This is ugly. Instead of this we should have our own tool
     // build process that can generate protobuf definitions for this and other user cases. For now
     // this is easy and works and matches upstream Wormhole's `Makefile`.
-    let _ = Command::new("git")
+
+    const WORMHOLE_VERSION: &str = "2.18.1";
+
+    let wh_curl = Command::new("curl")
         .args([
-            "clone",
-            "https://github.com/wormhole-foundation/wormhole",
-            "wormhole",
+            "-s",
+            "-L",
+            format!("https://github.com/wormhole-foundation/wormhole/archive/refs/tags/v{WORMHOLE_VERSION}.tar.gz").as_str(),
         ])
+        .stdout(Stdio::piped())
+        .spawn()
+        .expect("failed to download wormhole archive");
+
+    let _ = Command::new("tar")
+        .args(["xvz"])
+        .stdin(Stdio::from(wh_curl.stdout.unwrap()))
         .output()
-        .expect("failed to execute process");
+        .expect("failed to extract wormhole archive");
 
     // Move the tools directory to the root of the repo because that's where the build script
     // expects it to be, paths get hardcoded into the binaries.
     let _ = Command::new("mv")
-        .args(["wormhole/tools", "tools"])
+        .args([
+            format!("wormhole-{WORMHOLE_VERSION}/tools").as_str(),
+            "tools",
+        ])
         .output()
-        .expect("failed to execute process");
+        .expect("failed to move wormhole tools directory");
 
     // Move the protobuf definitions to the src/network directory, we don't have to do this
     // but it is more intuitive when debugging.
     let _ = Command::new("mv")
         .args([
-            "wormhole/proto/gossip/v1/gossip.proto",
+            format!("wormhole-{WORMHOLE_VERSION}/proto/gossip/v1/gossip.proto").as_str(),
             "src/network/p2p.proto",
         ])
         .output()
-        .expect("failed to execute process");
+        .expect("failed to move wormhole protobuf definitions");
 
     // Build the protobuf compiler.
     let _ = Command::new("./build.sh")
         .current_dir("tools")
         .output()
-        .expect("failed to execute process");
+        .expect("failed to run protobuf compiler build script");
 
     // Make the protobuf compiler executable.
     let _ = Command::new("chmod")
         .args(["+x", "tools/bin/*"])
         .output()
-        .expect("failed to execute process");
+        .expect("failed to make protofuf compiler executable");
 
     // Generate the protobuf definitions. See buf.gen.yaml to see how we rename the module for our
     // particular use case.
     let _ = Command::new("./tools/bin/buf")
         .args(["generate", "--path", "src"])
         .output()
-        .expect("failed to execute process");
+        .expect("failed to generate protobuf definitions");
 
     // Build the Go library.
     let mut cmd = Command::new("go");
@@ -71,8 +87,6 @@ fn main() {
     // Tell Rust to link our Go library at compile time.
     println!("cargo:rustc-link-search=native={out_var}");
     println!("cargo:rustc-link-lib=static=pythnet");
-
-    #[cfg(target_arch = "aarch64")]
     println!("cargo:rustc-link-lib=resolv");
 
     let status = cmd.status().unwrap();

+ 1 - 1
hermes/shell.nix

@@ -7,7 +7,7 @@ with pkgs; mkShell {
     clang
     llvmPackages.libclang
     nettle
-    openssl
+    openssl_1_1
     pkgconfig
     rustup
     systemd

+ 32 - 23
hermes/src/network/rpc.rs → hermes/src/api.rs

@@ -1,18 +1,17 @@
 use {
-    self::ws::dispatch_updates,
-    crate::{
-        network::p2p::OBSERVATIONS,
-        store::{
-            Store,
-            Update,
-        },
-    },
+    self::ws::notify_updates,
+    crate::store::Store,
     anyhow::Result,
     axum::{
         routing::get,
         Router,
     },
     std::sync::Arc,
+    tokio::{
+        signal,
+        sync::mpsc::Receiver,
+    },
+    tower_http::cors::CorsLayer,
 };
 
 mod rest;
@@ -21,12 +20,12 @@ mod ws;
 
 #[derive(Clone)]
 pub struct State {
-    pub store: Store,
+    pub store: Arc<Store>,
     pub ws:    Arc<ws::WsState>,
 }
 
 impl State {
-    pub fn new(store: Store) -> Self {
+    pub fn new(store: Arc<Store>) -> Self {
         Self {
             store,
             ws: Arc::new(ws::WsState::new()),
@@ -36,9 +35,9 @@ impl State {
 
 /// This method provides a background service that responds to REST requests
 ///
-/// Currently this is based on Axum due to the simplicity and strong ecosyjtem support for the
+/// Currently this is based on Axum due to the simplicity and strong ecosystem support for the
 /// packages they are based on (tokio & hyper).
-pub async fn spawn(rpc_addr: String, store: Store) -> Result<()> {
+pub async fn run(store: Arc<Store>, mut update_rx: Receiver<()>, rpc_addr: String) -> Result<()> {
     let state = State::new(store);
 
     // Initialize Axum Router. Note the type here is a `Router<State>` due to the use of the
@@ -47,32 +46,42 @@ pub async fn spawn(rpc_addr: String, store: Store) -> Result<()> {
     let app = app
         .route("/", get(rest::index))
         .route("/live", get(rest::live))
+        .route("/ready", get(rest::ready))
         .route("/ws", get(ws::ws_route_handler))
         .route("/api/latest_price_feeds", get(rest::latest_price_feeds))
         .route("/api/latest_vaas", get(rest::latest_vaas))
+        .route("/api/get_price_feed", get(rest::get_price_feed))
         .route("/api/get_vaa", get(rest::get_vaa))
         .route("/api/get_vaa_ccip", get(rest::get_vaa_ccip))
         .route("/api/price_feed_ids", get(rest::price_feed_ids))
-        .with_state(state.clone());
+        .with_state(state.clone())
+        .layer(CorsLayer::permissive()); // Permissive CORS layer to allow all origins
 
-    // Listen in the background for new VAA's from the Wormhole RPC.
+
+    // Call dispatch updates to websocket every 1 seconds
+    // FIXME use a channel to get updates from the store
     tokio::spawn(async move {
         loop {
-            if let Ok(observation) = OBSERVATIONS.1.lock().unwrap().recv() {
-                match state.store.store_update(Update::Vaa(observation)) {
-                    Ok(updated_feed_ids) => {
-                        tokio::spawn(dispatch_updates(updated_feed_ids, state.clone()));
-                    }
-                    Err(e) => log::error!("Failed to process VAA: {:?}", e),
-                }
-            }
+            // Panics if the update channel is closed, which should never happen.
+            // If it happens we have no way to recover, so we just panic.
+            update_rx
+                .recv()
+                .await
+                .expect("state update channel is closed");
+
+            notify_updates(state.ws.clone()).await;
         }
     });
 
     // Binds the axum's server to the configured address and port. This is a blocking call and will
     // not return until the server is shutdown.
-    axum::Server::bind(&rpc_addr.parse()?)
+    axum::Server::try_bind(&rpc_addr.parse()?)?
         .serve(app.into_make_service())
+        .with_graceful_shutdown(async {
+            signal::ctrl_c()
+                .await
+                .expect("Ctrl-c signal handler failed.");
+        })
         .await?;
 
     Ok(())

+ 99 - 39
hermes/src/network/rpc/rest.rs → hermes/src/api/rest.rs

@@ -1,10 +1,14 @@
-use super::types::PriceIdInput;
 use {
-    super::types::RpcPriceFeed,
-    crate::store::RequestTime,
+    super::types::{
+        PriceIdInput,
+        RpcPriceFeed,
+    },
     crate::{
         impl_deserialize_for_hex_string_wrapper,
-        store::UnixTimestamp,
+        store::types::{
+            RequestTime,
+            UnixTimestamp,
+        },
     },
     anyhow::Result,
     axum::{
@@ -16,7 +20,6 @@ use {
         },
         Json,
     },
-    axum_extra::extract::Query, // Axum extra Query allows us to parse multi-value query parameters.
     base64::{
         engine::general_purpose::STANDARD as base64_standard_engine,
         Engine as _,
@@ -26,11 +29,14 @@ use {
         DerefMut,
     },
     pyth_sdk::PriceIdentifier,
+    serde_qs::axum::QsQuery,
+    std::collections::HashSet,
 };
 
 pub enum RestError {
     UpdateDataNotFound,
     CcipUpdateDataNotFound,
+    InvalidCCIPInput,
 }
 
 impl IntoResponse for RestError {
@@ -49,14 +55,17 @@ impl IntoResponse for RestError {
 
                 (StatusCode::BAD_GATEWAY, "CCIP update data not found").into_response()
             }
+            RestError::InvalidCCIPInput => {
+                (StatusCode::BAD_REQUEST, "Invalid CCIP input").into_response()
+            }
         }
     }
 }
 
 pub async fn price_feed_ids(
     State(state): State<super::State>,
-) -> Result<Json<Vec<PriceIdentifier>>, RestError> {
-    let price_feeds = state.store.get_price_feed_ids();
+) -> Result<Json<HashSet<PriceIdentifier>>, RestError> {
+    let price_feeds = state.store.get_price_feed_ids().await;
     Ok(Json(price_feeds))
 }
 
@@ -68,19 +77,19 @@ pub struct LatestVaasQueryParams {
 
 pub async fn latest_vaas(
     State(state): State<super::State>,
-    Query(params): Query<LatestVaasQueryParams>,
+    QsQuery(params): QsQuery<LatestVaasQueryParams>,
 ) -> Result<Json<Vec<String>>, RestError> {
     let price_ids: Vec<PriceIdentifier> = params.ids.into_iter().map(|id| id.into()).collect();
     let price_feeds_with_update_data = state
         .store
         .get_price_feeds_with_update_data(price_ids, RequestTime::Latest)
+        .await
         .map_err(|_| RestError::UpdateDataNotFound)?;
     Ok(Json(
         price_feeds_with_update_data
-            .batch_vaa
-            .update_data
+            .wormhole_merkle_update_data
             .iter()
-            .map(|vaa_bytes| base64_standard_engine.encode(vaa_bytes)) // TODO: Support multiple
+            .map(|bytes| base64_standard_engine.encode(bytes)) // TODO: Support multiple
             // encoding formats
             .collect(),
     ))
@@ -97,25 +106,61 @@ pub struct LatestPriceFeedsQueryParams {
 
 pub async fn latest_price_feeds(
     State(state): State<super::State>,
-    Query(params): Query<LatestPriceFeedsQueryParams>,
+    QsQuery(params): QsQuery<LatestPriceFeedsQueryParams>,
 ) -> Result<Json<Vec<RpcPriceFeed>>, RestError> {
     let price_ids: Vec<PriceIdentifier> = params.ids.into_iter().map(|id| id.into()).collect();
     let price_feeds_with_update_data = state
         .store
         .get_price_feeds_with_update_data(price_ids, RequestTime::Latest)
+        .await
         .map_err(|_| RestError::UpdateDataNotFound)?;
     Ok(Json(
         price_feeds_with_update_data
-            .batch_vaa
-            .price_infos
-            .into_values()
-            .map(|price_info| {
-                RpcPriceFeed::from_price_info(price_info, params.verbose, params.binary)
+            .price_feeds
+            .into_iter()
+            .map(|price_feed| {
+                RpcPriceFeed::from_price_feed_update(price_feed, params.verbose, params.binary)
             })
             .collect(),
     ))
 }
 
+#[derive(Debug, serde::Deserialize)]
+pub struct GetPriceFeedQueryParams {
+    id:           PriceIdInput,
+    publish_time: UnixTimestamp,
+    #[serde(default)]
+    verbose:      bool,
+    #[serde(default)]
+    binary:       bool,
+}
+
+pub async fn get_price_feed(
+    State(state): State<super::State>,
+    QsQuery(params): QsQuery<GetPriceFeedQueryParams>,
+) -> Result<Json<RpcPriceFeed>, RestError> {
+    let price_id: PriceIdentifier = params.id.into();
+
+    let price_feeds_with_update_data = state
+        .store
+        .get_price_feeds_with_update_data(
+            vec![price_id],
+            RequestTime::FirstAfter(params.publish_time),
+        )
+        .await
+        .map_err(|_| RestError::UpdateDataNotFound)?;
+
+    Ok(Json(RpcPriceFeed::from_price_feed_update(
+        price_feeds_with_update_data
+            .price_feeds
+            .into_iter()
+            .next()
+            .ok_or(RestError::UpdateDataNotFound)?,
+        params.verbose,
+        params.binary,
+    )))
+}
+
 #[derive(Debug, serde::Deserialize)]
 pub struct GetVaaQueryParams {
     id:           PriceIdInput,
@@ -124,14 +169,14 @@ pub struct GetVaaQueryParams {
 
 #[derive(Debug, serde::Serialize)]
 pub struct GetVaaResponse {
-    pub vaa:          String,
+    vaa:          String,
     #[serde(rename = "publishTime")]
-    pub publish_time: UnixTimestamp,
+    publish_time: UnixTimestamp,
 }
 
 pub async fn get_vaa(
     State(state): State<super::State>,
-    Query(params): Query<GetVaaQueryParams>,
+    QsQuery(params): QsQuery<GetVaaQueryParams>,
 ) -> Result<Json<GetVaaResponse>, RestError> {
     let price_id: PriceIdentifier = params.id.into();
 
@@ -141,21 +186,21 @@ pub async fn get_vaa(
             vec![price_id],
             RequestTime::FirstAfter(params.publish_time),
         )
+        .await
         .map_err(|_| RestError::UpdateDataNotFound)?;
 
     let vaa = price_feeds_with_update_data
-        .batch_vaa
-        .update_data
+        .wormhole_merkle_update_data
         .get(0)
-        .map(|vaa_bytes| base64_standard_engine.encode(vaa_bytes))
+        .map(|bytes| base64_standard_engine.encode(bytes))
         .ok_or(RestError::UpdateDataNotFound)?;
 
     let publish_time = price_feeds_with_update_data
-        .batch_vaa
-        .price_infos
-        .get(&price_id)
-        .map(|price_info| price_info.publish_time)
-        .ok_or(RestError::UpdateDataNotFound)?;
+        .price_feeds
+        .get(0)
+        .ok_or(RestError::UpdateDataNotFound)?
+        .price_feed
+        .publish_time;
 
     Ok(Json(GetVaaResponse { vaa, publish_time }))
 }
@@ -176,31 +221,44 @@ pub struct GetVaaCcipResponse {
 
 pub async fn get_vaa_ccip(
     State(state): State<super::State>,
-    Query(params): Query<GetVaaCcipQueryParams>,
+    QsQuery(params): QsQuery<GetVaaCcipQueryParams>,
 ) -> Result<Json<GetVaaCcipResponse>, RestError> {
-    let price_id: PriceIdentifier = PriceIdentifier::new(params.data[0..32].try_into().unwrap());
-    let publish_time = UnixTimestamp::from_be_bytes(params.data[32..40].try_into().unwrap());
+    let price_id: PriceIdentifier = PriceIdentifier::new(
+        params.data[0..32]
+            .try_into()
+            .map_err(|_| RestError::InvalidCCIPInput)?,
+    );
+    let publish_time = UnixTimestamp::from_be_bytes(
+        params.data[32..40]
+            .try_into()
+            .map_err(|_| RestError::InvalidCCIPInput)?,
+    );
 
     let price_feeds_with_update_data = state
         .store
         .get_price_feeds_with_update_data(vec![price_id], RequestTime::FirstAfter(publish_time))
+        .await
         .map_err(|_| RestError::CcipUpdateDataNotFound)?;
 
-    let vaa = price_feeds_with_update_data
-        .batch_vaa
-        .update_data
+    let bytes = price_feeds_with_update_data
+        .wormhole_merkle_update_data
         .get(0) // One price feed has only a single VAA as proof.
         .ok_or(RestError::UpdateDataNotFound)?;
 
     Ok(Json(GetVaaCcipResponse {
-        data: format!("0x{}", hex::encode(vaa)),
+        data: format!("0x{}", hex::encode(bytes)),
     }))
 }
 
-// This function implements the `/live` endpoint. It returns a `200` status code. This endpoint is
-// used by the Kubernetes liveness probe.
-pub async fn live() -> Result<impl IntoResponse, std::convert::Infallible> {
-    Ok(())
+pub async fn live() -> Response {
+    (StatusCode::OK, "OK").into_response()
+}
+
+pub async fn ready(State(state): State<super::State>) -> Response {
+    match state.store.is_ready().await {
+        true => (StatusCode::OK, "OK").into_response(),
+        false => (StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable").into_response(),
+    }
 }
 
 // This is the index page for the REST service. It will list all the available endpoints.
@@ -208,9 +266,11 @@ pub async fn live() -> Result<impl IntoResponse, std::convert::Infallible> {
 pub async fn index() -> impl IntoResponse {
     Json([
         "/live",
+        "/ready",
         "/api/price_feed_ids",
         "/api/latest_price_feeds?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&verbose=true)(&binary=true)",
         "/api/latest_vaas?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&...",
+        "/api/get_price_feed?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>(&verbose=true)(&binary=true)",
         "/api/get_vaa?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>",
         "/api/get_vaa_ccip?data=<0x<price_feed_id_32_bytes>+<publish_time_unix_timestamp_be_8_bytes>>",
     ])

+ 33 - 13
hermes/src/network/rpc/types.rs → hermes/src/api/types.rs

@@ -1,8 +1,9 @@
 use {
     crate::{
         impl_deserialize_for_hex_string_wrapper,
-        store::{
-            proof::batch_vaa::PriceInfo,
+        store::types::{
+            PriceFeedUpdate,
+            Slot,
             UnixTimestamp,
         },
     },
@@ -18,6 +19,7 @@ use {
         Price,
         PriceIdentifier,
     },
+    wormhole_sdk::Chain,
 };
 
 
@@ -40,9 +42,8 @@ type Base64String = String;
 
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 pub struct RpcPriceFeedMetadata {
+    pub slot:                       Slot,
     pub emitter_chain:              u16,
-    pub attestation_time:           UnixTimestamp,
-    pub sequence_number:            u64,
     pub price_service_receive_time: UnixTimestamp,
 }
 
@@ -51,26 +52,45 @@ pub struct RpcPriceFeed {
     pub id:        PriceIdentifier,
     pub price:     Price,
     pub ema_price: Price,
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub metadata:  Option<RpcPriceFeedMetadata>,
     /// Vaa binary represented in base64.
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub vaa:       Option<Base64String>,
 }
 
 impl RpcPriceFeed {
     // TODO: Use a Encoding type to have None, Base64, and Hex variants instead of binary flag.
     // TODO: Use a Verbosity type to define None, or Full instead of verbose flag.
-    pub fn from_price_info(price_info: PriceInfo, verbose: bool, binary: bool) -> Self {
+    pub fn from_price_feed_update(
+        price_feed_update: PriceFeedUpdate,
+        verbose: bool,
+        binary: bool,
+    ) -> Self {
+        let price_feed_message = price_feed_update.price_feed;
+
         Self {
-            id:        price_info.price_feed.id,
-            price:     price_info.price_feed.get_price_unchecked(),
-            ema_price: price_info.price_feed.get_ema_price_unchecked(),
+            id:        PriceIdentifier::new(price_feed_message.feed_id),
+            price:     Price {
+                price:        price_feed_message.price,
+                conf:         price_feed_message.conf,
+                expo:         price_feed_message.exponent,
+                publish_time: price_feed_message.publish_time,
+            },
+            ema_price: Price {
+                price:        price_feed_message.ema_price,
+                conf:         price_feed_message.ema_conf,
+                expo:         price_feed_message.exponent,
+                publish_time: price_feed_message.publish_time,
+            },
             metadata:  verbose.then_some(RpcPriceFeedMetadata {
-                emitter_chain:              price_info.emitter_chain,
-                attestation_time:           price_info.attestation_time,
-                sequence_number:            price_info.sequence_number,
-                price_service_receive_time: price_info.receive_time,
+                emitter_chain:              Chain::Pythnet.into(),
+                price_service_receive_time: price_feed_update.received_at,
+                slot:                       price_feed_update.slot,
             }),
-            vaa:       binary.then_some(base64_standard_engine.encode(price_info.vaa_bytes)),
+            vaa:       binary.then_some(
+                base64_standard_engine.encode(price_feed_update.wormhole_merkle_update_data),
+            ),
         }
     }
 }

+ 88 - 62
hermes/src/network/rpc/ws.rs → hermes/src/api/ws.rs

@@ -3,8 +3,14 @@ use {
         PriceIdInput,
         RpcPriceFeed,
     },
-    crate::store::Store,
-    anyhow::Result,
+    crate::store::{
+        types::RequestTime,
+        Store,
+    },
+    anyhow::{
+        anyhow,
+        Result,
+    },
     axum::{
         extract::{
             ws::{
@@ -33,14 +39,21 @@ use {
     },
     std::{
         collections::HashMap,
-        sync::atomic::{
-            AtomicUsize,
-            Ordering,
+        pin::Pin,
+        sync::{
+            atomic::{
+                AtomicUsize,
+                Ordering,
+            },
+            Arc,
         },
+        time::Duration,
     },
     tokio::sync::mpsc,
 };
 
+pub const PING_INTERVAL_DURATION: Duration = Duration::from_secs(30);
+pub const NOTIFICATIONS_CHAN_LEN: usize = 1000;
 
 pub async fn ws_route_handler(
     ws: WebSocketUpgrade,
@@ -52,19 +65,14 @@ pub async fn ws_route_handler(
 async fn websocket_handler(stream: WebSocket, state: super::State) {
     let ws_state = state.ws.clone();
     let id = ws_state.subscriber_counter.fetch_add(1, Ordering::SeqCst);
-
-    let (sender, receiver) = stream.split();
-
-    // TODO: Use a configured value for the buffer size or make it const static
-    // TODO: Use redis stream to source the updates instead of a channel
-    let (tx, rx) = mpsc::channel::<Vec<PriceIdentifier>>(1000);
-
-    ws_state.subscribers.insert(id, tx);
-
     log::debug!("New websocket connection, assigning id: {}", id);
 
-    let mut subscriber = Subscriber::new(id, state.store.clone(), rx, receiver, sender);
+    let (notify_sender, notify_receiver) = mpsc::channel::<()>(NOTIFICATIONS_CHAN_LEN);
+    let (sender, receiver) = stream.split();
+    let mut subscriber =
+        Subscriber::new(id, state.store.clone(), notify_receiver, receiver, sender);
 
+    ws_state.subscribers.insert(id, notify_sender);
     subscriber.run().await;
 }
 
@@ -75,18 +83,20 @@ pub type SubscriberId = usize;
 pub struct Subscriber {
     id:                      SubscriberId,
     closed:                  bool,
-    store:                   Store,
-    update_rx:               mpsc::Receiver<Vec<PriceIdentifier>>,
+    store:                   Arc<Store>,
+    notify_receiver:         mpsc::Receiver<()>,
     receiver:                SplitStream<WebSocket>,
     sender:                  SplitSink<WebSocket, Message>,
     price_feeds_with_config: HashMap<PriceIdentifier, PriceFeedClientConfig>,
+    ping_interval_future:    Pin<Box<tokio::time::Sleep>>,
+    responded_to_ping:       bool,
 }
 
 impl Subscriber {
     pub fn new(
         id: SubscriberId,
-        store: Store,
-        update_rx: mpsc::Receiver<Vec<PriceIdentifier>>,
+        store: Arc<Store>,
+        notify_receiver: mpsc::Receiver<()>,
         receiver: SplitStream<WebSocket>,
         sender: SplitSink<WebSocket, Message>,
     ) -> Self {
@@ -94,17 +104,19 @@ impl Subscriber {
             id,
             closed: false,
             store,
-            update_rx,
+            notify_receiver,
             receiver,
             sender,
             price_feeds_with_config: HashMap::new(),
+            ping_interval_future: Box::pin(tokio::time::sleep(PING_INTERVAL_DURATION)),
+            responded_to_ping: true, // We start with true so we don't close the connection immediately
         }
     }
 
     pub async fn run(&mut self) {
         while !self.closed {
             if let Err(e) = self.handle_next().await {
-                log::error!("Subscriber {}: Error handling next message: {}", self.id, e);
+                log::warn!("Subscriber {}: Error handling next message: {}", self.id, e);
                 break;
             }
         }
@@ -112,11 +124,11 @@ impl Subscriber {
 
     async fn handle_next(&mut self) -> Result<()> {
         tokio::select! {
-            maybe_update_feed_ids = self.update_rx.recv() => {
-                let update_feed_ids = maybe_update_feed_ids.ok_or_else(|| {
-                    anyhow::anyhow!("Update channel closed.")
-                })?;
-                self.handle_price_feeds_update(update_feed_ids).await?;
+            maybe_update_feeds = self.notify_receiver.recv() => {
+                if maybe_update_feeds.is_none() {
+                    return Err(anyhow!("Update channel closed. This should never happen. Closing connection."));
+                };
+                self.handle_price_feeds_update().await?;
             },
             maybe_message_or_err = self.receiver.next() => {
                 match maybe_message_or_err {
@@ -128,40 +140,49 @@ impl Subscriber {
                     Some(message_or_err) => self.handle_client_message(message_or_err?).await?
                 }
             },
+            _  = &mut self.ping_interval_future => {
+                if !self.responded_to_ping {
+                    log::debug!("Subscriber {} did not respond to ping. Closing connection.", self.id);
+                    self.closed = true;
+                    return Ok(());
+                }
+                self.responded_to_ping = false;
+                self.sender.send(Message::Ping(vec![])).await?;
+                self.ping_interval_future = Box::pin(tokio::time::sleep(PING_INTERVAL_DURATION));
+            }
         }
 
         Ok(())
     }
 
-    async fn handle_price_feeds_update(
-        &mut self,
-        price_feed_ids: Vec<PriceIdentifier>,
-    ) -> Result<()> {
-        for price_feed_id in price_feed_ids {
-            if let Some(config) = self.price_feeds_with_config.get(&price_feed_id) {
-                let price_feeds_with_update_data = self.store.get_price_feeds_with_update_data(
-                    vec![price_feed_id],
-                    crate::store::RequestTime::Latest,
-                )?;
-                let price_info = price_feeds_with_update_data
-                    .batch_vaa
-                    .price_infos
-                    .get(&price_feed_id)
-                    .ok_or_else(|| {
-                        anyhow::anyhow!("Price feed {} not found.", price_feed_id.to_string())
-                    })?
-                    .clone();
-                let price_feed =
-                    RpcPriceFeed::from_price_info(price_info, config.verbose, config.binary);
-                // Feed does not flush the message and will allow us
-                // to send multiple messages in a single flush.
-                self.sender
-                    .feed(Message::Text(serde_json::to_string(
-                        &ServerMessage::PriceUpdate { price_feed },
-                    )?))
-                    .await?;
-            }
+    async fn handle_price_feeds_update(&mut self) -> Result<()> {
+        let price_feed_ids = self.price_feeds_with_config.keys().cloned().collect();
+        for update in self
+            .store
+            .get_price_feeds_with_update_data(price_feed_ids, RequestTime::Latest)
+            .await?
+            .price_feeds
+        {
+            let config = self
+                .price_feeds_with_config
+                .get(&PriceIdentifier::new(update.price_feed.feed_id))
+                .ok_or(anyhow::anyhow!(
+                    "Config missing, price feed list was poisoned during iteration."
+                ))?;
+
+            self.sender
+                .feed(Message::Text(serde_json::to_string(
+                    &ServerMessage::PriceUpdate {
+                        price_feed: RpcPriceFeed::from_price_feed_update(
+                            update,
+                            config.verbose,
+                            config.binary,
+                        ),
+                    },
+                )?))
+                .await?;
         }
+
         self.sender.flush().await?;
         Ok(())
     }
@@ -176,6 +197,14 @@ impl Subscriber {
         let maybe_client_message = match message {
             Message::Text(text) => serde_json::from_str::<ClientMessage>(&text),
             Message::Binary(data) => serde_json::from_slice::<ClientMessage>(&data),
+            Message::Ping(_) => {
+                // Axum will send Pong automatically
+                return Ok(());
+            }
+            Message::Pong(_) => {
+                self.responded_to_ping = true;
+                return Ok(());
+            }
             _ => {
                 return Ok(());
             }
@@ -184,7 +213,7 @@ impl Subscriber {
         match maybe_client_message {
             Err(e) => {
                 self.sender
-                    .feed(
+                    .send(
                         serde_json::to_string(&ServerMessage::Response(
                             ServerResponseMessage::Err {
                                 error: e.to_string(),
@@ -224,20 +253,17 @@ impl Subscriber {
     }
 }
 
-pub async fn dispatch_updates(update_feed_ids: Vec<PriceIdentifier>, state: super::State) {
-    let ws_state = state.ws.clone();
-    let update_feed_ids_ref = &update_feed_ids;
-
+pub async fn notify_updates(ws_state: Arc<WsState>) {
     let closed_subscribers: Vec<Option<SubscriberId>> = join_all(
         ws_state
             .subscribers
             .iter_mut()
             .map(|subscriber| async move {
-                match subscriber.send(update_feed_ids_ref.clone()).await {
+                match subscriber.send(()).await {
                     Ok(_) => None,
                     Err(e) => {
                         log::debug!("Error sending update to subscriber: {}", e);
-                        Some(subscriber.key().clone())
+                        Some(*subscriber.key())
                     }
                 }
             }),
@@ -260,7 +286,7 @@ pub struct PriceFeedClientConfig {
 
 pub struct WsState {
     pub subscriber_counter: AtomicUsize,
-    pub subscribers:        DashMap<SubscriberId, mpsc::Sender<Vec<PriceIdentifier>>>,
+    pub subscribers:        DashMap<SubscriberId, mpsc::Sender<()>>,
 }
 
 impl WsState {

+ 23 - 31
hermes/src/config.rs

@@ -1,9 +1,7 @@
 use {
     libp2p::Multiaddr,
-    std::{
-        net::SocketAddr,
-        path::PathBuf,
-    },
+    solana_sdk::pubkey::Pubkey,
+    std::net::SocketAddr,
     structopt::StructOpt,
 };
 
@@ -12,24 +10,30 @@ use {
 /// Some of these arguments are not currently used, but are included for future use to guide the
 /// structure of the application.
 #[derive(StructOpt, Debug)]
-#[structopt(name = "pythnet", about = "PythNet")]
+#[structopt(name = "hermes", about = "Hermes")]
 pub enum Options {
-    /// Run the PythNet P2P service.
     Run {
-        /// A Path to a protobuf encoded ed25519 private key.
-        #[structopt(short, long)]
-        id: Option<PathBuf>,
+        #[structopt(long, env = "PYTHNET_WS_ENDPOINT")]
+        pythnet_ws_endpoint: String,
 
-        /// A Path to a protobuf encoded secp256k1 private key.
-        #[structopt(long)]
-        id_secp256k1: Option<PathBuf>,
+        #[structopt(long, env = "PYTHNET_HTTP_ENDPOINT")]
+        pythnet_http_endpoint: String,
 
         /// Network ID for Wormhole
-        #[structopt(long, env = "WORMHOLE_NETWORK_ID")]
+        #[structopt(
+            long,
+            default_value = "/wormhole/mainnet/2",
+            env = "WORMHOLE_NETWORK_ID"
+        )]
         wh_network_id: String,
 
         /// Multiaddresses for Wormhole bootstrap peers (separated by comma).
-        #[structopt(long, use_delimiter = true, env = "WORMHOLE_BOOTSTRAP_ADDRS")]
+        #[structopt(
+            long,
+            use_delimiter = true,
+            default_value = "/dns4/wormhole-mainnet-v2-bootstrap.certus.one/udp/8999/quic/p2p/12D3KooWQp644DK27fd3d4Km3jr7gHiuJJ5ZGmy8hH4py7fP4FP7",
+            env = "WORMHOLE_BOOTSTRAP_ADDRS"
+        )]
         wh_bootstrap_addrs: Vec<Multiaddr>,
 
         /// Multiaddresses to bind Wormhole P2P to (separated by comma)
@@ -41,24 +45,12 @@ pub enum Options {
         )]
         wh_listen_addrs: Vec<Multiaddr>,
 
-        /// The address to bind the RPC server to.
+        /// The address to bind the API server to.
         #[structopt(long, default_value = "127.0.0.1:33999")]
-        rpc_addr: SocketAddr,
-
-        /// Multiaddress to bind Pyth P2P server to.
-        #[structopt(long, default_value = "/ip4/127.0.0.1/tcp/34000")]
-        p2p_addr: Multiaddr,
-
-        /// A bootstrapping peer to join the cluster.
-        #[allow(dead_code)]
-        #[structopt(long)]
-        p2p_peer: Vec<SocketAddr>,
-    },
+        api_addr: SocketAddr,
 
-    /// Generate a new keypair.
-    Keygen {
-        /// The path to write the generated key to.
-        #[structopt(short, long)]
-        output: PathBuf,
+        /// Address of the Wormhole contract on the target PythNet cluster.
+        #[structopt(long, default_value = "H3fxXJ86ADW2PNuDDmZJg6mzTtPxkYCpNuQUTgmJ7AjU")]
+        wh_contract_addr: Pubkey,
     },
 }

+ 29 - 82
hermes/src/main.rs

@@ -1,88 +1,63 @@
 #![feature(never_type)]
+#![feature(slice_group_by)]
 
 use {
     crate::store::Store,
     anyhow::Result,
-    futures::{
-        channel::mpsc::Receiver,
-        SinkExt,
-    },
-    std::time::Duration,
     structopt::StructOpt,
-    tokio::{
-        spawn,
-        time::sleep,
-    },
 };
 
+mod api;
 mod config;
 mod macros;
 mod network;
 mod store;
 
-/// A Wormhole VAA is an array of bytes. TODO: Decoding.
-#[derive(Debug, Clone, Eq, Hash, PartialEq, serde::Serialize, serde::Deserialize)]
-pub struct Vaa {
-    pub data: Vec<u8>,
-}
-
-/// A PythNet AccountUpdate is a 32-byte address and a variable length data field.
-///
-/// This type is emitted by the Geyser plugin when an observed account is updated and is forwrarded
-/// to this process via IPC.
-#[derive(Debug, Clone, Eq, Hash, PartialEq, serde::Serialize, serde::Deserialize)]
-pub struct AccountUpdate {
-    addr: [u8; 32],
-    data: Vec<u8>,
-}
-
-/// Handler for LibP2P messages. Currently these consist only of Wormhole Observations.
-fn handle_message(_observation: network::p2p::Observation) -> Result<()> {
-    println!("Rust: Received Observation");
-    Ok(())
-}
-
 /// Initialize the Application. This can be invoked either by real main, or by the Geyser plugin.
-async fn init(_update_channel: Receiver<AccountUpdate>) -> Result<()> {
-    log::info!("Initializing PythNet...");
+async fn init() -> Result<()> {
+    log::info!("Initializing Hermes...");
 
     // Parse the command line arguments with StructOpt, will exit automatically on `--help` or
     // with invalid arguments.
     match config::Options::from_args() {
         config::Options::Run {
-            id: _,
-            id_secp256k1: _,
+            pythnet_ws_endpoint,
+            pythnet_http_endpoint,
             wh_network_id,
             wh_bootstrap_addrs,
             wh_listen_addrs,
-            rpc_addr,
-            p2p_addr,
-            p2p_peer: _,
+            wh_contract_addr,
+            api_addr,
         } => {
-            log::info!("Starting PythNet...");
+            // A channel to emit state updates to api
+            let (update_tx, update_rx) = tokio::sync::mpsc::channel(1000);
+
+            log::info!("Running Hermes...");
+            let store = Store::new_with_local_cache(update_tx, 1000);
 
             // Spawn the P2P layer.
-            log::info!("Starting P2P server on {}", p2p_addr);
+            log::info!("Starting P2P server on {:?}", wh_listen_addrs);
             network::p2p::spawn(
-                handle_message,
+                store.clone(),
                 wh_network_id.to_string(),
                 wh_bootstrap_addrs,
                 wh_listen_addrs,
             )
             .await?;
 
-            // Spawn the RPC server.
-            log::info!("Starting RPC server on {}", rpc_addr);
-
-            // TODO: Add max size to the config
-            network::rpc::spawn(rpc_addr.to_string(), Store::new_with_local_cache(1000)).await?;
-
-            // Wait on Ctrl+C similar to main.
-            tokio::signal::ctrl_c().await?;
-        }
+            // Spawn the Pythnet listener
+            log::info!("Starting Pythnet listener using {}", pythnet_ws_endpoint);
+            network::pythnet::spawn(
+                store.clone(),
+                pythnet_ws_endpoint,
+                pythnet_http_endpoint,
+                wh_contract_addr,
+            )
+            .await?;
 
-        config::Options::Keygen { output: _ } => {
-            println!("Currently not implemented.");
+            // Run the RPC server and wait for it to shutdown gracefully.
+            log::info!("Starting RPC server on {}", api_addr);
+            api::run(store.clone(), update_rx, api_addr.to_string()).await?;
         }
     }
 
@@ -93,43 +68,15 @@ async fn init(_update_channel: Receiver<AccountUpdate>) -> Result<()> {
 async fn main() -> Result<!> {
     env_logger::init();
 
-    // Generate a stream of fake AccountUpdates when run in binary mode. This is temporary until
-    // the Geyser component of the accumulator work is complete.
-    let (mut tx, rx) = futures::channel::mpsc::channel(1);
-
-    spawn(async move {
-        let mut data = 0u32;
-
-        loop {
-            // Simulate PythNet block time.
-            sleep(Duration::from_millis(200)).await;
-
-            // Ignore the return type of `send`, since we don't care if the receiver is closed.
-            // It's better to let the process continue to run as this is just a temporary hack.
-            let _ = SinkExt::send(
-                &mut tx,
-                AccountUpdate {
-                    addr: [0; 32],
-                    data: {
-                        data += 1;
-                        let mut data = data.to_be_bytes().to_vec();
-                        data.resize(32, 0);
-                        data
-                    },
-                },
-            )
-            .await;
-        }
-    });
-
     tokio::spawn(async move {
         // Launch the application. If it fails, print the full backtrace and exit. RUST_BACKTRACE
         // should be set to 1 for this otherwise it will only print the top-level error.
-        if let Err(result) = init(rx).await {
+        if let Err(result) = init().await {
             eprintln!("{}", result.backtrace());
             for cause in result.chain() {
                 eprintln!("{cause}");
             }
+            std::process::exit(1);
         }
     });
 

+ 1 - 1
hermes/src/network.rs

@@ -1,2 +1,2 @@
 pub mod p2p;
-pub mod rpc;
+pub mod pythnet;

+ 72 - 2
hermes/src/network/p2p.go

@@ -28,7 +28,12 @@ import "C"
 import (
 	"context"
 	"fmt"
+	"os"
 	"strings"
+	"time"
+
+	"net/http"
+	_ "net/http/pprof"
 
 	"github.com/libp2p/go-libp2p"
 	"github.com/libp2p/go-libp2p/core/crypto"
@@ -44,6 +49,7 @@ import (
 	pubsub "github.com/libp2p/go-libp2p-pubsub"
 	libp2ptls "github.com/libp2p/go-libp2p/p2p/security/tls"
 	libp2pquic "github.com/libp2p/go-libp2p/p2p/transport/quic"
+	libp2pquicreuse "github.com/libp2p/go-libp2p/p2p/transport/quicreuse"
 )
 
 //export RegisterObservationCallback
@@ -52,11 +58,29 @@ func RegisterObservationCallback(f C.callback_t, network_id, bootstrap_addrs, li
 	bootstrapAddrs := strings.Split(C.GoString(bootstrap_addrs), ",")
 	listenAddrs := strings.Split(C.GoString(listen_addrs), ",")
 
+	// Bind pprof to 6060 for debugging Go code.
 	go func() {
+		http.ListenAndServe("127.0.0.1:6060", nil)
+	}()
+
+	var startTime int64
+	var recoverRerun func()
+
+	routine := func() {
+		defer recoverRerun()
+
+		// Record the current time
+		startTime = time.Now().UnixNano()
+
 		ctx := context.Background()
 
 		// Setup base network configuration.
 		priv, _, err := crypto.GenerateKeyPair(crypto.Ed25519, -1)
+		if err != nil {
+			err := fmt.Errorf("Failed to generate key pair: %w", err)
+			fmt.Println(err)
+			return
+		}
 
 		// Setup libp2p Connection Manager.
 		mgr, err := connmgr.NewConnManager(
@@ -76,6 +100,28 @@ func RegisterObservationCallback(f C.callback_t, network_id, bootstrap_addrs, li
 			libp2p.Identity(priv),
 			libp2p.ListenAddrStrings(listenAddrs...),
 			libp2p.Security(libp2ptls.ID, libp2ptls.New),
+			// Disable Reuse because upon panic, the Close() call on the p2p reactor does not properly clean up the
+			// open ports (they are kept around for re-use, this seems to be a libp2p bug in the reuse `gc()` call
+			// which can be found here:
+			//
+			// https://github.com/libp2p/go-libp2p/blob/master/p2p/transport/quicreuse/reuse.go#L97
+			//
+			// By disabling this we get correct Close() behaviour.
+			//
+			// IMPORTANT: Normally re-use allows libp2p to dial on the same port that is used to listen for traffic
+			// and by disabling this dialing uses a random high port (32768-60999) which causes the nodes that we
+			// connect to by dialing (instead of them connecting to us) will respond on the high range port instead
+			// of the specified Dial port. This requires firewalls to be configured to allow (UDP 32768-60999) which
+			// should be specified in our documentation.
+			//
+			// The best way to securely enable this range is via the conntrack module, which can statefully allow
+			// UDP packets only when a sent UDP packet is present in the conntrack table. This rule looks roughly
+			// like this:
+			//
+			// iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+			//
+			// Which is a standard rule in many firewall configurations (RELATED is the key flag).
+			libp2p.QUICReuse(libp2pquicreuse.NewConnManager, libp2pquicreuse.DisableReuseport()),
 			libp2p.Transport(libp2pquic.NewTransport),
 			libp2p.ConnectionManager(mgr),
 			libp2p.Routing(func(h host.Host) (routing.PeerRouting, error) {
@@ -107,6 +153,8 @@ func RegisterObservationCallback(f C.callback_t, network_id, bootstrap_addrs, li
 			return
 		}
 
+		defer h.Close()
+
 		topic := fmt.Sprintf("%s/%s", networkID, "broadcast")
 		ps, err := pubsub.NewGossipSub(ctx, h)
 		if err != nil {
@@ -122,6 +170,8 @@ func RegisterObservationCallback(f C.callback_t, network_id, bootstrap_addrs, li
 			return
 		}
 
+		defer th.Close()
+
 		sub, err := th.Subscribe()
 		if err != nil {
 			err := fmt.Errorf("Failed to subscribe topic: %w", err)
@@ -129,6 +179,8 @@ func RegisterObservationCallback(f C.callback_t, network_id, bootstrap_addrs, li
 			return
 		}
 
+		defer sub.Cancel()
+
 		for {
 			for {
 				select {
@@ -151,16 +203,34 @@ func RegisterObservationCallback(f C.callback_t, network_id, bootstrap_addrs, li
 					case *GossipMessage_SignedVaaWithQuorum:
 						vaaBytes := msg.GetSignedVaaWithQuorum().GetVaa()
 						cBytes := C.CBytes(vaaBytes)
-						defer C.free(cBytes)
 						C.invoke(f, C.observation_t{
 							vaa:     (*C.char)(cBytes),
 							vaa_len: C.size_t(len(vaaBytes)),
 						})
+						C.free(cBytes)
 					}
 				}
 			}
 		}
-	}()
+	}
+
+	recoverRerun = func() {
+		// Print the error if any and recall routine
+		if err := recover(); err != nil {
+			fmt.Fprintf(os.Stderr, "p2p.go error: %v\n", err)
+		}
+
+		// Sleep for 1 second if the time elapsed is less than 30 seconds
+		// to avoid spamming the network with requests.
+		elapsed := time.Duration(time.Now().UnixNano() - startTime)
+		if elapsed < 30*time.Second {
+			time.Sleep(1 * time.Second)
+		}
+
+		go routine()
+	}
+
+	go routine()
 }
 
 func main() {

+ 56 - 19
hermes/src/network/p2p.rs

@@ -10,6 +10,10 @@
 //! their infrastructure.
 
 use {
+    crate::store::{
+        types::Update,
+        Store,
+    },
     anyhow::Result,
     libp2p::Multiaddr,
     std::{
@@ -22,6 +26,7 @@ use {
                 Receiver,
                 Sender,
             },
+            Arc,
             Mutex,
         },
     },
@@ -66,7 +71,14 @@ lazy_static::lazy_static! {
 extern "C" fn proxy(o: ObservationC) {
     // Create a fixed slice from the pointer and length.
     let vaa = unsafe { std::slice::from_raw_parts(o.vaa, o.vaa_len) }.to_owned();
-    if let Err(e) = OBSERVATIONS.0.lock().unwrap().send(vaa) {
+    // The chances of the mutex getting poisioned is very low and if it happens
+    // there is no way for us to recover from it.
+    if let Err(e) = OBSERVATIONS
+        .0
+        .lock()
+        .expect("Cannot acquire p2p channel lock")
+        .send(vaa)
+    {
         log::error!("Failed to send observation: {}", e);
     }
 }
@@ -76,15 +88,11 @@ extern "C" fn proxy(o: ObservationC) {
 /// TODO: handle_message should be capable of handling more than just Observations. But we don't
 /// have our own P2P network, we pass it in to keep the code structure and read directly from the
 /// OBSERVATIONS channel in the RPC for now.
-pub fn bootstrap<H>(
-    _handle_message: H,
+pub fn bootstrap(
     network_id: String,
     wh_bootstrap_addrs: Vec<Multiaddr>,
     wh_listen_addrs: Vec<Multiaddr>,
-) -> Result<()>
-where
-    H: Fn(Observation) -> Result<()> + 'static,
-{
+) -> Result<()> {
     let network_id_cstr = CString::new(network_id)?;
     let wh_bootstrap_addrs_cstr = CString::new(
         wh_bootstrap_addrs
@@ -114,20 +122,49 @@ where
 }
 
 // Spawn's the P2P layer as a separate thread via Go.
-pub async fn spawn<H>(
-    handle_message: H,
+pub async fn spawn(
+    store: Arc<Store>,
     network_id: String,
     wh_bootstrap_addrs: Vec<Multiaddr>,
     wh_listen_addrs: Vec<Multiaddr>,
-) -> Result<()>
-where
-    H: Fn(Observation) -> Result<()> + Send + 'static,
-{
-    bootstrap(
-        handle_message,
-        network_id,
-        wh_bootstrap_addrs,
-        wh_listen_addrs,
-    )?;
+) -> Result<()> {
+    std::thread::spawn(|| bootstrap(network_id, wh_bootstrap_addrs, wh_listen_addrs).unwrap());
+
+    tokio::spawn(async move {
+        // Listen in the background for new VAA's from the p2p layer
+        // and update the state accordingly.
+        loop {
+            let vaa_bytes = tokio::task::spawn_blocking(|| {
+                let observation = OBSERVATIONS.1.lock();
+                let observation = match observation {
+                    Ok(observation) => observation,
+                    Err(e) => {
+                        // This should never happen, but if it does, we want to panic and crash
+                        // as it is not recoverable.
+                        panic!("Failed to lock p2p observation channel: {e}");
+                    }
+                };
+
+                match observation.recv() {
+                    Ok(vaa_bytes) => vaa_bytes,
+                    Err(e) => {
+                        // This should never happen, but if it does, we want to panic and crash
+                        // as it is not recoverable.
+                        panic!("Failed to receive p2p observation: {e}");
+                    }
+                }
+            })
+            .await
+            .unwrap();
+
+            let store = store.clone();
+            tokio::spawn(async move {
+                if let Err(e) = store.store_update(Update::Vaa(vaa_bytes)).await {
+                    log::error!("Failed to process VAA: {:?}", e);
+                }
+            });
+        }
+    });
+
     Ok(())
 }

+ 310 - 0
hermes/src/network/pythnet.rs

@@ -0,0 +1,310 @@
+//! This module connects to the Pythnet RPC server and listens for accumulator
+//! updates. It then sends the updates to the store module for processing and
+//! storage.
+
+use {
+    crate::store::{
+        types::{
+            AccumulatorMessages,
+            Update,
+        },
+        wormhole::{
+            BridgeData,
+            GuardianSet,
+            GuardianSetData,
+        },
+        Store,
+    },
+    anyhow::{
+        anyhow,
+        Result,
+    },
+    borsh::BorshDeserialize,
+    futures::stream::StreamExt,
+    solana_account_decoder::UiAccountEncoding,
+    solana_client::{
+        nonblocking::{
+            pubsub_client::PubsubClient,
+            rpc_client::RpcClient,
+        },
+        rpc_config::{
+            RpcAccountInfoConfig,
+            RpcProgramAccountsConfig,
+        },
+        rpc_filter::{
+            Memcmp,
+            MemcmpEncodedBytes,
+            RpcFilterType,
+        },
+    },
+    solana_sdk::{
+        account::Account,
+        commitment_config::CommitmentConfig,
+        pubkey::Pubkey,
+        system_program,
+    },
+    std::{
+        sync::Arc,
+        time::Duration,
+    },
+    tokio::time::Instant,
+};
+
+/// Using a Solana RPC endpoint, fetches the target GuardianSet based on an index.
+async fn fetch_guardian_set(
+    client: &RpcClient,
+    wormhole_contract_addr: Pubkey,
+    guardian_set_index: u32,
+) -> Result<GuardianSet> {
+    // Fetch GuardianSet account from Solana RPC.
+    let guardian_set = client
+        .get_account_with_commitment(
+            &Pubkey::find_program_address(
+                &[b"GuardianSet", &guardian_set_index.to_be_bytes()],
+                &wormhole_contract_addr,
+            )
+            .0,
+            CommitmentConfig::confirmed(),
+        )
+        .await;
+
+    let guardian_set = match guardian_set {
+        Ok(response) => match response.value {
+            Some(guardian_set) => guardian_set,
+            None => return Err(anyhow!("GuardianSet account not found")),
+        },
+        Err(err) => return Err(anyhow!("Failed to fetch GuardianSet account: {}", err)),
+    };
+
+    // Deserialize the result into a GuardianSet, this is where we can
+    // extract the new Signer set.
+    match GuardianSetData::deserialize(&mut guardian_set.data.as_ref()) {
+        Ok(guardian_set) => Ok(GuardianSet {
+            keys: guardian_set.keys,
+        }),
+
+        Err(err) => Err(anyhow!(
+            "Failed to deserialize GuardianSet account: {}",
+            err
+        )),
+    }
+}
+
+/// Using a Solana RPC endpoint, fetches the target Bridge state.
+///
+/// You can use this function to get access to metadata about the Wormhole state by reading the
+/// Bridge account. We currently use this to find the active guardian set index.
+async fn fetch_bridge_data(
+    client: &RpcClient,
+    wormhole_contract_addr: &Pubkey,
+) -> Result<BridgeData> {
+    // Fetch Bridge account from Solana RPC.
+    let bridge = client
+        .get_account_with_commitment(
+            &Pubkey::find_program_address(&[b"Bridge"], wormhole_contract_addr).0,
+            CommitmentConfig::confirmed(),
+        )
+        .await;
+
+    let bridge = match bridge {
+        Ok(response) => match response.value {
+            Some(bridge) => bridge,
+            None => return Err(anyhow!("Bridge account not found")),
+        },
+        Err(err) => return Err(anyhow!("Failed to fetch Bridge account: {}", err)),
+    };
+
+    // Deserialize the result into a BridgeData, this is where we can
+    // extract the new Signer set.
+    match BridgeData::deserialize(&mut bridge.data.as_ref()) {
+        Ok(bridge) => Ok(bridge),
+        Err(err) => Err(anyhow!("Failed to deserialize Bridge account: {}", err)),
+    }
+}
+
+pub async fn run(store: Arc<Store>, pythnet_ws_endpoint: String) -> Result<!> {
+    let client = PubsubClient::new(pythnet_ws_endpoint.as_ref()).await?;
+
+    let config = RpcProgramAccountsConfig {
+        account_config: RpcAccountInfoConfig {
+            commitment: Some(CommitmentConfig::confirmed()),
+            encoding: Some(UiAccountEncoding::Base64Zstd),
+            ..Default::default()
+        },
+        filters:        Some(vec![RpcFilterType::Memcmp(Memcmp {
+            offset:   0,
+            bytes:    MemcmpEncodedBytes::Bytes(b"PAS1".to_vec()),
+            encoding: None,
+        })]),
+        with_context:   Some(true),
+    };
+
+    // Listen for all PythNet accounts, we will filter down to the Accumulator related accounts.
+    let (mut notif, _unsub) = client
+        .program_subscribe(&system_program::id(), Some(config))
+        .await?;
+
+    loop {
+        match notif.next().await {
+            Some(update) => {
+                let account: Account = match update.value.account.decode() {
+                    Some(account) => account,
+                    None => {
+                        log::error!("Failed to decode account from update: {:?}", update);
+                        continue;
+                    }
+                };
+
+                let accumulator_messages = AccumulatorMessages::try_from_slice(&account.data);
+                match accumulator_messages {
+                    Ok(accumulator_messages) => {
+                        let (candidate, _) = Pubkey::find_program_address(
+                            &[
+                                b"AccumulatorState",
+                                &accumulator_messages.ring_index().to_be_bytes(),
+                            ],
+                            &system_program::id(),
+                        );
+
+                        if candidate.to_string() == update.value.pubkey {
+                            let store = store.clone();
+                            tokio::spawn(async move {
+                                if let Err(err) = store
+                                    .store_update(Update::AccumulatorMessages(accumulator_messages))
+                                    .await
+                                {
+                                    log::error!("Failed to store accumulator messages: {:?}", err);
+                                }
+                            });
+                        } else {
+                            log::error!(
+                                "Failed to verify the messages public key: {:?} != {:?}",
+                                candidate,
+                                update.value.pubkey
+                            );
+                        }
+                    }
+
+                    Err(err) => {
+                        log::error!("Failed to parse AccumulatorMessages: {:?}", err);
+                    }
+                };
+            }
+            None => {
+                return Err(anyhow!("Pythnet network listener terminated"));
+            }
+        }
+    }
+}
+
+/// Fetch existing GuardianSet accounts from Wormhole.
+///
+/// This method performs the necessary work to pull down the bridge state and associated guardian
+/// sets from a deployed Wormhole contract. Note that we only fetch the last two accounts due to
+/// the fact that during a Wormhole upgrade, there will only be messages produces from those two.
+async fn fetch_existing_guardian_sets(
+    store: Arc<Store>,
+    pythnet_http_endpoint: String,
+    wormhole_contract_addr: Pubkey,
+) -> Result<()> {
+    let client = RpcClient::new(pythnet_http_endpoint.to_string());
+    let bridge = fetch_bridge_data(&client, &wormhole_contract_addr).await?;
+
+    // Fetch the current GuardianSet we know is valid for signing.
+    let current =
+        fetch_guardian_set(&client, wormhole_contract_addr, bridge.guardian_set_index).await?;
+
+    log::info!(
+        "Retrieved Current GuardianSet ({}): {}",
+        bridge.guardian_set_index,
+        current
+    );
+
+    store
+        .update_guardian_set(bridge.guardian_set_index, current)
+        .await;
+
+    // If there are more than one guardian set, we want to fetch the previous one as well as it
+    // may still be in transition phase if a guardian upgrade has just occurred.
+    if bridge.guardian_set_index >= 1 {
+        let previous = fetch_guardian_set(
+            &client,
+            wormhole_contract_addr,
+            bridge.guardian_set_index - 1,
+        )
+        .await?;
+
+        log::info!(
+            "Retrieved Previous GuardianSet ({}): {}",
+            bridge.guardian_set_index - 1,
+            previous
+        );
+
+        store
+            .update_guardian_set(bridge.guardian_set_index - 1, previous)
+            .await;
+    }
+
+    Ok(())
+}
+
+
+pub async fn spawn(
+    store: Arc<Store>,
+    pythnet_ws_endpoint: String,
+    pythnet_http_endpoint: String,
+    wormhole_contract_addr: Pubkey,
+) -> Result<()> {
+    fetch_existing_guardian_sets(
+        store.clone(),
+        pythnet_http_endpoint.clone(),
+        wormhole_contract_addr,
+    )
+    .await?;
+
+    {
+        let store = store.clone();
+        let pythnet_ws_endpoint = pythnet_ws_endpoint.clone();
+        tokio::spawn(async move {
+            loop {
+                let current_time = Instant::now();
+
+                if let Err(ref e) = run(store.clone(), pythnet_ws_endpoint.clone()).await {
+                    log::error!("Error in Pythnet network listener: {:?}", e);
+                }
+
+                if current_time.elapsed() < Duration::from_secs(30) {
+                    log::error!(
+                        "Pythnet network listener is restarting too quickly. Sleeping for 1s"
+                    );
+                    tokio::time::sleep(Duration::from_secs(1)).await;
+                }
+            }
+        });
+    }
+
+    {
+        let store = store.clone();
+        let pythnet_http_endpoint = pythnet_http_endpoint.clone();
+        tokio::spawn(async move {
+            loop {
+                tokio::time::sleep(Duration::from_secs(60)).await;
+
+                match fetch_existing_guardian_sets(
+                    store.clone(),
+                    pythnet_http_endpoint.clone(),
+                    wormhole_contract_addr,
+                )
+                .await
+                {
+                    Ok(_) => {}
+                    Err(err) => {
+                        log::error!("Failed to poll for new guardian sets: {:?}", err);
+                    }
+                }
+            }
+        });
+    }
+
+    Ok(())
+}

+ 262 - 43
hermes/src/store.rs

@@ -1,73 +1,292 @@
 use {
     self::{
-        proof::batch_vaa::PriceInfosWithUpdateData,
-        storage::Storage,
+        proof::wormhole_merkle::construct_update_data,
+        storage::{
+            MessageState,
+            MessageStateFilter,
+            StorageInstance,
+        },
+        types::{
+            PriceFeedUpdate,
+            PriceFeedsWithUpdateData,
+            RequestTime,
+            Update,
+        },
+        wormhole::GuardianSet,
     },
-    anyhow::Result,
+    crate::store::{
+        proof::wormhole_merkle::{
+            construct_message_states_proofs,
+            store_wormhole_merkle_verified_message,
+        },
+        storage::CompletedAccumulatorState,
+        types::{
+            ProofSet,
+            UnixTimestamp,
+        },
+        wormhole::verify_vaa,
+    },
+    anyhow::{
+        anyhow,
+        Result,
+    },
+    byteorder::BigEndian,
     pyth_sdk::PriceIdentifier,
-    std::sync::Arc,
+    pythnet_sdk::{
+        messages::{
+            Message,
+            MessageType,
+        },
+        wire::{
+            from_slice,
+            v1::{
+                WormholeMessage,
+                WormholePayload,
+            },
+        },
+    },
+    std::{
+        collections::{
+            BTreeMap,
+            BTreeSet,
+            HashSet,
+        },
+        sync::Arc,
+        time::{
+            SystemTime,
+            UNIX_EPOCH,
+        },
+    },
+    tokio::{
+        sync::{
+            mpsc::Sender,
+            RwLock,
+        },
+        time::{
+            Duration,
+            Instant,
+        },
+    },
+    wormhole_sdk::{
+        Address,
+        Chain,
+        Vaa,
+    },
 };
 
 pub mod proof;
 pub mod storage;
+pub mod types;
+pub mod wormhole;
 
-pub type UnixTimestamp = u64;
-
-#[derive(Clone, PartialEq, Eq, Debug)]
-pub enum RequestTime {
-    Latest,
-    FirstAfter(UnixTimestamp),
-}
-
-pub enum Update {
-    Vaa(Vec<u8>),
-}
-
-pub struct PriceFeedsWithUpdateData {
-    pub batch_vaa: PriceInfosWithUpdateData,
-}
-
-pub type State = Arc<Box<dyn Storage>>;
+const OBSERVED_CACHE_SIZE: usize = 1000;
 
-#[derive(Clone)]
 pub struct Store {
-    pub state: State,
+    pub storage:                  StorageInstance,
+    pub observed_vaa_seqs:        RwLock<BTreeSet<u64>>,
+    pub guardian_set:             RwLock<BTreeMap<u32, GuardianSet>>,
+    pub update_tx:                Sender<()>,
+    pub last_completed_update_at: RwLock<Option<Instant>>,
 }
 
 impl Store {
-    pub fn new_with_local_cache(max_size_per_key: usize) -> Self {
-        Self {
-            state: Arc::new(Box::new(storage::local_cache::LocalCache::new(
-                max_size_per_key,
-            ))),
-        }
+    pub fn new_with_local_cache(update_tx: Sender<()>, cache_size: u64) -> Arc<Self> {
+        Arc::new(Self {
+            storage: storage::local_storage::LocalStorage::new_instance(cache_size),
+            observed_vaa_seqs: RwLock::new(Default::default()),
+            guardian_set: RwLock::new(Default::default()),
+            update_tx,
+            last_completed_update_at: RwLock::new(None),
+        })
     }
 
-    /// Stores the update data in the store and returns the price identifiers for which
-    /// price feeds were updated.
-    pub fn store_update(&self, update: Update) -> Result<Vec<PriceIdentifier>> {
-        match update {
+    /// Stores the update data in the store
+    pub async fn store_update(&self, update: Update) -> Result<()> {
+        let slot = match update {
             Update::Vaa(vaa_bytes) => {
-                proof::batch_vaa::store_vaa_update(self.state.clone(), vaa_bytes)
+                let vaa =
+                    serde_wormhole::from_slice::<Vaa<&serde_wormhole::RawMessage>>(&vaa_bytes)?;
+
+                if vaa.emitter_chain != Chain::Pythnet
+                    || vaa.emitter_address != Address(pythnet_sdk::ACCUMULATOR_EMITTER_ADDRESS)
+                {
+                    return Ok(()); // Ignore VAA from other emitters
+                }
+
+                if self.observed_vaa_seqs.read().await.contains(&vaa.sequence) {
+                    return Ok(()); // Ignore VAA if we have already seen it
+                }
+
+                let vaa = verify_vaa(self, vaa).await;
+
+                let vaa = match vaa {
+                    Ok(vaa) => vaa,
+                    Err(err) => {
+                        log::info!("Ignoring invalid VAA: {:?}", err);
+                        return Ok(());
+                    }
+                };
+
+                {
+                    let mut observed_vaa_seqs = self.observed_vaa_seqs.write().await;
+                    observed_vaa_seqs.insert(vaa.sequence);
+                    while observed_vaa_seqs.len() > OBSERVED_CACHE_SIZE {
+                        observed_vaa_seqs.pop_first();
+                    }
+                }
+
+                match WormholeMessage::try_from_bytes(vaa.payload)?.payload {
+                    WormholePayload::Merkle(proof) => {
+                        log::info!("Storing merkle proof for slot {:?}", proof.slot,);
+                        store_wormhole_merkle_verified_message(self, proof.clone(), vaa_bytes)
+                            .await?;
+                        proof.slot
+                    }
+                }
             }
-        }
+            Update::AccumulatorMessages(accumulator_messages) => {
+                let slot = accumulator_messages.slot;
+                log::info!("Storing accumulator messages for slot {:?}.", slot,);
+                self.storage
+                    .update_accumulator_state(
+                        slot,
+                        Box::new(|mut state| {
+                            state.accumulator_messages = Some(accumulator_messages);
+                            state
+                        }),
+                    )
+                    .await?;
+                slot
+            }
+        };
+
+        let state = match self.storage.fetch_accumulator_state(slot).await? {
+            Some(state) => state,
+            None => return Ok(()),
+        };
+
+        let completed_state = state.try_into();
+        let completed_state: CompletedAccumulatorState = match completed_state {
+            Ok(completed_state) => completed_state,
+            Err(_) => {
+                return Ok(());
+            }
+        };
+
+        // Once the accumulator reaches a complete state for a specific slot
+        // we can build the message states
+        self.build_message_states(completed_state).await?;
+
+        self.update_tx.send(()).await?;
+
+        self.last_completed_update_at
+            .write()
+            .await
+            .replace(Instant::now());
+
+        Ok(())
+    }
+
+    async fn build_message_states(&self, completed_state: CompletedAccumulatorState) -> Result<()> {
+        let wormhole_merkle_message_states_proofs =
+            construct_message_states_proofs(&completed_state)?;
+
+        let current_time: UnixTimestamp =
+            SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as _;
+
+        let message_states = completed_state
+            .accumulator_messages
+            .raw_messages
+            .into_iter()
+            .enumerate()
+            .map(|(idx, raw_message)| {
+                Ok(MessageState::new(
+                    from_slice::<BigEndian, _>(raw_message.as_ref())
+                        .map_err(|e| anyhow!("Failed to deserialize message: {:?}", e))?,
+                    raw_message,
+                    ProofSet {
+                        wormhole_merkle_proof: wormhole_merkle_message_states_proofs
+                            .get(idx)
+                            .ok_or(anyhow!("Missing proof for message"))?
+                            .clone(),
+                    },
+                    completed_state.slot,
+                    current_time,
+                ))
+            })
+            .collect::<Result<Vec<_>>>()?;
+
+        log::info!("Message states len: {:?}", message_states.len());
+
+        self.storage.store_message_states(message_states).await?;
+
+        Ok(())
     }
 
-    pub fn get_price_feeds_with_update_data(
+    pub async fn update_guardian_set(&self, id: u32, guardian_set: GuardianSet) {
+        let mut guardian_sets = self.guardian_set.write().await;
+        guardian_sets.insert(id, guardian_set);
+    }
+
+    pub async fn get_price_feeds_with_update_data(
         &self,
         price_ids: Vec<PriceIdentifier>,
         request_time: RequestTime,
     ) -> Result<PriceFeedsWithUpdateData> {
-        Ok(PriceFeedsWithUpdateData {
-            batch_vaa: proof::batch_vaa::get_price_infos_with_update_data(
-                self.state.clone(),
-                price_ids,
+        let messages = self
+            .storage
+            .fetch_message_states(
+                price_ids
+                    .iter()
+                    .map(|price_id| price_id.to_bytes())
+                    .collect(),
                 request_time,
-            )?,
+                MessageStateFilter::Only(MessageType::PriceFeedMessage),
+            )
+            .await?;
+
+        let price_feeds = messages
+            .iter()
+            .map(|message_state| match message_state.message {
+                Message::PriceFeedMessage(price_feed) => Ok(PriceFeedUpdate {
+                    price_feed,
+                    received_at: message_state.received_at,
+                    slot: message_state.slot,
+                    wormhole_merkle_update_data: construct_update_data(vec![message_state])?
+                        .into_iter()
+                        .next()
+                        .ok_or(anyhow!("Missing update data for message"))?,
+                }),
+                _ => Err(anyhow!("Invalid message state type")),
+            })
+            .collect::<Result<Vec<_>>>()?;
+
+        let update_data = construct_update_data(messages.iter().collect())?;
+
+        Ok(PriceFeedsWithUpdateData {
+            price_feeds,
+            wormhole_merkle_update_data: update_data,
         })
     }
 
-    pub fn get_price_feed_ids(&self) -> Vec<PriceIdentifier> {
-        proof::batch_vaa::get_price_feed_ids(self.state.clone())
+    pub async fn get_price_feed_ids(&self) -> HashSet<PriceIdentifier> {
+        self.storage
+            .message_state_keys()
+            .await
+            .iter()
+            .map(|key| PriceIdentifier::new(key.feed_id))
+            .collect()
+    }
+
+    pub async fn is_ready(&self) -> bool {
+        const STALENESS_THRESHOLD: Duration = Duration::from_secs(30);
+
+        let last_completed_update_at = self.last_completed_update_at.read().await;
+        match last_completed_update_at.as_ref() {
+            Some(last_completed_update_at) => {
+                last_completed_update_at.elapsed() < STALENESS_THRESHOLD
+            }
+            None => false,
+        }
     }
 }

+ 1 - 1
hermes/src/store/proof.rs

@@ -1 +1 @@
-pub mod batch_vaa;
+pub mod wormhole_merkle;

+ 0 - 175
hermes/src/store/proof/batch_vaa.rs

@@ -1,175 +0,0 @@
-use {
-    crate::store::{
-        storage::{
-            Key,
-            StorageData,
-        },
-        RequestTime,
-        State,
-        UnixTimestamp,
-    },
-    anyhow::{
-        anyhow,
-        Result,
-    },
-    pyth_sdk::{
-        Price,
-        PriceFeed,
-        PriceIdentifier,
-    },
-    pyth_wormhole_attester_sdk::{
-        BatchPriceAttestation,
-        PriceAttestation,
-        PriceStatus,
-    },
-    std::{
-        collections::{
-            HashMap,
-            HashSet,
-        },
-        time::{
-            SystemTime,
-            UNIX_EPOCH,
-        },
-    },
-    wormhole::VAA,
-};
-
-// TODO: We need to add more metadata to this struct.
-#[derive(Clone, Default, PartialEq, Debug)]
-pub struct PriceInfo {
-    pub price_feed:       PriceFeed,
-    pub vaa_bytes:        Vec<u8>,
-    pub publish_time:     UnixTimestamp,
-    pub emitter_chain:    u16,
-    pub attestation_time: UnixTimestamp,
-    pub receive_time:     UnixTimestamp,
-    pub sequence_number:  u64,
-}
-
-#[derive(Clone, Default)]
-pub struct PriceInfosWithUpdateData {
-    pub price_infos: HashMap<PriceIdentifier, PriceInfo>,
-    pub update_data: Vec<Vec<u8>>,
-}
-
-pub fn store_vaa_update(state: State, vaa_bytes: Vec<u8>) -> Result<Vec<PriceIdentifier>> {
-    // FIXME: Vaa bytes might not be a valid Pyth BatchUpdate message nor originate from Our emitter.
-    // We should check that.
-    // FIXME: We receive multiple vaas for the same update (due to different signedVAAs). We need
-    // to drop them.
-    let vaa = VAA::from_bytes(&vaa_bytes)?;
-    let batch_price_attestation = BatchPriceAttestation::deserialize(vaa.payload.as_slice())
-        .map_err(|_| anyhow!("Failed to deserialize VAA"))?;
-
-    let mut updated_price_feed_ids = Vec::new();
-
-    for price_attestation in batch_price_attestation.price_attestations {
-        let price_feed = price_attestation_to_price_feed(price_attestation.clone());
-
-        let publish_time = price_feed.get_price_unchecked().publish_time.try_into()?;
-
-        let price_info = PriceInfo {
-            price_feed,
-            vaa_bytes: vaa_bytes.clone(),
-            publish_time,
-            emitter_chain: vaa.emitter_chain.into(),
-            attestation_time: price_attestation.attestation_time.try_into()?,
-            receive_time: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
-            sequence_number: vaa.sequence,
-        };
-
-        let key = Key::BatchVaa(price_feed.id);
-        state.insert(key, publish_time, StorageData::BatchVaa(price_info))?;
-
-        // FIXME: Only add price feed if it's newer
-        // or include whether it's newer or not in the vector
-        updated_price_feed_ids.push(price_feed.id);
-    }
-
-    Ok(updated_price_feed_ids)
-}
-
-
-pub fn get_price_infos_with_update_data(
-    state: State,
-    price_ids: Vec<PriceIdentifier>,
-    request_time: RequestTime,
-) -> Result<PriceInfosWithUpdateData> {
-    let mut price_infos = HashMap::new();
-    let mut vaas: HashSet<Vec<u8>> = HashSet::new();
-    for price_id in price_ids {
-        let key = Key::BatchVaa(price_id);
-        let maybe_data = state.get(key, request_time.clone())?;
-
-        match maybe_data {
-            Some(StorageData::BatchVaa(price_info)) => {
-                vaas.insert(price_info.vaa_bytes.clone());
-                price_infos.insert(price_id, price_info);
-            }
-            None => {
-                return Err(anyhow!("No price feed found for price id: {:?}", price_id));
-            }
-        }
-    }
-    let update_data: Vec<Vec<u8>> = vaas.into_iter().collect();
-    Ok(PriceInfosWithUpdateData {
-        price_infos,
-        update_data,
-    })
-}
-
-
-pub fn get_price_feed_ids(state: State) -> Vec<PriceIdentifier> {
-    // Currently we have only one type and filter map is not necessary.
-    // But we might have more types in the future.
-    #[allow(clippy::unnecessary_filter_map)]
-    state
-        .keys()
-        .into_iter()
-        .filter_map(|key| match key {
-            Key::BatchVaa(price_id) => Some(price_id),
-        })
-        .collect()
-}
-
-/// Convert a PriceAttestation to a PriceFeed.
-///
-/// We cannot implmenet this function as From/Into trait because none of these types are defined in this crate.
-/// Ideally we need to move this method to the wormhole_attester sdk crate or have our own implementation of PriceFeed.
-pub fn price_attestation_to_price_feed(price_attestation: PriceAttestation) -> PriceFeed {
-    if price_attestation.status == PriceStatus::Trading {
-        PriceFeed::new(
-            // This conversion is done because the identifier on the wormhole_attester uses sdk v0.5.0 and this crate uses 0.7.0
-            PriceIdentifier::new(price_attestation.price_id.to_bytes()),
-            Price {
-                price:        price_attestation.price,
-                conf:         price_attestation.conf,
-                publish_time: price_attestation.publish_time,
-                expo:         price_attestation.expo,
-            },
-            Price {
-                price:        price_attestation.ema_price,
-                conf:         price_attestation.ema_conf,
-                publish_time: price_attestation.publish_time,
-                expo:         price_attestation.expo,
-            },
-        )
-    } else {
-        PriceFeed::new(
-            PriceIdentifier::new(price_attestation.price_id.to_bytes()),
-            Price {
-                price:        price_attestation.prev_price,
-                conf:         price_attestation.prev_conf,
-                publish_time: price_attestation.prev_publish_time,
-                expo:         price_attestation.expo,
-            },
-            Price {
-                price:        price_attestation.ema_price,
-                conf:         price_attestation.ema_conf,
-                publish_time: price_attestation.prev_publish_time,
-                expo:         price_attestation.expo,
-            },
-        )
-    }
-}

+ 135 - 0
hermes/src/store/proof/wormhole_merkle.rs

@@ -0,0 +1,135 @@
+use {
+    crate::store::{
+        storage::{
+            CompletedAccumulatorState,
+            MessageState,
+        },
+        Store,
+    },
+    anyhow::{
+        anyhow,
+        Result,
+    },
+    pythnet_sdk::{
+        accumulators::{
+            merkle::{
+                MerklePath,
+                MerkleTree,
+            },
+            Accumulator,
+        },
+        hashers::keccak256_160::Keccak160,
+        wire::{
+            to_vec,
+            v1::{
+                AccumulatorUpdateData,
+                MerklePriceUpdate,
+                Proof,
+                WormholeMerkleRoot,
+            },
+        },
+    },
+};
+
+#[derive(Clone, PartialEq, Debug)]
+pub struct WormholeMerkleState {
+    pub root: WormholeMerkleRoot,
+    pub vaa:  Vec<u8>,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+pub struct WormholeMerkleMessageProof {
+    pub vaa:   Vec<u8>,
+    pub proof: MerklePath<Keccak160>,
+}
+
+pub async fn store_wormhole_merkle_verified_message(
+    store: &Store,
+    root: WormholeMerkleRoot,
+    vaa_bytes: Vec<u8>,
+) -> Result<()> {
+    store
+        .storage
+        .update_accumulator_state(
+            root.slot,
+            Box::new(|mut state| {
+                state.wormhole_merkle_state = Some(WormholeMerkleState {
+                    root,
+                    vaa: vaa_bytes,
+                });
+                state
+            }),
+        )
+        .await?;
+    Ok(())
+}
+
+pub fn construct_message_states_proofs(
+    completed_accumulator_state: &CompletedAccumulatorState,
+) -> Result<Vec<WormholeMerkleMessageProof>> {
+    let accumulator_messages = &completed_accumulator_state.accumulator_messages;
+    let wormhole_merkle_state = &completed_accumulator_state.wormhole_merkle_state;
+
+    // Check whether the state is valid
+    let merkle_acc = match MerkleTree::<Keccak160>::from_set(
+        accumulator_messages.raw_messages.iter().map(|m| m.as_ref()),
+    ) {
+        Some(merkle_acc) => merkle_acc,
+        None => return Ok(vec![]), // It only happens when the message set is empty
+    };
+
+    if merkle_acc.root.as_bytes() != wormhole_merkle_state.root.root {
+        return Err(anyhow!("Invalid merkle root"));
+    }
+
+    accumulator_messages
+        .raw_messages
+        .iter()
+        .map(|m| {
+            Ok(WormholeMerkleMessageProof {
+                vaa:   wormhole_merkle_state.vaa.clone(),
+                proof: merkle_acc
+                    .prove(m.as_ref())
+                    .ok_or(anyhow!("Failed to prove message"))?,
+            })
+        })
+        .collect::<Result<Vec<WormholeMerkleMessageProof>>>()
+}
+
+pub fn construct_update_data(mut message_states: Vec<&MessageState>) -> Result<Vec<Vec<u8>>> {
+    message_states.sort_by_key(
+        |m| m.proof_set.wormhole_merkle_proof.vaa.clone(), // FIXME: This is not efficient
+    );
+
+    message_states
+        .group_by(|a, b| {
+            a.proof_set.wormhole_merkle_proof.vaa == b.proof_set.wormhole_merkle_proof.vaa
+        })
+        .map(|messages| {
+            let vaa = messages
+                .get(0)
+                .ok_or(anyhow!("Empty message set"))?
+                .proof_set
+                .wormhole_merkle_proof
+                .vaa
+                .clone();
+
+            Ok(to_vec::<_, byteorder::BE>(&AccumulatorUpdateData::new(
+                Proof::WormholeMerkle {
+                    vaa:     vaa.into(),
+                    updates: messages
+                        .iter()
+                        .map(|message| {
+                            Ok(MerklePriceUpdate {
+                                message: to_vec::<_, byteorder::BE>(&message.message)
+                                    .map_err(|e| anyhow!("Failed to serialize message: {}", e))?
+                                    .into(),
+                                proof:   message.proof_set.wormhole_merkle_proof.proof.clone(),
+                            })
+                        })
+                        .collect::<Result<_>>()?,
+                },
+            ))?)
+        })
+        .collect::<Result<Vec<Vec<u8>>>>()
+}

+ 219 - 14
hermes/src/store/storage.rs

@@ -1,23 +1,119 @@
 use {
     super::{
-        proof::batch_vaa::PriceInfo,
-        RequestTime,
-        UnixTimestamp,
+        proof::wormhole_merkle::WormholeMerkleState,
+        types::{
+            AccumulatorMessages,
+            ProofSet,
+            RawMessage,
+            RequestTime,
+            Slot,
+            UnixTimestamp,
+        },
+    },
+    anyhow::{
+        anyhow,
+        Result,
+    },
+    async_trait::async_trait,
+    pythnet_sdk::messages::{
+        FeedId,
+        Message,
+        MessageType,
     },
-    anyhow::Result,
-    pyth_sdk::PriceIdentifier,
 };
 
-pub mod local_cache;
+pub mod local_storage;
+
+#[derive(Clone, PartialEq, Debug)]
+pub struct AccumulatorState {
+    pub slot:                  Slot,
+    pub accumulator_messages:  Option<AccumulatorMessages>,
+    pub wormhole_merkle_state: Option<WormholeMerkleState>,
+}
 
 #[derive(Clone, PartialEq, Debug)]
-pub enum StorageData {
-    BatchVaa(PriceInfo),
+pub struct CompletedAccumulatorState {
+    pub slot:                  Slot,
+    pub accumulator_messages:  AccumulatorMessages,
+    pub wormhole_merkle_state: WormholeMerkleState,
+}
+
+impl TryFrom<AccumulatorState> for CompletedAccumulatorState {
+    type Error = anyhow::Error;
+
+    fn try_from(state: AccumulatorState) -> Result<Self> {
+        let accumulator_messages = state
+            .accumulator_messages
+            .ok_or_else(|| anyhow!("missing accumulator messages"))?;
+        let wormhole_merkle_state = state
+            .wormhole_merkle_state
+            .ok_or_else(|| anyhow!("missing wormhole merkle state"))?;
+        Ok(Self {
+            slot: state.slot,
+            accumulator_messages,
+            wormhole_merkle_state,
+        })
+    }
 }
 
 #[derive(Clone, PartialEq, Eq, Debug, Hash)]
-pub enum Key {
-    BatchVaa(PriceIdentifier),
+pub struct MessageStateKey {
+    pub feed_id: FeedId,
+    pub type_:   MessageType,
+}
+
+#[derive(Clone, PartialEq, Eq, Debug, PartialOrd, Ord)]
+pub struct MessageStateTime {
+    pub publish_time: UnixTimestamp,
+    pub slot:         Slot,
+}
+
+#[derive(Clone, PartialEq, Debug)]
+pub struct MessageState {
+    pub slot:        Slot,
+    pub message:     Message,
+    pub raw_message: RawMessage,
+    pub proof_set:   ProofSet,
+    pub received_at: UnixTimestamp,
+}
+
+impl MessageState {
+    pub fn time(&self) -> MessageStateTime {
+        MessageStateTime {
+            publish_time: self.message.publish_time(),
+            slot:         self.slot,
+        }
+    }
+
+    pub fn key(&self) -> MessageStateKey {
+        MessageStateKey {
+            feed_id: self.message.feed_id(),
+            type_:   self.message.into(),
+        }
+    }
+
+    pub fn new(
+        message: Message,
+        raw_message: RawMessage,
+        proof_set: ProofSet,
+        slot: Slot,
+        received_at: UnixTimestamp,
+    ) -> Self {
+        Self {
+            slot,
+            message,
+            raw_message,
+            proof_set,
+            received_at,
+        }
+    }
+}
+
+#[derive(Clone, Copy)]
+#[allow(dead_code)]
+pub enum MessageStateFilter {
+    All,
+    Only(MessageType),
 }
 
 /// This trait defines the interface for update data storage
@@ -27,8 +123,117 @@ pub enum Key {
 /// data to abstract the details of the update data, and so each update data is stored
 /// under a separate key. The caller is responsible for specifying the right
 /// key for the update data they wish to access.
-pub trait Storage: Sync + Send {
-    fn insert(&self, key: Key, time: UnixTimestamp, value: StorageData) -> Result<()>;
-    fn get(&self, key: Key, request_time: RequestTime) -> Result<Option<StorageData>>;
-    fn keys(&self) -> Vec<Key>;
+#[async_trait]
+pub trait Storage: Send + Sync {
+    async fn message_state_keys(&self) -> Vec<MessageStateKey>;
+    async fn store_message_states(&self, message_states: Vec<MessageState>) -> Result<()>;
+    async fn fetch_message_states(
+        &self,
+        ids: Vec<FeedId>,
+        request_time: RequestTime,
+        filter: MessageStateFilter,
+    ) -> Result<Vec<MessageState>>;
+
+    /// Store the accumulator state. Please note that this call will replace the
+    /// existing accumulator state for the given state's slot. If you wish to
+    /// update the accumulator state, use `update_accumulator_state` instead.
+    async fn store_accumulator_state(&self, state: AccumulatorState) -> Result<()>;
+    async fn fetch_accumulator_state(&self, slot: Slot) -> Result<Option<AccumulatorState>>;
+
+    /// Update the accumulator state inplace using the provided callback. The callback
+    /// takes the current state and returns the new state. If there is no accumulator
+    /// state for the given slot, the callback will be called with an empty accumulator state.
+    async fn update_accumulator_state(
+        &self,
+        slot: Slot,
+        callback: Box<dyn (FnOnce(AccumulatorState) -> AccumulatorState) + Send>,
+    ) -> Result<()>;
+}
+
+pub type StorageInstance = Box<dyn Storage>;
+
+#[cfg(test)]
+mod test {
+    use {
+        super::*,
+        pythnet_sdk::wire::v1::WormholeMerkleRoot,
+    };
+
+    #[test]
+    pub fn test_complete_accumulator_state_try_from_accumulator_state_works() {
+        let accumulator_state = AccumulatorState {
+            slot:                  1,
+            accumulator_messages:  None,
+            wormhole_merkle_state: None,
+        };
+
+        assert!(CompletedAccumulatorState::try_from(accumulator_state).is_err());
+
+        let accumulator_state = AccumulatorState {
+            slot:                  1,
+            accumulator_messages:  Some(AccumulatorMessages {
+                slot:         1,
+                magic:        [0; 4],
+                ring_size:    10,
+                raw_messages: vec![],
+            }),
+            wormhole_merkle_state: None,
+        };
+
+        assert!(CompletedAccumulatorState::try_from(accumulator_state).is_err());
+
+        let accumulator_state = AccumulatorState {
+            slot:                  1,
+            accumulator_messages:  None,
+            wormhole_merkle_state: Some(WormholeMerkleState {
+                vaa:  vec![],
+                root: WormholeMerkleRoot {
+                    slot:      1,
+                    ring_size: 10,
+                    root:      [0; 20],
+                },
+            }),
+        };
+
+        assert!(CompletedAccumulatorState::try_from(accumulator_state).is_err());
+
+        let accumulator_state = AccumulatorState {
+            slot:                  1,
+            accumulator_messages:  Some(AccumulatorMessages {
+                slot:         1,
+                magic:        [0; 4],
+                ring_size:    10,
+                raw_messages: vec![],
+            }),
+            wormhole_merkle_state: Some(WormholeMerkleState {
+                vaa:  vec![],
+                root: WormholeMerkleRoot {
+                    slot:      1,
+                    ring_size: 10,
+                    root:      [0; 20],
+                },
+            }),
+        };
+
+        assert_eq!(
+            CompletedAccumulatorState::try_from(accumulator_state).unwrap(),
+            CompletedAccumulatorState {
+                slot:                  1,
+                accumulator_messages:  AccumulatorMessages {
+                    slot:         1,
+                    magic:        [0; 4],
+                    ring_size:    10,
+                    raw_messages: vec![],
+                },
+                wormhole_merkle_state: WormholeMerkleState {
+                    vaa:  vec![],
+                    root: WormholeMerkleRoot {
+                        slot:      1,
+                        ring_size: 10,
+                        root:      [0; 20],
+                    },
+                },
+            }
+        );
+    }
 }

+ 0 - 106
hermes/src/store/storage/local_cache.rs

@@ -1,106 +0,0 @@
-use {
-    super::{
-        super::RequestTime,
-        Key,
-        Storage,
-        StorageData,
-        UnixTimestamp,
-    },
-    anyhow::Result,
-    dashmap::DashMap,
-    std::{
-        collections::VecDeque,
-        sync::Arc,
-    },
-};
-
-#[derive(Clone, PartialEq, Debug)]
-pub struct Record {
-    pub time:  UnixTimestamp,
-    pub value: StorageData,
-}
-
-#[derive(Clone)]
-pub struct LocalCache {
-    cache:            Arc<DashMap<Key, VecDeque<Record>>>,
-    max_size_per_key: usize,
-}
-
-impl LocalCache {
-    pub fn new(max_size_per_key: usize) -> Self {
-        Self {
-            cache: Arc::new(DashMap::new()),
-            max_size_per_key,
-        }
-    }
-}
-
-impl Storage for LocalCache {
-    /// Add a new db entry to the cache.
-    ///
-    /// This method keeps the backed store sorted for efficiency, and removes
-    /// the oldest record in the cache if the max_size is reached. Entries are
-    /// usually added in increasing order and likely to be inserted near the
-    /// end of the deque. The function is optimized for this specific case.
-    fn insert(&self, key: Key, time: UnixTimestamp, value: StorageData) -> Result<()> {
-        let mut key_cache = self.cache.entry(key).or_insert_with(VecDeque::new);
-
-        let record = Record { time, value };
-
-        key_cache.push_back(record);
-
-        // Shift the pushed record until it's in the right place.
-        let mut i = key_cache.len() - 1;
-        while i > 0 && key_cache[i - 1].time > key_cache[i].time {
-            key_cache.swap(i - 1, i);
-            i -= 1;
-        }
-
-        // Remove the oldest record if the max size is reached.
-        if key_cache.len() > self.max_size_per_key {
-            key_cache.pop_front();
-        }
-
-        Ok(())
-    }
-
-    fn get(&self, key: Key, request_time: RequestTime) -> Result<Option<StorageData>> {
-        match self.cache.get(&key) {
-            Some(key_cache) => {
-                let record = match request_time {
-                    RequestTime::Latest => key_cache.back().cloned(),
-                    RequestTime::FirstAfter(time) => {
-                        // If the requested time is before the first element in the vector, we are
-                        // not sure that the first element is the closest one.
-                        if let Some(oldest_record) = key_cache.front() {
-                            if time < oldest_record.time {
-                                return Ok(None);
-                            }
-                        }
-
-                        // Binary search returns Ok(idx) if the element is found at index idx or Err(idx) if it's not
-                        // found which idx is the index where the element should be inserted to keep the vector sorted.
-                        // Getting idx within any of the match arms will give us the index of the element that is
-                        // closest after or equal to the requested time.
-                        let idx = match key_cache.binary_search_by_key(&time, |record| record.time)
-                        {
-                            Ok(idx) => idx,
-                            Err(idx) => idx,
-                        };
-
-                        // We are using `get` to handle out of bound idx. This happens if the
-                        // requested time is after the last element in the vector.
-                        key_cache.get(idx).cloned()
-                    }
-                };
-
-                Ok(record.map(|record| record.value))
-            }
-            None => Ok(None),
-        }
-    }
-
-    fn keys(&self) -> Vec<Key> {
-        self.cache.iter().map(|entry| entry.key().clone()).collect()
-    }
-}

+ 862 - 0
hermes/src/store/storage/local_storage.rs

@@ -0,0 +1,862 @@
+use {
+    super::{
+        AccumulatorState,
+        MessageState,
+        MessageStateFilter,
+        MessageStateKey,
+        MessageStateTime,
+        RequestTime,
+        Storage,
+        StorageInstance,
+    },
+    crate::store::types::Slot,
+    anyhow::{
+        anyhow,
+        Result,
+    },
+    async_trait::async_trait,
+    dashmap::DashMap,
+    pythnet_sdk::messages::{
+        FeedId,
+        MessageType,
+    },
+    std::{
+        collections::VecDeque,
+        sync::Arc,
+    },
+    strum::IntoEnumIterator,
+    tokio::sync::RwLock,
+};
+
+#[derive(Clone)]
+pub struct LocalStorage {
+    message_cache:     Arc<DashMap<MessageStateKey, VecDeque<MessageState>>>,
+    accumulator_cache: Arc<RwLock<VecDeque<AccumulatorState>>>,
+    cache_size:        u64,
+}
+
+impl LocalStorage {
+    pub fn new_instance(cache_size: u64) -> StorageInstance {
+        Box::new(Self {
+            message_cache: Arc::new(DashMap::new()),
+            accumulator_cache: Arc::new(RwLock::new(VecDeque::new())),
+            cache_size,
+        })
+    }
+
+    fn retrieve_message_state(
+        &self,
+        key: MessageStateKey,
+        request_time: RequestTime,
+    ) -> Option<MessageState> {
+        match self.message_cache.get(&key) {
+            Some(key_cache) => {
+                match request_time {
+                    RequestTime::Latest => key_cache.back().cloned(),
+                    RequestTime::FirstAfter(time) => {
+                        // If the requested time is before the first element in the vector, we are
+                        // not sure that the first element is the closest one.
+                        if let Some(oldest_record) = key_cache.front() {
+                            if time < oldest_record.time().publish_time {
+                                return None;
+                            }
+                        }
+
+                        let lookup_time = MessageStateTime {
+                            publish_time: time,
+                            slot:         0,
+                        };
+
+                        // Binary search returns Ok(idx) if the element is found at index idx or Err(idx) if it's not
+                        // found which idx is the index where the element should be inserted to keep the vector sorted.
+                        // Getting idx within any of the match arms will give us the index of the element that is
+                        // closest after or equal to the requested time.
+                        let idx = match key_cache
+                            .binary_search_by_key(&lookup_time, |record| record.time())
+                        {
+                            Ok(idx) => idx,
+                            Err(idx) => idx,
+                        };
+
+                        // We are using `get` to handle out of bound idx. This happens if the
+                        // requested time is after the last element in the vector.
+                        key_cache.get(idx).cloned()
+                    }
+                }
+            }
+            None => None,
+        }
+    }
+
+    /// Store the accumulator state in the cache assuming that the lock is already acquired.
+    fn store_accumulator_state_impl(
+        &self,
+        state: AccumulatorState,
+        cache: &mut VecDeque<AccumulatorState>,
+    ) {
+        cache.push_back(state);
+
+        let mut i = cache.len().saturating_sub(1);
+        while i > 0 && cache[i - 1].slot > cache[i].slot {
+            cache.swap(i - 1, i);
+            i -= 1;
+        }
+
+        if cache.len() > self.cache_size as usize {
+            cache.pop_front();
+        }
+    }
+}
+
+#[async_trait]
+impl Storage for LocalStorage {
+    /// Add a new db entry to the cache.
+    ///
+    /// This method keeps the backed store sorted for efficiency, and removes
+    /// the oldest record in the cache if the max_size is reached. Entries are
+    /// usually added in increasing order and likely to be inserted near the
+    /// end of the deque. The function is optimized for this specific case.
+    async fn store_message_states(&self, message_states: Vec<MessageState>) -> Result<()> {
+        for message_state in message_states {
+            let key = message_state.key();
+
+            let mut key_cache = self.message_cache.entry(key).or_insert_with(VecDeque::new);
+
+            key_cache.push_back(message_state);
+
+            // Shift the pushed record until it's in the right place.
+            let mut i = key_cache.len().saturating_sub(1);
+            while i > 0 && key_cache[i - 1].time() > key_cache[i].time() {
+                key_cache.swap(i - 1, i);
+                i -= 1;
+            }
+
+            // FIXME remove equal elements by key and time
+
+            // Remove the oldest record if the max size is reached.
+            if key_cache.len() > self.cache_size as usize {
+                key_cache.pop_front();
+            }
+        }
+
+        Ok(())
+    }
+
+    async fn fetch_message_states(
+        &self,
+        ids: Vec<FeedId>,
+        request_time: RequestTime,
+        filter: MessageStateFilter,
+    ) -> Result<Vec<MessageState>> {
+        ids.into_iter()
+            .flat_map(|id| {
+                let request_time = request_time.clone();
+                let message_types: Vec<MessageType> = match filter {
+                    MessageStateFilter::All => MessageType::iter().collect(),
+                    MessageStateFilter::Only(t) => vec![t],
+                };
+
+                message_types.into_iter().map(move |message_type| {
+                    let key = MessageStateKey {
+                        feed_id: id,
+                        type_:   message_type,
+                    };
+                    self.retrieve_message_state(key, request_time.clone())
+                        .ok_or(anyhow!("Message not found"))
+                })
+            })
+            .collect()
+    }
+
+    async fn message_state_keys(&self) -> Vec<MessageStateKey> {
+        self.message_cache
+            .iter()
+            .map(|entry| entry.key().clone())
+            .collect()
+    }
+
+    async fn store_accumulator_state(&self, state: super::AccumulatorState) -> Result<()> {
+        let mut accumulator_cache = self.accumulator_cache.write().await;
+        self.store_accumulator_state_impl(state, &mut accumulator_cache);
+        Ok(())
+    }
+
+    async fn fetch_accumulator_state(&self, slot: Slot) -> Result<Option<super::AccumulatorState>> {
+        let accumulator_cache = self.accumulator_cache.read().await;
+        match accumulator_cache.binary_search_by_key(&slot, |state| state.slot) {
+            Ok(idx) => Ok(accumulator_cache.get(idx).cloned()),
+            Err(_) => Ok(None),
+        }
+    }
+
+    async fn update_accumulator_state(
+        &self,
+        slot: Slot,
+        callback: Box<dyn (FnOnce(AccumulatorState) -> AccumulatorState) + Send>,
+    ) -> Result<()> {
+        let mut accumulator_cache = self.accumulator_cache.write().await;
+        match accumulator_cache.binary_search_by_key(&slot, |state| state.slot) {
+            Ok(idx) => {
+                let state = accumulator_cache.get_mut(idx).unwrap();
+                *state = callback(state.clone());
+            }
+            Err(_) => {
+                let state = callback(AccumulatorState {
+                    slot,
+                    accumulator_messages: None,
+                    wormhole_merkle_state: None,
+                });
+                self.store_accumulator_state_impl(state, &mut accumulator_cache);
+            }
+        }
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use {
+        super::*,
+        crate::store::{
+            proof::wormhole_merkle::{
+                WormholeMerkleMessageProof,
+                WormholeMerkleState,
+            },
+            types::{
+                AccumulatorMessages,
+                ProofSet,
+            },
+        },
+        futures::future::join_all,
+        pyth_sdk::UnixTimestamp,
+        pythnet_sdk::{
+            accumulators::merkle::MerklePath,
+            hashers::keccak256_160::Keccak160,
+            messages::{
+                Message,
+                PriceFeedMessage,
+            },
+            wire::v1::WormholeMerkleRoot,
+        },
+    };
+
+    pub fn create_dummy_price_feed_message_state(
+        feed_id: FeedId,
+        publish_time: i64,
+        slot: Slot,
+    ) -> MessageState {
+        MessageState {
+            slot,
+            raw_message: vec![],
+            message: Message::PriceFeedMessage(PriceFeedMessage {
+                feed_id,
+                publish_time,
+                price: 1,
+                conf: 2,
+                exponent: 3,
+                ema_price: 4,
+                ema_conf: 5,
+                prev_publish_time: 6,
+            }),
+            received_at: publish_time,
+            proof_set: ProofSet {
+                wormhole_merkle_proof: WormholeMerkleMessageProof {
+                    vaa:   vec![],
+                    proof: MerklePath::<Keccak160>::new(vec![]),
+                },
+            },
+        }
+    }
+
+    pub async fn create_and_store_dummy_price_feed_message_state(
+        storage: &StorageInstance,
+        feed_id: FeedId,
+        publish_time: UnixTimestamp,
+        slot: Slot,
+    ) -> MessageState {
+        let message_state = create_dummy_price_feed_message_state(feed_id, publish_time, slot);
+        storage
+            .store_message_states(vec![message_state.clone()])
+            .await
+            .unwrap();
+        message_state
+    }
+
+    #[tokio::test]
+    pub async fn test_store_and_retrieve_latest_message_state_works() {
+        // Initialize a storage with a cache size of 2 per key.
+        let storage = LocalStorage::new_instance(2);
+
+        // Create and store a message state with feed id [1....] and publish time 10 at slot 5.
+        let message_state =
+            create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
+
+        // The latest message state should be the one we just stored.
+        assert_eq!(
+            storage
+                .fetch_message_states(
+                    vec![[1; 32]],
+                    RequestTime::Latest,
+                    MessageStateFilter::Only(MessageType::PriceFeedMessage),
+                )
+                .await
+                .unwrap(),
+            vec![message_state]
+        );
+    }
+
+    #[tokio::test]
+    pub async fn test_store_and_retrieve_latest_message_state_with_multiple_update_works() {
+        // Initialize a storage with a cache size of 2 per key.
+        let storage = LocalStorage::new_instance(2);
+
+        // Create and store a message state with feed id [1....] and publish time 10 at slot 5.
+        let _old_message_state =
+            create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
+
+        // Create and store a message state with feed id [1....] and publish time 20 at slot 10.
+        let new_message_state =
+            create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 20, 10).await;
+
+        // The latest message state should be the one with publish time 20.
+        assert_eq!(
+            storage
+                .fetch_message_states(
+                    vec![[1; 32]],
+                    RequestTime::Latest,
+                    MessageStateFilter::Only(MessageType::PriceFeedMessage)
+                )
+                .await
+                .unwrap(),
+            vec![new_message_state]
+        );
+    }
+
+    #[tokio::test]
+    pub async fn test_store_and_retrieve_latest_message_state_with_out_of_order_update_works() {
+        // Initialize a storage with a cache size of 2 per key.
+        let storage = LocalStorage::new_instance(2);
+
+        // Create and store a message state with feed id [1....] and publish time 20 at slot 10.
+        let new_message_state =
+            create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 20, 10).await;
+
+        // Create and store a message state with feed id [1....] and publish time 10 at slot 5.
+        let _old_message_state =
+            create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
+
+        // The latest message state should be the one with publish time 20.
+        assert_eq!(
+            storage
+                .fetch_message_states(
+                    vec![[1; 32]],
+                    RequestTime::Latest,
+                    MessageStateFilter::Only(MessageType::PriceFeedMessage)
+                )
+                .await
+                .unwrap(),
+            vec![new_message_state]
+        );
+    }
+
+    #[tokio::test]
+    pub async fn test_store_and_retrieve_first_after_message_state_works() {
+        // Initialize a storage with a cache size of 2 per key.
+        let storage = LocalStorage::new_instance(2);
+
+        // Create and store a message state with feed id [1....] and publish time 10 at slot 5.
+        let old_message_state =
+            create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
+
+        // Create and store a message state with feed id [1....] and publish time 13 at slot 10.
+        let new_message_state =
+            create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 13, 10).await;
+
+        // The first message state after time 10 should be the old message state.
+        assert_eq!(
+            storage
+                .fetch_message_states(
+                    vec![[1; 32]],
+                    RequestTime::FirstAfter(10),
+                    MessageStateFilter::Only(MessageType::PriceFeedMessage)
+                )
+                .await
+                .unwrap(),
+            vec![old_message_state]
+        );
+
+        // Querying the first after pub time 11, 12, 13 should all return the new message state.
+        for request_time in 11..14 {
+            assert_eq!(
+                storage
+                    .fetch_message_states(
+                        vec![[1; 32]],
+                        RequestTime::FirstAfter(request_time),
+                        MessageStateFilter::Only(MessageType::PriceFeedMessage)
+                    )
+                    .await
+                    .unwrap(),
+                vec![new_message_state.clone()]
+            );
+        }
+    }
+
+    #[tokio::test]
+    pub async fn test_store_and_retrieve_latest_message_state_with_same_pubtime_works() {
+        // Initialize a storage with a cache size of 2 per key.
+        let storage = LocalStorage::new_instance(2);
+
+        // Create and store a message state with feed id [1....] and publish time 10 at slot 5.
+        let slightly_older_message_state =
+            create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
+
+        // Create and store a message state with feed id [1....] and publish time 10 at slot 7.
+        let slightly_newer_message_state =
+            create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 7).await;
+
+        // The latest message state should be the one with the higher slot.
+        assert_eq!(
+            storage
+                .fetch_message_states(
+                    vec![[1; 32]],
+                    RequestTime::Latest,
+                    MessageStateFilter::Only(MessageType::PriceFeedMessage),
+                )
+                .await
+                .unwrap(),
+            vec![slightly_newer_message_state]
+        );
+
+        // Querying the first message state after time 10 should return the one with the lower slot.
+        assert_eq!(
+            storage
+                .fetch_message_states(
+                    vec![[1; 32]],
+                    RequestTime::FirstAfter(10),
+                    MessageStateFilter::Only(MessageType::PriceFeedMessage),
+                )
+                .await
+                .unwrap(),
+            vec![slightly_older_message_state]
+        );
+    }
+
+
+    #[tokio::test]
+    pub async fn test_store_and_retrieve_first_after_message_state_fails_for_past_time() {
+        // Initialize a storage with a cache size of 2 per key.
+        let storage = LocalStorage::new_instance(2);
+
+        // Create and store a message state with feed id [1....] and publish time 10 at slot 5.
+        create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
+
+        // Create and store a message state with feed id [1....] and publish time 13 at slot 10.
+        create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 13, 10).await;
+
+        // Query the message state before the available times should return an error.
+        // This is because we are not sure that the first available message is really the first.
+        assert!(storage
+            .fetch_message_states(
+                vec![[1; 32]],
+                RequestTime::FirstAfter(9),
+                MessageStateFilter::Only(MessageType::PriceFeedMessage)
+            )
+            .await
+            .is_err());
+    }
+
+    #[tokio::test]
+    pub async fn test_store_and_retrieve_first_after_message_state_fails_for_future_time() {
+        // Initialize a storage with a cache size of 2 per key.
+        let storage = LocalStorage::new_instance(2);
+
+        // Create and store a message state with feed id [1....] and publish time 10 at slot 5.
+        create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
+
+        // Create and store a message state with feed id [1....] and publish time 13 at slot 10.
+        create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 13, 10).await;
+
+        // Query the message state after the available times should return an error.
+        assert!(storage
+            .fetch_message_states(
+                vec![[1; 32]],
+                RequestTime::FirstAfter(14),
+                MessageStateFilter::Only(MessageType::PriceFeedMessage)
+            )
+            .await
+            .is_err());
+    }
+
+    #[tokio::test]
+    pub async fn test_store_more_message_states_than_cache_size_evicts_old_messages() {
+        // Initialize a storage with a cache size of 2 per key.
+        let storage = LocalStorage::new_instance(2);
+
+        // Create and store a message state with feed id [1....] and publish time 10 at slot 5.
+        create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
+
+        // Create and store a message state with feed id [1....] and publish time 13 at slot 10.
+        create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 13, 10).await;
+
+        // Create and store a message state with feed id [1....] and publish time 20 at slot 14.
+        create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 20, 14).await;
+
+        // The message at time 10 should be evicted and querying for it should return an error.
+        assert!(storage
+            .fetch_message_states(
+                vec![[1; 32]],
+                RequestTime::FirstAfter(10),
+                MessageStateFilter::Only(MessageType::PriceFeedMessage)
+            )
+            .await
+            .is_err());
+    }
+
+    #[tokio::test]
+    pub async fn test_store_and_receive_multiple_message_feed_ids_works() {
+        // Initialize a storage with a cache size of 1 per key.
+        let storage = LocalStorage::new_instance(1);
+
+        // Create and store a message state with feed id [1....] and publish time 10 at slot 5.
+        let message_state_1 =
+            create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
+
+        // Create and store a message state with feed id [2....] and publish time 13 at slot 10.
+        let message_state_2 =
+            create_and_store_dummy_price_feed_message_state(&storage, [2; 32], 10, 5).await;
+
+        // Check both message states can be retrieved.
+        assert_eq!(
+            storage
+                .fetch_message_states(
+                    vec![[1; 32], [2; 32]],
+                    RequestTime::Latest,
+                    MessageStateFilter::Only(MessageType::PriceFeedMessage),
+                )
+                .await
+                .unwrap(),
+            vec![message_state_1, message_state_2]
+        );
+    }
+
+    #[tokio::test]
+    pub async fn test_receive_not_existent_message_fails() {
+        // Initialize a storage with a cache size of 2 per key.
+        let storage = LocalStorage::new_instance(2);
+
+        create_and_store_dummy_price_feed_message_state(&storage, [1; 32], 10, 5).await;
+
+        // Check both message states can be retrieved.
+        assert!(storage
+            .fetch_message_states(
+                vec![[2; 32]],
+                RequestTime::Latest,
+                MessageStateFilter::Only(MessageType::PriceFeedMessage),
+            )
+            .await
+            .is_err());
+    }
+
+    pub fn create_empty_accumulator_state_at_slot(slot: Slot) -> AccumulatorState {
+        AccumulatorState {
+            slot,
+            accumulator_messages: None,
+            wormhole_merkle_state: None,
+        }
+    }
+
+    pub async fn create_and_store_empty_accumulator_state_at_slot(
+        storage: &StorageInstance,
+        slot: Slot,
+    ) -> AccumulatorState {
+        let accumulator_state = create_empty_accumulator_state_at_slot(slot);
+        storage
+            .store_accumulator_state(accumulator_state.clone())
+            .await
+            .unwrap();
+        accumulator_state
+    }
+
+    #[tokio::test]
+    pub async fn test_store_and_receive_accumulator_state_works() {
+        // Initialize a storage with a cache size of 2 per key and the accumulator state.
+        let storage = LocalStorage::new_instance(2);
+
+        // Create and store an accumulator state with slot 10.
+        let accumulator_state =
+            create_and_store_empty_accumulator_state_at_slot(&storage, 10).await;
+
+        // Make sure the retrieved accumulator state is what we stored.
+        assert_eq!(
+            storage.fetch_accumulator_state(10).await.unwrap().unwrap(),
+            accumulator_state
+        );
+    }
+
+    #[tokio::test]
+    pub async fn test_store_and_receive_accumulator_state_works_on_overwrite() {
+        // Initialize a storage with a cache size of 2 per key and the accumulator state.
+        let storage = LocalStorage::new_instance(2);
+
+        // Create and store an accumulator state with slot 10.
+        let mut accumulator_state =
+            create_and_store_empty_accumulator_state_at_slot(&storage, 10).await;
+
+        // Retrieve the accumulator state and make sure it is what we stored.
+        assert_eq!(
+            storage.fetch_accumulator_state(10).await.unwrap().unwrap(),
+            accumulator_state
+        );
+
+        // Change the state to have accumulator messages
+        // We mutate the existing state because the normal flow is like this.
+        accumulator_state.accumulator_messages = Some(AccumulatorMessages {
+            magic:        [0; 4],
+            slot:         10,
+            ring_size:    3,
+            raw_messages: vec![],
+        });
+
+        // Store the accumulator state again.
+        storage
+            .store_accumulator_state(accumulator_state.clone())
+            .await
+            .unwrap();
+
+        // Make sure the retrieved accumulator state is what we stored.
+        assert_eq!(
+            storage.fetch_accumulator_state(10).await.unwrap().unwrap(),
+            accumulator_state
+        );
+    }
+
+    #[tokio::test]
+    pub async fn test_store_and_receive_multiple_accumulator_state_works() {
+        // Initialize a storage with a cache size of 2 per key and the accumulator state.
+        let storage = LocalStorage::new_instance(2);
+
+        let accumulator_state_at_slot_10 =
+            create_and_store_empty_accumulator_state_at_slot(&storage, 10).await;
+        let accumulator_state_at_slot_20 =
+            create_and_store_empty_accumulator_state_at_slot(&storage, 20).await;
+
+        // Retrieve the accumulator states and make sure it is what we stored.
+        assert_eq!(
+            storage.fetch_accumulator_state(10).await.unwrap().unwrap(),
+            accumulator_state_at_slot_10
+        );
+
+        assert_eq!(
+            storage.fetch_accumulator_state(20).await.unwrap().unwrap(),
+            accumulator_state_at_slot_20
+        );
+    }
+
+    #[tokio::test]
+    pub async fn test_store_and_receive_accumulator_state_evicts_cache() {
+        // Initialize a storage with a cache size of 2 per key and the accumulator state.
+        let storage = LocalStorage::new_instance(2);
+
+        let _accumulator_state_at_slot_10 =
+            create_and_store_empty_accumulator_state_at_slot(&storage, 10).await;
+        let accumulator_state_at_slot_20 =
+            create_and_store_empty_accumulator_state_at_slot(&storage, 20).await;
+        let accumulator_state_at_slot_30 =
+            create_and_store_empty_accumulator_state_at_slot(&storage, 30).await;
+
+        // The accumulator state at slot 10 should be evicted from the cache.
+        assert_eq!(storage.fetch_accumulator_state(10).await.unwrap(), None);
+
+        // Retrieve the rest of accumulator states and make sure it is what we stored.
+        assert_eq!(
+            storage.fetch_accumulator_state(20).await.unwrap().unwrap(),
+            accumulator_state_at_slot_20
+        );
+
+        assert_eq!(
+            storage.fetch_accumulator_state(30).await.unwrap().unwrap(),
+            accumulator_state_at_slot_30
+        );
+    }
+
+    #[tokio::test]
+    pub async fn test_update_accumulator_state_works() {
+        // Initialize a storage with a cache size of 2 per key and the accumulator state.
+        let storage = LocalStorage::new_instance(2);
+
+        // Create an empty accumulator state at slot 10.
+        create_and_store_empty_accumulator_state_at_slot(&storage, 10).await;
+
+        // Update the accumulator state with slot 10.
+        let accumulator_messages = AccumulatorMessages {
+            magic:        [0; 4],
+            slot:         10,
+            ring_size:    3,
+            raw_messages: vec![],
+        };
+
+        let accumulator_messages_clone = accumulator_messages.clone();
+
+        storage
+            .update_accumulator_state(
+                10,
+                Box::new(|mut accumulator_state| {
+                    accumulator_state.accumulator_messages = Some(accumulator_messages_clone);
+                    accumulator_state
+                }),
+            )
+            .await
+            .unwrap();
+
+        // Make sure the retrieved accumulator state is what we stored.
+        assert_eq!(
+            storage.fetch_accumulator_state(10).await.unwrap(),
+            Some(AccumulatorState {
+                slot:                  10,
+                accumulator_messages:  Some(accumulator_messages),
+                wormhole_merkle_state: None,
+            })
+        );
+    }
+
+    #[tokio::test]
+    pub async fn test_update_accumulator_state_works_on_nonexistent_state() {
+        // Initialize an empty storage with a cache size of 2 per key and the accumulator state.
+        let storage = LocalStorage::new_instance(2);
+
+        // Update the accumulator state with slot 10.
+        let accumulator_messages = AccumulatorMessages {
+            magic:        [0; 4],
+            slot:         10,
+            ring_size:    3,
+            raw_messages: vec![],
+        };
+        let accumulator_messages_clone = accumulator_messages.clone();
+        storage
+            .update_accumulator_state(
+                10,
+                Box::new(|mut accumulator_state| {
+                    accumulator_state.accumulator_messages = Some(accumulator_messages_clone);
+                    accumulator_state
+                }),
+            )
+            .await
+            .unwrap();
+
+        // Make sure the retrieved accumulator state is what we stored.
+        assert_eq!(
+            storage.fetch_accumulator_state(10).await.unwrap(),
+            Some(AccumulatorState {
+                slot:                  10,
+                accumulator_messages:  Some(accumulator_messages),
+                wormhole_merkle_state: None,
+            })
+        );
+    }
+
+    #[tokio::test]
+    pub async fn test_update_accumulator_state_works_on_concurrent_updates() {
+        // Initialize an empty storage with a cache size of 20 per key and the accumulator state.
+        let storage = LocalStorage::new_instance(20);
+
+
+        // Run this check 10 times to make sure the concurrent updates work.
+        let mut futures = vec![];
+
+        for slot in 1..10 {
+            futures.push(storage.update_accumulator_state(
+                slot,
+                Box::new(|mut accumulator_state| {
+                    accumulator_state.accumulator_messages = Some(AccumulatorMessages {
+                        magic:        [0; 4],
+                        slot:         123,
+                        ring_size:    3,
+                        raw_messages: vec![],
+                    });
+                    accumulator_state
+                }),
+            ));
+            futures.push(storage.update_accumulator_state(
+                slot,
+                Box::new(|mut accumulator_state| {
+                    accumulator_state.wormhole_merkle_state = Some(WormholeMerkleState {
+                        root: WormholeMerkleRoot {
+                            root:      [0; 20],
+                            slot:      123,
+                            ring_size: 3,
+                        },
+                        vaa:  vec![],
+                    });
+                    accumulator_state
+                }),
+            ));
+        }
+
+        join_all(futures).await;
+
+        for slot in 1..10 {
+            assert_eq!(
+                storage.fetch_accumulator_state(slot).await.unwrap(),
+                Some(AccumulatorState {
+                    slot,
+                    accumulator_messages: Some(AccumulatorMessages {
+                        magic:        [0; 4],
+                        slot:         123,
+                        ring_size:    3,
+                        raw_messages: vec![],
+                    }),
+                    wormhole_merkle_state: Some(WormholeMerkleState {
+                        root: WormholeMerkleRoot {
+                            root:      [0; 20],
+                            slot:      123,
+                            ring_size: 3,
+                        },
+                        vaa:  vec![],
+                    }),
+                })
+            )
+        }
+    }
+
+    #[tokio::test]
+    pub async fn test_update_accumulator_state_evicts_cache() {
+        // Initialize a storage with a cache size of 2 per key and the accumulator state.
+        let storage = LocalStorage::new_instance(2);
+
+        storage
+            .update_accumulator_state(10, Box::new(|accumulator_state| accumulator_state))
+            .await
+            .unwrap();
+        storage
+            .update_accumulator_state(20, Box::new(|accumulator_state| accumulator_state))
+            .await
+            .unwrap();
+        storage
+            .update_accumulator_state(30, Box::new(|accumulator_state| accumulator_state))
+            .await
+            .unwrap();
+
+        // The accumulator state at slot 10 should be evicted from the cache.
+        assert_eq!(storage.fetch_accumulator_state(10).await.unwrap(), None);
+
+        // Retrieve the rest of accumulator states and make sure it is what we stored.
+        assert_eq!(
+            storage.fetch_accumulator_state(20).await.unwrap().unwrap(),
+            AccumulatorState {
+                slot:                  20,
+                accumulator_messages:  None,
+                wormhole_merkle_state: None,
+            }
+        );
+
+        assert_eq!(
+            storage.fetch_accumulator_state(30).await.unwrap().unwrap(),
+            AccumulatorState {
+                slot:                  30,
+                accumulator_messages:  None,
+                wormhole_merkle_state: None,
+            }
+        );
+    }
+}

+ 61 - 0
hermes/src/store/types.rs

@@ -0,0 +1,61 @@
+use {
+    super::proof::wormhole_merkle::WormholeMerkleMessageProof,
+    borsh::BorshDeserialize,
+    pythnet_sdk::messages::PriceFeedMessage,
+};
+
+#[derive(Clone, PartialEq, Debug)]
+pub struct ProofSet {
+    pub wormhole_merkle_proof: WormholeMerkleMessageProof,
+}
+
+pub type Slot = u64;
+pub type UnixTimestamp = i64;
+
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub enum RequestTime {
+    Latest,
+    FirstAfter(UnixTimestamp),
+}
+
+pub type RawMessage = Vec<u8>;
+
+/// Accumulator messages coming from Pythnet validators.
+///
+/// The validators writes the accumulator messages using Borsh with
+/// the following struct. We cannot directly have messages as Vec<Messages>
+/// because they are serialized using big-endian byte order and Borsh
+/// uses little-endian byte order.
+#[derive(Clone, PartialEq, Debug, BorshDeserialize)]
+pub struct AccumulatorMessages {
+    pub magic:        [u8; 4],
+    pub slot:         u64,
+    pub ring_size:    u32,
+    pub raw_messages: Vec<RawMessage>,
+}
+
+impl AccumulatorMessages {
+    pub fn ring_index(&self) -> u32 {
+        (self.slot % self.ring_size as u64) as u32
+    }
+}
+
+pub enum Update {
+    Vaa(Vec<u8>),
+    AccumulatorMessages(AccumulatorMessages),
+}
+
+pub struct PriceFeedUpdate {
+    pub price_feed:                  PriceFeedMessage,
+    pub slot:                        Slot,
+    pub received_at:                 UnixTimestamp,
+    /// Wormhole merkle update data for this single price feed update.
+    /// This field is available for backward compatibility and will be
+    /// removed in the future.
+    pub wormhole_merkle_update_data: Vec<u8>,
+}
+
+pub struct PriceFeedsWithUpdateData {
+    pub price_feeds:                 Vec<PriceFeedUpdate>,
+    pub wormhole_merkle_update_data: Vec<Vec<u8>>,
+}

+ 133 - 0
hermes/src/store/wormhole.rs

@@ -0,0 +1,133 @@
+use {
+    super::Store,
+    anyhow::{
+        anyhow,
+        Result,
+    },
+    secp256k1::{
+        ecdsa::{
+            RecoverableSignature,
+            RecoveryId,
+        },
+        Message,
+        Secp256k1,
+    },
+    serde_wormhole::RawMessage,
+    sha3::{
+        Digest,
+        Keccak256,
+    },
+    wormhole_sdk::{
+        vaa::{
+            Body,
+            Header,
+        },
+        Vaa,
+    },
+};
+
+/// A small wrapper around [u8; 20] guardian set key types.
+#[derive(Eq, PartialEq, Clone, Hash, Debug)]
+pub struct GuardianSet {
+    pub keys: Vec<[u8; 20]>,
+}
+
+impl std::fmt::Display for GuardianSet {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "[")?;
+        for (i, key) in self.keys.iter().enumerate() {
+            // Comma seperated printing of the keys using hex encoding.
+            if i != 0 {
+                write!(f, ", ")?;
+            }
+
+            write!(f, "{}", hex::encode(key))?;
+        }
+        write!(f, "]")
+    }
+}
+
+/// BridgeData extracted from wormhole bridge account, due to no API.
+#[derive(borsh::BorshDeserialize)]
+#[allow(dead_code)]
+pub struct BridgeData {
+    pub guardian_set_index: u32,
+    pub last_lamports:      u64,
+    pub config:             BridgeConfig,
+}
+
+/// BridgeConfig extracted from wormhole bridge account, due to no API.
+#[derive(borsh::BorshDeserialize)]
+#[allow(dead_code)]
+pub struct BridgeConfig {
+    pub guardian_set_expiration_time: u32,
+    pub fee:                          u64,
+}
+
+/// GuardianSetData extracted from wormhole bridge account, due to no API.
+#[derive(borsh::BorshDeserialize)]
+pub struct GuardianSetData {
+    pub index:           u32,
+    pub keys:            Vec<[u8; 20]>,
+    pub creation_time:   u32,
+    pub expiration_time: u32,
+}
+
+/// Verifies a VAA to ensure it is signed by the Wormhole guardian set.
+pub async fn verify_vaa<'a>(
+    store: &Store,
+    vaa: Vaa<&'a RawMessage>,
+) -> Result<Vaa<&'a RawMessage>> {
+    let (header, body): (Header, Body<&RawMessage>) = vaa.into();
+    let digest = body.digest()?;
+    let guardian_set = store.guardian_set.read().await;
+    let guardian_set = guardian_set
+        .get(&header.guardian_set_index)
+        .ok_or_else(|| {
+            anyhow!(
+                "Message signed by an unknown guardian set: {}",
+                header.guardian_set_index
+            )
+        })?;
+
+    let mut num_correct_signers = 0;
+    for sig in header.signatures.iter() {
+        let signer_id: usize = sig.index.into();
+
+        let sig = sig.signature;
+
+        // Recover the public key from ecdsa signature from [u8; 65] that has (v, r, s) format
+        let recid = RecoveryId::from_i32(sig[64].into())?;
+
+        let secp = Secp256k1::new();
+
+        // To get the address we need to use the uncompressed public key
+        let pubkey: &[u8; 65] = &secp
+            .recover_ecdsa(
+                &Message::from_slice(&digest.secp256k_hash)?,
+                &RecoverableSignature::from_compact(&sig[..64], recid)?,
+            )?
+            .serialize_uncompressed();
+
+        // The address is the last 20 bytes of the Keccak256 hash of the public key
+        let mut keccak = Keccak256::new();
+        keccak.update(&pubkey[1..]);
+        let address: [u8; 32] = keccak.finalize().into();
+        let address: [u8; 20] = address[address.len() - 20..].try_into()?;
+
+        if guardian_set.keys.get(signer_id) == Some(&address) {
+            num_correct_signers += 1;
+        }
+    }
+
+    let quorum = (guardian_set.keys.len() * 2 + 2) / 3;
+    if num_correct_signers < quorum {
+        return Err(anyhow!(
+            "Not enough correct signatures. Expected {:?}, received {:?}",
+            quorum,
+            num_correct_signers
+        ));
+    }
+
+    Ok((header, body).into())
+}

+ 0 - 1755
message_buffer/Cargo.lock

@@ -1,1755 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 3
-
-[[package]]
-name = "ahash"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
-dependencies = [
- "getrandom 0.2.8",
- "once_cell",
- "version_check",
-]
-
-[[package]]
-name = "aho-corasick"
-version = "0.7.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "anchor-attribute-access-control"
-version = "0.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d5e1a413b311b039d29b61d0dbb401c9dbf04f792497ceca87593454bf6d7dd"
-dependencies = [
- "anchor-syn",
- "anyhow",
- "proc-macro2",
- "quote",
- "regex",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "anchor-attribute-account"
-version = "0.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cca9aeaf633c6e2365fed0525dcac68610be58eee5dc69d3b86fe0b1d4b320b9"
-dependencies = [
- "anchor-syn",
- "anyhow",
- "bs58 0.4.0",
- "proc-macro2",
- "quote",
- "rustversion",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "anchor-attribute-constant"
-version = "0.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "788e44f9e8501dabeb6f9229da0f3268fb2ae3208912608ffaa056a72031296f"
-dependencies = [
- "anchor-syn",
- "proc-macro2",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "anchor-attribute-error"
-version = "0.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea0c4d8c7e4a2605ede6fcdced9690288b2f74e24768619a85229d57e597bc97"
-dependencies = [
- "anchor-syn",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "anchor-attribute-event"
-version = "0.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a3b07d5c5d87b5edc72428b447b8e9ee1143b83dd1afc6a6b1d352c6a6164d8"
-dependencies = [
- "anchor-syn",
- "anyhow",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "anchor-attribute-program"
-version = "0.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b22ad0445115dbea5869b1d062da49ae125abed9132fc20c33227f25e42dfa6b"
-dependencies = [
- "anchor-syn",
- "anyhow",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "anchor-derive-accounts"
-version = "0.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48daeff6781ba2f02961b0ad211feb9a2de75af345d42c62b1a252fd4dfb0724"
-dependencies = [
- "anchor-syn",
- "anyhow",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "anchor-derive-space"
-version = "0.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4fe2886f92c4f33ec1b2b8b2b43ca1b9070cf4929e63c7eaaa09a9f2c0d5123"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "anchor-lang"
-version = "0.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbbe5d1c7c057c6d63b4f2f538a320e4a22111126c9966340c3d9490e2f15ed1"
-dependencies = [
- "anchor-attribute-access-control",
- "anchor-attribute-account",
- "anchor-attribute-constant",
- "anchor-attribute-error",
- "anchor-attribute-event",
- "anchor-attribute-program",
- "anchor-derive-accounts",
- "anchor-derive-space",
- "arrayref",
- "base64 0.13.1",
- "bincode",
- "borsh",
- "bytemuck",
- "solana-program",
- "thiserror",
-]
-
-[[package]]
-name = "anchor-syn"
-version = "0.27.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11cb31fe143aedb36fc41409ea072aa0b840cbea727e62eb2ff6e7b6cea036ff"
-dependencies = [
- "anyhow",
- "bs58 0.3.1",
- "heck",
- "proc-macro2",
- "quote",
- "serde",
- "serde_json",
- "sha2 0.9.9",
- "syn 1.0.109",
- "thiserror",
-]
-
-[[package]]
-name = "anyhow"
-version = "1.0.70"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
-
-[[package]]
-name = "ark-bn254"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea691771ebbb28aea556c044e2e5c5227398d840cee0c34d4d20fa8eb2689e8c"
-dependencies = [
- "ark-ec",
- "ark-ff",
- "ark-std",
-]
-
-[[package]]
-name = "ark-ec"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dea978406c4b1ca13c2db2373b05cc55429c3575b8b21f1b9ee859aa5b03dd42"
-dependencies = [
- "ark-ff",
- "ark-serialize",
- "ark-std",
- "derivative",
- "num-traits",
- "zeroize",
-]
-
-[[package]]
-name = "ark-ff"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6"
-dependencies = [
- "ark-ff-asm",
- "ark-ff-macros",
- "ark-serialize",
- "ark-std",
- "derivative",
- "num-bigint",
- "num-traits",
- "paste",
- "rustc_version 0.3.3",
- "zeroize",
-]
-
-[[package]]
-name = "ark-ff-asm"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44"
-dependencies = [
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "ark-ff-macros"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20"
-dependencies = [
- "num-bigint",
- "num-traits",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "ark-serialize"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671"
-dependencies = [
- "ark-std",
- "digest 0.9.0",
-]
-
-[[package]]
-name = "ark-std"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c"
-dependencies = [
- "num-traits",
- "rand 0.8.5",
-]
-
-[[package]]
-name = "array-bytes"
-version = "1.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ad284aeb45c13f2fb4f084de4a420ebf447423bdf9386c0540ce33cb3ef4b8c"
-
-[[package]]
-name = "arrayref"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
-
-[[package]]
-name = "arrayvec"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
-
-[[package]]
-name = "autocfg"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
-
-[[package]]
-name = "base64"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
-
-[[package]]
-name = "base64"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
-
-[[package]]
-name = "bincode"
-version = "1.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "bitflags"
-version = "1.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
-
-[[package]]
-name = "bitmaps"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2"
-dependencies = [
- "typenum",
-]
-
-[[package]]
-name = "blake3"
-version = "1.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef"
-dependencies = [
- "arrayref",
- "arrayvec",
- "cc",
- "cfg-if",
- "constant_time_eq",
- "digest 0.10.6",
-]
-
-[[package]]
-name = "block-buffer"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
-dependencies = [
- "block-padding",
- "generic-array",
-]
-
-[[package]]
-name = "block-buffer"
-version = "0.10.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "block-padding"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"
-
-[[package]]
-name = "borsh"
-version = "0.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa"
-dependencies = [
- "borsh-derive",
- "hashbrown 0.11.2",
-]
-
-[[package]]
-name = "borsh-derive"
-version = "0.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775"
-dependencies = [
- "borsh-derive-internal",
- "borsh-schema-derive-internal",
- "proc-macro-crate",
- "proc-macro2",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "borsh-derive-internal"
-version = "0.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "borsh-schema-derive-internal"
-version = "0.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "bs58"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb"
-
-[[package]]
-name = "bs58"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3"
-
-[[package]]
-name = "bumpalo"
-version = "3.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
-
-[[package]]
-name = "bv"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340"
-dependencies = [
- "feature-probe",
- "serde",
-]
-
-[[package]]
-name = "bytemuck"
-version = "1.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
-dependencies = [
- "bytemuck_derive",
-]
-
-[[package]]
-name = "bytemuck_derive"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aca418a974d83d40a0c1f0c5cba6ff4bc28d8df099109ca459a2118d40b6322"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "byteorder"
-version = "1.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
-
-[[package]]
-name = "cc"
-version = "1.0.79"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
-dependencies = [
- "jobserver",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
-
-[[package]]
-name = "console_error_panic_hook"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
-dependencies = [
- "cfg-if",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "console_log"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f"
-dependencies = [
- "log",
- "web-sys",
-]
-
-[[package]]
-name = "constant_time_eq"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b"
-
-[[package]]
-name = "cpufeatures"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "crossbeam-channel"
-version = "0.5.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c"
-dependencies = [
- "cfg-if",
- "crossbeam-utils",
-]
-
-[[package]]
-name = "crossbeam-deque"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
-dependencies = [
- "cfg-if",
- "crossbeam-epoch",
- "crossbeam-utils",
-]
-
-[[package]]
-name = "crossbeam-epoch"
-version = "0.9.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
-dependencies = [
- "autocfg",
- "cfg-if",
- "crossbeam-utils",
- "memoffset",
- "scopeguard",
-]
-
-[[package]]
-name = "crossbeam-utils"
-version = "0.8.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "crunchy"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
-
-[[package]]
-name = "crypto-common"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
-dependencies = [
- "generic-array",
- "typenum",
-]
-
-[[package]]
-name = "crypto-mac"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
-dependencies = [
- "generic-array",
- "subtle",
-]
-
-[[package]]
-name = "curve25519-dalek"
-version = "3.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0"
-dependencies = [
- "byteorder",
- "digest 0.9.0",
- "rand_core 0.5.1",
- "serde",
- "subtle",
- "zeroize",
-]
-
-[[package]]
-name = "derivative"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "digest"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "digest"
-version = "0.10.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
-dependencies = [
- "block-buffer 0.10.4",
- "crypto-common",
- "subtle",
-]
-
-[[package]]
-name = "either"
-version = "1.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
-
-[[package]]
-name = "feature-probe"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da"
-
-[[package]]
-name = "generic-array"
-version = "0.14.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
-dependencies = [
- "serde",
- "typenum",
- "version_check",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.1.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
-dependencies = [
- "cfg-if",
- "js-sys",
- "libc",
- "wasi 0.9.0+wasi-snapshot-preview1",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.2.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
-dependencies = [
- "cfg-if",
- "js-sys",
- "libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.11.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
-dependencies = [
- "ahash",
-]
-
-[[package]]
-name = "hashbrown"
-version = "0.12.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
-dependencies = [
- "ahash",
-]
-
-[[package]]
-name = "heck"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
-dependencies = [
- "unicode-segmentation",
-]
-
-[[package]]
-name = "hermit-abi"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "hmac"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840"
-dependencies = [
- "crypto-mac",
- "digest 0.9.0",
-]
-
-[[package]]
-name = "hmac-drbg"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1"
-dependencies = [
- "digest 0.9.0",
- "generic-array",
- "hmac",
-]
-
-[[package]]
-name = "im"
-version = "15.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9"
-dependencies = [
- "bitmaps",
- "rand_core 0.6.4",
- "rand_xoshiro",
- "rayon",
- "serde",
- "sized-chunks",
- "typenum",
- "version_check",
-]
-
-[[package]]
-name = "itertools"
-version = "0.10.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
-dependencies = [
- "either",
-]
-
-[[package]]
-name = "itoa"
-version = "1.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
-
-[[package]]
-name = "jobserver"
-version = "0.1.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "js-sys"
-version = "0.3.61"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
-dependencies = [
- "wasm-bindgen",
-]
-
-[[package]]
-name = "keccak"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768"
-dependencies = [
- "cpufeatures",
-]
-
-[[package]]
-name = "lazy_static"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
-
-[[package]]
-name = "libc"
-version = "0.2.140"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
-
-[[package]]
-name = "libsecp256k1"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73"
-dependencies = [
- "arrayref",
- "base64 0.12.3",
- "digest 0.9.0",
- "hmac-drbg",
- "libsecp256k1-core",
- "libsecp256k1-gen-ecmult",
- "libsecp256k1-gen-genmult",
- "rand 0.7.3",
- "serde",
- "sha2 0.9.9",
- "typenum",
-]
-
-[[package]]
-name = "libsecp256k1-core"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80"
-dependencies = [
- "crunchy",
- "digest 0.9.0",
- "subtle",
-]
-
-[[package]]
-name = "libsecp256k1-gen-ecmult"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3"
-dependencies = [
- "libsecp256k1-core",
-]
-
-[[package]]
-name = "libsecp256k1-gen-genmult"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d"
-dependencies = [
- "libsecp256k1-core",
-]
-
-[[package]]
-name = "lock_api"
-version = "0.4.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
-dependencies = [
- "autocfg",
- "scopeguard",
-]
-
-[[package]]
-name = "log"
-version = "0.4.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "memchr"
-version = "2.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
-
-[[package]]
-name = "memmap2"
-version = "0.5.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "memoffset"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
-dependencies = [
- "autocfg",
-]
-
-[[package]]
-name = "message_buffer"
-version = "0.1.0"
-dependencies = [
- "anchor-lang",
- "bytemuck",
- "byteorder",
-]
-
-[[package]]
-name = "mock-cpi-caller"
-version = "0.1.0"
-dependencies = [
- "anchor-lang",
- "bytemuck",
- "message_buffer",
-]
-
-[[package]]
-name = "num-bigint"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
-dependencies = [
- "autocfg",
- "num-integer",
- "num-traits",
-]
-
-[[package]]
-name = "num-derive"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "num-integer"
-version = "0.1.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
-dependencies = [
- "autocfg",
- "num-traits",
-]
-
-[[package]]
-name = "num-traits"
-version = "0.2.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
-dependencies = [
- "autocfg",
-]
-
-[[package]]
-name = "num_cpus"
-version = "1.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
-dependencies = [
- "hermit-abi",
- "libc",
-]
-
-[[package]]
-name = "once_cell"
-version = "1.17.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
-
-[[package]]
-name = "opaque-debug"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
-
-[[package]]
-name = "parking_lot"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-sys",
-]
-
-[[package]]
-name = "paste"
-version = "1.0.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79"
-
-[[package]]
-name = "pbkdf2"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd"
-dependencies = [
- "crypto-mac",
-]
-
-[[package]]
-name = "pest"
-version = "2.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cbd939b234e95d72bc393d51788aec68aeeb5d51e748ca08ff3aad58cb722f7"
-dependencies = [
- "thiserror",
- "ucd-trie",
-]
-
-[[package]]
-name = "ppv-lite86"
-version = "0.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
-
-[[package]]
-name = "proc-macro-crate"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
-dependencies = [
- "toml",
-]
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.52"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "rand"
-version = "0.7.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
-dependencies = [
- "getrandom 0.1.16",
- "libc",
- "rand_chacha 0.2.2",
- "rand_core 0.5.1",
- "rand_hc",
-]
-
-[[package]]
-name = "rand"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
-dependencies = [
- "rand_chacha 0.3.1",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.5.1",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
-dependencies = [
- "ppv-lite86",
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
-dependencies = [
- "getrandom 0.1.16",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.6.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
-dependencies = [
- "getrandom 0.2.8",
-]
-
-[[package]]
-name = "rand_hc"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
-dependencies = [
- "rand_core 0.5.1",
-]
-
-[[package]]
-name = "rand_xoshiro"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
-dependencies = [
- "rand_core 0.6.4",
-]
-
-[[package]]
-name = "rayon"
-version = "1.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
-dependencies = [
- "either",
- "rayon-core",
-]
-
-[[package]]
-name = "rayon-core"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
-dependencies = [
- "crossbeam-channel",
- "crossbeam-deque",
- "crossbeam-utils",
- "num_cpus",
-]
-
-[[package]]
-name = "redox_syscall"
-version = "0.2.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
-dependencies = [
- "bitflags",
-]
-
-[[package]]
-name = "regex"
-version = "1.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
-]
-
-[[package]]
-name = "regex-syntax"
-version = "0.6.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
-
-[[package]]
-name = "rustc-hash"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
-
-[[package]]
-name = "rustc_version"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
-dependencies = [
- "semver 0.11.0",
-]
-
-[[package]]
-name = "rustc_version"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
-dependencies = [
- "semver 1.0.17",
-]
-
-[[package]]
-name = "rustversion"
-version = "1.0.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
-
-[[package]]
-name = "ryu"
-version = "1.0.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
-
-[[package]]
-name = "scopeguard"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
-
-[[package]]
-name = "semver"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
-dependencies = [
- "semver-parser",
-]
-
-[[package]]
-name = "semver"
-version = "1.0.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
-
-[[package]]
-name = "semver-parser"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
-dependencies = [
- "pest",
-]
-
-[[package]]
-name = "serde"
-version = "1.0.158"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9"
-dependencies = [
- "serde_derive",
-]
-
-[[package]]
-name = "serde_bytes"
-version = "0.11.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "serde_derive"
-version = "1.0.158"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.3",
-]
-
-[[package]]
-name = "serde_json"
-version = "1.0.94"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea"
-dependencies = [
- "itoa",
- "ryu",
- "serde",
-]
-
-[[package]]
-name = "sha2"
-version = "0.9.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
-dependencies = [
- "block-buffer 0.9.0",
- "cfg-if",
- "cpufeatures",
- "digest 0.9.0",
- "opaque-debug",
-]
-
-[[package]]
-name = "sha2"
-version = "0.10.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest 0.10.6",
-]
-
-[[package]]
-name = "sha3"
-version = "0.10.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9"
-dependencies = [
- "digest 0.10.6",
- "keccak",
-]
-
-[[package]]
-name = "sized-chunks"
-version = "0.6.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e"
-dependencies = [
- "bitmaps",
- "typenum",
-]
-
-[[package]]
-name = "smallvec"
-version = "1.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
-
-[[package]]
-name = "solana-frozen-abi"
-version = "1.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48f7051cccdf891ac2603cdd295eb651529fe2b678b6b3af60b82dec9a9b3b06"
-dependencies = [
- "ahash",
- "blake3",
- "block-buffer 0.9.0",
- "bs58 0.4.0",
- "bv",
- "byteorder",
- "cc",
- "either",
- "generic-array",
- "getrandom 0.1.16",
- "hashbrown 0.12.3",
- "im",
- "lazy_static",
- "log",
- "memmap2",
- "once_cell",
- "rand_core 0.6.4",
- "rustc_version 0.4.0",
- "serde",
- "serde_bytes",
- "serde_derive",
- "serde_json",
- "sha2 0.10.6",
- "solana-frozen-abi-macro",
- "subtle",
- "thiserror",
-]
-
-[[package]]
-name = "solana-frozen-abi-macro"
-version = "1.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06395428329810ade1d2518a7e75d8a6f02d01fe548aabb60ff1ba6a2eaebbe5"
-dependencies = [
- "proc-macro2",
- "quote",
- "rustc_version 0.4.0",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "solana-program"
-version = "1.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ae9f0fa7db3a4e90fa0df2723ac8cbc042e579cf109cd0380bc5a8c88bed924"
-dependencies = [
- "ark-bn254",
- "ark-ec",
- "ark-ff",
- "array-bytes",
- "base64 0.13.1",
- "bincode",
- "bitflags",
- "blake3",
- "borsh",
- "borsh-derive",
- "bs58 0.4.0",
- "bv",
- "bytemuck",
- "cc",
- "console_error_panic_hook",
- "console_log",
- "curve25519-dalek",
- "getrandom 0.2.8",
- "itertools",
- "js-sys",
- "lazy_static",
- "libc",
- "libsecp256k1",
- "log",
- "memoffset",
- "num-bigint",
- "num-derive",
- "num-traits",
- "parking_lot",
- "rand 0.7.3",
- "rand_chacha 0.2.2",
- "rustc_version 0.4.0",
- "rustversion",
- "serde",
- "serde_bytes",
- "serde_derive",
- "serde_json",
- "sha2 0.10.6",
- "sha3",
- "solana-frozen-abi",
- "solana-frozen-abi-macro",
- "solana-sdk-macro",
- "thiserror",
- "tiny-bip39",
- "wasm-bindgen",
- "zeroize",
-]
-
-[[package]]
-name = "solana-sdk-macro"
-version = "1.15.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f809319358d5da7c3a0ac08ebf4d87b21170d928dbb7260254e8f3061f7f9e0e"
-dependencies = [
- "bs58 0.4.0",
- "proc-macro2",
- "quote",
- "rustversion",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "subtle"
-version = "2.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
-
-[[package]]
-name = "syn"
-version = "1.0.109"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "syn"
-version = "2.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8234ae35e70582bfa0f1fedffa6daa248e41dd045310b19800c4a36382c8f60"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "synstructure"
-version = "0.12.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "unicode-xid",
-]
-
-[[package]]
-name = "thiserror"
-version = "1.0.40"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
-dependencies = [
- "thiserror-impl",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.40"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.3",
-]
-
-[[package]]
-name = "tiny-bip39"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d"
-dependencies = [
- "anyhow",
- "hmac",
- "once_cell",
- "pbkdf2",
- "rand 0.7.3",
- "rustc-hash",
- "sha2 0.9.9",
- "thiserror",
- "unicode-normalization",
- "wasm-bindgen",
- "zeroize",
-]
-
-[[package]]
-name = "tinyvec"
-version = "1.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
-dependencies = [
- "tinyvec_macros",
-]
-
-[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
-
-[[package]]
-name = "toml"
-version = "0.5.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "typenum"
-version = "1.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
-
-[[package]]
-name = "ucd-trie"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
-
-[[package]]
-name = "unicode-normalization"
-version = "0.1.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
-dependencies = [
- "tinyvec",
-]
-
-[[package]]
-name = "unicode-segmentation"
-version = "1.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
-
-[[package]]
-name = "unicode-xid"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
-
-[[package]]
-name = "version_check"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
-
-[[package]]
-name = "wasi"
-version = "0.9.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
-
-[[package]]
-name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
-
-[[package]]
-name = "wasm-bindgen"
-version = "0.2.84"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
-dependencies = [
- "cfg-if",
- "wasm-bindgen-macro",
-]
-
-[[package]]
-name = "wasm-bindgen-backend"
-version = "0.2.84"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
-dependencies = [
- "bumpalo",
- "log",
- "once_cell",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-macro"
-version = "0.2.84"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
-dependencies = [
- "quote",
- "wasm-bindgen-macro-support",
-]
-
-[[package]]
-name = "wasm-bindgen-macro-support"
-version = "0.2.84"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "wasm-bindgen-backend",
- "wasm-bindgen-shared",
-]
-
-[[package]]
-name = "wasm-bindgen-shared"
-version = "0.2.84"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
-
-[[package]]
-name = "web-sys"
-version = "0.3.61"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.45.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
-dependencies = [
- "windows-targets",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
-dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
-]
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
-
-[[package]]
-name = "zeroize"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd"
-dependencies = [
- "zeroize_derive",
-]
-
-[[package]]
-name = "zeroize_derive"
-version = "1.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "synstructure",
-]

+ 0 - 64
message_buffer/programs/message_buffer/src/instructions/delete_buffer.rs

@@ -1,64 +0,0 @@
-use {
-    crate::{
-        state::*,
-        MessageBufferError,
-        MESSAGE,
-    },
-    anchor_lang::prelude::*,
-};
-
-pub fn delete_buffer<'info>(
-    ctx: Context<'_, '_, '_, 'info, DeleteBuffer<'info>>,
-    allowed_program_auth: Pubkey,
-    base_account_key: Pubkey,
-    bump: u8,
-) -> Result<()> {
-    let message_buffer_account_info = ctx
-        .remaining_accounts
-        .first()
-        .ok_or(MessageBufferError::MessageBufferNotProvided)?;
-
-    ctx.accounts
-        .whitelist
-        .is_allowed_program_auth(&allowed_program_auth)?;
-
-    MessageBuffer::check_discriminator(message_buffer_account_info)?;
-
-    let expected_key = Pubkey::create_program_address(
-        &[
-            allowed_program_auth.as_ref(),
-            MESSAGE.as_bytes(),
-            base_account_key.as_ref(),
-            &[bump],
-        ],
-        &crate::ID,
-    )
-    .map_err(|_| MessageBufferError::InvalidPDA)?;
-
-    require_keys_eq!(
-        message_buffer_account_info.key(),
-        expected_key,
-        MessageBufferError::InvalidPDA
-    );
-    let loader = AccountLoader::<MessageBuffer>::try_from_unchecked(
-        &crate::ID,
-        message_buffer_account_info,
-    )?;
-    loader.close(ctx.accounts.admin.to_account_info())?;
-    Ok(())
-}
-
-#[derive(Accounts)]
-pub struct DeleteBuffer<'info> {
-    #[account(
-        seeds = [b"message".as_ref(), b"whitelist".as_ref()],
-        bump = whitelist.bump,
-        has_one = admin,
-    )]
-    pub whitelist: Account<'info, Whitelist>,
-
-    // Also the recipient of the lamports from closing the buffer account
-    #[account(mut)]
-    pub admin: Signer<'info>,
-    // remaining_account:  - [AccumulatorInput PDA]
-}

+ 0 - 54
message_buffer/programs/message_buffer/src/instructions/put_all.rs

@@ -1,54 +0,0 @@
-use {
-    crate::{
-        state::*,
-        MessageBufferError,
-    },
-    anchor_lang::prelude::*,
-    std::mem,
-};
-
-
-pub fn put_all<'info>(
-    ctx: Context<'_, '_, '_, 'info, PutAll<'info>>,
-    base_account_key: Pubkey,
-    messages: Vec<Vec<u8>>,
-) -> Result<()> {
-    let cpi_caller_auth = ctx.accounts.whitelist_verifier.is_allowed()?;
-    let message_buffer_account_info = ctx
-        .remaining_accounts
-        .first()
-        .ok_or(MessageBufferError::MessageBufferNotProvided)?;
-
-    MessageBuffer::check_discriminator(message_buffer_account_info)?;
-
-    let account_data = &mut message_buffer_account_info.try_borrow_mut_data()?;
-    let header_end_index = mem::size_of::<MessageBuffer>() + 8;
-
-    let (header_bytes, body_bytes) = account_data.split_at_mut(header_end_index);
-
-    let message_buffer: &mut MessageBuffer = bytemuck::from_bytes_mut(&mut header_bytes[8..]);
-
-    message_buffer.validate(
-        message_buffer_account_info.key(),
-        cpi_caller_auth,
-        base_account_key,
-    )?;
-
-    message_buffer.refresh_header();
-
-    let (num_msgs, num_bytes) = message_buffer.put_all_in_buffer(body_bytes, &messages);
-
-    if num_msgs != messages.len() {
-        // FIXME: make this into an emit! event
-        msg!("unable to fit all messages in accumulator input account. Wrote {}/{} messages and {} bytes", num_msgs, messages.len(), num_bytes);
-    }
-
-    Ok(())
-}
-
-#[derive(Accounts)]
-#[instruction( base_account_key: Pubkey)]
-pub struct PutAll<'info> {
-    pub whitelist_verifier: WhitelistVerifier<'info>,
-    // remaining_accounts:  - [AccumulatorInput PDA]
-}

+ 0 - 111
message_buffer/programs/message_buffer/src/instructions/resize_buffer.rs

@@ -1,111 +0,0 @@
-use {
-    crate::{
-        state::*,
-        MessageBufferError,
-        MESSAGE,
-    },
-    anchor_lang::{
-        prelude::*,
-        solana_program::entrypoint::MAX_PERMITTED_DATA_INCREASE,
-        system_program::{
-            self,
-            Transfer,
-        },
-    },
-};
-
-pub fn resize_buffer<'info>(
-    ctx: Context<'_, '_, '_, 'info, ResizeBuffer<'info>>,
-    allowed_program_auth: Pubkey,
-    base_account_key: Pubkey,
-    buffer_bump: u8,
-    target_size: u32,
-) -> Result<()> {
-    let message_buffer_account_info = ctx
-        .remaining_accounts
-        .first()
-        .ok_or(MessageBufferError::MessageBufferNotProvided)?;
-
-    ctx.accounts
-        .whitelist
-        .is_allowed_program_auth(&allowed_program_auth)?;
-    MessageBuffer::check_discriminator(message_buffer_account_info)?;
-
-    require_gte!(
-        target_size,
-        MessageBuffer::HEADER_LEN as u32,
-        MessageBufferError::MessageBufferTooSmall
-    );
-    let target_size = target_size as usize;
-    let target_size_delta = target_size.saturating_sub(message_buffer_account_info.data_len());
-    require_gte!(
-        MAX_PERMITTED_DATA_INCREASE,
-        target_size_delta,
-        MessageBufferError::TargetSizeDeltaExceeded
-    );
-
-    let expected_key = Pubkey::create_program_address(
-        &[
-            allowed_program_auth.as_ref(),
-            MESSAGE.as_bytes(),
-            base_account_key.as_ref(),
-            &[buffer_bump],
-        ],
-        &crate::ID,
-    )
-    .map_err(|_| MessageBufferError::InvalidPDA)?;
-
-    require_keys_eq!(
-        message_buffer_account_info.key(),
-        expected_key,
-        MessageBufferError::InvalidPDA
-    );
-
-    // allow for delta == 0 in case Rent requirements have changed
-    // and additional lamports need to be transferred.
-    // the realloc step will be a no-op in this case.
-    if target_size_delta >= 0 {
-        let target_rent = Rent::get()?.minimum_balance(target_size);
-        if message_buffer_account_info.lamports() < target_rent {
-            system_program::transfer(
-                CpiContext::new(
-                    ctx.accounts.system_program.to_account_info(),
-                    Transfer {
-                        from: ctx.accounts.admin.to_account_info(),
-                        to:   message_buffer_account_info.to_account_info(),
-                    },
-                ),
-                target_rent - message_buffer_account_info.lamports(),
-            )?;
-        }
-        message_buffer_account_info
-            .realloc(target_size, false)
-            .map_err(|_| MessageBufferError::ReallocFailed)?;
-    } else {
-        // Not transferring excess lamports back to admin.
-        // Account will retain more lamports than necessary.
-        message_buffer_account_info.realloc(target_size, false)?;
-    }
-    Ok(())
-}
-
-#[derive(Accounts)]
-#[instruction(
-    allowed_program_auth: Pubkey, base_account_key: Pubkey,
-    buffer_bump: u8, target_size: u32
-)]
-pub struct ResizeBuffer<'info> {
-    #[account(
-        seeds = [b"message".as_ref(), b"whitelist".as_ref()],
-        bump = whitelist.bump,
-        has_one = admin,
-    )]
-    pub whitelist: Account<'info, Whitelist>,
-
-    // Also pays for account creation
-    #[account(mut)]
-    pub admin: Signer<'info>,
-
-    pub system_program: Program<'info, System>,
-    // remaining_accounts:  - [AccumulatorInput PDA]
-}

+ 0 - 11
message_buffer/programs/message_buffer/src/macros.rs

@@ -1,11 +0,0 @@
-#[macro_export]
-macro_rules! accumulator_input_seeds {
-    ($accumulator_input:expr, $cpi_caller_pid:expr, $base_account:expr) => {
-        &[
-            $cpi_caller_pid.as_ref(),
-            b"message".as_ref(),
-            $base_account.as_ref(),
-            &[$accumulator_input.bump],
-        ]
-    };
-}

+ 0 - 2316
message_buffer/yarn.lock

@@ -1,2316 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.8.3":
-  version "7.18.6"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
-  integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
-  dependencies:
-    "@babel/highlight" "^7.18.6"
-
-"@babel/helper-validator-identifier@^7.18.6":
-  version "7.19.1"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
-  integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
-
-"@babel/highlight@^7.18.6":
-  version "7.18.6"
-  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
-  integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==
-  dependencies:
-    "@babel/helper-validator-identifier" "^7.18.6"
-    chalk "^2.0.0"
-    js-tokens "^4.0.0"
-
-"@babel/runtime@^7.12.5", "@babel/runtime@^7.17.2":
-  version "7.21.0"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
-  integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
-  dependencies:
-    regenerator-runtime "^0.13.11"
-
-"@coral-xyz/anchor@^0.27.0":
-  version "0.27.0"
-  resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.27.0.tgz#621e5ef123d05811b97e49973b4ed7ede27c705c"
-  integrity sha512-+P/vPdORawvg3A9Wj02iquxb4T0C5m4P6aZBVYysKl4Amk+r6aMPZkUhilBkD6E4Nuxnoajv3CFykUfkGE0n5g==
-  dependencies:
-    "@coral-xyz/borsh" "^0.27.0"
-    "@solana/web3.js" "^1.68.0"
-    base64-js "^1.5.1"
-    bn.js "^5.1.2"
-    bs58 "^4.0.1"
-    buffer-layout "^1.2.2"
-    camelcase "^6.3.0"
-    cross-fetch "^3.1.5"
-    crypto-hash "^1.3.0"
-    eventemitter3 "^4.0.7"
-    js-sha256 "^0.9.0"
-    pako "^2.0.3"
-    snake-case "^3.0.4"
-    superstruct "^0.15.4"
-    toml "^3.0.0"
-
-"@coral-xyz/borsh@^0.27.0":
-  version "0.27.0"
-  resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.27.0.tgz#700c647ea5262b1488957ac7fb4e8acf72c72b63"
-  integrity sha512-tJKzhLukghTWPLy+n8K8iJKgBq1yLT/AxaNd10yJrX8mI56ao5+OFAKAqW/h0i79KCvb4BK0VGO5ECmmolFz9A==
-  dependencies:
-    bn.js "^5.1.2"
-    buffer-layout "^1.2.0"
-
-"@lumina-dev/test@^0.0.12":
-  version "0.0.12"
-  resolved "https://registry.yarnpkg.com/@lumina-dev/test/-/test-0.0.12.tgz#b30e606f10ae813dcc7abd6dccd5defc73d473cf"
-  integrity sha512-Tc3wafgJGKJAhUoILdKmRimugtO3KEYMtmrFICLHfZyPm53/H/9LkyHxetUDmmB1Ttq0yFafgDg9jX5Lx9XwMA==
-  dependencies:
-    bs58 "^5.0.0"
-    cors "^2.8.5"
-    express "^4.18.2"
-    nanoid "^3.3.4"
-    react-dev-utils "^12.0.1"
-    zod "^3.20.2"
-
-"@noble/ed25519@^1.7.0":
-  version "1.7.3"
-  resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.3.tgz#57e1677bf6885354b466c38e2b620c62f45a7123"
-  integrity sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==
-
-"@noble/hashes@^1.1.2":
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1"
-  integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==
-
-"@noble/secp256k1@^1.6.3":
-  version "1.7.1"
-  resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c"
-  integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==
-
-"@nodelib/fs.scandir@2.1.5":
-  version "2.1.5"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
-  integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
-  dependencies:
-    "@nodelib/fs.stat" "2.0.5"
-    run-parallel "^1.1.9"
-
-"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
-  integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
-
-"@nodelib/fs.walk@^1.2.3":
-  version "1.2.8"
-  resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
-  integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
-  dependencies:
-    "@nodelib/fs.scandir" "2.1.5"
-    fastq "^1.6.0"
-
-"@solana/buffer-layout@^4.0.0":
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15"
-  integrity sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==
-  dependencies:
-    buffer "~6.0.3"
-
-"@solana/web3.js@^1.68.0":
-  version "1.74.0"
-  resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.74.0.tgz#dbcbeabb830dd7cbbcf5e31404ca79c9785cbf2d"
-  integrity sha512-RKZyPqizPCxmpMGfpu4fuplNZEWCrhRBjjVstv5QnAJvgln1jgOfgui+rjl1ExnqDnWKg9uaZ5jtGROH/cwabg==
-  dependencies:
-    "@babel/runtime" "^7.12.5"
-    "@noble/ed25519" "^1.7.0"
-    "@noble/hashes" "^1.1.2"
-    "@noble/secp256k1" "^1.6.3"
-    "@solana/buffer-layout" "^4.0.0"
-    agentkeepalive "^4.2.1"
-    bigint-buffer "^1.1.5"
-    bn.js "^5.0.0"
-    borsh "^0.7.0"
-    bs58 "^4.0.1"
-    buffer "6.0.1"
-    fast-stable-stringify "^1.0.0"
-    jayson "^3.4.4"
-    node-fetch "^2.6.7"
-    rpc-websockets "^7.5.1"
-    superstruct "^0.14.2"
-
-"@types/bn.js@^5.1.0":
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.1.tgz#b51e1b55920a4ca26e9285ff79936bbdec910682"
-  integrity sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g==
-  dependencies:
-    "@types/node" "*"
-
-"@types/chai@^4.3.0":
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4"
-  integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==
-
-"@types/connect@^3.4.33":
-  version "3.4.35"
-  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
-  integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
-  dependencies:
-    "@types/node" "*"
-
-"@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5":
-  version "7.0.11"
-  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
-  integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
-
-"@types/json5@^0.0.29":
-  version "0.0.29"
-  resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
-  integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
-
-"@types/mocha@^9.0.0":
-  version "9.1.1"
-  resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4"
-  integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==
-
-"@types/node@*":
-  version "18.15.3"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014"
-  integrity sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==
-
-"@types/node@^12.12.54":
-  version "12.20.55"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
-  integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==
-
-"@types/parse-json@^4.0.0":
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
-  integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
-
-"@types/ws@^7.4.4":
-  version "7.4.7"
-  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702"
-  integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==
-  dependencies:
-    "@types/node" "*"
-
-"@ungap/promise-all-settled@1.1.2":
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
-  integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
-
-JSONStream@^1.3.5:
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
-  integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==
-  dependencies:
-    jsonparse "^1.2.0"
-    through ">=2.2.7 <3"
-
-accepts@~1.3.8:
-  version "1.3.8"
-  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
-  integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
-  dependencies:
-    mime-types "~2.1.34"
-    negotiator "0.6.3"
-
-address@^1.0.1, address@^1.1.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e"
-  integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==
-
-agentkeepalive@^4.2.1:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.3.0.tgz#bb999ff07412653c1803b3ced35e50729830a255"
-  integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==
-  dependencies:
-    debug "^4.1.0"
-    depd "^2.0.0"
-    humanize-ms "^1.2.1"
-
-ajv-keywords@^3.4.1:
-  version "3.5.2"
-  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
-  integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
-
-ajv@^6.12.2:
-  version "6.12.6"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
-  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
-  dependencies:
-    fast-deep-equal "^3.1.1"
-    fast-json-stable-stringify "^2.0.0"
-    json-schema-traverse "^0.4.1"
-    uri-js "^4.2.2"
-
-ansi-colors@4.1.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
-  integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
-
-ansi-regex@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
-  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
-
-ansi-styles@^3.2.1:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
-  integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
-  dependencies:
-    color-convert "^1.9.0"
-
-ansi-styles@^4.0.0, ansi-styles@^4.1.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
-  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
-  dependencies:
-    color-convert "^2.0.1"
-
-anymatch@~3.1.2:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
-  integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
-  dependencies:
-    normalize-path "^3.0.0"
-    picomatch "^2.0.4"
-
-argparse@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
-  integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
-
-array-flatten@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
-  integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
-
-array-union@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
-  integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
-
-arrify@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
-  integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==
-
-assertion-error@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
-  integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
-
-at-least-node@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
-  integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
-
-balanced-match@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
-  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-
-base-x@^3.0.2:
-  version "3.0.9"
-  resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320"
-  integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==
-  dependencies:
-    safe-buffer "^5.0.1"
-
-base-x@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a"
-  integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==
-
-base64-js@^1.3.1, base64-js@^1.5.1:
-  version "1.5.1"
-  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
-  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
-
-bigint-buffer@^1.1.5:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442"
-  integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==
-  dependencies:
-    bindings "^1.3.0"
-
-binary-extensions@^2.0.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
-  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
-
-bindings@^1.3.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
-  integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
-  dependencies:
-    file-uri-to-path "1.0.0"
-
-bn.js@^5.0.0, bn.js@^5.1.2, bn.js@^5.2.0:
-  version "5.2.1"
-  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
-  integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
-
-body-parser@1.20.1:
-  version "1.20.1"
-  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
-  integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
-  dependencies:
-    bytes "3.1.2"
-    content-type "~1.0.4"
-    debug "2.6.9"
-    depd "2.0.0"
-    destroy "1.2.0"
-    http-errors "2.0.0"
-    iconv-lite "0.4.24"
-    on-finished "2.4.1"
-    qs "6.11.0"
-    raw-body "2.5.1"
-    type-is "~1.6.18"
-    unpipe "1.0.0"
-
-borsh@^0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a"
-  integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==
-  dependencies:
-    bn.js "^5.2.0"
-    bs58 "^4.0.0"
-    text-encoding-utf-8 "^1.0.2"
-
-brace-expansion@^1.1.7:
-  version "1.1.11"
-  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
-  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
-  dependencies:
-    balanced-match "^1.0.0"
-    concat-map "0.0.1"
-
-braces@^3.0.2, braces@~3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
-  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
-  dependencies:
-    fill-range "^7.0.1"
-
-browser-stdout@1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
-  integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
-
-browserslist@^4.18.1:
-  version "4.21.5"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7"
-  integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==
-  dependencies:
-    caniuse-lite "^1.0.30001449"
-    electron-to-chromium "^1.4.284"
-    node-releases "^2.0.8"
-    update-browserslist-db "^1.0.10"
-
-bs58@^4.0.0, bs58@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
-  integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==
-  dependencies:
-    base-x "^3.0.2"
-
-bs58@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279"
-  integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==
-  dependencies:
-    base-x "^4.0.0"
-
-buffer-from@^1.0.0, buffer-from@^1.1.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
-  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
-
-buffer-layout@^1.2.0, buffer-layout@^1.2.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5"
-  integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==
-
-buffer@6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.1.tgz#3cbea8c1463e5a0779e30b66d4c88c6ffa182ac2"
-  integrity sha512-rVAXBwEcEoYtxnHSO5iWyhzV/O1WMtkUYWlfdLS7FjU4PnSJJHEfHXi/uHPI5EwltmOA794gN3bm3/pzuctWjQ==
-  dependencies:
-    base64-js "^1.3.1"
-    ieee754 "^1.2.1"
-
-buffer@~6.0.3:
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
-  integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
-  dependencies:
-    base64-js "^1.3.1"
-    ieee754 "^1.2.1"
-
-bufferutil@^4.0.1:
-  version "4.0.7"
-  resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad"
-  integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==
-  dependencies:
-    node-gyp-build "^4.3.0"
-
-bytes@3.1.2:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
-  integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
-
-call-bind@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
-  integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
-  dependencies:
-    function-bind "^1.1.1"
-    get-intrinsic "^1.0.2"
-
-callsites@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
-  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
-
-camelcase@^6.0.0, camelcase@^6.3.0:
-  version "6.3.0"
-  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
-  integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
-
-caniuse-lite@^1.0.30001449:
-  version "1.0.30001469"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001469.tgz#3dd505430c8522fdc9f94b4a19518e330f5c945a"
-  integrity sha512-Rcp7221ScNqQPP3W+lVOYDyjdR6dC+neEQCttoNr5bAyz54AboB4iwpnWgyi8P4YUsPybVzT4LgWiBbI3drL4g==
-
-chai@^4.3.4:
-  version "4.3.7"
-  resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51"
-  integrity sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==
-  dependencies:
-    assertion-error "^1.1.0"
-    check-error "^1.0.2"
-    deep-eql "^4.1.2"
-    get-func-name "^2.0.0"
-    loupe "^2.3.1"
-    pathval "^1.1.1"
-    type-detect "^4.0.5"
-
-chalk@^2.0.0:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
-  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
-  dependencies:
-    ansi-styles "^3.2.1"
-    escape-string-regexp "^1.0.5"
-    supports-color "^5.3.0"
-
-chalk@^4.1.0, chalk@^4.1.2:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
-  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
-  dependencies:
-    ansi-styles "^4.1.0"
-    supports-color "^7.1.0"
-
-check-error@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
-  integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==
-
-chokidar@3.5.3, chokidar@^3.4.2:
-  version "3.5.3"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
-  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
-  dependencies:
-    anymatch "~3.1.2"
-    braces "~3.0.2"
-    glob-parent "~5.1.2"
-    is-binary-path "~2.1.0"
-    is-glob "~4.0.1"
-    normalize-path "~3.0.0"
-    readdirp "~3.6.0"
-  optionalDependencies:
-    fsevents "~2.3.2"
-
-cliui@^7.0.2:
-  version "7.0.4"
-  resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
-  integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
-  dependencies:
-    string-width "^4.2.0"
-    strip-ansi "^6.0.0"
-    wrap-ansi "^7.0.0"
-
-color-convert@^1.9.0:
-  version "1.9.3"
-  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
-  integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
-  dependencies:
-    color-name "1.1.3"
-
-color-convert@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
-  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
-  dependencies:
-    color-name "~1.1.4"
-
-color-name@1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
-  integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
-
-color-name@~1.1.4:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
-  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
-commander@^2.20.3:
-  version "2.20.3"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
-  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
-
-concat-map@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
-  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
-
-content-disposition@0.5.4:
-  version "0.5.4"
-  resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
-  integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
-  dependencies:
-    safe-buffer "5.2.1"
-
-content-type@~1.0.4:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
-  integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
-
-cookie-signature@1.0.6:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
-  integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
-
-cookie@0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
-  integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
-
-cors@^2.8.5:
-  version "2.8.5"
-  resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
-  integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
-  dependencies:
-    object-assign "^4"
-    vary "^1"
-
-cosmiconfig@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982"
-  integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==
-  dependencies:
-    "@types/parse-json" "^4.0.0"
-    import-fresh "^3.1.0"
-    parse-json "^5.0.0"
-    path-type "^4.0.0"
-    yaml "^1.7.2"
-
-cross-fetch@^3.1.5:
-  version "3.1.5"
-  resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
-  integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
-  dependencies:
-    node-fetch "2.6.7"
-
-cross-spawn@^7.0.3:
-  version "7.0.3"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
-  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
-  dependencies:
-    path-key "^3.1.0"
-    shebang-command "^2.0.0"
-    which "^2.0.1"
-
-crypto-hash@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247"
-  integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==
-
-debug@2.6.9, debug@^2.6.0:
-  version "2.6.9"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
-  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
-  dependencies:
-    ms "2.0.0"
-
-debug@4.3.3:
-  version "4.3.3"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
-  integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
-  dependencies:
-    ms "2.1.2"
-
-debug@^4.1.0:
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
-  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
-  dependencies:
-    ms "2.1.2"
-
-decamelize@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
-  integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
-
-deep-eql@^4.1.2:
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d"
-  integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==
-  dependencies:
-    type-detect "^4.0.0"
-
-deepmerge@^4.2.2:
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
-  integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
-
-define-lazy-prop@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
-  integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
-
-delay@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d"
-  integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==
-
-depd@2.0.0, depd@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
-  integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
-
-destroy@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
-  integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
-
-detect-port-alt@^1.1.6:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275"
-  integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==
-  dependencies:
-    address "^1.0.1"
-    debug "^2.6.0"
-
-diff@5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
-  integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
-
-diff@^3.1.0:
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
-  integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
-
-dir-glob@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
-  integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
-  dependencies:
-    path-type "^4.0.0"
-
-dot-case@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
-  integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
-  dependencies:
-    no-case "^3.0.4"
-    tslib "^2.0.3"
-
-duplexer@^0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
-  integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==
-
-ee-first@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
-  integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
-
-electron-to-chromium@^1.4.284:
-  version "1.4.335"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.335.tgz#69c08baa608bbb58e290d83320190fa82c835efe"
-  integrity sha512-l/eowQqTnrq3gu+WSrdfkhfNHnPgYqlKAwxz7MTOj6mom19vpEDHNXl6dxDxyTiYuhemydprKr/HCrHfgk+OfQ==
-
-emoji-regex@^8.0.0:
-  version "8.0.0"
-  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
-  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
-
-encodeurl@~1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
-  integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
-
-error-ex@^1.3.1:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
-  integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
-  dependencies:
-    is-arrayish "^0.2.1"
-
-es6-promise@^4.0.3:
-  version "4.2.8"
-  resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
-  integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
-
-es6-promisify@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
-  integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==
-  dependencies:
-    es6-promise "^4.0.3"
-
-escalade@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
-  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
-
-escape-html@~1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
-  integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
-
-escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
-  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
-
-escape-string-regexp@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
-  integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
-
-etag@~1.8.1:
-  version "1.8.1"
-  resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
-  integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
-
-eventemitter3@^4.0.7:
-  version "4.0.7"
-  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
-  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
-
-express@^4.18.2:
-  version "4.18.2"
-  resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
-  integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
-  dependencies:
-    accepts "~1.3.8"
-    array-flatten "1.1.1"
-    body-parser "1.20.1"
-    content-disposition "0.5.4"
-    content-type "~1.0.4"
-    cookie "0.5.0"
-    cookie-signature "1.0.6"
-    debug "2.6.9"
-    depd "2.0.0"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    etag "~1.8.1"
-    finalhandler "1.2.0"
-    fresh "0.5.2"
-    http-errors "2.0.0"
-    merge-descriptors "1.0.1"
-    methods "~1.1.2"
-    on-finished "2.4.1"
-    parseurl "~1.3.3"
-    path-to-regexp "0.1.7"
-    proxy-addr "~2.0.7"
-    qs "6.11.0"
-    range-parser "~1.2.1"
-    safe-buffer "5.2.1"
-    send "0.18.0"
-    serve-static "1.15.0"
-    setprototypeof "1.2.0"
-    statuses "2.0.1"
-    type-is "~1.6.18"
-    utils-merge "1.0.1"
-    vary "~1.1.2"
-
-eyes@^0.1.8:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
-  integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==
-
-fast-deep-equal@^3.1.1:
-  version "3.1.3"
-  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
-  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
-
-fast-glob@^3.2.9:
-  version "3.2.12"
-  resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
-  integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
-  dependencies:
-    "@nodelib/fs.stat" "^2.0.2"
-    "@nodelib/fs.walk" "^1.2.3"
-    glob-parent "^5.1.2"
-    merge2 "^1.3.0"
-    micromatch "^4.0.4"
-
-fast-json-stable-stringify@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
-  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
-
-fast-stable-stringify@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz#5c5543462b22aeeefd36d05b34e51c78cb86d313"
-  integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==
-
-fastq@^1.6.0:
-  version "1.15.0"
-  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
-  integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
-  dependencies:
-    reusify "^1.0.4"
-
-file-uri-to-path@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
-  integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
-
-filesize@^8.0.6:
-  version "8.0.7"
-  resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8"
-  integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==
-
-fill-range@^7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
-  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
-  dependencies:
-    to-regex-range "^5.0.1"
-
-finalhandler@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
-  integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==
-  dependencies:
-    debug "2.6.9"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    on-finished "2.4.1"
-    parseurl "~1.3.3"
-    statuses "2.0.1"
-    unpipe "~1.0.0"
-
-find-up@5.0.0, find-up@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
-  integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
-  dependencies:
-    locate-path "^6.0.0"
-    path-exists "^4.0.0"
-
-find-up@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
-  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
-  dependencies:
-    locate-path "^3.0.0"
-
-flat@^5.0.2:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
-  integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
-
-fork-ts-checker-webpack-plugin@^6.5.0:
-  version "6.5.3"
-  resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3"
-  integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==
-  dependencies:
-    "@babel/code-frame" "^7.8.3"
-    "@types/json-schema" "^7.0.5"
-    chalk "^4.1.0"
-    chokidar "^3.4.2"
-    cosmiconfig "^6.0.0"
-    deepmerge "^4.2.2"
-    fs-extra "^9.0.0"
-    glob "^7.1.6"
-    memfs "^3.1.2"
-    minimatch "^3.0.4"
-    schema-utils "2.7.0"
-    semver "^7.3.2"
-    tapable "^1.0.0"
-
-forwarded@0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
-  integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
-
-fresh@0.5.2:
-  version "0.5.2"
-  resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
-  integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
-
-fs-extra@^9.0.0:
-  version "9.1.0"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
-  integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
-  dependencies:
-    at-least-node "^1.0.0"
-    graceful-fs "^4.2.0"
-    jsonfile "^6.0.1"
-    universalify "^2.0.0"
-
-fs-monkey@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3"
-  integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==
-
-fs.realpath@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
-  integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
-
-fsevents@~2.3.2:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
-  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
-
-function-bind@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
-  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
-
-get-caller-file@^2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
-  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-
-get-func-name@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
-  integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==
-
-get-intrinsic@^1.0.2:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"
-  integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==
-  dependencies:
-    function-bind "^1.1.1"
-    has "^1.0.3"
-    has-symbols "^1.0.3"
-
-glob-parent@^5.1.2, glob-parent@~5.1.2:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
-  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
-  dependencies:
-    is-glob "^4.0.1"
-
-glob@7.2.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
-  integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.0.4"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
-glob@^7.1.6:
-  version "7.2.3"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
-  integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.1.1"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
-global-modules@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
-  integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==
-  dependencies:
-    global-prefix "^3.0.0"
-
-global-prefix@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97"
-  integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==
-  dependencies:
-    ini "^1.3.5"
-    kind-of "^6.0.2"
-    which "^1.3.1"
-
-globby@^11.0.4:
-  version "11.1.0"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
-  integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
-  dependencies:
-    array-union "^2.1.0"
-    dir-glob "^3.0.1"
-    fast-glob "^3.2.9"
-    ignore "^5.2.0"
-    merge2 "^1.4.1"
-    slash "^3.0.0"
-
-graceful-fs@^4.1.6, graceful-fs@^4.2.0:
-  version "4.2.11"
-  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
-  integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
-
-growl@1.10.5:
-  version "1.10.5"
-  resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
-  integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==
-
-gzip-size@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"
-  integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==
-  dependencies:
-    duplexer "^0.1.2"
-
-has-flag@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
-  integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
-
-has-flag@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
-  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
-
-has-symbols@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
-  integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
-
-has@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
-  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
-  dependencies:
-    function-bind "^1.1.1"
-
-he@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
-  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
-
-http-errors@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
-  integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
-  dependencies:
-    depd "2.0.0"
-    inherits "2.0.4"
-    setprototypeof "1.2.0"
-    statuses "2.0.1"
-    toidentifier "1.0.1"
-
-humanize-ms@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
-  integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==
-  dependencies:
-    ms "^2.0.0"
-
-iconv-lite@0.4.24:
-  version "0.4.24"
-  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
-  integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
-  dependencies:
-    safer-buffer ">= 2.1.2 < 3"
-
-ieee754@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
-  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
-
-ignore@^5.2.0:
-  version "5.2.4"
-  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
-  integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
-
-immer@^9.0.7:
-  version "9.0.19"
-  resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.19.tgz#67fb97310555690b5f9cd8380d38fc0aabb6b38b"
-  integrity sha512-eY+Y0qcsB4TZKwgQzLaE/lqYMlKhv5J9dyd2RhhtGhNo2njPXDqU9XPfcNfa3MIDsdtZt5KlkIsirlo4dHsWdQ==
-
-import-fresh@^3.1.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
-  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
-  dependencies:
-    parent-module "^1.0.0"
-    resolve-from "^4.0.0"
-
-inflight@^1.0.4:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
-  integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
-  dependencies:
-    once "^1.3.0"
-    wrappy "1"
-
-inherits@2, inherits@2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
-  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
-ini@^1.3.5:
-  version "1.3.8"
-  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
-  integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
-
-ipaddr.js@1.9.1:
-  version "1.9.1"
-  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
-  integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
-
-is-arrayish@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
-  integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
-
-is-binary-path@~2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
-  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
-  dependencies:
-    binary-extensions "^2.0.0"
-
-is-docker@^2.0.0, is-docker@^2.1.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
-  integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
-
-is-extglob@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
-  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
-
-is-fullwidth-code-point@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
-  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
-
-is-glob@^4.0.1, is-glob@~4.0.1:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
-  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
-  dependencies:
-    is-extglob "^2.1.1"
-
-is-number@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
-  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
-
-is-plain-obj@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
-  integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
-
-is-root@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c"
-  integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==
-
-is-unicode-supported@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
-  integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
-
-is-wsl@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
-  integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
-  dependencies:
-    is-docker "^2.0.0"
-
-isexe@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
-  integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
-
-isomorphic-ws@^4.0.1:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
-  integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
-
-jayson@^3.4.4:
-  version "3.7.0"
-  resolved "https://registry.yarnpkg.com/jayson/-/jayson-3.7.0.tgz#b735b12d06d348639ae8230d7a1e2916cb078f25"
-  integrity sha512-tfy39KJMrrXJ+mFcMpxwBvFDetS8LAID93+rycFglIQM4kl3uNR3W4lBLE/FFhsoUCEox5Dt2adVpDm/XtebbQ==
-  dependencies:
-    "@types/connect" "^3.4.33"
-    "@types/node" "^12.12.54"
-    "@types/ws" "^7.4.4"
-    JSONStream "^1.3.5"
-    commander "^2.20.3"
-    delay "^5.0.0"
-    es6-promisify "^5.0.0"
-    eyes "^0.1.8"
-    isomorphic-ws "^4.0.1"
-    json-stringify-safe "^5.0.1"
-    lodash "^4.17.20"
-    uuid "^8.3.2"
-    ws "^7.4.5"
-
-js-sha256@^0.9.0:
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
-  integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==
-
-js-tokens@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
-  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-
-js-yaml@4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
-  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
-  dependencies:
-    argparse "^2.0.1"
-
-json-parse-even-better-errors@^2.3.0:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
-  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
-
-json-schema-traverse@^0.4.1:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
-  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
-
-json-stringify-safe@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
-  integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
-
-json5@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
-  integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
-  dependencies:
-    minimist "^1.2.0"
-
-jsonfile@^6.0.1:
-  version "6.1.0"
-  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
-  integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
-  dependencies:
-    universalify "^2.0.0"
-  optionalDependencies:
-    graceful-fs "^4.1.6"
-
-jsonparse@^1.2.0:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
-  integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
-
-kind-of@^6.0.2:
-  version "6.0.3"
-  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
-  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
-
-kleur@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
-  integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
-
-lines-and-columns@^1.1.6:
-  version "1.2.4"
-  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
-  integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
-
-loader-utils@^3.2.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576"
-  integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==
-
-locate-path@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
-  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
-  dependencies:
-    p-locate "^3.0.0"
-    path-exists "^3.0.0"
-
-locate-path@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
-  integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
-  dependencies:
-    p-locate "^5.0.0"
-
-lodash@^4.17.20:
-  version "4.17.21"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
-  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-
-log-symbols@4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
-  integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
-  dependencies:
-    chalk "^4.1.0"
-    is-unicode-supported "^0.1.0"
-
-loupe@^2.3.1:
-  version "2.3.6"
-  resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53"
-  integrity sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==
-  dependencies:
-    get-func-name "^2.0.0"
-
-lower-case@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
-  integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
-  dependencies:
-    tslib "^2.0.3"
-
-lru-cache@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
-  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
-  dependencies:
-    yallist "^4.0.0"
-
-make-error@^1.1.1:
-  version "1.3.6"
-  resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
-  integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
-
-media-typer@0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
-  integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
-
-memfs@^3.1.2:
-  version "3.4.13"
-  resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.13.tgz#248a8bd239b3c240175cd5ec548de5227fc4f345"
-  integrity sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg==
-  dependencies:
-    fs-monkey "^1.0.3"
-
-merge-descriptors@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
-  integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
-
-merge2@^1.3.0, merge2@^1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
-  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
-
-methods@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
-  integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
-
-micromatch@^4.0.4:
-  version "4.0.5"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
-  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
-  dependencies:
-    braces "^3.0.2"
-    picomatch "^2.3.1"
-
-mime-db@1.52.0:
-  version "1.52.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
-  integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
-
-mime-types@~2.1.24, mime-types@~2.1.34:
-  version "2.1.35"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
-  integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
-  dependencies:
-    mime-db "1.52.0"
-
-mime@1.6.0:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
-  integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
-
-minimatch@4.2.1:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4"
-  integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==
-  dependencies:
-    brace-expansion "^1.1.7"
-
-minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
-  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
-  dependencies:
-    brace-expansion "^1.1.7"
-
-minimist@^1.2.0, minimist@^1.2.6:
-  version "1.2.8"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
-  integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
-
-mkdirp@^0.5.1:
-  version "0.5.6"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
-  integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
-  dependencies:
-    minimist "^1.2.6"
-
-mocha@^9.0.3:
-  version "9.2.2"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9"
-  integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==
-  dependencies:
-    "@ungap/promise-all-settled" "1.1.2"
-    ansi-colors "4.1.1"
-    browser-stdout "1.3.1"
-    chokidar "3.5.3"
-    debug "4.3.3"
-    diff "5.0.0"
-    escape-string-regexp "4.0.0"
-    find-up "5.0.0"
-    glob "7.2.0"
-    growl "1.10.5"
-    he "1.2.0"
-    js-yaml "4.1.0"
-    log-symbols "4.1.0"
-    minimatch "4.2.1"
-    ms "2.1.3"
-    nanoid "3.3.1"
-    serialize-javascript "6.0.0"
-    strip-json-comments "3.1.1"
-    supports-color "8.1.1"
-    which "2.0.2"
-    workerpool "6.2.0"
-    yargs "16.2.0"
-    yargs-parser "20.2.4"
-    yargs-unparser "2.0.0"
-
-ms@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
-  integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
-
-ms@2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
-  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-
-ms@2.1.3, ms@^2.0.0:
-  version "2.1.3"
-  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
-  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-
-nanoid@3.3.1:
-  version "3.3.1"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
-  integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
-
-nanoid@^3.3.4:
-  version "3.3.4"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
-  integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
-
-negotiator@0.6.3:
-  version "0.6.3"
-  resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
-  integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
-
-no-case@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
-  integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
-  dependencies:
-    lower-case "^2.0.2"
-    tslib "^2.0.3"
-
-node-fetch@2.6.7:
-  version "2.6.7"
-  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
-  integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
-  dependencies:
-    whatwg-url "^5.0.0"
-
-node-fetch@^2.6.7:
-  version "2.6.9"
-  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
-  integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==
-  dependencies:
-    whatwg-url "^5.0.0"
-
-node-gyp-build@^4.3.0:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055"
-  integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==
-
-node-releases@^2.0.8:
-  version "2.0.10"
-  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
-  integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
-
-normalize-path@^3.0.0, normalize-path@~3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
-  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
-
-object-assign@^4:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
-  integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
-
-object-inspect@^1.9.0:
-  version "1.12.3"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
-  integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
-
-on-finished@2.4.1:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
-  integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
-  dependencies:
-    ee-first "1.1.1"
-
-once@^1.3.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
-  integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
-  dependencies:
-    wrappy "1"
-
-open@^8.4.0:
-  version "8.4.2"
-  resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9"
-  integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==
-  dependencies:
-    define-lazy-prop "^2.0.0"
-    is-docker "^2.1.1"
-    is-wsl "^2.2.0"
-
-p-limit@^2.0.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
-  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
-  dependencies:
-    p-try "^2.0.0"
-
-p-limit@^3.0.2:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
-  integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
-  dependencies:
-    yocto-queue "^0.1.0"
-
-p-locate@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
-  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
-  dependencies:
-    p-limit "^2.0.0"
-
-p-locate@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
-  integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
-  dependencies:
-    p-limit "^3.0.2"
-
-p-try@^2.0.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
-  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
-
-pako@^2.0.3:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
-  integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
-
-parent-module@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
-  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
-  dependencies:
-    callsites "^3.0.0"
-
-parse-json@^5.0.0:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
-  integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
-  dependencies:
-    "@babel/code-frame" "^7.0.0"
-    error-ex "^1.3.1"
-    json-parse-even-better-errors "^2.3.0"
-    lines-and-columns "^1.1.6"
-
-parseurl@~1.3.3:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
-  integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
-
-path-exists@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
-  integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==
-
-path-exists@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
-  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
-
-path-is-absolute@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
-  integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
-
-path-key@^3.1.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
-  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
-
-path-to-regexp@0.1.7:
-  version "0.1.7"
-  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
-  integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
-
-path-type@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
-  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
-
-pathval@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d"
-  integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==
-
-picocolors@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
-  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
-
-picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
-  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
-
-pkg-up@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5"
-  integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==
-  dependencies:
-    find-up "^3.0.0"
-
-prettier@^2.6.2:
-  version "2.8.5"
-  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.5.tgz#3dd8ae1ebddc4f6aa419c9b64d8c8319a7e0d982"
-  integrity sha512-3gzuxrHbKUePRBB4ZeU08VNkUcqEHaUaouNt0m7LGP4Hti/NuB07C7PPTM/LkWqXoJYJn2McEo5+kxPNrtQkLQ==
-
-prompts@^2.4.2:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
-  integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==
-  dependencies:
-    kleur "^3.0.3"
-    sisteransi "^1.0.5"
-
-proxy-addr@~2.0.7:
-  version "2.0.7"
-  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
-  integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
-  dependencies:
-    forwarded "0.2.0"
-    ipaddr.js "1.9.1"
-
-punycode@^2.1.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
-  integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
-
-qs@6.11.0:
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
-  integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
-  dependencies:
-    side-channel "^1.0.4"
-
-queue-microtask@^1.2.2:
-  version "1.2.3"
-  resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
-  integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
-
-randombytes@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
-  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
-  dependencies:
-    safe-buffer "^5.1.0"
-
-range-parser@~1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
-  integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
-
-raw-body@2.5.1:
-  version "2.5.1"
-  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
-  integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
-  dependencies:
-    bytes "3.1.2"
-    http-errors "2.0.0"
-    iconv-lite "0.4.24"
-    unpipe "1.0.0"
-
-react-dev-utils@^12.0.1:
-  version "12.0.1"
-  resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73"
-  integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==
-  dependencies:
-    "@babel/code-frame" "^7.16.0"
-    address "^1.1.2"
-    browserslist "^4.18.1"
-    chalk "^4.1.2"
-    cross-spawn "^7.0.3"
-    detect-port-alt "^1.1.6"
-    escape-string-regexp "^4.0.0"
-    filesize "^8.0.6"
-    find-up "^5.0.0"
-    fork-ts-checker-webpack-plugin "^6.5.0"
-    global-modules "^2.0.0"
-    globby "^11.0.4"
-    gzip-size "^6.0.0"
-    immer "^9.0.7"
-    is-root "^2.1.0"
-    loader-utils "^3.2.0"
-    open "^8.4.0"
-    pkg-up "^3.1.0"
-    prompts "^2.4.2"
-    react-error-overlay "^6.0.11"
-    recursive-readdir "^2.2.2"
-    shell-quote "^1.7.3"
-    strip-ansi "^6.0.1"
-    text-table "^0.2.0"
-
-react-error-overlay@^6.0.11:
-  version "6.0.11"
-  resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
-  integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
-
-readdirp@~3.6.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
-  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
-  dependencies:
-    picomatch "^2.2.1"
-
-recursive-readdir@^2.2.2:
-  version "2.2.3"
-  resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372"
-  integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==
-  dependencies:
-    minimatch "^3.0.5"
-
-regenerator-runtime@^0.13.11:
-  version "0.13.11"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
-  integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
-
-require-directory@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
-  integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
-
-resolve-from@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
-  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
-
-reusify@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
-  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
-
-rpc-websockets@^7.5.1:
-  version "7.5.1"
-  resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.5.1.tgz#e0a05d525a97e7efc31a0617f093a13a2e10c401"
-  integrity sha512-kGFkeTsmd37pHPMaHIgN1LVKXMi0JD782v4Ds9ZKtLlwdTKjn+CxM9A9/gLT2LaOuEcEFGL98h1QWQtlOIdW0w==
-  dependencies:
-    "@babel/runtime" "^7.17.2"
-    eventemitter3 "^4.0.7"
-    uuid "^8.3.2"
-    ws "^8.5.0"
-  optionalDependencies:
-    bufferutil "^4.0.1"
-    utf-8-validate "^5.0.2"
-
-run-parallel@^1.1.9:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
-  integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
-  dependencies:
-    queue-microtask "^1.2.2"
-
-safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0:
-  version "5.2.1"
-  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
-  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
-
-"safer-buffer@>= 2.1.2 < 3":
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
-  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
-
-schema-utils@2.7.0:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7"
-  integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==
-  dependencies:
-    "@types/json-schema" "^7.0.4"
-    ajv "^6.12.2"
-    ajv-keywords "^3.4.1"
-
-semver@^7.3.2:
-  version "7.3.8"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
-  integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
-  dependencies:
-    lru-cache "^6.0.0"
-
-send@0.18.0:
-  version "0.18.0"
-  resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
-  integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==
-  dependencies:
-    debug "2.6.9"
-    depd "2.0.0"
-    destroy "1.2.0"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    etag "~1.8.1"
-    fresh "0.5.2"
-    http-errors "2.0.0"
-    mime "1.6.0"
-    ms "2.1.3"
-    on-finished "2.4.1"
-    range-parser "~1.2.1"
-    statuses "2.0.1"
-
-serialize-javascript@6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
-  integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
-  dependencies:
-    randombytes "^2.1.0"
-
-serve-static@1.15.0:
-  version "1.15.0"
-  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
-  integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
-  dependencies:
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    parseurl "~1.3.3"
-    send "0.18.0"
-
-setprototypeof@1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
-  integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
-
-shebang-command@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
-  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
-  dependencies:
-    shebang-regex "^3.0.0"
-
-shebang-regex@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
-  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
-
-shell-quote@^1.7.3:
-  version "1.8.0"
-  resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba"
-  integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==
-
-side-channel@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
-  integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
-  dependencies:
-    call-bind "^1.0.0"
-    get-intrinsic "^1.0.2"
-    object-inspect "^1.9.0"
-
-sisteransi@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
-  integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
-
-slash@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
-  integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
-
-snake-case@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
-  integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==
-  dependencies:
-    dot-case "^3.0.4"
-    tslib "^2.0.3"
-
-source-map-support@^0.5.6:
-  version "0.5.21"
-  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
-  integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
-  dependencies:
-    buffer-from "^1.0.0"
-    source-map "^0.6.0"
-
-source-map@^0.6.0:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
-  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
-
-statuses@2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
-  integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
-
-string-width@^4.1.0, string-width@^4.2.0:
-  version "4.2.3"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
-  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
-  dependencies:
-    emoji-regex "^8.0.0"
-    is-fullwidth-code-point "^3.0.0"
-    strip-ansi "^6.0.1"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
-  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
-  dependencies:
-    ansi-regex "^5.0.1"
-
-strip-bom@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
-  integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
-
-strip-json-comments@3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
-  integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
-
-superstruct@^0.14.2:
-  version "0.14.2"
-  resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b"
-  integrity sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==
-
-superstruct@^0.15.4:
-  version "0.15.5"
-  resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.15.5.tgz#0f0a8d3ce31313f0d84c6096cd4fa1bfdedc9dab"
-  integrity sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==
-
-supports-color@8.1.1:
-  version "8.1.1"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
-  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
-  dependencies:
-    has-flag "^4.0.0"
-
-supports-color@^5.3.0:
-  version "5.5.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
-  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
-  dependencies:
-    has-flag "^3.0.0"
-
-supports-color@^7.1.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
-  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
-  dependencies:
-    has-flag "^4.0.0"
-
-tapable@^1.0.0:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
-  integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
-
-text-encoding-utf-8@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13"
-  integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==
-
-text-table@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
-  integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
-
-"through@>=2.2.7 <3":
-  version "2.3.8"
-  resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
-  integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
-
-to-regex-range@^5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
-  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
-  dependencies:
-    is-number "^7.0.0"
-
-toidentifier@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
-  integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
-
-toml@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee"
-  integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==
-
-tr46@~0.0.3:
-  version "0.0.3"
-  resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
-  integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
-
-ts-mocha@^10.0.0:
-  version "10.0.0"
-  resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-10.0.0.tgz#41a8d099ac90dbbc64b06976c5025ffaebc53cb9"
-  integrity sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw==
-  dependencies:
-    ts-node "7.0.1"
-  optionalDependencies:
-    tsconfig-paths "^3.5.0"
-
-ts-node@7.0.1:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf"
-  integrity sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==
-  dependencies:
-    arrify "^1.0.0"
-    buffer-from "^1.1.0"
-    diff "^3.1.0"
-    make-error "^1.1.1"
-    minimist "^1.2.0"
-    mkdirp "^0.5.1"
-    source-map-support "^0.5.6"
-    yn "^2.0.0"
-
-tsconfig-paths@^3.5.0:
-  version "3.14.2"
-  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"
-  integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==
-  dependencies:
-    "@types/json5" "^0.0.29"
-    json5 "^1.0.2"
-    minimist "^1.2.6"
-    strip-bom "^3.0.0"
-
-tslib@^2.0.3:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
-  integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
-
-type-detect@^4.0.0, type-detect@^4.0.5:
-  version "4.0.8"
-  resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
-  integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
-
-type-is@~1.6.18:
-  version "1.6.18"
-  resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
-  integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
-  dependencies:
-    media-typer "0.3.0"
-    mime-types "~2.1.24"
-
-typescript@^4.3.5:
-  version "4.9.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
-  integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
-
-universalify@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
-  integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
-
-unpipe@1.0.0, unpipe@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
-  integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
-
-update-browserslist-db@^1.0.10:
-  version "1.0.10"
-  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3"
-  integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==
-  dependencies:
-    escalade "^3.1.1"
-    picocolors "^1.0.0"
-
-uri-js@^4.2.2:
-  version "4.4.1"
-  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
-  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
-  dependencies:
-    punycode "^2.1.0"
-
-utf-8-validate@^5.0.2:
-  version "5.0.10"
-  resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2"
-  integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==
-  dependencies:
-    node-gyp-build "^4.3.0"
-
-utils-merge@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
-  integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
-
-uuid@^8.3.2:
-  version "8.3.2"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
-  integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
-
-vary@^1, vary@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
-  integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
-
-webidl-conversions@^3.0.0:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
-  integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
-
-whatwg-url@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
-  integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
-  dependencies:
-    tr46 "~0.0.3"
-    webidl-conversions "^3.0.0"
-
-which@2.0.2, which@^2.0.1:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
-  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
-  dependencies:
-    isexe "^2.0.0"
-
-which@^1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
-  integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
-  dependencies:
-    isexe "^2.0.0"
-
-workerpool@6.2.0:
-  version "6.2.0"
-  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b"
-  integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==
-
-wrap-ansi@^7.0.0:
-  version "7.0.0"
-  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
-  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
-  dependencies:
-    ansi-styles "^4.0.0"
-    string-width "^4.1.0"
-    strip-ansi "^6.0.0"
-
-wrappy@1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
-  integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
-
-ws@^7.4.5:
-  version "7.5.9"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
-  integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
-
-ws@^8.5.0:
-  version "8.13.0"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
-  integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
-
-y18n@^5.0.5:
-  version "5.0.8"
-  resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
-  integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
-
-yallist@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
-  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-
-yaml@^1.7.2:
-  version "1.10.2"
-  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
-  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
-
-yargs-parser@20.2.4:
-  version "20.2.4"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
-  integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
-
-yargs-parser@^20.2.2:
-  version "20.2.9"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
-  integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-
-yargs-unparser@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
-  integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==
-  dependencies:
-    camelcase "^6.0.0"
-    decamelize "^4.0.0"
-    flat "^5.0.2"
-    is-plain-obj "^2.1.0"
-
-yargs@16.2.0:
-  version "16.2.0"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
-  integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
-  dependencies:
-    cliui "^7.0.2"
-    escalade "^3.1.1"
-    get-caller-file "^2.0.5"
-    require-directory "^2.1.1"
-    string-width "^4.2.0"
-    y18n "^5.0.5"
-    yargs-parser "^20.2.2"
-
-yn@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"
-  integrity sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ==
-
-yocto-queue@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
-  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
-
-zod@^3.20.2:
-  version "3.21.4"
-  resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
-  integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 303 - 408
package-lock.json


+ 4 - 0
package.json

@@ -8,15 +8,19 @@
     "price_service/server",
     "price_service/sdk/js",
     "price_service/client/js",
+    "pythnet/message_buffer",
     "target_chains/aptos/sdk/js",
     "target_chains/cosmwasm/sdk/js",
+    "target_chains/cosmwasm/tools",
     "target_chains/ethereum/contracts",
     "target_chains/ethereum/sdk/js",
     "target_chains/ethereum/sdk/solidity",
+    "target_chains/ethereum/examples/oracle_swap/app",
     "third_party/pyth/p2w-relay",
     "wormhole_attester/sdk/js"
   ],
   "dependencies": {
+    "pre-commit": "^1.2.2",
     "prettier": "2.7.1"
   },
   "devDependencies": {

+ 36 - 2
price_pusher/README.md

@@ -37,9 +37,20 @@ The parameters above are configured per price feed in a price configuration YAML
   time_difference: 60 # Time difference threshold (in seconds) to push a newer price feed.
   price_deviation: 0.5 # The price deviation (%) threshold to push a newer price feed.
   confidence_ratio: 1 # The confidence/price (%) threshold to push a newer price feed.
+  # Optional block to configure whether this feed can be early updated. If at least one feed meets the
+  # triggering conditions above, all other feeds who meet the early update conditions will be included in
+  # the submitted batch of prices. This logic takes advantage of the fact that adding a feed to a larger
+  # batch of updates incurs a minimal gas cost. All fields below are optional (and interpreted as infinity if omitted)
+  # and have the same semantics as the corresponding fields above.
+  early_update:
+    time_difference: 30
+    price_deviation: 0.1
+    confidence_ratio: 0.5
 - ...
 ```
 
+Two sample YAML configuration files are available in the root of this repo.
+
 You can get the list of available price feeds from
 [here](https://pyth.network/developers/price-feed-ids/).
 
@@ -57,7 +68,7 @@ cd price_pusher
 npm run start -- evm --endpoint wss://example-rpc.com \
     --pyth-contract-address 0xff1a0f4744e8582DF...... \
     --price-service-endpoint https://example-pyth-price.com \
-    --price-config-file "path/to/price-config-file.yaml.testnet.sample.yaml" \
+    --price-config-file "path/to/price-config.testnet.sample.yaml" \
     --mnemonic-file "path/to/mnemonic.txt" \
     [--pushing-frequency 10] \
     [--polling-frequency 5] \
@@ -66,11 +77,34 @@ npm run start -- evm --endpoint wss://example-rpc.com \
 # For Injective
 npm run start -- injective --grpc-endpoint https://grpc-endpoint.com \
     --pyth-contract-address inj1z60tg0... --price-service-endpoint "https://example-pyth-price.com" \
-    --price-config-file "path/to/price-config-file.yaml.testnet.sample.yaml" \
+    --price-config-file "path/to/price-config.testnet.sample.yaml" \
+    --mnemonic-file "path/to/mnemonic.txt" \
+    [--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] \
 
+# For Sui
+npm run start -- sui \
+  --endpoint https://sui-testnet-rpc.allthatnode.com \
+  --pyth-package-id 0x975e063f398f720af4f33ec06a927f14ea76ca24f7f8dd544aa62ab9d5d15f44 \
+  --pyth-state-id 0xd8afde3a48b4ff7212bd6829a150f43f59043221200d63504d981f62bff2e27a \
+  --wormhole-package-id 0xcc029e2810f17f9f43f52262f40026a71fbdca40ed3803ad2884994361910b7e \
+  --wormhole-state-id 0xebba4cc4d614f7a7cdbe883acc76d1cc767922bc96778e7b68be0d15fce27c02 \
+  --price-feed-to-price-info-object-table-id 0xf8929174008c662266a1adde78e1e8e33016eb7ad37d379481e860b911e40ed5 \
+  --price-service-endpoint https://xc-testnet.pyth.network \
+  --mnemonic-file ./mnemonic \
+  --price-config-file ./price-config.testnet.sample.yaml \
+  [--pushing-frequency 10] \
+  [--polling-frequency 5] \
+  [--num-gas-objects 30]
+
 
 # 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"
+}

+ 11 - 0
price_pusher/config.sui.mainnet.sample.json

@@ -0,0 +1,11 @@
+{
+  "endpoint": "https://sui-testnet-rpc.allthatnode.com",
+  "pyth-package-id": "0x00b53b0f4174108627fbee72e2498b58d6a2714cded53fac537034c220d26302",
+  "pyth-state-id": "0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f",
+  "wormhole-package-id": "0x5306f64e312b581766351c07af79c72fcb1cd25147157fdc2f8ad76de9a3fb6a",
+  "wormhole-state-id": "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c",
+  "price-feed-to-price-info-object-table-id": "0x14b4697477d24c30c8eecc31dd1bd49a3115a9fe0db6bd4fd570cf14640b79a0",
+  "price-service-endpoint": "https://xc-mainnet.pyth.network",
+  "mnemonic-file": "./mnemonic",
+  "price-config-file": "./price-config.mainnet.sample.yaml"
+}

+ 11 - 0
price_pusher/config.sui.testnet.sample.json

@@ -0,0 +1,11 @@
+{
+  "endpoint": "https://sui-testnet-rpc.allthatnode.com",
+  "pyth-package-id": "0x975e063f398f720af4f33ec06a927f14ea76ca24f7f8dd544aa62ab9d5d15f44",
+  "pyth-state-id": "0xd8afde3a48b4ff7212bd6829a150f43f59043221200d63504d981f62bff2e27a",
+  "wormhole-package-id": "0xcc029e2810f17f9f43f52262f40026a71fbdca40ed3803ad2884994361910b7e",
+  "wormhole-state-id": "0xebba4cc4d614f7a7cdbe883acc76d1cc767922bc96778e7b68be0d15fce27c02",
+  "price-feed-to-price-info-object-table-id": "0xf8929174008c662266a1adde78e1e8e33016eb7ad37d379481e860b911e40ed5",
+  "price-service-endpoint": "https://xc-testnet.pyth.network",
+  "mnemonic-file": "./mnemonic",
+  "price-config-file": "./price-config.testnet.sample.yaml"
+}

+ 3 - 2
price_pusher/docker-compose.mainnet.sample.yaml

@@ -1,7 +1,7 @@
 services:
   spy:
     # Find latest Guardian images in https://github.com/wormhole-foundation/wormhole/pkgs/container/guardiand
-    image: ghcr.io/wormhole-foundation/guardiand:v2.14.8.1
+    image: ghcr.io/wormhole-foundation/guardiand:v2.17.0
     command:
       - "spy"
       - "--nodeKey"
@@ -16,7 +16,7 @@ services:
       - "warn"
   price-service:
     # Find latest price service images https://gallery.ecr.aws/pyth-network/xc-server
-    image: public.ecr.aws/pyth-network/xc-server:v3.0.0
+    image: public.ecr.aws/pyth-network/xc-server:v3.0.3
     environment:
       SPY_SERVICE_HOST: "spy:7072"
       SPY_SERVICE_FILTERS: |
@@ -35,6 +35,7 @@ services:
       READINESS_SPY_SYNC_TIME_SECONDS: "20"
       READINESS_NUM_LOADED_SYMBOLS: "50"
       LOG_LEVEL: warning
+      WORMHOLE_CLUSTER: mainnet
     healthcheck:
       test:
         [

+ 3 - 2
price_pusher/docker-compose.testnet.sample.yaml

@@ -1,7 +1,7 @@
 services:
   spy:
     # Find latest Guardian images in https://github.com/wormhole-foundation/wormhole/pkgs/container/guardiand
-    image: ghcr.io/wormhole-foundation/guardiand:v2.14.8.1
+    image: ghcr.io/wormhole-foundation/guardiand:v2.17.0
     command:
       - "spy"
       - "--nodeKey"
@@ -16,7 +16,7 @@ services:
       - "warn"
   price-service:
     # Find latest price service images https://gallery.ecr.aws/pyth-network/xc-server
-    image: public.ecr.aws/pyth-network/xc-server:v3.0.0
+    image: public.ecr.aws/pyth-network/xc-server:v3.0.3
     environment:
       SPY_SERVICE_HOST: "spy:7072"
       SPY_SERVICE_FILTERS: |
@@ -35,6 +35,7 @@ services:
       READINESS_SPY_SYNC_TIME_SECONDS: "20"
       READINESS_NUM_LOADED_SYMBOLS: "50"
       LOG_LEVEL: warning
+      WORMHOLE_CLUSTER: testnet
     healthcheck:
       test:
         [

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác