diff --git a/.github/workflows/push-attester-image.yml b/.github/workflows/push-attester-image.yml deleted file mode 100644 index 419bbefb..00000000 --- a/.github/workflows/push-attester-image.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Build and Push Attester Image -on: - push: - tags: - - pyth-wormhole-attester-v* - workflow_dispatch: - inputs: - dispatch_description: - description: "Dispatch description" - required: true - type: string -permissions: - contents: read - id-token: write -jobs: - p2w-attest-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/pyth-wormhole-attester-v') }} - run: | - PREFIX="refs/tags/pyth-wormhole-attester-" - 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/pyth-wormhole-attester-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 wormhole-attester/client/Dockerfile.p2w-attest . - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - env: - ECR_REGISTRY: public.ecr.aws - ECR_REPOSITORY: pyth-network/xc-attest diff --git a/.github/workflows/push-price-server-image.yml b/.github/workflows/push-price-server-image.yml deleted file mode 100644 index 2a67a110..00000000 --- a/.github/workflows/push-price-server-image.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Build and Push Price Service Image -on: - push: - tags: - - pyth-price-server-v* - workflow_dispatch: - inputs: - dispatch_description: - description: "Dispatch description" - required: true - type: string -permissions: - contents: read - id-token: write -jobs: - price-server-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/pyth-price-server-v') }} - run: | - PREFIX="refs/tags/pyth-price-server-" - 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/pyth-price-server-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 lerna -f Dockerfile.lerna . - DOCKER_BUILDKIT=1 docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f price_service/server/Dockerfile . - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - env: - ECR_REGISTRY: public.ecr.aws - ECR_REPOSITORY: pyth-network/xc-server diff --git a/governance/xc_admin/packages/proposer_server/package.json b/governance/xc_admin/packages/proposer_server/package.json index 104ef993..6f98bddf 100644 --- a/governance/xc_admin/packages/proposer_server/package.json +++ b/governance/xc_admin/packages/proposer_server/package.json @@ -23,6 +23,7 @@ "@pythnetwork/client": "^2.17.0", "@solana/web3.js": "^1.76.0", "@sqds/mesh": "^1.0.6", + "@types/cors": "^2.8.17", "cors": "^2.8.5", "ts-node": "^10.9.1", "xc_admin_common": "*" diff --git a/package-lock.json b/package-lock.json index f0931374..4e93c6df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "governance/xc_admin/packages/*", "governance/multisig_wh_message_builder", "price_pusher", - "price_service/server", "price_service/sdk/js", "price_service/client/js", "pythnet/message_buffer", @@ -24,7 +23,6 @@ "target_chains/ethereum/examples/oracle_swap/app", "target_chains/sui/sdk/js", "target_chains/sui/cli", - "wormhole_attester/sdk/js", "contract_manager" ], "dependencies": { @@ -1829,6 +1827,7 @@ "@pythnetwork/client": "^2.17.0", "@solana/web3.js": "^1.76.0", "@sqds/mesh": "^1.0.6", + "@types/cors": "^2.8.17", "cors": "^2.8.5", "ts-node": "^10.9.1", "xc_admin_common": "*" @@ -5190,40 +5189,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/@certusone/wormhole-spydk": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@certusone/wormhole-spydk/-/wormhole-spydk-0.0.1.tgz", - "integrity": "sha512-iBQoY3UnmGoWHcbn0FypA6hKsANhdHKi03UN0GPoDAeMY12j8ly+7r462TfLl5f4hOJVQd3UZ2qviohEmdicmg==", - "dependencies": { - "@grpc/grpc-js": "^1.4.4", - "protobufjs": "^6.11.2" - } - }, - "node_modules/@certusone/wormhole-spydk/node_modules/protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - }, "node_modules/@chain-registry/types": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/@chain-registry/types/-/types-0.16.0.tgz", @@ -5279,14 +5244,6 @@ "pbts": "bin/pbts" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/@confio/ics23": { "version": "0.6.8", "resolved": "https://registry.npmjs.org/@confio/ics23/-/ics23-0.6.8.tgz", @@ -5879,16 +5836,6 @@ "postcss-selector-parser": "^6.0.10" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, "node_modules/@ensdomains/address-encoder": { "version": "0.1.9", "resolved": "https://registry.npmjs.org/@ensdomains/address-encoder/-/address-encoder-0.1.9.tgz", @@ -6934,36 +6881,6 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, - "node_modules/@grpc/grpc-js": { - "version": "1.8.7", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.7.tgz", - "integrity": "sha512-dRAWjRFN1Zy9mzPNLkFFIWT8T6C9euwluzCHZUKuhC+Bk3MayNPcpgDRyG+sg+n2sitEUySKxUynirVpu9ItKw==", - "dependencies": { - "@grpc/proto-loader": "^0.7.0", - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", - "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", - "dependencies": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^7.0.0", - "yargs": "^16.2.0" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@hapi/bourne": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-2.1.0.tgz", @@ -11785,10 +11702,6 @@ "resolved": "price_service/sdk/js", "link": true }, - "node_modules/@pythnetwork/price-service-server": { - "resolved": "price_service/server", - "link": true - }, "node_modules/@pythnetwork/pyth-aptos-js": { "resolved": "target_chains/aptos/sdk/js", "link": true @@ -11813,10 +11726,6 @@ "resolved": "target_chains/sui/sdk/js", "link": true }, - "node_modules/@pythnetwork/wormhole-attester-sdk": { - "resolved": "wormhole_attester/sdk/js", - "link": true - }, "node_modules/@radix-ui/primitive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", @@ -18870,16 +18779,10 @@ "@types/node": "*" } }, - "node_modules/@types/cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", - "dev": true - }, "node_modules/@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dependencies": { "@types/node": "*" } @@ -18957,11 +18860,6 @@ "@types/node": "*" } }, - "node_modules/@types/hapi__joi": { - "version": "16.0.12", - "resolved": "https://registry.npmjs.org/@types/hapi__joi/-/hapi__joi-16.0.12.tgz", - "integrity": "sha512-xJYifuz59jXdWY5JMS15uvA3ycS3nQYOGqoIIE0+fwQ0qI3/4CxBc6RHsOTp6wk9M0NWEdpcTl02lOQOKMifbQ==" - }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -19020,15 +18918,6 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, - "node_modules/@types/keccak": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/keccak/-/keccak-3.0.1.tgz", - "integrity": "sha512-/MxAVmtyyeOvZ6dGf3ciLwFRuV5M8DRIyYNFGHYI6UyBW4/XqyO0LZw+JFMvaeY3cHItQAkELclBU1x5ank6mg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -19083,14 +18972,6 @@ "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", "dev": true }, - "node_modules/@types/morgan": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.4.tgz", - "integrity": "sha512-cXoc4k+6+YAllH3ZHmx4hf7La1dzUk6keTR4bF4b4Sc0mZxU/zK4wO7l+ZzezXm/jkYj/qC+uYGZrarZdIVvyQ==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node": { "version": "18.11.18", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", @@ -19196,15 +19077,6 @@ "@types/node": "*" } }, - "node_modules/@types/response-time": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@types/response-time/-/response-time-2.3.5.tgz", - "integrity": "sha512-4ANzp+I3K7sztFFAGPALWBvSl4ayaDSKzI2Bok+WNz+en2eB2Pvk6VCjR47PBXBWOkEg2r4uWpZOlXA5DNINOQ==", - "dependencies": { - "@types/express": "*", - "@types/node": "*" - } - }, "node_modules/@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -19266,25 +19138,6 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, - "node_modules/@types/superagent": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.16.tgz", - "integrity": "sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ==", - "dev": true, - "dependencies": { - "@types/cookiejar": "*", - "@types/node": "*" - } - }, - "node_modules/@types/supertest": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.12.tgz", - "integrity": "sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ==", - "dev": true, - "dependencies": { - "@types/superagent": "*" - } - }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.5", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", @@ -22132,22 +21985,6 @@ "node": ">=6.0.0" } }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -22301,11 +22138,6 @@ "file-uri-to-path": "1.0.0" } }, - "node_modules/bintrees": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", - "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" - }, "node_modules/bip32": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/bip32/-/bip32-2.0.6.tgz", @@ -22736,15 +22568,6 @@ "node": ">=10.0.0" } }, - "node_modules/builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/builtins": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", @@ -23721,15 +23544,6 @@ "node": ">=0.10.0" } }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -23746,15 +23560,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -23764,19 +23569,6 @@ "color-support": "bin.js" } }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -23797,15 +23589,6 @@ "node": ">=0.1.90" } }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, "node_modules/columnify": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz", @@ -23927,7 +23710,8 @@ "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "peer": true }, "node_modules/compressible": { "version": "2.0.18", @@ -24355,12 +24139,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true - }, "node_modules/copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -24370,12 +24148,6 @@ "node": ">=0.10.0" } }, - "node_modules/copy-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/copy-dir/-/copy-dir-1.3.0.tgz", - "integrity": "sha512-Q4+qBFnN4bwGwvtXXzbp4P/4iNk0MaiGAzvQ8OiMtlLjkIKjmNN689uVzShSM0908q7GoFHXIPx4zi75ocoaHw==", - "dev": true - }, "node_modules/copy-to-clipboard": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", @@ -26241,11 +26013,6 @@ "node": ">= 4" } }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" - }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -28753,16 +28520,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express-validation": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/express-validation/-/express-validation-4.1.0.tgz", - "integrity": "sha512-bAGRVdaJZ7yzCxELX29zvvJk0OUP/tw6tCtZtGnpK1O+yLY9qN8btwtclRQMGfIluZYIuxe++U/yPMlgEpU12A==", - "dependencies": { - "@types/express": "^4.17.13", - "@types/hapi__joi": "16.x.x", - "joi": "^17.6.0" - } - }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -29004,11 +28761,6 @@ "bser": "2.1.1" } }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" - }, "node_modules/fetch-cookie": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-0.11.0.tgz", @@ -29181,15 +28933,6 @@ "node": ">= 0.8" } }, - "node_modules/find": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/find/-/find-0.3.0.tgz", - "integrity": "sha512-iSd+O4OEYV/I36Zl8MdYJO0xD82wH528SaCieTVHhclgiYNe9y+yPKSwK+A7/WsmHL1EZ+pYUJBXWTL5qofksw==", - "dev": true, - "dependencies": { - "traverse-chain": "~0.1.0" - } - }, "node_modules/find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", @@ -29344,11 +29087,6 @@ "node": ">=0.4.0" } }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" - }, "node_modules/follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -29540,21 +29278,6 @@ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.1.tgz", "integrity": "sha512-EFRDrsMm/kyqbTQocNvRXMLjc7Es2Vk+IQFx/YW7hkUH1eBl4J1fqiP34l74Yt0pFLCNpc06fkbVk00008mzjg==" }, - "node_modules/formidable": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", - "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", - "dev": true, - "dependencies": { - "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", - "once": "^1.4.0", - "qs": "^6.11.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -31596,15 +31319,6 @@ "node": ">= 8" } }, - "node_modules/hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/hi-base32": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz", @@ -35539,11 +35253,6 @@ "node": ">= 8" } }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" - }, "node_modules/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -36256,7 +35965,8 @@ "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -36331,18 +36041,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/logform": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", - "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", - "dependencies": { - "@colors/colors": "1.5.0", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - } - }, "node_modules/logkitty": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", @@ -36501,6 +36199,7 @@ "version": "7.14.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.1.tgz", "integrity": "sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==", + "dev": true, "engines": { "node": ">=12" } @@ -37957,6 +37656,7 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "peer": true, "bin": { "mime": "cli.js" }, @@ -38641,53 +38341,6 @@ "node": "*" } }, - "node_modules/morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/morgan/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/morgan/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/morgan/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/mri": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", @@ -40159,14 +39812,6 @@ "wrappy": "1" } }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "dependencies": { - "fn.name": "1.x.x" - } - }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -42784,17 +42429,6 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "node_modules/prom-client": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.1.1.tgz", - "integrity": "sha512-hFU32q7UZQ59bVJQGUtm3I2PrJ3gWvoCkilX9sF165ks1qflhugVCeK+S1JjJYHvyt3o5kj68+q3bchormjnzw==", - "dependencies": { - "tdigest": "^0.1.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/promise": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", @@ -46810,18 +46444,6 @@ "node": ">=0.8" } }, - "node_modules/response-time": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.2.tgz", - "integrity": "sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw==", - "dependencies": { - "depd": "~1.1.0", - "on-headers": "~1.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -47168,6 +46790,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz", "integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==", + "dev": true, "engines": { "node": ">=10" } @@ -47810,19 +47433,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -48967,14 +48577,6 @@ "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "engines": { - "node": "*" - } - }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -49542,45 +49144,11 @@ "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==", "peer": true }, - "node_modules/superagent": { - "version": "8.0.9", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz", - "integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==", - "dev": true, - "dependencies": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.1.2", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=6.4.0 <13 || >=14" - } - }, "node_modules/superstruct": { "version": "0.15.5", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==" }, - "node_modules/supertest": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz", - "integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==", - "dev": true, - "dependencies": { - "methods": "^1.1.2", - "superagent": "^8.0.5" - }, - "engines": { - "node": ">=6.4.0" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -50210,14 +49778,6 @@ "node": ">=8" } }, - "node_modules/tdigest": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", - "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", - "dependencies": { - "bintrees": "1.0.2" - } - }, "node_modules/temp": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", @@ -50455,11 +50015,6 @@ "node": ">=0.10" } }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -50680,12 +50235,6 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "node_modules/traverse-chain": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", - "integrity": "sha512-up6Yvai4PYKhpNp5PkYtx50m3KbwQrqDwbuZP/ItyL64YEWHAvH6Md83LFLV/GRSk/BoUVwwgUzX6SOQSbsfAg==", - "dev": true - }, "node_modules/treeverse": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-2.0.0.tgz", @@ -50704,11 +50253,6 @@ "node": ">=8" } }, - "node_modules/triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" - }, "node_modules/truffle": { "version": "5.7.4", "resolved": "https://registry.npmjs.org/truffle/-/truffle-5.7.4.tgz", @@ -51413,14 +50957,6 @@ "node": ">=0.3.1" } }, - "node_modules/ts-retry-promise": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/ts-retry-promise/-/ts-retry-promise-0.7.0.tgz", - "integrity": "sha512-x6yWZXC4BfXy4UyMweOFvbS1yJ/Y5biSz/mEPiILtJZLrqD3ZxIpzVOGGgifHHdaSe3WxzFRtsRbychI6zofOg==", - "engines": { - "node": ">=6" - } - }, "node_modules/tsconfig-paths": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.2.tgz", @@ -51449,201 +50985,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, - "node_modules/tslint": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", - "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", - "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^4.0.1", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.3", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.13.0", - "tsutils": "^2.29.0" - }, - "bin": { - "tslint": "bin/tslint" - }, - "engines": { - "node": ">=4.8.0" - }, - "peerDependencies": { - "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" - } - }, - "node_modules/tslint-config-prettier": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz", - "integrity": "sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==", - "dev": true, - "bin": { - "tslint-config-prettier-check": "bin/check.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/tslint/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tslint/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/tslint/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tslint/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/tslint/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/tslint/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/tslint/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "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" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tslint/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/tslint/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/tslint/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/tslint/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/tslint/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tslint/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/tslint/node_modules/tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "peerDependencies": { - "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" - } - }, "node_modules/tsort": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz", @@ -53990,40 +53331,6 @@ "node": ">= 0.10.0" } }, - "node_modules/winston": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", - "integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==", - "dependencies": { - "@colors/colors": "1.5.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.4.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.5.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", - "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", - "dependencies": { - "logform": "^2.3.2", - "readable-stream": "^3.6.0", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 6.4.0" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -56434,6 +55741,7 @@ "price_service/server": { "name": "@pythnetwork/price-service-server", "version": "3.1.2", + "extraneous": true, "license": "Apache-2.0", "dependencies": { "@certusone/wormhole-sdk": "^0.9.9", @@ -56483,71 +55791,6 @@ "utf-8-validate": "^5.0.9" } }, - "price_service/server/node_modules/@certusone/wormhole-sdk": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.9.9.tgz", - "integrity": "sha512-seausUXqUIvUN19u4ef0VgMXNvyftQHrq5+A8AHHbsk14oBGRbvQ5JqeI+vgtKUMggK8jCaa/ICR1TnD7MW67Q==", - "dependencies": { - "@certusone/wormhole-sdk-proto-web": "0.0.6", - "@certusone/wormhole-sdk-wasm": "^0.0.1", - "@coral-xyz/borsh": "0.2.6", - "@injectivelabs/networks": "^1.0.52", - "@injectivelabs/sdk-ts": "1.0.289", - "@injectivelabs/utils": "1.0.45", - "@project-serum/anchor": "^0.25.0", - "@solana/spl-token": "^0.3.5", - "@solana/web3.js": "^1.66.2", - "@terra-money/terra.js": "^3.1.3", - "@xpla/xpla.js": "^0.2.1", - "algosdk": "^1.15.0", - "aptos": "^1.3.16", - "axios": "^0.24.0", - "bech32": "^2.0.0", - "binary-parser": "^2.2.1", - "bs58": "^4.0.1", - "elliptic": "^6.5.4", - "js-base64": "^3.6.1", - "near-api-js": "^1.0.0" - } - }, - "price_service/server/node_modules/@types/node": { - "version": "16.18.11", - "license": "MIT" - }, - "price_service/server/node_modules/@types/ws": { - "version": "8.5.4", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "price_service/server/node_modules/axios": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", - "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", - "dependencies": { - "follow-redirects": "^1.14.4" - } - }, - "price_service/server/node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" - }, - "price_service/server/node_modules/secp256k1": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.0.tgz", - "integrity": "sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA==", - "hasInstallScript": true, - "dependencies": { - "elliptic": "^6.5.4", - "node-addon-api": "^5.0.0", - "node-gyp-build": "^4.2.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, "pythnet/message_buffer": { "version": "0.1.0", "dependencies": { @@ -59420,32 +58663,6 @@ "engines": { "node": ">=12" } - }, - "wormhole_attester/sdk/js": { - "name": "@pythnetwork/wormhole-attester-sdk", - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "@pythnetwork/price-service-sdk": "*" - }, - "devDependencies": { - "@types/jest": "^29.4.0", - "@types/long": "^4.0.1", - "@types/node": "^16.6.1", - "copy-dir": "^1.3.0", - "find": "^0.3.0", - "jest": "^29.4.1", - "prettier": "^2.3.2", - "ts-jest": "^29.0.5", - "tslint": "^6.1.3", - "tslint-config-prettier": "^1.18.0", - "typescript": "^4.3.5" - } - }, - "wormhole_attester/sdk/js/node_modules/@types/node": { - "version": "16.18.11", - "dev": true, - "license": "MIT" } }, "dependencies": { @@ -61143,37 +60360,6 @@ "@types/node": "^18.0.3" } }, - "@certusone/wormhole-spydk": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@certusone/wormhole-spydk/-/wormhole-spydk-0.0.1.tgz", - "integrity": "sha512-iBQoY3UnmGoWHcbn0FypA6hKsANhdHKi03UN0GPoDAeMY12j8ly+7r462TfLl5f4hOJVQd3UZ2qviohEmdicmg==", - "requires": { - "@grpc/grpc-js": "^1.4.4", - "protobufjs": "^6.11.2" - }, - "dependencies": { - "protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - } - } - } - }, "@chain-registry/types": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/@chain-registry/types/-/types-0.16.0.tgz", @@ -61223,11 +60409,6 @@ } } }, - "@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" - }, "@confio/ics23": { "version": "0.6.8", "resolved": "https://registry.npmjs.org/@confio/ics23/-/ics23-0.6.8.tgz", @@ -61642,16 +60823,6 @@ "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", "requires": {} }, - "@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "requires": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, "@ensdomains/address-encoder": { "version": "0.1.9", "resolved": "https://registry.npmjs.org/@ensdomains/address-encoder/-/address-encoder-0.1.9.tgz", @@ -62340,27 +61511,6 @@ "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", "requires": {} }, - "@grpc/grpc-js": { - "version": "1.8.7", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.7.tgz", - "integrity": "sha512-dRAWjRFN1Zy9mzPNLkFFIWT8T6C9euwluzCHZUKuhC+Bk3MayNPcpgDRyG+sg+n2sitEUySKxUynirVpu9ItKw==", - "requires": { - "@grpc/proto-loader": "^0.7.0", - "@types/node": ">=12.12.47" - } - }, - "@grpc/proto-loader": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.4.tgz", - "integrity": "sha512-MnWjkGwqQ3W8fx94/c1CwqLsNmHHv2t0CFn+9++6+cDphC1lolpg9M2OU0iebIjK//pBNX9e94ho+gjx6vz39w==", - "requires": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^7.0.0", - "yargs": "^16.2.0" - } - }, "@hapi/bourne": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-2.1.0.tgz", @@ -68158,113 +67308,6 @@ "typescript": "^4.6.3" } }, - "@pythnetwork/price-service-server": { - "version": "file:price_service/server", - "requires": { - "@certusone/wormhole-sdk": "^0.9.9", - "@certusone/wormhole-spydk": "^0.0.1", - "@pythnetwork/price-service-sdk": "*", - "@pythnetwork/wormhole-attester-sdk": "*", - "@types/cors": "^2.8.12", - "@types/express": "^4.17.13", - "@types/jest": "^29.4.0", - "@types/keccak": "^3.0.1", - "@types/long": "^4.0.1", - "@types/morgan": "^1.9.3", - "@types/node": "^16.6.1", - "@types/node-fetch": "^2.6.2", - "@types/response-time": "^2.3.5", - "@types/secp256k1": "^4.0.3", - "@types/supertest": "^2.0.12", - "@types/ws": "^8.5.3", - "bufferutil": "^4.0.6", - "cors": "^2.8.5", - "dotenv": "^10.0.0", - "express": "^4.17.2", - "express-validation": "^4.0.1", - "http-status-codes": "^2.2.0", - "jest": "^29.4.0", - "joi": "^17.6.0", - "keccak": "^3.0.3", - "lru-cache": "^7.14.1", - "morgan": "^1.10.0", - "node-fetch": "^2.6.1", - "prettier": "^2.3.2", - "prom-client": "^14.0.1", - "response-time": "^2.3.2", - "secp256k1": "^5.0.0", - "supertest": "^6.2.3", - "ts-jest": "^29.0.5", - "ts-retry-promise": "^0.7.0", - "tslint": "^6.1.3", - "tslint-config-prettier": "^1.18.0", - "typescript": "^4.3.5", - "utf-8-validate": "^5.0.9", - "winston": "^3.3.3", - "ws": "^8.12.0" - }, - "dependencies": { - "@certusone/wormhole-sdk": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.9.9.tgz", - "integrity": "sha512-seausUXqUIvUN19u4ef0VgMXNvyftQHrq5+A8AHHbsk14oBGRbvQ5JqeI+vgtKUMggK8jCaa/ICR1TnD7MW67Q==", - "requires": { - "@certusone/wormhole-sdk-proto-web": "0.0.6", - "@certusone/wormhole-sdk-wasm": "^0.0.1", - "@coral-xyz/borsh": "0.2.6", - "@injectivelabs/networks": "^1.0.52", - "@injectivelabs/sdk-ts": "1.0.289", - "@injectivelabs/utils": "1.0.45", - "@project-serum/anchor": "^0.25.0", - "@solana/spl-token": "^0.3.5", - "@solana/web3.js": "^1.66.2", - "@terra-money/terra.js": "^3.1.3", - "@xpla/xpla.js": "^0.2.1", - "algosdk": "^1.15.0", - "aptos": "^1.3.16", - "axios": "^0.24.0", - "bech32": "^2.0.0", - "binary-parser": "^2.2.1", - "bs58": "^4.0.1", - "elliptic": "^6.5.4", - "js-base64": "^3.6.1", - "near-api-js": "^1.0.0" - } - }, - "@types/node": { - "version": "16.18.11" - }, - "@types/ws": { - "version": "8.5.4", - "requires": { - "@types/node": "*" - } - }, - "axios": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", - "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", - "requires": { - "follow-redirects": "^1.14.4" - } - }, - "node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" - }, - "secp256k1": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.0.tgz", - "integrity": "sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA==", - "requires": { - "elliptic": "^6.5.4", - "node-addon-api": "^5.0.0", - "node-gyp-build": "^4.2.0" - } - } - } - }, "@pythnetwork/pyth-aptos-js": { "version": "file:target_chains/aptos/sdk/js", "requires": { @@ -69821,29 +68864,6 @@ } } }, - "@pythnetwork/wormhole-attester-sdk": { - "version": "file:wormhole_attester/sdk/js", - "requires": { - "@pythnetwork/price-service-sdk": "*", - "@types/jest": "^29.4.0", - "@types/long": "^4.0.1", - "@types/node": "^16.6.1", - "copy-dir": "^1.3.0", - "find": "^0.3.0", - "jest": "^29.4.1", - "prettier": "^2.3.2", - "ts-jest": "^29.0.5", - "tslint": "^6.1.3", - "tslint-config-prettier": "^1.18.0", - "typescript": "^4.3.5" - }, - "dependencies": { - "@types/node": { - "version": "16.18.11", - "dev": true - } - } - }, "@radix-ui/primitive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", @@ -75444,16 +74464,10 @@ "@types/node": "*" } }, - "@types/cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", - "dev": true - }, "@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "requires": { "@types/node": "*" } @@ -75530,11 +74544,6 @@ "@types/node": "*" } }, - "@types/hapi__joi": { - "version": "16.0.12", - "resolved": "https://registry.npmjs.org/@types/hapi__joi/-/hapi__joi-16.0.12.tgz", - "integrity": "sha512-xJYifuz59jXdWY5JMS15uvA3ycS3nQYOGqoIIE0+fwQ0qI3/4CxBc6RHsOTp6wk9M0NWEdpcTl02lOQOKMifbQ==" - }, "@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -75593,15 +74602,6 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, - "@types/keccak": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/keccak/-/keccak-3.0.1.tgz", - "integrity": "sha512-/MxAVmtyyeOvZ6dGf3ciLwFRuV5M8DRIyYNFGHYI6UyBW4/XqyO0LZw+JFMvaeY3cHItQAkELclBU1x5ank6mg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -75656,14 +74656,6 @@ "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", "dev": true }, - "@types/morgan": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.4.tgz", - "integrity": "sha512-cXoc4k+6+YAllH3ZHmx4hf7La1dzUk6keTR4bF4b4Sc0mZxU/zK4wO7l+ZzezXm/jkYj/qC+uYGZrarZdIVvyQ==", - "requires": { - "@types/node": "*" - } - }, "@types/node": { "version": "18.11.18", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", @@ -75768,15 +74760,6 @@ "@types/node": "*" } }, - "@types/response-time": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@types/response-time/-/response-time-2.3.5.tgz", - "integrity": "sha512-4ANzp+I3K7sztFFAGPALWBvSl4ayaDSKzI2Bok+WNz+en2eB2Pvk6VCjR47PBXBWOkEg2r4uWpZOlXA5DNINOQ==", - "requires": { - "@types/express": "*", - "@types/node": "*" - } - }, "@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -75838,25 +74821,6 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, - "@types/superagent": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.16.tgz", - "integrity": "sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ==", - "dev": true, - "requires": { - "@types/cookiejar": "*", - "@types/node": "*" - } - }, - "@types/supertest": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.12.tgz", - "integrity": "sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ==", - "dev": true, - "requires": { - "@types/superagent": "*" - } - }, "@types/testing-library__jest-dom": { "version": "5.14.5", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", @@ -78084,21 +77048,6 @@ "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" }, - "basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "requires": { - "safe-buffer": "5.1.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } - } - }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -78218,11 +77167,6 @@ "file-uri-to-path": "1.0.0" } }, - "bintrees": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", - "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" - }, "bip32": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/bip32/-/bip32-2.0.6.tgz", @@ -78599,12 +77543,6 @@ "integrity": "sha512-pziaA+p/wdVImfcbsZLNF32EiWyujlQLwolMqUQE8xpKNOH7KmZQaY8sXN7DGOEzPAElo9QTaeNRfGnf3iOJbA==", "optional": true }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==", - "dev": true - }, "builtins": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", @@ -79376,30 +78314,6 @@ "object-visit": "^1.0.0" } }, - "color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "requires": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - }, - "dependencies": { - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - } - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -79413,15 +78327,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -79445,15 +78350,6 @@ "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "dev": true }, - "colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "requires": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, "columnify": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz", @@ -79558,7 +78454,8 @@ "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "peer": true }, "compressible": { "version": "2.0.18", @@ -80443,24 +79340,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true - }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", "peer": true }, - "copy-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/copy-dir/-/copy-dir-1.3.0.tgz", - "integrity": "sha512-Q4+qBFnN4bwGwvtXXzbp4P/4iNk0MaiGAzvQ8OiMtlLjkIKjmNN689uVzShSM0908q7GoFHXIPx4zi75ocoaHw==", - "dev": true - }, "copy-to-clipboard": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", @@ -81998,11 +80883,6 @@ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" }, - "enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" - }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -84093,16 +82973,6 @@ } } }, - "express-validation": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/express-validation/-/express-validation-4.1.0.tgz", - "integrity": "sha512-bAGRVdaJZ7yzCxELX29zvvJk0OUP/tw6tCtZtGnpK1O+yLY9qN8btwtclRQMGfIluZYIuxe++U/yPMlgEpU12A==", - "requires": { - "@types/express": "^4.17.13", - "@types/hapi__joi": "16.x.x", - "joi": "^17.6.0" - } - }, "ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -84279,11 +83149,6 @@ "bser": "2.1.1" } }, - "fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" - }, "fetch-cookie": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-0.11.0.tgz", @@ -84415,15 +83280,6 @@ } } }, - "find": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/find/-/find-0.3.0.tgz", - "integrity": "sha512-iSd+O4OEYV/I36Zl8MdYJO0xD82wH528SaCieTVHhclgiYNe9y+yPKSwK+A7/WsmHL1EZ+pYUJBXWTL5qofksw==", - "dev": true, - "requires": { - "traverse-chain": "~0.1.0" - } - }, "find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", @@ -84541,11 +83397,6 @@ "integrity": "sha512-2hJ5ACYeJCzNtiVULov6pljKOLygy0zddoqSI1fFetM+XRPpRshFdGEijtqlamA1XwyZ+7rhryI6FQFzvtLWUQ==", "peer": true }, - "fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" - }, "follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", @@ -84669,18 +83520,6 @@ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.1.tgz", "integrity": "sha512-EFRDrsMm/kyqbTQocNvRXMLjc7Es2Vk+IQFx/YW7hkUH1eBl4J1fqiP34l74Yt0pFLCNpc06fkbVk00008mzjg==" }, - "formidable": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", - "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", - "dev": true, - "requires": { - "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", - "once": "^1.4.0", - "qs": "^6.11.0" - } - }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -86175,12 +85014,6 @@ } } }, - "hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", - "dev": true - }, "hi-base32": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz", @@ -89165,11 +87998,6 @@ "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==" }, - "kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" - }, "language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -89746,7 +88574,8 @@ "lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true }, "lodash.debounce": { "version": "4.0.8", @@ -89815,18 +88644,6 @@ "is-unicode-supported": "^0.1.0" } }, - "logform": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", - "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", - "requires": { - "@colors/colors": "1.5.0", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - } - }, "logkitty": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/logkitty/-/logkitty-0.7.1.tgz", @@ -89958,7 +88775,8 @@ "lru-cache": { "version": "7.14.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.1.tgz", - "integrity": "sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==" + "integrity": "sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==", + "dev": true }, "ltgt": { "version": "2.2.1", @@ -91405,7 +90223,8 @@ "mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "peer": true }, "mime-db": { "version": "1.52.0", @@ -91895,46 +90714,6 @@ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", "dev": true }, - "morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "requires": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "requires": { - "ee-first": "1.1.1" - } - } - } - }, "mri": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", @@ -93060,14 +91839,6 @@ "wrappy": "1" } }, - "one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "requires": { - "fn.name": "1.x.x" - } - }, "onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -94905,14 +93676,6 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "prom-client": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.1.1.tgz", - "integrity": "sha512-hFU32q7UZQ59bVJQGUtm3I2PrJ3gWvoCkilX9sF165ks1qflhugVCeK+S1JjJYHvyt3o5kj68+q3bchormjnzw==", - "requires": { - "tdigest": "^0.1.1" - } - }, "promise": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", @@ -95003,6 +93766,7 @@ "@pythnetwork/client": "^2.17.0", "@solana/web3.js": "^1.76.0", "@sqds/mesh": "^1.0.6", + "@types/cors": "^2.8.17", "cors": "^2.8.5", "ts-node": "^10.9.1", "xc_admin_common": "*" @@ -98477,15 +97241,6 @@ "resolved": "https://registry.npmjs.org/response-iterator/-/response-iterator-0.2.6.tgz", "integrity": "sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==" }, - "response-time": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.2.tgz", - "integrity": "sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw==", - "requires": { - "depd": "~1.1.0", - "on-headers": "~1.0.1" - } - }, "responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -98724,7 +97479,8 @@ "safe-stable-stringify": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz", - "integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==" + "integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==", + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -99197,21 +97953,6 @@ "simple-concat": "^1.0.0" } }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - } - } - }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -100165,11 +98906,6 @@ "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" - }, "stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -100603,39 +99339,11 @@ "integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==", "peer": true }, - "superagent": { - "version": "8.0.9", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz", - "integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==", - "dev": true, - "requires": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.4", - "debug": "^4.3.4", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.1.2", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - } - }, "superstruct": { "version": "0.15.5", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==" }, - "supertest": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.3.tgz", - "integrity": "sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA==", - "dev": true, - "requires": { - "methods": "^1.1.2", - "superagent": "^8.0.5" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -101128,14 +99836,6 @@ "readable-stream": "^3.1.1" } }, - "tdigest": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", - "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", - "requires": { - "bintrees": "1.0.2" - } - }, "temp": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", @@ -101295,11 +99995,6 @@ "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", "dev": true }, - "text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -101489,12 +100184,6 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "traverse-chain": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", - "integrity": "sha512-up6Yvai4PYKhpNp5PkYtx50m3KbwQrqDwbuZP/ItyL64YEWHAvH6Md83LFLV/GRSk/BoUVwwgUzX6SOQSbsfAg==", - "dev": true - }, "treeverse": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-2.0.0.tgz", @@ -101507,11 +100196,6 @@ "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", "dev": true }, - "triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" - }, "truffle": { "version": "5.7.4", "resolved": "https://registry.npmjs.org/truffle/-/truffle-5.7.4.tgz", @@ -102031,11 +100715,6 @@ } } }, - "ts-retry-promise": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/ts-retry-promise/-/ts-retry-promise-0.7.0.tgz", - "integrity": "sha512-x6yWZXC4BfXy4UyMweOFvbS1yJ/Y5biSz/mEPiILtJZLrqD3ZxIpzVOGGgifHHdaSe3WxzFRtsRbychI6zofOg==" - }, "tsconfig-paths": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.2.tgz", @@ -102060,154 +100739,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, - "tslint": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", - "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^4.0.1", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.3", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.13.0", - "tsutils": "^2.29.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "requires": { - "minimist": "^1.2.6" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - } - } - }, - "tslint-config-prettier": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz", - "integrity": "sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==", - "dev": true - }, "tsort": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz", @@ -104109,34 +102640,6 @@ "integrity": "sha512-UD7d8HFA2+PZsbKyaOCEy8gMh1oDtHgJh1LfgjQ4zVXmYjAT/kvz3PueITKuqDiIXQe7yzpPnxX3lNc+AhQMyw==", "dev": true }, - "winston": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", - "integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==", - "requires": { - "@colors/colors": "1.5.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.4.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.5.0" - } - }, - "winston-transport": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", - "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", - "requires": { - "logform": "^2.3.2", - "readable-stream": "^3.6.0", - "triple-beam": "^1.3.0" - } - }, "word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 2f7196b2..dd153387 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "governance/xc_admin/packages/*", "governance/multisig_wh_message_builder", "price_pusher", - "price_service/server", "price_service/sdk/js", "price_service/client/js", "pythnet/message_buffer", @@ -19,7 +18,6 @@ "target_chains/ethereum/examples/oracle_swap/app", "target_chains/sui/sdk/js", "target_chains/sui/cli", - "wormhole_attester/sdk/js", "contract_manager" ], "dependencies": { diff --git a/price_service/server/.dockerignore b/price_service/server/.dockerignore deleted file mode 100644 index 221d37f8..00000000 --- a/price_service/server/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -lib -.env diff --git a/price_service/server/.env.sample b/price_service/server/.env.sample deleted file mode 100644 index 27c28c67..00000000 --- a/price_service/server/.env.sample +++ /dev/null @@ -1,24 +0,0 @@ -# Local DevNet: -SPY_SERVICE_HOST=0.0.0.0:7072 - -# Filters (if provided) should be valid JSON like below: -# These filters tell the spy to only retrieve messages sent from certain chains/contracts. -# See the docker-compose..yaml files for the appropriate configuration for a -# testnet/mainnet pyth price_service deployment. -SPY_SERVICE_FILTERS=[{"chain_id":1,"emitter_address":"71f8dcb863d176e2c420ad6610cf687359612b6fb392e0642b0ca6b1f186aa3b"}] - -WORMHOLE_CLUSTER=localnet - -# Number of seconds to sync with spy to be sure to have latest messages -READINESS_SPY_SYNC_TIME_SECONDS=60 -READINESS_NUM_LOADED_SYMBOLS=5 - -WS_PORT=6200 -REST_PORT=4200 -PROM_PORT=8081 - -# The default is to log with level info. -#LOG_LEVEL=debug - -REMOVE_EXPIRED_VALUES_INTERVAL_SECONDS=60 -CACHE_TTL_SECONDS=300 diff --git a/price_service/server/.gitignore b/price_service/server/.gitignore deleted file mode 100644 index 502167fa..00000000 --- a/price_service/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/lib diff --git a/price_service/server/Dockerfile b/price_service/server/Dockerfile deleted file mode 100644 index 73a2f55e..00000000 --- a/price_service/server/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -# Defined in Dockerfile.lerna -FROM lerna - -USER root -RUN apt-get update && apt-get install -y ncat - -WORKDIR /home/node/ -USER 1000 - -COPY --chown=1000:1000 price_service/server price_service/server -COPY --chown=1000:1000 price_service/sdk/js price_service/sdk/js -COPY --chown=1000:1000 wormhole_attester/sdk/js wormhole_attester/sdk/js - -RUN npx lerna run build --scope="@pythnetwork/price-service-server" --include-dependencies - -WORKDIR /home/node/price_service/server - -CMD [ "npm", "run", "start" ] diff --git a/price_service/server/README.md b/price_service/server/README.md deleted file mode 100644 index 76565680..00000000 --- a/price_service/server/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Pyth Price Service - -** Pyth price service is deprecated. Please use [Hermes](../../hermes/) instead. ** - -The Pyth price service is a webservice that listens to the Wormhole Network for Pyth price updates and serves them via a -convenient web API. The service allows users to easily query for recent price updates via a REST API, or subscribe to -a websocket for streaming updates. [Price service JS client](https://github.com/pyth-network/pyth-crosschain/tree/main/price_service/sdk/js) connects -to an instance of the price service in order to fetch on-demand price updates. - -## Wormhole Spy - -The price service depends on a Wormhole Spy to stream Pyth messages from the Wormhole Network to it. The -[spy](https://github.com/wormhole-foundation/wormhole/blob/main/node/cmd/spy/spy.go) is a Wormhole component that listens to the Wormhole verified -messages from the Wormhole Network peer-to-peer network; then, it streams the messages that are coming from certain emitters (e.g., Pyth data emitters) to its subscribers. - -The price service subscribes to the spy to fetch all verified prices coming from the Pyth data sources. The Pyth data sources should -be defined in `SPY_SERVICE_FILTERS` environment variable as a JSON array. - -## Run - -This repository contains testnet and mainnet docker-compose files to run -both the price service and spy. To run the mainnet docker compose file run -the following command: - -``` -docker compose -f docker-compose.mainnet.yaml up -``` - -Now your own instance of the price service is up and running! Running the following command should give you the Pyth price of ETH/USD : - -``` -curl localhost:4200/api/latest_price_feeds?ids[]=0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace -``` - -If everything is working, you should get an output like this : - -``` -[{"ema_price":{"conf":"52359655","expo":-8,"price":"169041770000","publish_time":1675365813},"id":"ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace","price":{"conf":"64041644","expo":-8,"price":"167043958356","publish_time":1675365813}}] -``` - -The compose files use a public release of Pyth price service and spy. If you wish to change the -price service you should: - -1. Build an image for using it first according to the section below. -2. Change the price service image to your local docker image (e.g., `pyth_price_server`) - -### Self-Hosting - -If you would like to host your own instance of the price service, we recommend running the process on a 4 core machine with 4 GB of RAM. -We also recommend using a host like [Latitude](https://www.latitude.sh/) or [Hetzner](https://www.hetzner.com/) and avoiding cloud service providers like AWS in order to reduce the cost. -The price service participates in a peer-to-peer network which can use a lot of bandwidth. -Cloud hosts like AWS charge high fees for bandwidth, which makes running the service much more expensive than necessary. -Using one of the recommended hosts above should cost $10-50 / month. - -## Build an image - -Build the image from [the repo root](../../) like below. It will create a -local image named `pyth_price_server`. - -``` -$ docker buildx build -f tilt_devnet/docker_images/Dockerfile.lerna -t lerna . -$ docker buildx build -f price_service/server/Dockerfile -t pyth_price_server . -``` - -If you wish to build price service without docker, please follow the instruction of the price service -[`Dockerfile`](./Dockerfile) - -## Known Issues - -The spy sometimes fails to connect to the peer-to-peer network on initialization. If this happens, the price -service will not be able to retrieve any data. You can fix this problem by quitting, removing the containers from Docker, -then restarting both containers. Simply stopping and starting the services tends not to work. diff --git a/price_service/server/docker-compose.mainnet.yaml b/price_service/server/docker-compose.mainnet.yaml deleted file mode 100644 index a4ae8ebe..00000000 --- a/price_service/server/docker-compose.mainnet.yaml +++ /dev/null @@ -1,61 +0,0 @@ -services: - spy: - # Find latest Guardian images in https://github.com/wormhole-foundation/wormhole/pkgs/container/guardiand - image: ghcr.io/wormhole-foundation/guardiand:v2.23.28 - restart: on-failure - command: - - "spy" - - "--nodeKey" - - "/node.key" - - "--spyRPC" - - "[::]:7072" - - "--bootstrap" - - "/dns4/wormhole-v2-mainnet-bootstrap.xlabs.xyz/udp/8999/quic/p2p/12D3KooWNQ9tVrcb64tw6bNs2CaNrUGPM7yRrKvBBheQ5yCyPHKC,/dns4/wormhole.mcf.rocks/udp/8999/quic/p2p/12D3KooWDZVv7BhZ8yFLkarNdaSWaB43D6UbQwExJ8nnGAEmfHcU,/dns4/wormhole-v2-mainnet-bootstrap.staking.fund/udp/8999/quic/p2p/12D3KooWG8obDX9DNi1KUwZNu9xkGwfKqTp2GFwuuHpWZ3nQruS1" - - "--network" - - "/wormhole/mainnet/2" - - "--logLevel" - - "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.8 - restart: on-failure - # Or alternatively use a locally built image - # image: pyth_price_server - environment: - SPY_SERVICE_HOST: "spy:7072" - SPY_SERVICE_FILTERS: | - [ - { - "chain_id": 1, - "emitter_address": "6bb14509a612f01fbbc4cffeebd4bbfb492a86df717ebe92eb6df432a3f00a25" - }, - { - "chain_id": 26, - "emitter_address": "f8cd23c2ab91237730770bbea08d61005cdda0984348f3f6eecb559638c0bba0" - } - ] - REST_PORT: "4200" - PROM_PORT: "8081" - READINESS_SPY_SYNC_TIME_SECONDS: "5" - READINESS_NUM_LOADED_SYMBOLS: "280" - LOG_LEVEL: warning - WORMHOLE_CLUSTER: mainnet - DB_API_CLUSTER: pythnet - REMOVE_EXPIRED_VALUES_INTERVAL_SECONDS: "60" - CACHE_TTL_SECONDS: "300" - DB_API_ENDPOINT: "https://web-api.pyth.network" - ports: - - "4200:4200" - healthcheck: - test: - [ - "CMD", - "wget", - "--no-verbose", - "--tries=1", - "--spider", - "http://localhost:4200/ready", - ] - start_period: 20s - depends_on: - - spy diff --git a/price_service/server/docker-compose.testnet.yaml b/price_service/server/docker-compose.testnet.yaml deleted file mode 100644 index eb4efeb4..00000000 --- a/price_service/server/docker-compose.testnet.yaml +++ /dev/null @@ -1,61 +0,0 @@ -services: - spy: - # Find latest Guardian images in https://github.com/wormhole-foundation/wormhole/pkgs/container/guardiand - image: ghcr.io/wormhole-foundation/guardiand:v2.23.28 - restart: on-failure - command: - - "spy" - - "--nodeKey" - - "/node.key" - - "--spyRPC" - - "[::]:7072" - - "--bootstrap" - - "/dns4/t-guardian-01.nodes.stable.io/udp/8999/quic/p2p/12D3KooWCW3LGUtkCVkHZmVSZHzL3C4WRKWfqAiJPz1NR7dT9Bxh,/dns4/t-guardian-02.nodes.stable.io/udp/8999/quic/p2p/12D3KooWJXA6goBCiWM8ucjzc4jVUBSqL9Rri6UpjHbkMPErz5zK" - - "--network" - - "/wormhole/testnet/2/1" - - "--logLevel" - - "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.8 - restart: on-failure - # Or alternatively use a locally built image - # image: pyth_price_server - environment: - SPY_SERVICE_HOST: "spy:7072" - SPY_SERVICE_FILTERS: | - [ - { - "chain_id": 1, - "emitter_address": "f346195ac02f37d60d4db8ffa6ef74cb1be3550047543a4a9ee9acf4d78697b0" - }, - { - "chain_id": 26, - "emitter_address": "a27839d641b07743c0cb5f68c51f8cd31d2c0762bec00dc6fcd25433ef1ab5b6" - } - ] - REST_PORT: "4200" - PROM_PORT: "8081" - READINESS_SPY_SYNC_TIME_SECONDS: "5" - READINESS_NUM_LOADED_SYMBOLS: "280" - LOG_LEVEL: warning - WORMHOLE_CLUSTER: testnet - DB_API_CLUSTER: devnet - REMOVE_EXPIRED_VALUES_INTERVAL_SECONDS: "60" - CACHE_TTL_SECONDS: "300" - DB_API_ENDPOINT: "https://web-api.pyth.network" - ports: - - "4200:4200" - healthcheck: - test: - [ - "CMD", - "wget", - "--no-verbose", - "--tries=1", - "--spider", - "http://localhost:4200/ready", - ] - start_period: 20s - depends_on: - - spy diff --git a/price_service/server/jest.config.js b/price_service/server/jest.config.js deleted file mode 100644 index 21a1e973..00000000 --- a/price_service/server/jest.config.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ -module.exports = { - preset: "ts-jest", - testEnvironment: "node", -}; diff --git a/price_service/server/package.json b/price_service/server/package.json deleted file mode 100644 index e87de00b..00000000 --- a/price_service/server/package.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "name": "@pythnetwork/price-service-server", - "version": "3.1.2", - "description": "Webservice for retrieving prices from the Pyth oracle.", - "private": "true", - "main": "index.js", - "scripts": { - "format": "prettier --write \"src/**/*.ts\"", - "build": "tsc", - "start": "node lib/index.js", - "test": "jest src/", - "lint": "tslint -p tsconfig.json", - "preversion": "npm run lint", - "version": "npm run format && git add -A src" - }, - "author": "", - "license": "Apache-2.0", - "devDependencies": { - "@types/jest": "^29.4.0", - "@types/keccak": "^3.0.1", - "@types/long": "^4.0.1", - "@types/node": "^16.6.1", - "@types/node-fetch": "^2.6.2", - "@types/secp256k1": "^4.0.3", - "@types/supertest": "^2.0.12", - "jest": "^29.4.0", - "prettier": "^2.3.2", - "supertest": "^6.2.3", - "ts-jest": "^29.0.5", - "tslint": "^6.1.3", - "tslint-config-prettier": "^1.18.0", - "typescript": "^4.3.5" - }, - "dependencies": { - "@certusone/wormhole-sdk": "^0.9.9", - "@certusone/wormhole-spydk": "^0.0.1", - "@pythnetwork/price-service-sdk": "*", - "@pythnetwork/wormhole-attester-sdk": "*", - "@types/cors": "^2.8.12", - "@types/express": "^4.17.13", - "@types/morgan": "^1.9.3", - "@types/response-time": "^2.3.5", - "@types/ws": "^8.5.3", - "cors": "^2.8.5", - "dotenv": "^10.0.0", - "express": "^4.17.2", - "express-validation": "^4.0.1", - "http-status-codes": "^2.2.0", - "joi": "^17.6.0", - "keccak": "^3.0.3", - "lru-cache": "^7.14.1", - "morgan": "^1.10.0", - "node-fetch": "^2.6.1", - "prom-client": "^14.0.1", - "response-time": "^2.3.2", - "secp256k1": "^5.0.0", - "ts-retry-promise": "^0.7.0", - "winston": "^3.3.3", - "ws": "^8.12.0" - }, - "directories": { - "lib": "lib" - }, - "keywords": [], - "optionalDependencies": { - "bufferutil": "^4.0.6", - "utf-8-validate": "^5.0.9" - } -} diff --git a/price_service/server/src/__tests__/listen.test.ts b/price_service/server/src/__tests__/listen.test.ts deleted file mode 100644 index 610a5db4..00000000 --- a/price_service/server/src/__tests__/listen.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { VaaConfig, VaaCache } from "../listen"; - -describe("VAA Cache works", () => { - test("Setting and getting works as expected", async () => { - const cache = new VaaCache(); - - expect(cache.get("a", 3)).toBeUndefined(); - - cache.set("a", 1, 0, "a-1"); - - expect(cache.get("a", 3)).toBeUndefined(); - - cache.set("a", 4, 3, "a-2"); - - expect(cache.get("a", 3)).toEqual({ - publishTime: 4, - lastAttestedPublishTime: 3, - vaa: "a-2", - }); - - cache.set("a", 10, 9, "a-3"); - cache.set("a", 10, 10, "a-4"); - cache.set("a", 10, 10, "a-5"); - cache.set("a", 10, 10, "a-6"); - cache.set("a", 11, 11, "a-7"); - - // Adding some elements with other keys to make sure - // they are not stored separately. - cache.set("b", 3, 2, "b-1"); - cache.set("b", 7, 6, "b-2"); - cache.set("b", 9, 8, "b-3"); - - expect(cache.get("a", 3)).toEqual({ - publishTime: 4, - lastAttestedPublishTime: 3, - vaa: "a-2", - }); - expect(cache.get("a", 4)).toEqual({ - publishTime: 4, - lastAttestedPublishTime: 3, - vaa: "a-2", - }); - expect(cache.get("a", 5)).toEqual({ - publishTime: 10, - lastAttestedPublishTime: 9, - vaa: "a-3", - }); - // There are multiple elements at 10, but we prefer to return the one with a lower lastAttestedPublishTime. - expect(cache.get("a", 10)).toEqual({ - publishTime: 10, - lastAttestedPublishTime: 9, - vaa: "a-3", - }); - // If the cache only contains elements where the lastAttestedPublishTime==publishTime, those will be returned. - // Note that this behavior is undesirable (as this means we can return a noncanonical VAA for a query time); - // this test simply documents it. - expect(cache.get("a", 11)).toEqual({ - publishTime: 11, - lastAttestedPublishTime: 11, - vaa: "a-7", - }); - - expect(cache.get("b", 3)).toEqual({ - publishTime: 3, - lastAttestedPublishTime: 2, - vaa: "b-1", - }); - expect(cache.get("b", 4)).toEqual({ - publishTime: 7, - lastAttestedPublishTime: 6, - vaa: "b-2", - }); - - // When no item item more recent than asked pubTime is asked it should return undefined - expect(cache.get("a", 12)).toBeUndefined(); - expect(cache.get("b", 10)).toBeUndefined(); - - // When the asked pubTime is less than the first existing pubTime we are not sure that - // this is the first vaa after that time, so we should return undefined. - expect(cache.get("a", 0)).toBeUndefined(); - expect(cache.get("b", 1)).toBeUndefined(); - expect(cache.get("b", 2)).toBeUndefined(); - }); - - test("removeExpiredValues clears the old values", async () => { - jest.useFakeTimers(); - - // TTL of 500 seconds for the cache - const cache = new VaaCache(500); - - cache.set("a", 300, 299, "a-1"); - cache.set("a", 700, 699, "a-2"); - cache.set("a", 900, 899, "a-3"); - - expect(cache.get("a", 300)).toEqual({ - publishTime: 300, - lastAttestedPublishTime: 299, - vaa: "a-1", - }); - - expect(cache.get("a", 500)).toEqual({ - publishTime: 700, - lastAttestedPublishTime: 699, - vaa: "a-2", - }); - - // Set time to second 1000 - jest.setSystemTime(1000 * 1000); - - cache.removeExpiredValues(); - - expect(cache.get("a", 300)).toBeUndefined(); - expect(cache.get("a", 500)).toBeUndefined(); - }); - - test("the cache clean loop works", async () => { - jest.useFakeTimers(); - - // TTL of 500 seconds for the cache and cleanup of every 100 seconds - const cache = new VaaCache(500, 100); - cache.runRemoveExpiredValuesLoop(); - - cache.set("a", 300, 299, "a-1"); - cache.set("a", 700, 699, "a-2"); - cache.set("a", 900, 899, "a-3"); - - expect(cache.get("a", 900)).toEqual({ - publishTime: 900, - lastAttestedPublishTime: 899, - vaa: "a-3", - }); - - // Set time to second 2000. Everything should be evicted from cache now. - jest.setSystemTime(2000 * 1000); - jest.advanceTimersToNextTimer(); - - expect(cache.get("a", 900)).toBeUndefined(); - }); -}); diff --git a/price_service/server/src/__tests__/rest.test.ts b/price_service/server/src/__tests__/rest.test.ts deleted file mode 100644 index 4ae0872a..00000000 --- a/price_service/server/src/__tests__/rest.test.ts +++ /dev/null @@ -1,566 +0,0 @@ -import { - HexString, - Price, - PriceFeed, - PriceFeedMetadata, -} from "@pythnetwork/price-service-sdk"; -import express, { Express } from "express"; -import { StatusCodes } from "http-status-codes"; -import request from "supertest"; -import { PriceInfo, PriceStore, VaaCache, VaaConfig } from "../listen"; -import { RestAPI, VaaResponse } from "../rest"; - -let priceInfo: PriceStore; -let app: Express; -let priceInfoMap: Map; -let vaasCache: VaaCache; - -function expandTo64Len(id: string): string { - return id.repeat(64).substring(0, 64); -} - -function dummyPriceFeed(id: string): PriceFeed { - return new PriceFeed({ - emaPrice: new Price({ - conf: "1", - expo: 2, - price: "3", - publishTime: 4, - }), - id, - price: new Price({ - conf: "5", - expo: 6, - price: "7", - publishTime: 8, - }), - }); -} - -function dummyPriceInfoPair( - id: HexString, - seqNum: number, - vaa: HexString -): [HexString, PriceInfo] { - return [ - id, - { - priceFeed: dummyPriceFeed(id), - publishTime: 1, - attestationTime: 2, - seqNum, - vaa: Buffer.from(vaa, "hex"), - emitterChainId: 0, - priceServiceReceiveTime: 0, - lastAttestedPublishTime: 0, - }, - ]; -} - -// Add some dummy data to the provided vaa cache. -function addAbcdDataToCache(id: string, cache: VaaCache) { - cache.set(id, 10, 9, "abcd10"); - cache.set(id, 20, 19, "abcd20"); - cache.set(id, 30, 29, "abcd30"); -} - -beforeAll(async () => { - priceInfoMap = new Map([ - dummyPriceInfoPair(expandTo64Len("abcd"), 1, "a1b2c3d4"), - dummyPriceInfoPair(expandTo64Len("ef01"), 1, "a1b2c3d4"), - dummyPriceInfoPair(expandTo64Len("3456"), 2, "bad01bad"), - dummyPriceInfoPair(expandTo64Len("10101"), 3, "bidbidbid"), - ]); - - vaasCache = new VaaCache(); - - priceInfo = { - getLatestPriceInfo: (priceFeedId: string) => { - return priceInfoMap.get(priceFeedId); - }, - addUpdateListener: (_callback: (priceInfo: PriceInfo) => any) => undefined, - getPriceIds: () => new Set(), - getVaa: (vaasCacheKey: string, publishTime: number) => { - return vaasCache.get(vaasCacheKey, publishTime); - }, - }; - - const api = new RestAPI({ port: 8889 }, priceInfo, () => true); - app = await api.createApp(); -}); - -describe("Latest Price Feed Endpoint", () => { - test("When called with valid ids, returns correct price feed", async () => { - const ids = [expandTo64Len("abcd"), expandTo64Len("3456")]; - const resp = await request(app) - .get("/api/latest_price_feeds") - .query({ ids }); - expect(resp.status).toBe(StatusCodes.OK); - expect(resp.body.length).toBe(2); - expect(resp.body).toContainEqual(dummyPriceFeed(ids[0]).toJson()); - expect(resp.body).toContainEqual(dummyPriceFeed(ids[1]).toJson()); - }); - - test("When called with valid ids with leading 0x, returns correct price feed", async () => { - const ids = [expandTo64Len("abcd"), expandTo64Len("3456")]; - const resp = await request(app) - .get("/api/latest_price_feeds") - .query({ - ids: ids.map((id) => "0x" + id), // Add 0x to the queries - }); - expect(resp.status).toBe(StatusCodes.OK); - expect(resp.body.length).toBe(2); - - // Please note that the response id is without 0x - expect(resp.body).toContainEqual(dummyPriceFeed(ids[0]).toJson()); - expect(resp.body).toContainEqual(dummyPriceFeed(ids[1]).toJson()); - }); - - test("When called with valid ids and verbose flag set to true, returns correct price feed with verbose information", async () => { - const ids = [expandTo64Len("abcd"), expandTo64Len("3456")]; - const resp = await request(app) - .get("/api/latest_price_feeds") - .query({ ids, verbose: true }); - expect(resp.status).toBe(StatusCodes.OK); - expect(resp.body.length).toBe(2); - expect(resp.body).toContainEqual({ - ...priceInfoMap.get(ids[0])!.priceFeed.toJson(), - metadata: new PriceFeedMetadata({ - attestationTime: priceInfoMap.get(ids[0])!.attestationTime, - emitterChain: priceInfoMap.get(ids[0])!.emitterChainId, - receiveTime: priceInfoMap.get(ids[0])!.priceServiceReceiveTime, - sequenceNumber: priceInfoMap.get(ids[0])!.seqNum, - }).toJson(), - }); - expect(resp.body).toContainEqual({ - ...priceInfoMap.get(ids[1])!.priceFeed.toJson(), - metadata: new PriceFeedMetadata({ - attestationTime: priceInfoMap.get(ids[1])!.attestationTime, - emitterChain: priceInfoMap.get(ids[1])!.emitterChainId, - receiveTime: priceInfoMap.get(ids[1])!.priceServiceReceiveTime, - sequenceNumber: priceInfoMap.get(ids[1])!.seqNum, - }).toJson(), - }); - }); - - test("When called with valid ids and binary flag set to true, returns correct price feed with binary vaa", async () => { - const ids = [expandTo64Len("abcd"), expandTo64Len("3456")]; - const resp = await request(app) - .get("/api/latest_price_feeds") - .query({ ids, binary: true }); - expect(resp.status).toBe(StatusCodes.OK); - expect(resp.body.length).toBe(2); - expect(resp.body).toContainEqual({ - ...priceInfoMap.get(ids[0])!.priceFeed.toJson(), - vaa: priceInfoMap.get(ids[0])!.vaa.toString("base64"), - }); - expect(resp.body).toContainEqual({ - ...priceInfoMap.get(ids[1])!.priceFeed.toJson(), - vaa: priceInfoMap.get(ids[1])!.vaa.toString("base64"), - }); - }); - - test("When called with a target_chain, returns correct price feed with binary vaa encoded properly", async () => { - const ids = [expandTo64Len("abcd"), expandTo64Len("3456")]; - const resp = await request(app) - .get("/api/latest_price_feeds") - .query({ ids, target_chain: "evm" }); - expect(resp.status).toBe(StatusCodes.OK); - expect(resp.body.length).toBe(2); - expect(resp.body).toContainEqual({ - ...priceInfoMap.get(ids[0])!.priceFeed.toJson(), - vaa: "0x" + priceInfoMap.get(ids[0])!.vaa.toString("hex"), - }); - expect(resp.body).toContainEqual({ - ...priceInfoMap.get(ids[1])!.priceFeed.toJson(), - vaa: "0x" + priceInfoMap.get(ids[1])!.vaa.toString("hex"), - }); - }); - - test("When called with some non-existent ids within ids, returns error mentioning non-existent ids", async () => { - const ids = [ - expandTo64Len("ab01"), - expandTo64Len("3456"), - expandTo64Len("effe"), - ]; - const resp = await request(app) - .get("/api/latest_price_feeds") - .query({ ids }); - expect(resp.status).toBe(StatusCodes.BAD_REQUEST); - expect(resp.body.message).toContain(ids[0]); - expect(resp.body.message).not.toContain(ids[1]); - expect(resp.body.message).toContain(ids[2]); - }); -}); - -describe("Latest Vaa Bytes Endpoint", () => { - test("When called with valid ids, returns vaa bytes as array, merged if necessary", async () => { - const ids = [ - expandTo64Len("abcd"), - expandTo64Len("ef01"), - expandTo64Len("3456"), - ]; - const resp = await request(app).get("/api/latest_vaas").query({ ids }); - expect(resp.status).toBe(StatusCodes.OK); - expect(resp.body.length).toBe(2); - expect(resp.body).toContain( - Buffer.from("a1b2c3d4", "hex").toString("base64") - ); - expect(resp.body).toContain( - Buffer.from("bad01bad", "hex").toString("base64") - ); - }); - - test("When called with target_chain, returns vaa bytes encoded correctly", async () => { - const ids = [ - expandTo64Len("abcd"), - expandTo64Len("ef01"), - expandTo64Len("3456"), - ]; - const resp = await request(app) - .get("/api/latest_vaas") - .query({ ids, target_chain: "evm" }); - expect(resp.status).toBe(StatusCodes.OK); - expect(resp.body.length).toBe(2); - expect(resp.body).toContain("0xa1b2c3d4"); - expect(resp.body).toContain("0xbad01bad"); - }); - - test("When called with valid ids with leading 0x, returns vaa bytes as array, merged if necessary", async () => { - const ids = [ - expandTo64Len("abcd"), - expandTo64Len("ef01"), - expandTo64Len("3456"), - ]; - - const resp = await request(app) - .get("/api/latest_vaas") - .query({ - ids: ids.map((id) => "0x" + id), // Add 0x to the queries - }); - - expect(resp.status).toBe(StatusCodes.OK); - expect(resp.body.length).toBe(2); - expect(resp.body).toContain( - Buffer.from("a1b2c3d4", "hex").toString("base64") - ); - expect(resp.body).toContain( - Buffer.from("bad01bad", "hex").toString("base64") - ); - }); - - test("When called with some non-existent ids within ids, returns error mentioning non-existent ids", async () => { - const ids = [ - expandTo64Len("ab01"), - expandTo64Len("3456"), - expandTo64Len("effe"), - ]; - const resp = await request(app).get("/api/latest_vaas").query({ ids }); - expect(resp.status).toBe(StatusCodes.BAD_REQUEST); - expect(resp.body.message).toContain(ids[0]); - expect(resp.body.message).not.toContain(ids[1]); - expect(resp.body.message).toContain(ids[2]); - }); -}); - -describe("Get VAA endpoint and Get VAA CCIP", () => { - test("When called with valid id and timestamp in the cache returns the correct answer", async () => { - const id = expandTo64Len("abcd"); - addAbcdDataToCache(id, vaasCache); - - const resp = await request(app).get("/api/get_vaa").query({ - id, - publish_time: 16, - }); - expect(resp.status).toBe(StatusCodes.OK); - expect(resp.body).toEqual({ - vaa: "abcd20", - publishTime: 20, - }); - - const pubTime16AsHex64Bit = "0000000000000010"; - const ccipResp = await request(app) - .get("/api/get_vaa_ccip") - .query({ - data: "0x" + id + pubTime16AsHex64Bit, - }); - expect(ccipResp.status).toBe(StatusCodes.OK); - expect(ccipResp.body).toEqual({ - data: "0x" + Buffer.from("abcd20", "base64").toString("hex"), - }); - }); - - test("When called with valid id with leading 0x and timestamp in the cache returns the correct answer", async () => { - const id = expandTo64Len("abcd"); - addAbcdDataToCache(id, vaasCache); - - const resp = await request(app) - .get("/api/get_vaa") - .query({ - id: "0x" + id, - publish_time: 16, - }); - expect(resp.status).toBe(StatusCodes.OK); - expect(resp.body).toEqual({ - vaa: "abcd20", - publishTime: 20, - }); - }); - - test("When called with target_chain, encodes resulting VAA in the right format", async () => { - const id = expandTo64Len("abcd"); - addAbcdDataToCache(id, vaasCache); - - const resp = await request(app) - .get("/api/get_vaa") - .query({ - id: "0x" + id, - publish_time: 16, - target_chain: "evm", - }); - expect(resp.status).toBe(StatusCodes.OK); - expect(resp.body).toEqual({ - vaa: "0x" + Buffer.from("abcd20", "base64").toString("hex"), - publishTime: 20, - }); - }); - - test("When called with invalid id returns price id found", async () => { - // dead does not exist in the ids - const id = expandTo64Len("dead"); - - const resp = await request(app).get("/api/get_vaa").query({ - id, - publish_time: 16, - }); - expect(resp.status).toBe(StatusCodes.BAD_REQUEST); - expect(resp.body.message).toContain(id); - - const pubTime16AsHex64Bit = "0000000000000010"; - const ccipResp = await request(app) - .get("/api/get_vaa_ccip") - .query({ - data: "0x" + id + pubTime16AsHex64Bit, - }); - expect(ccipResp.status).toBe(StatusCodes.BAD_REQUEST); - expect(ccipResp.body.message).toContain(id); - }); - - test("When called with valid id and timestamp not in the cache without db returns vaa not found", async () => { - const id = expandTo64Len("abcd"); - addAbcdDataToCache(id, vaasCache); - - const resp = await request(app) - .get("/api/get_vaa") - .query({ - id: "0x" + id, - publish_time: 5, - }); - expect(resp.status).toBe(StatusCodes.NOT_FOUND); - - const pubTime5AsHex64Bit = "0000000000000005"; - const ccipResp = await request(app) - .get("/api/get_vaa_ccip") - .query({ - data: "0x" + id + pubTime5AsHex64Bit, - }); - // On CCIP we expect bad gateway so the client want to retry other ccip endpoints. - expect(ccipResp.status).toBe(StatusCodes.BAD_GATEWAY); - }); - - test("When called with valid id and timestamp not in the cache with db returns ok", async () => { - const dbBackend = express(); - dbBackend.get("/vaa", (req, res) => { - const priceId = req.query.id; - const pubTime = Number(req.query.publishTime); - const cluster = req.query.cluster; - - res.json([ - { - vaa: `${cluster}${priceId}${pubTime}`, - publishTime: new Date(pubTime * 1000).toISOString(), - }, - ]); - }); - const dbApp = dbBackend.listen({ port: 37777 }); - - const apiWithDb = new RestAPI( - { - port: 8889, - dbApiCluster: "pythnet", - dbApiEndpoint: "http://localhost:37777", - }, - priceInfo, - () => true - ); - const appWithDb = await apiWithDb.createApp(); - - const id = expandTo64Len("abcd"); - addAbcdDataToCache(id, vaasCache); - - const resp = await request(appWithDb) - .get("/api/get_vaa") - .query({ - id: "0x" + id, - publish_time: 5, - }); - expect(resp.status).toBe(StatusCodes.OK); - expect(resp.body).toEqual({ - vaa: `pythnet${id}5`, - publishTime: 5, - }); - - const pubTime5AsHex64Bit = "0000000000000005"; - const ccipResp = await request(appWithDb) - .get("/api/get_vaa_ccip") - .query({ - data: "0x" + id + pubTime5AsHex64Bit, - }); - expect(ccipResp.status).toBe(StatusCodes.OK); - expect(ccipResp.body).toEqual({ - data: "0x" + Buffer.from(`pythnet${id}5`, "base64").toString("hex"), - }); - - dbApp.close(); - }); - - test( - "When called with valid id and timestamp not in the cache" + - "and not in the db returns vaa not found", - async () => { - const dbBackend = express(); - dbBackend.get("/vaa", (_req, res) => { - // Return an empty array when vaa is not there, this is the same - // behaviour as our api. - res.json([]); - }); - - const dbApp = dbBackend.listen({ port: 37777 }); - - const apiWithDb = new RestAPI( - { - port: 8889, - dbApiCluster: "pythnet", - dbApiEndpoint: "http://localhost:37777", - }, - priceInfo, - () => true - ); - const appWithDb = await apiWithDb.createApp(); - - const id = expandTo64Len("abcd"); - addAbcdDataToCache(id, vaasCache); - - const resp = await request(appWithDb) - .get("/api/get_vaa") - .query({ - id: "0x" + id, - publish_time: 5, - }); - expect(resp.status).toBe(StatusCodes.NOT_FOUND); - - const pubTime5AsHex64Bit = "0000000000000005"; - const ccipResp = await request(appWithDb) - .get("/api/get_vaa_ccip") - .query({ - data: "0x" + id + pubTime5AsHex64Bit, - }); - - // On CCIP we expect bad gateway so the client want to retry other ccip endpoints. - expect(ccipResp.status).toBe(StatusCodes.BAD_GATEWAY); - - dbApp.close(); - } - ); - - test( - "When called with valid id and timestamp not in the cache" + - "and db is not available returns internal server error", - async () => { - const apiWithDb = new RestAPI( - { - port: 8889, - dbApiCluster: "pythnet", - dbApiEndpoint: "http://localhost:37777", - }, - priceInfo, - () => true - ); - const appWithDb = await apiWithDb.createApp(); - - const id = expandTo64Len("abcd"); - addAbcdDataToCache(id, vaasCache); - - const resp = await request(appWithDb) - .get("/api/get_vaa") - .query({ - id: "0x" + id, - publish_time: 5, - }); - expect(resp.status).toBe(StatusCodes.INTERNAL_SERVER_ERROR); - - const pubTime5AsHex64Bit = "0000000000000005"; - const ccipResp = await request(appWithDb) - .get("/api/get_vaa_ccip") - .query({ - data: "0x" + id + pubTime5AsHex64Bit, - }); - expect(ccipResp.status).toBe(StatusCodes.INTERNAL_SERVER_ERROR); - } - ); - - test("vaaToPriceInfo works with accumulator update data", () => { - // An update data taken from Hermes with the following price feed: - // { - // "id":"e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", - // "price":{"price":"2836040669135","conf":"3282830965","expo":-8,"publish_time":1692280808}, - // "ema_price":{"price":"2845324900000","conf":"3211773100","expo":-8,"publish_time":1692280808}, - // "metadata":{"slot":89783664,"emitter_chain":26,"price_service_receive_time":1692280809} - // } - const updateData = Buffer.from( - "UE5BVQEAAAADuAEAAAADDQAsKPsmb7Vz7io3taJQKgoi1m/z0kqKgtpmlkv+ZuunX2Iegsf+8fuUtpHPLKgCWPU8PN2x9NyAZz5" + - "BY9M3SWwJAALYlM0U7f2GFWfEjKwSJlHZ5sf+n6KXCocVC66ImS2o0TD0SBhTWcp0KdcuzR1rY1jfIHaFpVneroRLbTjNrk/WAA" + - "MuAYxPVPf1DR30wYQo12Dbf+in3akTjhKERNQ+nPwRjxAyIQD+52LU3Rh2VL7nOIStMNTiBMaiWHywaPoXowWAAQbillhhX4MR+" + - "7h81PfxHIbiXBmER4c5M7spilWKkROb+VXhrqnVJL162t9TdhYk56PDIhvXO1Tm/ldjVJw130y0AAk6qpccfsxDZEmVN8LI4z87" + - "39Ni/kb+CB3yW2l2dWhKTjBeNanhK6TCCoNH/jRzWfrjrEk5zjNrUr82JwL4fR1OAQrYZescxbH26m8QHiH+RHzwlXpUKJgbHD5" + - "NnWtB7oFb9AFM15jbjd4yIEBEtAlXPE0Q4j+X+DLnCtZbLSQiYNh5AQvz70LTbYry1lEExuUcO+IRJiysw5AFyqZ9Y1E//WKIqg" + - "EysfcnHwoOxtDtAc5Z9sTUEYfPqQ1d27k3Yk0X7dvCAQ10cdG0qYHb+bQrYRIKKnb0aeCjkCs0HZQY2fXYmimyfTNfECclmPW9k" + - "+CfOvW0JKuFxC1l11zJ3zjsgN/peA8BAQ5oIFQGjq9qmf5gegE1DjuzXsGksKao6nsjTXYIspCczCe2h5KNQ9l5hws11hauUKS2" + - "0JoOYjHwxPD2x0adJKvkAQ+4UjVcZgVEQP8y3caqUDH81Ikcadz2bESpYg93dpnzZTH6A7Ue+RL34PTNx6cCRzukwQuhiStuyL1" + - "WYEIrLI4nABAjGv3EBXjWaPLUj59OzVnGkzxkr6C4KDjMmpsYNzx7I2lp2iQV46TM78El8i9h7twiEDUOSdC5CmfQjRpkP72yAB" + - "GVAQELUm2/SjkpF0O+/rVDgA/Y2/wMacD1ZDahdyvSNSFThn5NyRYA1JXGgIDxoYeAZgkr1gL1cjCLWiO+Bs9QARIiCvHfIkn2a" + - "YhYHQq/u6cHB/2DxE3OgbCZyTv8OVO55hQDkJ1gDwAec+IJ4M5Od4OxWEu+OywhJT7zUmwZko9MAGTeJ+kAAAAAABrhAfrtrFhR" + - "4yubI7X5QRqMK6xKrj7U3XuBHdGnLqSqcQAAAAAAWllxAUFVV1YAAAAAAAVZ/XAAACcQ8Xfx5wQ+nj1rn6IeTUAy+VER1nUBAFU" + - "A5i32yLSoX+GmfbRNwS3l2zMPesZrctxliv7fD0pBW0MAAAKUUTJXzwAAAADDrAZ1////+AAAAABk3ifoAAAAAGTeJ+cAAAKWep" + - "R2oAAAAAC/b8SsCasjFzENKvXWwOycuzCVaDWfm0IuuuesmamDKl2lNXss15orlNN+xHVNEEIIq7Xg8GRZGVLt43fkg7xli6EPQ" + - "/Nyxl6SixiYteNt1uTTh4M1lQTUjPxKnkE5JEea4RnhOWgmSAWMf8ft4KgE7hvRifV1JP0rOsNgsOYFRbs6iDKW1qLpxgZLMAiO" + - "clwS3Tjw2hj8sPfq1NHeVttsBEK5SIM14GjAuD/p2V0+NqHqMHxU/kfftg==", - "base64" - ); - - const priceInfo = RestAPI.vaaToPriceInfo( - "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", - updateData - ); - - expect(priceInfo).toBeDefined(); - expect(priceInfo?.priceFeed).toEqual( - new PriceFeed({ - id: "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", - price: new Price({ - price: "2836040669135", - conf: "3282830965", - publishTime: 1692280808, - expo: -8, - }), - emaPrice: new Price({ - price: "2845324900000", - conf: "3211773100", - publishTime: 1692280808, - expo: -8, - }), - }) - ); - expect(priceInfo?.emitterChainId).toEqual(26); - }); -}); diff --git a/price_service/server/src/__tests__/vaa.test.ts b/price_service/server/src/__tests__/vaa.test.ts deleted file mode 100644 index 2a2dcdef..00000000 --- a/price_service/server/src/__tests__/vaa.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { isValidVaa } from "../vaa"; -import { GuardianSignature, parseVaa } from "@certusone/wormhole-sdk"; -import { randomBytes } from "crypto"; - -const VAA = Buffer.from( - "AQAAAAMNABIL4Zs/yZlmGGUiAZujW8PMYR2ffWKTcuSNMRL+Yr3uQrVO1qxLToA8iksg/NWfsD3NeMSJujxSgd4fnjmqtSYBAiP92Eci7vsIVouS93bSack2bYg5ERXxZpcTb9LSWpEmILv62jbAd1HcbWu1w8WVbm++nqgbHH5S8eUY57QytegABIBcyvERWN2j9kb74zvQy+AEfXW6wjbrRKzlMvOUaKYpMG9nRzXkxd6wehsVFgV+i3G/lykR1hcrvgIczEPCuIYACPApsIJGheEpt/VQ4d36Tc0ZMzqq/kw1mTDJ8eKHikHeL8yFfo+Q9PtYK0CF1UYTKVpl32kFTtU+ubdKM7oVHMYBCiw25jnpX5+KOzxSTy+9Q5ovM3zqcN3yJBSbF80VL9N2AnehBhMTr1DylzpcYppdly4w/Iz5OHFGoZqT8dVgeY0AC0MseKj4EN0XUIGj8kXQ0CZKczfxywJPiueGTkAD6VkAOwpxnfZu212yXHAbojECKqtRCvb4UobTu+RK0pyemb0BDJKvSJ8RALV4CAGGWiS7XzHfa+/SxzCB6zxUsiOh0FGGEZBK+6i//7YUY83TOXp5SZzDGA0aH5tLXd6peL6np4ABDeHulcBX2LA1cIpmH+nqQLRq5zDPlKNBa6RVwHQUBVotBAWnCoTjOv+8xPZssl7r/BidPUbdu7j+0MGB/4/Oh6wBDub405biSsppFuBFxrBuFrJJdnsf3NvU5TWKF61aZKFtcWpxzyxNDsB3Nd7g+QYafiMkyL4okvvcthYaoiEzwX0BD1qhc5333/TKKbInkZcsitd0F/isWptZygRNqsh29f/xNuFyD4915mNWtsx3OaRAAkPcq21YzJb7ObzUB0OjhVcBEK46eqvVfpDHkF/w6+GWKACsICaAdgwDkmEwrCxXY2BgJe7cXkmDGl0Sfl8836AHd5OBwIC7g7EldFkLUanUUUwAEWpFfXwzaAnMQp+bO3RHKnpbPvJgKacjxFaCExe7dNkvYcVQ4UEC13QqIK3k7egZpHZp45O9AXfwmtpbBlJAvlgAEgu9te25pvTJ2alsQsxicrf5QyhDT7P6Ywr2WbNUnsfXKPFPC3U1P3G1yQOIjbUhrFtYkEGQ1+uZ4rNxsq2CchwBZGbcRwAAAAAAGvjNI8KrkSN3MHcLvqCNYQBc3aCYQ0jz9u7LVZY4wLugAAAAABlYLUwBUDJXSAADAAEAAQIABQCdLvoSNauGwJNctCSxAr5PIX500RCd+edd+oM4/A8JCHgvlYYrBFZwzSK+4xFMOXY6Sgi+62Y7FF0oPDHX0RAcTwAAAActs1rgAAAAAAFPo9T////4AAAABy9GhdAAAAAAARhiQwEAAAARAAAAFwAAAABkZtxHAAAAAGRm3EcAAAAAZGbcRgAAAActwjt4AAAAAAFAwzwAAAAAZGbcRkjWAz1zPieVDC4DUeJQVJHNkVSCT3FtlRNRTHS5+Y9YPdK2NoakUOxykN86HgtYPASB9lE1Ht+nY285rtVc+KMAAAACruGVUAAAAAAAv9F3////+AAAAAKux0rYAAAAAAC3HSIBAAAAFQAAABwAAAAAZGbcRwAAAABkZtxHAAAAAGRm3EYAAAACruGVUAAAAAAAv9F3AAAAAGRm3EY1FbOGHo/pPl9UC6QHfCFkBHgrhtXngHezy/0nMTqzvOYt9si0qF/hpn20TcEt5dszD3rGa3LcZYr+3w9KQVtDAAACcHgoTeAAAAAAJ08r4P////gAAAJwomgKAAAAAAAiQ9syAQAAABUAAAAfAAAAAGRm3EcAAAAAZGbcRwAAAABkZtxGAAACcHOoRAAAAAAAJlVaXAAAAABkZtxGm19z4AdefXA3YBIYDdupQnL2jYXq5BBOM1VhyYIlPUGhnQSsaWx6ZhbSkcfl0Td8yL5DfDJ7da213ButdF/K6AAAAAAE4NsJAAAAAAABWWT////4AAAAAATkLakAAAAAAAEvVQEAAAALAAAACwAAAABkZtxHAAAAAGRm3EcAAAAAZGbcRgAAAAAE4NsJAAAAAAABWWQAAAAAZGbcRuh2/NEwrdiYSjOqtSrza8G5+CLJ6+N286py1jCXThXw3O9Q3QpM0tzBfkXfFnbcszahGmHGnfegKZsBUMZy0lwAAAAAAG/y7wAAAAAAABJP////+AAAAAAAb/S7AAAAAAAAEfUBAAAAFQAAAB4AAAAAZGbcRwAAAABkZtxHAAAAAGRm3EYAAAAAAG/zhgAAAAAAABLmAAAAAGRm3EY=", - "base64" -); - -describe("VAA validation works", () => { - test("with valid signatures", async () => { - let parsedVaa = parseVaa(VAA); - - expect(isValidVaa(parsedVaa, "mainnet")).toBe(true); - }); - - test("with a wrong address", async () => { - let parsedVaa = parseVaa(VAA); - const vaaIndex = 8; - const setIndex1 = 4; - const setIndex2 = 5; - - // Replace the signature from guardian at setIndex1 with the one from - // setIndex2. - parsedVaa.guardianSignatures[vaaIndex] = { - index: setIndex1, - signature: parsedVaa.guardianSignatures[setIndex2].signature, - }; - - expect(isValidVaa(parsedVaa, "mainnet")).toBe(false); - }); - - test("with an invalid signature", async () => { - let parsedVaa = parseVaa(VAA); - const vaaIndex = 8; - const setIndex = 4; - - // Inject a random buffer as the signature of the guardian at setIndex. - parsedVaa.guardianSignatures[vaaIndex] = { - index: setIndex, - signature: randomBytes(65), // invalid signature - }; - - expect(isValidVaa(parsedVaa, "mainnet")).toBe(false); - }); -}); diff --git a/price_service/server/src/__tests__/ws.test.ts b/price_service/server/src/__tests__/ws.test.ts deleted file mode 100644 index ba4cffcc..00000000 --- a/price_service/server/src/__tests__/ws.test.ts +++ /dev/null @@ -1,516 +0,0 @@ -import { - HexString, - Price, - PriceFeed, - PriceFeedMetadata, -} from "@pythnetwork/price-service-sdk"; -import { Server } from "http"; -import { WebSocket, WebSocketServer } from "ws"; -import { sleep } from "../helpers"; -import { PriceInfo, PriceStore } from "../listen"; -import { ClientMessage, WebSocketAPI } from "../ws"; - -const port = 2524; - -let api: WebSocketAPI; -let server: Server; -let wss: WebSocketServer; - -let priceInfos: PriceInfo[]; - -function expandTo64Len(id: string): string { - return id.repeat(64).substring(0, 64); -} - -function dummyPriceInfo(id: HexString, vaa: HexString): PriceInfo { - return { - seqNum: 1, - publishTime: 0, - attestationTime: 2, - emitterChainId: 3, - priceFeed: dummyPriceFeed(id), - vaa: Buffer.from(vaa, "hex"), - priceServiceReceiveTime: 4, - lastAttestedPublishTime: -1, - }; -} - -function dummyPriceFeed(id: string): PriceFeed { - return new PriceFeed({ - emaPrice: new Price({ - conf: "1", - expo: 2, - price: "3", - publishTime: 4, - }), - id, - price: new Price({ - conf: "5", - expo: 6, - price: "7", - publishTime: 8, - }), - }); -} - -async function waitForSocketState( - client: WebSocket, - state: number -): Promise { - while (client.readyState !== state) { - await sleep(10); - } -} - -async function waitForMessages(messages: any[], cnt: number): Promise { - while (messages.length < cnt) { - await sleep(10); - } -} - -async function createSocketClient(): Promise<[WebSocket, any[]]> { - const client = new WebSocket(`ws://localhost:${port}/ws`); - - await waitForSocketState(client, client.OPEN); - - const messages: any[] = []; - - client.on("message", (data) => { - messages.push(JSON.parse(data.toString())); - }); - - return [client, messages]; -} - -beforeAll(async () => { - priceInfos = [ - dummyPriceInfo(expandTo64Len("abcd"), "a1b2c3d4"), - dummyPriceInfo(expandTo64Len("ef01"), "a1b2c3d4"), - dummyPriceInfo(expandTo64Len("2345"), "bad01bad"), - dummyPriceInfo(expandTo64Len("6789"), "bidbidbid"), - ]; - - const priceInfo: PriceStore = { - getLatestPriceInfo: (_priceFeedId: string) => undefined, - addUpdateListener: (_callback: (priceInfo: PriceInfo) => any) => undefined, - getPriceIds: () => new Set(priceInfos.map((info) => info.priceFeed.id)), - getVaa: (_vaasCacheKey: string) => undefined, - }; - - api = new WebSocketAPI(priceInfo); - - server = new Server(); - server.listen(port); - - wss = api.run(server); -}); - -afterAll(async () => { - wss.close(); - server.close(); -}); - -describe("Client receives data", () => { - test("When subscribes with valid ids without verbose flag, returns correct price feed", async () => { - const [client, serverMessages] = await createSocketClient(); - - const message: ClientMessage = { - ids: [priceInfos[0].priceFeed.id, priceInfos[1].priceFeed.id], - type: "subscribe", - }; - - client.send(JSON.stringify(message)); - - await waitForMessages(serverMessages, 1); - - expect(serverMessages[0]).toStrictEqual({ - type: "response", - status: "success", - }); - - api.dispatchPriceFeedUpdate(priceInfos[0]); - - await waitForMessages(serverMessages, 2); - - expect(serverMessages[1]).toEqual({ - type: "price_update", - price_feed: priceInfos[0].priceFeed.toJson(), - }); - - api.dispatchPriceFeedUpdate(priceInfos[1]); - - await waitForMessages(serverMessages, 3); - - expect(serverMessages[2]).toEqual({ - type: "price_update", - price_feed: priceInfos[1].priceFeed.toJson(), - }); - - client.close(); - await waitForSocketState(client, client.CLOSED); - }); - - test("When subscribes with valid ids and verbose flag set to true, returns correct price feed with metadata", async () => { - const [client, serverMessages] = await createSocketClient(); - - const message: ClientMessage = { - ids: [priceInfos[0].priceFeed.id, priceInfos[1].priceFeed.id], - type: "subscribe", - verbose: true, - }; - - client.send(JSON.stringify(message)); - - await waitForMessages(serverMessages, 1); - - expect(serverMessages[0]).toStrictEqual({ - type: "response", - status: "success", - }); - - api.dispatchPriceFeedUpdate(priceInfos[0]); - - await waitForMessages(serverMessages, 2); - - expect(serverMessages[1]).toEqual({ - type: "price_update", - price_feed: { - ...priceInfos[0].priceFeed.toJson(), - metadata: new PriceFeedMetadata({ - attestationTime: 2, - emitterChain: 3, - receiveTime: 4, - sequenceNumber: 1, - }).toJson(), - }, - }); - - api.dispatchPriceFeedUpdate(priceInfos[1]); - - await waitForMessages(serverMessages, 3); - - expect(serverMessages[2]).toEqual({ - type: "price_update", - price_feed: { - ...priceInfos[1].priceFeed.toJson(), - metadata: new PriceFeedMetadata({ - attestationTime: 2, - emitterChain: 3, - receiveTime: 4, - sequenceNumber: 1, - }).toJson(), - }, - }); - - client.close(); - await waitForSocketState(client, client.CLOSED); - }); - - test("When subscribes with valid ids and verbose flag set to false, returns correct price feed without metadata", async () => { - const [client, serverMessages] = await createSocketClient(); - - const message: ClientMessage = { - ids: [priceInfos[0].priceFeed.id, priceInfos[1].priceFeed.id], - type: "subscribe", - verbose: false, - }; - - client.send(JSON.stringify(message)); - - await waitForMessages(serverMessages, 1); - - expect(serverMessages[0]).toStrictEqual({ - type: "response", - status: "success", - }); - - api.dispatchPriceFeedUpdate(priceInfos[0]); - - await waitForMessages(serverMessages, 2); - - expect(serverMessages[1]).toEqual({ - type: "price_update", - price_feed: priceInfos[0].priceFeed.toJson(), - }); - - api.dispatchPriceFeedUpdate(priceInfos[1]); - - await waitForMessages(serverMessages, 3); - - expect(serverMessages[2]).toEqual({ - type: "price_update", - price_feed: priceInfos[1].priceFeed.toJson(), - }); - - client.close(); - await waitForSocketState(client, client.CLOSED); - }); - - test("When subscribes with valid ids and binary flag set to true, returns correct price feed with vaa", async () => { - const [client, serverMessages] = await createSocketClient(); - - const message: ClientMessage = { - ids: [priceInfos[0].priceFeed.id, priceInfos[1].priceFeed.id], - type: "subscribe", - binary: true, - }; - - client.send(JSON.stringify(message)); - - await waitForMessages(serverMessages, 1); - - expect(serverMessages[0]).toStrictEqual({ - type: "response", - status: "success", - }); - - api.dispatchPriceFeedUpdate(priceInfos[0]); - - await waitForMessages(serverMessages, 2); - - expect(serverMessages[1]).toEqual({ - type: "price_update", - price_feed: { - ...priceInfos[0].priceFeed.toJson(), - vaa: priceInfos[0].vaa.toString("base64"), - }, - }); - - api.dispatchPriceFeedUpdate(priceInfos[1]); - - await waitForMessages(serverMessages, 3); - - expect(serverMessages[2]).toEqual({ - type: "price_update", - price_feed: { - ...priceInfos[1].priceFeed.toJson(), - vaa: priceInfos[1].vaa.toString("base64"), - }, - }); - - client.close(); - await waitForSocketState(client, client.CLOSED); - }); - - test("When subscribes with valid ids and binary flag set to false, returns correct price feed without vaa", async () => { - const [client, serverMessages] = await createSocketClient(); - - const message: ClientMessage = { - ids: [priceInfos[0].priceFeed.id, priceInfos[1].priceFeed.id], - type: "subscribe", - binary: false, - }; - - client.send(JSON.stringify(message)); - - await waitForMessages(serverMessages, 1); - - expect(serverMessages[0]).toStrictEqual({ - type: "response", - status: "success", - }); - - api.dispatchPriceFeedUpdate(priceInfos[0]); - - await waitForMessages(serverMessages, 2); - - expect(serverMessages[1]).toEqual({ - type: "price_update", - price_feed: priceInfos[0].priceFeed.toJson(), - }); - - api.dispatchPriceFeedUpdate(priceInfos[1]); - - await waitForMessages(serverMessages, 3); - - expect(serverMessages[2]).toEqual({ - type: "price_update", - price_feed: priceInfos[1].priceFeed.toJson(), - }); - - client.close(); - await waitForSocketState(client, client.CLOSED); - }); - - test("When subscribes with invalid ids, returns error", async () => { - const [client, serverMessages] = await createSocketClient(); - - const message: ClientMessage = { - ids: [expandTo64Len("aaaa")], - type: "subscribe", - }; - - client.send(JSON.stringify(message)); - - await waitForMessages(serverMessages, 1); - - expect(serverMessages.length).toBe(1); - expect(serverMessages[0].type).toBe("response"); - expect(serverMessages[0].status).toBe("error"); - - client.close(); - await waitForSocketState(client, client.CLOSED); - }); - - test("When subscribes for Price Feed A, doesn't receive updates for Price Feed B", async () => { - const [client, serverMessages] = await createSocketClient(); - - const message: ClientMessage = { - ids: [priceInfos[0].priceFeed.id], - type: "subscribe", - }; - - client.send(JSON.stringify(message)); - - await waitForMessages(serverMessages, 1); - - expect(serverMessages[0]).toStrictEqual({ - type: "response", - status: "success", - }); - - api.dispatchPriceFeedUpdate(priceInfos[1]); - - await sleep(100); - - api.dispatchPriceFeedUpdate(priceInfos[0]); - - await waitForMessages(serverMessages, 2); - - expect(serverMessages[1]).toEqual({ - type: "price_update", - price_feed: priceInfos[0].priceFeed.toJson(), - }); - - await sleep(100); - expect(serverMessages.length).toBe(2); - - client.close(); - await waitForSocketState(client, client.CLOSED); - }); - - test("When subscribes for Price Feed A, receives updated and when unsubscribes stops receiving", async () => { - const [client, serverMessages] = await createSocketClient(); - - let message: ClientMessage = { - ids: [priceInfos[0].priceFeed.id], - type: "subscribe", - }; - - client.send(JSON.stringify(message)); - - await waitForMessages(serverMessages, 1); - - expect(serverMessages[0]).toStrictEqual({ - type: "response", - status: "success", - }); - - api.dispatchPriceFeedUpdate(priceInfos[0]); - - await waitForMessages(serverMessages, 2); - - expect(serverMessages[1]).toEqual({ - type: "price_update", - price_feed: priceInfos[0].priceFeed.toJson(), - }); - - message = { - ids: [priceInfos[0].priceFeed.id], - type: "unsubscribe", - }; - - client.send(JSON.stringify(message)); - - await waitForMessages(serverMessages, 3); - - expect(serverMessages[2]).toStrictEqual({ - type: "response", - status: "success", - }); - - api.dispatchPriceFeedUpdate(priceInfos[0]); - - await sleep(100); - - expect(serverMessages.length).toBe(3); - - client.close(); - await waitForSocketState(client, client.CLOSED); - }); - - test("Unsubscribe on not subscribed price feed is ok", async () => { - const [client, serverMessages] = await createSocketClient(); - - const message: ClientMessage = { - ids: [priceInfos[0].priceFeed.id], - type: "unsubscribe", - }; - - client.send(JSON.stringify(message)); - - await waitForMessages(serverMessages, 1); - - expect(serverMessages[0]).toStrictEqual({ - type: "response", - status: "success", - }); - - client.close(); - await waitForSocketState(client, client.CLOSED); - }); - - test("Multiple clients with different price feed works", async () => { - const [client1, serverMessages1] = await createSocketClient(); - const [client2, serverMessages2] = await createSocketClient(); - - const message1: ClientMessage = { - ids: [priceInfos[0].priceFeed.id], - type: "subscribe", - }; - - client1.send(JSON.stringify(message1)); - - const message2: ClientMessage = { - ids: [priceInfos[1].priceFeed.id], - type: "subscribe", - }; - - client2.send(JSON.stringify(message2)); - - await waitForMessages(serverMessages1, 1); - await waitForMessages(serverMessages2, 1); - - expect(serverMessages1[0]).toStrictEqual({ - type: "response", - status: "success", - }); - - expect(serverMessages2[0]).toStrictEqual({ - type: "response", - status: "success", - }); - - api.dispatchPriceFeedUpdate(priceInfos[0]); - api.dispatchPriceFeedUpdate(priceInfos[1]); - - await waitForMessages(serverMessages1, 2); - await waitForMessages(serverMessages2, 2); - - expect(serverMessages1[1]).toEqual({ - type: "price_update", - price_feed: priceInfos[0].priceFeed.toJson(), - }); - - expect(serverMessages2[1]).toEqual({ - type: "price_update", - price_feed: priceInfos[1].priceFeed.toJson(), - }); - - client1.close(); - client2.close(); - - await waitForSocketState(client1, client1.CLOSED); - await waitForSocketState(client2, client2.CLOSED); - }); -}); diff --git a/price_service/server/src/encoding.ts b/price_service/server/src/encoding.ts deleted file mode 100644 index e453119e..00000000 --- a/price_service/server/src/encoding.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Utilities for encoding VAAs for specific target chains - -// List of all possible target chains. Note that "default" is an option because we need at least one chain -// with a base64 encoding (which is the old default behavior of all API methods). -export type TargetChain = "evm" | "cosmos" | "aptos" | "default"; -export const validTargetChains = ["evm", "cosmos", "aptos", "default"]; -export const defaultTargetChain: TargetChain = "default"; - -// Possible encodings of the binary VAA data as a string. -// "0x" is the same as "hex" with a leading "0x" prepended to the hex string. -export type VaaEncoding = "base64" | "hex" | "0x"; -export const defaultVaaEncoding: VaaEncoding = "base64"; -export const chainToEncoding: Record = { - evm: "0x", - cosmos: "base64", - // TODO: I think aptos actually wants a number[] for this data... need to decide how to - // handle that case. - aptos: "base64", - default: "base64", -}; - -// Given a VAA represented as either a string in base64 or a Buffer, encode it as a string -// appropriate for the given targetChain. -export function encodeVaaForChain( - vaa: string | Buffer, - targetChain: TargetChain -): string { - const encoding = chainToEncoding[targetChain]; - - let vaaBuffer: Buffer; - if (typeof vaa === "string") { - if (encoding === defaultVaaEncoding) { - return vaa; - } else { - vaaBuffer = Buffer.from(vaa, defaultVaaEncoding as BufferEncoding); - } - } else { - vaaBuffer = vaa; - } - - switch (encoding) { - case "0x": - return "0x" + vaaBuffer.toString("hex"); - default: - return vaaBuffer.toString(encoding); - } -} diff --git a/price_service/server/src/helpers.ts b/price_service/server/src/helpers.ts deleted file mode 100644 index a5a3abdc..00000000 --- a/price_service/server/src/helpers.ts +++ /dev/null @@ -1,45 +0,0 @@ -// Time in seconds -export type TimestampInSec = number; -export type DurationInSec = number; -export type DurationInMs = number; - -export function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -// Shorthand for optional/mandatory envs -export function envOrErr(env: string): string { - const val = process.env[env]; - if (!val) { - throw new Error(`environment variable "${env}" must be set`); - } - return String(process.env[env]); -} - -export function parseToOptionalNumber( - s: string | undefined -): number | undefined { - if (s === undefined) { - return undefined; - } - - return parseInt(s, 10); -} - -export function removeLeading0x(s: string): string { - if (s.startsWith("0x")) { - return s.substring(2); - } - - return s; -} - -// Helper for treating T | undefined as an optional value. This lets you pick a -// default if value is undefined. -export function getOrElse(value: T | undefined, defaultValue: T): T { - if (value === undefined) { - return defaultValue; - } else { - return value; - } -} diff --git a/price_service/server/src/index.ts b/price_service/server/src/index.ts deleted file mode 100644 index 4646b40b..00000000 --- a/price_service/server/src/index.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { envOrErr, parseToOptionalNumber } from "./helpers"; -import { Listener } from "./listen"; -import { initLogger } from "./logging"; -import { PromClient } from "./promClient"; -import { RestAPI } from "./rest"; -import { WebSocketAPI } from "./ws"; - -let configFile: string = ".env"; -if (process.env.PYTH_PRICE_SERVICE_CONFIG) { - configFile = process.env.PYTH_PRICE_SERVICE_CONFIG; -} - -// tslint:disable:no-console -console.log("Loading config file [%s]", configFile); -// tslint:disable:no-var-requires -require("dotenv").config({ path: configFile }); - -// Set up the logger. -initLogger({ logLevel: process.env.LOG_LEVEL }); - -async function run() { - const promClient = new PromClient({ - name: "price_service", - port: parseInt(envOrErr("PROM_PORT"), 10), - }); - - const listener = new Listener( - { - spyServiceHost: envOrErr("SPY_SERVICE_HOST"), - filtersRaw: process.env.SPY_SERVICE_FILTERS, - wormholeCluster: process.env.WORMHOLE_CLUSTER, - readiness: { - spySyncTimeSeconds: parseInt( - envOrErr("READINESS_SPY_SYNC_TIME_SECONDS"), - 10 - ), - numLoadedSymbols: parseInt( - envOrErr("READINESS_NUM_LOADED_SYMBOLS"), - 10 - ), - }, - cacheCleanupLoopInterval: parseToOptionalNumber( - process.env.REMOVE_EXPIRED_VALUES_INTERVAL_SECONDS - ), - cacheTtl: parseToOptionalNumber(process.env.CACHE_TTL_SECONDS), - }, - promClient - ); - - // In future if we have more components we will modify it to include them all - const isReady = () => listener.isReady(); - - const restAPI = new RestAPI( - { - port: parseInt(envOrErr("REST_PORT"), 10), - dbApiEndpoint: process.env.DB_API_ENDPOINT, - dbApiCluster: process.env.DB_API_CLUSTER, - }, - listener, - isReady, - promClient - ); - - const wsAPI = new WebSocketAPI(listener, promClient); - - listener.run(); - - const server = await restAPI.run(); - wsAPI.run(server); -} - -run(); diff --git a/price_service/server/src/listen.ts b/price_service/server/src/listen.ts deleted file mode 100644 index 2c04e0d1..00000000 --- a/price_service/server/src/listen.ts +++ /dev/null @@ -1,469 +0,0 @@ -import { - createSpyRPCServiceClient, - subscribeSignedVAA, -} from "@certusone/wormhole-spydk"; - -import { ChainId, uint8ArrayToHex, parseVaa } from "@certusone/wormhole-sdk"; - -import { - FilterEntry, - SubscribeSignedVAAResponse, -} from "@certusone/wormhole-spydk/lib/cjs/proto/spy/v1/spy"; -import { ClientReadableStream } from "@grpc/grpc-js"; -import { - getBatchSummary, - parseBatchPriceAttestation, - priceAttestationToPriceFeed, - PriceAttestation, -} from "@pythnetwork/wormhole-attester-sdk"; -import { HexString, PriceFeed } from "@pythnetwork/price-service-sdk"; -import LRUCache from "lru-cache"; -import { DurationInSec, sleep, TimestampInSec } from "./helpers"; -import { logger } from "./logging"; -import { PromClient } from "./promClient"; -import { isValidVaa, WormholeCluster, wormholeClusterFromString } from "./vaa"; - -export type PriceInfo = { - vaa: Buffer; - seqNum: number; - publishTime: TimestampInSec; - attestationTime: TimestampInSec; - lastAttestedPublishTime: TimestampInSec; - priceFeed: PriceFeed; - emitterChainId: number; - priceServiceReceiveTime: number; -}; - -export function createPriceInfo( - priceAttestation: PriceAttestation, - vaa: Buffer, - sequence: bigint, - emitterChain: number -): PriceInfo { - const priceFeed = priceAttestationToPriceFeed(priceAttestation); - return { - seqNum: Number(sequence), - vaa, - publishTime: priceAttestation.publishTime, - attestationTime: priceAttestation.attestationTime, - lastAttestedPublishTime: priceAttestation.lastAttestedPublishTime, - priceFeed, - emitterChainId: emitterChain, - priceServiceReceiveTime: Math.floor(new Date().getTime() / 1000), - }; -} - -export interface PriceStore { - getPriceIds(): Set; - getLatestPriceInfo(priceFeedId: HexString): PriceInfo | undefined; - addUpdateListener(callback: (priceInfo: PriceInfo) => any): void; - getVaa(priceFeedId: string, publishTime: number): VaaConfig | undefined; -} - -type ListenerReadinessConfig = { - spySyncTimeSeconds: number; - numLoadedSymbols: number; -}; - -type ListenerConfig = { - spyServiceHost: string; - wormholeCluster?: string; - filtersRaw?: string; - readiness: ListenerReadinessConfig; - webApiEndpoint?: string; - webApiCluster?: string; - cacheCleanupLoopInterval?: DurationInSec; - cacheTtl?: DurationInSec; -}; - -type VaaKey = string; - -export type VaaConfig = { - publishTime: number; - lastAttestedPublishTime: number; - vaa: string; -}; - -export class VaaCache { - private cache: Map; - private ttl: DurationInSec; - private cacheCleanupLoopInterval: DurationInSec; - - constructor( - ttl: DurationInSec = 300, - cacheCleanupLoopInterval: DurationInSec = 60 - ) { - this.cache = new Map(); - this.ttl = ttl; - this.cacheCleanupLoopInterval = cacheCleanupLoopInterval; - } - - set( - key: VaaKey, - publishTime: TimestampInSec, - lastAttestedPublishTime: TimestampInSec, - vaa: string - ): void { - if (this.cache.has(key)) { - this.cache.get(key)!.push({ publishTime, lastAttestedPublishTime, vaa }); - } else { - this.cache.set(key, [{ publishTime, lastAttestedPublishTime, vaa }]); - } - } - - get(key: VaaKey, publishTime: TimestampInSec): VaaConfig | undefined { - if (!this.cache.has(key)) { - return undefined; - } else { - const vaaConf = this.find(this.cache.get(key)!, publishTime); - return vaaConf; - } - } - - private find( - arr: VaaConfig[], - publishTime: TimestampInSec - ): VaaConfig | undefined { - // If the publishTime is less than the first element we are - // not sure that this VAA is actually the first VAA after that - // time. - if (arr.length === 0 || publishTime < arr[0].publishTime) { - return undefined; - } - let left = 0; - let right = arr.length - 1; - let nextLargest = -1; - - while (left <= right) { - const middle = Math.floor((left + right) / 2); - if ( - arr[middle].publishTime === publishTime && - arr[middle].lastAttestedPublishTime < publishTime - ) { - return arr[middle]; - } else if (arr[middle].publishTime < publishTime) { - left = middle + 1; - } else { - nextLargest = middle; - right = middle - 1; - } - } - - return nextLargest !== -1 ? arr[nextLargest] : undefined; - } - - async removeExpiredValues() { - const now = Math.floor(Date.now() / 1000); - for (const key of this.cache.keys()) { - this.cache.set( - key, - this.cache - .get(key)! - .filter((vaaConf) => now - vaaConf.publishTime < this.ttl) - ); - } - } - - runRemoveExpiredValuesLoop() { - setInterval( - this.removeExpiredValues.bind(this), - this.cacheCleanupLoopInterval * 1000 - ); - } -} - -export class Listener implements PriceStore { - // Mapping of Price Feed Id to Vaa - private priceFeedVaaMap = new Map(); - private promClient: PromClient | undefined; - private spyServiceHost: string; - private filters: FilterEntry[] = []; - private ignorePricesOlderThanSecs: number; - private spyConnectionTime: TimestampInSec | undefined; - private readinessConfig: ListenerReadinessConfig; - private updateCallbacks: ((priceInfo: PriceInfo) => any)[]; - private observedVaas: LRUCache; - private vaasCache: VaaCache; - private wormholeCluster: WormholeCluster; - - constructor(config: ListenerConfig, promClient?: PromClient) { - this.promClient = promClient; - this.spyServiceHost = config.spyServiceHost; - this.loadFilters(config.filtersRaw); - // Don't store any prices received from wormhole that are over 5 minutes old. - this.ignorePricesOlderThanSecs = 60; - this.readinessConfig = config.readiness; - this.updateCallbacks = []; - this.observedVaas = new LRUCache({ - max: 10000, // At most 10000 items - ttl: 60 * 1000, // 1 minutes which is equal to ignorePricesOlderThanSecs - }); - this.vaasCache = new VaaCache( - config.cacheTtl, - config.cacheCleanupLoopInterval - ); - if (config.wormholeCluster !== undefined) { - this.wormholeCluster = wormholeClusterFromString(config.wormholeCluster); - } else { - this.wormholeCluster = "mainnet"; - } - } - - private loadFilters(filtersRaw?: string) { - if (!filtersRaw) { - logger.info("No filters provided. Will process all signed VAAs"); - return; - } - - const parsedJsonFilters = JSON.parse(filtersRaw); - - for (const filter of parsedJsonFilters) { - const myChainId = parseInt(filter.chain_id, 10) as ChainId; - const myEmitterAddress = filter.emitter_address; - const myEmitterFilter: FilterEntry = { - emitterFilter: { - chainId: myChainId, - emitterAddress: myEmitterAddress, - }, - }; - logger.info( - "adding filter: chainId: [" + - myEmitterFilter.emitterFilter!.chainId + - "], emitterAddress: [" + - myEmitterFilter.emitterFilter!.emitterAddress + - "]" - ); - this.filters.push(myEmitterFilter); - } - - logger.info("loaded " + this.filters.length + " filters"); - } - - async run() { - logger.info( - "pyth_relay starting up, will listen for signed VAAs from " + - this.spyServiceHost - ); - - this.vaasCache.runRemoveExpiredValuesLoop(); - - while (true) { - let stream: ClientReadableStream | undefined; - try { - const client = createSpyRPCServiceClient(this.spyServiceHost); - stream = await subscribeSignedVAA(client, { filters: this.filters }); - - stream!.on("data", ({ vaaBytes }: { vaaBytes: Buffer }) => { - this.processVaa(vaaBytes); - }); - - this.spyConnectionTime = this.currentTimeInSeconds(); - - let connected = true; - stream!.on("error", (err: any) => { - logger.error("spy service returned an error: %o", err); - connected = false; - }); - - stream!.on("close", () => { - logger.error("spy service closed the connection!"); - connected = false; - }); - - logger.info("connected to spy service, listening for messages"); - - while (connected) { - await sleep(1000); - } - } catch (e) { - logger.error("spy service threw an exception: %o", e); - } - - if (stream) { - stream.destroy(); - } - this.spyConnectionTime = undefined; - - await sleep(1000); - logger.info("attempting to reconnect to the spy service"); - } - } - - isNewPriceInfo( - cachedInfo: PriceInfo | undefined, - observedInfo: PriceInfo - ): boolean { - if (cachedInfo === undefined) { - return true; - } - - if (cachedInfo.attestationTime < observedInfo.attestationTime) { - return true; - } - - if ( - cachedInfo.attestationTime === observedInfo.attestationTime && - cachedInfo.seqNum < observedInfo.seqNum - ) { - return true; - } - - return false; - } - - async processVaa(vaa: Buffer) { - const parsedVaa = parseVaa(vaa); - - const vaaEmitterAddressHex = Buffer.from(parsedVaa.emitterAddress).toString( - "hex" - ); - - const observedVaasKey: VaaKey = `${parsedVaa.emitterChain}#${vaaEmitterAddressHex}#${parsedVaa.sequence}`; - - if (this.observedVaas.has(observedVaasKey)) { - return; - } - - if (!isValidVaa(parsedVaa, this.wormholeCluster)) { - logger.info("Ignoring an invalid VAA"); - return; - } - - let batchAttestation; - - try { - batchAttestation = parseBatchPriceAttestation( - Buffer.from(parsedVaa.payload) - ); - } catch (e: any) { - logger.error(e, e.stack); - logger.error("Parsing failed. Dropping vaa: %o", parsedVaa); - return; - } - - if (batchAttestation.priceAttestations.length === 0) { - return; - } - - // Attestation time is the same in all feeds in the batch. - // Return early if an attestation is old to exclude it from - // the counter metric. - if ( - batchAttestation.priceAttestations[0].attestationTime < - this.currentTimeInSeconds() - this.ignorePricesOlderThanSecs - ) { - return; - } - - // There is no `await` clause to release the current thread since the previous check - // but this is here to ensure this is correct as the code evolves. - if (this.observedVaas.has(observedVaasKey)) { - return; - } else { - this.observedVaas.set(observedVaasKey, true); - this.promClient?.incReceivedVaa(); - } - - for (const priceAttestation of batchAttestation.priceAttestations) { - const key = priceAttestation.priceId; - - const priceInfo = createPriceInfo( - priceAttestation, - vaa, - parsedVaa.sequence, - parsedVaa.emitterChain - ); - const cachedPriceInfo = this.priceFeedVaaMap.get(key); - - if (this.isNewPriceInfo(cachedPriceInfo, priceInfo)) { - this.vaasCache.set( - priceInfo.priceFeed.id, - priceInfo.publishTime, - priceInfo.lastAttestedPublishTime, - priceInfo.vaa.toString("base64") - ); - this.priceFeedVaaMap.set(key, priceInfo); - - if (cachedPriceInfo !== undefined) { - this.promClient?.addPriceUpdatesAttestationTimeGap( - priceAttestation.attestationTime - cachedPriceInfo.attestationTime - ); - this.promClient?.addPriceUpdatesPublishTimeGap( - priceAttestation.publishTime - cachedPriceInfo.publishTime - ); - } - - for (const callback of this.updateCallbacks) { - callback(priceInfo); - } - } - } - - logger.info( - "Parsed a new Batch Price Attestation: [" + - parsedVaa.emitterChain + - ":" + - uint8ArrayToHex(parsedVaa.emitterAddress) + - "], seqNum: " + - parsedVaa.sequence + - ", Batch Summary: " + - getBatchSummary(batchAttestation) - ); - } - - getVaa(priceFeedId: string, publishTime: number): VaaConfig | undefined { - return this.vaasCache.get(priceFeedId, publishTime); - } - - getLatestPriceInfo(priceFeedId: string): PriceInfo | undefined { - return this.priceFeedVaaMap.get(priceFeedId); - } - - addUpdateListener(callback: (priceInfo: PriceInfo) => any) { - this.updateCallbacks.push(callback); - } - - getPriceIds(): Set { - return new Set(this.priceFeedVaaMap.keys()); - } - - isReady(): boolean { - const currentTime: TimestampInSec = Math.floor(Date.now() / 1000); - if ( - this.spyConnectionTime === undefined || - currentTime < - this.spyConnectionTime + this.readinessConfig.spySyncTimeSeconds - ) { - return false; - } - if (this.priceFeedVaaMap.size < this.readinessConfig.numLoadedSymbols) { - return false; - } - - // if too many price feeds are stale it probably means that the price service - // is not receiving messages from Wormhole at all and is essentially dead. - const stalenessThreshold = 60; - const maxToleratedStaleFeeds = 10; - - const priceIds = [...this.getPriceIds()]; - let stalePriceCnt = 0; - - for (const priceId of priceIds) { - const latency = - currentTime - this.getLatestPriceInfo(priceId)!.attestationTime; - if (latency > stalenessThreshold) { - stalePriceCnt++; - } - } - - if (stalePriceCnt > maxToleratedStaleFeeds) { - return false; - } - - return true; - } - - private currentTimeInSeconds(): number { - return new Date().getTime() / 1000; - } -} diff --git a/price_service/server/src/logging.ts b/price_service/server/src/logging.ts deleted file mode 100644 index e74627db..00000000 --- a/price_service/server/src/logging.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as winston from "winston"; - -export let logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); - -// Logger should be initialized before using logger -export function initLogger(config?: { logLevel?: string }) { - let logLevel = "info"; - if (config?.logLevel) { - logLevel = config.logLevel; - } - - let transport: any; - // tslint:disable:no-console - console.log( - "price_service is logging to the console at level [%s]", - logLevel - ); - - transport = new winston.transports.Console({ - level: logLevel, - }); - - const logConfiguration = { - transports: [transport], - format: winston.format.combine( - winston.format.splat(), - winston.format.simple(), - winston.format.timestamp({ - format: "YYYY-MM-DD HH:mm:ss.SSS", - }), - winston.format.printf( - (info: any) => `${[info.timestamp]}|${info.level}|${info.message}` - ) - ), - }; - - logger = winston.createLogger(logConfiguration); -} diff --git a/price_service/server/src/promClient.ts b/price_service/server/src/promClient.ts deleted file mode 100644 index c9b00e3d..00000000 --- a/price_service/server/src/promClient.ts +++ /dev/null @@ -1,90 +0,0 @@ -import http = require("http"); -import client = require("prom-client"); -import { DurationInMs, DurationInSec } from "./helpers"; -import { logger } from "./logging"; - -// NOTE: To create a new metric: -// 1) Create a private counter/gauge with appropriate name and help in metrics section of PromHelper -// 2) Create a method to set the metric to a value (such as `incIncoming` function below) -// 3) Register the metric using `register.registerMetric` function. - -const SERVICE_PREFIX = "pyth__price_service__"; - -type WebSocketInteractionType = - | "connection" - | "close" - | "timeout" - | "server_update" - | "client_message"; - -export class PromClient { - private register = new client.Registry(); - - // Actual metrics - private receivedVaaCounter = new client.Counter({ - name: `${SERVICE_PREFIX}vaas_received`, - help: "number of Pyth VAAs received", - }); - private priceUpdatesPublishTimeGapHistogram = new client.Histogram({ - name: `${SERVICE_PREFIX}price_updates_publish_time_gap_seconds`, - help: "Summary of publish time gaps between price updates", - buckets: [1, 3, 5, 10, 15, 30, 60, 120], - }); - private priceUpdatesAttestationTimeGapHistogram = new client.Histogram({ - name: `${SERVICE_PREFIX}price_updates_attestation_time_gap_seconds`, - help: "Summary of attestation time gaps between price updates", - buckets: [1, 3, 5, 10, 15, 30, 60, 120], - }); - private webSocketInteractionCounter = new client.Counter({ - name: `${SERVICE_PREFIX}websocket_interaction`, - help: "number of Web Socket interactions", - labelNames: ["type", "status"], - }); - // End metrics - - private server = http.createServer(async (req, res) => { - if (req.url === "/metrics") { - // Return all metrics in the Prometheus exposition format - res.setHeader("Content-Type", this.register.contentType); - res.write(await this.register.metrics()); - res.end(); - } - }); - - constructor(config: { name: string; port: number }) { - this.register.setDefaultLabels({ - app: config.name, - }); - // Register each metric - this.register.registerMetric(this.receivedVaaCounter); - this.register.registerMetric(this.priceUpdatesPublishTimeGapHistogram); - this.register.registerMetric(this.priceUpdatesAttestationTimeGapHistogram); - this.register.registerMetric(this.webSocketInteractionCounter); - // End registering metric - - logger.info("prometheus client listening on port " + config.port); - this.server.listen(config.port); - } - - incReceivedVaa() { - this.receivedVaaCounter.inc(); - } - - addPriceUpdatesPublishTimeGap(gap: DurationInSec) { - this.priceUpdatesPublishTimeGapHistogram.observe(gap); - } - - addPriceUpdatesAttestationTimeGap(gap: DurationInSec) { - this.priceUpdatesAttestationTimeGapHistogram.observe(gap); - } - - addWebSocketInteraction( - type: WebSocketInteractionType, - status: "ok" | "err" - ) { - this.webSocketInteractionCounter.inc({ - type, - status, - }); - } -} diff --git a/price_service/server/src/rest.ts b/price_service/server/src/rest.ts deleted file mode 100644 index 784bf39e..00000000 --- a/price_service/server/src/rest.ts +++ /dev/null @@ -1,673 +0,0 @@ -import { HexString, Price, PriceFeed } from "@pythnetwork/price-service-sdk"; -import cors from "cors"; -import express, { NextFunction, Request, Response } from "express"; -import { Joi, schema, validate, ValidationError } from "express-validation"; -import { Server } from "http"; -import { StatusCodes } from "http-status-codes"; -import morgan from "morgan"; -import fetch from "node-fetch"; -import { parseBatchPriceAttestation } from "@pythnetwork/wormhole-attester-sdk"; -import { removeLeading0x, TimestampInSec } from "./helpers"; -import { createPriceInfo, PriceInfo, PriceStore } from "./listen"; -import { logger } from "./logging"; -import { PromClient } from "./promClient"; -import { retry } from "ts-retry-promise"; -import { parseVaa } from "@certusone/wormhole-sdk"; -import { getOrElse } from "./helpers"; -import { - TargetChain, - validTargetChains, - defaultTargetChain, - encodeVaaForChain, -} from "./encoding"; - -const MORGAN_LOG_FORMAT = - ':remote-addr - :remote-user ":method :url HTTP/:http-version"' + - ' :status :res[content-length] :response-time ms ":referrer" ":user-agent"'; - -// GET argument string to represent the options for target_chain -export const targetChainArgString = `target_chain=<${validTargetChains.join( - "|" -)}>`; - -export class RestException extends Error { - statusCode: number; - message: string; - constructor(statusCode: number, message: string) { - super(message); - this.statusCode = statusCode; - this.message = message; - } - - static PriceFeedIdNotFound(notFoundIds: string[]): RestException { - return new RestException( - StatusCodes.BAD_REQUEST, - `Price Feed(s) with id(s) ${notFoundIds.join(", ")} not found.` - ); - } - - static DbApiError(): RestException { - return new RestException(StatusCodes.INTERNAL_SERVER_ERROR, `DB API Error`); - } - - static VaaNotFound(): RestException { - return new RestException(StatusCodes.NOT_FOUND, "VAA not found."); - } -} - -function asyncWrapper( - callback: (req: Request, res: Response, next: NextFunction) => Promise -) { - return (req: Request, res: Response, next: NextFunction) => { - callback(req, res, next).catch(next); - }; -} - -export type VaaResponse = { - publishTime: number; - vaa: string; -}; - -export class RestAPI { - private port: number; - private priceFeedVaaInfo: PriceStore; - private isReady: (() => boolean) | undefined; - private promClient: PromClient | undefined; - private dbApiEndpoint?: string; - private dbApiCluster?: string; - - constructor( - config: { port: number; dbApiEndpoint?: string; dbApiCluster?: string }, - priceFeedVaaInfo: PriceStore, - isReady?: () => boolean, - promClient?: PromClient - ) { - this.port = config.port; - this.dbApiEndpoint = config.dbApiEndpoint; - this.dbApiCluster = config.dbApiCluster; - this.priceFeedVaaInfo = priceFeedVaaInfo; - this.isReady = isReady; - this.promClient = promClient; - } - - async getVaaWithDbLookup( - priceFeedId: string, - publishTime: TimestampInSec - ): Promise { - // Try to fetch the vaa from the local cache - const vaaConfig = this.priceFeedVaaInfo.getVaa(priceFeedId, publishTime); - let vaa: VaaResponse | undefined; - - // if publishTime is older than cache ttl or vaa is not found, fetch from db - if (vaaConfig !== undefined) { - vaa = { - vaa: vaaConfig.vaa, - publishTime: vaaConfig.publishTime, - }; - } else if (vaa === undefined && this.dbApiEndpoint && this.dbApiCluster) { - const priceFeedWithoutLeading0x = removeLeading0x(priceFeedId); - - try { - const data = (await retry( - () => - fetch( - `${this.dbApiEndpoint}/vaa?id=${priceFeedWithoutLeading0x}&publishTime=${publishTime}&cluster=${this.dbApiCluster}` - ).then((res) => res.json()), - { retries: 3 } - )) as any[]; - if (data.length > 0) { - vaa = { - vaa: data[0].vaa, - publishTime: Math.floor( - new Date(data[0].publishTime).getTime() / 1000 - ), - }; - } - } catch (e: any) { - logger.error(`DB API Error: ${e}`); - throw RestException.DbApiError(); - } - } - - return vaa; - } - - // Extract the price info from an Accumulator update. This is a temporary solution until hermes adoption - // to maintain backward compatibility when the db migrates to the new update format. - static extractPriceInfoFromAccumulatorUpdate( - priceFeedId: string, - updateData: Buffer - ): PriceInfo | undefined { - let offset = 0; - offset += 4; // magic - offset += 1; // major version - offset += 1; // minor version - - const trailingHeaderSize = updateData.readUint8(offset); - offset += 1 + trailingHeaderSize; - - const updateType = updateData.readUint8(offset); - offset += 1; - - // There is a single update type of 0 for now. - if (updateType !== 0) { - logger.error(`Invalid accumulator update type: ${updateType}`); - return undefined; - } - - const vaaLength = updateData.readUint16BE(offset); - offset += 2; - - const vaaBuffer = updateData.slice(offset, offset + vaaLength); - const vaa = parseVaa(vaaBuffer); - offset += vaaLength; - - const numUpdates = updateData.readUint8(offset); - offset += 1; - - // Iterate through the updates to find the price info with the given id - for (let i = 0; i < numUpdates; i++) { - const messageLength = updateData.readUint16BE(offset); - offset += 2; - - const message = updateData.slice(offset, offset + messageLength); - offset += messageLength; - - const proofLength = updateData.readUint8(offset); - offset += 1; - - // ignore proofs - offset += proofLength; - - // Checket whether the message is a price feed update - // from the given price id and if so, extract the price info - let messageOffset = 0; - const messageType = message.readUint8(messageOffset); - messageOffset += 1; - - // MessageType of 0 is a price feed update - if (messageType !== 0) { - continue; - } - - const priceId = message - .slice(messageOffset, messageOffset + 32) - .toString("hex"); - messageOffset += 32; - - if (priceId !== priceFeedId) { - continue; - } - - const price = message.readBigInt64BE(messageOffset); - messageOffset += 8; - const conf = message.readBigUint64BE(messageOffset); - messageOffset += 8; - const expo = message.readInt32BE(messageOffset); - messageOffset += 4; - const publishTime = message.readBigInt64BE(messageOffset); - messageOffset += 8; - const prevPublishTime = message.readBigInt64BE(messageOffset); - messageOffset += 8; - const emaPrice = message.readBigInt64BE(messageOffset); - messageOffset += 8; - const emaConf = message.readBigUint64BE(messageOffset); - - return { - priceFeed: new PriceFeed({ - id: priceFeedId, - price: new Price({ - price: price.toString(), - conf: conf.toString(), - expo, - publishTime: Number(publishTime), - }), - emaPrice: new Price({ - price: emaPrice.toString(), - conf: emaConf.toString(), - expo, - publishTime: Number(publishTime), - }), - }), - publishTime: Number(publishTime), - vaa: vaaBuffer, - seqNum: Number(vaa.sequence), - emitterChainId: vaa.emitterChain, - // These are not available in the accumulator update format - // but are required by the PriceInfo type. - attestationTime: Number(publishTime), - lastAttestedPublishTime: Number(prevPublishTime), - priceServiceReceiveTime: Number(publishTime), - }; - } - - return undefined; - } - - static vaaToPriceInfo( - priceFeedId: string, - vaa: Buffer - ): PriceInfo | undefined { - // Vaa could be the update data from the db with the Accumulator format. - const ACCUMULATOR_MAGIC = "504e4155"; - if (vaa.slice(0, 4).toString("hex") === ACCUMULATOR_MAGIC) { - return RestAPI.extractPriceInfoFromAccumulatorUpdate(priceFeedId, vaa); - } - - const parsedVaa = parseVaa(vaa); - - let batchAttestation; - - try { - batchAttestation = parseBatchPriceAttestation( - Buffer.from(parsedVaa.payload) - ); - } catch (e: any) { - logger.error(e, e.stack); - logger.error("Parsing historical VAA failed: %o", parsedVaa); - return undefined; - } - - for (const priceAttestation of batchAttestation.priceAttestations) { - if (priceAttestation.priceId === priceFeedId) { - return createPriceInfo( - priceAttestation, - vaa, - parsedVaa.sequence, - parsedVaa.emitterChain - ); - } - } - - return undefined; - } - - priceInfoToJson( - priceInfo: PriceInfo, - verbose: boolean, - targetChain: TargetChain | undefined - ): object { - return { - ...priceInfo.priceFeed.toJson(), - ...(verbose && { - metadata: { - emitter_chain: priceInfo.emitterChainId, - attestation_time: priceInfo.attestationTime, - sequence_number: priceInfo.seqNum, - price_service_receive_time: priceInfo.priceServiceReceiveTime, - }, - }), - ...(targetChain !== undefined && { - vaa: encodeVaaForChain(priceInfo.vaa, targetChain), - }), - }; - } - - // Run this function without blocking (`await`) if you want to run it async. - async createApp() { - const app = express(); - app.use(cors()); - - const winstonStream = { - write: (text: string) => { - logger.info(text); - }, - }; - - app.use(morgan(MORGAN_LOG_FORMAT, { stream: winstonStream })); - - const endpoints: string[] = []; - - const latestVaasInputSchema: schema = { - query: Joi.object({ - ids: Joi.array() - .items(Joi.string().regex(/^(0x)?[a-f0-9]{64}$/)) - .required(), - target_chain: Joi.string() - .valid(...validTargetChains) - .optional(), - }).required(), - }; - app.get( - "/api/latest_vaas", - validate(latestVaasInputSchema), - (req: Request, res: Response) => { - const priceIds = (req.query.ids as string[]).map(removeLeading0x); - const targetChain = getOrElse( - req.query.target_chain as TargetChain | undefined, - defaultTargetChain - ); - - // Multiple price ids might share same vaa, we use sequence number as - // key of a vaa and deduplicate using a map of seqnum to vaa bytes. - const vaaMap = new Map(); - - const notFoundIds: string[] = []; - - for (const id of priceIds) { - const latestPriceInfo = this.priceFeedVaaInfo.getLatestPriceInfo(id); - - if (latestPriceInfo === undefined) { - notFoundIds.push(id); - continue; - } - - vaaMap.set(latestPriceInfo.seqNum, latestPriceInfo.vaa); - } - - if (notFoundIds.length > 0) { - throw RestException.PriceFeedIdNotFound(notFoundIds); - } - - const jsonResponse = Array.from(vaaMap.values(), (vaa) => - encodeVaaForChain(vaa, targetChain) - ); - - res.json(jsonResponse); - } - ); - endpoints.push( - `api/latest_vaas?ids[]=&ids[]=&..&${targetChainArgString}` - ); - - const getVaaInputSchema: schema = { - query: Joi.object({ - id: Joi.string() - .regex(/^(0x)?[a-f0-9]{64}$/) - .required(), - publish_time: Joi.number().required(), - target_chain: Joi.string() - .valid(...validTargetChains) - .optional(), - }).required(), - }; - - app.get( - "/api/get_vaa", - validate(getVaaInputSchema), - asyncWrapper(async (req: Request, res: Response) => { - const priceFeedId = removeLeading0x(req.query.id as string); - const publishTime = Number(req.query.publish_time as string); - const targetChain = getOrElse( - req.query.target_chain as TargetChain | undefined, - defaultTargetChain - ); - - if ( - this.priceFeedVaaInfo.getLatestPriceInfo(priceFeedId) === undefined - ) { - throw RestException.PriceFeedIdNotFound([priceFeedId]); - } - - const vaaConfig = await this.getVaaWithDbLookup( - priceFeedId, - publishTime - ); - if (vaaConfig === undefined) { - throw RestException.VaaNotFound(); - } else { - vaaConfig.vaa = encodeVaaForChain(vaaConfig.vaa, targetChain); - res.json(vaaConfig); - } - }) - ); - - endpoints.push( - `api/get_vaa?id=&publish_time=&${targetChainArgString}` - ); - - const getVaaCcipInputSchema: schema = { - query: Joi.object({ - data: Joi.string() - .regex(/^0x[a-f0-9]{80}$/) - .required(), - }).required(), - }; - - // CCIP compatible endpoint. Read more information about it from - // https://eips.ethereum.org/EIPS/eip-3668 - app.get( - "/api/get_vaa_ccip", - validate(getVaaCcipInputSchema), - asyncWrapper(async (req: Request, res: Response) => { - const dataHex = req.query.data as string; - const data = Buffer.from(removeLeading0x(dataHex), "hex"); - - const priceFeedId = data.slice(0, 32).toString("hex"); - const publishTime = Number(data.readBigInt64BE(32)); - - if ( - this.priceFeedVaaInfo.getLatestPriceInfo(priceFeedId) === undefined - ) { - throw RestException.PriceFeedIdNotFound([priceFeedId]); - } - - const vaa = await this.getVaaWithDbLookup(priceFeedId, publishTime); - - if (vaa === undefined) { - // Returning Bad Gateway error because CCIP expects a 5xx error if it needs to - // retry or try other endpoints. Bad Gateway seems the best choice here as this - // is not an internal error and could happen on two scenarios: - // 1. DB Api is not responding well (Bad Gateway is appropriate here) - // 2. Publish time is a few seconds before current time and a VAA - // Will be available in a few seconds. So we want the client to retry. - res - .status(StatusCodes.BAD_GATEWAY) - .json({ "message:": "VAA not found." }); - } else { - const resData = "0x" + Buffer.from(vaa.vaa, "base64").toString("hex"); - - res.json({ - data: resData, - }); - } - }) - ); - - endpoints.push( - "api/get_vaa_ccip?data=<0x+>" - ); - - const latestPriceFeedsInputSchema: schema = { - query: Joi.object({ - ids: Joi.array() - .items(Joi.string().regex(/^(0x)?[a-f0-9]{64}$/)) - .required(), - verbose: Joi.boolean(), - binary: Joi.boolean(), - target_chain: Joi.string() - .valid(...validTargetChains) - .optional(), - }).required(), - }; - app.get( - "/api/latest_price_feeds", - validate(latestPriceFeedsInputSchema), - (req: Request, res: Response) => { - const priceIds = (req.query.ids as string[]).map(removeLeading0x); - // verbose is optional, default to false - const verbose = req.query.verbose === "true"; - // The binary and target_chain are somewhat redundant. Binary still exists for backward compatibility reasons. - // No VAA will be returned if both arguments are omitted. binary=true is the same as target_chain=default - let targetChain = req.query.target_chain as TargetChain | undefined; - if (targetChain === undefined && req.query.binary === "true") { - targetChain = defaultTargetChain; - } - - const responseJson = []; - - const notFoundIds: string[] = []; - - for (const id of priceIds) { - const latestPriceInfo = this.priceFeedVaaInfo.getLatestPriceInfo(id); - - if (latestPriceInfo === undefined) { - notFoundIds.push(id); - continue; - } - - responseJson.push( - this.priceInfoToJson(latestPriceInfo, verbose, targetChain) - ); - } - - if (notFoundIds.length > 0) { - throw RestException.PriceFeedIdNotFound(notFoundIds); - } - - res.json(responseJson); - } - ); - endpoints.push( - "api/latest_price_feeds?ids[]=&ids[]=&.." - ); - endpoints.push( - "api/latest_price_feeds?ids[]=&ids[]=&..&verbose=true" - ); - endpoints.push( - "api/latest_price_feeds?ids[]=&ids[]=&..&verbose=true&binary=true" - ); - endpoints.push( - `api/latest_price_feeds?ids[]=&ids[]=&..&verbose=true&${targetChainArgString}` - ); - - const getPriceFeedInputSchema: schema = { - query: Joi.object({ - id: Joi.string() - .regex(/^(0x)?[a-f0-9]{64}$/) - .required(), - publish_time: Joi.number().required(), - verbose: Joi.boolean(), - binary: Joi.boolean(), - target_chain: Joi.string() - .valid(...validTargetChains) - .optional(), - }).required(), - }; - - app.get( - "/api/get_price_feed", - validate(getPriceFeedInputSchema), - asyncWrapper(async (req: Request, res: Response) => { - const priceFeedId = removeLeading0x(req.query.id as string); - const publishTime = Number(req.query.publish_time as string); - // verbose is optional, default to false - const verbose = req.query.verbose === "true"; - // The binary and target_chain are somewhat redundant. Binary still exists for backward compatibility reasons. - // No VAA will be returned if both arguments are omitted. binary=true is the same as target_chain=default - let targetChain = req.query.target_chain as TargetChain | undefined; - if (targetChain === undefined && req.query.binary === "true") { - targetChain = defaultTargetChain; - } - - if ( - this.priceFeedVaaInfo.getLatestPriceInfo(priceFeedId) === undefined - ) { - throw RestException.PriceFeedIdNotFound([priceFeedId]); - } - - const vaa = await this.getVaaWithDbLookup(priceFeedId, publishTime); - if (vaa === undefined) { - throw RestException.VaaNotFound(); - } - - const priceInfo = RestAPI.vaaToPriceInfo( - priceFeedId, - Buffer.from(vaa.vaa, "base64") - ); - - if (priceInfo === undefined) { - throw RestException.VaaNotFound(); - } else { - res.json(this.priceInfoToJson(priceInfo, verbose, targetChain)); - } - }) - ); - - endpoints.push( - "api/get_price_feed?id=&publish_time=" - ); - endpoints.push( - "api/get_price_feed?id=&publish_time=&verbose=true" - ); - endpoints.push( - "api/get_price_feed?id=&publish_time=&binary=true" - ); - - app.get("/api/price_feed_ids", (req: Request, res: Response) => { - const availableIds = this.priceFeedVaaInfo.getPriceIds(); - res.json([...availableIds]); - }); - endpoints.push("api/price_feed_ids"); - - const staleFeedsInputSchema: schema = { - query: Joi.object({ - threshold: Joi.number().required(), - }).required(), - }; - app.get( - "/api/stale_feeds", - validate(staleFeedsInputSchema), - (req: Request, res: Response) => { - const stalenessThresholdSeconds = Number(req.query.threshold as string); - - const currentTime: TimestampInSec = Math.floor(Date.now() / 1000); - - const priceIds = [...this.priceFeedVaaInfo.getPriceIds()]; - const stalePrices: Record = {}; - - for (const priceId of priceIds) { - const latency = - currentTime - - this.priceFeedVaaInfo.getLatestPriceInfo(priceId)!.attestationTime; - if (latency > stalenessThresholdSeconds) { - stalePrices[priceId] = latency; - } - } - - res.json(stalePrices); - } - ); - endpoints.push("/api/stale_feeds?threshold="); - - app.get("/ready", (_, res: Response) => { - if (this.isReady === undefined || this.isReady!()) { - res.sendStatus(StatusCodes.OK); - } else { - res.sendStatus(StatusCodes.SERVICE_UNAVAILABLE); - } - }); - endpoints.push("ready"); - - app.get("/live", (_, res: Response) => { - res.sendStatus(StatusCodes.OK); - }); - endpoints.push("live"); - - // Websocket endpoint - endpoints.push("ws"); - - app.get("/", (_, res: Response) => res.json(endpoints)); - - app.use((err: any, _: Request, res: Response, next: NextFunction) => { - if (err instanceof ValidationError) { - return res.status(err.statusCode).json(err); - } - - if (err instanceof RestException) { - return res.status(err.statusCode).json(err); - } - - return next(err); - }); - - return app; - } - - async run(): Promise { - const app = await this.createApp(); - return app.listen(this.port, () => - logger.debug("listening on REST port " + this.port) - ); - } -} diff --git a/price_service/server/src/vaa.ts b/price_service/server/src/vaa.ts deleted file mode 100644 index c5db45ce..00000000 --- a/price_service/server/src/vaa.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { logger } from "./logging"; -import { ParsedVaa } from "@certusone/wormhole-sdk"; -import { GuardianSet } from "@certusone/wormhole-spydk/lib/cjs/proto/publicrpc/v1/publicrpc"; -import * as secp256k1 from "secp256k1"; -import * as keccak from "keccak"; - -const WormholeClusters = ["localnet", "testnet", "mainnet"] as const; -export type WormholeCluster = typeof WormholeClusters[number]; - -export function wormholeClusterFromString(s: string): WormholeCluster { - if (WormholeClusters.includes(s as WormholeCluster)) { - return s as WormholeCluster; - } - throw new Error(`Invalid wormhole cluster: ${s}`); -} - -const guardianSets: Record = { - localnet: { - index: 0, - addresses: ["0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe"], - }, - testnet: { - index: 0, - addresses: ["0x13947Bd48b18E53fdAeEe77F3473391aC727C638"], - }, - mainnet: { - index: 3, - addresses: [ - "0x58CC3AE5C097b213cE3c81979e1B9f9570746AA5", - "0xfF6CB952589BDE862c25Ef4392132fb9D4A42157", - "0x114De8460193bdf3A2fCf81f86a09765F4762fD1", - "0x107A0086b32d7A0977926A205131d8731D39cbEB", - "0x8C82B2fd82FaeD2711d59AF0F2499D16e726f6b2", - "0x11b39756C042441BE6D8650b69b54EbE715E2343", - "0x54Ce5B4D348fb74B958e8966e2ec3dBd4958a7cd", - "0x15e7cAF07C4e3DC8e7C469f92C8Cd88FB8005a20", - "0x74a3bf913953D695260D88BC1aA25A4eeE363ef0", - "0x000aC0076727b35FBea2dAc28fEE5cCB0fEA768e", - "0xAF45Ced136b9D9e24903464AE889F5C8a723FC14", - "0xf93124b7c738843CBB89E864c862c38cddCccF95", - "0xD2CC37A4dc036a8D232b48f62cDD4731412f4890", - "0xDA798F6896A3331F64b48c12D1D57Fd9cbe70811", - "0x71AA1BE1D36CaFE3867910F99C09e347899C19C3", - "0x8192b6E7387CCd768277c17DAb1b7a5027c0b3Cf", - "0x178e21ad2E77AE06711549CFBB1f9c7a9d8096e8", - "0x5E1487F35515d02A92753504a8D75471b9f49EdB", - "0x6FbEBc898F403E4773E95feB15E80C9A99c8348d", - ], - }, -}; - -export function isValidVaa(vaa: ParsedVaa, cluster: WormholeCluster): boolean { - const currentGuardianSet = guardianSets[cluster]; - if (vaa.guardianSetIndex !== currentGuardianSet.index) { - return false; - } - - const threshold = Math.ceil((currentGuardianSet.addresses.length * 2) / 3); - if (vaa.guardianSignatures.length < threshold) { - return false; - } - - // It's not possible to call a signature verification function directly - // because we only have the addresses of the guardians and not their public - // keys. Instead, we compare the address extracted from the public key that - // signed the VAA with the corresponding address stored in the guardian set. - - const messageHash = keccak.default("keccak256").update(vaa.hash).digest(); - let counter = 0; - - try { - vaa.guardianSignatures.forEach((sig) => { - // Each signature is a 65-byte secp256k1 signature with the recovery ID at - // the last byte. It is not the compact representation from EIP-2098. - const recoveryID = sig.signature[64] % 2; - const signature = sig.signature.slice(0, 64); - const publicKey = Buffer.from( - secp256k1.ecdsaRecover(signature, recoveryID, messageHash, false) - ); - // The first byte of the public key is the prefix (0x03 or 0x04) - // indicating if the public key is compressed. Remove it before hashing. - const publicKeyHash = keccak - .default("keccak256") - .update(publicKey.slice(1)) - .digest(); - // The last 20 bytes of the hash are the address. - const address = publicKeyHash.slice(-20).toString("hex"); - - if ( - checksumAddress(address) === currentGuardianSet.addresses[sig.index] - ) { - counter++; - } - }); - - return counter === vaa.guardianSignatures.length; - } catch (error) { - logger.warn("Error validating VAA signatures:", error); - - return false; - } -} - -function checksumAddress(address: string) { - address = address.toLowerCase().replace("0x", ""); - const hash = keccak.default("keccak256").update(address).digest("hex"); - let ret = "0x"; - - for (let i = 0; i < address.length; i++) { - if (parseInt(hash[i], 16) >= 8) { - ret += address[i].toUpperCase(); - } else { - ret += address[i]; - } - } - - return ret; -} diff --git a/price_service/server/src/ws.ts b/price_service/server/src/ws.ts deleted file mode 100644 index 32598f8c..00000000 --- a/price_service/server/src/ws.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { HexString } from "@pythnetwork/price-service-sdk"; -import * as http from "http"; -import Joi from "joi"; -import WebSocket, { RawData, WebSocketServer } from "ws"; -import { PriceInfo, PriceStore } from "./listen"; -import { logger } from "./logging"; -import { PromClient } from "./promClient"; - -const ClientMessageSchema: Joi.Schema = Joi.object({ - type: Joi.string().valid("subscribe", "unsubscribe").required(), - ids: Joi.array() - .items(Joi.string().regex(/^(0x)?[a-f0-9]{64}$/)) - .required(), - verbose: Joi.boolean(), - binary: Joi.boolean(), -}).required(); - -export type ClientMessage = { - type: "subscribe" | "unsubscribe"; - ids: HexString[]; - verbose?: boolean; - binary?: boolean; -}; - -export type ServerResponse = { - type: "response"; - status: "success" | "error"; - error?: string; -}; - -export type ServerPriceUpdate = { - type: "price_update"; - price_feed: any; -}; - -export type PriceFeedConfig = { - verbose: boolean; - binary: boolean; -}; - -export type ServerMessage = ServerResponse | ServerPriceUpdate; - -export class WebSocketAPI { - private wsCounter: number; - private priceFeedClients: Map>; - private priceFeedClientsConfig: Map< - HexString, - Map - >; - private aliveClients: Set; - private wsId: Map; - private priceFeedVaaInfo: PriceStore; - private promClient: PromClient | undefined; - - constructor(priceFeedVaaInfo: PriceStore, promClient?: PromClient) { - this.priceFeedVaaInfo = priceFeedVaaInfo; - this.priceFeedClients = new Map(); - this.priceFeedClientsConfig = new Map(); - this.aliveClients = new Set(); - this.wsCounter = 0; - this.wsId = new Map(); - this.promClient = promClient; - } - - private addPriceFeedClient( - ws: WebSocket, - id: HexString, - verbose: boolean = false, - binary: boolean = false - ) { - if (!this.priceFeedClients.has(id)) { - this.priceFeedClients.set(id, new Set()); - this.priceFeedClientsConfig.set(id, new Map([[ws, { verbose, binary }]])); - } else { - this.priceFeedClientsConfig.get(id)!.set(ws, { verbose, binary }); - } - this.priceFeedClients.get(id)!.add(ws); - } - - private delPriceFeedClient(ws: WebSocket, id: HexString) { - if (!this.priceFeedClients.has(id)) { - return; - } - this.priceFeedClients.get(id)!.delete(ws); - this.priceFeedClientsConfig.get(id)!.delete(ws); - } - - dispatchPriceFeedUpdate(priceInfo: PriceInfo) { - if (this.priceFeedClients.get(priceInfo.priceFeed.id) === undefined) { - logger.info( - `Sending ${priceInfo.priceFeed.id} price update to no clients.` - ); - return; - } - - const clients: Set = this.priceFeedClients.get( - priceInfo.priceFeed.id - )!; - logger.info( - `Sending ${priceInfo.priceFeed.id} price update to ${ - clients.size - } clients: ${Array.from(clients.values()).map((ws, _idx, _arr) => - this.wsId.get(ws) - )}` - ); - - for (const client of clients.values()) { - this.promClient?.addWebSocketInteraction("server_update", "ok"); - - const config = this.priceFeedClientsConfig - .get(priceInfo.priceFeed.id)! - .get(client); - - const verbose = config?.verbose; - const binary = config?.binary; - - const priceUpdate: ServerPriceUpdate = { - type: "price_update", - price_feed: { - ...priceInfo.priceFeed.toJson(), - ...(verbose && { - metadata: { - emitter_chain: priceInfo.emitterChainId, - attestation_time: priceInfo.attestationTime, - sequence_number: priceInfo.seqNum, - price_service_receive_time: priceInfo.priceServiceReceiveTime, - }, - }), - ...(binary && { - vaa: priceInfo.vaa.toString("base64"), - }), - }, - }; - - client.send(JSON.stringify(priceUpdate)); - } - } - - clientClose(ws: WebSocket) { - for (const clients of this.priceFeedClients.values()) { - if (clients.has(ws)) { - clients.delete(ws); - } - } - - this.aliveClients.delete(ws); - this.wsId.delete(ws); - } - - handleMessage(ws: WebSocket, data: RawData) { - try { - const jsonData = JSON.parse(data.toString()); - const validationResult = ClientMessageSchema.validate(jsonData); - if (validationResult.error !== undefined) { - throw validationResult.error; - } - - const message = jsonData as ClientMessage; - - message.ids = message.ids.map((id) => { - if (id.startsWith("0x")) { - return id.substring(2); - } - return id; - }); - - const availableIds = this.priceFeedVaaInfo.getPriceIds(); - const notFoundIds = message.ids.filter((id) => !availableIds.has(id)); - - if (notFoundIds.length > 0) { - throw new Error( - `Price Feeds with ids ${notFoundIds.join(", ")} not found` - ); - } - - if (message.type === "subscribe") { - message.ids.forEach((id) => - this.addPriceFeedClient( - ws, - id, - message.verbose === true, - message.binary === true - ) - ); - } else { - message.ids.forEach((id) => this.delPriceFeedClient(ws, id)); - } - } catch (e: any) { - const errorResponse: ServerResponse = { - type: "response", - status: "error", - error: e.message, - }; - - logger.info( - `Invalid request ${data.toString()} from client ${this.wsId.get(ws)}` - ); - this.promClient?.addWebSocketInteraction("client_message", "err"); - - ws.send(JSON.stringify(errorResponse)); - return; - } - - logger.info( - `Successful request ${data.toString()} from client ${this.wsId.get(ws)}` - ); - this.promClient?.addWebSocketInteraction("client_message", "ok"); - - const response: ServerResponse = { - type: "response", - status: "success", - }; - - ws.send(JSON.stringify(response)); - } - - run(server: http.Server): WebSocketServer { - const wss = new WebSocketServer({ - server, - path: "/ws", - maxPayload: 100 * 1024, // 100 KiB - }); - - wss.on("connection", (ws: WebSocket, request: http.IncomingMessage) => { - logger.info( - `Incoming ws connection from ${request.socket.remoteAddress}, assigned id: ${this.wsCounter}` - ); - - this.wsId.set(ws, this.wsCounter); - this.wsCounter += 1; - - ws.on("message", (data: RawData) => this.handleMessage(ws, data)); - - this.aliveClients.add(ws); - - ws.on("pong", (_data) => { - this.aliveClients.add(ws); - }); - - ws.on("error", (err: Error) => { - logger.warn(`Err with client ${this.wsId.get(ws)}: ${err}`); - }); - - ws.on("close", (_code: number, _reason: Buffer) => { - logger.info(`client ${this.wsId.get(ws)} closed the connection.`); - this.promClient?.addWebSocketInteraction("close", "ok"); - - this.clientClose(ws); - }); - - this.promClient?.addWebSocketInteraction("connection", "ok"); - }); - - const pingInterval = setInterval(() => { - wss.clients.forEach((ws) => { - if (this.aliveClients.has(ws) === false) { - logger.info( - `client ${this.wsId.get(ws)} timed out. terminating connection` - ); - this.promClient?.addWebSocketInteraction("timeout", "ok"); - this.clientClose(ws); - ws.terminate(); - return; - } - - this.aliveClients.delete(ws); - ws.ping(); - }); - }, 30000); - - wss.on("close", () => { - clearInterval(pingInterval); - }); - - this.priceFeedVaaInfo.addUpdateListener( - this.dispatchPriceFeedUpdate.bind(this) - ); - - return wss; - } -} diff --git a/price_service/server/tsconfig.json b/price_service/server/tsconfig.json deleted file mode 100644 index 02896c7d..00000000 --- a/price_service/server/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src"], - "exclude": ["node_modules", "**/__tests__/*"], - "compilerOptions": { - "rootDir": "src/", - "outDir": "./lib" - } -} diff --git a/price_service/server/tslint.json b/price_service/server/tslint.json deleted file mode 100644 index faea87cd..00000000 --- a/price_service/server/tslint.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": ["tslint:recommended", "tslint-config-prettier"], - "rules": { - "max-classes-per-file": { - "severity": "off" - } - } -} diff --git a/wormhole_attester/client/Cargo.toml b/wormhole_attester/client/Cargo.toml deleted file mode 100644 index 9c09d4ff..00000000 --- a/wormhole_attester/client/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "pyth-wormhole-attester-client" -version = "5.0.0" -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -name = "pyth_wormhole_attester_client" - -[[bin]] -name = "pwhac" -path = "src/main.rs" - - -[features] -default = ["pyth-wormhole-attester/client", "wormhole-bridge-solana/client", "pyth-wormhole-attester/trace"] - -[dependencies] -borsh = "=0.9.3" -clap = {version = "3.1.18", features = ["derive"]} -env_logger = "0.8.4" -log = "0.4.14" -wormhole-bridge-solana = {git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.14.8"} -pyth-wormhole-attester = {path = "../program"} -pyth-wormhole-attester-sdk = { path = "../sdk/rust", features=["solana"] } -pyth-sdk-solana = "0.6.1" -serde = "1" -serde_yaml = "0.8" -shellexpand = "2.1.0" -solana-client = "=1.10.31" -solana-program = "=1.10.31" -solana-sdk = "=1.10.31" -solana-transaction-status = "=1.10.31" -solitaire = {git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.14.8"} -tokio = {version = "1", features = ["sync", "rt-multi-thread", "time"]} -futures = "0.3.21" -sha3 = "0.10.6" -generic-array = "0.14.6" -lazy_static = "1.4.0" -prometheus = "0.13.3" -warp = "0.3.3" -http = "0.2.8" - -[dev-dependencies] -solana-program-test = "=1.10.31" -solana-sdk = "=1.10.31" -serial_test = "1.0.0" diff --git a/wormhole_attester/client/Dockerfile.p2w-attest b/wormhole_attester/client/Dockerfile.p2w-attest deleted file mode 100644 index f597186b..00000000 --- a/wormhole_attester/client/Dockerfile.p2w-attest +++ /dev/null @@ -1,31 +0,0 @@ -#syntax=docker/dockerfile:1.2@sha256:e2a8561e419ab1ba6b2fe6cbdf49fd92b95912df1cf7d313c3e2230a333fdbcc -FROM ghcr.io/certusone/solana:1.10.31@sha256:d31e8db926a1d3fbaa9d9211d9979023692614b7b64912651aba0383e8c01bad AS solana - -RUN apt-get update && apt-get install -yq python3 libudev-dev ncat -RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && apt-get install -y nodejs - -ADD governance/remote_executor /usr/src/governance/remote_executor -ADD wormhole_attester /usr/src/wormhole_attester - -WORKDIR /usr/src/wormhole_attester - -ENV EMITTER_ADDRESS="11111111111111111111111111111115" -ENV BRIDGE_ADDRESS="Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o" - -RUN --mount=type=cache,target=/root/.cache \ - --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=target \ - cargo test --package pyth-wormhole-attester-client && \ - cargo build --package pyth-wormhole-attester-client && \ - mv target/debug/pwhac /usr/local/bin/pwhac - -ADD third_party/pyth/pyth_utils.py /usr/src/pyth/pyth_utils.py -ADD third_party/pyth/p2w_autoattest.py /usr/src/pyth/p2w_autoattest.py -ADD tilt_devnet/secrets/solana /solana-secrets - -RUN chmod a+rx /usr/src/pyth/*.py - -ENV P2W_OWNER_KEYPAIR="/solana-secrets/p2w_owner.json" -ENV P2W_ATTESTATIONS_PORT="4343" -ENV PYTH_PUBLISHER_KEYPAIR="/solana-secrets/pyth_publisher.json" -ENV PYTH_PROGRAM_KEYPAIR="/solana-secrets/pyth_program.json" diff --git a/wormhole_attester/client/src/attestation_cfg.rs b/wormhole_attester/client/src/attestation_cfg.rs deleted file mode 100644 index 21bbc506..00000000 --- a/wormhole_attester/client/src/attestation_cfg.rs +++ /dev/null @@ -1,656 +0,0 @@ -use { - crate::{ - attestation_cfg::SymbolConfig::{ - Key, - Name, - }, - P2WProductAccount, - }, - log::{ - info, - warn, - }, - serde::{ - de::Error, - Deserialize, - Deserializer, - Serialize, - Serializer, - }, - solana_program::pubkey::Pubkey, - std::{ - collections::{ - HashMap, - HashSet, - }, - str::FromStr, - }, -}; - -/// Pyth2wormhole config specific to attestation requests -#[derive(Clone, Debug, Hash, Deserialize, Serialize, PartialEq, Eq)] -pub struct AttestationConfig { - #[serde(default = "default_min_msg_reuse_interval_ms")] - pub min_msg_reuse_interval_ms: u64, - #[serde(default = "default_max_msg_accounts")] - pub max_msg_accounts: u64, - - /// How many consecutive attestation failures cause the service to - /// report as unhealthy. - #[serde(default = "default_healthcheck_window_size")] - pub healthcheck_window_size: u64, - - #[serde(default = "default_enable_healthcheck")] - pub enable_healthcheck: bool, - - /// Optionally, we take a mapping account to add remaining symbols from a Pyth deployments. - /// These symbols are processed under `default_attestation_conditions`. - #[serde( - deserialize_with = "opt_pubkey_string_de", - serialize_with = "opt_pubkey_string_ser", - default // Uses Option::default() which is None - )] - pub mapping_addr: Option, - /// The known symbol list will be reloaded based off this - /// interval, to account for mapping changes. Note: This interval - /// will only work if the mapping address is defined. Whenever - /// it's time to look up the mapping, new attestation jobs are - /// started lazily, only if mapping contents affected the known - /// symbol list, and before stopping the pre-existing obsolete - /// jobs to maintain uninterrupted cranking. - #[serde(default = "default_mapping_reload_interval_mins")] - pub mapping_reload_interval_mins: u64, - #[serde(default = "default_min_rpc_interval_ms")] - /// Rate-limiting minimum delay between RPC requests in milliseconds - pub min_rpc_interval_ms: u64, - /// Attestation conditions that will be used for any symbols included in the mapping - /// that aren't explicitly in one of the groups below, and any groups without explicitly - /// configured attestation conditions. - #[serde(default)] - pub default_attestation_conditions: AttestationConditions, - - /// Groups of symbols to publish. - pub symbol_groups: Vec, -} - -impl AttestationConfig { - /// Instantiate the batches of symbols to attest by matching the config against the collection - /// of on-chain product accounts. - pub fn instantiate_batches( - &self, - product_accounts: &[P2WProductAccount], - max_batch_size: usize, - ) -> Vec { - // Construct mapping from the name of each product account to its corresponding symbols - let mut name_to_symbols: HashMap> = HashMap::new(); - for product_account in product_accounts { - for price_account_key in &product_account.price_account_keys { - if let Some(name) = &product_account.name { - let symbol = P2WSymbol { - name: Some(name.clone()), - product_addr: product_account.key, - price_addr: *price_account_key, - }; - - name_to_symbols - .entry(name.clone()) - .or_default() - .push(symbol); - } - } - } - - // Instantiate batches from the configured symbol groups. - let mut configured_batches: Vec = vec![]; - for group in &self.symbol_groups { - let group_symbols: Vec = group - .symbols - .iter() - .flat_map(|symbol| match &symbol { - Key { - name, - product, - price, - } => { - vec![P2WSymbol { - name: name.clone(), - product_addr: *product, - price_addr: *price, - }] - } - Name { name } => { - let maybe_matched_symbols: Option<&Vec> = - name_to_symbols.get(name); - if let Some(matched_symbols) = maybe_matched_symbols { - matched_symbols.clone() - } else { - // It's slightly unfortunate that this is a warning, but it seems better than crashing. - // The data in the mapping account can change while the attester is running and trigger this case, - // which means that it is not necessarily a configuration problem. - // Note that any named symbols in the config which fail to match will still be included - // in the remaining_symbols group below. - warn!( - "Could not find product account for configured symbol {}", - name - ); - vec![] - } - } - }) - .collect(); - - let group_conditions = group - .conditions - .as_ref() - .unwrap_or(&self.default_attestation_conditions); - configured_batches.extend(AttestationConfig::partition_into_batches( - &group.group_name, - max_batch_size, - group_conditions, - group_symbols, - )) - } - - // Find any accounts not included in existing batches and group them into a remainder batch - let existing_price_accounts: HashSet = configured_batches - .iter() - .flat_map(|batch| batch.symbols.iter().map(|symbol| symbol.price_addr)) - .chain( - configured_batches - .iter() - .flat_map(|batch| batch.symbols.iter().map(|symbol| symbol.price_addr)), - ) - .collect(); - - let mut remaining_symbols: Vec = vec![]; - for product_account in product_accounts { - for price_account_key in &product_account.price_account_keys { - if !existing_price_accounts.contains(price_account_key) { - let symbol = P2WSymbol { - name: product_account.name.clone(), - product_addr: product_account.key, - price_addr: *price_account_key, - }; - remaining_symbols.push(symbol); - } - } - } - let remaining_batches = AttestationConfig::partition_into_batches( - &"mapping".to_owned(), - max_batch_size, - &self.default_attestation_conditions, - remaining_symbols, - ); - - let all_batches = configured_batches - .into_iter() - .chain(remaining_batches.into_iter()) - .collect::>(); - - for batch in &all_batches { - info!( - "Batch {:?}, {} symbols", - batch.group_name, - batch.symbols.len(), - ); - } - - all_batches - } - - /// Partition symbols into a collection of batches, each of which contains no more than - /// `max_batch_size` symbols. - fn partition_into_batches( - batch_name: &String, - max_batch_size: usize, - conditions: &AttestationConditions, - symbols: Vec, - ) -> Vec { - symbols - .as_slice() - .chunks(max_batch_size) - .map(move |batch_symbols| SymbolBatch { - group_name: batch_name.to_owned(), - symbols: batch_symbols.to_vec(), - conditions: conditions.clone(), - }) - .collect() - } -} - -#[derive(Clone, Debug, Hash, Deserialize, Serialize, PartialEq, Eq)] -pub struct SymbolGroupConfig { - pub group_name: String, - /// Attestation conditions applied to all symbols in this group - /// If not provided, use the default attestation conditions from `AttestationConfig`. - pub conditions: Option, - - /// The symbols to publish in this group. - pub symbols: Vec, -} - -/// Config entry for a symbol to attest. -#[derive(Clone, Debug, Hash, Deserialize, Serialize, PartialEq, Eq)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum SymbolConfig { - /// A symbol specified by its product name. - Name { - /// The name of the symbol. This name is matched against the "symbol" field in the product - /// account metadata. If multiple price accounts have this name (either because 2 product - /// accounts have the same symbol or a single product account has multiple price accounts), - /// it matches *all* of them and puts them into this group. - name: String, - }, - /// A symbol specified by its product and price account keys. - Key { - /// Optional human-readable name for the symbol (for logging purposes). - /// This field does not need to match the on-chain data for the product. - name: Option, - - #[serde( - deserialize_with = "pubkey_string_de", - serialize_with = "pubkey_string_ser" - )] - product: Pubkey, - #[serde( - deserialize_with = "pubkey_string_de", - serialize_with = "pubkey_string_ser" - )] - price: Pubkey, - }, -} - -impl ToString for SymbolConfig { - fn to_string(&self) -> String { - match &self { - Name { name } => name.clone(), - Key { - name: Some(name), - product: _, - price: _, - } => name.clone(), - Key { - name: None, - product, - price: _, - } => { - format!("Unnamed product {product}") - } - } - } -} - -/// A batch of symbols that's ready to be attested. Includes all necessary information -/// (such as price/product account keys). -#[derive(Clone, Debug, Hash, Deserialize, Serialize, PartialEq, Eq)] -pub struct SymbolBatch { - pub group_name: String, - /// Attestation conditions applied to all symbols in this group - pub conditions: AttestationConditions, - pub symbols: Vec, -} - -pub const fn default_max_msg_accounts() -> u64 { - 1_000_000 -} - -pub const fn default_min_msg_reuse_interval_ms() -> u64 { - 10_000 // 10s -} - -pub const fn default_healthcheck_window_size() -> u64 { - 100 -} - -pub const fn default_enable_healthcheck() -> bool { - true -} - -pub const fn default_mapping_reload_interval_mins() -> u64 { - 15 -} - -pub const fn default_min_rpc_interval_ms() -> u64 { - 150 -} - -pub const fn default_min_interval_ms() -> u64 { - 60_000 -} - -pub const fn default_rate_limit_interval_secs() -> u32 { - 1 -} - -pub const fn default_max_batch_jobs() -> usize { - 20 -} - -/// Per-group attestation resend rules. Attestation is triggered if -/// any of the active conditions is met. Option<> fields can be -/// de-activated with None. All conditions are inactive by default, -/// except for the non-Option ones. -#[derive(Clone, Debug, Hash, Deserialize, Serialize, PartialEq, Eq)] -pub struct AttestationConditions { - /// Lower bound on attestation rate. Attestation is triggered - /// unconditionally whenever the specified interval elapses since - /// last attestation. - #[serde(default = "default_min_interval_ms")] - pub min_interval_ms: u64, - - /// Upper bound on attestation rate. Attesting the same batch - /// before this many seconds pass fails the tx. This limit is - /// enforced on-chain, letting concurret attesters prevent - /// redundant batch resends and tx expenses. NOTE: The client - /// logic does not include rate limit failures in monitoring error - /// counts. 0 effectively disables this feature. - #[serde(default = "default_rate_limit_interval_secs")] - pub rate_limit_interval_secs: u32, - - /// Limit concurrent attestation attempts per batch. This setting - /// should act only as a failsafe cap on resource consumption and is - /// best set well above the expected average number of jobs. - #[serde(default = "default_max_batch_jobs")] - pub max_batch_jobs: usize, - - /// Trigger attestation if price changes by the specified - /// percentage, expressed in integer basis points (1bps = 0.01%) - #[serde(default)] - pub price_changed_bps: Option, - - /// Trigger attestation if publish_time advances at least the - /// specified amount. - #[serde(default)] - pub publish_time_min_delta_secs: Option, -} - -impl AttestationConditions { - /// Used by should_resend() to check if it needs to make the expensive RPC request - pub fn need_onchain_lookup(&self) -> bool { - // Bug trap for new fields that also need to be included in - // the returned expression - let AttestationConditions { - min_interval_ms: _min_interval_ms, - max_batch_jobs: _max_batch_jobs, - price_changed_bps, - publish_time_min_delta_secs, - rate_limit_interval_secs: _, - } = self; - - price_changed_bps.is_some() || publish_time_min_delta_secs.is_some() - } -} - -impl Default for AttestationConditions { - fn default() -> Self { - Self { - min_interval_ms: default_min_interval_ms(), - max_batch_jobs: default_max_batch_jobs(), - price_changed_bps: None, - publish_time_min_delta_secs: None, - rate_limit_interval_secs: default_rate_limit_interval_secs(), - } - } -} - -#[derive(Clone, Default, Debug, Hash, Deserialize, Serialize, PartialEq, Eq)] -pub struct P2WSymbol { - /// User-defined human-readable name - pub name: Option, - - #[serde( - deserialize_with = "pubkey_string_de", - serialize_with = "pubkey_string_ser" - )] - pub product_addr: Pubkey, - #[serde( - deserialize_with = "pubkey_string_de", - serialize_with = "pubkey_string_ser" - )] - pub price_addr: Pubkey, -} - -impl ToString for P2WSymbol { - fn to_string(&self) -> String { - self.name - .clone() - .unwrap_or(format!("Unnamed product {}", self.product_addr)) - } -} - -// Helper methods for strinigified SOL addresses - -fn pubkey_string_ser(k: &Pubkey, ser: S) -> Result -where - S: Serializer, -{ - ser.serialize_str(&k.to_string()) -} - -fn pubkey_string_de<'de, D>(de: D) -> Result -where - D: Deserializer<'de>, -{ - let pubkey_string = String::deserialize(de)?; - let pubkey = Pubkey::from_str(&pubkey_string).map_err(D::Error::custom)?; - Ok(pubkey) -} - -fn opt_pubkey_string_ser(k_opt: &Option, ser: S) -> Result -where - S: Serializer, -{ - let k_str_opt = (*k_opt).map(|k| k.to_string()); - - Option::::serialize(&k_str_opt, ser) -} - -fn opt_pubkey_string_de<'de, D>(de: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - match Option::::deserialize(de)? { - Some(k) => Ok(Some(Pubkey::from_str(&k).map_err(D::Error::custom)?)), - None => Ok(None), - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::attestation_cfg::SymbolConfig::{ - Key, - Name, - }, - solitaire::ErrBox, - }; - - #[test] - fn test_sanity() -> Result<(), ErrBox> { - let fastbois = SymbolGroupConfig { - group_name: "fast bois".to_owned(), - conditions: Some(AttestationConditions { - min_interval_ms: 5, - ..Default::default() - }), - symbols: vec![ - Name { - name: "ETHUSD".to_owned(), - }, - Key { - name: Some("BTCUSD".to_owned()), - product: Pubkey::new_unique(), - price: Pubkey::new_unique(), - }, - ], - }; - - let slowbois = SymbolGroupConfig { - group_name: "slow bois".to_owned(), - conditions: Some(AttestationConditions { - min_interval_ms: 200, - ..Default::default() - }), - symbols: vec![ - Name { - name: "CNYAUD".to_owned(), - }, - Key { - name: None, - product: Pubkey::new_unique(), - price: Pubkey::new_unique(), - }, - ], - }; - - let cfg = AttestationConfig { - min_msg_reuse_interval_ms: 1000, - max_msg_accounts: 100_000, - enable_healthcheck: true, - healthcheck_window_size: 100, - min_rpc_interval_ms: 2123, - mapping_addr: None, - mapping_reload_interval_mins: 42, - default_attestation_conditions: AttestationConditions::default(), - symbol_groups: vec![fastbois, slowbois], - }; - - let serialized = serde_yaml::to_string(&cfg)?; - - let deserialized: AttestationConfig = serde_yaml::from_str(&serialized)?; - - assert_eq!(cfg, deserialized); - - Ok(()) - } - - #[test] - fn test_instantiate_batches() -> Result<(), ErrBox> { - let btc_product_key = Pubkey::new_unique(); - let btc_price_key = Pubkey::new_unique(); - - let eth_product_key = Pubkey::new_unique(); - let eth_price_key_1 = Pubkey::new_unique(); - let eth_price_key_2 = Pubkey::new_unique(); - - let unk_product_key = Pubkey::new_unique(); - let unk_price_key = Pubkey::new_unique(); - - let eth_dup_product_key = Pubkey::new_unique(); - let eth_dup_price_key = Pubkey::new_unique(); - - let attestation_conditions_1 = AttestationConditions { - min_interval_ms: 5, - ..Default::default() - }; - - let products = vec![ - P2WProductAccount { - name: Some("ETHUSD".to_owned()), - key: eth_product_key, - price_account_keys: vec![eth_price_key_1, eth_price_key_2], - }, - P2WProductAccount { - name: None, - key: unk_product_key, - price_account_keys: vec![unk_price_key], - }, - ]; - - let group1 = SymbolGroupConfig { - group_name: "group 1".to_owned(), - conditions: Some(attestation_conditions_1.clone()), - symbols: vec![ - Key { - name: Some("BTCUSD".to_owned()), - price: btc_price_key, - product: btc_product_key, - }, - Name { - name: "ETHUSD".to_owned(), - }, - ], - }; - - let group2 = SymbolGroupConfig { - group_name: "group 2".to_owned(), - conditions: None, - symbols: vec![Key { - name: Some("ETHUSD".to_owned()), - price: eth_dup_price_key, - product: eth_dup_product_key, - }], - }; - - let default_attestation_conditions = AttestationConditions { - min_interval_ms: 1, - ..Default::default() - }; - - let cfg = AttestationConfig { - min_msg_reuse_interval_ms: 1000, - max_msg_accounts: 100_000, - healthcheck_window_size: 100, - enable_healthcheck: true, - min_rpc_interval_ms: 2123, - mapping_addr: None, - mapping_reload_interval_mins: 42, - default_attestation_conditions: default_attestation_conditions.clone(), - symbol_groups: vec![group1, group2], - }; - - let batches = cfg.instantiate_batches(&products, 2); - - assert_eq!( - batches, - vec![ - SymbolBatch { - group_name: "group 1".to_owned(), - conditions: attestation_conditions_1.clone(), - symbols: vec![ - P2WSymbol { - name: Some("BTCUSD".to_owned()), - product_addr: btc_product_key, - price_addr: btc_price_key, - }, - P2WSymbol { - name: Some("ETHUSD".to_owned()), - product_addr: eth_product_key, - price_addr: eth_price_key_1, - } - ], - }, - SymbolBatch { - group_name: "group 1".to_owned(), - conditions: attestation_conditions_1, - symbols: vec![P2WSymbol { - name: Some("ETHUSD".to_owned()), - product_addr: eth_product_key, - price_addr: eth_price_key_2, - }], - }, - SymbolBatch { - group_name: "group 2".to_owned(), - conditions: default_attestation_conditions.clone(), - symbols: vec![P2WSymbol { - name: Some("ETHUSD".to_owned()), - product_addr: eth_dup_product_key, - price_addr: eth_dup_price_key, - }], - }, - SymbolBatch { - group_name: "mapping".to_owned(), - conditions: default_attestation_conditions, - symbols: vec![P2WSymbol { - name: None, - product_addr: unk_product_key, - price_addr: unk_price_key, - }], - } - ] - ); - - Ok(()) - } -} diff --git a/wormhole_attester/client/src/batch_state.rs b/wormhole_attester/client/src/batch_state.rs deleted file mode 100644 index 6ba0476c..00000000 --- a/wormhole_attester/client/src/batch_state.rs +++ /dev/null @@ -1,159 +0,0 @@ -use { - crate::{ - attestation_cfg::SymbolBatch, - AttestationConditions, - P2WSymbol, - }, - log::{ - debug, - warn, - }, - pyth_sdk_solana::state::PriceAccount, - solana_client::nonblocking::rpc_client::RpcClient, - std::time::{ - Duration, - Instant, - }, -}; - -/// Runtime representation of a batch. It refers to the original group -/// from the config. -#[derive(Debug)] -pub struct BatchState { - pub group_name: String, - pub symbols: Vec, - pub last_known_symbol_states: Vec>, - pub conditions: AttestationConditions, - pub last_job_finished_at: Instant, -} - -impl<'a> BatchState { - pub fn new(group: &SymbolBatch) -> Self { - Self { - group_name: group.group_name.clone(), - symbols: group.symbols.clone(), - conditions: group.conditions.clone(), - last_known_symbol_states: vec![None; group.symbols.len()], - last_job_finished_at: Instant::now(), - } - } - - /// Evaluate the configured attestation conditions for this - /// batch. RPC is used to update last known state. Returns - /// Some("") if any trigger condition was met. Only the - /// first encountered condition is mentioned. - pub async fn should_resend(&mut self, c: &RpcClient) -> Option { - let mut ret = None; - - let sym_count = self.symbols.len(); - let pubkeys: Vec<_> = self.symbols.iter().map(|s| s.price_addr).collect(); - - // min interval - if self.last_job_finished_at.elapsed() - > Duration::from_millis(self.conditions.min_interval_ms) - { - ret = Some(format!( - "minimum interval of {}s elapsed since last state change", - self.conditions.min_interval_ms - )); - } - - // Only lookup and compare symbols if the conditions require - if self.conditions.need_onchain_lookup() { - let new_symbol_states: Vec> = - match c.get_multiple_accounts(&pubkeys).await { - Ok(acc_opts) => { - acc_opts - .into_iter() - .enumerate() - .map(|(idx, opt)| { - // Take each Some(acc), make it None and log on load_price_account() error - opt.and_then(|acc| { - pyth_sdk_solana::state::load_price_account(&acc.data) - .cloned() // load_price_account() transmutes the data reference into another reference, and owning acc_opts is not enough - .map_err(|e| { - warn!( - "Could not parse symbol {}/{}: {}", - idx, sym_count, e - ); - e - }) - .ok() // Err becomes None - }) - }) - .collect() - } - Err(e) => { - warn!("Could not look up any symbols on-chain: {}", e); - vec![None; sym_count] - } - }; - - for (idx, old_new_tup) in self - .last_known_symbol_states - .iter_mut() // Borrow mutably to make the update easier - .zip(new_symbol_states.iter()) - .enumerate() - { - // Only evaluate this symbol if a triggering condition is not already met - if ret.is_some() { - break; - } - match old_new_tup { - (Some(old), Some(new)) => { - // publish_time_changed - if let Some(min_delta_secs) = self.conditions.publish_time_min_delta_secs { - if new.timestamp - old.timestamp > min_delta_secs as i64 { - ret = Some(format!( - "publish_time advanced by at least {}s for {:?}", - min_delta_secs, - self.symbols[idx].to_string(), - )) - } - - // price_changed_bps - } else if let Some(bps) = self.conditions.price_changed_bps { - let pct = bps as f64 / 100.0; - let price_pct_diff = ((old.agg.price as f64 - new.agg.price as f64) - / old.agg.price as f64 - * 100.0) - .abs(); - - if price_pct_diff > pct { - ret = Some(format!( - "price moved by at least {}% for {:?}", - pct, - self.symbols[idx].to_string() - )) - } - } - } - _ => { - debug!( - "Symbol {:?} {}/{}, old or new state value is None, skipping...", - self.symbols[idx].to_string(), - idx + 1, - sym_count - ); - } - } - } - // Update with newer state only if a condition was met. We - // don't want to shadow changes that may happen over a larger - // period between state lookups. - if ret.is_some() { - for (old, new) in self - .last_known_symbol_states - .iter_mut() - .zip(new_symbol_states.into_iter()) - { - if new.is_some() { - *old = new; - } - } - } - } - - ret - } -} diff --git a/wormhole_attester/client/src/cli.rs b/wormhole_attester/client/src/cli.rs deleted file mode 100644 index ee213564..00000000 --- a/wormhole_attester/client/src/cli.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! CLI options - -use { - clap::{ - Parser, - Subcommand, - }, - solana_program::pubkey::Pubkey, - solana_sdk::commitment_config::CommitmentConfig, - std::{ - net::SocketAddr, - path::PathBuf, - }, -}; - -#[derive(Parser)] -#[clap( - about = "A client for the pyth2wormhole Solana program", - author = "Pyth Network Contributors" -)] -pub struct Cli { - #[clap( - long, - help = "Identity JSON file for the entity meant to cover transaction costs", - default_value = "~/.config/solana/id.json" - )] - pub payer: String, - #[clap(short, long, default_value = "http://localhost:8899")] - pub rpc_url: String, - #[clap(long, default_value = "confirmed")] - pub commitment: CommitmentConfig, - #[clap(long)] - pub p2w_addr: Pubkey, - #[clap(subcommand)] - pub action: Action, -} - -#[derive(Subcommand)] -pub enum Action { - #[clap(about = "Initialize a pyth2wormhole program freshly deployed under ")] - Init { - /// The bridge program account - #[clap(short = 'w', long = "wh-prog")] - wh_prog: Pubkey, - #[clap(short = 'o', long = "owner")] - owner_addr: Pubkey, - #[clap(short = 'p', long = "pyth-owner")] - pyth_owner_addr: Pubkey, - /// Option<> makes sure not specifying this flag does not imply "false" - #[clap(long = "is-active")] - is_active: Option, - #[clap(long = "ops-owner")] - ops_owner_addr: Option, - }, - #[clap( - about = "Use an existing pyth2wormhole program to attest product price information to another chain" - )] - // Note: defaults target SOL mainnet-beta conditions at implementation time - Attest { - #[clap(short = 'f', long = "--config", help = "Attestation YAML config")] - attestation_cfg: PathBuf, - #[clap( - short = 't', - long = "--timeout", - help = "How many seconds to wait before giving up on tx confirmation.", - default_value = "20" - )] - confirmation_timeout_secs: u64, - #[clap( - short = 'm', - long, - help = "Address to use for serving Prometheus metrics.", - default_value = "[::]:3000" - )] - metrics_bind_addr: SocketAddr, - }, - #[clap(about = "Retrieve a pyth2wormhole program's current settings")] - GetConfig, - #[clap(about = "Update an existing pyth2wormhole program's settings")] - SetConfig { - /// Current owner keypair path - #[clap( - long, - default_value = "~/.config/solana/id.json", - help = "Keypair file for the current config owner" - )] - owner: String, - /// New owner to set - #[clap(long = "new-owner")] - new_owner_addr: Option, - #[clap(long = "new-wh-prog")] - new_wh_prog: Option, - #[clap(long = "new-pyth-owner")] - new_pyth_owner_addr: Option, - #[clap(long = "is-active")] - is_active: Option, - #[clap(long = "ops-owner")] - ops_owner_addr: Option, - #[clap(long = "remove-ops-owner", conflicts_with = "ops-owner-addr")] - remove_ops_owner: bool, - }, - #[clap( - about = "Migrate existing pyth2wormhole program settings to a newer format version. Client version must match the deployed contract." - )] - Migrate { - /// owner keypair path - #[clap( - long, - default_value = "~/.config/solana/id.json", - help = "Keypair file for the current config owner" - )] - owner: String, - }, - #[clap(about = "Print out emitter address for the specified pyth2wormhole contract")] - GetEmitter, - #[clap(about = "Set the value of is_active config as ops_owner")] - SetIsActive { - /// Current ops owner keypair path - #[clap( - long, - default_value = "~/.config/solana/id.json", - help = "Keypair file for the current ops owner" - )] - ops_owner: String, - #[clap( - index = 1, - possible_values = ["true", "false"], - )] - new_is_active: String, - }, -} diff --git a/wormhole_attester/client/src/config_file.rs b/wormhole_attester/client/src/config_file.rs deleted file mode 100644 index 4e83e5e1..00000000 --- a/wormhole_attester/client/src/config_file.rs +++ /dev/null @@ -1,38 +0,0 @@ -#[derive(Deserialize, Serialize)] -pub struct Config { - symbols: Vec, -} - -/// Config entry for a Pyth2Wormhole product + price pair -#[derive(Deserialize, Serialize)] -pub struct P2WSymbol { - /// Optional human-readable name, never used on-chain; makes - /// attester logs and the config easier to understand - name: Option, - product: Pubkey, - price: Pubkey, -} - -#[testmod] -mod tests { - #[test] - fn test_sanity() -> Result<(), ErrBox> { - let serialized = r#" -symbols: - - name: ETH/USD - product_addr: 11111111111111111111111111111111 - price_addr: 11111111111111111111111111111111 - - name: SOL/EUR - product_addr: 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi - price_addr: 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi - - name: BTC/CNY - product_addr: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR - price_addr: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR - - # no name - product_addr: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR - price_addr: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR -"#; - let deserialized = serde_yaml::from_str(serialized)?; - Ok(()) - } -} diff --git a/wormhole_attester/client/src/healthcheck.rs b/wormhole_attester/client/src/healthcheck.rs deleted file mode 100644 index f069c708..00000000 --- a/wormhole_attester/client/src/healthcheck.rs +++ /dev/null @@ -1,54 +0,0 @@ -use { - crate::attestation_cfg, - std::{ - collections::VecDeque, - convert::TryInto, - sync::Arc, - }, - tokio::sync::Mutex, -}; - -lazy_static::lazy_static! { - pub static ref HEALTHCHECK_STATE: Arc> = Arc::new(Mutex::new(HealthCheckState::new(attestation_cfg::default_healthcheck_window_size().try_into().expect("could not convert window size to usize"), attestation_cfg::default_enable_healthcheck()))); -} - -/// Helper structure for deciding service health -pub struct HealthCheckState { - /// Whether to report the healthy/unhealthy status - pub enable: bool, - /// Sliding LIFO window over last `max_window_size` attestation results (true = ok, false = error) - pub window: VecDeque, - /// Window size - pub max_window_size: usize, -} - - -impl HealthCheckState { - pub fn new(max_window_size: usize, enable: bool) -> Self { - Self { - enable, - window: VecDeque::with_capacity(max_window_size), - max_window_size, - } - } - /// Check service health, return None if not enough data is present - pub fn is_healthy(&self) -> Option { - if self.window.len() >= self.max_window_size && self.enable { - // If all results are false, return false (unhealthy). - Some(self.window.iter().any(|entry| *entry)) - } else { - // The window isn't big enough yet or the healthcheck is disabled - None - } - } - - /// Rotate the window - pub fn add_result(&mut self, res: bool) { - self.window.push_front(res); - - // Trim window back to size if needed. truncate() deletes from - // the back and has no effect if new size is greater than - // current size. - self.window.truncate(self.max_window_size); - } -} diff --git a/wormhole_attester/client/src/lib.rs b/wormhole_attester/client/src/lib.rs deleted file mode 100644 index a3e5f268..00000000 --- a/wormhole_attester/client/src/lib.rs +++ /dev/null @@ -1,539 +0,0 @@ -pub mod attestation_cfg; -pub mod batch_state; -pub mod healthcheck; -pub mod message; -pub mod util; - -pub use { - attestation_cfg::{ - AttestationConditions, - AttestationConfig, - P2WSymbol, - }, - batch_state::BatchState, - healthcheck::{ - HealthCheckState, - HEALTHCHECK_STATE, - }, - message::P2WMessageQueue, - pyth_wormhole_attester::Pyth2WormholeConfig, - util::{ - start_metrics_server, - RLMutex, - RLMutexGuard, - }, -}; -use { - borsh::{ - BorshDeserialize, - BorshSerialize, - }, - bridge::{ - accounts::{ - Bridge, - FeeCollector, - Sequence, - SequenceDerivationData, - }, - types::ConsistencyLevel, - }, - log::{ - debug, - trace, - warn, - }, - pyth_sdk_solana::state::{ - load_mapping_account, - load_price_account, - load_product_account, - }, - pyth_wormhole_attester::{ - attestation_state::AttestationStatePDA, - config::{ - OldP2WConfigAccount, - P2WConfigAccount, - }, - message::{ - P2WMessage, - P2WMessageDrvData, - }, - AttestData, - }, - pyth_wormhole_attester_sdk::P2WEmitter, - solana_client::nonblocking::rpc_client::RpcClient, - solana_program::{ - hash::Hash, - instruction::{ - AccountMeta, - Instruction, - }, - pubkey::Pubkey, - system_program, - sysvar::{ - clock, - rent, - }, - }, - solana_sdk::{ - signer::{ - keypair::Keypair, - Signer, - }, - transaction::Transaction, - }, - solitaire::{ - processors::seeded::Seeded, - AccountState, - ErrBox, - }, -}; - -/// Future-friendly version of solitaire::ErrBox -pub type ErrBoxSend = Box; - -pub fn gen_init_tx( - payer: Keypair, - p2w_addr: Pubkey, - config: Pyth2WormholeConfig, - latest_blockhash: Hash, -) -> Result { - let payer_pubkey = payer.pubkey(); - let acc_metas = vec![ - // new_config - AccountMeta::new( - P2WConfigAccount::<{ AccountState::Uninitialized }>::key(None, &p2w_addr), - false, - ), - // payer - AccountMeta::new(payer.pubkey(), true), - // system_program - AccountMeta::new(system_program::id(), false), - ]; - - let ix_data = ( - pyth_wormhole_attester::instruction::Instruction::Initialize, - config, - ); - - let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas); - - let signers = vec![&payer]; - - let tx_signed = Transaction::new_signed_with_payer::>( - &[ix], - Some(&payer_pubkey), - &signers, - latest_blockhash, - ); - Ok(tx_signed) -} - -pub fn get_set_config_ix( - p2w_addr: &Pubkey, - owner_pubkey: &Pubkey, - payer_pubkey: &Pubkey, - new_config: Pyth2WormholeConfig, -) -> Result { - let acc_metas = vec![ - // config - AccountMeta::new( - P2WConfigAccount::<{ AccountState::Initialized }>::key(None, p2w_addr), - false, - ), - // current_owner - AccountMeta::new(*owner_pubkey, true), - // payer - AccountMeta::new(*payer_pubkey, true), - // system_program - AccountMeta::new(system_program::id(), false), - ]; - let ix_data = ( - pyth_wormhole_attester::instruction::Instruction::SetConfig, - new_config, - ); - Ok(Instruction::new_with_bytes( - *p2w_addr, - ix_data.try_to_vec()?.as_slice(), - acc_metas, - )) -} - -pub fn gen_set_config_tx( - payer: Keypair, - p2w_addr: Pubkey, - owner: Keypair, - new_config: Pyth2WormholeConfig, - latest_blockhash: Hash, -) -> Result { - let ix = get_set_config_ix(&p2w_addr, &owner.pubkey(), &payer.pubkey(), new_config)?; - - let signers = vec![&owner, &payer]; - let tx_signed = Transaction::new_signed_with_payer::>( - &[ix], - Some(&payer.pubkey()), - &signers, - latest_blockhash, - ); - Ok(tx_signed) -} - -pub fn get_set_is_active_ix( - p2w_addr: &Pubkey, - ops_owner_pubkey: &Pubkey, - payer_pubkey: &Pubkey, - new_is_active: bool, -) -> Result { - let acc_metas = vec![ - // config - AccountMeta::new( - P2WConfigAccount::<{ AccountState::Initialized }>::key(None, p2w_addr), - false, - ), - // ops_owner - AccountMeta::new(*ops_owner_pubkey, true), - // payer - AccountMeta::new(*payer_pubkey, true), - ]; - - let ix_data = ( - pyth_wormhole_attester::instruction::Instruction::SetIsActive, - new_is_active, - ); - Ok(Instruction::new_with_bytes( - *p2w_addr, - ix_data.try_to_vec()?.as_slice(), - acc_metas, - )) -} - -pub fn gen_set_is_active_tx( - payer: Keypair, - p2w_addr: Pubkey, - ops_owner: Keypair, - new_is_active: bool, - latest_blockhash: Hash, -) -> Result { - let ix = get_set_is_active_ix( - &p2w_addr, - &ops_owner.pubkey(), - &payer.pubkey(), - new_is_active, - )?; - - let signers = vec![&ops_owner, &payer]; - let tx_signed = Transaction::new_signed_with_payer::>( - &[ix], - Some(&payer.pubkey()), - &signers, - latest_blockhash, - ); - Ok(tx_signed) -} - -pub fn gen_migrate_tx( - payer: Keypair, - p2w_addr: Pubkey, - owner: Keypair, - latest_blockhash: Hash, -) -> Result { - let payer_pubkey = payer.pubkey(); - - let acc_metas = vec![ - // new_config - AccountMeta::new( - P2WConfigAccount::<{ AccountState::Uninitialized }>::key(None, &p2w_addr), - false, - ), - // old_config - AccountMeta::new(OldP2WConfigAccount::key(None, &p2w_addr), false), - // owner - AccountMeta::new(owner.pubkey(), true), - // payer - AccountMeta::new(payer.pubkey(), true), - // system_program - AccountMeta::new(system_program::id(), false), - ]; - - let ix_data = ( - pyth_wormhole_attester::instruction::Instruction::Migrate, - (), - ); - - let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas); - - let signers = vec![&owner, &payer]; - - let tx_signed = Transaction::new_signed_with_payer::>( - &[ix], - Some(&payer_pubkey), - &signers, - latest_blockhash, - ); - Ok(tx_signed) -} - -/// Get the current config account data for given p2w program address -pub async fn get_config_account( - rpc_client: &RpcClient, - p2w_addr: &Pubkey, -) -> Result { - let p2w_config_addr = P2WConfigAccount::<{ AccountState::Initialized }>::key(None, p2w_addr); - - let config = Pyth2WormholeConfig::try_from_slice( - rpc_client - .get_account_data(&p2w_config_addr) - .await? - .as_slice(), - )?; - - Ok(config) -} - -/// Generate an Instruction for making the attest() contract -/// call. -pub fn gen_attest_tx( - p2w_addr: Pubkey, - p2w_config: &Pyth2WormholeConfig, // Must be fresh, not retrieved inside to keep side effects away - payer: &Keypair, - wh_msg_id: u64, - symbols: &[P2WSymbol], - latest_blockhash: Hash, - // Desired rate limit interval. If all of the symbols are over - // the limit, the tx will fail. 0 means off. - rate_limit_interval_secs: u32, -) -> Result { - let emitter_addr = P2WEmitter::key(None, &p2w_addr); - - let seq_addr = Sequence::key( - &SequenceDerivationData { - emitter_key: &emitter_addr, - }, - &p2w_config.wh_prog, - ); - - let p2w_config_addr = P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_addr); - if symbols.len() > p2w_config.max_batch_size as usize { - return Err((format!( - "Expected up to {} symbols for batch, {} were found", - p2w_config.max_batch_size, - symbols.len() - )) - .into()); - } - // Initial attest() accounts - let mut acc_metas = vec![ - // payer - AccountMeta::new(payer.pubkey(), true), - // system_program - AccountMeta::new_readonly(system_program::id(), false), - // config - AccountMeta::new_readonly(p2w_config_addr, false), - ]; - - // Batch contents and padding if applicable - let mut padded_symbols = { - let mut not_padded: Vec<_> = symbols - .iter() - .flat_map(|s| { - let state_address = AttestationStatePDA::key(&s.price_addr, &p2w_addr); - vec![ - AccountMeta::new(state_address, false), - AccountMeta::new_readonly(s.price_addr, false), - ] - }) - .collect(); - - // Align to max batch size with null accounts - let mut padding_accounts = - vec![ - AccountMeta::new_readonly(Pubkey::new_from_array([0u8; 32]), false); - 2 * (p2w_config.max_batch_size as usize - symbols.len()) - ]; - not_padded.append(&mut padding_accounts); - - not_padded - }; - - acc_metas.append(&mut padded_symbols); - - // Continue with other pyth_wormhole_attester accounts - let mut acc_metas_remainder = vec![ - // clock - AccountMeta::new_readonly(clock::id(), false), - // wh_prog - AccountMeta::new_readonly(p2w_config.wh_prog, false), - // wh_bridge - AccountMeta::new( - Bridge::<{ AccountState::Initialized }>::key(None, &p2w_config.wh_prog), - false, - ), - // wh_message - AccountMeta::new( - P2WMessage::key( - &P2WMessageDrvData { - id: wh_msg_id, - batch_size: symbols.len() as u16, - message_owner: payer.pubkey(), - }, - &p2w_addr, - ), - false, - ), - // wh_emitter - AccountMeta::new_readonly(emitter_addr, false), - // wh_sequence - AccountMeta::new(seq_addr, false), - // wh_fee_collector - AccountMeta::new(FeeCollector::<'_>::key(None, &p2w_config.wh_prog), false), - AccountMeta::new_readonly(rent::id(), false), - ]; - - acc_metas.append(&mut acc_metas_remainder); - - let ix_data = ( - pyth_wormhole_attester::instruction::Instruction::Attest, - AttestData { - consistency_level: ConsistencyLevel::Confirmed, - message_account_id: wh_msg_id, - rate_limit_interval_secs, - }, - ); - - let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas); - - let tx_signed = Transaction::new_signed_with_payer::>( - &[ix], - Some(&payer.pubkey()), - &vec![payer], - latest_blockhash, - ); - Ok(tx_signed) -} - -/// Enumerates all products and their prices in a Pyth mapping. -/// Returns map of: product address => [price addresses] -pub async fn crawl_pyth_mapping( - rpc_client: &RpcClient, - first_mapping_addr: &Pubkey, -) -> Result, ErrBox> { - let mut ret: Vec = vec![]; - - let mut n_mappings = 1; // We assume the first one must be valid - let mut n_products_total = 0; // Grand total products in all mapping accounts - let mut n_prices_total = 0; // Grand total prices in all product accounts in all mapping accounts - - let mut mapping_addr = *first_mapping_addr; - - // loop until the last non-zero MappingAccount.next account - loop { - let mapping_bytes = rpc_client.get_account_data(&mapping_addr).await?; - let mapping = match load_mapping_account(&mapping_bytes) { - Ok(p) => p, - Err(e) => { - warn!( - "Mapping: Could not parse account {} as a Pyth mapping, crawling terminated. Error: {:?}", - mapping_addr, e - ); - break; - } - }; - - // Products in this mapping account - let mut n_mapping_products = 0; - - // loop through all products in this mapping; filter out zeroed-out empty product slots - for prod_addr in mapping.products.iter().filter(|p| *p != &Pubkey::default()) { - let prod_bytes = rpc_client.get_account_data(prod_addr).await?; - let prod = match load_product_account(&prod_bytes) { - Ok(p) => p, - Err(e) => { - warn!("Mapping {}: Could not parse account {} as a Pyth product, skipping to next product. Error: {:?}", mapping_addr, prod_addr, e); - continue; - } - }; - - let mut prod_name = None; - for (key, val) in prod.iter() { - if key.eq_ignore_ascii_case("symbol") { - prod_name = Some(val.to_owned()); - } - } - - let mut price_addr = prod.px_acc; - let mut n_prod_prices = 0; - - // the product might have no price, can happen in tilt due to race-condition, failed tx to add price, ... - if price_addr == Pubkey::default() { - debug!( - "Found product with addr {} that has no prices. \ - This should not happen in a production enviornment.", - prod_addr - ); - - continue; - } - - // loop until the last non-zero PriceAccount.next account - let mut price_accounts: Vec = vec![]; - loop { - let price_bytes = rpc_client.get_account_data(&price_addr).await?; - let price = match load_price_account(&price_bytes) { - Ok(p) => p, - Err(e) => { - warn!("Product {}: Could not parse account {} as a Pyth price, skipping to next product. Error: {:?}", prod_addr, price_addr, e); - break; - } - }; - - price_accounts.push(price_addr); - n_prod_prices += 1; - - if price.next == Pubkey::default() { - trace!( - "Product {}: processed {} price(s)", - prod_addr, - n_prod_prices - ); - break; - } - - price_addr = price.next; - } - ret.push(P2WProductAccount { - key: *prod_addr, - name: prod_name.clone(), - price_account_keys: price_accounts, - }); - - n_prices_total += n_prod_prices; - } - n_mapping_products += 1; - n_products_total += n_mapping_products; - - // Traverse other mapping accounts if applicable - if mapping.next == Pubkey::default() { - trace!( - "Mapping {}: processed {} products", - mapping_addr, - n_mapping_products - ); - - break; - } - mapping_addr = mapping.next; - n_mappings += 1; - } - debug!( - "Processed {} price(s) in {} product account(s), in {} mapping account(s)", - n_prices_total, n_products_total, n_mappings - ); - - Ok(ret) -} - -#[derive(Clone, Debug)] -pub struct P2WProductAccount { - pub key: Pubkey, - pub name: Option, - pub price_account_keys: Vec, -} diff --git a/wormhole_attester/client/src/main.rs b/wormhole_attester/client/src/main.rs deleted file mode 100644 index 0b3b54cc..00000000 --- a/wormhole_attester/client/src/main.rs +++ /dev/null @@ -1,781 +0,0 @@ -use { - pyth_wormhole_attester::error::AttesterCustomError, - pyth_wormhole_attester_client::util::send_and_confirm_transaction_with_config, - solana_client::rpc_config::RpcSendTransactionConfig, - solana_program::instruction::InstructionError, - solana_sdk::transaction::TransactionError, -}; - -pub mod cli; - -use { - clap::Parser, - cli::{ - Action, - Cli, - }, - futures::future::{ - Future, - TryFutureExt, - }, - generic_array::GenericArray, - lazy_static::lazy_static, - log::{ - debug, - error, - info, - warn, - LevelFilter, - }, - prometheus::{ - register_histogram, - register_int_counter, - register_int_gauge, - Histogram, - IntCounter, - IntGauge, - }, - pyth_wormhole_attester::{ - attest::P2W_MAX_BATCH_SIZE, - Pyth2WormholeConfig, - }, - pyth_wormhole_attester_client::{ - attestation_cfg::SymbolBatch, - crawl_pyth_mapping, - gen_attest_tx, - gen_init_tx, - gen_migrate_tx, - gen_set_config_tx, - gen_set_is_active_tx, - get_config_account, - healthcheck::HealthCheckState, - start_metrics_server, - AttestationConfig, - BatchState, - ErrBoxSend, - P2WMessageQueue, - P2WSymbol, - RLMutex, - HEALTHCHECK_STATE, - }, - pyth_wormhole_attester_sdk::P2WEmitter, - sha3::{ - Digest, - Sha3_256, - }, - solana_client::{ - nonblocking::rpc_client::RpcClient, - rpc_config::RpcTransactionConfig, - }, - solana_program::pubkey::Pubkey, - solana_sdk::{ - commitment_config::CommitmentConfig, - signature::read_keypair_file, - signer::keypair::Keypair, - }, - solana_transaction_status::UiTransactionEncoding, - solitaire::{ - processors::seeded::Seeded, - ErrBox, - }, - std::{ - fs::File, - net::SocketAddr, - sync::Arc, - time::{ - Duration, - Instant, - }, - }, - tokio::{ - sync::{ - Mutex, - Semaphore, - }, - task::JoinHandle, - }, -}; - -pub const SEQNO_PREFIX: &str = "Program log: Sequence: "; - -lazy_static! { - static ref ATTESTATIONS_OK_CNT: IntCounter = - register_int_counter!("attestations_ok", "Number of successful attestations") - .expect("FATAL: Could not instantiate ATTESTATIONS_OK_CNT"); - static ref ATTESTATIONS_ERR_CNT: IntCounter = - register_int_counter!("attestations_err", "Number of failed attestations") - .expect("FATAL: Could not instantiate ATTESTATIONS_ERR_CNT"); - static ref LAST_SEQNO_GAUGE: IntGauge = register_int_gauge!( - "last_seqno", - "Latest sequence number produced by this attester" - ) - .expect("FATAL: Could not instantiate LAST_SEQNO_GAUGE"); - static ref SOL_RPC_TX_PROCESSING_HIST: Histogram = register_histogram!( - "sol_rpc_tx_processing", - "How long in seconds it takes to send a transaction to the Solana RPC", - vec![0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 3.5, 4.0, 5.0, 10.0, 20.0, 30.0, 60.0] // Buckets, 1.0 = 1 second - ) - .expect("FATAL: Could not instantiate SOL_RPC_TX_PROCESSING_HIST"); -} - -#[tokio::main(flavor = "multi_thread")] -async fn main() -> Result<(), ErrBox> { - let cli = Cli::parse(); - init_logging(); - - // All other CLI actions make rpc requests, this one's meant to be - // off-chain explicitly - if let Action::GetEmitter = cli.action { - let emitter_addr = P2WEmitter::key(None, &cli.p2w_addr); - println!("{emitter_addr}"); - - // Exit early - return Ok(()); - } - - let payer = read_keypair_file(&*shellexpand::tilde(&cli.payer))?; - - let rpc_client = RpcClient::new_with_commitment(cli.rpc_url.clone(), cli.commitment); - - let p2w_addr = cli.p2w_addr; - - let latest_blockhash = rpc_client.get_latest_blockhash().await?; - - match cli.action { - Action::Init { - owner_addr, - pyth_owner_addr, - wh_prog, - is_active, - ops_owner_addr, - } => { - let tx = gen_init_tx( - payer, - p2w_addr, - Pyth2WormholeConfig { - owner: owner_addr, - wh_prog, - pyth_owner: pyth_owner_addr, - is_active: is_active.unwrap_or(true), - max_batch_size: P2W_MAX_BATCH_SIZE, - ops_owner: ops_owner_addr, - }, - latest_blockhash, - )?; - rpc_client - .send_and_confirm_transaction_with_spinner(&tx) - .await?; - println!( - "Initialized with config:\n{:?}", - get_config_account(&rpc_client, &p2w_addr).await? - ); - } - Action::GetConfig => { - println!("{:?}", get_config_account(&rpc_client, &p2w_addr).await?); - } - Action::SetConfig { - ref owner, - new_owner_addr, - new_wh_prog, - new_pyth_owner_addr, - is_active, - ops_owner_addr, - remove_ops_owner, - } => { - let old_config = get_config_account(&rpc_client, &p2w_addr).await?; - - let new_ops_owner = if remove_ops_owner { - None - } else if let Some(given_ops_owner) = ops_owner_addr { - Some(given_ops_owner) - } else { - old_config.ops_owner - }; - - let tx = gen_set_config_tx( - payer, - p2w_addr, - read_keypair_file(&*shellexpand::tilde(&owner))?, - Pyth2WormholeConfig { - owner: new_owner_addr.unwrap_or(old_config.owner), - wh_prog: new_wh_prog.unwrap_or(old_config.wh_prog), - pyth_owner: new_pyth_owner_addr.unwrap_or(old_config.pyth_owner), - is_active: is_active.unwrap_or(old_config.is_active), - max_batch_size: P2W_MAX_BATCH_SIZE, - ops_owner: new_ops_owner, - }, - latest_blockhash, - )?; - rpc_client - .send_and_confirm_transaction_with_spinner(&tx) - .await?; - println!( - "Applied config:\n{:?}", - get_config_account(&rpc_client, &p2w_addr).await? - ); - } - Action::Migrate { ref owner } => { - let tx = gen_migrate_tx( - payer, - p2w_addr, - read_keypair_file(&*shellexpand::tilde(&owner))?, - latest_blockhash, - )?; - rpc_client - .send_and_confirm_transaction_with_spinner(&tx) - .await?; - println!( - "Applied config:\n{:?}", - get_config_account(&rpc_client, &p2w_addr).await? - ); - } - Action::Attest { - ref attestation_cfg, - confirmation_timeout_secs, - metrics_bind_addr, - } => { - // Load the attestation config yaml - let attestation_cfg: AttestationConfig = - serde_yaml::from_reader(File::open(attestation_cfg)?)?; - - // Derive seeded accounts - let emitter_addr = P2WEmitter::key(None, &p2w_addr); - - info!("Using emitter addr {}", emitter_addr); - // Note: For global rate-limitting of RPC requests, we use a - // custom Mutex wrapper which enforces a delay of rpc_interval - // between RPC accesses. - let rpc_cfg = Arc::new(RLMutex::new( - RpcCfg { - url: cli.rpc_url, - timeout: Duration::from_secs(confirmation_timeout_secs), - commitment: cli.commitment, - }, - Duration::from_millis(attestation_cfg.min_rpc_interval_ms), - )); - - handle_attest(rpc_cfg, payer, p2w_addr, attestation_cfg, metrics_bind_addr).await?; - } - Action::GetEmitter => unreachable! {}, // It is handled early in this function. - Action::SetIsActive { - ops_owner, - new_is_active, - } => { - let tx = gen_set_is_active_tx( - payer, - p2w_addr, - read_keypair_file(&*shellexpand::tilde(&ops_owner))?, - new_is_active.eq_ignore_ascii_case("true"), - latest_blockhash, - )?; - rpc_client - .send_and_confirm_transaction_with_spinner(&tx) - .await?; - println!( - "Applied config:\n{:?}", - get_config_account(&rpc_client, &p2w_addr).await? - ); - } - } - - Ok(()) -} - -/// Continuously send batch attestations for symbols of an attestation config. -async fn handle_attest( - rpc_cfg: Arc>, - payer: Keypair, - p2w_addr: Pubkey, - attestation_cfg: AttestationConfig, - metrics_bind_addr: SocketAddr, -) -> Result<(), ErrBox> { - // Update healthcheck window size from config - if attestation_cfg.healthcheck_window_size == 0 { - return Err(format!( - "{} must be above 0", - stringify!(attestation_cfg.healthcheck_window_size) - ) - .into()); - } - - *HEALTHCHECK_STATE.lock().await = HealthCheckState::new( - attestation_cfg.healthcheck_window_size as usize, - attestation_cfg.enable_healthcheck, - ); - - if !attestation_cfg.enable_healthcheck { - warn!("WARNING: Healthcheck is disabled"); - } - - tokio::spawn(start_metrics_server(metrics_bind_addr)); - - info!("Started serving metrics on {}", metrics_bind_addr); - - info!( - "Crawling mapping {:?} every {} minutes", - attestation_cfg.mapping_addr, attestation_cfg.mapping_reload_interval_mins - ); - - // Used for easier detection of config changes - let mut hasher = Sha3_256::new(); - let mut old_sched_futs_state: Option<(JoinHandle<_>, GenericArray)> = None; // (old_futs_handle, old_config_hash) - - // For enforcing min_msg_reuse_interval_ms, we keep a piece of - // state that creates or reuses accounts if enough time had - // passed. It is crucial that this queue is reused across mapping - // lookups, so that previous symbol set's messages have enough - // time to be picked up by Wormhole guardians. - let message_q_mtx = Arc::new(Mutex::new(P2WMessageQueue::new( - Duration::from_millis(attestation_cfg.min_msg_reuse_interval_ms), - attestation_cfg.max_msg_accounts as usize, - ))); - - let mut batch_cfg = vec![]; - // This loop cranks attestations without interruption. This is - // achieved by spinning up a new up-to-date symbol set before - // letting go of the previous one. Additionally, hash of on-chain - // and attestation configs is used to prevent needless reloads of - // an unchanged symbol set. - loop { - let start_time = Instant::now(); // Helps timekeep mapping lookups accurately - - let config = match get_config_account(&lock_and_make_rpc(&rpc_cfg).await, &p2w_addr).await { - Ok(c) => c, - Err(e) => { - error!( - "Could not look up latest on-chain config in top-level loop: {:?}", - e - ); - continue; - } - }; - - // Use the mapping if specified - // If we cannot query the mapping account, retain the existing batch configuration. - batch_cfg = attestation_config_to_batches( - &rpc_cfg, - &attestation_cfg, - config.max_batch_size as usize, - ) - .await - .unwrap_or(batch_cfg); - - - // Hash currently known config - hasher.update(serde_yaml::to_vec(&batch_cfg)?); - hasher.update(borsh::to_vec(&config)?); - - let new_cfg_hash = hasher.finalize_reset(); - - if let Some((old_handle, old_cfg_hash)) = old_sched_futs_state.as_ref() { - // Ignore unchanged configs - if &new_cfg_hash == old_cfg_hash { - info!("Note: Attestation config and on-chain config unchanged, not stopping existing attestation sched jobs"); - } else { - // Process changed config into attestation scheduling futures - info!("Spinning up attestation sched jobs"); - // Start the new sched futures - let new_sched_futs_handle = tokio::spawn(prepare_attestation_sched_jobs( - &batch_cfg, - &config, - &rpc_cfg, - &p2w_addr, - &payer, - message_q_mtx.clone(), - )); - - // Quit old sched futures - old_handle.abort(); - - // The just started futures become the on-going attestation state - old_sched_futs_state = Some((new_sched_futs_handle, new_cfg_hash)); - } - } else { - // Base case for first attestation attempt - old_sched_futs_state = Some(( - tokio::spawn(prepare_attestation_sched_jobs( - &batch_cfg, - &config, - &rpc_cfg, - &p2w_addr, - &payer, - message_q_mtx.clone(), - )), - new_cfg_hash, - )); - } - - // Sum up elapsed time, wait for next run accurately - let target = Duration::from_secs(attestation_cfg.mapping_reload_interval_mins * 60); - let elapsed = start_time.elapsed(); - - let remaining = target.saturating_sub(elapsed); - - if remaining == Duration::from_secs(0) { - warn!( - "Processing took more than desired mapping lookup interval of {} seconds, not sleeping. Consider increasing {}", - target.as_secs(), - // stringify prints the up-to-date setting name automatically - stringify!(attestation_cfg.mapping_reload_interval_mins) - ); - } else { - info!( - "Processing new mapping took {}.{}s, next config/mapping refresh in {}.{}s", - elapsed.as_secs(), - elapsed.subsec_millis(), - remaining.as_secs(), - remaining.subsec_millis() - ); - } - - tokio::time::sleep(remaining).await; - } -} - -#[derive(Clone)] -pub struct RpcCfg { - pub url: String, - pub timeout: Duration, - pub commitment: CommitmentConfig, -} - -/// Helper function for claiming the rate-limited mutex and constructing an RPC instance -async fn lock_and_make_rpc(rlmtx: &RLMutex) -> RpcClient { - let RpcCfg { - url, - timeout, - commitment, - } = rlmtx.lock().await.clone(); - RpcClient::new_with_timeout_and_commitment(url, timeout, commitment) -} - - -/// Generate batches to attest by retrieving the on-chain product account data and grouping it -/// according to the configuration in `attestation_cfg`. -async fn attestation_config_to_batches( - rpc_cfg: &Arc>, - attestation_cfg: &AttestationConfig, - max_batch_size: usize, -) -> Result, ErrBox> { - // Use the mapping if specified - let products = if let Some(mapping_addr) = attestation_cfg.mapping_addr.as_ref() { - let product_accounts_res = - crawl_pyth_mapping(&lock_and_make_rpc(rpc_cfg).await, mapping_addr).await; - - if let Err(err) = &product_accounts_res { - error!( - "Could not crawl mapping {}: {:?}", - attestation_cfg.mapping_addr.unwrap_or_default(), - err - ); - } - - product_accounts_res? - } else { - vec![] - }; - - Ok(attestation_cfg.instantiate_batches(&products, max_batch_size)) -} - -/// Constructs attestation scheduling jobs from attestation config. -fn prepare_attestation_sched_jobs( - batch_cfg: &[SymbolBatch], - p2w_cfg: &Pyth2WormholeConfig, - rpc_cfg: &Arc>, - p2w_addr: &Pubkey, - payer: &Keypair, - message_q_mtx: Arc>, -) -> futures::future::JoinAll>> { - // Flatten attestation config into a plain list of batches - let batches: Vec<_> = batch_cfg.iter().map(BatchState::new).collect(); - - let batch_count = batches.len(); - - // Create attestation scheduling routines; see attestation_sched_job() for details - let attestation_sched_futs = batches.into_iter().enumerate().map(|(idx, batch)| { - attestation_sched_job(AttestationSchedJobArgs { - batch, - batch_no: idx + 1, - batch_count, - rpc_cfg: rpc_cfg.clone(), - p2w_addr: *p2w_addr, - config: p2w_cfg.clone(), - payer: Keypair::from_bytes(&payer.to_bytes()).unwrap(), - message_q_mtx: message_q_mtx.clone(), - }) - }); - - futures::future::join_all(attestation_sched_futs) -} - -/// The argument count on attestation_sched_job got out of hand. This -/// helps keep the correct order in check. -pub struct AttestationSchedJobArgs { - pub batch: BatchState, - pub batch_no: usize, - pub batch_count: usize, - pub rpc_cfg: Arc>, - pub p2w_addr: Pubkey, - pub config: Pyth2WormholeConfig, - pub payer: Keypair, - pub message_q_mtx: Arc>, -} - -/// A future that decides how a batch is sent in daemon mode. -/// -/// Attestations of the batch are scheduled continuously using -/// spawn(), which means that a next attestation of the same batch -/// begins immediately when a condition is met without waiting for the -/// previous attempt to finish. Subsequent attestations are started -/// according to the attestation_conditions field on the -/// batch. Concurrent requests per batch are limited by the -/// max_batch_jobs field to prevent excess memory usage on network -/// slowdowns etc.. -async fn attestation_sched_job(args: AttestationSchedJobArgs) -> Result<(), ErrBoxSend> { - let AttestationSchedJobArgs { - mut batch, - batch_no, - batch_count, - rpc_cfg, - p2w_addr, - config, - payer, - message_q_mtx, - } = args; - - // Stagger this sched job by batch_no * 10 milliseconds. It - // mitigates uneven distribution of tx requests which may resolve - // RPC timeouts on larger interval-based symbol groups. - tokio::time::sleep(Duration::from_millis(batch_no as u64 * 10)).await; - - // Enforces the max batch job count - let sema = Arc::new(Semaphore::new(batch.conditions.max_batch_jobs)); - loop { - debug!( - "Batch {}/{}, group {:?}: Scheduling attestation job", - batch_no, batch_count, batch.group_name - ); - - // park this routine until a resend condition is met - loop { - if let Some(reason) = batch - .should_resend(&lock_and_make_rpc(&rpc_cfg).await) - .await - { - info!( - "Batch {}/{}, group {}: Resending (reason: {:?})", - batch_no, batch_count, batch.group_name, reason - ); - break; - } - } - - if sema.available_permits() == 0 { - warn!( - "Batch {}/{}, group {:?}: Ran out of job \ - permits, some attestation conditions may be \ - delayed. For better accuracy, increase \ - max_batch_jobs or adjust attestation \ - conditions", - batch_no, batch_count, batch.group_name - ); - } - - let job = attestation_job(AttestationJobArgs { - rlmtx: rpc_cfg.clone(), - batch_no, - batch_count, - group_name: batch.group_name.clone(), - p2w_addr, - config: config.clone(), - payer: Keypair::from_bytes(&payer.to_bytes()).unwrap(), // Keypair has no clone - symbols: batch.symbols.to_vec(), - max_jobs_sema: sema.clone(), - message_q_mtx: message_q_mtx.clone(), - rate_limit_interval_secs: batch.conditions.rate_limit_interval_secs, - }); - - // This short-lived permit prevents scheduling excess - // attestation jobs hanging on the max jobs semaphore (which could - // eventually eat all memory). It is freed as soon as we leave - // this code block. - let _permit4sched = sema.acquire().await?; - - // Spawn the job in background - let _detached_job: JoinHandle<_> = tokio::spawn(job); - - batch.last_job_finished_at = Instant::now(); - } -} - -/// Arguments for attestation_job(). This struct rules out same-type -/// ordering errors due to the large argument count -pub struct AttestationJobArgs { - pub rlmtx: Arc>, - pub batch_no: usize, - pub batch_count: usize, - pub group_name: String, - pub p2w_addr: Pubkey, - pub config: Pyth2WormholeConfig, - pub payer: Keypair, - pub symbols: Vec, - pub max_jobs_sema: Arc, - pub rate_limit_interval_secs: u32, - pub message_q_mtx: Arc>, -} - -/// A future for a single attempt to attest a batch on Solana. -async fn attestation_job(args: AttestationJobArgs) -> Result<(), ErrBoxSend> { - let AttestationJobArgs { - rlmtx, - batch_no, - batch_count, - group_name, - p2w_addr, - config, - payer, - symbols, - max_jobs_sema, - rate_limit_interval_secs, - message_q_mtx, - } = args; - let batch_no4err_msg = batch_no; - let batch_count4err_msg = batch_count; - let group_name4err_msg = group_name.clone(); - - // The following async block is just wrapping the job in a log - // statement and err counter increase in case the job fails. It is - // done by using the or_else() future method. No other actions are - // performed and the error is propagated up the stack. - // - // This is necessary to learn about errors in jobs started with - // tokio::spawn() because in this package spawned futures are - // never explicitly awaited on. - // - // Previously, the or_else() existed in attestation_sched_job() - // which schedules this future. It was moved here for readability, - // after introduction of Prometheus metrics and the healthcheck, - // which helped keep metrics updates closer together. - let job_with_err_msg = (async move { - // Will be dropped after attestation is complete - let _permit = max_jobs_sema.acquire().await?; - - debug!( - "Batch {}/{}, group {:?}: Starting attestation job", - batch_no, batch_count, group_name - ); - let rpc = lock_and_make_rpc(&rlmtx).await; // Reuse the same lock for the blockhash/tx/get_transaction - let latest_blockhash = rpc - .get_latest_blockhash() - .map_err(|e| -> ErrBoxSend { e.into() }) - .await?; - - let wh_msg_id = message_q_mtx.lock().await.get_account()?.id; - - let tx = gen_attest_tx( - p2w_addr, - &config, - &payer, - wh_msg_id, - symbols.as_slice(), - latest_blockhash, - rate_limit_interval_secs, - )?; - - let tx_processing_start_time = Instant::now(); - - let sig = match send_and_confirm_transaction_with_config(&rpc, &tx, RpcSendTransactionConfig { - // Decreases probability of rate limit race conditions - skip_preflight: true, - ..Default::default() - }).await { - Ok(s) => Ok(s), - Err(e) => match e.get_transaction_error() { - Some(TransactionError::InstructionError(_idx, InstructionError::Custom(code))) - if code == AttesterCustomError::AttestRateLimitReached as u32 => - { - info!( - "Batch {}/{}, group {:?} OK: configured {} second rate limit interval reached, backing off", - batch_no, batch_count, group_name, rate_limit_interval_secs, - ); - // Note: We return early if rate limit tx - // error is detected. This ensures that we - // don't count this attempt in ok/err - // monitoring and healthcheck counters. - return Ok(()); - } - _other => Err(e), - }, - }?; - let tx_data = rpc - .get_transaction_with_config( - &sig, - RpcTransactionConfig { - encoding: Some(UiTransactionEncoding::Json), - commitment: Some(rpc.commitment()), - max_supported_transaction_version: None, - }, - ) - .await?; - - let tx_processing_duration = tx_processing_start_time.elapsed(); - - // Manually insert the value into histogram. NOTE: We're not - // using the start_timer() method because it would record - // durations even for early returns in error conditions which - // would look weird in monitoring. - SOL_RPC_TX_PROCESSING_HIST.observe(tx_processing_duration.as_secs_f64()); - - let seqno = tx_data - .transaction - .meta - .and_then(|meta| meta.log_messages) - .and_then(|logs| { - let mut seqno = None; - for log in logs { - if log.starts_with(SEQNO_PREFIX) { - seqno = Some(log.replace(SEQNO_PREFIX, "")); - break; - } - } - seqno - }) - .ok_or_else(|| -> ErrBoxSend { "No seqno in program logs".to_string().into() })?; - - info!( - "Batch {}/{}, group {:?} OK. Sequence: {}", - batch_no, batch_count, group_name, seqno - ); - ATTESTATIONS_OK_CNT.inc(); - LAST_SEQNO_GAUGE.set(seqno.parse::()?); - - HEALTHCHECK_STATE.lock().await.add_result(true); // Report this job as successful to healthcheck - Result::<(), ErrBoxSend>::Ok(()) - }) - .or_else(move |e| async move { - // log any errors coming from the job - warn!( - "Batch {}/{}, group {:?} ERR: {:?}", - batch_no4err_msg, batch_count4err_msg, group_name4err_msg, e - ); - - // Bump counters - ATTESTATIONS_ERR_CNT.inc(); - - HEALTHCHECK_STATE.lock().await.add_result(false); // Report this job as failed to healthcheck - - Err(e) - }); - - job_with_err_msg.await -} - -fn init_logging() { - if std::env::var("RUST_LOG").is_ok() { - env_logger::init() - } else { - // Default to info if RUST_LOG not set - env_logger::builder().filter_level(LevelFilter::Info).init(); - } -} diff --git a/wormhole_attester/client/src/message.rs b/wormhole_attester/client/src/message.rs deleted file mode 100644 index bf606a9d..00000000 --- a/wormhole_attester/client/src/message.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Re-usable message scheme for pyth2wormhole - -use { - crate::ErrBoxSend, - log::debug, - std::{ - collections::VecDeque, - time::{ - Duration, - Instant, - }, - }, -}; - -/// One of the accounts tracked by the attestation client. -#[derive(Clone, Debug)] -pub struct P2WMessageAccount { - /// Unique ID that lets us derive unique accounts for use on-chain - pub id: u64, - /// Last time we've posted a message to wormhole with this account - pub last_used: Instant, -} - -/// An umbrella data structure for tracking all message accounts in use -#[derive(Clone, Debug)] -pub struct P2WMessageQueue { - /// The tracked accounts. Sorted from oldest to newest, as guaranteed by get_account() - accounts: VecDeque, - /// How much time needs to pass between reuses - grace_period: Duration, - /// A hard cap on how many accounts will be created. - max_accounts: usize, -} - -impl P2WMessageQueue { - pub fn new(grace_period: Duration, max_accounts: usize) -> Self { - Self { - accounts: VecDeque::new(), - grace_period, - max_accounts, - } - } - /// Finds or creates an account with last_used at least grace_period in the past. - /// - /// This method governs the self.accounts queue and preserves its sorted state. - pub fn get_account(&mut self) -> Result { - // Pick or add an account to use as message - let acc = match self.accounts.pop_front() { - // Exists and is old enough for reuse - Some(mut existing_acc) if existing_acc.last_used.elapsed() > self.grace_period => { - existing_acc.last_used = Instant::now(); - existing_acc - } - // Exists but isn't old enough for reuse - Some(existing_too_new_acc) => { - // Counter-act the pop, this account is still oldest - // and will be old enough eventually. - self.accounts.push_front(existing_too_new_acc); - - // Make sure we're not going over the limit - if self.accounts.len() >= self.max_accounts { - return Err(format!( - "Max message queue size of {} reached.", - self.max_accounts - ) - .into()); - } - - debug!( - "Increasing message queue size to {}", - self.accounts.len() + 1 - ); - - // Use a new account instead - P2WMessageAccount { - id: self.accounts.len() as u64, - last_used: Instant::now(), - } - } - // Base case: Queue is empty, use a new account - None => P2WMessageAccount { - id: self.accounts.len() as u64, - last_used: Instant::now(), - }, - }; - // The chosen account becomes the newest, push it to the very end. - self.accounts.push_back(acc.clone()); - Ok(acc) - } -} - -#[cfg(test)] -pub mod test { - use super::*; - - #[test] - fn test_empty_grows_only_as_needed() -> Result<(), ErrBoxSend> { - let mut q = P2WMessageQueue::new(Duration::from_millis(500), 100_000); - - // Empty -> 1 account - let acc = q.get_account()?; - - assert_eq!(q.accounts.len(), 1); - assert_eq!(acc.id, 0); - - // 1 -> 2 accounts, not enough time passes - let acc2 = q.get_account()?; - - assert_eq!(q.accounts.len(), 2); - assert_eq!(acc2.id, 1); - - std::thread::sleep(Duration::from_millis(600)); - - // Account 0 should be in front, enough time passed - let acc3 = q.get_account()?; - - assert_eq!(q.accounts.len(), 2); - assert_eq!(acc3.id, 0); - - // Account 1 also qualifies - let acc4 = q.get_account()?; - - assert_eq!(q.accounts.len(), 2); - assert_eq!(acc4.id, 1); - - // 2 -> 3 accounts, not enough time passes - let acc5 = q.get_account()?; - - assert_eq!(q.accounts.len(), 3); - assert_eq!(acc5.id, 2); - - // We should end up with 0, 1 and 2 in order - assert_eq!(q.accounts[0].id, 0); - assert_eq!(q.accounts[1].id, 1); - assert_eq!(q.accounts[2].id, 2); - - Ok(()) - } -} diff --git a/wormhole_attester/client/src/util.rs b/wormhole_attester/client/src/util.rs deleted file mode 100644 index 3be7909b..00000000 --- a/wormhole_attester/client/src/util.rs +++ /dev/null @@ -1,266 +0,0 @@ -use { - crate::HEALTHCHECK_STATE, - http::status::StatusCode, - log::{ - error, - trace, - }, - prometheus::TextEncoder, - solana_client::{ - client_error::Result as SolClientResult, - nonblocking::rpc_client::RpcClient, - rpc_config::RpcSendTransactionConfig, - rpc_request::RpcError, - }, - solana_sdk::{ - commitment_config::CommitmentConfig, - signature::Signature, - transaction::{ - uses_durable_nonce, - Transaction, - }, - }, - std::{ - net::SocketAddr, - ops::{ - Deref, - DerefMut, - }, - time::{ - Duration, - Instant, - }, - }, - tokio::{ - sync::{ - Mutex, - MutexGuard, - }, - time::sleep, - }, - warp::{ - reply, - Filter, - Rejection, - Reply, - }, -}; - -/// Rate-limited mutex. Ensures there's a period of minimum rl_interval between lock acquisitions -pub struct RLMutex { - mtx: Mutex>, - rl_interval: Duration, -} - -/// Helper to make the last_released writes also guarded by the mutex -pub struct RLMutexState { - /// Helps make sure regular passage of time is subtracted from sleep duration - last_released: Instant, - val: T, -} - -impl Deref for RLMutexState { - type Target = T; - fn deref(&self) -> &Self::Target { - &self.val - } -} - -impl DerefMut for RLMutexState { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.val - } -} - -/// Helper wrapper to record lock release times via Drop -pub struct RLMutexGuard<'a, T> { - guard: MutexGuard<'a, RLMutexState>, -} - -impl<'a, T> Drop for RLMutexGuard<'a, T> { - fn drop(&mut self) { - let state: &mut RLMutexState = - MutexGuard::<'a, RLMutexState>::deref_mut(&mut self.guard); - state.last_released = Instant::now(); - } -} - -impl<'a, T> Deref for RLMutexGuard<'a, T> { - type Target = T; - fn deref(&self) -> &Self::Target { - self.guard.deref() - } -} - -impl<'a, T> DerefMut for RLMutexGuard<'a, T> { - fn deref_mut(&mut self) -> &mut T { - self.guard.deref_mut() - } -} - -impl RLMutex { - pub fn new(val: T, rl_interval: Duration) -> Self { - Self { - mtx: Mutex::new(RLMutexState { - last_released: Instant::now().checked_sub(rl_interval).unwrap(), - val, - }), - rl_interval, - } - } - - pub async fn lock(&self) -> RLMutexGuard<'_, T> { - let guard = self.mtx.lock().await; - let elapsed = guard.last_released.elapsed(); - if elapsed < self.rl_interval { - let sleep_time = self.rl_interval - elapsed; - trace!( - "RLMutex: Parking lock future for {}.{}s", - sleep_time.as_secs(), - sleep_time.subsec_millis() - ); - - tokio::time::sleep(sleep_time).await; - } - - RLMutexGuard { guard } - } -} - -async fn metrics_handler() -> Result { - let encoder = TextEncoder::new(); - match encoder.encode_to_string(&prometheus::gather()) { - Ok(encoded_metrics) => Ok(reply::with_status(encoded_metrics, StatusCode::OK)), - Err(e) => { - error!("Could not serve metrics: {}", e.to_string()); - Ok(reply::with_status( - "".to_string(), - StatusCode::INTERNAL_SERVER_ERROR, - )) - } - } -} - -/// Shares healthcheck result via HTTP status codes. The idea is to -/// get a yes/no health answer using a plain HTTP request. Note: Curl -/// does not treat 3xx statuses as errors by default. -async fn healthcheck_handler() -> Result { - let hc_state = HEALTHCHECK_STATE.lock().await; - match hc_state.is_healthy() { - // Healthy - 200 OK - Some(true) => { - let ok_count = hc_state - .window - .iter() - .fold(0usize, |acc, val| if *val { acc + 1 } else { acc }); - let msg = format!( - "healthy, {} of {} last attestations OK", - ok_count, hc_state.max_window_size - ); - Ok(reply::with_status(msg, StatusCode::OK)) - } - // Unhealthy - 500 Internal Server Error - Some(false) => { - let msg = format!( - "unhealthy, all of {} latest attestations returned error", - hc_state.max_window_size - ); - Ok(reply::with_status(msg, StatusCode::INTERNAL_SERVER_ERROR)) - } - // No data - 503 Service Unavailable - None => { - let msg = if hc_state.enable { - format!( - "Not enough data in window, {} of {} min attempts made", - hc_state.window.len(), - hc_state.max_window_size - ) - } else { - "Healthcheck disabled (enable_healthcheck is false)".to_string() - }; - Ok(reply::with_status(msg, StatusCode::SERVICE_UNAVAILABLE)) - } - } -} - -/// Serves Prometheus metrics and the result of the healthcheck -pub async fn start_metrics_server(addr: impl Into + 'static) { - let metrics_route = warp::path("metrics") // The Prometheus metrics subpage is standardized to always be /metrics - .and(warp::path::end()) - .and_then(metrics_handler); - let healthcheck_route = warp::path("healthcheck") - .and(warp::path::end()) - .and_then(healthcheck_handler); - - warp::serve(metrics_route.or(healthcheck_route)) - .bind(addr) - .await; -} - -/// WARNING: Copied verbatim from v1.10.31, be careful when bumping -/// solana crate versions! -/// -/// TODO(2023-03-02): Use an upstream method when -/// it's available. -/// -/// This method is almost identical to -/// RpcClient::send_and_confirm_transaction(). The only difference is -/// that we let the user specify the config and replace -/// send_transaction() inside with -/// send_transaction_with_config(). This variant is currently missing -/// from solana_client. -pub async fn send_and_confirm_transaction_with_config( - client: &RpcClient, - transaction: &Transaction, - config: RpcSendTransactionConfig, -) -> SolClientResult { - const SEND_RETRIES: usize = 1; - const GET_STATUS_RETRIES: usize = usize::MAX; - - 'sending: for _ in 0..SEND_RETRIES { - let signature = client - .send_transaction_with_config(transaction, config) - .await?; - - let recent_blockhash = if uses_durable_nonce(transaction).is_some() { - let (recent_blockhash, ..) = client - .get_latest_blockhash_with_commitment(CommitmentConfig::processed()) - .await?; - recent_blockhash - } else { - transaction.message.recent_blockhash - }; - - for status_retry in 0..GET_STATUS_RETRIES { - match client.get_signature_status(&signature).await? { - Some(Ok(_)) => return Ok(signature), - Some(Err(e)) => return Err(e.into()), - None => { - if !client - .is_blockhash_valid(&recent_blockhash, CommitmentConfig::processed()) - .await? - { - // Block hash is not found by some reason - break 'sending; - } else if cfg!(not(test)) - // Ignore sleep at last step. - && status_retry < GET_STATUS_RETRIES - { - // Retry twice a second - sleep(Duration::from_millis(500)).await; - - continue; - } - } - } - } - } - - Err(RpcError::ForUser( - "unable to confirm transaction. \ - This can happen in situations such as transaction expiration \ - and insufficient fee-payer funds" - .to_string(), - ) - .into()) -} diff --git a/wormhole_attester/client/tests/fixtures/mod.rs b/wormhole_attester/client/tests/fixtures/mod.rs deleted file mode 100644 index 593d2717..00000000 --- a/wormhole_attester/client/tests/fixtures/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod passthrough; -pub mod pyth; diff --git a/wormhole_attester/client/tests/fixtures/passthrough.rs b/wormhole_attester/client/tests/fixtures/passthrough.rs deleted file mode 100644 index 072bac35..00000000 --- a/wormhole_attester/client/tests/fixtures/passthrough.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Trivial program for mocking other programs easily -use { - solana_program::{ - account_info::AccountInfo, - msg, - program_error::ProgramError, - }, - solana_program_test::*, - solana_sdk::pubkey::Pubkey, -}; - -pub fn passthrough_entrypoint( - program_id: &Pubkey, - account_infos: &[AccountInfo], - _data: &[u8], -) -> Result<(), ProgramError> { - msg!(&format!("Program {program_id}")); - msg!(&format!("account_infos {account_infos:?}")); - Ok(()) -} - -pub fn add_passthrough(pt: &mut ProgramTest, name: &str, prog_id: Pubkey) { - pt.add_program(name, prog_id, processor!(passthrough_entrypoint)) -} diff --git a/wormhole_attester/client/tests/fixtures/pyth.rs b/wormhole_attester/client/tests/fixtures/pyth.rs deleted file mode 100644 index ecf96184..00000000 --- a/wormhole_attester/client/tests/fixtures/pyth.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! This module contains test fixtures for instantiating plausible -//! Pyth accounts for testing purposes. -use { - pyth_sdk_solana::state::{ - AccountType, - PriceAccount, - ProductAccount, - MAGIC, - PROD_ATTR_SIZE, - VERSION, - }, - solana_program_test::*, - solana_sdk::{ - account::Account, - pubkey::Pubkey, - rent::Rent, - }, -}; - -/// Create a pair of brand new product/price accounts that point at each other -pub fn add_test_symbol(pt: &mut ProgramTest, owner: &Pubkey) -> (Pubkey, Pubkey) { - // Generate pubkeys - let prod_id = Pubkey::new_unique(); - let price_id = Pubkey::new_unique(); - - // Instantiate - let prod = { - ProductAccount { - magic: MAGIC, - ver: VERSION, - atype: AccountType::Product as u32, - size: 0, - px_acc: price_id, - attr: [0u8; PROD_ATTR_SIZE], - } - }; - - let price = PriceAccount { - magic: MAGIC, - ver: VERSION, - atype: AccountType::Price as u32, - prod: prod_id, - ..Default::default() - }; - - // Cast to raw bytes - let prod_buf = &[prod]; - let prod_bytes = unsafe { - let (_prefix, bytes, _suffix) = prod_buf.align_to::(); - bytes - }; - let price_buf = &[price]; - let price_bytes = unsafe { - let (_prefix, bytes, _suffix) = price_buf.align_to::(); - bytes - }; - - // Compute exemption rent - let prod_lamports = Rent::default().minimum_balance(prod_bytes.len()); - let price_lamports = Rent::default().minimum_balance(price_bytes.len()); - - // Populate the accounts - let prod_acc = Account { - lamports: prod_lamports, - data: (*prod_bytes).to_vec(), - owner: *owner, - rent_epoch: 0, - executable: false, - }; - let price_acc = Account { - lamports: price_lamports, - data: (*price_bytes).to_vec(), - owner: *owner, - rent_epoch: 0, - executable: false, - }; - - pt.add_account(prod_id, prod_acc); - pt.add_account(price_id, price_acc); - - (prod_id, price_id) -} diff --git a/wormhole_attester/client/tests/test_attest.rs b/wormhole_attester/client/tests/test_attest.rs deleted file mode 100644 index 3c935ae1..00000000 --- a/wormhole_attester/client/tests/test_attest.rs +++ /dev/null @@ -1,128 +0,0 @@ -pub mod fixtures; - -use { - bridge::accounts::{ - Bridge, - BridgeConfig, - BridgeData, - }, - fixtures::{ - passthrough, - pyth, - }, - pyth_wormhole_attester::config::{ - P2WConfigAccount, - Pyth2WormholeConfig, - }, - pyth_wormhole_attester_client as p2wc, - solana_program_test::*, - solana_sdk::{ - account::Account, - pubkey::Pubkey, - rent::Rent, - }, - solitaire::{ - processors::seeded::Seeded, - AccountState, - BorshSerialize, - }, -}; - -#[tokio::test] -async fn test_happy_path() -> Result<(), p2wc::ErrBoxSend> { - // Programs - let p2w_program_id = Pubkey::new_unique(); - let wh_fixture_program_id = Pubkey::new_unique(); - - // Authorities - let p2w_owner = Pubkey::new_unique(); - let pyth_owner = Pubkey::new_unique(); - let ops_owner = Pubkey::new_unique(); - - // On-chain state - let p2w_config = Pyth2WormholeConfig { - owner: p2w_owner, - wh_prog: wh_fixture_program_id, - max_batch_size: pyth_wormhole_attester::attest::P2W_MAX_BATCH_SIZE, - pyth_owner, - is_active: true, - ops_owner: Some(ops_owner), - }; - - let bridge_config = BridgeData { - config: BridgeConfig { - fee: 0xdeadbeef, - ..Default::default() - }, - ..Default::default() - }; - - // Populate test environment - let mut p2w_test = ProgramTest::new( - "pyth_wormhole_attester", - p2w_program_id, - processor!(pyth_wormhole_attester::instruction::solitaire), - ); - - // Plant a filled config account - let p2w_config_bytes = p2w_config.try_to_vec()?; - let p2w_config_account = Account { - lamports: Rent::default().minimum_balance(p2w_config_bytes.len()), - data: p2w_config_bytes, - owner: p2w_program_id, - executable: false, - rent_epoch: 0, - }; - let p2w_config_addr = - P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_program_id); - - p2w_test.add_account(p2w_config_addr, p2w_config_account); - - // Plant a bridge config - let bridge_config_bytes = bridge_config.try_to_vec()?; - let wh_bridge_config_account = Account { - lamports: Rent::default().minimum_balance(bridge_config_bytes.len()), - data: bridge_config_bytes, - owner: wh_fixture_program_id, - executable: false, - rent_epoch: 0, - }; - - let wh_bridge_config_addr = - Bridge::<{ AccountState::Initialized }>::key(None, &wh_fixture_program_id); - - p2w_test.add_account(wh_bridge_config_addr, wh_bridge_config_account); - - passthrough::add_passthrough(&mut p2w_test, "wormhole", wh_fixture_program_id); - let (prod_id, price_id) = pyth::add_test_symbol(&mut p2w_test, &pyth_owner); - - let ctx = p2w_test.start_with_context().await; - - let symbols = vec![p2wc::P2WSymbol { - name: Some("Mock symbol".to_owned()), - product_addr: prod_id, - price_addr: price_id, - }]; - - let _attest_tx = p2wc::gen_attest_tx( - p2w_program_id, - &p2w_config, - &ctx.payer, - 0, - symbols.as_slice(), - ctx.last_blockhash, - 0, - )?; - - // NOTE: 2022-09-05 - // Execution of this transaction is commented out as for some unknown reasons - // Solana test suite has some unknown behavior in this transaction. It is probably a - // memory leak that causes either segfault or an invalid error (after a reading an unkown - // variable from memory). It is probably solved in the following PR: - // https://github.com/solana-labs/solana/pull/26507 - // - // TODO: add this check when the above PR is released in our Solana package. - // ctx.banks_client.process_transaction(attest_tx).await?; - - Ok(()) -} diff --git a/wormhole_attester/client/tests/test_migrate.rs b/wormhole_attester/client/tests/test_migrate.rs deleted file mode 100644 index cf901612..00000000 --- a/wormhole_attester/client/tests/test_migrate.rs +++ /dev/null @@ -1,177 +0,0 @@ -//! Checks for migrating the previous config schema into the current one - -pub mod fixtures; - -use { - fixtures::passthrough, - log::info, - pyth_wormhole_attester::config::{ - OldP2WConfigAccount, - OldPyth2WormholeConfig, - P2WConfigAccount, - Pyth2WormholeConfig, - }, - pyth_wormhole_attester_client as p2wc, - serial_test::serial, - solana_program::system_program, - solana_program_test::*, - solana_sdk::{ - account::Account, - pubkey::Pubkey, - rent::Rent, - signature::Signer, - signer::keypair::Keypair, - }, - solitaire::{ - processors::seeded::Seeded, - AccountState, - BorshSerialize, - }, -}; - -#[tokio::test] -#[serial] -async fn test_migrate_works() -> Result<(), solitaire::ErrBox> { - info!("Starting"); - // Programs - let p2w_program_id = Pubkey::new_unique(); - let wh_fixture_program_id = Pubkey::new_unique(); - - // Authorities - let p2w_owner = Keypair::new(); - let pyth_owner = Pubkey::new_unique(); - - // On-chain state - let old_p2w_config = OldPyth2WormholeConfig { - owner: p2w_owner.pubkey(), - wh_prog: wh_fixture_program_id, - max_batch_size: pyth_wormhole_attester::attest::P2W_MAX_BATCH_SIZE, - pyth_owner, - is_active: true, - }; - - info!("Before ProgramTest::new()"); - - // Populate test environment - let mut p2w_test = ProgramTest::new( - "pyth_wormhole_attester", - p2w_program_id, - processor!(pyth_wormhole_attester::instruction::solitaire), - ); - - // Plant filled config accounts - let old_p2w_config_bytes = old_p2w_config.try_to_vec()?; - let old_p2w_config_account = Account { - lamports: Rent::default().minimum_balance(old_p2w_config_bytes.len()), - data: old_p2w_config_bytes, - owner: p2w_program_id, - executable: false, - rent_epoch: 0, - }; - let old_p2w_config_addr = OldP2WConfigAccount::key(None, &p2w_program_id); - - info!("Before add_account() calls"); - - p2w_test.add_account(old_p2w_config_addr, old_p2w_config_account); - - // Add system program because the contract creates an account for new configuration account - passthrough::add_passthrough(&mut p2w_test, "system", system_program::id()); - - info!("System program under {}", system_program::id()); - - info!("Before start_with_context"); - let mut ctx = p2w_test.start_with_context().await; - - let migrate_tx = - p2wc::gen_migrate_tx(ctx.payer, p2w_program_id, p2w_owner, ctx.last_blockhash)?; - info!("Before process_transaction"); - - // Migration should fail because the new config account is already initialized - ctx.banks_client.process_transaction(migrate_tx).await?; - - Ok(()) -} - -#[tokio::test] -#[serial] -async fn test_migrate_already_migrated() -> Result<(), solitaire::ErrBox> { - info!("Starting"); - // Programs - let p2w_program_id = Pubkey::new_unique(); - let wh_fixture_program_id = Pubkey::new_unique(); - - // Authorities - let p2w_owner = Keypair::new(); - let pyth_owner = Pubkey::new_unique(); - let ops_owner = Keypair::new(); - - // On-chain state - let old_p2w_config = OldPyth2WormholeConfig { - owner: p2w_owner.pubkey(), - wh_prog: wh_fixture_program_id, - max_batch_size: pyth_wormhole_attester::attest::P2W_MAX_BATCH_SIZE, - pyth_owner, - is_active: true, - }; - - let new_p2w_config = Pyth2WormholeConfig { - owner: p2w_owner.pubkey(), - wh_prog: wh_fixture_program_id, - max_batch_size: pyth_wormhole_attester::attest::P2W_MAX_BATCH_SIZE, - pyth_owner, - is_active: true, - ops_owner: Some(ops_owner.pubkey()), - }; - - info!("Before ProgramTest::new()"); - - // Populate test environment - let mut p2w_test = ProgramTest::new( - "pyth_wormhole_attester", - p2w_program_id, - processor!(pyth_wormhole_attester::instruction::solitaire), - ); - - // Plant filled config accounts - let old_p2w_config_bytes = old_p2w_config.try_to_vec()?; - let old_p2w_config_account = Account { - lamports: Rent::default().minimum_balance(old_p2w_config_bytes.len()), - data: old_p2w_config_bytes, - owner: p2w_program_id, - executable: false, - rent_epoch: 0, - }; - let old_p2w_config_addr = OldP2WConfigAccount::key(None, &p2w_program_id); - - let new_p2w_config_bytes = new_p2w_config.try_to_vec()?; - let new_p2w_config_account = Account { - lamports: Rent::default().minimum_balance(new_p2w_config_bytes.len()), - data: new_p2w_config_bytes, - owner: p2w_program_id, - executable: false, - rent_epoch: 0, - }; - let new_p2w_config_addr = - P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_program_id); - - info!("Before add_account() calls"); - - p2w_test.add_account(old_p2w_config_addr, old_p2w_config_account); - p2w_test.add_account(new_p2w_config_addr, new_p2w_config_account); - - info!("Before start_with_context"); - let mut ctx = p2w_test.start_with_context().await; - - let migrate_tx = - p2wc::gen_migrate_tx(ctx.payer, p2w_program_id, p2w_owner, ctx.last_blockhash)?; - info!("Before process_transaction"); - - // Migration should fail because the new config account is already initialized - assert!(ctx - .banks_client - .process_transaction(migrate_tx) - .await - .is_err()); - - Ok(()) -} diff --git a/wormhole_attester/client/tests/test_set_is_active.rs b/wormhole_attester/client/tests/test_set_is_active.rs deleted file mode 100644 index 78a14a37..00000000 --- a/wormhole_attester/client/tests/test_set_is_active.rs +++ /dev/null @@ -1,196 +0,0 @@ -pub mod fixtures; - -use { - pyth_wormhole_attester::config::{ - P2WConfigAccount, - Pyth2WormholeConfig, - }, - pyth_wormhole_attester_client as p2wc, - solana_program_test::*, - solana_sdk::{ - account::Account, - pubkey::Pubkey, - rent::Rent, - signature::Signer, - signer::keypair::Keypair, - }, - solitaire::{ - processors::seeded::Seeded, - AccountState, - BorshSerialize, - }, -}; - -fn clone_keypair(keypair: &Keypair) -> Keypair { - // Unwrap as we are surely copying a keypair and we are in test env. - Keypair::from_bytes(keypair.to_bytes().as_ref()).unwrap() -} - -#[tokio::test] -async fn test_setting_is_active_works() -> Result<(), p2wc::ErrBoxSend> { - // Programs - let p2w_program_id = Pubkey::new_unique(); - let wh_fixture_program_id = Pubkey::new_unique(); - - // Authorities - let p2w_owner = Pubkey::new_unique(); - let pyth_owner = Pubkey::new_unique(); - let ops_owner = Keypair::new(); - - // On-chain state - let p2w_config = Pyth2WormholeConfig { - owner: p2w_owner, - wh_prog: wh_fixture_program_id, - max_batch_size: pyth_wormhole_attester::attest::P2W_MAX_BATCH_SIZE, - pyth_owner, - is_active: true, - ops_owner: Some(ops_owner.pubkey()), - }; - - // Populate test environment - let mut p2w_test = ProgramTest::new( - "pyth_wormhole_attester", - p2w_program_id, - processor!(pyth_wormhole_attester::instruction::solitaire), - ); - - // Plant a filled config account - let p2w_config_bytes = p2w_config.try_to_vec()?; - let p2w_config_account = Account { - lamports: Rent::default().minimum_balance(p2w_config_bytes.len()), - data: p2w_config_bytes, - owner: p2w_program_id, - executable: false, - rent_epoch: 0, - }; - let p2w_config_addr = - P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_program_id); - - p2w_test.add_account(p2w_config_addr, p2w_config_account); - - let mut ctx = p2w_test.start_with_context().await; - - // Setting to false should work - let set_is_active_false_tx = p2wc::gen_set_is_active_tx( - clone_keypair(&ctx.payer), - p2w_program_id, - clone_keypair(&ops_owner), - false, - ctx.last_blockhash, - ) - .map_err(|e| e.to_string())?; - - ctx.banks_client - .process_transaction(set_is_active_false_tx) - .await?; - - let config = ctx - .banks_client - .get_account_data_with_borsh::(p2w_config_addr) - .await?; - - assert!(!config.is_active); - - // Setting to true should work - let set_is_active_true_tx = p2wc::gen_set_is_active_tx( - clone_keypair(&ctx.payer), - p2w_program_id, - clone_keypair(&ops_owner), - true, - ctx.last_blockhash, - ) - .map_err(|e| e.to_string())?; - - ctx.banks_client - .process_transaction(set_is_active_true_tx) - .await?; - - let config = ctx - .banks_client - .get_account_data_with_borsh::(p2w_config_addr) - .await?; - - assert!(config.is_active); - - // A wrong signer cannot handle it - - let set_is_active_true_tx = p2wc::gen_set_is_active_tx( - clone_keypair(&ctx.payer), - p2w_program_id, - clone_keypair(&ctx.payer), - true, - ctx.last_blockhash, - ) - .map_err(|e| e.to_string())?; - - assert!(ctx - .banks_client - .process_transaction(set_is_active_true_tx) - .await - .is_err()); - - Ok(()) -} - -#[tokio::test] -async fn test_setting_is_active_does_not_work_without_ops_owner() -> Result<(), p2wc::ErrBoxSend> { - // Programs - let p2w_program_id = Pubkey::new_unique(); - let wh_fixture_program_id = Pubkey::new_unique(); - - // Authorities - let p2w_owner = Pubkey::new_unique(); - let pyth_owner = Keypair::new(); - - // On-chain state - let p2w_config = Pyth2WormholeConfig { - owner: p2w_owner, - wh_prog: wh_fixture_program_id, - max_batch_size: pyth_wormhole_attester::attest::P2W_MAX_BATCH_SIZE, - pyth_owner: pyth_owner.pubkey(), - is_active: true, - ops_owner: None, - }; - - // Populate test environment - let mut p2w_test = ProgramTest::new( - "pyth_wormhole_attester", - p2w_program_id, - processor!(pyth_wormhole_attester::instruction::solitaire), - ); - - // Plant a filled config account - let p2w_config_bytes = p2w_config.try_to_vec()?; - let p2w_config_account = Account { - lamports: Rent::default().minimum_balance(p2w_config_bytes.len()), - data: p2w_config_bytes, - owner: p2w_program_id, - executable: false, - rent_epoch: 0, - }; - let p2w_config_addr = - P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_program_id); - - p2w_test.add_account(p2w_config_addr, p2w_config_account); - - let mut ctx = p2w_test.start_with_context().await; - - // No one could should be able to handle - // For example pyth_owner is used here. - let set_is_active_true_tx = p2wc::gen_set_is_active_tx( - clone_keypair(&ctx.payer), - p2w_program_id, - pyth_owner, - true, - ctx.last_blockhash, - ) - .map_err(|e| e.to_string())?; - - assert!(ctx - .banks_client - .process_transaction(set_is_active_true_tx) - .await - .is_err()); - - Ok(()) -} diff --git a/wormhole_attester/governance/Cargo.toml b/wormhole_attester/governance/Cargo.toml deleted file mode 100644 index 594d4f68..00000000 --- a/wormhole_attester/governance/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "pyth-wormhole-attester-governance" -version = "0.1.0" -description = "CLI to generate governance payloads for the attester" -edition = "2018" - -[dependencies] -anyhow = "1.0.65" -clap = {version = "3.1.18", features = ["derive"]} -solana-sdk = "=1.10.31" -pyth-wormhole-attester-client = {path = "../client/"} -hex = "0.4.3" -remote-executor = {path = "../../governance/remote_executor/programs/remote-executor/"} -borsh = "0.9.3" diff --git a/wormhole_attester/governance/src/cli.rs b/wormhole_attester/governance/src/cli.rs deleted file mode 100644 index e54025a2..00000000 --- a/wormhole_attester/governance/src/cli.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! CLI options -use { - clap::{ - Parser, - Subcommand, - }, - solana_sdk::pubkey::Pubkey, -}; - -#[derive(Parser, Debug)] -#[clap( - about = "A cli for the remote executor", - author = "Pyth Network Contributors" -)] -pub struct Cli { - #[clap(subcommand)] - pub action: Action, -} - -#[derive(Subcommand, Debug)] -pub enum Action { - #[clap(about = "Get set upgrade authority payload for squads-cli")] - GetSetConfig { - #[clap(long, help = "Program id")] - program_id: Pubkey, - #[clap(long, help = "Current owner")] - owner: Pubkey, - #[clap(long, help = "Payer")] - payer: Pubkey, - #[clap(long, help = "Config : New owner")] - new_owner: Pubkey, - #[clap(long, help = "Config : Wormhole program id")] - wormhole: Pubkey, - #[clap(long, help = "Config : Pyth program id")] - pyth_owner: Pubkey, - #[clap(long, help = "Config : Max batch size")] - max_batch_size: u16, - #[clap(long, help = "Config : Is active")] - is_active: bool, - #[clap(long, help = "Config : Ops owner")] - ops_owner: Option, - }, - #[clap(about = "Get upgrade program payload for squads-cli")] - GetSetIsActive { - #[clap(long, help = "Program id")] - program_id: Pubkey, - #[clap(long, help = "Current ops owner")] - ops_owner: Pubkey, - #[clap(long, help = "Payer")] - payer: Pubkey, - #[clap(long, help = "Config : Is active")] - is_active: bool, - }, -} diff --git a/wormhole_attester/governance/src/main.rs b/wormhole_attester/governance/src/main.rs deleted file mode 100644 index da31fab3..00000000 --- a/wormhole_attester/governance/src/main.rs +++ /dev/null @@ -1,70 +0,0 @@ -use { - anyhow::Result, - borsh::BorshSerialize, - clap::Parser, - cli::{ - Action, - Cli, - }, - pyth_wormhole_attester_client::{ - get_set_config_ix, - get_set_is_active_ix, - Pyth2WormholeConfig, - }, - remote_executor::state::governance_payload::{ - ExecutorPayload, - GovernanceHeader, - InstructionData, - }, -}; - -mod cli; - -fn main() -> Result<()> { - let cli = Cli::parse(); - match cli.action { - Action::GetSetConfig { - program_id, - owner, - payer, - new_owner, - wormhole, - pyth_owner, - max_batch_size, - is_active, - ops_owner, - } => { - let new_config = Pyth2WormholeConfig { - owner: new_owner, - wh_prog: wormhole, - pyth_owner, - max_batch_size, - is_active, - ops_owner, - }; - let ix = get_set_config_ix(&program_id, &owner, &payer, new_config).unwrap(); - let payload = ExecutorPayload { - header: GovernanceHeader::executor_governance_header(), - instructions: vec![InstructionData::from(&ix)], - } - .try_to_vec()?; - println!("Set config payload : {:?}", hex::encode(payload)); - Ok(()) - } - Action::GetSetIsActive { - program_id, - ops_owner, - payer, - is_active, - } => { - let ix = get_set_is_active_ix(&program_id, &ops_owner, &payer, is_active).unwrap(); - let payload = ExecutorPayload { - header: GovernanceHeader::executor_governance_header(), - instructions: vec![InstructionData::from(&ix)], - } - .try_to_vec()?; - println!("Set is active payload : {:?}", hex::encode(payload)); - Ok(()) - } - } -} diff --git a/wormhole_attester/program/Cargo.lock b/wormhole_attester/program/Cargo.lock deleted file mode 100644 index 255b45f7..00000000 --- a/wormhole_attester/program/Cargo.lock +++ /dev/null @@ -1,859 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "ahash" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" - -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - -[[package]] -name = "arrayref" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "blake3" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if 0.1.10", - "constant_time_eq", - "crypto-mac", - "digest 0.9.0", -] - -[[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 0.14.4", -] - -[[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.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a7111f797cc721407885a323fb071636aee57f750b1a4ddc27397eba168a74" -dependencies = [ - "borsh-derive", - "hashbrown", -] - -[[package]] -name = "borsh-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307f3740906bac2c118a8122fe22681232b244f1369273e45f1156b45c43d2dd" -dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", - "proc-macro-crate", - "proc-macro2", - "syn", -] - -[[package]] -name = "borsh-derive-internal" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2104c73179359431cc98e016998f2f23bc7a05bc53e79741bcba705f30047bc" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "borsh-schema-derive-internal" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae29eb8418fcd46f723f8691a2ac06857d31179d33d2f2d91eb13967de97c728" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "bridge" -version = "0.1.0" -dependencies = [ - "borsh", - "byteorder", - "primitive-types", - "sha3", - "solana-program", - "solitaire", -] - -[[package]] -name = "bs58" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb" - -[[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 = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "cc" -version = "1.0.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" - -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - -[[package]] -name = "cpufeatures" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" -dependencies = [ - "libc", -] - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "crypto-mac" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" -dependencies = [ - "generic-array 0.14.4", - "subtle", -] - -[[package]] -name = "curve25519-dalek" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434e1720189a637d44fe464f4df1e6eb900b4835255b14354497c78af37d9bb8" -dependencies = [ - "byteorder", - "digest 0.8.1", - "rand_core", - "subtle", - "zeroize", -] - -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array 0.14.4", -] - -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - -[[package]] -name = "env_logger" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "feature-probe" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" - -[[package]] -name = "fixed-hash" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" -dependencies = [ - "static_assertions", -] - -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - -[[package]] -name = "generic-array" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" -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 1.0.0", - "libc", - "wasi", -] - -[[package]] -name = "hashbrown" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" -dependencies = [ - "ahash", -] - -[[package]] -name = "hermit-abi" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" -dependencies = [ - "libc", -] - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "itertools" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" -dependencies = [ - "either", -] - -[[package]] -name = "keccak" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" - -[[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.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" - -[[package]] -name = "log" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if 1.0.0", -] - -[[package]] -name = "memchr" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" - -[[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", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "ppv-lite86" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" - -[[package]] -name = "primitive-types" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2415937401cb030a2a0a4d922483f945fa068f52a7dbb22ce0fe5f2b6f6adace" -dependencies = [ - "fixed-hash", - "uint", -] - -[[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.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "pyth-client" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "731e2d5b2b790fc676518b29e41dddf7f69f23c61f27ab25cc9ae5b75ee190ad" - -[[package]] -name = "pyth2wormhole" -version = "0.1.0" -dependencies = [ - "borsh", - "bridge", - "pyth-client", - "rocksalt", - "solana-program", - "solitaire", -] - -[[package]] -name = "quote" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom", - "libc", - "rand_chacha", - "rand_core", - "rand_hc", -] - -[[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", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core", -] - -[[package]] -name = "regex" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - -[[package]] -name = "rocksalt" -version = "0.1.0" -dependencies = [ - "byteorder", - "proc-macro2", - "quote", - "sha3", - "solana-program", - "syn", -] - -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver", -] - -[[package]] -name = "rustversion" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" - -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - -[[package]] -name = "serde" -version = "1.0.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "sha2" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" -dependencies = [ - "block-buffer", - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "sha3" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" -dependencies = [ - "block-buffer", - "digest 0.9.0", - "keccak", - "opaque-debug", -] - -[[package]] -name = "solana-frozen-abi" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b81e60d88b1fe0322bba6f3fe6b0d7299df2f2ededa8d95ec77b934fabb967b" -dependencies = [ - "bs58", - "bv", - "generic-array 0.14.4", - "log", - "memmap2", - "rustc_version", - "serde", - "serde_derive", - "sha2", - "solana-frozen-abi-macro", - "solana-logger", - "thiserror", -] - -[[package]] -name = "solana-frozen-abi-macro" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f617daa0187bcc4665d63fcf9454c998e9cdad6a33181f6214558d738230bfe2" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - -[[package]] -name = "solana-logger" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b3e2b14bdcbb7b41de9ef5a541ac501ba3fbd07999cbcf7ea9006b3ae28b67b" -dependencies = [ - "env_logger", - "lazy_static", - "log", -] - -[[package]] -name = "solana-program" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5d59f9d358c09db6461fae1fde6075a456685d856c004ef21af092a830e4e7" -dependencies = [ - "bincode", - "blake3", - "borsh", - "borsh-derive", - "bs58", - "bv", - "curve25519-dalek", - "hex", - "itertools", - "lazy_static", - "log", - "num-derive", - "num-traits", - "rand", - "rustc_version", - "rustversion", - "serde", - "serde_bytes", - "serde_derive", - "sha2", - "sha3", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-logger", - "solana-sdk-macro", - "thiserror", -] - -[[package]] -name = "solana-sdk-macro" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27426b2a09676929c5e49df96967bbcffff003183c11a3c3ef11d78bac4aaaa" -dependencies = [ - "bs58", - "proc-macro2", - "quote", - "rustversion", - "syn", -] - -[[package]] -name = "solitaire" -version = "0.1.0" -dependencies = [ - "borsh", - "byteorder", - "rocksalt", - "sha3", - "solana-program", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "subtle" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" - -[[package]] -name = "syn" -version = "1.0.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "termcolor" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "toml" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" -dependencies = [ - "serde", -] - -[[package]] -name = "typenum" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" - -[[package]] -name = "uint" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11fe9a9348741cf134085ad57c249508345fe16411b3d7fb4ff2da2f1d6382e" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - -[[package]] -name = "version_check" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "zeroize" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" diff --git a/wormhole_attester/program/Cargo.toml b/wormhole_attester/program/Cargo.toml deleted file mode 100644 index aaa02954..00000000 --- a/wormhole_attester/program/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "pyth-wormhole-attester" -version = "2.0.1" -description = "Pyth-over-Wormhole Solana contract" -edition = "2018" - -[lib] -crate-type = ["cdylib", "lib"] -name = "pyth_wormhole_attester" - -[features] -default = ["wormhole-bridge-solana/no-entrypoint"] -client = ["solitaire/client", "no-entrypoint"] -trace = ["solitaire/trace", "wormhole-bridge-solana/trace"] -no-entrypoint = [] - -[dependencies] -wormhole-bridge-solana = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.14.8" } -solitaire = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.14.8"} -rocksalt = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.14.8"} -solana-program = "=1.10.31" -borsh = "=0.9.3" -pyth-client = "0.2.2" -pyth-wormhole-attester-sdk = { path = "../sdk/rust", features = ["solana"] } -serde = { version = "1", optional = true} -serde_derive = { version = "1", optional = true} -serde_json = { version = "1", optional = true} -pyth-sdk-solana = { version = "0.5.0" } diff --git a/wormhole_attester/program/src/attest.rs b/wormhole_attester/program/src/attest.rs deleted file mode 100644 index 1b536295..00000000 --- a/wormhole_attester/program/src/attest.rs +++ /dev/null @@ -1,407 +0,0 @@ -use { - crate::{ - attestation_state::AttestationStatePDA, - config::P2WConfigAccount, - error::AttesterCustomError, - message::{ - P2WMessage, - P2WMessageDrvData, - }, - }, - borsh::{ - BorshDeserialize, - BorshSerialize, - }, - bridge::{ - accounts::BridgeData, - types::ConsistencyLevel, - }, - pyth_sdk_solana::state::PriceStatus, - pyth_wormhole_attester_sdk::{ - BatchPriceAttestation, - Identifier, - P2WEmitter, - PriceAttestation, - }, - solana_program::{ - clock::Clock, - program::invoke_signed, - program_error::ProgramError, - rent::Rent, - }, - solitaire::{ - trace, - AccountState, - ExecutionContext, - FromAccounts, - Info, - Keyed, - Mut, - Peel, - Result as SoliResult, - Seeded, - Signer, - SolitaireError, - Sysvar, - }, -}; - -/// Important: must be manually maintained until native Solitaire -/// variable len vector support. -/// -/// The number must reflect how many pyth state/price pairs are -/// expected in the Attest struct below. The constant itself is only -/// used in the on-chain config in order for attesters to learn the -/// correct value dynamically. -pub const P2W_MAX_BATCH_SIZE: u16 = 5; - -#[derive(FromAccounts)] -pub struct Attest<'b> { - // Payer also used for wormhole - pub payer: Mut>>, - pub system_program: Info<'b>, - pub config: P2WConfigAccount<'b, { AccountState::Initialized }>, - - // Hardcoded state/price pairs, bypassing Solitaire's variable-length limitations - // Any change to the number of accounts must include an appropriate change to P2W_MAX_BATCH_SIZE - pub pyth_state: Mut>, - pub pyth_price: Info<'b>, - - pub pyth_state2: Option>>, - pub pyth_price2: Option>, - - pub pyth_state3: Option>>, - pub pyth_price3: Option>, - - pub pyth_state4: Option>>, - pub pyth_price4: Option>, - - pub pyth_state5: Option>>, - pub pyth_price5: Option>, - - // Did you read the comment near `pyth_state`? - // pub pyth_state6: Option>>, - // pub pyth_price6: Option>, - - // pub pyth_state7: Option>>, - // pub pyth_price7: Option>, - - // pub pyth_state8: Option>>, - // pub pyth_price8: Option>, - - // pub pyth_state9: Option>>, - // pub pyth_price9: Option>, - - // pub pyth_state10: Option>>, - // pub pyth_price10: Option>, - pub clock: Sysvar<'b, Clock>, - - /// Wormhole program address - must match the config value - pub wh_prog: Info<'b>, - - // wormhole's post_message_unreliable accounts - // - // This contract makes no attempt to exhaustively validate - // Wormhole's account inputs. Only the wormhole contract address - // is validated (see above). - /// Bridge config needed for fee calculation - pub wh_bridge: Mut>, - - /// Account to store the posted message. - /// This account is a PDA from the attestation contract - /// which is owned by the wormhole core contract. - pub wh_message: Mut>, - - /// Emitter of the VAA - pub wh_emitter: P2WEmitter<'b>, - - /// Tracker for the emitter sequence - pub wh_sequence: Mut>, - - // We reuse our payer - // pub wh_payer: Mut>>, - /// Account to collect tx fee - pub wh_fee_collector: Mut>, - - pub wh_rent: Sysvar<'b, Rent>, -} - -#[derive(BorshDeserialize, BorshSerialize)] -pub struct AttestData { - pub consistency_level: ConsistencyLevel, - pub message_account_id: u64, - /// Fail the transaction if the global attestation rate of all - /// symbols in this batch is more frequent than the passed - /// interval. This is checked using the attestation time stored in - /// attestation state. This enables all of the clients to only - /// contribute attestations if their desired interval is not - /// already reached. If at least one symbol has been waiting - /// longer than this interval, we attest the whole batch. 0 - /// effectively disables this feature. - pub rate_limit_interval_secs: u32, -} - -pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> SoliResult<()> { - if !accs.config.is_active { - // msg instead of trace makes sure we're not silent about this in prod - solana_program::msg!("This attester program is disabled!"); - - return Err(SolitaireError::Custom(4242)); - } - - accs.config.verify_derivation(ctx.program_id, None)?; - - if accs.config.wh_prog != *accs.wh_prog.key { - trace!(&format!( - "Wormhole program account mismatch (expected {:?}, got {:?})", - accs.config.wh_prog, accs.wh_prog.key - )); - return Err(ProgramError::InvalidAccountData.into()); - } - - - // Make the specified prices iterable - let mut price_pair_opts = [ - (Some(&mut accs.pyth_state), Some(&accs.pyth_price)), - (accs.pyth_state2.as_mut(), accs.pyth_price2.as_ref()), - (accs.pyth_state3.as_mut(), accs.pyth_price3.as_ref()), - (accs.pyth_state4.as_mut(), accs.pyth_price4.as_ref()), - (accs.pyth_state5.as_mut(), accs.pyth_price5.as_ref()), - // Did you read the comment near `pyth_state`? - // (accs.pyth_state6.as_mut(), accs.pyth_price6.as_ref()), - // (accs.pyth_state7.as_mut(), accs.pyth_price7.as_ref()), - // (accs.pyth_state8.as_mut(), accs.pyth_price8.as_ref()), - // (accs.pyth_state9.as_mut(), accs.pyth_price9.as_ref()), - // (accs.pyth_state10.as_mut(), accs.pyth_price10.as_ref()), - ]; - - let price_pairs: Vec<(_, _)> = price_pair_opts - .iter_mut() - .filter_map(|pair| match pair { - // Only use this pair if both accounts are Some - (Some(state), Some(price)) => Some((state, price)), - _other => None, - }) - .collect(); - - - trace!("{} Pyth symbols received", price_pairs.len()); - - // Collect the validated symbols here for batch serialization - let mut attestations = Vec::with_capacity(price_pairs.len()); - - let this_attestation_time = accs.clock.unix_timestamp; - - - let mut over_rate_limit = true; - for (state, price) in price_pairs.into_iter() { - // Pyth must own the price - if accs.config.pyth_owner != *price.owner { - trace!(&format!( - "Price {:?}: owner pubkey mismatch (expected pyth_owner {:?}, got unknown price owner {:?})", - price, accs.config.pyth_owner, price.owner - )); - return Err(SolitaireError::InvalidOwner(*price.owner)); - } - - // State pubkey must reproduce from the price id - let state_addr_from_price = AttestationStatePDA::key(price.key, ctx.program_id); - if state_addr_from_price != *state.0 .0.info().key { - trace!(&format!( - "Price {:?}: pubkey does not produce the passed state account (expected {:?} from seeds, {:?} was passed)", - price.key, state_addr_from_price, state.0.0.info().key - )); - return Err(ProgramError::InvalidAccountData.into()); - } - - let price_data_ref = price.try_borrow_data()?; - - // Parse the upstream Pyth struct to extract current publish - // time for payload construction - let price_struct = - pyth_sdk_solana::state::load_price_account(&price_data_ref).map_err(|e| { - trace!(&e.to_string()); - ProgramError::InvalidAccountData - })?; - - // Retrieve and rotate last_attested_tradind_publish_time - - - // Pick the value to store for the next attestation of this - // symbol. We use the prev_ value if the symbol is not - // currently being traded. The oracle marks the last known - // trading timestamp with it. - let new_last_attested_trading_publish_time = match price_struct.agg.status { - PriceStatus::Trading => price_struct.timestamp, - _ => price_struct.prev_timestamp, - }; - - // Retrieve the timestamp saved during the previous - // attestation. Use the new_* value if no existind state is - // present on-chain - let current_last_attested_trading_publish_time = if state.0 .0.is_initialized() { - // Use the existing on-chain value - state.0 .0 .1.last_attested_trading_publish_time - } else { - // Fall back to the new value if the state is not initialized - new_last_attested_trading_publish_time - }; - - // Build an attestatioin struct for this symbol using the just decided current value - let attestation = PriceAttestation::from_pyth_price_struct( - Identifier::new(price.key.to_bytes()), - this_attestation_time, - current_last_attested_trading_publish_time, - price_struct, - ); - - // Evaluate rate limit - should be smaller than duration from last attestation - let trading_publish_time_diff = - new_last_attested_trading_publish_time - state.0 .0.last_attested_trading_publish_time; - let attestation_time_diff = this_attestation_time - state.0 .0.last_attestation_time; - - // We like to have the rate_limit for trading publish_time because that is the field that - // the users consume. Also, when the price is not trading and trading_publish_time is the - // same, we still want to send the prices (on a lower frequency). - if trading_publish_time_diff >= data.rate_limit_interval_secs as i64 - || attestation_time_diff >= 2 * data.rate_limit_interval_secs as i64 - { - over_rate_limit = false; - } else { - trace!("Price {:?}: over rate limit", price.key); - } - - // Save the new value for the next attestation of this symbol - state.0 .0.last_attested_trading_publish_time = new_last_attested_trading_publish_time; - - // Update last attestation time - state.0 .0.last_attestation_time = this_attestation_time; - - // handling of last_attested_trading_publish_time ends here - - if !state.0 .0.is_initialized() { - // Serialize the state to learn account size for creation - let state_serialized = state.0 .0 .1.try_to_vec()?; - - let seeds = state.self_bumped_seeds(price.key, ctx.program_id); - solitaire::create_account( - ctx, - state.0 .0.info(), - accs.payer.key, - solitaire::CreationLamports::Exempt, - state_serialized.len(), - ctx.program_id, - solitaire::IsSigned::SignedWithSeeds(&[seeds - .iter() - .map(|s| s.as_slice()) - .collect::>() - .as_slice()]), - )?; - trace!("Attestation state init OK"); - } - - - attestations.push(attestation); - } - - // Do not proceed if none of the symbols is under rate limit - if over_rate_limit { - trace!("All symbols over limit, bailing out"); - return Err( - ProgramError::Custom(AttesterCustomError::AttestRateLimitReached as u32).into(), - ); - } - - let batch_attestation = BatchPriceAttestation { - price_attestations: attestations, - }; - - trace!("Attestations successfully created"); - - let bridge_config = BridgeData::try_from_slice(&accs.wh_bridge.try_borrow_mut_data()?)?.config; - - // Pay wormhole fee - let transfer_ix = solana_program::system_instruction::transfer( - accs.payer.key, - accs.wh_fee_collector.info().key, - bridge_config.fee, - ); - solana_program::program::invoke(&transfer_ix, ctx.accounts)?; - - let payload = batch_attestation.serialize().map_err(|e| { - trace!(&e.to_string()); - ProgramError::InvalidAccountData - })?; - - let wh_msg_drv_data = P2WMessageDrvData { - message_owner: *accs.payer.key, - batch_size: batch_attestation.price_attestations.len() as u16, - id: data.message_account_id, - }; - - if !P2WMessage::key(&wh_msg_drv_data, ctx.program_id).eq(accs.wh_message.info().key) { - trace!( - "Invalid seeds for wh message pubkey. Expected {} with given seeds {:?}, got {}", - P2WMessage::key(&wh_msg_drv_data, ctx.program_id), - P2WMessage::seeds(&wh_msg_drv_data) - .iter_mut() - .map(|seed| seed.as_slice()) - .collect::>() - .as_slice(), - accs.wh_message.info().key - ); - return Err(ProgramError::InvalidSeeds.into()); - } - - let ix = bridge::instructions::post_message_unreliable( - *accs.wh_prog.info().key, - *accs.payer.info().key, - *accs.wh_emitter.info().key, - *accs.wh_message.info().key, - 0, - payload, - data.consistency_level, - )?; - - trace!(&format!( - "Cross-call Seeds: {:?}", - [ - // message seeds - P2WMessage::seeds(&wh_msg_drv_data) - .iter_mut() - .map(|seed| seed.as_slice()) - .collect::>() - .as_slice(), - // emitter seeds - P2WEmitter::seeds(None) - .iter_mut() - .map(|seed| seed.as_slice()) - .collect::>() - .as_slice(), - ] - )); - - trace!("attest() finished, cross-calling wormhole"); - - invoke_signed( - &ix, - ctx.accounts, - [ - // message seeds - P2WMessage::bumped_seeds(&wh_msg_drv_data, ctx.program_id) - .iter_mut() - .map(|seed| seed.as_slice()) - .collect::>() - .as_slice(), - // emitter seeds - P2WEmitter::bumped_seeds(None, ctx.program_id) - .iter_mut() - .map(|seed| seed.as_slice()) - .collect::>() - .as_slice(), - ] - .as_slice(), - )?; - - Ok(()) -} diff --git a/wormhole_attester/program/src/attestation_state.rs b/wormhole_attester/program/src/attestation_state.rs deleted file mode 100644 index 33e0a38b..00000000 --- a/wormhole_attester/program/src/attestation_state.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! Implementation of per-symbol on-chain state. Currently used to -//! store latest successful attestation time for each price. - -use { - borsh::{ - BorshDeserialize, - BorshSerialize, - }, - solana_program::{ - clock::UnixTimestamp, - pubkey::Pubkey, - }, - solitaire::{ - AccountOwner, - AccountState, - Data, - Owned, - Peel, - Seeded, - }, -}; - -/// On-chain state for a single price attestation -#[derive(BorshSerialize, BorshDeserialize, Default)] -pub struct AttestationState { - /// The last trading publish_time this attester saw - pub last_attested_trading_publish_time: UnixTimestamp, - /// The last time this symbol was attested - pub last_attestation_time: UnixTimestamp, -} - -impl Owned for AttestationState { - fn owner(&self) -> AccountOwner { - AccountOwner::This - } -} - -pub struct AttestationStatePDA<'b>( - pub Data<'b, AttestationState, { AccountState::MaybeInitialized }>, -); - -impl Seeded<&Pubkey> for AttestationStatePDA<'_> { - fn seeds(symbol_id: &Pubkey) -> Vec> { - vec![ - "p2w-attestation-state-v1".as_bytes().to_vec(), - symbol_id.to_bytes().to_vec(), - ] - } -} - -impl<'a, 'b: 'a> Peel<'a, 'b> for AttestationStatePDA<'b> { - fn peel(ctx: &mut solitaire::Context<'a, 'b, I>) -> solitaire::Result - where - Self: Sized, - { - Ok(Self(Data::peel(ctx)?)) - } - - fn persist(&self, program_id: &Pubkey) -> solitaire::Result<()> { - self.0.persist(program_id) - } -} diff --git a/wormhole_attester/program/src/config.rs b/wormhole_attester/program/src/config.rs deleted file mode 100644 index e8e0919d..00000000 --- a/wormhole_attester/program/src/config.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! On-chain state for the pyth2wormhole SOL contract. -//! -//! Important: Changes to max batch size must be reflected in the -//! instruction logic in attest.rs (look there for more -//! details). Mismatches between config and contract logic may confuse -//! attesters. -//! -//! How to add a new config schema: -//! X - new config version number -//! Y = X - 1; previous config number -//! 1. Add a next Pyth2WormholeConfigVX struct, -//! e.g. Pyth2WormholeConfigV3, -//! 2. Add a P2WConfigAccountVX type alias with a unique seed str -//! 3. Implement From for the new struct, -//! e.g. From for Pyth2WormholeConfigV3 -//! 4. Advance Pyth2WormholeConfig, P2WConfigAccount, -//! OldPyth2WormholeConfig, OldP2WConfigAccount typedefs to use the -//! previous and new config structs. -//! 5. Deploy and call migrate() to verify -//! 6. (optional) Remove/comment out config structs and aliases from -//! before version Y. - -use { - borsh::{ - BorshDeserialize, - BorshSerialize, - }, - solana_program::pubkey::Pubkey, - solitaire::{ - processors::seeded::AccountOwner, - AccountState, - Data, - Derive, - Owned, - }, -}; - -/// Aliases for current config schema (to migrate into) -pub type Pyth2WormholeConfig = Pyth2WormholeConfigV3; -pub type P2WConfigAccount<'b, const IS_INITIALIZED: AccountState> = - P2WConfigAccountV3<'b, IS_INITIALIZED>; - -impl Owned for Pyth2WormholeConfig { - fn owner(&self) -> AccountOwner { - AccountOwner::This - } -} - -/// Aliases for previous config schema (to migrate from) -pub type OldPyth2WormholeConfig = Pyth2WormholeConfigV2; -pub type OldP2WConfigAccount<'b> = P2WConfigAccountV2<'b, { AccountState::Initialized }>; // Old config must always be initialized - -impl Owned for OldPyth2WormholeConfig { - fn owner(&self) -> AccountOwner { - AccountOwner::This - } -} - -/// Initial config format -#[derive(Clone, Default, BorshDeserialize, BorshSerialize)] -#[cfg_attr(feature = "client", derive(Debug))] -pub struct Pyth2WormholeConfigV1 { - /// Authority owning this contract - pub owner: Pubkey, - /// Wormhole bridge program - pub wh_prog: Pubkey, - /// Authority owning Pyth price data - pub pyth_owner: Pubkey, - pub max_batch_size: u16, -} - -pub type P2WConfigAccountV1<'b, const IS_INITIALIZED: AccountState> = - Derive, "pyth2wormhole-config">; - -/// Added is_active -#[derive(Clone, Default, BorshDeserialize, BorshSerialize)] -#[cfg_attr(feature = "client", derive(Debug))] -pub struct Pyth2WormholeConfigV2 { - /// Authority owning this contract - pub owner: Pubkey, - /// Wormhole bridge program - pub wh_prog: Pubkey, - /// Authority owning Pyth price data - pub pyth_owner: Pubkey, - /// How many product/price pairs can be sent and attested at once - /// - /// Important: Whenever the corresponding logic in attest.rs - /// changes its expected number of symbols per batch, this config - /// must be updated accordingly on-chain. - pub max_batch_size: u16, - - /// If set to false, attest() will reject all calls unconditionally - pub is_active: bool, -} - -/// Note: If you get stuck with a pre-existing config account -/// (e.g. someone transfers into a PDA that we're not using yet), it's -/// usually easier to change the seed slightly -/// (e.g. pyth2wormhole-config-v2 -> pyth2wormhole-config-v2.1). This -/// saves a lot of time coding around this edge case. -pub type P2WConfigAccountV2<'b, const IS_INITIALIZED: AccountState> = - Derive, "pyth2wormhole-config-v2.1">; - -impl From for Pyth2WormholeConfigV2 { - fn from(old: Pyth2WormholeConfigV1) -> Self { - let Pyth2WormholeConfigV1 { - owner, - wh_prog, - pyth_owner, - max_batch_size, - } = old; - - Self { - owner, - wh_prog, - pyth_owner, - max_batch_size, - is_active: true, - } - } -} - -// Added ops_owner which can toggle the is_active field -#[derive(Clone, Default, Hash, BorshDeserialize, BorshSerialize, PartialEq, Eq)] -#[cfg_attr(feature = "client", derive(Debug))] -pub struct Pyth2WormholeConfigV3 { - /// Authority owning this contract - pub owner: Pubkey, - /// Wormhole bridge program - pub wh_prog: Pubkey, - /// Authority owning Pyth price data - pub pyth_owner: Pubkey, - /// How many product/price pairs can be sent and attested at once - /// - /// Important: Whenever the corresponding logic in attest.rs - /// changes its expected number of symbols per batch, this config - /// must be updated accordingly on-chain. - pub max_batch_size: u16, - - /// If set to false, attest() will reject all calls unconditionally - pub is_active: bool, - - // If the ops_owner exists, it can toggle the value of `is_active` - pub ops_owner: Option, -} - -pub type P2WConfigAccountV3<'b, const IS_INITIALIZED: AccountState> = - Derive, "pyth2wormhole-config-v3">; - -impl From for Pyth2WormholeConfigV3 { - fn from(old: Pyth2WormholeConfigV2) -> Self { - let Pyth2WormholeConfigV2 { - owner, - wh_prog, - pyth_owner, - max_batch_size, - is_active: _, - } = old; - - Self { - owner, - wh_prog, - pyth_owner, - max_batch_size, - is_active: true, - ops_owner: None, - } - } -} diff --git a/wormhole_attester/program/src/error.rs b/wormhole_attester/program/src/error.rs deleted file mode 100644 index 6471c53f..00000000 --- a/wormhole_attester/program/src/error.rs +++ /dev/null @@ -1,6 +0,0 @@ -/// Append-only custom error list. -#[repr(u32)] -pub enum AttesterCustomError { - /// Explicitly checked for in client code, change carefully - AttestRateLimitReached = 13, -} diff --git a/wormhole_attester/program/src/initialize.rs b/wormhole_attester/program/src/initialize.rs deleted file mode 100644 index 29a8f872..00000000 --- a/wormhole_attester/program/src/initialize.rs +++ /dev/null @@ -1,39 +0,0 @@ -use { - crate::config::{ - P2WConfigAccount, - Pyth2WormholeConfig, - }, - solitaire::{ - trace, - AccountState, - CreationLamports, - ExecutionContext, - FromAccounts, - Info, - Keyed, - Mut, - Peel, - Result as SoliResult, - Signer, - }, -}; - -#[derive(FromAccounts)] -pub struct Initialize<'b> { - pub new_config: Mut>, - pub payer: Mut>>, - pub system_program: Info<'b>, -} - -/// Must be called right after deployment -pub fn initialize( - ctx: &ExecutionContext, - accs: &mut Initialize, - data: Pyth2WormholeConfig, -) -> SoliResult<()> { - accs.new_config - .create(ctx, accs.payer.info().key, CreationLamports::Exempt)?; - accs.new_config.1 = data; - - Ok(()) -} diff --git a/wormhole_attester/program/src/lib.rs b/wormhole_attester/program/src/lib.rs deleted file mode 100644 index 2bcbe70c..00000000 --- a/wormhole_attester/program/src/lib.rs +++ /dev/null @@ -1,46 +0,0 @@ -#![allow(incomplete_features)] -#![feature(adt_const_params)] -pub mod attest; -pub mod attestation_state; -pub mod config; -pub mod error; -pub mod initialize; -pub mod message; -pub mod migrate; -pub mod set_config; -pub mod set_is_active; - -use solitaire::solitaire; -pub use { - attest::{ - attest, - Attest, - AttestData, - }, - config::Pyth2WormholeConfig, - initialize::{ - initialize, - Initialize, - }, - migrate::{ - migrate, - Migrate, - }, - pyth_client, - set_config::{ - set_config, - SetConfig, - }, - set_is_active::{ - set_is_active, - SetIsActive, - }, -}; - -solitaire! { - Attest => attest, - Initialize => initialize, - SetConfig => set_config, - Migrate => migrate, - SetIsActive => set_is_active -} diff --git a/wormhole_attester/program/src/message.rs b/wormhole_attester/program/src/message.rs deleted file mode 100644 index b4a333a7..00000000 --- a/wormhole_attester/program/src/message.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Index-based PDA for storing unreliable wormhole message -//! -//! The main goal of this PDA is to take advantage of wormhole message -//! reuse securely. This is achieved by tying the account derivation -//! data to the payer account of the attest() instruction. Inside -//! attest(), payer must be a signer, and the message account must be -//! derived with their address as message_owner in -//! `P2WMessageDrvData`. - -use { - borsh::{ - BorshDeserialize, - BorshSerialize, - }, - bridge::PostedMessageUnreliable, - solana_program::pubkey::Pubkey, - solitaire::{ - processors::seeded::Seeded, - AccountState, - Mut, - }, -}; - -pub type P2WMessage<'a> = Mut>; - -#[derive(BorshDeserialize, BorshSerialize)] -pub struct P2WMessageDrvData { - /// The key owning this message account - pub message_owner: Pubkey, - /// Size of the batch. It is important that all messages have the same size - /// - /// NOTE: 2022-09-05 - /// Currently wormhole does not resize accounts if they have different - /// payload sizes; this (along with versioning the seed literal below) is - /// a workaround to have different PDAs for different batch sizes. - pub batch_size: u16, - /// Index for keeping many accounts per owner - pub id: u64, -} - -impl<'a> Seeded<&P2WMessageDrvData> for P2WMessage<'a> { - fn seeds(data: &P2WMessageDrvData) -> Vec> { - vec![ - // See the note at 2022-09-05 above. - // Change the version in the literal whenever you change the - // price attestation data. - "p2w-message-v2".as_bytes().to_vec(), - data.message_owner.to_bytes().to_vec(), - data.batch_size.to_be_bytes().to_vec(), - data.id.to_be_bytes().to_vec(), - ] - } -} diff --git a/wormhole_attester/program/src/migrate.rs b/wormhole_attester/program/src/migrate.rs deleted file mode 100644 index 69112a9f..00000000 --- a/wormhole_attester/program/src/migrate.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Instruction used to migrate on-chain configuration from an older format - -use { - crate::config::{ - OldP2WConfigAccount, - OldPyth2WormholeConfig, - P2WConfigAccount, - Pyth2WormholeConfig, - }, - solana_program::{ - program_error::ProgramError, - system_program, - }, - solitaire::{ - trace, - AccountState, - CreationLamports, - ExecutionContext, - FromAccounts, - Info, - Keyed, - Mut, - Peel, - Result as SoliResult, - Signer, - SolitaireError, - }, -}; - -/// Migration accounts meant to evolve with subsequent config accounts -/// -/// NOTE: This account struct assumes Solitaire is able to validate the -/// Uninitialized requirement on the new_config account -#[derive(FromAccounts)] -pub struct Migrate<'b> { - /// New config account to be populated. Must be unused. - pub new_config: Mut>, - /// Old config using the previous format. - pub old_config: Mut>, - /// Current owner authority of the program - pub current_owner: Mut>>, - /// Payer account for updating the account data - pub payer: Mut>>, - /// For creating the new config account - pub system_program: Info<'b>, -} - -pub fn migrate(ctx: &ExecutionContext, accs: &mut Migrate, _data: ()) -> SoliResult<()> { - let old_config: &OldPyth2WormholeConfig = &accs.old_config.1; - - if &old_config.owner != accs.current_owner.info().key { - trace!( - "Current config owner account mismatch (expected {:?})", - old_config.owner - ); - return Err(SolitaireError::InvalidSigner( - *accs.current_owner.info().key, - )); - } - - if *accs.system_program.key != system_program::id() { - trace!( - "Invalid system program, expected {:?}), found {}", - system_program::id(), - accs.system_program.key - ); - return Err(SolitaireError::InvalidSigner(*accs.system_program.key)); - } - - // Populate new config - accs.new_config - .create(ctx, accs.payer.info().key, CreationLamports::Exempt)?; - accs.new_config.1 = Pyth2WormholeConfig::from(old_config.clone()); - - // Reclaim old config lamports - - // Save current balance - let old_config_balance_val: u64 = accs.old_config.info().lamports(); - - // Drain old config - **accs.old_config.info().lamports.borrow_mut() = 0; - - // Credit payer with saved balance - let new_payer_balance = accs - .payer - .info() - .lamports - .borrow_mut() - .checked_add(old_config_balance_val) - .ok_or_else(|| { - trace!("Overflow on payer balance increase"); - SolitaireError::ProgramError(ProgramError::Custom(0xDEADBEEF)) - })?; - - **accs.payer.info().lamports.borrow_mut() = new_payer_balance; - - Ok(()) -} diff --git a/wormhole_attester/program/src/set_config.rs b/wormhole_attester/program/src/set_config.rs deleted file mode 100644 index fc031128..00000000 --- a/wormhole_attester/program/src/set_config.rs +++ /dev/null @@ -1,85 +0,0 @@ -use { - crate::config::{ - P2WConfigAccount, - Pyth2WormholeConfig, - }, - borsh::BorshSerialize, - solana_program::{ - program::invoke, - rent::Rent, - system_instruction, - sysvar::Sysvar, - }, - solitaire::{ - trace, - AccountState, - ExecutionContext, - FromAccounts, - Info, - Keyed, - Mut, - Peel, - Result as SoliResult, - Signer, - SolitaireError, - }, -}; - -#[derive(FromAccounts)] -pub struct SetConfig<'b> { - /// Current config used by the program - pub config: Mut>, - /// Current owner authority of the program - pub current_owner: Mut>>, - /// Payer account for updating the account data - pub payer: Mut>>, - /// Used for rent adjustment transfer - pub system_program: Info<'b>, -} - -/// Alters the current settings of pyth2wormhole -pub fn set_config( - ctx: &ExecutionContext, - accs: &mut SetConfig, - data: Pyth2WormholeConfig, -) -> SoliResult<()> { - let cfg_struct: &Pyth2WormholeConfig = &accs.config; // unpack Data via nested Deref impls - if &cfg_struct.owner != accs.current_owner.info().key { - trace!( - "Current owner account mismatch (expected {:?})", - cfg_struct.owner - ); - return Err(SolitaireError::InvalidSigner( - *accs.current_owner.info().key, - )); - } - - let old_size = accs.config.info().data_len(); - let new_size = data.try_to_vec()?.len(); - - // Realloc if mismatched - if old_size != new_size { - accs.config.info().realloc(new_size, false)?; - } - - accs.config.1 = data; - - // Adjust lamports - let acc_lamports = accs.config.info().lamports(); - - let new_lamports = Rent::get()?.minimum_balance(new_size); - - let diff_lamports: u64 = (acc_lamports as i64 - new_lamports as i64).unsigned_abs(); - - if acc_lamports < new_lamports { - // Less than enough lamports, debit the payer - let transfer_ix = system_instruction::transfer( - accs.payer.info().key, - accs.config.info().key, - diff_lamports, - ); - invoke(&transfer_ix, ctx.accounts)?; - } - - Ok(()) -} diff --git a/wormhole_attester/program/src/set_is_active.rs b/wormhole_attester/program/src/set_is_active.rs deleted file mode 100644 index 1a74f07a..00000000 --- a/wormhole_attester/program/src/set_is_active.rs +++ /dev/null @@ -1,54 +0,0 @@ -use { - crate::config::{ - P2WConfigAccount, - Pyth2WormholeConfig, - }, - solitaire::{ - trace, - AccountState, - ExecutionContext, - FromAccounts, - Info, - Keyed, - Mut, - Peel, - Result as SoliResult, - Signer, - SolitaireError, - }, -}; - -#[derive(FromAccounts)] -pub struct SetIsActive<'b> { - /// Current config used by the program - pub config: Mut>, - /// Current owner authority of the program - pub ops_owner: Mut>>, - /// Payer account for updating the account data - pub payer: Mut>>, -} - -/// Alters the current settings of pyth2wormhole -pub fn set_is_active( - _ctx: &ExecutionContext, - accs: &mut SetIsActive, - new_is_active: bool, -) -> SoliResult<()> { - let cfg_struct: &mut Pyth2WormholeConfig = &mut accs.config; // unpack Data via nested Deref impls - match &cfg_struct.ops_owner { - None => Err(SolitaireError::InvalidOwner(*accs.ops_owner.info().key)), - Some(current_ops_owner) => { - if current_ops_owner != accs.ops_owner.info().key { - trace!( - "Ops owner account mismatch (expected {:?})", - current_ops_owner - ); - return Err(SolitaireError::InvalidOwner(*accs.ops_owner.info().key)); - } - - cfg_struct.is_active = new_is_active; - - Ok(()) - } - } -} diff --git a/wormhole_attester/sdk/js/.gitignore b/wormhole_attester/sdk/js/.gitignore deleted file mode 100644 index 90d09611..00000000 --- a/wormhole_attester/sdk/js/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# ethereum contracts -/contracts -/src/*-contracts/ - -# tsproto output -/src/proto - -# build -/lib diff --git a/wormhole_attester/sdk/js/README.md b/wormhole_attester/sdk/js/README.md deleted file mode 100644 index 61d4d545..00000000 --- a/wormhole_attester/sdk/js/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Pyth2wormhole SDK - -This project contains a library for interacting with pyth2wormhole and adjacent APIs. - -# Install - -For now, the in-house dependencies are referenced by relative -path. The commands below will build those. For an automated version of -this process, please refer to `p2w-integration-observer`'s Dockerfile and/or our [Tilt](https://tilt.dev) -devnet with `pyth` enabled. - -```shell -# Run the commands in this README's directory for --prefix to work -$ npm --prefix ../../../ethereum ci && npm --prefix ../../../ethereum run build # ETH contracts -$ npm --prefix ../../../sdk/js ci # Wormhole SDK -$ npm ci && npm run build # Pyth2wormhole SDK -``` diff --git a/wormhole_attester/sdk/js/jest.config.js b/wormhole_attester/sdk/js/jest.config.js deleted file mode 100644 index 21a1e973..00000000 --- a/wormhole_attester/sdk/js/jest.config.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ -module.exports = { - preset: "ts-jest", - testEnvironment: "node", -}; diff --git a/wormhole_attester/sdk/js/package.json b/wormhole_attester/sdk/js/package.json deleted file mode 100644 index 64f7e38a..00000000 --- a/wormhole_attester/sdk/js/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "@pythnetwork/wormhole-attester-sdk", - "version": "1.1.0", - "description": "Pyth Wormhole Attester SDk", - "private": "true", - "types": "lib/index.d.ts", - "main": "lib/index.js", - "files": [ - "lib/**/*" - ], - "scripts": { - "build": "tsc", - "format": "prettier --write \"src/**/*.ts\"", - "lint": "tslint -p tsconfig.json", - "test": "jest src/", - "postversion": "git push && git push --tags", - "preversion": "npm run lint", - "version": "npm run format && git add -A src" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/pyth-network/pyth-crosschain.git" - }, - "author": "Pyth Data Association", - "license": "MIT", - "devDependencies": { - "@types/jest": "^29.4.0", - "@types/long": "^4.0.1", - "@types/node": "^16.6.1", - "copy-dir": "^1.3.0", - "find": "^0.3.0", - "jest": "^29.4.1", - "prettier": "^2.3.2", - "ts-jest": "^29.0.5", - "tslint": "^6.1.3", - "tslint-config-prettier": "^1.18.0", - "typescript": "^4.3.5" - }, - "bugs": { - "url": "https://github.com/pyth-network/pyth-crosschain/issues" - }, - "homepage": "https://github.com/pyth-network/pyth-crosschain#readme", - "dependencies": { - "@pythnetwork/price-service-sdk": "*" - } -} diff --git a/wormhole_attester/sdk/js/src/__tests__/index.test.ts b/wormhole_attester/sdk/js/src/__tests__/index.test.ts deleted file mode 100644 index e30335aa..00000000 --- a/wormhole_attester/sdk/js/src/__tests__/index.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { - parseBatchPriceAttestation, - Price, - PriceFeed, - PriceAttestation, - PriceAttestationStatus, - priceAttestationToPriceFeed, -} from "../index"; - -describe("Deserializing Batch Price Attestation works", () => { - test("when batch has 3 price feeds", () => { - // Generated from the rust sdk test_batch_serde - const fixture = - "50325748000300010001020003009D01010101010101010101010101010101010101010101010101010" + - "10101010101FEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFEFE0000002B" + - "AD2FEED70000000000000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D00" + - "000DEADBEEFFADE00000000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0000" + - "DEADBEEFFACE0202020202020202020202020202020202020202020202020202020202020202FDFDFDF" + - "DFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFDFD0000002BAD2FEED70000000000" + - "000065FFFFFFFDFFFFFFFFFFFFFFD6000000000000002A010001E14C0004E6D00000DEADBEEFFADE000" + - "00000DADEBEEF00000000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0000DEADBEEFFACE030303" + - "0303030303030303030303030303030303030303030303030303030303FCFCFCFCFCFCFCFCFCFCFCFCF" + - "CFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFCFC0000002BAD2FEED70000000000000065FFFFFFFDFFFF" + - "FFFFFFFFFFD6000000000000002A010001E14C0004E6D00000DEADBEEFFADE00000000DADEBEEF00000" + - "000DEADBABE0000DEADFACEBEEF000000BADBADBEEF0000DEADBEEFFACE"; - - const data = Buffer.from(fixture, "hex"); - const batchPriceAttestation = parseBatchPriceAttestation(data); - - expect(batchPriceAttestation.priceAttestations.length).toBe(3); - - // values are from the rust sdk mock_attestation - batchPriceAttestation.priceAttestations.forEach((pa, idx) => { - expect(pa).toEqual({ - productId: Buffer.from(Array(32).fill(idx + 1)).toString("hex"), - priceId: Buffer.from(Array(32).fill(255 - idx - 1)).toString("hex"), - price: (0x2bad2feed7).toString(), - conf: "101", - emaPrice: "-42", - emaConf: "42", - expo: -3, - status: PriceAttestationStatus.Trading, - numPublishers: 123212, - maxNumPublishers: 321232, - attestationTime: 0xdeadbeeffade, - publishTime: 0xdadebeef, - prevPublishTime: 0xdeadbabe, - prevPrice: (0xdeadfacebeef).toString(), - prevConf: (0xbadbadbeef).toString(), - lastAttestedPublishTime: 0xdeadbeefface, - }); - }); - }); -}); - -describe("Price Attestation to Price Feed works", () => { - test("when status is trading", () => { - const priceAttestation = { - productId: "012345", - priceId: "abcde", - price: "100", - conf: "5", - emaPrice: "103", - emaConf: "3", - expo: -3, - status: PriceAttestationStatus.Trading, - numPublishers: 1, - maxNumPublishers: 2, - attestationTime: 1000, - publishTime: 1000, - prevPublishTime: 998, - prevPrice: "101", - prevConf: "6", - lastAttestedPublishTime: 997, - }; - - const priceFeed = priceAttestationToPriceFeed(priceAttestation); - expect(priceFeed).toEqual( - new PriceFeed({ - id: "abcde", - price: new Price({ - price: "100", - conf: "5", - expo: -3, - publishTime: 1000, - }), - emaPrice: new Price({ - price: "103", - conf: "3", - expo: -3, - publishTime: 1000, - }), - }) - ); - }); - - test("when status is not trading", () => { - const priceAttestation = { - productId: "012345", - priceId: "abcde", - price: "100", - conf: "5", - emaPrice: "103", - emaConf: "3", - expo: -3, - status: PriceAttestationStatus.Unknown, - numPublishers: 1, - maxNumPublishers: 2, - attestationTime: 1000, - publishTime: 1000, - prevPublishTime: 998, - prevPrice: "101", - prevConf: "6", - lastAttestedPublishTime: 997, - }; - - const priceFeed = priceAttestationToPriceFeed(priceAttestation); - expect(priceFeed).toEqual( - new PriceFeed({ - id: "abcde", - price: new Price({ - price: "101", - conf: "6", - expo: -3, - publishTime: 998, - }), - emaPrice: new Price({ - price: "103", - conf: "3", - expo: -3, - publishTime: 998, - }), - }) - ); - }); -}); diff --git a/wormhole_attester/sdk/js/src/index.ts b/wormhole_attester/sdk/js/src/index.ts deleted file mode 100644 index 4c327466..00000000 --- a/wormhole_attester/sdk/js/src/index.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { - PriceFeed, - Price, - UnixTimestamp, -} from "@pythnetwork/price-service-sdk"; - -export { - PriceFeed, - Price, - UnixTimestamp, -} from "@pythnetwork/price-service-sdk"; - -export enum PriceAttestationStatus { - Unknown = 0, - Trading = 1, - Halted = 2, - Auction = 3, - Ignored = 4, -} - -export type PriceAttestation = { - productId: string; - priceId: string; - price: string; - conf: string; - expo: number; - emaPrice: string; - emaConf: string; - status: PriceAttestationStatus; - numPublishers: number; - maxNumPublishers: number; - attestationTime: UnixTimestamp; - publishTime: UnixTimestamp; - prevPublishTime: UnixTimestamp; - prevPrice: string; - prevConf: string; - lastAttestedPublishTime: UnixTimestamp; -}; - -export type BatchPriceAttestation = { - priceAttestations: PriceAttestation[]; -}; - -/// Precedes every message implementing the wormhole attester serialization format -const P2W_FORMAT_MAGIC: string = "P2WH"; -const P2W_FORMAT_VER_MAJOR = 3; -const P2W_FORMAT_VER_MINOR = 0; -const P2W_FORMAT_PAYLOAD_ID = 2; - -export function parsePriceAttestation(bytes: Buffer): PriceAttestation { - let offset = 0; - - const productId = bytes.slice(offset, offset + 32).toString("hex"); - offset += 32; - - const priceId = bytes.slice(offset, offset + 32).toString("hex"); - offset += 32; - - const price = bytes.readBigInt64BE(offset).toString(); - offset += 8; - - const conf = bytes.readBigUint64BE(offset).toString(); - offset += 8; - - const expo = bytes.readInt32BE(offset); - offset += 4; - - const emaPrice = bytes.readBigInt64BE(offset).toString(); - offset += 8; - - const emaConf = bytes.readBigUint64BE(offset).toString(); - offset += 8; - - const status = bytes.readUint8(offset) as PriceAttestationStatus; - offset += 1; - - const numPublishers = bytes.readUint32BE(offset); - offset += 4; - - const maxNumPublishers = bytes.readUint32BE(offset); - offset += 4; - - const attestationTime = Number(bytes.readBigInt64BE(offset)); - offset += 8; - - const publishTime = Number(bytes.readBigInt64BE(offset)); - offset += 8; - - const prevPublishTime = Number(bytes.readBigInt64BE(offset)); - offset += 8; - - const prevPrice = bytes.readBigInt64BE(offset).toString(); - offset += 8; - - const prevConf = bytes.readBigUint64BE(offset).toString(); - offset += 8; - - const lastAttestedPublishTime = Number(bytes.readBigInt64BE(offset)); - offset += 8; - - return { - productId, - priceId, - price, - conf, - expo, - emaPrice, - emaConf, - status, - numPublishers, - maxNumPublishers, - attestationTime, - publishTime, - prevPublishTime, - prevPrice, - prevConf, - lastAttestedPublishTime, - }; -} - -// Read the sdk/rust as the reference implementation and documentation. -export function parseBatchPriceAttestation( - bytes: Buffer -): BatchPriceAttestation { - let offset = 0; - - const magic = bytes.slice(offset, offset + 4).toString("utf8"); - offset += 4; - if (magic !== P2W_FORMAT_MAGIC) { - throw new Error(`Invalid magic: ${magic}, expected: ${P2W_FORMAT_MAGIC}`); - } - - const versionMajor = bytes.readUInt16BE(offset); - offset += 2; - if (versionMajor !== P2W_FORMAT_VER_MAJOR) { - throw new Error( - `Unsupported major version: ${versionMajor}, expected: ${P2W_FORMAT_VER_MAJOR}` - ); - } - - const versionMinor = bytes.readUInt16BE(offset); - offset += 2; - if (versionMinor < P2W_FORMAT_VER_MINOR) { - throw new Error( - `Unsupported minor version: ${versionMinor}, expected: ${P2W_FORMAT_VER_MINOR}` - ); - } - - // Header size is added for future-compatibility - const headerSize = bytes.readUint16BE(offset); - offset += 2; - - let headerOffset = 0; - - const payloadId = bytes.readUint8(offset + headerOffset); - headerOffset += 1; - - if (payloadId !== P2W_FORMAT_PAYLOAD_ID) { - throw new Error( - `Invalid payloadId: ${payloadId}, expected: ${P2W_FORMAT_PAYLOAD_ID}` - ); - } - - offset += headerSize; - - const batchLen = bytes.readUInt16BE(offset); - offset += 2; - - const attestationSize = bytes.readUint16BE(offset); - offset += 2; - - const priceAttestations: PriceAttestation[] = []; - - for (let i = 0; i < batchLen; i += 1) { - priceAttestations.push( - parsePriceAttestation(bytes.subarray(offset, offset + attestationSize)) - ); - offset += attestationSize; - } - - if (offset !== bytes.length) { - throw new Error(`Invalid length: ${bytes.length}, expected: ${offset}`); - } - - return { - priceAttestations, - }; -} - -// Returns a hash of all priceIds within the batch, it can be used to identify whether there is a -// new batch with exact same symbols (and ignore the old one) -export function getBatchAttestationHashKey( - batchAttestation: BatchPriceAttestation -): string { - const priceIds: string[] = batchAttestation.priceAttestations.map( - (priceAttestation) => priceAttestation.priceId - ); - priceIds.sort(); - - return priceIds.join("#"); -} - -export function getBatchSummary(batch: BatchPriceAttestation): string { - const abstractRepresentation = { - num_attestations: batch.priceAttestations.length, - prices: batch.priceAttestations.map((priceAttestation) => { - const priceFeed = priceAttestationToPriceFeed(priceAttestation); - return { - price_id: priceFeed.id, - price: priceFeed.getPriceUnchecked().getPriceAsNumberUnchecked(), - conf: priceFeed.getEmaPriceUnchecked().getConfAsNumberUnchecked(), - }; - }), - }; - return JSON.stringify(abstractRepresentation); -} - -export function priceAttestationToPriceFeed( - priceAttestation: PriceAttestation -): PriceFeed { - const emaPrice: Price = new Price({ - conf: priceAttestation.emaConf, - expo: priceAttestation.expo, - price: priceAttestation.emaPrice, - publishTime: priceAttestation.publishTime, - }); - - let price: Price; - - if (priceAttestation.status === PriceAttestationStatus.Trading) { - // 1 means trading - price = new Price({ - conf: priceAttestation.conf, - expo: priceAttestation.expo, - price: priceAttestation.price, - publishTime: priceAttestation.publishTime, - }); - } else { - price = new Price({ - conf: priceAttestation.prevConf, - expo: priceAttestation.expo, - price: priceAttestation.prevPrice, - publishTime: priceAttestation.prevPublishTime, - }); - - // emaPrice won't get updated if the status is unknown and hence it uses - // the previous publish time - emaPrice.publishTime = priceAttestation.prevPublishTime; - } - - return new PriceFeed({ - emaPrice, - id: priceAttestation.priceId, - price, - }); -} diff --git a/wormhole_attester/sdk/js/tsconfig.json b/wormhole_attester/sdk/js/tsconfig.json deleted file mode 100644 index be27cdde..00000000 --- a/wormhole_attester/sdk/js/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "include": ["src"], - "exclude": ["node_modules", "**/__tests__/*"], - "compilerOptions": { - "rootDir": "src/", - "outDir": "./lib" - } -} diff --git a/wormhole_attester/sdk/js/tslint.json b/wormhole_attester/sdk/js/tslint.json deleted file mode 100644 index d2b69dde..00000000 --- a/wormhole_attester/sdk/js/tslint.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": ["tslint:recommended", "tslint-config-prettier"], - "linterOptions": { - "exclude": ["src/proto/**"] - } -}