diff --git a/.github/actions/dockerize-neon-tests/action.yml b/.github/actions/dockerize-neon-tests/action.yml index 76b8b94868..677ab7560c 100644 --- a/.github/actions/dockerize-neon-tests/action.yml +++ b/.github/actions/dockerize-neon-tests/action.yml @@ -4,6 +4,9 @@ inputs: image_tag: # id of input description: 'neon tests image tag' required: true + image_name: + description: 'neon tests image name' + required: true oz_tag: description: 'tag name for oz tests' required: true @@ -14,6 +17,9 @@ inputs: docker_password: description: 'docker hub password' required: true + docker_hub_org_name: + description: 'docker hub organization name' + required: true runs: using: composite @@ -33,15 +39,20 @@ runs: - name: Build and push neon tests docker image id: docker_pipeline shell: bash + env: + DOCKER_HUB_ORG_NAME: ${{ inputs.docker_hub_org_name }} run: | - image_id="neonlabsorg/neon_tests" + image_name=${{ inputs.image_name }} delimeter=$(printf "%0.s-" {1..30}) - echo " ${delimeter} Build new docker image ${image_id} ${delimeter}" - docker build . --no-cache --tag ${image_id}:${{ inputs.image_tag }} --build-arg OZ_TAG='${{ inputs.oz_tag }}' --build-arg CONTRACTS_BRANCH="${{ steps.feature_branch.outputs.value || 'develop'}}" + echo " ${delimeter} Build new docker image ${image_name} ${delimeter}" + docker build . --no-cache --tag ${image_name}:${{ inputs.image_tag }} \ + --build-arg OZ_TAG='${{ inputs.oz_tag }}' \ + --build-arg CONTRACTS_BRANCH="${{ steps.feature_branch.outputs.value || 'develop'}}" \ + --build-arg DOCKER_HUB_ORG_NAME="${{env.DOCKER_HUB_ORG_NAME}}" if [[ "${{ steps.feature_branch.outputs.value }}" != '' ]]; then - docker tag ${image_id}:${{ inputs.image_tag }} ${image_id}:${{ steps.feature_branch.outputs.value }} + docker tag ${image_name}:${{ inputs.image_tag }} ${image_name}:${{ steps.feature_branch.outputs.value }} fi; echo "${delimeter} Login into Docker registry as ${{ inputs.docker_username }} ${delimeter}" echo "${{ inputs.docker_password }}" | docker login -u ${{ inputs.docker_username }} --password-stdin echo "${delimeter} Push image ${image_id} to Docker registry ${delimeter}" - docker push --all-tags ${image_id} + docker push --all-tags ${image_name} diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index 1902806046..2841172465 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -32,13 +32,20 @@ on: - 8 - 12 - auto + generate_cost_report: + type: boolean + default: false + required: false + description: "Flag defining whether cost report should be generated" env: NETWORK: ${{ github.event.inputs.network || 'terraform' }} NUMPROCESSES: ${{ github.event.inputs.numprocesses || 8 }} BUILD_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" SOLANA_URL: "${{ secrets.SOLANA_URL }}" FAUCET_URL: "${{ secrets.DEVNET_FAUCET_URL }}" - IMAGE: neonlabsorg/neon_tests + IMAGE: ${{ github.repository_owner }}/neon_tests + DOCKER_HUB_ORG_NAME: ${{ github.repository_owner }} + GENERATE_COST_REPORT: ${{ github.event.inputs.generate_cost_report || 'false' }} jobs: dockerize: if: ${{ github.ref_name != 'develop'}} @@ -51,6 +58,8 @@ jobs: image_tag: ${{ github.sha }} docker_username: ${{ secrets.DOCKER_USERNAME }} docker_password: ${{ secrets.DOCKER_PASSWORD }} + image_name: ${{ env.IMAGE }} + docker_hub_org_name: ${{ env.DOCKER_HUB_ORG_NAME }} prepare-env: runs-on: ubuntu-20.04 if: always() @@ -126,7 +135,7 @@ jobs: /bin/bash -c "export SOLANA_URL=http://${{ env.SOLANA_IP }}:8899 \ && export NEON_CORE_API_URL=http://${{ env.SOLANA_IP }}:8085/api \ && export NEON_CORE_API_RPC_URL=http://${{ env.SOLANA_IP }}:3100 \ - && python3 ./clickfile.py run evm --numprocesses 8 --keep-error-log" + && python3 ./clickfile.py run evm --numprocesses 8 --network terraform --keep-error-log" - name: Set failed test group to evm if: failure() run: echo "FAILED_TEST_GROUP=evm" >> $GITHUB_ENV @@ -134,12 +143,35 @@ jobs: timeout-minutes: 60 id: basic run: | - docker exec -i ${{ env.CONTAINER }} \ - /bin/bash -c "export DEVNET_FAUCET_URL=${{ env.FAUCET_URL }} && \ - python3 ./clickfile.py run basic --network ${{ env.NETWORK }} --numprocesses ${{ env.NUMPROCESSES }}" + CMD="python3 ./clickfile.py run basic --network ${{ env.NETWORK }} --numprocesses ${{ env.NUMPROCESSES }}" + + if [[ "${{ env.GENERATE_COST_REPORT }}" == "true" ]]; then + CMD="$CMD --cost_reports_dir reports/cost_reports" + fi + + docker exec -i ${{ env.CONTAINER }} /bin/bash -c "export DEVNET_FAUCET_URL=${{ env.FAUCET_URL }} && $CMD" + - name: Set failed test group to basic if: failure() run: echo "FAILED_TEST_GROUP=basic" >> $GITHUB_ENV + - name: Copy cost reports from container + if: ${{ env.GENERATE_COST_REPORT == 'true' }} + run: | + mkdir -p ./reports/cost_reports/ && \ + docker cp ${{ env.CONTAINER }}:/opt/neon-tests/reports/cost_reports/. ./reports/cost_reports/ + - name: Upload cost reports as artifacts + if: ${{ env.GENERATE_COST_REPORT == 'true' }} + uses: actions/upload-artifact@v4 + with: + name: cost-reports + path: reports/cost_reports/**.json + - name: Save Cost Reports to cost_reports.md and echo to Summary + if: ${{ env.GENERATE_COST_REPORT == 'true' }} + run: | + docker exec -i -e NETWORK=${{ env.NETWORK }} ${{ env.CONTAINER }} \ + python3 ./clickfile.py dapps save_dapps_cost_report_to_md \ + --directory reports/cost_reports && \ + docker exec -i ${{ env.CONTAINER }} cat cost_reports.md >> $GITHUB_STEP_SUMMARY - name: "Generate allure report" if: always() uses: ./.github/actions/generate-allure-report @@ -154,7 +186,7 @@ jobs: run: | docker exec -i ${{ env.CONTAINER }} \ python3 ./clickfile.py send-notification -u ${{ secrets.SLACK_QA_CHANNEL_URL }} \ - -b ${{ env.BUILD_URL }} -n ${{ env.NETWORK }} --test-group ${{ env.FAILED_TEST_GROUP }} + -b ${{ env.BUILD_URL }} --network ${{ env.NETWORK }} --test-group ${{ env.FAILED_TEST_GROUP }} - name: Remove docker container if: always() run: docker rm -f ${{ env.CONTAINER }} @@ -178,4 +210,3 @@ jobs: uses: ./.github/actions/destroy-tf-stand with: ci_stands_key_hcloud: ${{ secrets.CI_STANDS_KEY_HCLOUD }} - diff --git a/.github/workflows/compiler_compatibility.yml b/.github/workflows/compiler_compatibility.yml index 5cd63f4847..d26b4b8865 100644 --- a/.github/workflows/compiler_compatibility.yml +++ b/.github/workflows/compiler_compatibility.yml @@ -26,8 +26,9 @@ env: FAUCET_URL: "${{ secrets.DEVNET_FAUCET_URL }}" SOLANA_URL: "${{ secrets.SOLANA_URL }}" NUMPROCESSES: 2 - IMAGE: neonlabsorg/neon_tests + IMAGE: ${{ github.repository_owner }}/neon_tests CONTAINER: compilers-${{ github.run_id }} + DOCKER_HUB_ORG_NAME: ${{ github.repository_owner }} jobs: dockerize: if: ${{ github.ref_name != 'develop'}} @@ -41,7 +42,9 @@ jobs: image_tag: ${{ github.sha }} docker_username: ${{ secrets.DOCKER_USERNAME }} docker_password: ${{ secrets.DOCKER_PASSWORD }} - + image_name: ${{ env.IMAGE }} + docker_hub_org_name: ${{ env.DOCKER_HUB_ORG_NAME }} + prepare-env: runs-on: ubuntu-20.04 if: always() @@ -120,7 +123,7 @@ jobs: run: | docker exec -i ${{ env.CONTAINER }} \ python3 ./clickfile.py send-notification -u ${{ secrets.SLACK_QA_CHANNEL_URL }} \ - -b ${{ env.BUILD_URL }} -n ${{ env.NETWORK }} --test-group compiler_compatibility + -b ${{ env.BUILD_URL }} --network ${{ env.NETWORK }} --test-group compiler_compatibility - name: Remove docker container if: always() run: docker rm -f ${{ env.CONTAINER }} diff --git a/.github/workflows/dapps.yml b/.github/workflows/dapps.yml index 6b7c10e6dc..9bdf85fa34 100644 --- a/.github/workflows/dapps.yml +++ b/.github/workflows/dapps.yml @@ -118,6 +118,7 @@ jobs: proxy_ip: ${{ needs.prepare.outputs.proxy_ip }} solana_ip: ${{ needs.prepare.outputs.solana_ip }} external_call: false + repo: "tests" notify: runs-on: ubuntu-20.04 diff --git a/.github/workflows/dapps_reusable.yml b/.github/workflows/dapps_reusable.yml index 0261deffe5..5dd8239df5 100644 --- a/.github/workflows/dapps_reusable.yml +++ b/.github/workflows/dapps_reusable.yml @@ -4,52 +4,86 @@ run-name: dApps reusable (${{ inputs.dapps }}, ${{ inputs.network }}) on: workflow_call: inputs: - network: - type: string - default: custom - required: true - description: "Stand name" - dapps: - type: string - required: true - description: "List of dapps separated by commas (if empty, all will be run)" - proxy_url: - type: string - required: true - description: "proxy_url (fill only for custom stand)" - solana_url: - type: string - required: true - description: "solana_url (fill only for custom stand)" - faucet_url: - type: string - required: true - description: "faucet_url (fill only for custom stand)" - network_id: - type: string - required: true - description: "network_id (fill only for custom stand)" - pr_url_for_report: - type: string - required: true - description: "Url to send the report as comment for PR" - proxy_ip: - type: string - required: true - solana_ip: - type: string - required: true - external_call: - description: 'To distinguish calls from other repositories and the current one' - type: boolean - required: false - default: true + network: + type: string + default: custom + required: true + description: "Stand name" + dapps: + type: string + required: true + description: "List of dapps separated by commas (if empty, all will be run)" + proxy_url: + type: string + required: true + description: "proxy_url (fill only for custom stand)" + solana_url: + type: string + required: true + description: "solana_url (fill only for custom stand)" + faucet_url: + type: string + required: true + description: "faucet_url (fill only for custom stand)" + network_id: + type: string + required: true + description: "network_id (fill only for custom stand)" + pr_url_for_report: + type: string + required: true + description: "Url to send the report as comment for PR" + proxy_ip: + type: string + required: true + solana_ip: + type: string + required: true + external_call: + description: 'To distinguish calls from other repositories and the current one' + type: boolean + required: false + default: true + repo: + type: string + description: "Repository type: tests | proxy | evm" + required: false + evm_tag: + type: string + required: false + description: "Neon EVM Docker tag" + evm_sha_tag: + type: string + required: false + description: "Neon EVM GitHub commit SHA" + proxy_tag: + type: string + required: false + description: "Proxy Docker tag" + proxy_sha_tag: + type: string + required: false + description: "Proxy GitHub commit SHA" + proxy_pr_version_branch: + type: string + required: false + description: "Proxy version branch" + evm_pr_version_branch: + type: string + required: false + description: "Neon EVM version branch" + env: NETWORK: ${{ inputs.network }} DUMP_ENVS: True DEVNET_FAUCET_URL: ${{ secrets.DEVNET_FAUCET_URL }} DEVNET_SOLANA_URL: ${{ secrets.SOLANA_URL }} + TEST_RESULTS_DB_HOST: ${{ secrets.TEST_RESULTS_DB_HOST }} + TEST_RESULTS_DB_PORT: ${{ secrets.TEST_RESULTS_DB_PORT }} + TEST_RESULTS_DB_NAME: ${{ secrets.TEST_RESULTS_DB_NAME }} + TEST_RESULTS_DB_USER: ${{ secrets.TEST_RESULTS_DB_USER }} + TEST_RESULTS_DB_PASSWORD: ${{ secrets.TEST_RESULTS_DB_PASSWORD }} jobs: uniswap-v2: @@ -67,7 +101,7 @@ jobs: - if: ${{ inputs.external_call }} uses: actions/checkout@v4 with: - repository: neonlabsorg/neon-tests + repository: ${{ github.repository_owner }}/neon-tests ref: develop - name: Install python requirements id: requirements @@ -84,7 +118,7 @@ jobs: id: uniswap-v2 env: ACCOUNTS: ${{ env.ACCOUNTS }} - IMAGE: neonlabsorg/uniswap-v2:latest + IMAGE: ${{ github.repository_owner }}/uniswap-v2:latest run: | test -t 1 && USE_TTY="-t" docker pull ${{ env.IMAGE }} @@ -119,7 +153,7 @@ jobs: - if: ${{ inputs.external_call }} uses: actions/checkout@v4 with: - repository: neonlabsorg/neon-tests + repository: ${{ github.repository_owner }}/neon-tests ref: develop - name: Install python requirements id: requirements @@ -136,7 +170,7 @@ jobs: timeout-minutes: 30 env: ACCOUNTS: ${{ env.ACCOUNTS }} - IMAGE: neonlabsorg/uniswap-v3:latest + IMAGE: ${{ github.repository_owner }}/uniswap-v3:latest run: | test -t 1 && USE_TTY="-t" docker pull ${{ env.IMAGE }} @@ -170,7 +204,7 @@ jobs: - if: ${{ inputs.external_call }} uses: actions/checkout@v4 with: - repository: neonlabsorg/neon-tests + repository: ${{ github.repository_owner }}/neon-tests ref: develop - name: Install python requirements id: requirements @@ -187,7 +221,7 @@ jobs: timeout-minutes: 60 env: ACCOUNTS: ${{ env.ACCOUNTS }} - IMAGE: neonlabsorg/saddle_tests:latest + IMAGE: ${{ github.repository_owner }}/saddle_tests:latest run: | docker pull ${{ env.IMAGE }} docker run -i -d --name=saddle-${{ github.run_number }} ${{ env.IMAGE }} /bin/bash @@ -222,7 +256,7 @@ jobs: - if: ${{ inputs.external_call }} uses: actions/checkout@v4 with: - repository: neonlabsorg/neon-tests + repository: ${{ github.repository_owner }}/neon-tests ref: develop - name: Install python requirements id: requirements @@ -239,7 +273,7 @@ jobs: timeout-minutes: 60 env: ACCOUNTS: ${{ env.ACCOUNTS }} - IMAGE: neonlabsorg/aave_tests:latest + IMAGE: ${{ github.repository_owner }}/aave_tests:latest run: | docker pull ${{ env.IMAGE }} docker run -i -d --name=aave-v2-${{ github.run_number }} ${{ env.IMAGE }} /bin/bash @@ -268,7 +302,7 @@ jobs: - if: ${{ inputs.external_call }} uses: actions/checkout@v4 with: - repository: neonlabsorg/neon-tests + repository: ${{ github.repository_owner }}/neon-tests ref: develop - name: Install python requirements id: requirements @@ -285,7 +319,7 @@ jobs: timeout-minutes: 120 env: ACCOUNTS: ${{ env.ACCOUNTS }} - IMAGE: neonlabsorg/aave-v3-core:latest + IMAGE: ${{ github.repository_owner }}/aave-v3-core:latest run: | docker pull ${{ env.IMAGE }} docker run -i -d --name=aave-v3-${{ github.run_number }} ${{ env.IMAGE }} /bin/bash @@ -314,7 +348,7 @@ jobs: - if: ${{ inputs.external_call }} uses: actions/checkout@v4 with: - repository: neonlabsorg/neon-tests + repository: ${{ github.repository_owner }}/neon-tests ref: develop - name: Install python requirements id: requirements @@ -330,7 +364,7 @@ jobs: timeout-minutes: 60 env: ACCOUNTS: ${{ env.ACCOUNTS }} - IMAGE: neonlabsorg/curve_tests:latest + IMAGE: ${{ github.repository_owner }}/curve_tests:latest run: | docker pull ${{ env.IMAGE }} docker run -i -d --name curve-${{ github.run_number }} ${{ env.IMAGE }} /bin/bash @@ -365,7 +399,7 @@ jobs: - if: ${{ inputs.external_call }} uses: actions/checkout@v4 with: - repository: neonlabsorg/neon-tests + repository: ${{ github.repository_owner }}/neon-tests ref: develop - name: Install python requirements id: requirements @@ -397,7 +431,7 @@ jobs: - if: ${{ inputs.external_call }} uses: actions/checkout@v4 with: - repository: neonlabsorg/neon-tests + repository: ${{ github.repository_owner }}/neon-tests ref: develop - name: Install python requirements id: requirements @@ -413,7 +447,7 @@ jobs: timeout-minutes: 30 env: ACCOUNTS: ${{ env.ACCOUNTS }} - IMAGE: neonlabsorg/compound_tests:latest + IMAGE: ${{ github.repository_owner }}/compound_tests:latest run: | docker pull ${{ env.IMAGE }} docker run -i -d --name compound-${{ github.run_number }} ${{ env.IMAGE }} /bin/bash @@ -449,7 +483,7 @@ jobs: - if: ${{ inputs.external_call }} uses: actions/checkout@v4 with: - repository: neonlabsorg/neon-tests + repository: ${{ github.repository_owner }}/neon-tests ref: develop - name: Install python requirements id: requirements @@ -466,7 +500,7 @@ jobs: timeout-minutes: 60 env: ACCOUNTS: ${{ env.ACCOUNTS }} - IMAGE: neonlabsorg/robonomics_tests:latest + IMAGE: ${{ github.repository_owner }}/robonomics_tests:latest run: | docker pull ${{ env.IMAGE }} docker run -i -d --entrypoint "sleep" --name=robonomics-${{ github.run_number }} ${{ env.IMAGE }} infinity @@ -498,7 +532,7 @@ jobs: - if: ${{ inputs.external_call }} uses: actions/checkout@v4 with: - repository: neonlabsorg/neon-tests + repository: ${{ github.repository_owner }}/neon-tests ref: develop - name: Install python requirements id: requirements @@ -514,7 +548,7 @@ jobs: timeout-minutes: 60 env: ACCOUNTS: ${{ env.ACCOUNTS }} - IMAGE: neonlabsorg/yearn_tests:latest + IMAGE: ${{ github.repository_owner }}/yearn_tests:latest run: | docker pull ${{ env.IMAGE }} docker run -i -d --name yearn-${{ github.run_number }} ${{ env.IMAGE }} /bin/bash @@ -550,7 +584,7 @@ jobs: - if: ${{ inputs.external_call }} uses: actions/checkout@v4 with: - repository: neonlabsorg/neon-tests + repository: ${{ github.repository_owner }}/neon-tests ref: develop - name: Install python requirements id: requirements @@ -567,7 +601,7 @@ jobs: timeout-minutes: 60 env: ACCOUNTS: ${{ env.ACCOUNTS }} - IMAGE: neonlabsorg/pancake:latest + IMAGE: ${{ github.repository_owner }}/pancake:latest run: | test -t 1 && USE_TTY="-t" docker pull ${{ env.IMAGE }} @@ -602,7 +636,7 @@ jobs: - if: ${{ inputs.external_call }} uses: actions/checkout@v4 with: - repository: neonlabsorg/neon-tests + repository: ${{ github.repository_owner }}/neon-tests ref: develop - name: Install python requirements id: requirements @@ -662,8 +696,78 @@ jobs: with: name: pancake-report path: reports/ - - name: "Swap report" + - name: "Save Cost Report to DB" + id: save-cost-report-to-db + if: ${{ inputs.repo == 'evm' || inputs.repo == 'proxy' }} + run: | + python3 ./clickfile.py dapps save_dapps_cost_report_to_db \ + --directory=reports \ + --repo="${{inputs.repo}}" \ + --evm_tag="${{inputs.evm_tag}}" \ + --proxy_tag="${{inputs.proxy_tag}}" \ + --evm_commit_sha="${{ inputs.evm_sha_tag }}" \ + --proxy_commit_sha="${{ inputs.proxy_sha_tag }}" + - name: "Define if reports should be compared and set to outputs.compare" + id: set-do-compare-reports + run: | + if [[ + ('${{ inputs.repo }}' == 'proxy' && '${{ inputs.proxy_tag }}' != 'latest' && ! '${{ inputs.proxy_tag }}' =~ ^[vt][0-9]+\.[0-9]+\.x$) || + ('${{ inputs.repo }}' == 'evm' && '${{ inputs.evm_tag }}' != 'latest' && ! '${{ inputs.evm_tag }}' =~ ^[vt][0-9]+\.[0-9]+\.x$) + ]]; then + echo "compare=true" >> "$GITHUB_OUTPUT" + else + echo "compare=false" >> "$GITHUB_OUTPUT" + fi + - name: "Compare Cost Reports and save to cost_reports.pdf" + id: compare-cost-reports + if: ${{ steps.set-do-compare-reports.outputs.compare == 'true' && steps.save-cost-report-to-db.outcome == 'success' }} + run: | + python3 ./clickfile.py dapps compare_dapp_cost_reports \ + --repo "${{inputs.repo}}" \ + --evm_tag="${{inputs.evm_tag}}" \ + --proxy_tag="${{inputs.proxy_tag}}" \ + --version_branch "${{ inputs.repo == 'evm' && inputs.evm_pr_version_branch || inputs.proxy_pr_version_branch }}" \ + --history_depth_limit 10 + - name: "Upload cost_reports.pdf to artifacts" + id: upload-cost-report-pdf + if: ${{ steps.compare-cost-reports.outcome == 'success' }} + uses: actions/upload-artifact@v4 + with: + name: cost_reports + path: cost_reports.pdf + - name: "Get the cost_reports.zip download url and save it to cost_reports.md" + if: ${{ steps.upload-cost-report-pdf.outcome == 'success' }} + id: save-cost-reports-zip-url + env: + GITHUB_TOKEN: ${{ secrets.GHTOKEN }} + run: | + ARTIFACT_NAME="cost_reports" + + ARTIFACT_INFO=$(gh api -X GET "/repos/${{ github.repository }}/actions/artifacts" | jq -r ".artifacts[] | select(.name==\"${ARTIFACT_NAME}\") | {id: .id, run_id: .workflow_run.id}") + echo "ARTIFACT_INFO: ${ARTIFACT_INFO}" + + ARTIFACT_ID=$(echo "${ARTIFACT_INFO}" | jq -r '.id' | head -n 1) + echo "ARTIFACT_ID: ${ARTIFACT_ID}" + + RUN_ID=$(echo "${ARTIFACT_INFO}" | jq -r '.run_id' | head -n 1) + echo "RUN_ID: ${RUN_ID}" + + ARTIFACT_URL="https://github.com/${{ github.repository }}/actions/runs/${RUN_ID}/artifacts/${ARTIFACT_ID}" + echo "🔗[Cost report](${ARTIFACT_URL})" > cost_reports.md + - name: "Save Cost Reports as markdown tables to cost_reports.md if Cost Reports were not compared" + id: dump-cost-reports-to-markdown + if: ${{ steps.set-do-compare-reports.outputs.compare == 'false' || steps.save-cost-reports-zip-url.outcome != 'success' }} + run: | + python3 ./clickfile.py dapps save_dapps_cost_report_to_md \ + --directory reports + - name: "Add cost_reports.zip download link or Cost Reports markdown tables to summary" + if: ${{ steps.dump-cost-reports-to-markdown.outcome == 'success' || steps.save-cost-reports-zip-url.outcome == 'success' }} + run: | + cat cost_reports.md >> $GITHUB_STEP_SUMMARY + - name: "Add PR comment" + if: ${{ inputs.pr_url_for_report != '' }} run: | - python3 ./clickfile.py dapps report --directory=reports \ - --pr_url_for_report=${{ inputs.pr_url_for_report }} \ - --token=${{secrets.GHTOKEN}} + python3 ./clickfile.py dapps add_pr_comment \ + --pr_url_for_report="${{ inputs.pr_url_for_report }}" \ + --token="${{ secrets.GHTOKEN }}" \ + --md_file cost_reports.md diff --git a/.github/workflows/dockerize_neon_tests.yml b/.github/workflows/dockerize_neon_tests.yml index 207acaf741..37830f4329 100644 --- a/.github/workflows/dockerize_neon_tests.yml +++ b/.github/workflows/dockerize_neon_tests.yml @@ -44,3 +44,5 @@ jobs: docker_username: ${{ secrets.DOCKER_USERNAME }} docker_password: ${{ secrets.DOCKER_PASSWORD }} oz_tag: ${{ steps.define-env.outputs.oz_tag }} + image_name: ${{ github.repository_owner }}/neon_tests + docker_hub_org_name: ${{ github.repository_owner }} diff --git a/.github/workflows/economy.yml b/.github/workflows/economy.yml index a87dd2dec8..826559a6ce 100644 --- a/.github/workflows/economy.yml +++ b/.github/workflows/economy.yml @@ -17,8 +17,9 @@ on: env: NETWORK: ${{ github.event.inputs.network || 'terraform' }} BUILD_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - IMAGE: neonlabsorg/neon_tests + IMAGE: ${{ github.repository_owner }}/neon_tests CONTAINER: "economy-${{ github.run_id }}" + DOCKER_HUB_ORG_NAME: ${{ github.repository_owner }} jobs: dockerize: if: ${{ github.ref_name != 'develop'}} @@ -32,6 +33,8 @@ jobs: image_tag: ${{ github.sha }} docker_username: ${{ secrets.DOCKER_USERNAME }} docker_password: ${{ secrets.DOCKER_PASSWORD }} + image_name: ${{ env.IMAGE }} + docker_hub_org_name: ${{ env.DOCKER_HUB_ORG_NAME }} prepare-env: runs-on: ubuntu-20.04 if: always() diff --git a/.github/workflows/graph.yml b/.github/workflows/graph.yml index 1f327c3389..1e139927b0 100644 --- a/.github/workflows/graph.yml +++ b/.github/workflows/graph.yml @@ -29,6 +29,6 @@ jobs: uses: peter-evans/repository-dispatch@v2 with: token: ${{ secrets.GHTOKEN }} - repository: neonlabsorg/graph-node + repository: ${{ github.repository_owner }}/graph-node event-type: integration-tests client-payload: '{"accounts": "${{ env.ACCOUNTS }}"}' diff --git a/.github/workflows/old_openzeppelin.yml b/.github/workflows/old_openzeppelin.yml index 12528d1f81..ee5a8441b2 100644 --- a/.github/workflows/old_openzeppelin.yml +++ b/.github/workflows/old_openzeppelin.yml @@ -31,7 +31,7 @@ env: JOBS_NUMBER: "8" NETWORK: night-stand RUNNER: neon-hosted - IMAGE: neonlabsorg/neon_tests + IMAGE: ${{ github.repository_owner }}/neon_tests BUILD_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" jobs: diff --git a/.github/workflows/openzeppelin.yml b/.github/workflows/openzeppelin.yml index 2b2382f760..e3facb938e 100644 --- a/.github/workflows/openzeppelin.yml +++ b/.github/workflows/openzeppelin.yml @@ -27,10 +27,10 @@ on: env: JOBS_NUMBER: ${{ github.event.inputs.jobsNumber || '8' }} NETWORK: ${{ github.event.inputs.network || 'terraform' }} - IMAGE: neonlabsorg/neon_tests + IMAGE: ${{ github.repository_owner }}/neon_tests CONTAINER: oz-${{ github.run_id }} BUILD_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - + DOCKER_HUB_ORG_NAME: ${{ github.repository_owner }} jobs: dockerize: if: github.ref_name != 'develop' || github.event.inputs.oz_tag !='' @@ -44,6 +44,8 @@ jobs: docker_username: ${{ secrets.DOCKER_USERNAME }} docker_password: ${{ secrets.DOCKER_PASSWORD }} oz_tag: ${{ github.event.inputs.oz_tag || 'latest' }} + image_name: ${{ env.IMAGE }} + docker_hub_org_name: ${{ github.repository_owner }} prepare-env: runs-on: ubuntu-20.04 steps: diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 71b253bc63..e78ff191a6 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -6,12 +6,12 @@ on: - cron: "0 1 * * 5" workflow_dispatch: env: - IMAGE: neonlabsorg/neon_tests + IMAGE: ${{ github.repository_owner }}/neon_tests CONTAINER: services-${{ github.run_id }} FAUCET_URL: "${{ secrets.DEVNET_FAUCET_URL }}" SOLANA_URL: "${{ secrets.SOLANA_URL }}" BUILD_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - + DOCKER_HUB_ORG_NAME: ${{ github.repository_owner }} jobs: dockerize: if: ${{ github.ref_name != 'develop'}} @@ -25,7 +25,8 @@ jobs: image_tag: ${{ github.sha }} docker_username: ${{ secrets.DOCKER_USERNAME }} docker_password: ${{ secrets.DOCKER_PASSWORD }} - + image_name: ${{ env.IMAGE }} + docker_hub_org_name: ${{ env.DOCKER_HUB_ORG_NAME }} playwright: name: "Playwright UI tests" needs: diff --git a/.github/workflows/services.yml b/.github/workflows/services.yml index 562dd8fdcc..e7197c5906 100644 --- a/.github/workflows/services.yml +++ b/.github/workflows/services.yml @@ -33,8 +33,9 @@ env: FAUCET_URL: "${{ secrets.DEVNET_FAUCET_URL }}" SOLANA_URL: "${{ secrets.SOLANA_URL }}" NUMPROCESSES: 4 - IMAGE: neonlabsorg/neon_tests + IMAGE: ${{ github.repository_owner }}/neon_tests CONTAINER: services-${{ github.run_id }} + DOCKER_HUB_ORG_NAME: ${{ github.repository_owner }} jobs: dockerize: if: ${{ github.ref_name != 'develop'}} @@ -48,7 +49,8 @@ jobs: image_tag: ${{ github.sha }} docker_username: ${{ secrets.DOCKER_USERNAME }} docker_password: ${{ secrets.DOCKER_PASSWORD }} - + image_name: ${{ env.IMAGE }} + docker_hub_org_name: ${{ env.DOCKER_HUB_ORG_NAME }} tests: name: "Services tests" needs: diff --git a/.gitignore b/.gitignore index 4aa28c3a50..c6c1d0f054 100644 --- a/.gitignore +++ b/.gitignore @@ -248,3 +248,5 @@ reports uniswap_contracts.json *.url *.lock +node_modules +loadtesting/k6/data \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index db268979e6..bff8423e8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ARG OZ_TAG=latest - -FROM neonlabsorg/openzeppelin-contracts:$OZ_TAG as oz-contracts +ARG DOCKER_HUB_ORG_NAME +FROM $DOCKER_HUB_ORG_NAME/openzeppelin-contracts:$OZ_TAG as oz-contracts FROM ubuntu:20.04 ENV TZ=Europe/Moscow @@ -63,6 +63,8 @@ ADD ./ /opt/neon-tests # Install all requirements RUN python3 ./clickfile.py requirements -d all +ARG DOCKER_HUB_ORG_NAME +ENV DOCKER_HUB_ORG_NAME=${DOCKER_HUB_ORG_NAME} ARG CONTRACTS_BRANCH RUN python3 ./clickfile.py update-contracts --branch ${CONTRACTS_BRANCH} diff --git a/clickfile.py b/clickfile.py index 00ce3ad57c..eae3b54eb1 100755 --- a/clickfile.py +++ b/clickfile.py @@ -18,9 +18,14 @@ import pytest +from deploy.cli.cost_report import prepare_report_data, report_data_to_markdown +from deploy.test_results_db.db_handler import PostgresTestResultsHandler +from deploy.test_results_db.test_results_handler import TestResultsHandler from utils.error_log import error_log +from utils.faucet import Faucet from utils.slack_notification import SlackNotification -from utils.types import TestGroup +from utils.types import TestGroup, RepoType +from utils.accounts import EthAccounts try: import click @@ -42,7 +47,8 @@ from utils import cloud from utils.operator import Operator from utils.web3client import NeonChainWeb3Client - from utils.prices import get_sol_price + from utils.k6_helpers import k6_prepare_accounts + from utils.prices import get_sol_price_with_retry from utils.helpers import wait_condition from utils.apiclient import JsonRPCSession except ImportError: @@ -56,9 +62,7 @@ } SRC_ALLURE_CATEGORIES = Path("./allure/categories.json") - DST_ALLURE_CATEGORIES = Path("./allure-results/categories.json") - DST_ALLURE_ENVIRONMENT = Path("./allure-results/environment.properties") BASE_EXTENSIONS_TPL_DATA = "ui/extensions/data" @@ -69,12 +73,14 @@ HOME_DIR = Path(__file__).absolute().parent OZ_BALANCES = "./compatibility/results/oz_balance.json" -NEON_EVM_GITHUB_URL = "https://api.github.com/repos/neonlabsorg/neon-evm" +DOCKER_HUB_ORG_NAME = os.environ.get("DOCKER_HUB_ORG_NAME") +NEON_EVM_GITHUB_URL = f"https://api.github.com/repos/{DOCKER_HUB_ORG_NAME}/neon-evm" HOODIES_CHAINLINK_GITHUB_URL = "https://github.com/hoodieshq/chainlink-neon" -PROXY_GITHUB_URL = "https://api.github.com/repos/neonlabsorg/neon-proxy.py" -FAUCET_GITHUB_URL = "https://api.github.com/repos/neonlabsorg/neon-faucet" +PROXY_GITHUB_URL = f"https://api.github.com/repos/{DOCKER_HUB_ORG_NAME}/neon-proxy.py" +FAUCET_GITHUB_URL = f"https://api.github.com/repos/{DOCKER_HUB_ORG_NAME}/neon-faucet" EXTERNAL_CONTRACT_PATH = Path.cwd() / "contracts" / "external" VERSION_BRANCH_TEMPLATE = r"[vt]{1}\d{1,2}\.\d{1,2}\.x.*" +GITHUB_TAG_PATTERN = re.compile(r'^[vt]\d{1,2}\.\d{1,2}\.\d{1,2}$') TEST_GROUPS: tp.Tuple[TestGroup, ...] = tp.get_args(TestGroup) @@ -92,6 +98,7 @@ class EnvName(str, enum.Enum): GETH = "geth" TRACER_CI = "tracer_ci" CUSTOM = "custom" + DOCKER_NET = "docker_net" def green(s): @@ -170,7 +177,7 @@ def float_2_str(d): after = get_tokens_balances(op) profitability = dict( neon=round(float(after["neon"] - pre["neon"]) * 0.25, 2), - sol=round((float(pre["sol"] - after["sol"])) * get_sol_price(), 2), + sol=round((float(pre["sol"] - after["sol"])) * get_sol_price_with_retry(), 2), ) path = Path(OZ_BALANCES) path.absolute().parent.mkdir(parents=True, exist_ok=True) @@ -450,11 +457,11 @@ def get_evm_pinned_version(branch): resp = requests.get(f"{PROXY_GITHUB_URL}/contents/.github/workflows/pipeline.yml?ref={branch}") if resp.status_code != 200: - click.echo(f"Can't get pipeline file for branch {branch}: {resp.text}") + click.echo(f"Can't get pipeline file for {PROXY_GITHUB_URL}: {resp.text}") raise click.ClickException(f"Can't get pipeline file for branch {branch}") info = resp.json() pipeline_file = yaml.safe_load(requests.get(info["download_url"]).text) - tag = pipeline_file["env"]["NEON_EVM_TAG"] + tag = pipeline_file["env"]["DEFAULT_NEON_EVM_TAG"] if tag == "latest": return "develop" if re.match(r"[vt]{1}\d{1,2}\.\d{1,2}.*", tag) is not None: @@ -516,20 +523,21 @@ def update_contracts(branch): update_contracts_from_git(HOODIES_CHAINLINK_GITHUB_URL, "hoodies_chainlink", "main") update_contracts_from_git( - "https://github.com/neonlabsorg/neon-contracts.git", "neon-contracts", "main", update_npm=False + f"https://github.com/{DOCKER_HUB_ORG_NAME}/neon-contracts.git", "neon-contracts", "main", update_npm=False ) subprocess.check_call(f'npm ci --prefix {EXTERNAL_CONTRACT_PATH / "neon-contracts" / "ERC20ForSPL"}', shell=True) @cli.command(help="Run any type of tests") -@click.option("-n", "--network", default=EnvName.NIGHT_STAND.value, type=click.Choice(EnvName), +@click.option("-n", "--network", type=click.Choice(EnvName), help="In which stand run tests") @click.option("-j", "--jobs", default=8, help="Number of parallel jobs (for openzeppelin)") @click.option("-p", "--numprocesses", help="Number of parallel jobs for basic tests") @click.option("-a", "--amount", default=20000, help="Requested amount from faucet") @click.option("-u", "--users", default=8, help="Accounts numbers used in OZ tests") -@click.option("-c", "--case", default='', type=str, help="Specific test case name pattern to run") +@click.option("-c", "--case", default="", type=str, help="Specific test case name pattern to run") @click.option("--marker", help="Run tests by mark") +@click.option("--cost_reports_dir", default="", help="Directory where CostReports will be created") @click.option( "--ui-item", default="all", @@ -559,6 +567,7 @@ def run( case, keep_error_log: bool, marker: str, + cost_reports_dir: str, ): if not network and name == "ui": network = "devnet" @@ -605,16 +614,27 @@ def run( raise click.ClickException("Unknown test name") if name == "tracer": - assert wait_for_tracer_service(network) + if network != "geth": + assert wait_for_tracer_service(network) + + if case: + if " " in case: + command += f' -vk "{case}"' + else: + command += f" -vk {case}" - if case != '': - command += " -vk {}".format(case) if marker: - command += f' -m {marker}' + if " " in marker: + command += f' -m "{marker}"' + else: + command += f" -m {marker}" command += f" -s --network={network} --make-report --test-group {name}" if keep_error_log: command += " --keep-error-log" + if cost_reports_dir: + command += f" --cost_reports_dir {cost_reports_dir}" + args = command.split()[1:] exit_code = int(pytest.main(args=args)) if name != "ui": @@ -1056,19 +1076,211 @@ def dapps(): pass -@dapps.command("report", help="Print dapps report (from .json files)") +@dapps.command("save_dapps_cost_report_to_db", help="Save dApps Cost Report to db") +@click.option("-d", "--directory", default="reports", help="Directory with reports") +@click.option("--repo", type=click.Choice(tp.get_args(RepoType)), required=True) +@click.option("--evm_tag", required=True) +@click.option("--proxy_tag", required=True) +@click.option("--evm_commit_sha", required=True) +@click.option("--proxy_commit_sha", required=True) +def save_dapps_cost_report_to_db( + directory: str, + repo: RepoType, + evm_tag: str, + proxy_tag: str, + evm_commit_sha: str, + proxy_commit_sha: str, +): + tag = evm_tag if repo == "evm" else proxy_tag + + report_data = prepare_report_data(directory) + db = PostgresTestResultsHandler() + + # define if previous similar reports should be deleted + is_neon_evm_tag_version_branch = bool(re.fullmatch(VERSION_BRANCH_TEMPLATE, evm_tag)) + is_proxy_tag_version_branch = bool(re.fullmatch(VERSION_BRANCH_TEMPLATE, proxy_tag)) + + if evm_tag == proxy_tag == "latest": + click.echo("This is a merge to develop") + do_delete = False + elif is_neon_evm_tag_version_branch and is_proxy_tag_version_branch and evm_tag == proxy_tag: + click.echo(f"This is a merge to version branch {evm_tag}") + do_delete = False + else: + do_delete = True + + # delete them if needed + if do_delete: + report_ids_old = db.get_cost_report_ids(repo=repo, tag=tag) + if report_ids_old: + db.delete_data_by_report_ids(report_ids=report_ids_old) + db.delete_reports(report_ids=report_ids_old) + + # save the new report + report_id_new = db.save_cost_report( + repo=repo, + neon_evm_tag=evm_tag, + proxy_tag=proxy_tag, + evm_commit_sha=evm_commit_sha, + proxy_commit_sha=proxy_commit_sha, + ) + db.save_cost_report_data(report_data=report_data, cost_report_id=report_id_new) + + +@dapps.command("save_dapps_cost_report_to_md", help="Save dApps Cost Report to cost_reports.md") @click.option("-d", "--directory", default="reports", help="Directory with reports") +def save_dapps_cost_report_to_md(directory: str): + report_data = prepare_report_data(directory) + + # Add 'gas_used_%' column after 'gas_used' + report_data.insert( + report_data.columns.get_loc("gas_used") + 1, + "gas_used_%", + (report_data["gas_used"] / report_data["gas_estimated"]) * 100, + ) + report_data["gas_used_%"] = report_data["gas_used_%"].round(2) + + # Dump report_data DataFrame to markdown, grouped by the dApp + report_as_markdown_table = report_data_to_markdown(df=report_data) + Path("cost_reports.md").write_text(report_as_markdown_table) + + +@dapps.command("compare_dapp_cost_reports", help="Compare dApp results") +@click.option("--repo", type=click.Choice(tp.get_args(RepoType)), required=True) +@click.option("--evm_tag", required=True) +@click.option("--proxy_tag", required=True) +@click.option("--version_branch", required=True) +@click.option("--history_depth_limit", type=int, help="How many runs to include into statistical analysis") +def compare_dapp_results( + repo: RepoType, + evm_tag: str, + proxy_tag: str, + version_branch: str, + history_depth_limit: int, +): + """ + >>> compared_service_tag + v1.1.1 - GitHub tag + feature_foo - feature branch + + >>> other_service_tag + v1.1.x - version branch + feature_foo - feature branch + latest - develop branch + """ + click.echo(f"compare_dapp_results: {locals()}") + + compared_service_tag = evm_tag if repo == "evm" else proxy_tag + other_service_tag = evm_tag if repo == "proxy" else proxy_tag + db = PostgresTestResultsHandler() + + # define the tags against which the comparison will be done + previous_tags: list[str] + + if re.fullmatch(GITHUB_TAG_PATTERN, compared_service_tag): + previous_tags = db.get_previous_tags( + repo=repo, + tag=compared_service_tag, + limit=history_depth_limit, + ) + else: + if version_branch: + previous_tags = [version_branch] + else: + previous_tags = ["latest"] + + click.echo(f"previous_tags: {previous_tags}") + + historical_data = db.get_historical_data( + depth=history_depth_limit, + repo=repo, + latest_tag=compared_service_tag, + previous_tags=previous_tags, + ) + + # get commit sha for compared_service and other_service + data_sample_row = historical_data[ + (historical_data["repo"] == repo) & + (historical_data["neon_evm_tag"] == evm_tag) & + (historical_data["proxy_tag"] == proxy_tag) + ].iloc[0] + + if repo == "evm": + compared_service_commit_sha = data_sample_row["evm_commit_sha"] + other_service_commit_sha = data_sample_row["proxy_commit_sha"] + elif repo == "proxy": + compared_service_commit_sha = data_sample_row["proxy_commit_sha"] + other_service_commit_sha = data_sample_row["evm_commit_sha"] + else: + raise ValueError(f'Unknown repo "{repo}"') + + compared_service_sha_string = f", commit sha {compared_service_commit_sha}" if compared_service_commit_sha else "" + other_service_sha_string = f", commit sha {other_service_commit_sha}" if other_service_commit_sha else "" + + # generate plots and save to pdf + other_service_name = "neon_evm" if repo == "proxy" else "proxy" + test_results_handler = TestResultsHandler() + test_results_handler.generate_and_save_plots_pdf( + historical_data=historical_data, + title_end=f"on {repo}:{compared_service_tag}{compared_service_sha_string}\n" + f"with {other_service_name}:{other_service_tag}{other_service_sha_string}", + output_pdf="cost_reports.pdf", + ) + + +@dapps.command("add_pr_comment", help="Add PR comment with dApp cost reports") @click.option("--pr_url_for_report", default="", help="Url to send the report as comment for PR") @click.option("--token", default="", help="github token") -def make_dapps_report(directory, pr_url_for_report, token): - report_data = dapps_cli.prepare_report_data(directory) - dapps_cli.print_report(report_data) - if pr_url_for_report: - gh_client = GithubClient(token) - gh_client.delete_last_comment(pr_url_for_report) - format_data = dapps_cli.format_report_for_github_comment(report_data) - gh_client.add_comment_to_pr(pr_url_for_report, format_data) +@click.option("--md_file", help="File with markdown for the comment") +def add_pr_comment(pr_url_for_report: str, token: str, md_file: str): + gh_client = GithubClient(token=token) + gh_client.delete_last_comment(pr_url_for_report) + + with open(md_file) as f: + markdown = f.read() + + gh_client.add_comment_to_pr(pr_url_for_report, markdown) + +@cli.group() +@click.pass_context +def k6(ctx): + """Commands for k6 load tests.""" + + +@k6.command("build", help="Prepare k6 performance test") +@click.option("-t", "--tag", default="05e0ce5", help="Eth plugin tag or commit sha to use") +@catch_traceback +def build(tag): + xk6_install = 'go install go.k6.io/xk6/cmd/xk6@latest' + xk6_build = f'xk6 build --with github.com/szkiba/xk6-prometheus --with github.com/neonlabsorg/xk6-ethereum@{tag}' + + command_install = subprocess.run(xk6_install, shell=True) + + if command_install.returncode != 0: + sys.exit(command_install.returncode) + + command_build = subprocess.run(xk6_build, shell=True) + if command_build.returncode != 0: + sys.exit(command_build.returncode) + + +@k6.command("run", help="Run k6 performance test") +@click.option("-n", "--network",required=True, default="local", help="Which network to use for envs assignment") +@click.option("-s", "--script", required=True, default="./loadtesting/k6/tests/sendNeon.test.js", help="Path to k6 script") +@click.option("-u", "--users", default=None, required=True, help="Number of users (will be generated before load test run)") +@click.option("-b", "--balance", default=None, required=True, help="Initial balance of each user in Neon") +@click.option("-a", "--bank_account", default=None, required=False, help="Bank account address") +@catch_traceback +def run(network, script, users, balance, bank_account): + network_object = network_manager.get_network_object(network) + k6_prepare_accounts(network, network_object, users, balance, bank_account) + + print("Running load test scenario...") + command = f'./k6 run {script}' + command_run = subprocess.run(command, shell=True) + if command_run.returncode != 0: + sys.exit(command_run.returncode) if __name__ == "__main__": cli() diff --git a/conftest.py b/conftest.py index 9e2ff7e167..d2683d9e8b 100644 --- a/conftest.py +++ b/conftest.py @@ -1,3 +1,4 @@ +import builtins import os import json import shutil @@ -10,7 +11,8 @@ from _pytest.config.argparsing import Parser from _pytest.nodes import Item from _pytest.runner import runtestprotocol -from solana.keypair import Keypair +from solders.keypair import Keypair +from web3.middleware import geth_poa_middleware from clickfile import TEST_GROUPS, EnvName from utils.types import TestGroup @@ -23,6 +25,7 @@ pytest_plugins = ["ui.plugins.browser"] +COST_REPORT_DIR: pathlib.Path = pathlib.Path() @dataclass @@ -57,8 +60,14 @@ def pytest_addoption(parser: Parser): default=False, help="Store tests result to file", ) + parser.addoption( + "--cost_reports_dir", + default="", + type=pathlib.Path, + help=f"Saves cost reports .json files in {COST_REPORT_DIR}", + ) known_args = parser.parse_known_args(args=sys.argv[1:]) - test_group_required = True if known_args.make_report else False + test_group_required = known_args.make_report parser.addoption( "--test-group", choices=TEST_GROUPS, @@ -81,6 +90,9 @@ def pytest_sessionstart(session: pytest.Session): if not keep_error_log: error_log.clear() + if COST_REPORT_DIR != pathlib.Path() and COST_REPORT_DIR.exists() and COST_REPORT_DIR.is_dir(): + shutil.rmtree(COST_REPORT_DIR) + def pytest_runtest_protocol(item: Item, nextitem): ihook = item.ihook @@ -100,6 +112,14 @@ def pytest_runtest_protocol(item: Item, nextitem): def pytest_configure(config: Config): + # redirect print to stderr for xdist-spawned processes because otherwise print statements get lost + if "PYTEST_XDIST_WORKER" in os.environ: + original_print = builtins.print + builtins.print = lambda *args, **kwargs: original_print(*args, file=sys.stderr, **kwargs) + + global COST_REPORT_DIR + COST_REPORT_DIR = config.getoption("--cost_reports_dir") + solana_url_env_vars = ["SOLANA_URL", "DEVNET_INTERNAL_RPC", "MAINNET_INTERNAL_RPC"] network_name = config.getoption("--network") envs_file = config.getoption("--envs") @@ -130,7 +150,7 @@ def pytest_configure(config: Config): if "NEON_TOKEN_MINT" not in os.environ or not os.environ["NEON_TOKEN_MINT"]: os.environ["NEON_TOKEN_MINT"] = env["spl_neon_mint"] if "CHAIN_ID" not in os.environ or not os.environ["CHAIN_ID"]: - os.environ["CHAIN_ID"]: env["network_ids"]["neon"] + os.environ["CHAIN_ID"] = str(env["network_ids"]["neon"]) if network_name == "terraform": env["solana_url"] = env["solana_url"].replace("", os.environ.get("SOLANA_IP")) @@ -148,22 +168,22 @@ def env_name(pytestconfig: Config) -> EnvName: @pytest.fixture(scope="session") def operator_keypair(): with open("operator-keypair.json", "r") as key: - secret_key = json.load(key)[:32] - return Keypair.from_secret_key(secret_key) + secret_key = json.load(key) + return Keypair.from_bytes(secret_key) @pytest.fixture(scope="session") def evm_loader_keypair(): with open("evm_loader-keypair.json", "r") as key: - secret_key = json.load(key)[:32] - return Keypair.from_secret_key(secret_key) + secret_key = json.load(key) + return Keypair.from_bytes(secret_key) @pytest.fixture(scope="session", autouse=True) def allure_environment(pytestconfig: Config, web3_client_session: NeonChainWeb3Client): opts = {} network_name = pytestconfig.getoption("--network") - if network_name != "geth" and network_name != "mainnet" and "neon_evm" not in os.getenv("PYTEST_CURRENT_TEST"): + if network_name != "geth" and network_name != "mainnet" and "neon_evm" not in os.getenv("PYTEST_CURRENT_TEST"): opts = { "Network": pytestconfig.environment.proxy_url, "Proxy.Version": web3_client_session.get_proxy_version()["result"], @@ -181,15 +201,20 @@ def allure_environment(pytestconfig: Config, web3_client_session: NeonChainWeb3C shutil.copy(categories_from, categories_to) if "CI" in os.environ: + github_server_url = os.environ.get("GITHUB_SERVER_URL", "https://github.com") + github_organization = os.environ.get("GITHUB_REPOSITORY_OWNER") + github_repository = os.environ.get("GITHUB_REPOSITORY", "neon-tests") + actions_url = f"{github_server_url}/{github_organization}/{github_repository}/actions" + with open(allure_path / "executor.json", "w+") as f: json.dump( { "name": "Github Action", "type": "github", - "url": "https://github.com/neonlabsorg/neon-tests/actions", + "url": actions_url, "buildOrder": os.environ.get("GITHUB_RUN_ID", "0"), "buildName": os.environ.get("GITHUB_WORKFLOW", "neon-tests"), - "buildUrl": f'{os.environ.get("GITHUB_SERVER_URL", "https://github.com")}/{os.environ.get("GITHUB_REPOSITORY", "neon-tests")}/actions/runs/{os.environ.get("GITHUB_RUN_ID", "0")}', + "buildUrl": f'{actions_url}/runs/{os.environ.get("GITHUB_RUN_ID", "0")}', "reportUrl": "", "reportName": "Allure report for neon-tests", }, @@ -198,16 +223,21 @@ def allure_environment(pytestconfig: Config, web3_client_session: NeonChainWeb3C @pytest.fixture(scope="session") -def web3_client_session(pytestconfig: Config) -> NeonChainWeb3Client: +def web3_client_session( + pytestconfig: Config, + env_name: EnvName, +) -> NeonChainWeb3Client: client = NeonChainWeb3Client( pytestconfig.environment.proxy_url, tracer_url=pytestconfig.environment.tracer_url, ) + if env_name is EnvName.GETH: + client._web3.middleware_onion.inject(geth_poa_middleware, layer=0) # noqa return client @pytest.fixture(scope="session") -def sol_client_session(pytestconfig: Config): +def sol_client_session(pytestconfig: Config) -> SolanaClient: client = SolanaClient( pytestconfig.environment.solana_url, pytestconfig.environment.account_seed_version, @@ -223,4 +253,4 @@ def faucet(pytestconfig: Config, web3_client_session) -> Faucet: @pytest.fixture(scope="session") def accounts_session(pytestconfig: Config, web3_client_session, faucet, eth_bank_account): accounts = EthAccounts(web3_client_session, faucet, eth_bank_account) - return accounts \ No newline at end of file + return accounts diff --git a/contracts/EIPs/EIP161/contract_b.sol b/contracts/EIPs/EIP161/contract_b.sol index b8c533334d..9eab076662 100644 --- a/contracts/EIPs/EIP161/contract_b.sol +++ b/contracts/EIPs/EIP161/contract_b.sol @@ -2,12 +2,14 @@ pragma solidity ^0.8.0; contract ContractB { event ContractBDeployed(address indexed creator, address indexed contractAddress); + event Result(uint256 indexed result); constructor() { emit ContractBDeployed(msg.sender, address(this)); } - function getOne() public pure returns (uint256) { + function getOne() public returns (uint256) { + emit Result(1); return 1; } } diff --git a/contracts/common/StorageSoliditySource.sol b/contracts/common/StorageSoliditySource.sol index 4a212f60dd..11d925d59f 100644 --- a/contracts/common/StorageSoliditySource.sol +++ b/contracts/common/StorageSoliditySource.sol @@ -6,38 +6,68 @@ pragma solidity >=0.4.0; */ contract Storage { uint256 number; + uint256 numberTwo; + uint256 time; + uint256[] public values; - /** - * @dev Stores value in variable - * @param num value to store - */ function store(uint256 num) public { number = num; + values = [number]; } - /** - * @dev Returns value - * @return value of 'number' - */ function retrieve() public view returns (uint256) { return number; } - /** - * @dev Returns code for given address - * @return value of '_addr.code' - */ + function retrieveSenderBalance() public view returns (uint256) { + return msg.sender.balance; + } + + function storeSumOfNumbers(uint256 num1, uint256 num2) public view returns (uint256) { + if (number == 101) { + num1 = 0; + } + if (numberTwo == 103) { + num2 = 5; + } + return num1 + num2; + } + function at(address _addr) public view returns (bytes memory) { return _addr.code; } + + function storeBlock() public { + number = block.number; + values = [number]; + } + + function storeBlockTimestamp() public returns (uint256) { + number = block.timestamp; + values = [number]; + return block.timestamp; + } + + function storeBlockInfo() public { + number = block.number; + time = block.timestamp; + values = [number, time]; + } + + function returnDoubledNumber(uint256 num) public view returns (uint256) { + if (number == 102) { + num = 10; + } + return num * 2; + } } contract StorageMultipleVars { string public data = "test"; - uint256 constant public number = 1; + uint256 public constant number = 1; uint256 public notSet; - function setData(string memory _data) public { + function setData(string memory _data) public { data = _data; } -} \ No newline at end of file +} diff --git a/contracts/opcodes/EIP1559BaseFee.sol b/contracts/opcodes/EIP1559BaseFee.sol new file mode 100644 index 0000000000..ce835c9f69 --- /dev/null +++ b/contracts/opcodes/EIP1559BaseFee.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.8.7; + +contract BaseFeeOpcode { + event Log(uint256 baseFee); + + function baseFee() public returns (uint256) { + return block.basefee; + } + function baseFeeTrx() public { + uint256 baseFee = block.basefee; + emit Log(baseFee); + } +} \ No newline at end of file diff --git a/contracts/opcodes/UnsupportedOpcodes.sol b/contracts/opcodes/UnsupportedOpcodes.sol index fcfe17d7b4..4f6bbd990c 100644 --- a/contracts/opcodes/UnsupportedOpcodes.sol +++ b/contracts/opcodes/UnsupportedOpcodes.sol @@ -2,10 +2,6 @@ pragma solidity ^0.8.4; contract UnsupportedOpcodes { - function baseFee() public returns (uint256){ - return block.basefee; - } - function coinbase() public returns (address){ return block.coinbase; } diff --git a/deploy/cli/cost_report.py b/deploy/cli/cost_report.py new file mode 100644 index 0000000000..69f364121e --- /dev/null +++ b/deploy/cli/cost_report.py @@ -0,0 +1,97 @@ +import glob +import json +import os +import pathlib +import re +from collections import Counter + +import pandas as pd + +from deploy.cli.dapps import NETWORK_MANAGER +from deploy.cli.infrastructure import get_solana_accounts_transactions_compute_units +from utils.web3client import NeonChainWeb3Client + + +def prepare_report_data(directory: str) -> pd.DataFrame: + proxy_url = NETWORK_MANAGER.get_network_param(os.environ.get("NETWORK"), "proxy_url") + web3_client = NeonChainWeb3Client(proxy_url) + + reports = {} + for path in glob.glob(str(pathlib.Path(directory) / "*-report.json")): + with open(path, "r") as f: + rep = json.load(f) + if isinstance(rep, list): + for r in rep: + if "actions" in r: + reports[r["name"]] = r["actions"] + else: + if "actions" in rep: + reports[rep["name"]] = rep["actions"] + + data = [] + + for app, actions in reports.items(): + counts = Counter([action["name"].lower().strip() for action in actions]) + duplicate_actions = [action for action, count in counts.items() if count > 1] + added_numbers = {dup_action: 1 for dup_action in duplicate_actions} + + for action in actions: + # Ensure action name is unique by appending a counter if necessary + base_action_name = action["name"].lower().strip() + if base_action_name in duplicate_actions: + added_number = added_numbers[base_action_name] + unique_action_name = f"{base_action_name} {added_number}" + added_numbers[base_action_name] += 1 + else: + unique_action_name = base_action_name + + accounts, trx, compute_units = get_solana_accounts_transactions_compute_units(action["tx"]) + # accounts, trx, compute_units = (2, 12, 8946) + tx = web3_client.get_transaction_by_hash(action["tx"]) + estimated_gas = int(tx.gas) if tx and tx.gas else None + # estimated_gas = 122879 + used_gas = int(action["usedGas"]) + + data.append( + { + "dapp_name": app.lower().strip(), + "action": unique_action_name, + "acc_count": accounts, + "trx_count": trx, + "gas_estimated": estimated_gas, + "gas_used": used_gas, + "compute_units": compute_units, + } + ) + + df = pd.DataFrame(data) + if df.empty: + raise Exception(f"no reports found in {directory}") + return df + + +def report_data_to_markdown(df: pd.DataFrame) -> str: + report_content = "" + dapp_names = df['dapp_name'].unique() + df.columns = [col.upper() for col in df.columns] + df['GAS_USED_%'] = df['GAS_USED_%'].apply(lambda x: f"{x:.2f}") + + for dapp_name in dapp_names: + dapp_df = df[df['DAPP_NAME'] == dapp_name].drop(columns='DAPP_NAME') + + # sort by ACTION (to mitigate [action 1, action 10, action 2, ...]) + dapp_df[['ACTION_TEXT', 'ACTION_NUM']] = dapp_df['ACTION'].apply(split_action).apply(pd.Series) + dapp_df = dapp_df.sort_values(by=['ACTION_TEXT', 'ACTION_NUM']) + dapp_df = dapp_df.drop(columns=['ACTION_TEXT', 'ACTION_NUM']) + + report_content += f'\n## Cost Report for "{dapp_name.title()}" dApp\n\n' + report_content += dapp_df.to_markdown(index=False) + "\n" + + return report_content + + +def split_action(action) -> tuple[str, int]: + match = re.match(r"(.+?)\s*(\d*)$", action) + text_ = match.group(1) + number = int(match.group(2)) if match.group(2).isdigit() else 0 + return text_, number diff --git a/deploy/cli/dapps.py b/deploy/cli/dapps.py index 7559bff6a7..661db455da 100644 --- a/deploy/cli/dapps.py +++ b/deploy/cli/dapps.py @@ -1,18 +1,8 @@ import os -import glob -import json import typing as tp -import pathlib -import tabulate - -from deploy.cli.infrastructure import get_solana_accounts_in_tx from deploy.cli.network_manager import NetworkManager -from utils.web3client import NeonChainWeb3Client - - -REPORT_HEADERS = ["Action", "Fee", "Cost in $", "Accounts", "TRx", "Estimated Gas", "Used Gas", "Used % of EG"] NETWORK_MANAGER = NetworkManager() @@ -25,62 +15,3 @@ def set_github_env(envs: tp.Dict, upper=True) -> None: env_file.write(f"\n{key.upper() if upper else key}={str(value)}") -def prepare_report_data(directory): - proxy_url = NETWORK_MANAGER.get_network_param(os.environ.get("NETWORK"), "proxy_url") - web3_client = NeonChainWeb3Client(proxy_url) - out = {} - reports = {} - for path in glob.glob(str(pathlib.Path(directory) / "*-report.json")): - with open(path, "r") as f: - rep = json.load(f) - if type(rep) is list: - for r in rep: - if "actions" in r: - reports[r["name"]] = r["actions"] - else: - if "actions" in rep: - reports[rep["name"]] = rep["actions"] - - for app in reports: - out[app] = [] - for action in reports[app]: - accounts, trx = get_solana_accounts_in_tx(action["tx"]) - tx = web3_client.get_transaction_by_hash(action["tx"]) - estimated_gas = int(tx.gas) if tx and tx.gas else None - used_gas = int(action["usedGas"]) - row = [action["name"]] - fee = used_gas * int(action["gasPrice"]) / 1000000000000000000 - used_gas_percentage = round(used_gas * 100 / estimated_gas, 2) if estimated_gas else None - row.append(fee) - row.append(fee * web3_client.get_token_usd_gas_price()) - row.append(accounts) - row.append(trx) - row.append(estimated_gas) - row.append(used_gas) - row.append(used_gas_percentage) - out[app].append(row) - return out - - -def print_report(data): - report_content = "" - for app in data: - report_content += f'Cost report for "{app.title()}" dApp\n' - report_content += "----------------------------------------\n" - report_content += tabulate.tabulate(data[app], REPORT_HEADERS, tablefmt="simple_grid") + "\n" - - print(report_content) - return report_content - - -def format_report_for_github_comment(data): - headers = "| " + " | ".join(REPORT_HEADERS) + " |\n" - headers += "| --- | --- | --- | --- | --- | --- | --- |--- |\n" - report_content = "" - - for app in data: - report_content += f'\nCost report for "{app.title()}" dApp\n\n' - report_content += headers - for action_data in data[app]: - report_content += "| " + " | ".join([str(item) for item in action_data]) + " | " + "\n" - return report_content diff --git a/deploy/cli/infrastructure.py b/deploy/cli/infrastructure.py index 77e9ba3c3d..8a0d18aa71 100644 --- a/deploy/cli/infrastructure.py +++ b/deploy/cli/infrastructure.py @@ -6,6 +6,7 @@ import pathlib import logging +import click from paramiko.client import SSHClient from scp import SCPClient @@ -26,7 +27,6 @@ os.environ["TF_VAR_run_number"] = os.environ.get("GITHUB_RUN_NUMBER", "0") os.environ["TF_VAR_branch"] = os.environ.get("GITHUB_REF_NAME", "develop").replace("/", "-").replace("_", "-") -os.environ["TF_VAR_dockerhub_org_name"] = os.environ.get("DOCKERHUB_ORG_NAME", "neonlabsorg") terraform = Terraform(working_dir=pathlib.Path(__file__).parent.parent / "hetzner") @@ -56,6 +56,8 @@ def deploy_infrastructure( os.environ["TF_VAR_faucet_model_commit"] = faucet_tag os.environ["TF_VAR_proxy_image_tag"] = proxy_tag os.environ["TF_VAR_proxy_model_commit"] = proxy_branch + os.environ["TF_VAR_dockerhub_org_name"] = os.environ.get("GITHUB_REPOSITORY_OWNER") + if use_real_price: os.environ["TF_VAR_use_real_price"] = "1" @@ -85,6 +87,7 @@ def destroy_infrastructure(): os.environ["TF_VAR_faucet_model_commit"] = "develop" os.environ["TF_VAR_proxy_image_tag"] = "latest" os.environ["TF_VAR_proxy_model_commit"] = "develop" + os.environ["TF_VAR_dockerhub_org_name"] = os.environ.get("GITHUB_REPOSITORY_OWNER") log = logging.getLogger() log.handlers = [] @@ -152,7 +155,7 @@ def prepare_accounts(network_name, count, amount) -> tp.List: return accounts -def get_solana_accounts_in_tx(eth_transaction): +def get_solana_accounts_transactions_compute_units(eth_transaction): network = os.environ.get("NETWORK") network_manager = NetworkManager(network) solana_url = network_manager.get_network_param(network, "solana_url") @@ -166,8 +169,30 @@ def get_solana_accounts_in_tx(eth_transaction): print(f"get_slot={sol_client.get_slot()}") tr = sol_client.get_transaction(Signature.from_string(trx["result"][0]), max_supported_transaction_version=0) print(f"get_transaction({trx}): {tr}") + + solana_transaction_hashes = trx["result"] + compute_units = 0 + + for solana_transaction_hash in solana_transaction_hashes: + solana_transaction = sol_client.get_transaction( + tx_sig=Signature.from_string(solana_transaction_hash), + max_supported_transaction_version=0, + ) + + try: + log_messages = solana_transaction.value.transaction.meta.log_messages + except AttributeError: + click.echo(f"WARNING: no log messages in transaction {solana_transaction_hash}: {solana_transaction}") + continue + + for message in log_messages[::-1]: + match = re.match(r'^.+consumed (\d+) of \d+ compute units$', message) + if match: + compute_units += int(match.group(1)) + break + if tr.value.transaction.transaction.message.address_table_lookups: alt = tr.value.transaction.transaction.message.address_table_lookups - return len(alt[0].writable_indexes) + len(alt[0].readonly_indexes), len(trx["result"]) + return len(alt[0].writable_indexes) + len(alt[0].readonly_indexes), len(trx["result"]), compute_units else: - return len(tr.value.transaction.transaction.message.account_keys), len(trx["result"]) + return len(tr.value.transaction.transaction.message.account_keys), len(trx["result"]), compute_units diff --git a/deploy/hetzner/proxy_init.sh b/deploy/hetzner/proxy_init.sh index 5d4eba7898..6b1a2cb489 100644 --- a/deploy/hetzner/proxy_init.sh +++ b/deploy/hetzner/proxy_init.sh @@ -19,7 +19,7 @@ sudo chmod +x /usr/local/bin/docker-compose # Get docker-compose file cd /opt -curl -O https://raw.githubusercontent.com/neonlabsorg/neon-proxy.py/${proxy_model_commit}/docker-compose/docker-compose-ci.yml +curl -O https://raw.githubusercontent.com/${dockerhub_org_name}/neon-proxy.py/${proxy_model_commit}/docker-compose/docker-compose-ci.yml # Set required environment variables diff --git a/deploy/hetzner/solana_init.sh b/deploy/hetzner/solana_init.sh index dbd52e3db5..21603f4262 100644 --- a/deploy/hetzner/solana_init.sh +++ b/deploy/hetzner/solana_init.sh @@ -45,7 +45,7 @@ export PROXY_IMAGE_NAME="neon-proxy.py" # Receive docker-compose file and create override file cd /opt -curl -O https://raw.githubusercontent.com/neonlabsorg/neon-proxy.py/${proxy_model_commit}/docker-compose/docker-compose-ci.yml +curl -O https://raw.githubusercontent.com/${dockerhub_org_name}/neon-proxy.py/${proxy_model_commit}/docker-compose/docker-compose-ci.yml cat > docker-compose-ci.override.yml< str: - for line in open(self.path).readlines(): - m = re.search(r"FROM\s*(.*)", line) - if m: - return m.group(1) - raise Exception(f"No declaration FROM in dockerfile {self.path}") - - @property # type: ignore - @functools.lru_cache() - def context(self) -> str: - for line in open(self.path).readlines(): - m = re.search(r"^#\s*context=(.*)", line) - if m: - return m.group(1) - return clickfile.HOME_DIR - - -@dataclasses.dataclass() -class Image: - dockerfile: tp.Union[pathlib.Path, Dockerfile] - name: tp.Optional[str] = None - tag_name: tp.Optional[str] = None - prefix: tp.Optional[str] = IMAGE_PREFIX - - def __post_init__(self): - if isinstance(self.dockerfile, pathlib.Path): - self.dockerfile = Dockerfile((clickfile.HOME_DIR / self.dockerfile).as_posix()) - - @property - def tag(self) -> str: - return f"{self.prefix}/{self.name}:{self.tag_name}" - - def build(self, **kwargs) -> None: - kwargs.setdefault("tag", self.tag) - docker_build(self.dockerfile, **kwargs) - - def __str__(self) -> str: - return self.tag - - -@functools.lru_cache() -def docker_build( - dockerfile: Dockerfile, - tag: tp.Optional[str] = None, - cache: bool = True, - build_args: tp.Optional[tp.Tuple[tp.Tuple[str, str]]] = None, -): - pull_image = not tp.cast(str, dockerfile.from_).startswith(IMAGE_PREFIX) - - def var_args(): - args = [ - f"-t {tag}" if tag else None, - "--no-cache" if not cache else None, - "--pull" if pull_image else None, - ] - if build_args: - args.extend(f"--build-arg {k}={v}" for k, v in build_args) - return " ".join(filter(None, args)) - - env.header("Build {}".format(tag)) - image_before = docker_image_inspect(tag) - env.shell( - f"docker build {dockerfile.context}" - f" -f {dockerfile.path}" - f" {var_args()}" - ) - image_after = docker_image_inspect(tag) - - return image_before and image_before["Id"] != image_after["Id"] - - -def docker_image_inspect(image: str) -> tp.Optional[dict]: - """Inspect docker image.""" - with env.quiet(): - out = env.shell(f"docker image inspect {image}") - return json.loads(out)[0] if out else None - - -def docker_image_exists(image: str) -> bool: - """Returns True when image exists in a local cache (== pulled). - It's faster then calling docker inspect. - """ - return bool(env.shell(f"docker images -q {image}", capture=True)) - - -def docker_inspect(container: str) -> tp.Optional[dict]: - """Inspect docker container.""" - with env.quiet(): - out = env.shell(f"docker inspect {container}") - return json.loads(out)[0] if out else None - - -def docker_inspect_network(network: str) -> tp.Optional[dict]: - """Inspect docker network.""" - with env.quiet(): - out = env.shell(f"docker network inspect {network}") - return json.loads(out)[0] if out else None - - -def docker_pull(image: str) -> bool: - """Pulls image, returns True if pulled a fresh one.""" - if ":" not in image: - image = f"{image}:latest" - return "Status: Downloaded newer image" in env.shell(f"docker pull {image}", tee=True) - - -def cleanup_docker_networks(): - """Remove test networks that are left after.""" - print(env.header("Cleanup unused docker networks")) - with env.quiet(): - env.shell("docker network prune -f") - - -def cleanup_running_containers(): - """Kill and remove long-running medium tests containers.""" - env.header("Cleanup old containers") - - long_running_re = re.compile(r"Up \d+ hours") - kill_containers = [] - - ps_output = env.shell('docker ps --format="{{.Names}} {{.Image}} {{.Status}}"', capture=True) - for row in ps_output.splitlines(): - container_name, image_name, running_for = row.split(" ", 2) - if ( - long_running_re.match(running_for) - # skip DevBox - and "neonlabs/devbox" not in image_name - ): - kill_containers.append(container_name) - - if kill_containers: - env.pprint_list("Killing containers:", kill_containers, env.yellow) - env.shell("docker rm -f {}".format(" ".join(kill_containers))) - - - diff --git a/deploy/infra/utils/env.py b/deploy/infra/utils/env.py deleted file mode 100644 index d0729771c9..0000000000 --- a/deploy/infra/utils/env.py +++ /dev/null @@ -1,204 +0,0 @@ -import contextlib -import functools -import os -import subprocess # nosec -import sys -import traceback -import typing as tp - -import clickfile -import click - -SEMVER_RE = r"([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9a-zA-Z-]+))?" -"""Semver regex. - -Example: `X.Y.Z` or `X.Y.Z-string` -""" - - -class Nope(click.ClickException): - """Warning. - """ - - exit_code = 0 - - def show(self, file=None): - click.echo(yellow(f"Nope: {self.format_message()}")) - - -class Error(Exception): - pass - - -_warn_only = False -_quiet = False - - -@contextlib.contextmanager -def quiet(): - global _quiet - prev = _quiet - try: - _quiet = True - yield - finally: - _quiet = prev - - -def quiet_func(func, *args, **kwargs): - try: - func(*args, **kwargs) - return True - except click.Abort: - return False - - -@contextlib.contextmanager -def warn_only(): - global _warn_only - prev = _warn_only - try: - _warn_only = True - yield - finally: - _warn_only = prev - - -@contextlib.contextmanager -def settings(warn_only=False): - global _warn_only - prev = _warn_only - try: - _warn_only = warn_only - yield - finally: - _warn_only = prev - - -def with_pycache_dropped(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - shell(f"find {clickfile.HOME_DIR} -name __pycache__ | xargs rm -rf", check=False) - return func(*args, **kwargs) - - return wrapper - - -@contextlib.contextmanager -def lcd(path): - prev = os.getcwd() - try: - os.chdir(path) - yield - finally: - os.chdir(prev) - - -@contextlib.contextmanager -def shell_env(**kwargs): - unset = object() - prev = {k: os.environ.get(k, unset) for k in kwargs} - try: - os.environ.update(kwargs) - yield - finally: - for k, v in prev.items(): - if v is unset: - del os.environ[k] - else: - os.environ[k] = v - - -@contextlib.contextmanager -def cli_error_handler(): - try: - yield - except Exception as err: - with_trace = not isinstance(err, (subprocess.SubprocessError, Error)) - if with_trace: - print(red(traceback.format_exc())) - else: - print(red(str(err))) - sys.exit(1) - - -def num_processes(): - return max(os.cpu_count() // 4, 1) - - -def is_docker_container(): - """Return True in this code is run inside docker container.""" - return os.path.exists("/.dockerenv") - - -def is_devbox(): - """Return True in this code is run inside devbox.""" - return os.environ.get("DEVBOX") - - -def shell(*args, **kwargs): - """Run a command in shell. - - Accepts all arguments that ``subprocess.run()`` does, - with some extra opinionated defaults: - - :: - - shell: True - check: True - encoding: utf-8 - - :keyword bool capture: Capture and return stdout (Default: False) - """ - kwargs["shell"] = True - if "check" not in kwargs: - kwargs["check"] = True # check for exit code - kwargs["encoding"] = "utf-8" # open streams in text mode - if kwargs.pop("capture", False): - kwargs["stdout"] = subprocess.PIPE - if _quiet: - kwargs["stdout"] = subprocess.PIPE - kwargs["stderr"] = subprocess.PIPE - try: - print(click.style("$ " + args[0].strip(), bold=True)) - proc = subprocess.run(*args, **kwargs) # nosec - return proc.stdout.strip() if proc.stdout is not None else None - except subprocess.CalledProcessError as e: - if _warn_only: - print(red(str(e))) - elif _quiet: - return - else: - raise - - -def hyphenify(s: str) -> str: - return s.replace("_", "-") - - -def unhyphenify(s: str) -> str: - return s.replace("-", "_") - - -def header(s): - print(click.style("\n\n{}\n".format(s), fg="white", bold=True)) - - -def green(s): - return click.style(s, fg="green") - - -def yellow(s): - return click.style(s, fg="yellow") - - -def red(s): - return click.style(s, fg="red") - - -def pprint_list( - header: str, data: tp.Iterable, colorer: tp.Callable = green, sort: bool = True -) -> None: - print(colorer(f"{header}")) - print(colorer(" - " + "\n - ".join(sorted(data) if sort else data))) - print(colorer(f"({len(data)} objects listed)")) diff --git a/deploy/requirements/click.txt b/deploy/requirements/click.txt index b8c6bdccfd..42d891f6f4 100644 --- a/deploy/requirements/click.txt +++ b/deploy/requirements/click.txt @@ -5,7 +5,7 @@ paramiko==2.12.0 scp==0.14.4 tabulate>=0.8.10 web3==6.17.1 -solana==0.28.0 +solana==0.34.3 py-solc-x==1.1.1 pythclient==0.0.2 boto3==1.28.54 @@ -18,3 +18,8 @@ deepdiff==7.0.1 pydantic==2.7.3 slack-sdk==3.30.0 filelock==3.15.4 +polling2==0.5.0 +psycopg2-binary==2.9.9 +sqlalchemy==2.0.31 +pandas==2.2.2 +matplotlib==3.9.1 \ No newline at end of file diff --git a/deploy/test_results_db/__init__.py b/deploy/test_results_db/__init__.py new file mode 100644 index 0000000000..57af14b466 --- /dev/null +++ b/deploy/test_results_db/__init__.py @@ -0,0 +1,4 @@ +# this is necessary to make sure Base is aware of all the tables + +from .table_models.cost_report import CostReport +from .table_models.dapp_data import DappData diff --git a/deploy/test_results_db/db_handler.py b/deploy/test_results_db/db_handler.py new file mode 100644 index 0000000000..aa07574738 --- /dev/null +++ b/deploy/test_results_db/db_handler.py @@ -0,0 +1,241 @@ +import os +import re +import signal +import typing as tp + +import click +from sqlalchemy import create_engine, Engine, distinct, desc +from sqlalchemy.orm import sessionmaker, Query +import pandas as pd +from packaging import version + +from deploy.test_results_db.table_models.base import Base +from deploy.test_results_db.table_models.cost_report import CostReport +from deploy.test_results_db.table_models.dapp_data import DappData +from utils.types import RepoType +from utils.version import remove_heading_chars_till_first_digit + + +class PostgresTestResultsHandler: + def __init__(self): + self.engine: Engine = self.__create_engine() + self.Session = sessionmaker(bind=self.engine) + self.session = self.Session() + self.__create_tables_if_needed() + signal.signal(signal.SIGTERM, self.__handle_exit) + signal.signal(signal.SIGINT, self.__handle_exit) + + @staticmethod + def __create_engine() -> Engine: + db_host = os.environ["TEST_RESULTS_DB_HOST"] + db_port = os.environ["TEST_RESULTS_DB_PORT"] + db_name = os.environ["TEST_RESULTS_DB_NAME"] + db_user = os.environ["TEST_RESULTS_DB_USER"] + db_password = os.environ["TEST_RESULTS_DB_PASSWORD"] + db_url = f"postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}" + engine = create_engine(db_url) + return engine + + def __create_tables_if_needed(self): + Base.metadata.create_all(self.engine) + + def __handle_exit(self, signum, frame): + self.Session.close_all() + + def get_cost_report_ids(self, repo: str, tag: str) -> list[int]: + tag_column = CostReport.neon_evm_tag if repo == "evm" else CostReport.proxy_tag + report_ids = self.session.query(CostReport.id).filter( + CostReport.repo == repo, + tag_column == tag, + ).all() + report_ids = [id_[0] for id_ in report_ids] + return report_ids + + def delete_data_by_report_ids(self, report_ids: tp.List[int]): + click.echo(f"delete_data_by_report_ids: {report_ids}") + self.session.query(DappData).filter(DappData.cost_report_id.in_(report_ids)).delete(synchronize_session=False) + self.session.commit() + + def delete_reports(self, report_ids: tp.List[int]): + click.echo(f"delete_reports: {report_ids}") + self.session.query(CostReport).filter(CostReport.id.in_(report_ids)).delete(synchronize_session=False) + self.session.commit() + + def save_cost_report( + self, + repo: RepoType, + neon_evm_tag: str, + proxy_tag: str, + evm_commit_sha: tp.Optional[str], + proxy_commit_sha: str, + ) -> int: + click.echo(f"save_cost_report: {locals()}") + cost_report = CostReport( + repo=repo, + neon_evm_tag=neon_evm_tag, + proxy_tag=proxy_tag, + evm_commit_sha=evm_commit_sha or None, + proxy_commit_sha=proxy_commit_sha, + ) + self.session.add(cost_report) + self.session.commit() + return cost_report.id + + def save_cost_report_data(self, report_data: pd.DataFrame, cost_report_id: int): + click.echo(f"save_cost_report_data for cost_report_id: {cost_report_id}") + dapp_groups = report_data.groupby("dapp_name") + + for dapp_name, group in dapp_groups: + for _, row in group.iterrows(): + cost_report_data = DappData( + cost_report_id=cost_report_id, + dapp_name=str(dapp_name), + action=row["action"], + acc_count=row["acc_count"], + trx_count=row["trx_count"], + gas_estimated=row["gas_estimated"], + gas_used=row["gas_used"], + compute_units=row["compute_units"], + ) + self.session.add(cost_report_data) + + self.session.commit() + + def get_previous_tags(self, repo: RepoType, tag: str, limit: int) -> list[str]: + """ + + :param repo: + :param tag: must match GITHUB_TAG_PATTERN + :param limit: + :return: + """ + from clickfile import GITHUB_TAG_PATTERN + + assert re.fullmatch(GITHUB_TAG_PATTERN, tag) + tag_column = CostReport.neon_evm_tag if repo == "evm" else CostReport.proxy_tag + tags: list[str] = ( + self.session.query(distinct(tag_column)) + .filter( + CostReport.repo == repo, + ) + .all() + ) + tags = [tag[0] for tag in tags] + tags = [tag for tag in tags if GITHUB_TAG_PATTERN.match(tag)] + sorted_tags: list[str] = sorted( + [t for t in tags if t is not None], + key=lambda t: version.parse(remove_heading_chars_till_first_digit(t)), + reverse=True, + ) + latest_tag: version.Version = version.parse(remove_heading_chars_till_first_digit(tag)) + + # Find the index of the first tag that is less than latest_tag + for i, tag_ in enumerate(sorted_tags): + if version.parse(remove_heading_chars_till_first_digit(tag_)) < latest_tag: + # Return all tags before the found index, limited by the specified limit + previous_tags: list[str] = sorted_tags[i:] + return previous_tags[:limit] + else: + return [] + + def get_historical_data( + self, + depth: int, + repo: RepoType, + latest_tag: str, + previous_tags: list[str], + ) -> pd.DataFrame: + """ + :param depth: + :param repo: + :param latest_tag: + :param previous_tags: ["latest] or ["v3.1.x"] or ["v3.1.0", "v3.1.1", ...] + """ + + tag_column = CostReport.neon_evm_tag if repo == "evm" else CostReport.proxy_tag + + # Fetch previous CostReport entries + previous_reports: Query = ( + self.session.query(CostReport) + .filter( + CostReport.repo == repo, + tag_column.in_(previous_tags), + ) + .order_by(desc(CostReport.timestamp)) + ) + + # offset the previous_reports query by 1 if it's a merge event because latest_tag is the same as previous_tags + offset = 1 if latest_tag in previous_tags else 0 + previous_reports: list[CostReport] = previous_reports.offset(offset).limit(depth - 1).all() + + # Fetch last CostReport + last_report: CostReport = ( + self.session.query(CostReport) + .filter( + CostReport.repo == repo, + tag_column == latest_tag, + ) + .order_by(desc(CostReport.timestamp)) + .first() + ) + + cost_report_entries: list[CostReport] = [last_report] + previous_reports + cost_report_ids: list[int] = [r.id for r in previous_reports] + [last_report.id] + + # Define the dapps that are present in the last_report + dapp_name_tuples = ( + self.session.query(distinct(DappData.dapp_name)).filter(DappData.cost_report_id == last_report.id).all() + ) + dapp_names = [dapp_name_tuple[0] for dapp_name_tuple in dapp_name_tuples] + + # Define the actions that are present in the last_report + actions_tuples = ( + self.session.query(distinct(DappData.action)).filter(DappData.cost_report_id == last_report.id).all() + ) + actions = [action_tuple[0] for action_tuple in actions_tuples] + + # Fetch DappData entries for cost_report_ids but only for dapps and actions present in last_report + dapp_data_entries: list[tp.Type[DappData]] = ( + self.session.query(DappData) + .filter( + DappData.cost_report_id.in_(cost_report_ids), + DappData.dapp_name.in_(dapp_names), + DappData.action.in_(actions), + ) + .all() + ) + + # Convert the list of CostReport objects to a dictionary for quick lookup + cost_report_dict: dict[int, CostReport] = {r.id: r for r in cost_report_entries} + + # prepare data for the DataFrame + df_data = [] + for data_entry in dapp_data_entries: + cost_report: tp.Optional[CostReport] = cost_report_dict.get(data_entry.cost_report_id) + if cost_report: + tag = cost_report.neon_evm_tag if repo == "evm" else cost_report.proxy_tag + df_data.append( + { + "repo": cost_report.repo, + "timestamp": cost_report.timestamp, + "neon_evm_tag": cost_report.neon_evm_tag, + "proxy_tag": cost_report.proxy_tag, + "tag": tag, + "dapp_name": data_entry.dapp_name, + "action": data_entry.action, + "acc_count": data_entry.acc_count, + "trx_count": data_entry.trx_count, + "gas_estimated": data_entry.gas_estimated, + "gas_used": data_entry.gas_used, + "evm_commit_sha": cost_report.evm_commit_sha, + "proxy_commit_sha": cost_report.proxy_commit_sha, + "compute_units": data_entry.compute_units, + } + ) + + # Initialize the DataFrame and sort it + df = pd.DataFrame(data=df_data) + if not df.empty: + df = df.sort_values(by=["timestamp", "dapp_name", "action"]) + + return df diff --git a/deploy/test_results_db/table_models/base.py b/deploy/test_results_db/table_models/base.py new file mode 100644 index 0000000000..e632e965d1 --- /dev/null +++ b/deploy/test_results_db/table_models/base.py @@ -0,0 +1,4 @@ +from sqlalchemy.orm import declarative_base + + +Base = declarative_base() diff --git a/deploy/test_results_db/table_models/cost_report.py b/deploy/test_results_db/table_models/cost_report.py new file mode 100644 index 0000000000..956eab5a5f --- /dev/null +++ b/deploy/test_results_db/table_models/cost_report.py @@ -0,0 +1,29 @@ +from datetime import datetime + +from sqlalchemy import Column, Integer, String, DateTime +from sqlalchemy.orm import relationship + +from deploy.test_results_db.table_models.base import Base +from utils.types import RepoType + + +class CostReport(Base): + __tablename__ = "cost_report" + + id: int = Column(Integer, primary_key=True, autoincrement=True) + timestamp: datetime.utcnow = Column(DateTime, default=datetime.utcnow, nullable=False) + repo: RepoType = Column(String(255), nullable=False) + neon_evm_tag: str = Column(String(255), nullable=False) + proxy_tag: str = Column(String(255), nullable=False) + evm_commit_sha: str = Column(String(255), nullable=True) + proxy_commit_sha: str = Column(String(255), nullable=False) + + dapp_data = relationship("DappData", back_populates="report", cascade="all, delete-orphan") + + def __repr__(self): + formatted_timestamp = self.timestamp.strftime("%Y-%m-%d %H:%M:%S") + return ( + f"" + ) diff --git a/deploy/test_results_db/table_models/dapp_data.py b/deploy/test_results_db/table_models/dapp_data.py new file mode 100644 index 0000000000..219f9683a8 --- /dev/null +++ b/deploy/test_results_db/table_models/dapp_data.py @@ -0,0 +1,25 @@ +from sqlalchemy import Column, Integer, String, Numeric, ForeignKey +from sqlalchemy.orm import relationship + +from deploy.test_results_db.table_models.base import Base + + +class DappData(Base): + __tablename__ = 'dapp_data' + + id: int = Column(Integer, primary_key=True, autoincrement=True) + cost_report_id: int = Column(Integer, ForeignKey('cost_report.id'), nullable=False) + dapp_name: str = Column(String(255), nullable=False) + action: str = Column(String(255), nullable=False) + acc_count: int = Column(Integer, nullable=False) + trx_count: int = Column(Integer, nullable=False) + gas_estimated: int = Column(Integer, nullable=False) + gas_used: int = Column(Integer, nullable=False) + compute_units: int = Column(Integer, nullable=False) + + report = relationship('CostReport', back_populates='dapp_data') + + def __repr__(self): + return (f"") diff --git a/deploy/test_results_db/test_results_handler.py b/deploy/test_results_db/test_results_handler.py new file mode 100644 index 0000000000..e58ca8929d --- /dev/null +++ b/deploy/test_results_db/test_results_handler.py @@ -0,0 +1,187 @@ +import textwrap +from decimal import Decimal, ROUND_HALF_UP + +import pandas as pd +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker +from matplotlib.backends.backend_pdf import PdfPages + + +class TestResultsHandler: + @staticmethod + def generate_and_save_plots_pdf( + historical_data: pd.DataFrame, + title_end: str, + output_pdf: str, + ) -> str: + historical_data["timestamp"] = pd.to_datetime(historical_data["timestamp"], errors="coerce") + historical_data["acc_count"] = historical_data["acc_count"].apply(Decimal) + historical_data["trx_count"] = historical_data["trx_count"].apply(Decimal) + historical_data["gas_estimated"] = historical_data["gas_estimated"].apply(Decimal) + historical_data["gas_used"] = historical_data["gas_used"].apply(Decimal) + historical_data["gas_used_%"] = ( + (historical_data["gas_used"] / historical_data["gas_estimated"]) * Decimal("100") + ).apply(lambda x: x.quantize(Decimal("0.00"), rounding=ROUND_HALF_UP)) + historical_data["compute_units"] = historical_data["compute_units"].apply(Decimal) + + # analyze only the dapps that are present in the latest report + latest_timestamp = historical_data["timestamp"].max() + latest_report_data = historical_data[historical_data["timestamp"] == latest_timestamp] + dapp_names = latest_report_data["dapp_name"].unique() + metrics = ["acc_count", "trx_count", "gas_estimated", "gas_used", "gas_used_%", "compute_units"] + historical_data = historical_data.sort_values(by=["timestamp"]) + + unique_timestamps = historical_data["timestamp"].unique().tolist() + x_tick_labels = historical_data.groupby('timestamp')['tag'].first().tolist() + + with PdfPages(output_pdf) as pdf: + for dapp_name in dapp_names: + # Filter data for the current dapp_name + dapp_data = historical_data[historical_data["dapp_name"] == dapp_name] + actions = dapp_data["action"].unique() + + num_rows = len(actions) + num_cols = len(metrics) + axes: plt.Axes + fig, axes = plt.subplots(nrows=num_rows, ncols=num_cols, figsize=(5 * num_cols, 3 * num_rows), sharex="col") + fig.suptitle(t=f'Cost report for "{dapp_name}" dApp\n{title_end}', fontsize=16, fontweight="bold") + + for action_idx, action in enumerate(actions): + # Calculate y-axis limits for each metric + buffer_fraction = Decimal("0.5") + action_data = dapp_data[dapp_data["action"] == action] + y_limits = {} + + # define y limits + for metric in metrics: + metric_data = action_data[metric] + min_val = metric_data.min() + max_val = metric_data.max() + range_val = max_val - min_val + + if range_val == 0: + min_val -= abs(min_val) * buffer_fraction + max_val += abs(max_val) * buffer_fraction + else: + min_val -= range_val * buffer_fraction + max_val += range_val * buffer_fraction + + y_limits[metric] = (min_val, max_val) + + for metric_idx, metric in enumerate(metrics): + ax = axes[action_idx, metric_idx] if num_rows > 1 else axes[metric_idx] + data_subset = dapp_data[dapp_data["action"] == action].copy() + + # normalize data + need_to_add_rows = len(unique_timestamps) - len(data_subset) + if need_to_add_rows: + for i, unique_timestamp in enumerate(unique_timestamps): + if unique_timestamp not in data_subset["timestamp"].values: + new_row = pd.DataFrame( + data=[{ + "timestamp": unique_timestamp, + "tag": x_tick_labels[i], + "dapp_name": data_subset.iloc[0]["dapp_name"], + "action": data_subset.iloc[0]["action"], + }], + columns=data_subset.columns, + ) + data_subset = pd.concat([data_subset, new_row], ignore_index=True) + + data_subset = data_subset.sort_values(by='timestamp').reset_index(drop=True) + + if not data_subset.empty: + prev_value = None + prev_is_valid = True + + # Fill data for smooth lines + data_subset_filled = data_subset.copy() + + for column in data_subset.columns: + for i in range(1, len(data_subset) - 1): + if pd.isna(data_subset_filled.loc[i, column]): + above_value = data_subset_filled.loc[i - 1, column] + below_value = data_subset_filled.loc[i + 1, column] + if pd.notna(above_value) and pd.notna(below_value): + try: # for numerical data + data_subset_filled.loc[i, column] = (above_value + below_value) / 2 + except TypeError: # for non-numerical data + data_subset_filled.loc[i, column] = above_value or below_value + + # Plot grey lines before scatter + ax.plot( + data_subset_filled.index, + data_subset_filled[metric], + color="darkgrey", + linestyle="-", + linewidth=2, + zorder=1, + ) + + # Plot blue or red dots + for i, (x, y) in enumerate(zip(data_subset.index, data_subset[metric])): + if not pd.isna(y): + # last 2 dots should be larger + dot_size = 50 if i < len(data_subset[metric]) - 2 else 150 + if prev_value is not None and y != prev_value: + # next data point has different value compared to the previous one + ax.scatter(x, y, color="red", zorder=2, s=dot_size) + ax.annotate( + f"{y}", + (x, y), + textcoords="offset points", + xytext=(15, 5), + ha="center", + color="red", + rotation=60, + ) + + if prev_is_valid: + # if the previous data point had the same value as the preceding one + ax.annotate( + f"{prev_value}", + (prev_x, prev_y), + textcoords="offset points", + xytext=(15, 5), + ha="center", + color="blue", + rotation=60, + ) + prev_is_valid = False + else: + ax.scatter(x, y, color="blue", s=dot_size) + prev_is_valid = True + + prev_value = y + prev_x, prev_y = x, y + + # Set x-axis ticks and labels + ax.set_xticks(range(len(x_tick_labels))) + ax.set_xticklabels(x_tick_labels, rotation=45) + + # Set y-axis limits and labels + ax.tick_params(axis="y", labelsize=8) + ax.set_ylim(float(y_limits[metric][0]), float(y_limits[metric][1])) + has_decimals = any(Decimal(str(value)) % 1 != 0 for value in data_subset[metric]) + if has_decimals: + ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.3f")) + else: + ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.0f")) + + if metric_idx == 0: + multiline_action = textwrap.fill(action, width=30) + ax.set_ylabel(multiline_action) + + if action_idx == 0: + ax.set_title(metric) + + # add vertical grey dotted line before the last two dots + if len(data_subset[metric]) > 2: + ax.axvline(x=len(data_subset[metric]) - 2.5, color='#a6a4a4', linestyle=':') + + plt.tight_layout() + plt.subplots_adjust(top=0.9) + pdf.savefig(fig) + plt.close(fig) + + return output_pdf diff --git a/docs/tests/audit/erc/test_EIP1559.md b/docs/tests/audit/erc/test_EIP1559.md new file mode 100644 index 0000000000..7c22fe25c7 --- /dev/null +++ b/docs/tests/audit/erc/test_EIP1559.md @@ -0,0 +1,33 @@ +# Overview + +Test suite for EIP-1559 + +# Tests list + +| Test case | Description | XFailed | +|--------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|---------| +| TestEIP1559::test_transfer_positive | Verifies successful execution of a positive transfer using EIP-1559 transaction type, ensuring correct fee calculation and gas management. | | +| TestEIP1559::test_deploy_positive | Tests successful deployment of a contract using EIP-1559 transaction type with accurate fee calculation and gas management. | | +| TestEIP1559::test_contract_function_call_positive | Validates the invocation of a contract function with correct parameters using EIP-1559 transactions. | | +| TestEIP1559::test_transfer_negative | Tests various negative scenarios for transfer transactions with incorrect parameters, ensuring proper error handling. | | +| TestEIP1559::test_deploy_negative | Evaluates various negative scenarios for contract deployment with incorrect parameters, ensuring proper error handling. | | +| TestEIP1559::test_insufficient_funds | Checks handling of transactions with insufficient funds, verifying appropriate error messages are returned. | | +| TestRpcMaxPriorityFeePerGas::test_positive | Verifies correctness of retrieving max priority fee per gas using RPC methods in EIP-1559 context. | | +| TestRpcFeeHistory::test_positive_first_block | Tests retrieval of fee history from the first block, ensuring accurate data is returned. | | +| TestRpcFeeHistory::test_positive_zero_block_count | Validates fee history retrieval with a zero block count, ensuring no data is returned. | | +| TestRpcFeeHistory::test_positive_fewer_blocks_than_count | Checks fee history retrieval with fewer blocks than specified count, ensuring accurate data is returned. | | +| TestRpcFeeHistory::test_positive_earliest_block | Verifies accurate retrieval of fee history from the earliest block available. | | +| TestRpcFeeHistory::test_positive_pending_block | Tests fee history retrieval for a pending block, ensuring correct handling of pending status. | | +| TestRpcFeeHistory::test_positive_max_blocks | Checks maximum number of blocks allowed for fee history retrieval, ensuring correct data handling. | | +| TestRpcFeeHistory::test_negative_cases | Evaluates various negative cases for fee history retrieval, ensuring proper error codes are returned as expected. | | +| TestRpcNeonMethods::test_neon_get_transaction_by_sender_nonce | Verifies correct retrieval of transactions by sender nonce using Neon methods. | | +| TestRpcNeonMethods::test_neon_get_solana_transaction_by_neon_transaction | Tests the retrieval of Solana transactions using Neon transaction data, ensuring accurate mapping. | | +| TestRpcNeonMethods::test_neon_get_transaction_receipt | Validates the retrieval of transaction receipts using Neon methods, ensuring accurate data is returned. | | +| TestRpcEthMethods::test_get_transaction_by_hash | Verifies correct retrieval of transactions by hash using Eth methods, ensuring data integrity. | | +| TestRpcEthMethods::test_get_transaction_by_block_hash_and_index | Tests retrieval of transactions by block hash and index using Eth methods, ensuring accurate data is returned. | | +| TestRpcEthMethods::test_get_transaction_by_block_number_and_index | Checks retrieval of transactions by block number and index using Eth methods, ensuring accurate data is returned. | | +| TestRpcEthMethods::test_get_block_by_hash | Validates retrieval of blocks by hash using Eth methods, ensuring accurate data is returned. | | +| TestRpcEthMethods::test_get_block_by_number | Tests retrieval of blocks by block number using Eth methods, ensuring accurate data is returned. | | +| TestRpcEthMethods::test_get_transaction_receipt | Checks retrieval of transaction receipts using Eth methods, ensuring accurate data is returned. | | +| TestAccessList::test_transfer | Verifies proper handling of access list entries in transfer transactions, ensuring correct data management. | | +| TestAccessList::test_deploy | Tests correct application of access list entries during contract deployment, ensuring accurate transaction handling. | | diff --git a/docs/tests/audit/evm/opcodes/test_base_opcodes.md b/docs/tests/audit/evm/opcodes/test_base_opcodes.md index 872d55fd3b..c1b4a8d22c 100644 --- a/docs/tests/audit/evm/opcodes/test_base_opcodes.md +++ b/docs/tests/audit/evm/opcodes/test_base_opcodes.md @@ -4,9 +4,12 @@ Verify basic opcodes # Tests list -| Test case | Description | XFailed | -|----------------------------------|----------------------------------|---------| -| TestOpCodes::test_base_opcodes | Execute base opcodes in contract | | -| TestOpCodes::test_stop | Execute stop opcode | | -| TestOpCodes::test_invalid_opcode | Execute invalid opcode | | -| TestOpCodes::test_revert | Execute revert opcode | | +| Test case | Description | XFailed | +|---------------------------------------------|------------------------------------------------------------------|---------| +| TestOpCodes::test_base_opcodes | Execute base opcodes in contract | | +| TestOpCodes::test_stop | Execute stop opcode | | +| TestOpCodes::test_invalid_opcode | Execute invalid opcode | | +| TestOpCodes::test_revert | Execute revert opcode | | +| TestBASEFEEOpcode::test_base_fee_call | Verifies accuracy of base fee retrieval for call function | | +| TestBASEFEEOpcode::test_base_fee_trx_type_0 | Verifies accuracy of base fee retrieval for an old transaction | | +| TestBASEFEEOpcode::test_base_fee_trx_type_2 | Verifies accuracy of base fee retrieval for a type 2 transaction | | diff --git a/envs.json b/envs.json index de01c0ae9d..a2c3c82ec5 100644 --- a/envs.json +++ b/envs.json @@ -127,5 +127,20 @@ "spl_neon_mint": "HPsV9Deocecw3GeZv1FkAPNCBRfuVyfw9MMwjwRe1xaU", "neon_erc20wrapper_address": "0x053e3d1b12726f648B2e45CEAbDF9078B742576D", "use_bank": false + }, + "docker_net": { + "evm_loader": "53DfF883gyixYNXnM7s5xhdeyV8mVk9T4i2hGV9vG9io", + "proxy_url": "http://proxy:9090/solana", + "network_ids": { + "neon": 111, + "sol": 112, + "usdt": 113, + "eth": 114 + }, + "solana_url": "http://solana:8899/", + "faucet_url": "http://faucet:3333/", + "tracer_url": "http://tracer:9091", + "spl_neon_mint": "HPsV9Deocecw3GeZv1FkAPNCBRfuVyfw9MMwjwRe1xaU", + "neon_erc20wrapper_address": "0x053e3d1b12726f648B2e45CEAbDF9078B742576D" } } \ No newline at end of file diff --git a/integration/tests/basic/erc/conftest.py b/integration/tests/basic/erc/conftest.py index 5c3474e235..f0909f0ceb 100644 --- a/integration/tests/basic/erc/conftest.py +++ b/integration/tests/basic/erc/conftest.py @@ -1,9 +1,7 @@ -import random -import string - import pytest from _pytest.config import Config -from solana.publickey import PublicKey +from solders.keypair import Keypair +from solders.pubkey import Pubkey from solana.rpc.types import TxOpts from solana.transaction import Transaction from spl.token.instructions import ( @@ -16,24 +14,26 @@ @pytest.fixture(scope="function") -def solana_associated_token_mintable_erc20(erc20_spl_mintable, sol_client, solana_account): - token_mint = PublicKey(erc20_spl_mintable.contract.functions.tokenMint().call()) +def solana_associated_token_mintable_erc20( + erc20_spl_mintable, sol_client, solana_account: Keypair +) -> tuple[Keypair, Pubkey, Pubkey]: + token_mint = Pubkey(erc20_spl_mintable.contract.functions.tokenMint().call()) trx = Transaction() - trx.add(create_associated_token_account(solana_account.public_key, solana_account.public_key, token_mint)) + trx.add(create_associated_token_account(solana_account.pubkey(), solana_account.pubkey(), token_mint)) opts = TxOpts(skip_preflight=True, skip_confirmation=False) sol_client.send_transaction(trx, solana_account, opts=opts) - solana_address = get_associated_token_address(solana_account.public_key, token_mint) + solana_address = get_associated_token_address(solana_account.pubkey(), token_mint) yield solana_account, token_mint, solana_address @pytest.fixture(scope="function") -def solana_associated_token_erc20(erc20_spl, sol_client, solana_account): +def solana_associated_token_erc20(erc20_spl, sol_client, solana_account: Keypair) -> tuple[Keypair, Pubkey, Pubkey]: token_mint = erc20_spl.token_mint.pubkey trx = Transaction() - trx.add(create_associated_token_account(solana_account.public_key, solana_account.public_key, token_mint)) + trx.add(create_associated_token_account(solana_account.pubkey(), solana_account.pubkey(), token_mint)) opts = TxOpts(skip_preflight=True, skip_confirmation=False) sol_client.send_transaction(trx, solana_account, opts=opts) - solana_address = get_associated_token_address(solana_account.public_key, token_mint) + solana_address = get_associated_token_address(solana_account.pubkey(), token_mint) yield solana_account, token_mint, solana_address @@ -58,6 +58,7 @@ def invalid_nft_receiver(web3_client_session, faucet, accounts): ) return contract + @pytest.fixture(scope="class") def multiple_actions_erc20(web3_client_session, accounts, erc20_spl_mintable): contract, contract_deploy_tx = web3_client_session.deploy_and_get_contract( diff --git a/integration/tests/basic/erc/test_EIP1559.py b/integration/tests/basic/erc/test_EIP1559.py new file mode 100644 index 0000000000..72ef61dea1 --- /dev/null +++ b/integration/tests/basic/erc/test_EIP1559.py @@ -0,0 +1,895 @@ +import time +import typing as tp + +import base58 +import rlp +import allure +import pytest +import web3 +import web3.types +from solders.signature import Signature +from eth_account.signers.local import LocalAccount +from web3._utils.fee_utils import _fee_history_priority_fee_estimate # noqa +from web3.contract import Contract +from web3.exceptions import TimeExhausted + +from utils import helpers +from utils.apiclient import JsonRPCSession +from utils.faucet import Faucet +from utils.models.fee_history_model import EthFeeHistoryResult +from utils.solana_client import SolanaClient +from utils.types import TransactionType +from utils.web3client import NeonChainWeb3Client, Web3Client +from utils.accounts import EthAccounts + + +TX_TIMEOUT = 10 + +NEGATIVE_PARAMETERS = ( + "max_priority_fee_per_gas, max_fee_per_gas, expected_exception, exception_message_regex", + [ + ( # Zero values + 0, + 0, + Exception, + None, + ), + ( # Negative values + -1000, + -500, + rlp.exceptions.ObjectSerializationError, + r'Serialization failed because of field maxPriorityFeePerGas \("Cannot serialize negative integers"\)', + ), + ( # Large values (potential overflow) + 2 ** 256, + 2 ** 256, + ValueError, + "{'code': -32000, 'message': '.+'}", + ), + ( # Fractional values + 1.5, + 1.5, + TypeError, + "Transaction had invalid fields: {'maxPriorityFeePerGas': 1.5, 'maxFeePerGas': 1.5}", + ), + ( # Invalid types + "invalid", + "invalid", + TypeError, + "Transaction had invalid fields: {'maxPriorityFeePerGas': 'invalid', 'maxFeePerGas': 'invalid'}", + ), + ( # Mismatched values + 100000000000, + 50000000000, + Exception, + r"{'code': -32000, 'message': .*}", + ), + ( # Underpriced + 10000000000, + 5000000000, + Exception, + r"{'code': -32000, 'message': .*}", + ), + ( # Zero base fee + 0, + 1000000000, + Exception, + None + ), + ( # Missing max_priority_fee_per_gas + None, + 1000000000, + TypeError, + r"Missing kwargs: \['maxPriorityFeePerGas'\]", + ), + ( # Missing max_fee_per_gas + 1000000000, + None, + TypeError, + r"Missing kwargs: \['maxFeePerGas'\]", + ), + ], +) + + +def validate_transfer_positive( + accounts: EthAccounts, + web3_client: NeonChainWeb3Client, + access_list: tp.Optional[list[web3.types.AccessListEntry]], +): + sender = accounts[0] + recipient = web3_client.create_account() + + balance_sender_before = web3_client.get_balance(sender.address) + balance_recipient_before = web3_client.get_balance(recipient.address) + + latest_block: web3.types.BlockData = web3_client._web3.eth.get_block(block_identifier="latest") # noqa + base_fee_per_gas = latest_block.baseFeePerGas # noqa + max_priority_fee_per_gas = web3_client._web3.eth._max_priority_fee() or 21283 # noqa + base_fee_multiplier = 1.1 + max_fee_per_gas = int((base_fee_multiplier * base_fee_per_gas) + max_priority_fee_per_gas) + + value = balance_sender_before // 2 + + tx_params = web3_client.make_raw_tx_eip_1559( + chain_id="auto", + from_=sender.address, + to=recipient.address, + value=value, + nonce="auto", + gas="auto", + max_priority_fee_per_gas=max_priority_fee_per_gas, + max_fee_per_gas=max_fee_per_gas, + base_fee_multiplier=base_fee_multiplier, + data=None, + access_list=access_list, + ) + + receipt = web3_client.send_transaction(account=sender, transaction=tx_params, timeout=15) + assert receipt.type == 2 + + balance_sender_after = web3_client.get_balance(sender.address) + balance_recipient_after = web3_client.get_balance(recipient.address) + estimated_gas = tx_params["gas"] + block_gas_limit = latest_block["gasLimit"] + gas_used = receipt["gasUsed"] + effective_gas_price = receipt["effectiveGasPrice"] + cumulative_gas_used = receipt["cumulativeGasUsed"] + total_fee_paid = gas_used * effective_gas_price + expected_balance_sender_after = balance_sender_before - value - total_fee_paid + + # Validate the base fee + block = web3_client._web3.eth.get_block(receipt['blockNumber']) # noqa + assert block['baseFeePerGas'] <= base_fee_per_gas * base_fee_multiplier + + assert balance_sender_after == expected_balance_sender_after, ( + f"Expected sender balance: {expected_balance_sender_after}, " + f"Actual sender balance: {balance_sender_after}" + ) + assert balance_recipient_after == balance_recipient_before + value, ( + f"Expected recipient balance: {balance_recipient_before + value}, " + f"Actual recipient balance: {balance_recipient_after}" + ) + + # Verify that the effective gas price does not exceed the max fee per gas + assert effective_gas_price <= max_fee_per_gas, ( + f"Effective gas price: {effective_gas_price}, Max fee per gas: {max_fee_per_gas}" + ) + + # Validate gas used does not exceed the estimated gas + assert gas_used <= estimated_gas, ( + f"Gas used: {gas_used}, Estimated gas: {estimated_gas}" + ) + + # Validate cumulative gas used does not exceed block gas limit + assert cumulative_gas_used <= block_gas_limit, ( + f"Cumulative gas used: {cumulative_gas_used}, Block gas limit: {block_gas_limit}" + ) + + +def validate_deploy_positive( + accounts: EthAccounts, + web3_client: NeonChainWeb3Client, + access_list: tp.Optional[list[web3.types.AccessListEntry]], +): + account = accounts[0] + balance_before = web3_client.get_balance(account.address) + + contract_iface = helpers.get_contract_interface( + contract="common/Common.sol", + version="0.8.12", + contract_name="Common", + ) + + latest_block: web3.types.BlockData = web3_client._web3.eth.get_block(block_identifier="latest") # noqa + base_fee_per_gas = latest_block.baseFeePerGas # noqa + max_priority_fee_per_gas = web3_client._web3.eth._max_priority_fee() # noqa + max_fee_per_gas = (5 * base_fee_per_gas) + max_priority_fee_per_gas + + tx_params = web3_client.make_raw_tx_eip_1559( + chain_id="auto", + from_=account.address, + to=None, + value=0, + nonce="auto", + gas="auto", + max_priority_fee_per_gas=max_priority_fee_per_gas, + max_fee_per_gas=max_fee_per_gas, + data=contract_iface["bin"], + access_list=access_list, + ) + + receipt = web3_client.send_transaction(account=account, transaction=tx_params) + assert receipt.type == 2 + contract = web3_client._web3.eth.contract(address=receipt["contractAddress"], abi=contract_iface["abi"]) # noqa + + assert contract.address == receipt["contractAddress"], "Contract deployment failed" + + balance_after = web3_client.get_balance(account.address) + estimated_gas = tx_params["gas"] + block_gas_limit = latest_block["gasLimit"] + gas_used = receipt["gasUsed"] + effective_gas_price = receipt["effectiveGasPrice"] + cumulative_gas_used = receipt["cumulativeGasUsed"] + total_fee_paid = gas_used * effective_gas_price + + # Validate that sender's balance decreased by at least the gas fee + assert balance_before - balance_after >= total_fee_paid, "Sender balance did not decrease by gas fee" + + # Verify that the effective gas price does not exceed the max fee per gas + assert effective_gas_price <= max_fee_per_gas, ( + f"Effective gas price: {effective_gas_price}, Max fee per gas: {max_fee_per_gas}" + ) + + # Validate gas used does not exceed the estimated gas + assert gas_used <= estimated_gas, ( + f"Gas used: {gas_used}, Estimated gas: {estimated_gas}" + ) + + # Validate cumulative gas used does not exceed block gas limit + assert cumulative_gas_used <= block_gas_limit, ( + f"Cumulative gas used: {cumulative_gas_used}, Block gas limit: {block_gas_limit}" + ) + + +@allure.feature("EIP Verifications") +@allure.story("EIP-1559: New Transaction Type Support in Neon") +@pytest.mark.usefixtures("accounts", "web3_client") +class TestEIP1559: + web3_client: NeonChainWeb3Client + accounts: EthAccounts + + def test_transfer_positive(self): + validate_transfer_positive( + accounts=self.accounts, + web3_client=self.web3_client, + access_list=None, + ) + + @pytest.mark.neon_only + def test_transfer_invalid_chain_id_negative( + self, + json_rpc_client: JsonRPCSession, + ): + sender = self.accounts[0] + recipient = self.web3_client.create_account() + + tx_params = self.web3_client.make_raw_tx_eip_1559( + chain_id=None, + from_=sender.address, + to=recipient.address, + value=10000, + nonce="auto", + gas="auto", + max_priority_fee_per_gas="auto", + max_fee_per_gas="auto", + data=None, + access_list=None, + ) + + # sign_transaction automatically sets chainId to 0 + signed_tx = self.web3_client._web3.eth.account.sign_transaction(tx_params, sender.key) + response = json_rpc_client.send_rpc(method="eth_sendRawTransaction", params=signed_tx.rawTransaction.hex()) + assert "result" not in response + assert "error" in response + assert response["error"]["code"] == -32000 + + def test_deploy_positive(self): + validate_deploy_positive( + accounts=self.accounts, + web3_client=self.web3_client, + access_list=None, + ) + + def test_contract_function_call_positive( + self, + ): + account = self.accounts[0] + contract_a, _ = self.web3_client.deploy_and_get_contract( + contract="EIPs/EIP161/contract_a_function.sol", + version="0.8.12", + account=account, + contract_name="ContractA", + ) + + tx_params = contract_a.functions.deploy_contract().build_transaction() + tx_params["nonce"] = self.web3_client.get_nonce(account) + + tx_receipt = self.web3_client.send_transaction( + account=account, + transaction=tx_params, + ) + + assert tx_receipt.type == 2 + assert tx_receipt.status == 1, "ContractB was not deployed" + assert tx_receipt.logs, "ContractB was not deployed" + contract_b_address = tx_receipt.logs[0].address + + contract_a_nonce = self.web3_client.get_nonce(contract_a.address) + assert contract_a_nonce == 2 + + contract_b: Contract = self.web3_client.get_deployed_contract( + address=contract_b_address, + contract_file="EIPs/EIP161/contract_b.sol", + contract_name="ContractB", + ) + + data = contract_b.functions.getOne().build_transaction()["data"] + tx_params = self.web3_client.make_raw_tx_eip_1559( + chain_id="auto", + from_=account.address, + to=contract_b.address, + value=0, + nonce="auto", + gas="auto", + max_priority_fee_per_gas="auto", + max_fee_per_gas="auto", + data=data, + access_list=None, + ) + + tx_receipt = self.web3_client.send_transaction(account, tx_params) + assert tx_receipt.type == 2 + result = int(tx_receipt.logs[0].topics[1].hex(), 16) + assert result == 1 + + @pytest.mark.parametrize(*NEGATIVE_PARAMETERS) + def test_transfer_negative( + self, + max_priority_fee_per_gas, + max_fee_per_gas, + expected_exception, + exception_message_regex, + ): + sender = self.accounts[1] + recipient = self.web3_client.create_account() + value = 1 + + tx_params = self.web3_client.make_raw_tx_eip_1559( + chain_id="auto", + from_=sender.address, + to=recipient.address, + value=value, + nonce="auto", + gas="auto", + max_priority_fee_per_gas=max_priority_fee_per_gas, + max_fee_per_gas=max_fee_per_gas, + data=None, + access_list=None, + ) + + with pytest.raises(expected_exception=expected_exception, match=exception_message_regex): + self.web3_client.send_transaction( + account=sender, + transaction=tx_params, + timeout=TX_TIMEOUT, + ) + + @pytest.mark.parametrize(*NEGATIVE_PARAMETERS) + def test_deploy_negative( + self, + max_priority_fee_per_gas, + max_fee_per_gas, + expected_exception, + exception_message_regex, + ): + account = self.accounts[3] + + contract_iface = helpers.get_contract_interface( + contract="common/Common.sol", + version="0.8.12", + contract_name="Common", + ) + + tx_params = self.web3_client.make_raw_tx_eip_1559( + chain_id="auto", + from_=account.address, + to=None, + value=0, + nonce="auto", + gas="auto", + max_priority_fee_per_gas=max_priority_fee_per_gas, + max_fee_per_gas=max_fee_per_gas, + data=contract_iface["bin"], + access_list=None, + ) + + with pytest.raises(expected_exception=expected_exception, match=exception_message_regex): + self.web3_client.send_transaction( + account=account, + transaction=tx_params, + timeout=TX_TIMEOUT, + ) + + def test_insufficient_funds( + self, + ): + sender = self.accounts[0] + balance = self.web3_client.get_balance(sender.address) + recipient = self.web3_client.create_account() + + tx_params = self.web3_client.make_raw_tx_eip_1559( + chain_id="auto", + from_=sender.address, + to=recipient.address, + value=balance, + nonce="auto", + gas="auto", + max_priority_fee_per_gas="auto", + max_fee_per_gas="auto", + data=None, + access_list=None, + ) + + error_msg_regex = r"{'code': -32000, 'message': 'insufficient funds for.+'}" + with pytest.raises(expected_exception=ValueError, match=error_msg_regex): + self.web3_client.send_transaction(account=sender, transaction=tx_params) + + def test_too_low_fee( + self, + faucet: Faucet, + ): + sender = self.web3_client.create_account_with_balance(faucet=faucet) + recipient = self.web3_client.create_account() + + base_fee_per_gas = self.web3_client.base_fee_per_gas() + + tx_params = self.web3_client.make_raw_tx_eip_1559( + chain_id="auto", + from_=sender.address, + to=recipient.address, + value=1000000, + nonce="auto", + gas="auto", + max_priority_fee_per_gas=0, + max_fee_per_gas=int(base_fee_per_gas * 0.8), + data=None, + access_list=None, + ) + + error_msg_regex = rf".+ not in the chain after \d+ seconds" + with pytest.raises(expected_exception=TimeExhausted, match=error_msg_regex): + self.web3_client.send_transaction(account=sender, transaction=tx_params, timeout=TX_TIMEOUT) + + @pytest.mark.neon_only + @pytest.mark.parametrize("max_priority_fee_per_gas, base_fee_multiplier", + [(1000000000, 1.1), (1000, 1.5)]) + def test_compute_unit_price( + self, + accounts: EthAccounts, + web3_client: NeonChainWeb3Client, + json_rpc_client: JsonRPCSession, + sol_client: SolanaClient, + max_priority_fee_per_gas, + base_fee_multiplier + ): + sender = accounts[0] + recipient = accounts[1] + + latest_block: web3.types.BlockData = web3_client._web3.eth.get_block(block_identifier="latest") # noqa + base_fee_per_gas = latest_block.baseFeePerGas # noqa + max_fee_per_gas = int((base_fee_multiplier * base_fee_per_gas) + max_priority_fee_per_gas) + + value = 1029380121 + + tx_params = web3_client.make_raw_tx_eip_1559( + chain_id="auto", + from_=sender.address, + to=recipient.address, + value=value, + nonce="auto", + gas="auto", + max_priority_fee_per_gas=max_priority_fee_per_gas, + max_fee_per_gas=max_fee_per_gas, + base_fee_multiplier=base_fee_multiplier, + data=None, + access_list=None, + ) + + receipt = web3_client.send_transaction(account=sender, transaction=tx_params) + assert receipt.type == 2 + solana_transaction_hash = web3_client.get_solana_trx_by_neon(receipt["transactionHash"].hex())["result"][0] + solana_transaction = sol_client.get_transaction_with_wait(Signature.from_string(solana_transaction_hash)) + + data_list = [instr.data for instr in solana_transaction.value.transaction.transaction.message.instructions] + + cu_price = None + for data in data_list: + instruction_code = base58.b58decode(data).hex()[0:2] + if instruction_code == "03": + cu_price = int.from_bytes(bytes.fromhex(base58.b58decode(data).hex()[2:]), "little") + + if cu_price is not None: + assert cu_price > 0 + else: + raise Exception(f"Compute Budget instruction is not found in Solana transaction {solana_transaction}") + + +@allure.feature("EIP Verifications") +@allure.story("EIP-1559: Verify JSON-RPC method eth_maxPriorityFeePerGas") +@pytest.mark.usefixtures("eip1559_setup") +class TestRpcMaxPriorityFeePerGas: + + @pytest.mark.need_eip1559_blocks(10) + def test_positive( + self, + json_rpc_client: JsonRPCSession, + web3_client: NeonChainWeb3Client, + ): + + response = json_rpc_client.send_rpc(method="eth_maxPriorityFeePerGas") + assert "error" not in response, response["error"] + max_priority_fee_per_gas = int(response["result"], 16) + + fee_history: web3.types.FeeHistory = web3_client._web3.eth.fee_history(10, "pending", [5]) + estimated_max_priority_fee_per_gas = _fee_history_priority_fee_estimate(fee_history=fee_history) + assert abs(max_priority_fee_per_gas - estimated_max_priority_fee_per_gas) <= 2000000000 + + +@allure.feature("EIP Verifications") +@allure.story("EIP-1559: Verify JSON-RPC method eth_feeHistory") +@pytest.mark.usefixtures("eip1559_setup") +@pytest.mark.neon_only +class TestRpcFeeHistory: + """ + eth_feeHistory + + parameters: + - blockCount + - newestBlock + - rewardPercentiles + + response: + - oldestBlock: Lowest number block of the returned range expressed as a hexadecimal number. + - baseFeePerGas: An array of block base fees per gas, including an extra block value. + The extra value is the next block after the newest block in the returned range. + Returns zeroes for blocks created before EIP-1559. + - gasUsedRatio: An array of block gas used ratios. These are calculated as the ratio of gasUsed and gasLimit. + - reward: An array of effective priority fee per gas data points from a single block. + All zeroes are returned if the block is empty. + """ + + @pytest.fixture(scope="class") + def first_block_number( + self, + web3_client: NeonChainWeb3Client, + ) -> int: + block = web3_client._web3.eth.get_block(block_identifier="earliest") + return block.number + + @pytest.mark.need_eip1559_blocks(1) + def test_positive_first_block( + self, + json_rpc_client: JsonRPCSession, + first_block_number: int, + ): + block_count = 20 + newest_block = first_block_number + reward_percentiles = [10, 50, 90] + + response = json_rpc_client.send_rpc( + method="eth_feeHistory", + params=[ + hex(block_count), + hex(newest_block), + reward_percentiles + ] + ) + + assert "error" not in response, response["error"] + fee_history = EthFeeHistoryResult(**response["result"]) + + assert len(fee_history.baseFeePerGas) == 2 + assert len(fee_history.gasUsedRatio) == 1 + assert int(fee_history.oldestBlock, 16) == newest_block + assert len(fee_history.reward) == 1 + + for block in fee_history.reward: + assert len(block) == len(reward_percentiles) + for tx_reward in block: + reward = int(tx_reward, 16) + assert reward >= 0 + + @pytest.mark.neon_only + def test_positive_zero_block_count( + self, + json_rpc_client: JsonRPCSession, + ): + block_count = 0 + newest_block = "latest" + reward_percentiles = [25, 50, 75] + + response = json_rpc_client.send_rpc( + method="eth_feeHistory", + params=[ + hex(block_count), + newest_block, + reward_percentiles + ] + ) + + assert "error" not in response, response["error"] + fee_history = EthFeeHistoryResult(**response["result"]) + + assert len(fee_history.baseFeePerGas) == 1 + assert int(fee_history.baseFeePerGas[0], 16) > 0 + + assert fee_history.gasUsedRatio == [] + assert int(fee_history.oldestBlock, 16) == 0 + assert fee_history.reward == [] + + @pytest.mark.neon_only + @pytest.mark.need_eip1559_blocks(3) + @pytest.mark.parametrize("reward_percentiles", ([], [50])) + def test_positive_fewer_blocks_than_count( + self, + json_rpc_client: JsonRPCSession, + first_block_number: int, + reward_percentiles: list[int] + ): + expected_block_count = 3 + newest_block = first_block_number + expected_block_count - 1 + block_count = first_block_number + expected_block_count - 1 + 10 + + response = json_rpc_client.send_rpc( + method="eth_feeHistory", + params=[ + hex(block_count), + hex(newest_block), + reward_percentiles + ] + ) + + assert "error" not in response, response["error"] + fee_history = EthFeeHistoryResult(**response["result"]) + + assert len(fee_history.baseFeePerGas) == expected_block_count + 1, fee_history.baseFeePerGas + assert len(fee_history.gasUsedRatio) == expected_block_count, fee_history.gasUsedRatio + + oldest_block = int(fee_history.oldestBlock, 16) + assert oldest_block == first_block_number, oldest_block + + if not reward_percentiles: + assert fee_history.reward is None, fee_history.reward + else: + assert len(fee_history.reward) == expected_block_count + + @pytest.mark.need_eip1559_blocks(1) + def test_positive_earliest_block( + self, + json_rpc_client: JsonRPCSession, + first_block_number: int, + ): + block_count = 1 + newest_block = "earliest" + reward_percentiles = [50] + + response = json_rpc_client.send_rpc( + method="eth_feeHistory", + params=[ + hex(block_count), + newest_block, + reward_percentiles + ] + ) + + assert "error" not in response, response["error"] + fee_history = EthFeeHistoryResult(**response["result"]) + + assert len(fee_history.baseFeePerGas) == 2, fee_history.baseFeePerGas + assert len(fee_history.gasUsedRatio) == 1, fee_history.gasUsedRatio + oldest_block = int(fee_history.oldestBlock, 16) + assert oldest_block == first_block_number, oldest_block + assert len(fee_history.reward) == 1, fee_history.reward + + for block in fee_history.reward: + assert len(block) == len(reward_percentiles) + for tx_reward in block: + reward = int(tx_reward, 16) + assert reward >= 0 + + @pytest.mark.need_eip1559_blocks(10) + def test_positive_pending_block( + self, + json_rpc_client: JsonRPCSession, + web3_client: NeonChainWeb3Client, + ): + block_count = 10 + newest_block = "pending" + reward_percentiles = [5] + + response = json_rpc_client.send_rpc( + method="eth_feeHistory", + params=[ + hex(block_count), + newest_block, + reward_percentiles + ] + ) + + assert "error" not in response, response["error"] + fee_history = EthFeeHistoryResult(**response["result"]) + + assert len(fee_history.baseFeePerGas) <= block_count + 1 + for base_fee_per_gas in fee_history.baseFeePerGas: + assert int(base_fee_per_gas, 16) > 0 + + assert len(fee_history.gasUsedRatio) == block_count + for gas_used_ratio in fee_history.gasUsedRatio: + assert gas_used_ratio >= 0 + + oldest_block_actual = int(fee_history.oldestBlock, 16) + oldest_block_expected = web3_client._web3.eth.get_block(block_identifier="pending")["number"] - block_count + assert oldest_block_actual - oldest_block_expected <= 1 # a new block may be added since feeHistory request + + assert len(fee_history.reward) == block_count + if fee_history.reward is not None: + for block in fee_history.reward: + for tx_reward in block: + reward = int(tx_reward, 16) + assert reward >= 0 + + @pytest.mark.need_eip1559_blocks(10) + @pytest.mark.parametrize("block_count", [1024, 1025]) + def test_positive_max_blocks( + self, + json_rpc_client: JsonRPCSession, + web3_client: NeonChainWeb3Client, + first_block_number: int, + block_count: int, + ): + newest_block = "latest" + reward_percentiles = [5, 25, 50, 75, 90] + last_block_number = web3_client._web3.eth.get_block(block_identifier="latest")["number"] + blocks_in_chain = last_block_number - first_block_number + 1 + expected_block_count = min(1024, blocks_in_chain) + + response = json_rpc_client.send_rpc( + method="eth_feeHistory", + params=[ + hex(block_count), + newest_block, + reward_percentiles + ] + ) + + assert "error" not in response, response["error"] + fee_history = EthFeeHistoryResult(**response["result"]) + oldest_block_actual = int(fee_history.oldestBlock, 16) + oldest_block_expected = last_block_number - expected_block_count + assert 0 <= oldest_block_actual - oldest_block_expected <= 5 # a few new blocks may be added + + assert 0 <= len(fee_history.baseFeePerGas) - (expected_block_count + 1) <= 2 + for base_fee_per_gas in fee_history.baseFeePerGas: + assert int(base_fee_per_gas, 16) > 0 + + assert len(fee_history.gasUsedRatio) - expected_block_count <= 2 + for gas_used_ratio in fee_history.gasUsedRatio: + assert gas_used_ratio >= 0 + + assert fee_history.reward is not None + assert len(fee_history.reward) - expected_block_count <= 2 + + for block in fee_history.reward: + assert len(block) - len(reward_percentiles) <= 2 + for tx_reward in block: + reward = int(tx_reward, 16) + assert reward >= 0 + + @pytest.mark.parametrize( + argnames=("block_count", "newest_block", "reward_percentiles", "error_code"), + argvalues=( + (1, "unknown", [], -32602), # Invalid newest block + (100, "latest", [90, 50, 10], -32000), # Non-monotonic reward percentiles + ), + ) + def test_negative_cases( + self, + json_rpc_client: JsonRPCSession, + block_count: int, + newest_block: str, + reward_percentiles: list[int], + error_code: int, + ): + response = json_rpc_client.send_rpc( + method="eth_feeHistory", + params=[ + hex(block_count), + newest_block, + reward_percentiles + ] + ) + assert "result" not in response, response["result"] + assert "error" in response + assert response["error"]["code"] == error_code + + +@allure.feature("EIP Verifications") +@allure.story("EIP-1559: Verify accessList does not break transactions") +@pytest.mark.usefixtures("accounts", "web3_client") +class TestAccessList: + web3_client: NeonChainWeb3Client + accounts: EthAccounts + + @pytest.fixture(scope="class") + def access_list(self) -> list[web3.types.AccessListEntry]: + return [ + web3.types.AccessListEntry( + address=web3.types.HexStr("0x0000000000000000000000000000000000000000"), + storageKeys=[ + web3.types.HexStr("0x0000000000000000000000000000000000000000000000000000000000000000"), + web3.types.HexStr("0x0000000000000000000000000000000000000000000000000000000000000001"), + web3.types.HexStr("0x0000000000000000000000000000000000000000000000000000000000000002"), + ], + ), + web3.types.AccessListEntry( + address=web3.types.HexStr("0x0000000000000000000000000000000000000001"), + storageKeys=[ + web3.types.HexStr("0x0000000000000000000000000000000000000000000000000000000000000000"), + web3.types.HexStr("0x0000000000000000000000000000000000000000000000000000000000000001"), + web3.types.HexStr("0x0000000000000000000000000000000000000000000000000000000000000002"), + ], + ), + ] + + def test_transfer( + self, + access_list: list[web3.types.AccessListEntry], + ): + validate_transfer_positive( + accounts=self.accounts, + web3_client=self.web3_client, + access_list=access_list, + ) + + def test_deploy( + self, + access_list: list[web3.types.AccessListEntry], + ): + validate_deploy_positive( + accounts=self.accounts, + web3_client=self.web3_client, + access_list=access_list, + ) + + +@allure.feature("EIP Verifications") +@allure.story("EIP-1559: multiple tokens") +@pytest.mark.neon_only +class TestMultipleTokens: + + @pytest.mark.multipletokens + def test_transfer_positive( + self, + web3_client: NeonChainWeb3Client, + web3_client_sol: Web3Client, + sol_client: SolanaClient, # noqa + account_with_all_tokens: LocalAccount, + class_account_sol_chain: LocalAccount, + ): + alice_neon_balance_before = web3_client.get_balance(account_with_all_tokens) + bob_neon_balance_before = web3_client.get_balance(class_account_sol_chain) + alice_sol_balance_before = web3_client_sol.get_balance(account_with_all_tokens) + bob_sol_balance_before = web3_client_sol.get_balance(class_account_sol_chain) + + value = 1000 + receipt = web3_client_sol.send_tokens( + from_=class_account_sol_chain, + to=account_with_all_tokens, + value=value, + tx_type=TransactionType.EIP_1559, + ) + + assert receipt["status"] == 1 + + # Make sure SOL balances changed + bob_sol_balance_after = web3_client_sol.get_balance(class_account_sol_chain) + alice_sol_balance_after = web3_client_sol.get_balance(account_with_all_tokens) + assert alice_sol_balance_after == alice_sol_balance_before + value + assert bob_sol_balance_after < bob_sol_balance_before - value + + # Make sure NEON balances did not change + alice_neon_balance_after = web3_client.get_balance(account_with_all_tokens) + bob_neon_balance_after = web3_client.get_balance(class_account_sol_chain) + assert alice_neon_balance_after == alice_neon_balance_before + assert bob_neon_balance_after == bob_neon_balance_before diff --git a/integration/tests/basic/erc/test_ERC20SPL.py b/integration/tests/basic/erc/test_ERC20SPL.py index 0b1d7bca11..854537955f 100644 --- a/integration/tests/basic/erc/test_ERC20SPL.py +++ b/integration/tests/basic/erc/test_ERC20SPL.py @@ -6,11 +6,14 @@ from _pytest.config import Config from solana.rpc.types import TokenAccountOpts, TxOpts from solana.transaction import Transaction +from solders.keypair import Keypair +from solders.pubkey import Pubkey from spl.token import instructions from spl.token.constants import TOKEN_PROGRAM_ID -from utils import metaplex +from utils import metaplex, stats_collector from utils.consts import ZERO_ADDRESS +from utils.erc20wrapper import ERC20Wrapper from utils.helpers import gen_hash_of_block, wait_condition, create_invalid_address from utils.web3client import NeonChainWeb3Client from utils.solana_client import SolanaClient @@ -35,7 +38,7 @@ class TestERC20SPL: sol_client: SolanaClient @pytest.fixture(scope="class") - def erc20_contract(self, erc20_spl, eth_bank_account, pytestconfig: Config): + def erc20_contract(self, erc20_spl, eth_bank_account, pytestconfig: Config) -> ERC20Wrapper: if pytestconfig.getoption("--network") == "mainnet": self.web3_client.send_neon(eth_bank_account, erc20_spl.account.address, 10) return erc20_spl @@ -80,6 +83,7 @@ def test_name(self, erc20_contract): name = erc20_contract.contract.functions.name().call() assert name == erc20_contract.name + @pytest.mark.cost_report def test_burn(self, erc20_contract, restore_balance): balance_before = erc20_contract.contract.functions.balanceOf(erc20_contract.account.address).call() total_before = erc20_contract.contract.functions.totalSupply().call() @@ -153,6 +157,7 @@ def test_burnFrom_no_enough_gas(self, erc20_contract, param, msg): with pytest.raises(ValueError, match=msg): erc20_contract.burn_from(new_account, erc20_contract.account.address, 1, **param) + @pytest.mark.cost_report def test_approve_more_than_total_supply(self, erc20_contract): new_account = self.accounts[0] amount = erc20_contract.contract.functions.totalSupply().call() + 1 @@ -193,6 +198,7 @@ def test_allowance_for_new_account(self, erc20_contract): ).call() assert allowance == 0 + @pytest.mark.cost_report def test_transfer(self, erc20_contract, restore_balance): new_account = self.accounts.create_account() balance_acc1_before = erc20_contract.contract.functions.balanceOf(erc20_contract.account.address).call() @@ -313,20 +319,20 @@ def test_transferSolana( self, erc20_contract, sol_client, - solana_associated_token_erc20, + solana_associated_token_erc20: tuple[Keypair, Pubkey, Pubkey], ): acc, token_mint, solana_address = solana_associated_token_erc20 amount = random.randint(10000, 1000000) - sol_balance_before = sol_client.get_balance(acc.public_key).value + sol_balance_before = sol_client.get_balance(acc.pubkey()).value contract_balance_before = erc20_contract.contract.functions.balanceOf(erc20_contract.account.address).call() opts = TokenAccountOpts(token_mint) - token_data = sol_client.get_token_accounts_by_owner_json_parsed(acc.public_key, opts).value[0] + token_data = sol_client.get_token_accounts_by_owner_json_parsed(acc.pubkey(), opts).value[0] token_balance_before = token_data.account.data.parsed["info"]["tokenAmount"]["amount"] erc20_contract.transfer_solana(erc20_contract.account, bytes(solana_address), amount) wait_condition( lambda: int( - sol_client.get_token_accounts_by_owner_json_parsed(acc.public_key, opts) + sol_client.get_token_accounts_by_owner_json_parsed(acc.pubkey(), opts) .value[0] .account.data.parsed["info"]["tokenAmount"]["amount"] ) @@ -334,8 +340,8 @@ def test_transferSolana( timeout_sec=30, ) - sol_balance_after = sol_client.get_balance(acc.public_key).value - token_data = sol_client.get_token_accounts_by_owner_json_parsed(acc.public_key, opts).value[0] + sol_balance_after = sol_client.get_balance(acc.pubkey()).value + token_data = sol_client.get_token_accounts_by_owner_json_parsed(acc.pubkey(), opts).value[0] token_balance_after = token_data.account.data.parsed["info"]["tokenAmount"]["amount"] contract_balance_after = erc20_contract.contract.functions.balanceOf(erc20_contract.account.address).call() @@ -349,25 +355,26 @@ def test_approveSolana( self, erc20_contract, sol_client, - solana_associated_token_erc20, + solana_associated_token_erc20: tuple[Keypair, Pubkey, Pubkey], ): acc, token_mint, solana_address = solana_associated_token_erc20 amount = random.randint(10000, 1000000) opts = TokenAccountOpts(token_mint) - erc20_contract.approve_solana(erc20_contract.account, bytes(acc.public_key), amount) + erc20_contract.approve_solana(erc20_contract.account, bytes(acc.pubkey()), amount) wait_condition( - lambda: len(sol_client.get_token_accounts_by_delegate_json_parsed(acc.public_key, opts).value) > 0, + lambda: len(sol_client.get_token_accounts_by_delegate_json_parsed(acc.pubkey(), opts).value) > 0, timeout_sec=30, ) - token_account = sol_client.get_token_accounts_by_delegate_json_parsed(acc.public_key, opts).value[0].account + token_account = sol_client.get_token_accounts_by_delegate_json_parsed(acc.pubkey(), opts).value[0].account assert int(token_account.data.parsed["info"]["delegatedAmount"]["amount"]) == amount assert int(token_account.data.parsed["info"]["delegatedAmount"]["decimals"]) == erc20_contract.decimals + @pytest.mark.cost_report def test_claim( self, erc20_contract, sol_client, - solana_associated_token_erc20, + solana_associated_token_erc20: tuple[Keypair, Pubkey, Pubkey], pytestconfig, ): acc, token_mint, solana_address = solana_associated_token_erc20 @@ -385,7 +392,7 @@ def test_claim( erc20_contract.contract.address, pytestconfig.environment.evm_loader, ), - owner=acc.public_key, + owner=acc.pubkey(), amount=sent_amount, signers=[], ) @@ -399,7 +406,9 @@ def test_claim( assert balance_after == balance_before - sent_amount + claim_amount, "Balance is not correct" - def test_claimTo(self, erc20_contract, sol_client, solana_associated_token_erc20, pytestconfig): + def test_claimTo( + self, erc20_contract, sol_client, solana_associated_token_erc20: tuple[Keypair, Pubkey, Pubkey], pytestconfig + ): new_account = self.accounts.create_account() acc, token_mint, solana_address = solana_associated_token_erc20 user1_balance_before = erc20_contract.contract.functions.balanceOf(erc20_contract.account.address).call() @@ -417,7 +426,7 @@ def test_claimTo(self, erc20_contract, sol_client, solana_associated_token_erc20 erc20_contract.contract.address, pytestconfig.environment.evm_loader, ), - owner=acc.public_key, + owner=acc.pubkey(), amount=sent_amount, signers=[], ) @@ -438,7 +447,7 @@ def test_claimTo(self, erc20_contract, sol_client, solana_associated_token_erc20 @allure.story("ERC20SPL: Tests for ERC20ForSPLMintable contract") @pytest.mark.usefixtures("accounts", "web3_client", "sol_client") @pytest.mark.neon_only -class TestERC20SPLMintable(TestERC20SPL): +class TestERC20SPLMintable: web3_client: NeonChainWeb3Client accounts: EthAccounts sol_client: SolanaClient @@ -479,7 +488,7 @@ def test_transferOwnership(self, erc20_contract, accounts, return_ownership): assert owner == accounts[2].address def test_metaplex_data(self, erc20_contract): - mint_key = erc20_contract.contract.functions.findMintAccount().call() + mint_key = Pubkey(erc20_contract.contract.functions.findMintAccount().call()) metaplex.wait_account_info(self.sol_client, mint_key) metadata = metaplex.get_metadata(self.sol_client, mint_key) assert metadata["data"]["name"] == erc20_contract.name @@ -494,6 +503,7 @@ def test_mint_to_self(self, erc20_contract, restore_balance): balance_after = erc20_contract.contract.functions.balanceOf(erc20_contract.account.address).call() assert balance_after == balance_before + amount + @pytest.mark.cost_report def test_mint_to_another_account(self, erc20_contract): new_account = self.accounts.create_account(0) amount = random.randint(1, MAX_TOKENS_AMOUNT) @@ -553,19 +563,21 @@ def test_totalSupply(self, erc20_contract): total_after = erc20_contract.contract.functions.totalSupply().call() assert total_before + amount == total_after, "Total supply is not correct" - def test_transferSolana(self, sol_client, erc20_contract, solana_associated_token_mintable_erc20): + def test_transferSolana( + self, sol_client, erc20_contract, solana_associated_token_mintable_erc20: tuple[Keypair, Pubkey, Pubkey] + ): acc, token_mint, solana_address = solana_associated_token_mintable_erc20 amount = random.randint(10000, 1000000) - sol_balance_before = sol_client.get_balance(acc.public_key).value + sol_balance_before = sol_client.get_balance(acc.pubkey()).value contract_balance_before = erc20_contract.contract.functions.balanceOf(erc20_contract.account.address).call() opts = TokenAccountOpts(token_mint) - token_data = sol_client.get_token_accounts_by_owner_json_parsed(acc.public_key, opts).value[0] + token_data = sol_client.get_token_accounts_by_owner_json_parsed(acc.pubkey(), opts).value[0] token_balance_before = token_data.account.data.parsed["info"]["tokenAmount"]["amount"] erc20_contract.transfer_solana(erc20_contract.account, bytes(solana_address), amount) wait_condition( lambda: int( - sol_client.get_token_accounts_by_owner_json_parsed(acc.public_key, opts) + sol_client.get_token_accounts_by_owner_json_parsed(acc.pubkey(), opts) .value[0] .account.data.parsed["info"]["tokenAmount"]["amount"] ) @@ -573,8 +585,8 @@ def test_transferSolana(self, sol_client, erc20_contract, solana_associated_toke timeout_sec=30, ) - sol_balance_after = sol_client.get_balance(acc.public_key).value - token_data = sol_client.get_token_accounts_by_owner_json_parsed(acc.public_key, opts).value[0] + sol_balance_after = sol_client.get_balance(acc.pubkey()).value + token_data = sol_client.get_token_accounts_by_owner_json_parsed(acc.pubkey(), opts).value[0] token_balance_after = token_data.account.data.parsed["info"]["tokenAmount"]["amount"] contract_balance_after = erc20_contract.contract.functions.balanceOf(erc20_contract.account.address).call() @@ -588,17 +600,17 @@ def test_approveSolana( self, erc20_contract, sol_client, - solana_associated_token_mintable_erc20, + solana_associated_token_mintable_erc20: tuple[Keypair, Pubkey, Pubkey], ): acc, token_mint, solana_address = solana_associated_token_mintable_erc20 amount = random.randint(10000, 1000000) opts = TokenAccountOpts(token_mint) - erc20_contract.approve_solana(erc20_contract.account, bytes(acc.public_key), amount) + erc20_contract.approve_solana(erc20_contract.account, bytes(acc.pubkey()), amount) wait_condition( - lambda: len(sol_client.get_token_accounts_by_delegate_json_parsed(acc.public_key, opts).value) > 0, + lambda: len(sol_client.get_token_accounts_by_delegate_json_parsed(acc.pubkey(), opts).value) > 0, timeout_sec=30, ) - token_account = sol_client.get_token_accounts_by_delegate_json_parsed(acc.public_key, opts).value[0].account + token_account = sol_client.get_token_accounts_by_delegate_json_parsed(acc.pubkey(), opts).value[0].account assert int(token_account.data.parsed["info"]["delegatedAmount"]["amount"]) == amount assert int(token_account.data.parsed["info"]["delegatedAmount"]["decimals"]) == erc20_contract.decimals @@ -606,7 +618,7 @@ def test_claim( self, erc20_contract, sol_client, - solana_associated_token_mintable_erc20, + solana_associated_token_mintable_erc20: tuple[Keypair, Pubkey, Pubkey], pytestconfig, ): acc, token_mint, solana_address = solana_associated_token_mintable_erc20 @@ -624,7 +636,7 @@ def test_claim( erc20_contract.contract.address, pytestconfig.environment.evm_loader, ), - owner=acc.public_key, + owner=acc.pubkey(), amount=sent_amount, signers=[], ) @@ -642,7 +654,7 @@ def test_claimTo( self, erc20_contract, sol_client, - solana_associated_token_mintable_erc20, + solana_associated_token_mintable_erc20: tuple[Keypair, Pubkey, Pubkey], pytestconfig, ): acc, token_mint, solana_address = solana_associated_token_mintable_erc20 @@ -662,7 +674,7 @@ def test_claimTo( erc20_contract.contract.address, pytestconfig.environment.evm_loader, ), - owner=acc.public_key, + owner=acc.pubkey(), amount=sent_amount, signers=[], ) diff --git a/integration/tests/basic/erc/test_ERC721.py b/integration/tests/basic/erc/test_ERC721.py index 925513b457..b03b6effe4 100644 --- a/integration/tests/basic/erc/test_ERC721.py +++ b/integration/tests/basic/erc/test_ERC721.py @@ -7,7 +7,7 @@ import pytest import web3 import web3.exceptions -from solana.publickey import PublicKey +from solders.pubkey import Pubkey from solana.rpc.types import TokenAccountOpts, TxOpts from solana.transaction import Transaction from spl.token.instructions import ( @@ -16,7 +16,7 @@ ) from integration.tests.basic.helpers.assert_message import ErrorMessage -from utils import metaplex +from utils import metaplex, stats_collector from utils.accounts import EthAccounts from utils.consts import ZERO_ADDRESS from utils.erc721ForMetaplex import ERC721ForMetaplex @@ -45,25 +45,26 @@ class TestERC721: sol_client: SolanaClient @pytest.fixture(scope="function") - def token_id(self, erc721, web3_client): + def token_id(self, erc721, web3_client) -> int: seed = web3_client.text_to_bytes32(gen_hash_of_block(8)) uri = generate_text(min_len=10, max_len=200) - token_id = erc721.mint(seed, erc721.account.address, uri) - yield token_id + token_id_ = erc721.mint(seed, erc721.account.address, uri) + yield token_id_ @allure.step("Check metaplex data") - def metaplex_checks(self, token_id): + def metaplex_checks(self, token_id: int): solana_acc = base58.b58encode(token_id.to_bytes(32, "big")).decode("utf-8") - metaplex.wait_account_info(self.sol_client, solana_acc) - metadata = metaplex.get_metadata(self.sol_client, solana_acc) + metaplex.wait_account_info(self.sol_client, Pubkey.from_string(solana_acc)) + metadata = metaplex.get_metadata(self.sol_client, Pubkey.from_string(solana_acc)) assert metadata["mint"] == solana_acc.encode("utf-8") assert metadata["data"]["name"] == "Metaplex" assert metadata["data"]["symbol"] == "MPL" + @pytest.mark.cost_report def test_mint(self, erc721): seed = self.web3_client.text_to_bytes32(gen_hash_of_block(8)) uri = generate_text(min_len=10, max_len=200) - token_id = erc721.mint(seed, erc721.account.address, uri) + token_id: int = erc721.mint(seed, erc721.account.address, uri) self.metaplex_checks(token_id) def test_mint_with_used_seed(self, erc721, accounts): @@ -219,6 +220,7 @@ def test_transferFrom_no_enough_gas(self, erc721, token_id, accounts, param, msg **param, ) + @pytest.mark.cost_report def test_transferFrom_with_approval(self, erc721, token_id, accounts): recipient = accounts[2] @@ -436,25 +438,25 @@ def test_getApproved_incorrect_token(self, erc721): @pytest.mark.xfail(reason="NDEV-1333") def test_transferSolanaFrom(self, erc721, token_id, sol_client, solana_account): acc = solana_account - token_mint = PublicKey(base58.b58encode(token_id.to_bytes(32, "big")).decode("utf-8")) + token_mint = Pubkey(token_id.to_bytes(32, "big")) trx = Transaction() - trx.add(create_associated_token_account(acc.public_key, acc.public_key, token_mint)) + trx.add(create_associated_token_account(acc.pubkey(), acc.pubkey(), token_mint)) opts = TxOpts(skip_preflight=False, skip_confirmation=False) sol_client.send_transaction(trx, acc, opts=opts) - solana_address = bytes(get_associated_token_address(acc.public_key, token_mint)) + solana_address = bytes(get_associated_token_address(acc.pubkey(), token_mint)) erc721.transfer_solana_from(erc721.account.address, solana_address, token_id, erc721.account) opts = TokenAccountOpts(token_mint) wait_condition( lambda: int( - sol_client.get_token_accounts_by_owner_json_parsed(acc.public_key, opts) + sol_client.get_token_accounts_by_owner_json_parsed(acc.pubkey(), opts) .value[0] .account.data.parsed["info"]["tokenAmount"]["amount"] ) > 0 ) - token_data = sol_client.get_token_accounts_by_owner_json_parsed(acc.public_key, opts).value[0] + token_data = sol_client.get_token_accounts_by_owner_json_parsed(acc.pubkey(), opts).value[0] token_amount = token_data.account.data.parsed["info"]["tokenAmount"] assert int(token_amount["amount"]) == 1 assert int(token_amount["decimals"]) == 0 @@ -538,7 +540,7 @@ def test_mint_mint_transfer_transfer(self, multiple_actions_erc721): assert contract_balance == contract_balance_before, "Contract balance is not correct" def test_mint_mint_transfer_transfer_different_accounts(self, multiple_actions_erc721): - recipient= self.accounts[1] + recipient = self.accounts[1] sender_account = self.accounts[0] acc, contract = multiple_actions_erc721 diff --git a/integration/tests/basic/evm/conftest.py b/integration/tests/basic/evm/conftest.py index 64d96b4fb0..e1b58f9fd7 100644 --- a/integration/tests/basic/evm/conftest.py +++ b/integration/tests/basic/evm/conftest.py @@ -1,12 +1,7 @@ -import json -import pathlib - import pytest -from solana.publickey import PublicKey +from solders.pubkey import Pubkey from web3.contract import Contract -from packaging import version -from clickfile import network_manager -from utils import helpers, web3client +from utils import helpers from utils.accounts import EthAccounts from utils.solana_client import SolanaClient from utils.web3client import Web3Client @@ -87,7 +82,7 @@ def max_non_existent_solana_address( while address_exists: address_uint_256 -= 1 - pubkey = PublicKey(address_uint_256.to_bytes(32, byteorder='big')) + pubkey = Pubkey(address_uint_256.to_bytes(32, byteorder='big')) address_exists = sol_client_session.get_account_info(pubkey=pubkey).value is not None return address_uint_256 diff --git a/integration/tests/basic/evm/opcodes/test_base_opcodes.py b/integration/tests/basic/evm/opcodes/test_base_opcodes.py index f5ff5c283a..557b6ed2c0 100644 --- a/integration/tests/basic/evm/opcodes/test_base_opcodes.py +++ b/integration/tests/basic/evm/opcodes/test_base_opcodes.py @@ -4,6 +4,7 @@ from utils.accounts import EthAccounts from utils.consts import ZERO_HASH +from utils.types import TransactionType from utils.web3client import NeonChainWeb3Client @@ -14,6 +15,26 @@ class TestOpCodes: web3_client: NeonChainWeb3Client accounts: EthAccounts + @pytest.fixture(scope="class") + def mcopy_checker(self, web3_client, faucet, accounts): + contract, _ = web3_client.deploy_and_get_contract( + "opcodes/EIP5656MCopy", + "0.8.25", + accounts[0], + contract_name="MemoryCopy", + ) + return contract + + @pytest.fixture(scope="class") + def basefee_checker(self, web3_client, accounts): + contract, _ = web3_client.deploy_and_get_contract( + contract="opcodes/EIP1559BaseFee.sol", + contract_name="BaseFeeOpcode", + version="0.8.19", + account=accounts[0], + ) + return contract + def test_base_opcodes(self, opcodes_checker): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(sender_account) @@ -40,16 +61,6 @@ def test_revert(self, opcodes_checker): with pytest.raises(web3.exceptions.ContractLogicError, match="execution reverted"): opcodes_checker.functions.test_revert().build_transaction(tx) - @pytest.fixture(scope="class") - def mcopy_checker(self, web3_client, faucet, accounts): - contract, _ = web3_client.deploy_and_get_contract( - "opcodes/EIP5656MCopy", - "0.8.25", - accounts[0], - contract_name="MemoryCopy", - ) - return contract - @pytest.mark.proxy_version("v1.12.0") @pytest.mark.parametrize( "dst, src, length, expected_result", @@ -97,3 +108,42 @@ def test_tstore(self, accounts): self.web3_client.send_transaction(sender_account, instr) result = contract.functions.read().call() assert result.hex() == ZERO_HASH + + def test_base_fee_call( + self, + web3_client: NeonChainWeb3Client, + accounts: EthAccounts, + basefee_checker + ): + base_fee_contract = basefee_checker.functions.baseFee().call( + transaction={ + "gasPrice": 1_234_456 + }) + # put some random trivial gas_price + assert base_fee_contract == 1_234_456 + + def test_base_fee_trx_type_0( + self, + web3_client: NeonChainWeb3Client, + accounts: EthAccounts, + basefee_checker, + ): + tx = web3_client.make_raw_tx(accounts[0]) + instruction_tx = basefee_checker.functions.baseFeeTrx().build_transaction(tx) + resp = web3_client.send_transaction(accounts[0], instruction_tx) + base_fee_from_log = basefee_checker.events.Log().process_receipt(resp)[0]['args']['baseFee'] + assert base_fee_from_log == web3_client.gas_price() + + def test_base_fee_trx_type_2( + self, + web3_client: NeonChainWeb3Client, + accounts: EthAccounts, + basefee_checker, + ): + tx = web3_client.make_raw_tx(accounts[0], tx_type=TransactionType.EIP_1559) + instruction_tx = basefee_checker.functions.baseFeeTrx().build_transaction(tx) + instruction_tx["maxFeePerGas"] = 2000000000 + instruction_tx["maxPriorityFeePerGas"] = 2000000 + resp = web3_client.send_transaction(accounts[0], instruction_tx) + base_fee_from_log = basefee_checker.events.Log().process_receipt(resp)[0]['args']['baseFee'] + assert base_fee_from_log == instruction_tx["maxFeePerGas"] - instruction_tx["maxPriorityFeePerGas"] diff --git a/integration/tests/basic/evm/opcodes/test_unsupported_opcodes.py b/integration/tests/basic/evm/opcodes/test_unsupported_opcodes.py index ae77b03347..6be82a02c1 100644 --- a/integration/tests/basic/evm/opcodes/test_unsupported_opcodes.py +++ b/integration/tests/basic/evm/opcodes/test_unsupported_opcodes.py @@ -12,9 +12,6 @@ def contract(self, web3_client, accounts): contract, _ = web3_client.deploy_and_get_contract("opcodes/UnsupportedOpcodes", "0.8.10", accounts[0]) return contract - def test_basefee(self, contract): - assert contract.functions.baseFee().call() == 0 - def test_coinbase(self, contract): assert contract.functions.coinbase().call() == ZERO_ADDRESS diff --git a/integration/tests/basic/evm/test_get_block_hash.py b/integration/tests/basic/evm/test_get_block_hash.py index 4f946a67ed..5fb0c07fd7 100644 --- a/integration/tests/basic/evm/test_get_block_hash.py +++ b/integration/tests/basic/evm/test_get_block_hash.py @@ -1,6 +1,6 @@ import allure import pytest -from solana.transaction import PublicKey +from solders.pubkey import Pubkey from hexbytes import HexBytes from utils.accounts import EthAccounts @@ -33,16 +33,16 @@ def test_get_current_block_hash(self, blockhash_contract): ) def _get_slot_hash(self, number: int) -> HexBytes: - slot_hashes_id = PublicKey("SysvarS1otHashes111111111111111111111111111") + slot_hashes_id = Pubkey.from_string("SysvarS1otHashes111111111111111111111111111") account_info = self.sol_client.get_account_info(slot_hashes_id, "confirmed").value count = int.from_bytes(account_info.data[:8], "little") for i in range(0, count): offset = 8 + 40 * i - slot = int.from_bytes(account_info.data[offset : (offset + 8)], "little") + slot = int.from_bytes(account_info.data[offset: (offset + 8)], "little") if slot != number: continue - return HexBytes(account_info.data[(offset + 8) : (offset + 40)]) + return HexBytes(account_info.data[(offset + 8): (offset + 40)]) assert False, "Slot not found" diff --git a/integration/tests/basic/evm/test_metaplex.py b/integration/tests/basic/evm/test_metaplex.py index cf8ddd9ee7..70d5f2bd25 100644 --- a/integration/tests/basic/evm/test_metaplex.py +++ b/integration/tests/basic/evm/test_metaplex.py @@ -1,6 +1,6 @@ import allure import pytest -from solana.keypair import Keypair +from solders.keypair import Keypair from utils.accounts import EthAccounts from utils.web3client import NeonChainWeb3Client @@ -19,14 +19,14 @@ class TestPrecompiledMetaplex: @pytest.fixture(scope="class") def mint_id(self, web3_client, accounts, metaplex_caller): - mint = Keypair.generate() + mint = Keypair() tx = { "from": accounts[0].address, "nonce": web3_client.eth.get_transaction_count(accounts[0].address), "gasPrice": web3_client.gas_price(), } instruction_tx = metaplex_caller.functions.callCreateMetadata( - bytes(mint.public_key), NAME, SYMBOL, URI + bytes(mint.pubkey()), NAME, SYMBOL, URI ).build_transaction(tx) resp = web3_client.send_transaction(accounts[0], instruction_tx) log = metaplex_caller.events.LogBytes().process_receipt(resp)[0] @@ -35,9 +35,9 @@ def mint_id(self, web3_client, accounts, metaplex_caller): def test_create_metadata(self, metaplex): sender_account = self.accounts[0] - mint = Keypair.generate() + mint = Keypair() tx = self.web3_client.make_raw_tx(sender_account) - instruction_tx = metaplex.functions.createMetadata(bytes(mint.public_key), NAME, SYMBOL, URI).build_transaction( + instruction_tx = metaplex.functions.createMetadata(bytes(mint.pubkey()), NAME, SYMBOL, URI).build_transaction( tx ) @@ -46,9 +46,9 @@ def test_create_metadata(self, metaplex): def test_create_master_edition(self, metaplex): sender_account = self.accounts[0] - mint = Keypair.generate() + mint = Keypair() tx = self.web3_client.make_raw_tx(sender_account) - instruction_tx = metaplex.functions.createMasterEdition(bytes(mint.public_key), 0).build_transaction(tx) + instruction_tx = metaplex.functions.createMasterEdition(bytes(mint.pubkey()), 0).build_transaction(tx) receipt = self.web3_client.send_transaction(sender_account, instruction_tx) assert receipt["status"] == 1 diff --git a/integration/tests/basic/evm/test_query_account.py b/integration/tests/basic/evm/test_query_account.py index ce034a7a54..bc65e818a5 100644 --- a/integration/tests/basic/evm/test_query_account.py +++ b/integration/tests/basic/evm/test_query_account.py @@ -2,9 +2,9 @@ import base58 import pytest -from solana.publickey import PublicKey +from solders.pubkey import Pubkey from web3.contract import Contract -from solana.keypair import Keypair +from solders.keypair import Keypair from utils.accounts import EthAccounts from utils.helpers import wait_condition @@ -23,16 +23,16 @@ def test_owner_positive( new_solana_account = sol_client_session.create_account( payer=solana_account, size=0, - owner=solana_account.public_key, + owner=solana_account.pubkey(), ) - solana_account_address_uint256 = int.from_bytes(new_solana_account.public_key, byteorder="big") + solana_account_address_uint256 = int.from_bytes(new_solana_account.pubkey(), byteorder="big") success, actual_owner_address = query_account_caller_contract.functions.queryOwner( solana_account_address_uint256 ).call() - expected_owner_address = int.from_bytes(solana_account.public_key, byteorder="big") + expected_owner_address = int.from_bytes(solana_account.pubkey(), byteorder="big") assert success is True assert actual_owner_address == expected_owner_address @@ -48,10 +48,10 @@ def test_owner_through_transaction_positive( new_solana_account = sol_client_session.create_account( payer=solana_account, size=0, - owner=solana_account.public_key, + owner=solana_account.pubkey(), ) - solana_account_address_uint256 = int.from_bytes(new_solana_account.public_key, byteorder="big") + solana_account_address_uint256 = int.from_bytes(new_solana_account.pubkey(), byteorder="big") tx = web3_client.make_raw_tx(account) instruction_tx = query_account_caller_contract.functions.queryOwner( @@ -63,7 +63,7 @@ def test_owner_through_transaction_positive( log_processed = query_account_caller_contract.events.QueryResultUint256().process_log(log_raw) success = log_processed.args.success actual_owner_address = log_processed.args.result - expected_owner_address = int.from_bytes(solana_account.public_key, byteorder="big") + expected_owner_address = int.from_bytes(solana_account.pubkey(), byteorder="big") assert success is True assert actual_owner_address == expected_owner_address @@ -91,10 +91,10 @@ def test_length_positive( new_solana_account = sol_client_session.create_account( payer=solana_account, size=expected_length, - owner=solana_account.public_key, + owner=solana_account.pubkey(), ) - solana_account_address_uint256 = int.from_bytes(new_solana_account.public_key, byteorder="big") + solana_account_address_uint256 = int.from_bytes(new_solana_account.pubkey(), byteorder="big") success, actual_length = query_account_caller_contract.functions.queryLength( solana_account_address_uint256 @@ -128,11 +128,11 @@ def test_lamports_positive( new_solana_account = sol_client_session.create_account( payer=solana_account, size=size, - owner=solana_account.public_key, + owner=solana_account.pubkey(), lamports=expected_lamports_before, ) - solana_account_address_uint256 = int.from_bytes(new_solana_account.public_key, byteorder="big") + solana_account_address_uint256 = int.from_bytes(new_solana_account.pubkey(), byteorder="big") success, actual_lamports_before = query_account_caller_contract.functions.queryLamports( solana_account_address_uint256 @@ -142,7 +142,7 @@ def test_lamports_positive( assert actual_lamports_before == expected_lamports_before additional_lamports = random.randint(0, 1000) - sol_client_session.request_airdrop(pubkey=new_solana_account.public_key, lamports=additional_lamports) + sol_client_session.request_airdrop(pubkey=new_solana_account.pubkey(), lamports=additional_lamports) expected_lamports_after = expected_lamports_before + additional_lamports success, actual_lamports_after = query_account_caller_contract.functions.queryLamports( @@ -188,7 +188,7 @@ def test_executable_false( solana_account: Keypair, sol_client_session: SolanaClient, ): - solana_account_address_uint256 = int.from_bytes(solana_account.public_key, byteorder="big") + solana_account_address_uint256 = int.from_bytes(solana_account.pubkey(), byteorder="big") success, is_executable = query_account_caller_contract.functions.queryExecutable( solana_account_address_uint256 @@ -216,7 +216,7 @@ def test_rent_epoch_positive( query_account_caller_contract: Contract, sol_client_session: SolanaClient, ): - solana_account_address_uint256 = int.from_bytes(solana_account.public_key, byteorder="big") + solana_account_address_uint256 = int.from_bytes(solana_account.pubkey(), byteorder="big") success, rent_epoch_actual = query_account_caller_contract.functions.queryRentEpoch( solana_account_address_uint256 @@ -225,9 +225,9 @@ def test_rent_epoch_positive( assert success is True wait_condition( - func_cond=lambda: sol_client_session.get_account_info(solana_account.public_key).value is not None, + func_cond=lambda: sol_client_session.get_account_info(solana_account.pubkey()).value is not None, ) - account_info = sol_client_session.get_account_info(solana_account.public_key) + account_info = sol_client_session.get_account_info(solana_account.pubkey()) rent_epoch_expected = account_info.value.rent_epoch assert rent_epoch_actual == rent_epoch_expected @@ -250,11 +250,11 @@ def test_data_positive( query_account_caller_contract: Contract, sol_client_session: SolanaClient, ): - evm_loader_address_base58 = request.config.environment.evm_loader # noqa + evm_loader_address_base58: str = request.config.environment.evm_loader # noqa evm_loader_address = base58.b58decode(evm_loader_address_base58) solana_account_address_uint256 = int.from_bytes(evm_loader_address, byteorder="big") - evm_loader_public_key = PublicKey(evm_loader_address_base58) + evm_loader_public_key = Pubkey(evm_loader_address) length = len(sol_client_session.get_account_info(evm_loader_public_key).value.data) success, actual_data = query_account_caller_contract.functions.queryData( @@ -282,7 +282,7 @@ def test_data_through_transaction_positive( evm_loader_address = base58.b58decode(evm_loader_address_base58) solana_account_address_uint256 = int.from_bytes(evm_loader_address, byteorder="big") - evm_loader_public_key = PublicKey(evm_loader_address_base58) + evm_loader_public_key = Pubkey(evm_loader_address) length = len(sol_client_session.get_account_info(evm_loader_public_key).value.data) account = accounts[0] @@ -330,7 +330,7 @@ def test_data_negative_invalid_offset( evm_loader_address = base58.b58decode(evm_loader_address_base58) solana_account_address_uint256 = int.from_bytes(evm_loader_address, byteorder="big") - length = len(sol_client_session.get_account_info(PublicKey(evm_loader_address_base58)).value.data) + length = len(sol_client_session.get_account_info(Pubkey(evm_loader_address)).value.data) success, data = query_account_caller_contract.functions.queryData( solana_account_address_uint256, @@ -373,7 +373,7 @@ def test_data_negative_invalid_length( evm_loader_address = base58.b58decode(evm_loader_address_base58) solana_account_address_uint256 = int.from_bytes(evm_loader_address, byteorder="big") - length = len(sol_client_session.get_account_info(PublicKey(evm_loader_address_base58)).value.data) + length = len(sol_client_session.get_account_info(Pubkey(evm_loader_address)).value.data) success, data = query_account_caller_contract.functions.queryData( solana_account_address_uint256, diff --git a/integration/tests/basic/evm/test_solana_interoperability.py b/integration/tests/basic/evm/test_solana_interoperability.py index 727dd31e54..49b0262b5e 100644 --- a/integration/tests/basic/evm/test_solana_interoperability.py +++ b/integration/tests/basic/evm/test_solana_interoperability.py @@ -1,9 +1,12 @@ +import typing as tp + import pytest import spl -from solana.keypair import Keypair +from solders.keypair import Keypair from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts -from solana.transaction import AccountMeta, TransactionInstruction +from solana.transaction import AccountMeta, Instruction +from solders.pubkey import Pubkey from spl.token.client import Token as SplToken from spl.token.constants import TOKEN_PROGRAM_ID from spl.token.instructions import ( @@ -21,7 +24,7 @@ @pytest.fixture(scope="session") -def get_counter_value(): +def get_counter_value() -> tp.Iterator[int]: def gen_increment_counter(): count = 0 while True: @@ -40,15 +43,15 @@ class TestSolanaInteroperability: web3_client: NeonChainWeb3Client def test_counter_execute_with_get_return_data( - self, call_solana_caller, counter_resource_address, get_counter_value + self, call_solana_caller, counter_resource_address: bytes, get_counter_value ): sender = self.accounts[0] lamports = 0 - instruction = TransactionInstruction( + instruction = Instruction( program_id=COUNTER_ID, - keys=[ - AccountMeta(counter_resource_address, is_signer=False, is_writable=True), + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), ], data=bytes([0x1]), ) @@ -64,14 +67,14 @@ def test_counter_execute_with_get_return_data( assert int.from_bytes(event_logs[0].args.value, byteorder="little") == next(get_counter_value) assert bytes32_to_solana_pubkey(event_logs[0].args.program.hex()) == COUNTER_ID - def test_counter_with_seed(self, call_solana_caller, counter_resource_address, get_counter_value): + def test_counter_with_seed(self, call_solana_caller, counter_resource_address: bytes, get_counter_value): sender = self.accounts[0] lamports = 0 - instruction = TransactionInstruction( + instruction = Instruction( program_id=COUNTER_ID, - keys=[ - AccountMeta(counter_resource_address, is_signer=False, is_writable=True), + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), ], data=bytes([0x1]), ) @@ -85,14 +88,14 @@ def test_counter_with_seed(self, call_solana_caller, counter_resource_address, g event_logs = call_solana_caller.events.LogBytes().process_receipt(resp) assert int.from_bytes(event_logs[0].args.value, byteorder="little") == next(get_counter_value) - def test_counter_execute(self, call_solana_caller, counter_resource_address, get_counter_value): + def test_counter_execute(self, call_solana_caller, counter_resource_address: bytes, get_counter_value): sender = self.accounts[0] lamports = 0 - instruction = TransactionInstruction( + instruction = Instruction( program_id=COUNTER_ID, - keys=[ - AccountMeta(counter_resource_address, is_signer=False, is_writable=True), + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), ], data=bytes([0x1]), ) @@ -105,16 +108,16 @@ def test_counter_execute(self, call_solana_caller, counter_resource_address, get event_logs = call_solana_caller.events.LogBytes().process_receipt(resp) assert int.from_bytes(event_logs[0].args.value, byteorder="little") == next(get_counter_value) - def test_counter_batch_execute(self, call_solana_caller, counter_resource_address, get_counter_value): + def test_counter_batch_execute(self, call_solana_caller, counter_resource_address: bytes, get_counter_value): sender = self.accounts[0] call_params = [] current_counter = 0 for _ in range(10): - instruction = TransactionInstruction( + instruction = Instruction( program_id=COUNTER_ID, - keys=[ - AccountMeta(counter_resource_address, is_signer=False, is_writable=True), + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), ], data=bytes([0x1]), ) @@ -135,24 +138,24 @@ def test_transfer_with_pda_signature( self, call_solana_caller, sol_client, solana_account, pytestconfig, bank_account ): sender = self.accounts[0] - from_wallet = Keypair.generate() - to_wallet = Keypair.generate() + from_wallet = Keypair() + to_wallet = Keypair() amount = 100000 if pytestconfig.environment.use_bank: - sol_client.send_sol(bank_account, from_wallet.public_key, int(0.5 * 10**9)) + sol_client.send_sol(bank_account, from_wallet.pubkey(), int(0.5 * 10**9)) else: - sol_client.request_airdrop(from_wallet.public_key, 1000 * 10**9, commitment=Confirmed) + sol_client.request_airdrop(from_wallet.pubkey(), 1000 * 10**9, commitment=Confirmed) mint = spl.token.client.Token.create_mint( conn=sol_client, payer=from_wallet, - mint_authority=from_wallet.public_key, + mint_authority=from_wallet.pubkey(), decimals=9, program_id=TOKEN_PROGRAM_ID, ) mint.payer = from_wallet - from_token_account = mint.create_associated_token_account(from_wallet.public_key) - to_token_account = mint.create_associated_token_account(to_wallet.public_key) + from_token_account = mint.create_associated_token_account(from_wallet.pubkey()) + to_token_account = mint.create_associated_token_account(to_wallet.pubkey()) mint.mint_to( dest=from_token_account, mint_authority=from_wallet, @@ -160,22 +163,22 @@ def test_transfer_with_pda_signature( opts=TxOpts(skip_confirmation=False, skip_preflight=True), ) - authority_pubkey = call_solana_caller.functions.getSolanaPDA(bytes(TRANSFER_TOKENS_ID), b"authority").call() + authority_pubkey: bytes = call_solana_caller.functions.getSolanaPDA(bytes(TRANSFER_TOKENS_ID), b"authority").call() mint.set_authority( from_token_account, from_wallet, spl.token.instructions.AuthorityType.ACCOUNT_OWNER, - authority_pubkey, + Pubkey(authority_pubkey), opts=TxOpts(skip_confirmation=False, skip_preflight=True), ) - instruction = TransactionInstruction( + instruction = Instruction( program_id=TRANSFER_TOKENS_ID, - keys=[ + accounts=[ AccountMeta(from_token_account, is_signer=False, is_writable=True), AccountMeta(mint.pubkey, is_signer=False, is_writable=True), AccountMeta(to_token_account, is_signer=False, is_writable=True), - AccountMeta(authority_pubkey, is_signer=False, is_writable=True), + AccountMeta(Pubkey(authority_pubkey), is_signer=False, is_writable=True), AccountMeta(TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), ], data=bytes([0x0]), @@ -192,23 +195,23 @@ def test_transfer_with_pda_signature( def test_transfer_tokens_with_ext_authority(self, call_solana_caller, sol_client, pytestconfig, bank_account): sender = self.accounts[0] - from_wallet = Keypair.generate() - to_wallet = Keypair.generate() + from_wallet = Keypair() + to_wallet = Keypair() amount = 100000 if pytestconfig.environment.use_bank: - sol_client.send_sol(bank_account, from_wallet.public_key, int(0.5 * 10**9)) + sol_client.send_sol(bank_account, from_wallet.pubkey(), int(0.5 * 10**9)) else: - sol_client.request_airdrop(from_wallet.public_key, 1000 * 10**9, commitment=Confirmed) + sol_client.request_airdrop(from_wallet.pubkey(), 1000 * 10**9, commitment=Confirmed) mint = spl.token.client.Token.create_mint( conn=sol_client, payer=from_wallet, - mint_authority=from_wallet.public_key, + mint_authority=from_wallet.pubkey(), decimals=9, program_id=TOKEN_PROGRAM_ID, ) mint.payer = from_wallet - from_token_account = mint.create_associated_token_account(from_wallet.public_key) - to_token_account = mint.create_associated_token_account(to_wallet.public_key) + from_token_account = mint.create_associated_token_account(from_wallet.pubkey()) + to_token_account = mint.create_associated_token_account(to_wallet.pubkey()) mint.mint_to( dest=from_token_account, mint_authority=from_wallet, @@ -217,18 +220,18 @@ def test_transfer_tokens_with_ext_authority(self, call_solana_caller, sol_client ) seed = self.web3_client.text_to_bytes32("myseed") - authority = call_solana_caller.functions.getExtAuthority(seed).call({"from": sender.address}) + authority: bytes = call_solana_caller.functions.getExtAuthority(seed).call({"from": sender.address}) mint.set_authority( from_token_account, from_wallet, spl.token.instructions.AuthorityType.ACCOUNT_OWNER, - authority, + Pubkey(authority), opts=TxOpts(skip_confirmation=False, skip_preflight=True), ) instruction = transfer( - TransferParams(TOKEN_PROGRAM_ID, from_token_account, to_token_account, authority, amount) + TransferParams(TOKEN_PROGRAM_ID, from_token_account, to_token_account, Pubkey(authority), amount) ) serialized = serialize_instruction(TOKEN_PROGRAM_ID, instruction) @@ -241,26 +244,26 @@ def test_transfer_tokens_with_ext_authority(self, call_solana_caller, sol_client event_logs = call_solana_caller.events.LogBytes().process_receipt(resp) assert int.from_bytes(event_logs[0].args.value, byteorder="little") == 0 - def test_gas_estimate_for_wsol_transfer(self, solana_account, call_solana_caller, sol_client): + def test_gas_estimate_for_wsol_transfer(self, new_solana_account, call_solana_caller, sol_client): sender = self.accounts[0] mint = wSOL["address_spl"] - recipient = Keypair.generate() + recipient = Keypair() - spl_token = SplToken(sol_client, mint, TOKEN_PROGRAM_ID, solana_account) - ata_address_from = get_associated_token_address(solana_account.public_key, mint) - ata_address_to = get_associated_token_address(recipient.public_key, mint) - sol_client.create_associate_token_acc(solana_account, solana_account, mint) - sol_client.create_associate_token_acc(solana_account, recipient, mint) + spl_token = SplToken(sol_client, mint, TOKEN_PROGRAM_ID, new_solana_account) + ata_address_from = get_associated_token_address(new_solana_account.pubkey(), mint) + ata_address_to = get_associated_token_address(recipient.pubkey(), mint) + sol_client.create_associate_token_acc(new_solana_account, new_solana_account, mint) + sol_client.create_associate_token_acc(new_solana_account, recipient, mint) def get_gas_used_for_emulate_send_wsol(amount): - wrap_sol_tx = make_wSOL(amount, solana_account.public_key, ata_address_from) - sol_client.send_tx_and_check_status_ok(wrap_sol_tx, solana_account) + wrap_sol_tx = make_wSOL(amount, new_solana_account.pubkey(), ata_address_from) + sol_client.send_tx_and_check_status_ok(wrap_sol_tx, new_solana_account) seed = self.web3_client.text_to_bytes32("myseed") authority = call_solana_caller.functions.getExtAuthority(seed).call({"from": sender.address}).hex() authority = bytes32_to_solana_pubkey(authority) spl_token.set_authority( ata_address_from, - solana_account, + new_solana_account, spl.token.instructions.AuthorityType.ACCOUNT_OWNER, authority, opts=TxOpts(skip_confirmation=False, skip_preflight=True), @@ -282,15 +285,15 @@ def get_gas_used_for_emulate_send_wsol(amount): gas_used_amount2 = get_gas_used_for_emulate_send_wsol(10000 * 2) assert gas_used_amount1 == gas_used_amount2, "Gas used for different transfer amounts should be the same" - def test_limit_of_simple_instr_in_one_trx(self, call_solana_caller, counter_resource_address): + def test_limit_of_simple_instr_in_one_trx(self, call_solana_caller, counter_resource_address: bytes): sender = self.accounts[0] call_params = [] for _ in range(24): - instruction = TransactionInstruction( + instruction = Instruction( program_id=COUNTER_ID, - keys=[ - AccountMeta(counter_resource_address, is_signer=False, is_writable=True), + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), ], data=bytes([0x1]), ) @@ -301,4 +304,3 @@ def test_limit_of_simple_instr_in_one_trx(self, call_solana_caller, counter_reso instruction_tx = call_solana_caller.functions.batchExecute(call_params).build_transaction(tx) resp = self.web3_client.send_transaction(sender, instruction_tx) assert resp["status"] == 0 - diff --git a/integration/tests/basic/evm/test_spl_token.py b/integration/tests/basic/evm/test_spl_token.py index 0ba60b9ff4..9fbbb65355 100644 --- a/integration/tests/basic/evm/test_spl_token.py +++ b/integration/tests/basic/evm/test_spl_token.py @@ -4,7 +4,7 @@ import web3.exceptions from eth_abi import abi from eth_utils import is_hex, keccak -from solana.keypair import Keypair +from solders.keypair import Keypair from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts from solana.transaction import Transaction @@ -63,10 +63,10 @@ def token_mint(self, solana_account, spl_token_caller): txn.add( create_metadata_instruction( metadata, - solana_account.public_key, + solana_account.pubkey(), token_mint.pubkey, - solana_account.public_key, - solana_account.public_key, + solana_account.pubkey(), + solana_account.pubkey(), ) ) self.sol_client.send_transaction( @@ -94,10 +94,10 @@ def non_initialized_token_mint(self, solana_account, spl_token_caller): txn.add( create_metadata_instruction( metadata, - solana_account.public_key, + solana_account.pubkey(), token_mint.pubkey, - solana_account.public_key, - solana_account.public_key, + solana_account.pubkey(), + solana_account.pubkey(), ) ) self.sol_client.send_transaction( @@ -137,8 +137,8 @@ def non_initialized_acc(self, faucet, eth_bank_account): yield self.web3_client.create_account_with_balance(faucet, bank_account=eth_bank_account) def test_get_mint_for_non_initialized_acc(self, spl_token_caller): - acc = Keypair.generate() - mint = Mint(spl_token_caller.functions.getMint(bytes(acc.public_key)).call()) + acc = Keypair() + mint = Mint(spl_token_caller.functions.getMint(bytes(acc.pubkey())).call()) assert mint.supply == 0 assert mint.decimals == 0 assert mint.is_initialized is False @@ -203,15 +203,15 @@ def test_initialize_acc_incorrect_mint(self, spl_token_caller): sender_account = self.accounts[1] tx = self.web3_client.make_raw_tx(sender_account) - acc = Keypair.generate() + acc = Keypair() with pytest.raises(web3.exceptions.ContractLogicError, match=ErrorMessage.INCORRECT_PROGRAM_ID.value): spl_token_caller.functions.initializeAccount( - sender_account.address, bytes(acc.public_key) + sender_account.address, bytes(acc.pubkey()) ).build_transaction(tx) try: calldata = keccak(text="initializeAccount(address,bytes32)")[:4] + abi.encode( ["address", "bytes32"], - [sender_account.address, bytes(acc.public_key)], + [sender_account.address, bytes(acc.pubkey())], ) tx = self.web3_client.make_raw_tx( sender_account, spl_token_caller.address, data=calldata, gas=1000000, estimate_gas=False diff --git a/integration/tests/basic/indexer/test_instruction_parsing.py b/integration/tests/basic/indexer/test_instruction_parsing.py index d90a2b7c76..9655b0346e 100644 --- a/integration/tests/basic/indexer/test_instruction_parsing.py +++ b/integration/tests/basic/indexer/test_instruction_parsing.py @@ -1,9 +1,9 @@ -from collections import Counter - import pytest -from solana.transaction import AccountMeta, TransactionInstruction +from solana.transaction import AccountMeta, Instruction import allure +from solders.pubkey import Pubkey + from integration.tests.basic.helpers.rpc_checks import ( assert_instructions, assert_solana_trxs_in_neon_receipt, @@ -64,14 +64,14 @@ def test_cancel_with_hash(self, json_rpc_client, expected_error_checker): assert "CancelWithHash" in count_instructions(validated_response).keys() assert_solana_trxs_in_neon_receipt(json_rpc_client, resp["transactionHash"], validated_response) - def test_tx_exec_from_data_solana_call(self, call_solana_caller, counter_resource_address, json_rpc_client): + def test_tx_exec_from_data_solana_call(self, call_solana_caller, counter_resource_address: bytes, json_rpc_client): sender = self.accounts[0] lamports = 0 - instruction = TransactionInstruction( + instruction = Instruction( program_id=COUNTER_ID, - keys=[ - AccountMeta(counter_resource_address, is_signer=False, is_writable=True), + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), ], data=bytes([0x1]), ) @@ -153,16 +153,16 @@ def test_step_from_account(self, multiple_actions_erc721, json_rpc_client): assert_solana_trxs_in_neon_receipt(json_rpc_client, resp["transactionHash"], validated_response) def test_tx_exec_from_account_solana_call( - self, call_solana_caller, counter_resource_address, json_rpc_client + self, call_solana_caller, counter_resource_address: bytes, json_rpc_client ): sender = self.accounts[0] call_params = [] for _ in range(10): - instruction = TransactionInstruction( + instruction = Instruction( program_id=COUNTER_ID, - keys=[ - AccountMeta(counter_resource_address, is_signer=False, is_writable=True), + accounts=[ + AccountMeta(Pubkey(counter_resource_address), is_signer=False, is_writable=True), ], data=bytes([0x1]), ) diff --git a/integration/tests/basic/rpc/test_eip_1559_eth_rpc.py b/integration/tests/basic/rpc/test_eip_1559_eth_rpc.py new file mode 100644 index 0000000000..e8b85132ff --- /dev/null +++ b/integration/tests/basic/rpc/test_eip_1559_eth_rpc.py @@ -0,0 +1,218 @@ +import allure +import pytest + +from clickfile import EnvName +from utils.accounts import EthAccounts +from utils.apiclient import JsonRPCSession +from utils.web3client import NeonChainWeb3Client + + +@allure.feature("EIP Verifications") +@allure.story("EIP-1559: Verify new fields in eth_ JSON-RPC methods") +@pytest.mark.neon_only +class TestRpcEthMethods: + def test_get_transaction_by_hash( + self, + accounts: EthAccounts, + web3_client: NeonChainWeb3Client, + json_rpc_client: JsonRPCSession, + + ): + sender = accounts[0] + recipient = web3_client.create_account() + + base_fee_per_gas = web3_client.base_fee_per_gas() + max_priority_fee_per_gas = web3_client._web3.eth._max_priority_fee() # noqa + max_fee_per_gas = (5 * base_fee_per_gas) + max_priority_fee_per_gas + + receipt = web3_client.send_tokens_eip_1559( + from_=sender, + to=recipient.address, + value=100, + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + ) + response = json_rpc_client.send_rpc( + method="eth_getTransactionByHash", + params=[receipt.transactionHash.hex()], + ) + + assert "error" not in response, response["error"] + result = response.get("result") + assert result is not None + assert int(result['maxFeePerGas'], 16) == max_fee_per_gas + assert int(result['maxPriorityFeePerGas'], 16) == max_priority_fee_per_gas + + def test_get_transaction_by_block_hash_and_index( + self, + accounts: EthAccounts, + web3_client: NeonChainWeb3Client, + json_rpc_client: JsonRPCSession, + + ): + sender = accounts[0] + recipient = web3_client.create_account() + + base_fee_per_gas = web3_client.base_fee_per_gas() + max_priority_fee_per_gas = web3_client._web3.eth._max_priority_fee() # noqa + max_fee_per_gas = (5 * base_fee_per_gas) + max_priority_fee_per_gas + + receipt = web3_client.send_tokens_eip_1559( + from_=sender, + to=recipient.address, + value=100, + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + ) + transaction_index = hex(receipt.transactionIndex) + response = json_rpc_client.send_rpc( + method="eth_getTransactionByBlockHashAndIndex", + params=[receipt.blockHash.hex(), transaction_index], + ) + + assert "error" not in response, response["error"] + result = response.get("result") + assert result is not None + assert int(result['maxFeePerGas'], 16) == max_fee_per_gas + assert int(result['maxPriorityFeePerGas'], 16) == max_priority_fee_per_gas + + def test_get_transaction_by_block_number_and_index( + self, + accounts: EthAccounts, + web3_client: NeonChainWeb3Client, + json_rpc_client: JsonRPCSession, + ): + sender = accounts[0] + recipient = web3_client.create_account() + + base_fee_per_gas = web3_client.base_fee_per_gas() + max_priority_fee_per_gas = web3_client._web3.eth._max_priority_fee() # noqa + max_fee_per_gas = (5 * base_fee_per_gas) + max_priority_fee_per_gas + + receipt = web3_client.send_tokens_eip_1559( + from_=sender, + to=recipient.address, + value=100, + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + ) + block_number = hex(receipt.blockNumber) + index = hex(receipt.transactionIndex) + response = json_rpc_client.send_rpc( + method="eth_getTransactionByBlockNumberAndIndex", + params=[block_number, index], + ) + + assert "error" not in response, response["error"] + result = response.get("result") + assert result is not None + assert int(result['maxFeePerGas'], 16) == max_fee_per_gas + assert int(result['maxPriorityFeePerGas'], 16) == max_priority_fee_per_gas + + @pytest.mark.neon_only + @pytest.mark.parametrize( + argnames="full_transaction_objects", + argvalues=(True, False), + ) + def test_get_block_by_hash( + self, + accounts: EthAccounts, + web3_client: NeonChainWeb3Client, + json_rpc_client: JsonRPCSession, + full_transaction_objects: bool, + ): + sender = accounts[0] + recipient = web3_client.create_account() + + base_fee_from_history = web3_client.base_fee_per_gas() + base_fee_per_gas = base_fee_from_history * 2 + max_priority_fee_per_gas = web3_client._web3.eth._max_priority_fee() # noqa + max_fee_per_gas = base_fee_per_gas + max_priority_fee_per_gas + + receipt = web3_client.send_tokens_eip_1559( + from_=sender, + to=recipient.address, + value=100, + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + ) + block_hash = receipt.blockHash.hex() + response = json_rpc_client.send_rpc( + method="eth_getBlockByHash", + params=[block_hash, full_transaction_objects], + ) + + assert "error" not in response, response["error"] + result = response.get("result") + assert result is not None + + block_base_fee = int(result['baseFeePerGas'], 16) + assert block_base_fee <= base_fee_per_gas + + @pytest.mark.neon_only + @pytest.mark.parametrize( + argnames="full_transaction_objects", + argvalues=(True, False), + ) + def test_get_block_by_number( + self, + accounts: EthAccounts, + web3_client: NeonChainWeb3Client, + json_rpc_client: JsonRPCSession, + full_transaction_objects: bool, + ): + sender = accounts[0] + recipient = web3_client.create_account() + + base_fee_from_history = web3_client.base_fee_per_gas() + base_fee_per_gas = base_fee_from_history * 2 + max_priority_fee_per_gas = web3_client._web3.eth._max_priority_fee() # noqa + + receipt = web3_client.send_tokens_eip_1559( + from_=sender, + to=recipient.address, + value=100, + max_fee_per_gas=base_fee_per_gas + max_priority_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + ) + block_number = hex(receipt.blockNumber) + response = json_rpc_client.send_rpc( + method="eth_getBlockByNumber", + params=[block_number, full_transaction_objects], + ) + + assert "error" not in response, response["error"] + result = response.get("result") + assert result is not None + + block_base_fee = int(result['baseFeePerGas'], 16) + + assert block_base_fee <= base_fee_per_gas + + def test_get_transaction_receipt( + self, + accounts: EthAccounts, + web3_client: NeonChainWeb3Client, + env_name: EnvName, + ): + sender = accounts[0] + recipient = web3_client.create_account() + + base_fee_per_gas = web3_client.base_fee_per_gas() + max_priority_fee_per_gas = web3_client._web3.eth._max_priority_fee() # noqa + max_fee_per_gas = 2 * base_fee_per_gas + max_priority_fee_per_gas + + receipt = web3_client.send_tokens_eip_1559( + from_=sender, + to=recipient.address, + value=100, + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + ) + + actual_effective_gas_price = receipt.effectiveGasPrice + if env_name is EnvName.GETH: + expected_effective_gas_price = min(max_fee_per_gas, base_fee_per_gas + max_priority_fee_per_gas) + else: + expected_effective_gas_price = max_fee_per_gas + assert actual_effective_gas_price == expected_effective_gas_price diff --git a/integration/tests/basic/rpc/test_eip_1559_neon_rpc.py b/integration/tests/basic/rpc/test_eip_1559_neon_rpc.py new file mode 100644 index 0000000000..2ba38ba208 --- /dev/null +++ b/integration/tests/basic/rpc/test_eip_1559_neon_rpc.py @@ -0,0 +1,98 @@ +import allure +import pytest + +from utils.accounts import EthAccounts +from utils.apiclient import JsonRPCSession +from utils.web3client import NeonChainWeb3Client + + +@allure.feature("EIP Verifications") +@allure.story("EIP-1559: Verify new fields in neon_ JSON-RPC methods") +@pytest.mark.neon_only +class TestRpcNeonMethods: + + def test_neon_get_transaction_by_sender_nonce( + self, + accounts: EthAccounts, + web3_client: NeonChainWeb3Client, + json_rpc_client: JsonRPCSession, + ): + sender = accounts[0] + recipient = accounts[1] + nonce = web3_client.get_nonce(address=sender.address) + base_fee_per_gas = web3_client.base_fee_per_gas() + max_priority_fee_per_gas = web3_client._web3.eth._max_priority_fee() # noqa + max_fee_per_gas = (5 * base_fee_per_gas) + max_priority_fee_per_gas + + web3_client.send_tokens_eip_1559( + from_=sender, + to=recipient.address, + value=100, + nonce=nonce, + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + ) + + response = json_rpc_client.send_rpc( + method="neon_getTransactionBySenderNonce", params=[sender.address, nonce] + ) + + assert "error" not in response, response["error"] + max_priority_fee_per_gas_response = response["result"].get("maxPriorityFeePerGas") + assert max_priority_fee_per_gas_response is not None + assert int(max_priority_fee_per_gas_response, 16) == max_priority_fee_per_gas + + max_fee_per_gas_response = response["result"].get("maxFeePerGas") + assert max_fee_per_gas is not None + assert int(max_fee_per_gas_response, 16) == max_fee_per_gas + + def test_neon_get_solana_transaction_by_neon_transaction( + self, + accounts: EthAccounts, + web3_client: NeonChainWeb3Client, + json_rpc_client: JsonRPCSession, + ): + sender = accounts[0] + recipient = accounts[1] + + receipt = web3_client.send_tokens_eip_1559( + from_=sender, + to=recipient.address, + value=100, + ) + params = [receipt.transactionHash.hex()] + response = json_rpc_client.send_rpc(method="neon_getSolanaTransactionByNeonTransaction", params=params) + + assert "error" not in response, response["error"] + hash_solana = response["result"] + assert hash_solana + + def test_neon_get_transaction_receipt( + self, + accounts: EthAccounts, + web3_client: NeonChainWeb3Client, + json_rpc_client: JsonRPCSession, + ): + sender = accounts[0] + recipient = accounts[1] + + base_fee_per_gas = web3_client.base_fee_per_gas() + max_priority_fee_per_gas = web3_client._web3.eth._max_priority_fee() # noqa + max_fee_per_gas = (5 * base_fee_per_gas) + max_priority_fee_per_gas + + receipt = web3_client.send_tokens_eip_1559( + from_=sender, + to=recipient.address, + value=100, + max_priority_fee_per_gas=max_priority_fee_per_gas, + max_fee_per_gas=max_fee_per_gas, + ) + + params = [receipt.transactionHash.hex()] + response = json_rpc_client.send_rpc(method="neon_getTransactionReceipt", params=params) + + assert "error" not in response, response["error"] + + actual_effective_gas_price = int(response["result"].get("effectiveGasPrice"), 16) + assert actual_effective_gas_price >= base_fee_per_gas + assert actual_effective_gas_price <= max_fee_per_gas diff --git a/integration/tests/basic/test_deposit.py b/integration/tests/basic/test_deposit.py index 5f8011210c..c6305e6471 100644 --- a/integration/tests/basic/test_deposit.py +++ b/integration/tests/basic/test_deposit.py @@ -2,11 +2,11 @@ import allure import web3 from _pytest.config import Config -from solana.keypair import Keypair +from solders.keypair import Keypair from solana.rpc.commitment import Commitment from solana.rpc.types import TxOpts from solana.transaction import Transaction -from solana.publickey import PublicKey +from solders.pubkey import Pubkey from spl.token.client import Token as SplToken from spl.token.constants import TOKEN_PROGRAM_ID from spl.token.instructions import create_associated_token_account, get_associated_token_address @@ -28,17 +28,17 @@ class TestDeposit: accounts: EthAccounts sol_client: SolanaClient - def withdraw_neon(self, sender_account, dest_acc, move_amount): + def withdraw_neon(self, sender_account, dest_acc: Keypair, move_amount): contract, _ = self.web3_client.deploy_and_get_contract( "precompiled/NeonToken", "0.8.10", account=sender_account ) tx = self.web3_client.make_raw_tx(sender_account, amount=web3.Web3.to_wei(move_amount, "ether")) - instruction_tx = contract.functions.withdraw(bytes(dest_acc.public_key)).build_transaction(tx) + instruction_tx = contract.functions.withdraw(bytes(dest_acc.pubkey())).build_transaction(tx) receipt = self.web3_client.send_transaction(sender_account, instruction_tx) assert receipt["status"] == 1 @pytest.mark.mainnet - def test_transfer_neon_from_solana_to_neon(self, solana_account, neon_mint, evm_loader): + def test_transfer_neon_from_solana_to_neon(self, solana_account: Keypair, neon_mint, evm_loader): """Transfer Neon from Solana -> Neon""" amount = 0.1 sender_account = self.accounts[0] @@ -63,9 +63,9 @@ def test_create_and_transfer_new_token_from_solana_to_neon( self, solana_account, pytestconfig: Config, neon_mint, web3_client_usdt, operator_keypair, evm_loader ): amount = 5000 - new_sol_account = Keypair.generate() - token_mint = PublicKey(MULTITOKEN_MINTS["USDT"]) - evm_loader.request_airdrop(new_sol_account.public_key, 1 * LAMPORT_PER_SOL) + new_sol_account = Keypair() + token_mint = Pubkey.from_string(MULTITOKEN_MINTS["USDT"]) + evm_loader.request_airdrop(new_sol_account.pubkey(), 1 * LAMPORT_PER_SOL) new_account = self.accounts.create_account() evm_loader.deposit_neon_like_tokens_from_solana_to_neon( @@ -83,12 +83,12 @@ def test_transfer_wrapped_sol_token_from_solana_to_neon(self, solana_account, we full_amount = int(amount * LAMPORT_PER_SOL) mint_pubkey = wSOL["address_spl"] - ata_address = get_associated_token_address(solana_account.public_key, mint_pubkey) + ata_address = get_associated_token_address(solana_account.pubkey(), mint_pubkey) self.sol_client.create_associate_token_acc(solana_account, solana_account, mint_pubkey) # wrap SOL - wrap_sol_tx = make_wSOL(full_amount, solana_account.public_key, ata_address) + wrap_sol_tx = make_wSOL(full_amount, solana_account.pubkey(), ata_address) self.sol_client.send_tx_and_check_status_ok(wrap_sol_tx, solana_account) evm_loader.sent_token_from_solana_to_neon( @@ -112,7 +112,7 @@ class TestWithdraw: def withdraw(self, sender_acc, dest_acc, move_amount, withdraw_contract): tx = self.web3_client.make_raw_tx(sender_acc, amount=move_amount) - instruction_tx = withdraw_contract.functions.withdraw(bytes(dest_acc.public_key)).build_transaction(tx) + instruction_tx = withdraw_contract.functions.withdraw(bytes(dest_acc.pubkey())).build_transaction(tx) receipt = self.web3_client.send_transaction(sender_acc, instruction_tx) assert receipt["status"] == 1 @@ -130,10 +130,10 @@ def test_success_withdraw_to_non_existing_account( ): """Should successfully withdraw NEON tokens to previously non-existing Associated Token Account""" sender_account = self.accounts[0] - dest_acc = Keypair.generate() - self.sol_client.request_airdrop(dest_acc.public_key, 1_000_000_000) + dest_acc = Keypair() + self.sol_client.request_airdrop(dest_acc.pubkey(), 1_000_000_000) spl_neon_token = SplToken(self.sol_client, neon_mint, TOKEN_PROGRAM_ID, dest_acc) - dest_token_acc = get_associated_token_address(dest_acc.public_key, neon_mint) + dest_token_acc = get_associated_token_address(dest_acc.pubkey(), neon_mint) move_amount = self.web3_client._web3.to_wei(5, "ether") self.withdraw(sender_account, dest_acc, move_amount, withdraw_contract) @@ -148,14 +148,14 @@ def test_success_withdraw_to_existing_account( dest_acc = solana_account sender_account = self.accounts[0] - wait_condition(lambda: self.sol_client.get_balance(dest_acc.public_key) != 0) + wait_condition(lambda: self.sol_client.get_balance(dest_acc.pubkey()) != 0) trx = Transaction() - trx.add(create_associated_token_account(dest_acc.public_key, dest_acc.public_key, neon_mint)) + trx.add(create_associated_token_account(dest_acc.pubkey(), dest_acc.pubkey(), neon_mint)) opts = TxOpts(skip_preflight=True, skip_confirmation=False) self.sol_client.send_transaction(trx, dest_acc, opts=opts) - dest_token_acc = get_associated_token_address(dest_acc.public_key, neon_mint) + dest_token_acc = get_associated_token_address(dest_acc.pubkey(), neon_mint) move_amount_alan = 2_123_000_321_000_000_000 move_amount_galan = int(move_amount_alan / 1_000_000_000) @@ -177,17 +177,17 @@ def test_failed_withdraw_non_divisible_amount( sender_account = self.accounts[0] dest_acc = solana_account - spl_neon_token = SplToken(self.sol_client, neon_mint, TOKEN_PROGRAM_ID, dest_acc.public_key) + spl_neon_token = SplToken(self.sol_client, neon_mint, TOKEN_PROGRAM_ID, dest_acc) move_amount = pow(10, 18) + 123 - destination_balance_before = spl_neon_token.get_balance(dest_acc.public_key, commitment=Commitment("confirmed")) + destination_balance_before = spl_neon_token.get_balance(dest_acc.pubkey(), commitment=Commitment("confirmed")) with pytest.raises(AttributeError): _ = destination_balance_before.value with pytest.raises(web3_exceptions.ContractLogicError): self.withdraw(sender_account, dest_acc, move_amount, withdraw_contract) - destination_balance_after = spl_neon_token.get_balance(dest_acc.public_key, commitment=Commitment("confirmed")) + destination_balance_after = spl_neon_token.get_balance(dest_acc.pubkey(), commitment=Commitment("confirmed")) with pytest.raises(AttributeError): _ = destination_balance_after.value diff --git a/integration/tests/basic/test_payment_in_different_tokens.py b/integration/tests/basic/test_payment_in_different_tokens.py index 4cf7308ff8..ea6f350c42 100644 --- a/integration/tests/basic/test_payment_in_different_tokens.py +++ b/integration/tests/basic/test_payment_in_different_tokens.py @@ -290,7 +290,7 @@ def test_call_different_chains_contracts_in_one_transaction( assert item["common_contract"].functions.getNumber().call() == numbers[i] @pytest.mark.multipletokens - def test_send_non_neon_token_without_chain_id(self, account_with_all_tokens, web3_client_sol, sol_price, operator): + def test_send_non_neon_token_without_chain_id(self, account_with_all_tokens, web3_client_sol): # for transactions with non neon token and without chain_id NeonEVM should raise wrong chain id error # checks eip1820 acc2 = web3_client_sol.create_account() diff --git a/integration/tests/basic/test_wneon.py b/integration/tests/basic/test_wneon.py index cdab501070..eb9d56761c 100644 --- a/integration/tests/basic/test_wneon.py +++ b/integration/tests/basic/test_wneon.py @@ -191,14 +191,14 @@ def test_withdraw_wneon_from_neon_to_solana(self, wneon, neon_mint, solana_accou < withdraw_amount ) - wait_condition(lambda: self.sol_client.get_balance(solana_account.public_key) != 0) + wait_condition(lambda: self.sol_client.get_balance(solana_account.pubkey()) != 0) trx = Transaction() - trx.add(create_associated_token_account(solana_account.public_key, solana_account.public_key, neon_mint)) + trx.add(create_associated_token_account(solana_account.pubkey(), solana_account.pubkey(), neon_mint)) opts = TxOpts(skip_preflight=True, skip_confirmation=False) self.sol_client.send_transaction(trx, solana_account, opts=opts) - dest_token_acc = get_associated_token_address(solana_account.public_key, neon_mint) + dest_token_acc = get_associated_token_address(solana_account.pubkey(), neon_mint) spl_neon_token = SplToken(self.sol_client, neon_mint, TOKEN_PROGRAM_ID, solana_account) @@ -206,7 +206,7 @@ def test_withdraw_wneon_from_neon_to_solana(self, wneon, neon_mint, solana_accou neon_balance_before, wneon_balance_before = self.get_balances(wneon, recipient_account.address) instruction_tx = withdraw_contract.functions.withdraw( - bytes(solana_account.public_key), + bytes(solana_account.pubkey()), ).build_transaction( { "from": recipient_account.address, diff --git a/integration/tests/basic/transfer/test_transaction_validation.py b/integration/tests/basic/transfer/test_transaction_validation.py index 0877ac1877..a1bf3f5c79 100644 --- a/integration/tests/basic/transfer/test_transaction_validation.py +++ b/integration/tests/basic/transfer/test_transaction_validation.py @@ -3,13 +3,10 @@ import allure import pytest -import solana -from solana.rpc.core import RPCException from integration.tests.basic.helpers.assert_message import ErrorMessage from integration.tests.basic.helpers.rpc_checks import is_hex from utils.accounts import EthAccounts -from utils.solana_client import SolanaClient from utils.web3client import NeonChainWeb3Client from utils.helpers import gen_hash_of_block diff --git a/integration/tests/conftest.py b/integration/tests/conftest.py index 971a7574bc..1bc06232a0 100644 --- a/integration/tests/conftest.py +++ b/integration/tests/conftest.py @@ -1,3 +1,4 @@ +import logging import inspect import os import random @@ -9,10 +10,15 @@ import base58 import pytest from _pytest.config import Config -from solana.keypair import Keypair -from solana.publickey import PublicKey +from eth_account.signers.local import LocalAccount +from solders.keypair import Keypair +from solders.pubkey import Pubkey from solana.rpc import commitment from solana.rpc.types import TxOpts +from web3.contract import Contract + +from clickfile import EnvName +from utils.accounts import EthAccounts from utils.apiclient import JsonRPCSession from utils.consts import COUNTER_ID, LAMPORT_PER_SOL, MULTITOKEN_MINTS @@ -20,9 +26,12 @@ from utils.erc20wrapper import ERC20Wrapper from utils.evm_loader import EvmLoader from utils.operator import Operator -from utils.prices import get_sol_price +from utils.solana_client import SolanaClient +from utils.prices import get_sol_price_with_retry from utils.web3client import NeonChainWeb3Client, Web3Client +log = logging.getLogger(__name__) + NEON_AIRDROP_AMOUNT = 1_000 @@ -37,14 +46,14 @@ def json_rpc_client(pytestconfig: Config) -> JsonRPCSession: @pytest.fixture(scope="class") -def web3_client(request, web3_client_session): +def web3_client(request, web3_client_session) -> NeonChainWeb3Client: if inspect.isclass(request.cls): request.cls.web3_client = web3_client_session yield web3_client_session @pytest.fixture(scope="class") -def sol_client(request, sol_client_session): +def sol_client(request, sol_client_session) -> SolanaClient: if inspect.isclass(request.cls): request.cls.sol_client = sol_client_session yield sol_client_session @@ -95,7 +104,7 @@ def bank_account(pytestconfig: Config) -> tp.Optional[Keypair]: elif pytestconfig.getoption("--network") == "mainnet": private_key = os.environ.get("BANK_PRIVATE_KEY_MAINNET") key = base58.b58decode(private_key) - account = Keypair.from_secret_key(key) + account = Keypair.from_bytes(key) yield account @@ -110,18 +119,34 @@ def eth_bank_account(pytestconfig: Config, web3_client_session) -> tp.Optional[K @pytest.fixture(scope="session") -def solana_account(bank_account, pytestconfig: Config, sol_client_session): - account = Keypair.generate() +def solana_account(bank_account, pytestconfig: Config, sol_client_session) -> Keypair: + account = Keypair() + + if pytestconfig.environment.use_bank: + sol_client_session.send_sol(bank_account, account.pubkey(), int(0.5 * LAMPORT_PER_SOL)) + else: + sol_client_session.request_airdrop(account.pubkey(), 1 * LAMPORT_PER_SOL) + yield account + if pytestconfig.environment.use_bank: + balance = sol_client_session.get_balance(account.pubkey(), commitment=commitment.Confirmed).value + try: + sol_client_session.send_sol(account, bank_account.pubkey(), balance - 5000) + except: + pass + +@pytest.fixture(scope="function") +def new_solana_account(bank_account, pytestconfig: Config, sol_client_session): + account = Keypair() if pytestconfig.environment.use_bank: - sol_client_session.send_sol(bank_account, account.public_key, int(0.5 * LAMPORT_PER_SOL)) + sol_client_session.send_sol(bank_account, account.pubkey(), int(0.01 * LAMPORT_PER_SOL)) else: - sol_client_session.request_airdrop(account.public_key, 1 * LAMPORT_PER_SOL) + sol_client_session.request_airdrop(account.pubkey(), 1 * LAMPORT_PER_SOL) yield account if pytestconfig.environment.use_bank: - balance = sol_client_session.get_balance(account.public_key, commitment=commitment.Confirmed).value + balance = sol_client_session.get_balance(account.pubkey(), commitment=commitment.Confirmed).value try: - sol_client_session.send_sol(account, bank_account.public_key, balance - 5000) + sol_client_session.send_sol(account, bank_account.pubkey(), balance - 5000) except: pass @@ -148,7 +173,7 @@ def erc20_spl( solana_account, eth_bank_account, accounts_session, -): +) -> ERC20Wrapper: symbol = "".join([random.choice(string.ascii_uppercase) for _ in range(3)]) erc20 = ERC20Wrapper( web3_client_session, @@ -169,7 +194,7 @@ def erc20_spl( erc20.contract.address, pytestconfig.environment.evm_loader, ), - owner=erc20.solana_acc.public_key, + owner=erc20.solana_acc.pubkey(), amount=1000000000000000, opts=TxOpts(preflight_commitment=commitment.Confirmed, skip_confirmation=False), ) @@ -213,13 +238,20 @@ def erc20_spl_mintable( @pytest.fixture(scope="class") def class_account_sol_chain( - evm_loader, solana_account, web3_client, web3_client_sol, faucet, eth_bank_account, bank_account, pytestconfig -): + evm_loader, + solana_account, + web3_client, + web3_client_sol, + faucet, + eth_bank_account, + bank_account, + pytestconfig +) -> LocalAccount: account = web3_client.create_account_with_balance(faucet, bank_account=eth_bank_account) if pytestconfig.environment.use_bank: - evm_loader.send_sol(bank_account, solana_account.public_key, int(1 * LAMPORT_PER_SOL)) + evm_loader.send_sol(bank_account, solana_account.pubkey(), int(1 * LAMPORT_PER_SOL)) else: - evm_loader.request_airdrop(solana_account.public_key, 1 * LAMPORT_PER_SOL) + evm_loader.request_airdrop(solana_account.pubkey(), 1 * LAMPORT_PER_SOL) evm_loader.deposit_wrapped_sol_from_solana_to_neon( solana_account, account, @@ -253,9 +285,9 @@ def account_with_all_tokens( neon_account = web3_client.create_account_with_balance(faucet, bank_account=eth_bank_account, amount=500) if web3_client_sol: if pytestconfig.environment.use_bank: - evm_loader.send_sol(bank_account, solana_account.public_key, int(1 * LAMPORT_PER_SOL)) + evm_loader.send_sol(bank_account, solana_account.pubkey(), int(1 * LAMPORT_PER_SOL)) else: - evm_loader.request_airdrop(solana_account.public_key, 1 * LAMPORT_PER_SOL) + evm_loader.request_airdrop(solana_account.pubkey(), 1 * LAMPORT_PER_SOL) evm_loader.deposit_wrapped_sol_from_solana_to_neon( solana_account, neon_account, @@ -268,7 +300,7 @@ def account_with_all_tokens( mint = MULTITOKEN_MINTS["USDT"] else: mint = MULTITOKEN_MINTS["ETH"] - token_mint = PublicKey(mint) + token_mint = Pubkey.from_string(mint) evm_loader.mint_spl_to(token_mint, solana_account, 1000000000000000) @@ -284,12 +316,12 @@ def account_with_all_tokens( @pytest.fixture(scope="session") def neon_mint(pytestconfig: Config): - neon_mint = PublicKey(pytestconfig.environment.spl_neon_mint) + neon_mint = Pubkey.from_string(pytestconfig.environment.spl_neon_mint) return neon_mint @pytest.fixture(scope="class") -def withdraw_contract(web3_client, faucet, accounts): +def withdraw_contract(web3_client, faucet, accounts) -> Contract: contract, _ = web3_client.deploy_and_get_contract("precompiled/NeonToken", "0.8.10", account=accounts[1]) return contract @@ -413,15 +445,8 @@ def revert_contract_caller(web3_client, accounts, revert_contract): @pytest.fixture(scope="session") def sol_price() -> float: """Get SOL price from Solana mainnet""" - price = get_sol_price() - started = time.time() - timeout = 120 - while price is None and (time.time() - started) < timeout: - print("Can't get SOL price") - time.sleep(3) - price = get_sol_price() - with allure.step(f"SOL price {price}$"): - return price + return get_sol_price_with_retry() + @pytest.fixture(scope="session") @@ -503,7 +528,7 @@ def call_solana_caller(accounts, web3_client): @pytest.fixture(scope="class") -def counter_resource_address(call_solana_caller, accounts, web3_client): +def counter_resource_address(call_solana_caller, accounts, web3_client) -> bytes: tx = web3_client.make_raw_tx(accounts[0].address) salt = web3_client.text_to_bytes32("1") instruction_tx = call_solana_caller.functions.createResource(salt, 8, 100000, bytes(COUNTER_ID)).build_transaction( @@ -511,3 +536,64 @@ def counter_resource_address(call_solana_caller, accounts, web3_client): ) web3_client.send_transaction(accounts[0], instruction_tx) yield call_solana_caller.functions.getResourceAddress(salt).call() + + +@pytest.fixture(scope="class") +def eip1559_setup( + request: pytest.FixtureRequest, + pytestconfig: Config, + accounts_session: EthAccounts, + web3_client_session: NeonChainWeb3Client, + env_name: EnvName, +): + """ + Creates type-2 transactions in the db + """ + + # Get the max value of marker @pytest.mark.need_eip1559_blocks(...) among selected tests in the class + block_count = 1 + selected_tests_in_class = [item for item in request.session.items if item.cls is request.cls] # noqa + markers = request.cls.pytestmark + [mark for test in selected_tests_in_class for mark in test.own_markers] + for marker in markers: + if marker.name == "need_eip1559_blocks": + need_eip1559_blocks = marker.args[0] + block_count = max(block_count, need_eip1559_blocks) + + # Check if the latest blocks already have enough type-2 transactions. If that's the case - return + fee_history = web3_client_session._web3.eth.fee_history(block_count, 'latest', None) # noqa + base_fee_per_gas_history = fee_history["baseFeePerGas"] + if len(base_fee_per_gas_history) >= block_count + 1: + return + + # Send the transactions + sender = accounts_session[0] + recipient = web3_client_session.create_account() + gas_estimate: tp.Union[int, tp.Literal["auto"]] = "auto" + value = 10 + + for nonce in range(block_count): + tx_params = web3_client_session.make_raw_tx_eip_1559( + nonce=nonce, + chain_id="auto", + from_=sender.address, + to=recipient.address, + value=value, + gas=gas_estimate, + max_priority_fee_per_gas="auto", + max_fee_per_gas="auto", + access_list=None, + data=None, + ) + + if gas_estimate == "auto": + gas_estimate = int(tx_params["gas"]) + + start = time.time() + receipt = web3_client_session.send_transaction(account=sender, transaction=tx_params) + assert receipt.status == 1 + + # Sleep to make sure the next transaction goes to the next block + min_pause = 3 if env_name is EnvName.GETH else 0.4 + pause = min_pause - (time.time() - start) + if pause > 0: + time.sleep(pause) diff --git a/integration/tests/economy/conftest.py b/integration/tests/economy/conftest.py index 2aca9a6561..904450aa35 100644 --- a/integration/tests/economy/conftest.py +++ b/integration/tests/economy/conftest.py @@ -94,6 +94,6 @@ def mapping_actions_contract(account_with_all_tokens, client_and_price, web3_cli w3_client = client_and_price[0] make_nonce_the_biggest_for_chain(account_with_all_tokens, w3_client, [web3_client, web3_client_sol]) contract, _ = w3_client.deploy_and_get_contract( - contract="common/Common", version="0.8.12", contract_name="MappingActions", account=account_with_all_tokens + contract="common/Common", version="0.8.19", contract_name="MappingActions", account=account_with_all_tokens ) return contract diff --git a/integration/tests/economy/steps.py b/integration/tests/economy/steps.py index d8d8f3d263..898ff220b9 100644 --- a/integration/tests/economy/steps.py +++ b/integration/tests/economy/steps.py @@ -1,3 +1,4 @@ +import logging import time from decimal import Decimal @@ -11,24 +12,32 @@ from utils.helpers import wait_condition, hasattr_recursive +logger = logging.getLogger(__name__) + + @allure.step("Verify operator profit") def assert_profit(sol_diff, sol_price, token_diff, token_price, token_name): - sol_amount = sol_diff / LAMPORT_PER_SOL + expense_lamports = sol_diff / LAMPORT_PER_SOL if token_diff < 0: raise AssertionError(f"NEON has negative difference {token_diff}") - sol_cost = Decimal(sol_amount, DECIMAL_CONTEXT) * Decimal(sol_price, DECIMAL_CONTEXT) - token_cost = Decimal(token_diff, DECIMAL_CONTEXT) * Decimal(token_price, DECIMAL_CONTEXT) + expense_usd = Decimal(expense_lamports, DECIMAL_CONTEXT) * Decimal(sol_price, DECIMAL_CONTEXT) + revenue_usd = Decimal(token_diff, DECIMAL_CONTEXT) * Decimal(token_price, DECIMAL_CONTEXT) + profit_usd = revenue_usd - expense_usd + profit_percentage = (profit_usd / expense_usd * 100) + + log_level = logging.WARNING if profit_percentage < 2 else logging.INFO + logger.log(level=log_level, msg=f"Operator income: {profit_percentage}%") - msg = "Operator receive {:.9f} {} ({:.2f} $) and spend {:.9f} SOL ({:.2f} $), profit - {:.9f}% ".format( + msg = "Operator receive {:.9f} {} ({:.2f} $) and spend {:.9f} SOL ({:.2f} $), profit: {:.9f}% ".format( token_diff, token_name, - token_cost, - sol_amount, - sol_cost, - ((token_cost - sol_cost) / sol_cost * 100), + revenue_usd, + expense_lamports, + expense_usd, + profit_percentage, ) with allure.step(msg): - assert token_cost > sol_cost, msg + assert revenue_usd > expense_usd, msg @allure.step("Get single transaction gas") diff --git a/integration/tests/economy/test_economics.py b/integration/tests/economy/test_economics.py index 64de4f8462..fb5dc45c15 100644 --- a/integration/tests/economy/test_economics.py +++ b/integration/tests/economy/test_economics.py @@ -1,31 +1,37 @@ import json import os -import random from decimal import Decimal import allure import pytest import rlp import web3 +import web3.types from _pytest.config import Config -from solana.keypair import Keypair as SolanaAccount -from solana.publickey import PublicKey -from solana.rpc.types import Commitment, TxOpts +from eth_account.signers.local import LocalAccount +from solders.keypair import Keypair as SolanaAccount +from solders.pubkey import Pubkey +from solana.rpc.types import Commitment from solana.transaction import Transaction from spl.token.instructions import ( create_associated_token_account, get_associated_token_address, ) +from web3.contract import Contract +from utils.accounts import EthAccounts from utils.consts import LAMPORT_PER_SOL, Time from utils.erc20 import ERC20 from utils.helpers import wait_condition, gen_hash_of_block -from .const import INSUFFICIENT_FUNDS_ERROR, GAS_LIMIT_ERROR, BIG_STRING, TX_COST +from utils.operator import Operator +from utils.solana_client import SolanaClient +from utils.types import TransactionType +from utils.web3client import NeonChainWeb3Client, Web3Client +from .const import INSUFFICIENT_FUNDS_ERROR, GAS_LIMIT_ERROR, BIG_STRING, DECIMAL_CONTEXT from .steps import ( wait_for_block, assert_profit, get_gas_used_percent, - check_alt_on, check_alt_off, get_sol_trx_with_alt, ) @@ -59,14 +65,22 @@ def test_account_creation(self, client_and_price, operator): assert neon_balance_after == neon_balance_before assert sol_balance_after == sol_balance_before - def test_send_neon_to_non_existent_account(self, account_with_all_tokens, client_and_price, sol_price, operator): + @pytest.mark.parametrize("tx_type", TransactionType) + def test_send_neon_to_non_existent_account( + self, + account_with_all_tokens: LocalAccount, + client_and_price: tuple[Web3Client, float], + sol_price: float, + operator: Operator, + tx_type: TransactionType, + ): """Verify how many cost transfer of native chain token to new user""" w3_client, token_price = client_and_price sol_balance_before = operator.get_solana_balance() token_balance_before = operator.get_token_balance(w3_client) transfer_value = 50000000 acc2 = w3_client.create_account() - receipt = w3_client.send_tokens(account_with_all_tokens, acc2, transfer_value) + receipt = w3_client.send_tokens(account_with_all_tokens, acc2, transfer_value, tx_type=tx_type) assert w3_client.get_balance(acc2) == transfer_value sol_balance_after = operator.get_solana_balance() @@ -78,18 +92,26 @@ def test_send_neon_to_non_existent_account(self, account_with_all_tokens, client assert_profit(sol_diff, sol_price, token_diff, token_price, w3_client.native_token_name) get_gas_used_percent(w3_client, receipt) - def test_send_tokens_to_exist_account(self, account_with_all_tokens, client_and_price, sol_price, operator): + @pytest.mark.parametrize("tx_type", TransactionType) + def test_send_tokens_to_exist_account( + self, + account_with_all_tokens: LocalAccount, + client_and_price: tuple[Web3Client, float], + sol_price: float, + operator: Operator, + tx_type: TransactionType, + ): """Verify how many cost token send to use who was already initialized""" w3_client, token_price = client_and_price acc2 = w3_client.create_account() transfer_value = 5000 - w3_client.send_tokens(account_with_all_tokens, acc2, transfer_value // 2) + w3_client.send_tokens(account_with_all_tokens, acc2, transfer_value // 2, tx_type=tx_type) assert w3_client.get_balance(acc2) == transfer_value // 2 sol_balance_before = operator.get_solana_balance() token_balance_before = operator.get_token_balance(w3_client) - receipt = w3_client.send_tokens(account_with_all_tokens, acc2, transfer_value // 2) + receipt = w3_client.send_tokens(account_with_all_tokens, acc2, transfer_value // 2, tx_type=tx_type) assert w3_client.get_balance(acc2) == transfer_value @@ -125,20 +147,27 @@ def test_send_neon_token_without_chain_id( token_diff = web3_client.to_main_currency(token_balance_after - token_balance_before) assert_profit(sol_diff, sol_price, token_diff, neon_price, web3_client.native_token_name) - def test_send_when_not_enough_tokens_to_gas(self, client_and_price, account_with_all_tokens, operator): + @pytest.mark.parametrize("tx_type", TransactionType) + def test_send_when_not_enough_tokens_to_gas( + self, + client_and_price: tuple[Web3Client, float], + account_with_all_tokens: LocalAccount, + operator: Operator, + tx_type: TransactionType, + ): w3_client, token_price = client_and_price acc2 = w3_client.create_account() assert w3_client.get_balance(acc2) == 0 transfer_amount = 5000 - w3_client.send_tokens(account_with_all_tokens, acc2, transfer_amount) + w3_client.send_tokens(account_with_all_tokens, acc2, transfer_amount, tx_type=tx_type) sol_balance_before = operator.get_solana_balance() token_balance_before = operator.get_token_balance(w3_client) acc3 = w3_client.create_account() - with pytest.raises(ValueError, match=INSUFFICIENT_FUNDS_ERROR) as e: - w3_client.send_tokens(acc2, acc3, transfer_amount) + with pytest.raises(ValueError, match=INSUFFICIENT_FUNDS_ERROR): + w3_client.send_tokens(acc2, acc3, transfer_amount, tx_type=tx_type) sol_balance_after = operator.get_solana_balance() token_balance_after = operator.get_token_balance(w3_client) @@ -184,31 +213,45 @@ def test_erc721_mint(self, erc721, client_and_price, account_with_all_tokens, so token_diff = w3_client.to_main_currency(token_balance_after - token_balance_before) assert_profit(sol_diff, sol_price, token_diff, token_price, w3_client.native_token_name) + @pytest.mark.parametrize("tx_type", TransactionType) def test_withdraw_neon_unexisting_ata( - self, pytestconfig: Config, neon_price, sol_price, sol_client, operator, web3_client, accounts + + self, + pytestconfig: Config, + neon_price: float, + sol_price: float, + sol_client: SolanaClient, + operator: Operator, + web3_client: NeonChainWeb3Client, + accounts: EthAccounts, + tx_type: TransactionType, ): sender_account = accounts[0] sol_user = SolanaAccount() - sol_client.request_airdrop(sol_user.public_key, 5 * LAMPORT_PER_SOL) + sol_client.request_airdrop(sol_user.pubkey(), 5 * LAMPORT_PER_SOL) sol_balance_before = operator.get_solana_balance() neon_balance_before = operator.get_token_balance(web3_client) user_neon_balance_before = web3_client.get_balance(sender_account) move_amount = web3_client._web3.to_wei(5, "ether") + contract, _ = web3_client.deploy_and_get_contract( + contract="precompiled/NeonToken", + version="0.8.10", + account=sender_account, + tx_type=tx_type, + ) - contract, _ = web3_client.deploy_and_get_contract("precompiled/NeonToken", "0.8.10", account=sender_account) - - tx = self.web3_client.make_raw_tx(sender_account, amount=move_amount) + tx = self.web3_client.make_raw_tx(from_=sender_account, amount=move_amount, tx_type=tx_type) - instruction_tx = contract.functions.withdraw(bytes(sol_user.public_key)).build_transaction(tx) + instruction_tx = contract.functions.withdraw(bytes(sol_user.pubkey())).build_transaction(tx) receipt = web3_client.send_transaction(sender_account, instruction_tx) assert receipt["status"] == 1 assert (user_neon_balance_before - web3_client.get_balance(sender_account)) > 5 - balance = sol_client.get_account_info_json_parsed(sol_user.public_key, commitment=Commitment("confirmed")) + balance = sol_client.get_account_info_json_parsed(sol_user.pubkey(), commitment=Commitment("confirmed")) assert int(balance.value.lamports) == int(move_amount / 1_000_000_000) sol_balance_after = operator.get_solana_balance() @@ -228,30 +271,32 @@ def test_withdraw_neon_unexisting_ata( get_gas_used_percent(web3_client, receipt) + @pytest.mark.parametrize("tx_type", TransactionType) def test_withdraw_neon_existing_ata( - self, - pytestconfig, - neon_mint, - neon_price, - sol_price, - sol_client, - operator, - web3_client, - accounts, - withdraw_contract, + self, + pytestconfig: Config, + neon_mint: Pubkey, + neon_price: float, + sol_price: float, + sol_client: SolanaClient, + operator: Operator, + web3_client: NeonChainWeb3Client, + accounts: EthAccounts, + withdraw_contract: Contract, + tx_type: TransactionType, ): sender_account = accounts[0] sol_user = SolanaAccount() - sol_client.request_airdrop(sol_user.public_key, 5 * LAMPORT_PER_SOL) + sol_client.request_airdrop(sol_user.pubkey(), 5 * LAMPORT_PER_SOL) - wait_condition(lambda: sol_client.get_balance(sol_user.public_key) != 0) + wait_condition(lambda: sol_client.get_balance(sol_user.pubkey()) != 0) trx = Transaction() - trx.add(create_associated_token_account(sol_user.public_key, sol_user.public_key, neon_mint)) + trx.add(create_associated_token_account(sol_user.pubkey(), sol_user.pubkey(), neon_mint)) sol_client.send_tx_and_check_status_ok(trx, sol_user) - dest_token_acc = get_associated_token_address(sol_user.public_key, neon_mint) + dest_token_acc = get_associated_token_address(sol_user.pubkey(), neon_mint) sol_balance_before = operator.get_solana_balance() neon_balance_before = operator.get_token_balance(web3_client) @@ -259,8 +304,8 @@ def test_withdraw_neon_existing_ata( user_neon_balance_before = web3_client.get_balance(sender_account) move_amount = web3_client._web3.to_wei(5, "ether") - tx = web3_client.make_raw_tx(sender_account, amount=move_amount) - instruction_tx = withdraw_contract.functions.withdraw(bytes(sol_user.public_key)).build_transaction(tx) + tx = web3_client.make_raw_tx(sender_account, amount=move_amount, tx_type=tx_type) + instruction_tx = withdraw_contract.functions.withdraw(bytes(sol_user.pubkey())).build_transaction(tx) receipt = web3_client.send_transaction(sender_account, instruction_tx) assert receipt["status"] == 1 @@ -312,8 +357,16 @@ def test_erc20_transfer( assert_profit(sol_diff, sol_price, token_diff, token_price, w3_client.native_token_name) get_gas_used_percent(w3_client, transfer_tx) + @pytest.mark.parametrize("tx_type", TransactionType) def test_deploy_small_contract_less_100tx( - self, account_with_all_tokens, client_and_price, web3_client_sol, web3_client, sol_price, operator + self, + account_with_all_tokens: LocalAccount, + client_and_price: tuple[Web3Client, float], + web3_client_sol: Web3Client, + web3_client: NeonChainWeb3Client, + sol_price: float, + operator: Operator, + tx_type: TransactionType, ): """Verify we are bill minimum for 100 instruction""" w3_client, token_price = client_and_price @@ -321,11 +374,16 @@ def test_deploy_small_contract_less_100tx( token_balance_before = operator.get_token_balance(w3_client) make_nonce_the_biggest_for_chain(account_with_all_tokens, w3_client, [web3_client, web3_client_sol]) - contract, _ = w3_client.deploy_and_get_contract("common/Counter", "0.8.10", account=account_with_all_tokens) + contract, _ = w3_client.deploy_and_get_contract( + contract="common/Counter", + version="0.8.10", + account=account_with_all_tokens, + tx_type=tx_type, + ) sol_balance_after_deploy = operator.get_solana_balance() token_balance_after_deploy = operator.get_token_balance(w3_client) - tx = w3_client.make_raw_tx(account_with_all_tokens.address) + tx = w3_client.make_raw_tx(from_=account_with_all_tokens.address, tx_type=tx_type) inc_tx = contract.functions.inc().build_transaction(tx) @@ -344,18 +402,41 @@ def test_deploy_small_contract_less_100tx( ) get_gas_used_percent(w3_client, receipt) - def test_deploy_to_lost_contract_account(self, account_with_all_tokens, client_and_price, sol_price, operator): + @pytest.mark.parametrize("tx_type", TransactionType) + def test_deploy_to_lost_contract_account( + self, + account_with_all_tokens: LocalAccount, + client_and_price: tuple[Web3Client, float], + sol_price: float, + operator: Operator, + tx_type: TransactionType, + ): w3_client, token_price = client_and_price sol_balance_before = operator.get_solana_balance() token_balance_before = operator.get_token_balance(w3_client) acc2 = w3_client.create_account() - w3_client.send_tokens(account_with_all_tokens, acc2, 1) + w3_client.send_tokens(account_with_all_tokens, acc2, value=1, tx_type=tx_type) with pytest.raises(ValueError, match=INSUFFICIENT_FUNDS_ERROR): - w3_client.deploy_and_get_contract("common/Counter", "0.8.10", account=acc2) - w3_client.send_tokens(account_with_all_tokens, acc2, int(w3_client.get_balance(account_with_all_tokens) // 10)) - contract, contract_deploy_tx = w3_client.deploy_and_get_contract("common/Counter", "0.8.10", account=acc2) + w3_client.deploy_and_get_contract( + contract="common/Counter", + version="0.8.10", + account=acc2, + tx_type=tx_type, + ) + w3_client.send_tokens( + from_=account_with_all_tokens, + to=acc2, + value=int(w3_client.get_balance(account_with_all_tokens) // 10), + tx_type=tx_type, + ) + contract, contract_deploy_tx = w3_client.deploy_and_get_contract( + contract="common/Counter", + version="0.8.10", + account=acc2, + tx_type=tx_type, + ) sol_balance_after = operator.get_solana_balance() token_balance_after = operator.get_token_balance(w3_client) @@ -386,20 +467,32 @@ def test_contract_get_is_free(self, counter_contract, client_and_price, account_ assert token_balance_before == token_balance_after @pytest.mark.xfail(reason="https://neonlabs.atlassian.net/browse/NDEV-699") - def test_cost_resize_account(self, neon_price, sol_price, operator, web3_client, accounts): + @pytest.mark.parametrize("tx_type", TransactionType) + def test_cost_resize_account( + self, + neon_price: float, + sol_price: float, + operator: Operator, + web3_client: NeonChainWeb3Client, + accounts: EthAccounts, + tx_type: TransactionType + ): """Verify how much cost account resize""" sender_account = accounts[0] sol_balance_before = operator.get_solana_balance() neon_balance_before = operator.get_token_balance(web3_client) contract, contract_deploy_tx = web3_client.deploy_and_get_contract( - "common/IncreaseStorage", "0.8.10", account=sender_account + contract="common/IncreaseStorage", + version="0.8.10", + account=sender_account, + tx_type=tx_type, ) sol_balance_before_increase = operator.get_solana_balance() neon_balance_before_increase = operator.get_token_balance(web3_client) - tx = web3_client.make_raw_tx(sender_account) + tx = web3_client.make_raw_tx(from_=sender_account, tx_type=tx_type) inc_tx = contract.functions.inc().build_transaction(tx) instruction_receipt = web3_client.send_transaction(sender_account, inc_tx) @@ -419,15 +512,22 @@ def test_cost_resize_account(self, neon_price, sol_price, operator, web3_client, ) get_gas_used_percent(web3_client, instruction_receipt) + @pytest.mark.parametrize("tx_type", TransactionType) def test_contract_interact_1000_steps( - self, counter_contract, client_and_price, account_with_all_tokens, sol_price, operator + self, + counter_contract: Contract, + client_and_price: tuple[Web3Client, float], + account_with_all_tokens: LocalAccount, + sol_price: float, + operator: Operator, + tx_type: TransactionType, ): """Deploy a contract with more 500 instructions""" w3_client, token_price = client_and_price sol_balance_before = operator.get_solana_balance() token_balance_before = operator.get_token_balance(w3_client) - tx = w3_client.make_raw_tx(account_with_all_tokens.address) + tx = w3_client.make_raw_tx(from_=account_with_all_tokens.address, tx_type=tx_type) instruction_tx = counter_contract.functions.moreInstruction(0, 100).build_transaction(tx) # 1086 steps in evm instruction_receipt = w3_client.send_transaction(account_with_all_tokens, instruction_tx) @@ -443,15 +543,22 @@ def test_contract_interact_1000_steps( ) get_gas_used_percent(w3_client, instruction_receipt) + @pytest.mark.parametrize("tx_type", TransactionType) def test_contract_interact_500000_steps( - self, counter_contract, client_and_price, account_with_all_tokens, sol_price, operator + self, + counter_contract: Contract, + client_and_price: tuple[Web3Client, float], + account_with_all_tokens: LocalAccount, + sol_price: float, + operator: Operator, + tx_type: TransactionType, ): """Deploy a contract with more 500000 bpf""" w3_client, token_price = client_and_price sol_balance_before = operator.get_solana_balance() token_balance_before = operator.get_token_balance(w3_client) - tx = w3_client.make_raw_tx(account_with_all_tokens.address) + tx = w3_client.make_raw_tx(from_=account_with_all_tokens.address, tx_type=tx_type) instruction_tx = counter_contract.functions.moreInstruction(0, 3000).build_transaction(tx) @@ -471,8 +578,14 @@ def test_contract_interact_500000_steps( ) get_gas_used_percent(w3_client, instruction_receipt) + @pytest.mark.parametrize("tx_type", TransactionType) def test_send_transaction_with_gas_limit_reached( - self, counter_contract, client_and_price, account_with_all_tokens, operator + self, + counter_contract: Contract, + client_and_price: tuple[Web3Client, float], + account_with_all_tokens: LocalAccount, + operator: Operator, + tx_type: TransactionType, ): """Transaction with small amount of gas""" w3_client, token_price = client_and_price @@ -480,7 +593,7 @@ def test_send_transaction_with_gas_limit_reached( sol_balance_before = operator.get_solana_balance() token_balance_before = operator.get_token_balance(w3_client) - tx = w3_client.make_raw_tx(account_with_all_tokens.address, gas=1000) + tx = w3_client.make_raw_tx(from_=account_with_all_tokens.address, gas=1000, tx_type=tx_type) instruction_tx = counter_contract.functions.moreInstruction(0, 100).build_transaction(tx) with pytest.raises(ValueError, match=GAS_LIMIT_ERROR): @@ -492,18 +605,24 @@ def test_send_transaction_with_gas_limit_reached( assert sol_balance_after == sol_balance_before, "SOL Balance changes" assert token_balance_after == token_balance_before, "TOKEN Balance incorrect" + @pytest.mark.parametrize("tx_type", TransactionType) def test_send_transaction_with_insufficient_funds( - self, counter_contract, client_and_price, account_with_all_tokens, operator + self, + counter_contract: Contract, + client_and_price: tuple[Web3Client, float], + account_with_all_tokens: LocalAccount, + operator: Operator, + tx_type: TransactionType, ): """Transaction with insufficient funds on balance""" w3_client, token_price = client_and_price acc2 = w3_client.create_account() - w3_client.send_tokens(account_with_all_tokens, acc2, 100) + w3_client.send_tokens(from_=account_with_all_tokens, to=acc2, value=100, tx_type=tx_type) sol_balance_before = operator.get_solana_balance() token_balance_before = operator.get_token_balance(w3_client) - tx = w3_client.make_raw_tx(acc2.address) + tx = w3_client.make_raw_tx(from_=acc2.address, tx_type=tx_type) instruction_tx = counter_contract.functions.moreInstruction(0, 1500).build_transaction(tx) with pytest.raises(ValueError, match=INSUFFICIENT_FUNDS_ERROR): @@ -515,15 +634,22 @@ def test_send_transaction_with_insufficient_funds( assert sol_balance_before == sol_balance_after, "SOL Balance changed" assert token_balance_after == token_balance_before, "TOKEN Balance incorrect" + @pytest.mark.parametrize("tx_type", TransactionType) def test_tx_interact_more_1kb( - self, counter_contract, client_and_price, account_with_all_tokens, sol_price, operator + self, + counter_contract: Contract, + client_and_price: tuple[Web3Client, float], + account_with_all_tokens: LocalAccount, + sol_price: float, + operator: Operator, + tx_type: TransactionType, ): """Send to contract a big text (tx more than 1 kb)""" w3_client, token_price = client_and_price sol_balance_before = operator.get_solana_balance() token_balance_before = operator.get_token_balance(w3_client) - tx = w3_client.make_raw_tx(account_with_all_tokens.address) + tx = w3_client.make_raw_tx(from_=account_with_all_tokens.address, tx_type=tx_type) instruction_tx = counter_contract.functions.bigString(BIG_STRING).build_transaction(tx) @@ -541,8 +667,16 @@ def test_tx_interact_more_1kb( ) get_gas_used_percent(w3_client, instruction_receipt) + @pytest.mark.parametrize("tx_type", TransactionType) def test_deploy_contract_more_1kb( - self, client_and_price, account_with_all_tokens, web3_client, web3_client_sol, sol_price, operator + self, + client_and_price: tuple[Web3Client, float], + account_with_all_tokens: LocalAccount, + web3_client: NeonChainWeb3Client, + web3_client_sol: Web3Client, + sol_price: float, + operator: Operator, + tx_type: TransactionType, ): w3_client, token_price = client_and_price @@ -551,7 +685,10 @@ def test_deploy_contract_more_1kb( token_balance_before = operator.get_token_balance(w3_client) contract, contract_deploy_tx = w3_client.deploy_and_get_contract( - "common/Fat", "0.8.10", account=account_with_all_tokens + contract="common/Fat", + version="0.8.10", + account=account_with_all_tokens, + tx_type=tx_type, ) sol_balance_after = operator.get_solana_balance() @@ -566,8 +703,17 @@ def test_deploy_contract_more_1kb( ) get_gas_used_percent(w3_client, contract_deploy_tx) + @pytest.mark.parametrize("tx_type", TransactionType) def test_deploy_contract_to_payed( - self, client_and_price, account_with_all_tokens, web3_client, web3_client_sol, sol_price, operator, accounts + self, + client_and_price: tuple[Web3Client, float], + account_with_all_tokens: LocalAccount, + web3_client: NeonChainWeb3Client, + web3_client_sol: Web3Client, + sol_price: float, + operator: Operator, + accounts: EthAccounts, + tx_type: TransactionType, ): sender_account = accounts[0] w3_client, token_price = client_and_price @@ -575,13 +721,21 @@ def test_deploy_contract_to_payed( nonce = w3_client.eth.get_transaction_count(account_with_all_tokens.address) contract_address = w3_client.keccak(rlp.encode((bytes.fromhex(sender_account.address[2:]), nonce)))[-20:] - w3_client.send_tokens(account_with_all_tokens, w3_client.to_checksum_address(contract_address.hex()), 5000) + w3_client.send_tokens( + from_=account_with_all_tokens, + to=w3_client.to_checksum_address(contract_address.hex()), + value=5000, + tx_type=tx_type, + ) sol_balance_before = operator.get_solana_balance() token_balance_before = operator.get_token_balance(w3_client) contract, contract_deploy_tx = w3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=account_with_all_tokens + contract="common/Counter", + version="0.8.10", + account=account_with_all_tokens, + tx_type=tx_type, ) sol_balance_after = operator.get_solana_balance() @@ -595,8 +749,16 @@ def test_deploy_contract_to_payed( ) get_gas_used_percent(w3_client, contract_deploy_tx) + @pytest.mark.parametrize("tx_type", TransactionType) def test_deploy_contract_to_exist_unpayed( - self, client_and_price, account_with_all_tokens, web3_client, web3_client_sol, sol_price, operator + self, + client_and_price: tuple[Web3Client, float], + account_with_all_tokens: LocalAccount, + web3_client: NeonChainWeb3Client, + web3_client_sol: Web3Client, + sol_price: float, + operator: Operator, + tx_type: TransactionType, ): w3_client, token_price = client_and_price @@ -609,10 +771,19 @@ def test_deploy_contract_to_exist_unpayed( w3_client.keccak(rlp.encode((bytes.fromhex(account_with_all_tokens.address[2:]), nonce)))[-20:].hex() ) with pytest.raises(ValueError, match=GAS_LIMIT_ERROR): - w3_client.send_tokens(account_with_all_tokens, contract_address, 100, gas=1) + w3_client.send_tokens( + from_=account_with_all_tokens, + to=contract_address, + value=100, + gas=1, + tx_type=tx_type, + ) _, contract_deploy_tx = w3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=account_with_all_tokens + contract="common/Counter", + version="0.8.10", + account=account_with_all_tokens, + tx_type=tx_type, ) sol_balance_after_deploy = operator.get_solana_balance() @@ -632,15 +803,25 @@ def test_deploy_contract_to_exist_unpayed( @pytest.mark.slow @pytest.mark.timeout(16 * Time.MINUTE) + @pytest.mark.parametrize("tx_type", TransactionType) def test_deploy_contract_alt_on( - self, sol_client, neon_price, sol_price, operator, web3_client, accounts, alt_contract + self, + sol_client: SolanaClient, + neon_price: float, + sol_price: float, + operator: Operator, + web3_client: NeonChainWeb3Client, + accounts: EthAccounts, + alt_contract: Contract, + tx_type: TransactionType, + ): """Trigger transaction than requires more than 30 accounts""" - sender_account = accounts[0] + sender_account = accounts[1] accounts_quantity = 45 sol_balance_before = operator.get_solana_balance() neon_balance_before = operator.get_token_balance(web3_client) - tx = web3_client.make_raw_tx(sender_account) + tx = web3_client.make_raw_tx(from_=sender_account, tx_type=tx_type) instr = alt_contract.functions.fill(accounts_quantity).build_transaction(tx) receipt = web3_client.send_transaction(sender_account, instr) @@ -671,15 +852,26 @@ def test_deploy_contract_alt_on( get_gas_used_percent(web3_client, receipt) + @pytest.mark.parametrize("tx_type", TransactionType) def test_deploy_contract_alt_off( - self, sol_client, accounts, web3_client, sol_price, neon_price, operator, alt_contract + self, + sol_client: SolanaClient, + neon_price: float, + sol_price: float, + operator: Operator, + web3_client: NeonChainWeb3Client, + accounts: EthAccounts, + alt_contract: Contract, + tx_type: TransactionType, ): + # see logs by hash, try with new account + # if no fails - other tests interference """Trigger transaction than requires less than 30 accounts""" accounts_quantity = 10 sol_balance_before = operator.get_solana_balance() token_balance_before = operator.get_token_balance(web3_client) - tx = web3_client.make_raw_tx(accounts[0].address) + tx = web3_client.make_raw_tx(from_=accounts[0].address, tx_type=tx_type) instr = alt_contract.functions.fill(accounts_quantity).build_transaction(tx) receipt = web3_client.send_transaction(accounts[0], instr) @@ -719,27 +911,59 @@ def test_deploy_big_contract_with_structures(self, client_and_price, account_wit ) get_gas_used_percent(w3_client, receipt) + # @pytest.mark.skip(reason="work incorrect very often") + def test_deploy_big_contract_with_structures_eip_1559( + self, + web3_client: NeonChainWeb3Client, + accounts: EthAccounts, + neon_price: float, + sol_price: float, + operator: Operator, + ): + + sol_balance_before = operator.get_solana_balance() + token_balance_before = operator.get_token_balance(web3_client) + + account = accounts.create_account() + + contract, receipt = web3_client.deploy_and_get_contract( + contract="EIPs/ERC3475", + version="0.8.10", + account=account, + tx_type=TransactionType.EIP_1559, + ) + + sol_balance_after = operator.get_solana_balance() + token_balance_after = operator.get_token_balance(web3_client) + token_diff = web3_client.to_main_currency(token_balance_after - token_balance_before) + assert_profit( + sol_balance_before - sol_balance_after, sol_price, token_diff, neon_price, web3_client.native_token_name + ) + get_gas_used_percent(web3_client, receipt) + @pytest.mark.timeout(30 * Time.MINUTE) @pytest.mark.slow @pytest.mark.parametrize("value", [20, 30]) + @pytest.mark.parametrize("tx_type", TransactionType) def test_call_contract_with_mapping_updating( self, - client_and_price, - account_with_all_tokens, - sol_price, - web3_client, - web3_client_sol, - sol_client, - value, - operator, - mapping_actions_contract + client_and_price: tuple[Web3Client, float], + account_with_all_tokens: LocalAccount, + sol_price: float, + web3_client: NeonChainWeb3Client, + web3_client_sol: Web3Client, + sol_client: SolanaClient, + value: int, + operator: Operator, + mapping_actions_contract: Contract, + tx_type: TransactionType, ): w3_client, token_price = client_and_price sol_balance_before = operator.get_solana_balance() token_balance_before = operator.get_token_balance(w3_client) - tx = w3_client.make_raw_tx(account_with_all_tokens.address) + tx = w3_client.make_raw_tx(from_=account_with_all_tokens.address, tx_type=tx_type) instruction_tx = mapping_actions_contract.functions.replaceValues(value).build_transaction(tx) receipt = w3_client.send_transaction(account_with_all_tokens, instruction_tx) @@ -761,3 +985,104 @@ def test_call_contract_with_mapping_updating( assert_profit( sol_balance_before - sol_balance_after, sol_price, token_diff, token_price, w3_client.native_token_name ) + + @pytest.mark.skip(reason="work incorrect very often") + def test_eip_1559_zero_priority_fee( + self, + client_and_price: tuple[Web3Client, float], + operator: Operator, + account_with_all_tokens: LocalAccount, + sol_price: float, + ): + w3_client, token_price = client_and_price + sol_balance_before = operator.get_solana_balance() + token_balance_before = operator.get_token_balance(w3_client) + + last_block = w3_client._web3.eth.get_block(block_identifier="latest") + base_fee_per_gas = last_block.baseFeePerGas # noqa + gas_price = w3_client.gas_price() + assert base_fee_per_gas == gas_price + + recipient = w3_client.create_account() + transfer_value = 10 + + receipt = w3_client.send_tokens_eip_1559( + from_=account_with_all_tokens, + to=recipient, + value=transfer_value, + max_priority_fee_per_gas=0, + max_fee_per_gas=base_fee_per_gas, + ) + + assert w3_client.get_balance(recipient) == transfer_value + + sol_balance_after = operator.get_solana_balance() + token_balance_after = operator.get_token_balance(w3_client) + sol_diff = sol_balance_before - sol_balance_after + + assert sol_balance_before > sol_balance_after, "Operator SOL balance incorrect" + token_diff = w3_client.to_main_currency(token_balance_after - token_balance_before) + assert_profit(sol_diff, sol_price, token_diff, token_price, w3_client.native_token_name) + get_gas_used_percent(w3_client, receipt) + + def test_eip_1559_profit( + self, + client_and_price: tuple[Web3Client, float], + operator: Operator, + account_with_all_tokens: LocalAccount, + sol_price: float + ): + # Calculate profit and expense with a type-0 transaction + w3_client, token_price = client_and_price + sol_balance_before = operator.get_solana_balance() + token_balance_before = operator.get_token_balance(w3_client) + + recipient_0 = w3_client.create_account() + value = 1000000 + + w3_client.send_tokens( + from_=account_with_all_tokens, + to=recipient_0, + value=value, + ) + + sol_balance_after = operator.get_solana_balance() + token_balance_after = operator.get_token_balance(w3_client) + + token_diff = w3_client.to_main_currency(token_balance_after - token_balance_before) + operator_expense_type_0 = sol_balance_before - sol_balance_after + expense_usd_0 = Decimal(operator_expense_type_0 / LAMPORT_PER_SOL, DECIMAL_CONTEXT) * Decimal(sol_price, DECIMAL_CONTEXT) + revenue_usd_0 = Decimal(token_diff, DECIMAL_CONTEXT) * Decimal(token_price, DECIMAL_CONTEXT) + profit_tx_type_0 = revenue_usd_0 - expense_usd_0 + + # Calculate profit and expense with a type-2 transaction + sol_balance_before = operator.get_solana_balance() + token_balance_before = operator.get_token_balance(w3_client) + + base_fee_per_gas = w3_client.base_fee_per_gas() + base_fee_multiplier = 1.1 + max_priority_fee_per_gas = 100000000 + max_fee_per_gas = int((base_fee_per_gas * base_fee_multiplier) + max_priority_fee_per_gas) + recipient_2 = w3_client.create_account() + + w3_client.send_tokens_eip_1559( + from_=account_with_all_tokens, + to=recipient_2, + value=value, + max_priority_fee_per_gas=max_priority_fee_per_gas, + max_fee_per_gas=max_fee_per_gas, + ) + + sol_balance_after = operator.get_solana_balance() + token_balance_after = operator.get_token_balance(w3_client) + + operator_expense_type_2 = sol_balance_before - sol_balance_after + token_diff = w3_client.to_main_currency(token_balance_after - token_balance_before) + assert_profit(operator_expense_type_2, sol_price, token_diff, token_price, w3_client.native_token_name) + + expense_usd_2 = Decimal(operator_expense_type_2 / LAMPORT_PER_SOL, DECIMAL_CONTEXT) * Decimal(sol_price, DECIMAL_CONTEXT) + revenue_usd_2 = Decimal(token_diff, DECIMAL_CONTEXT) * Decimal(token_price, DECIMAL_CONTEXT) + profit_tx_type_2 = revenue_usd_2 - expense_usd_2 + + # compare operator profits + assert profit_tx_type_2 > profit_tx_type_0 diff --git a/integration/tests/migrations/test_account_migration.py b/integration/tests/migrations/test_account_migration.py index eecff784ff..fb46d160e2 100644 --- a/integration/tests/migrations/test_account_migration.py +++ b/integration/tests/migrations/test_account_migration.py @@ -10,6 +10,7 @@ from web3.logs import DISCARD from integration.tests.economy.steps import assert_profit +from eth_account.signers.local import LocalAccount from utils.erc20wrapper import ERC20Wrapper from utils.erc721ForMetaplex import ERC721ForMetaplex from utils.helpers import gen_hash_of_block @@ -43,7 +44,7 @@ def accounts(web3_client): @pytest.fixture(scope="class") -def bob(accounts): +def bob(accounts) -> LocalAccount: return accounts[0] @@ -54,25 +55,25 @@ def alice(accounts): @pytest.fixture(scope="class") def trx_list(): - list = [] - yield list + list_ = [] + yield list_ print("Trx list:") - for trx in list: + for trx in list_: print(trx.hex()) + @pytest.fixture(scope="class") def counter(web3_client, accounts): contract_address = os.environ.get("COUNTER_ADDRESS") if contract_address: - contract = web3_client.get_deployed_contract( - contract_address, contract_file="common/Counter" - ) + contract = web3_client.get_deployed_contract(contract_address, contract_file="common/Counter") print(f"Using Counter deployed earlier at {contract_address}") else: contract, _ = web3_client.deploy_and_get_contract("common/Counter", "0.8.10", account=accounts[0]) print(f"Counter deployed at address: {contract.address}") return contract + @pytest.fixture(scope="class") def counter_with_map(web3_client, accounts): contract_address = os.environ.get("COUNTER_MAP_ADDRESS") @@ -82,12 +83,13 @@ def counter_with_map(web3_client, accounts): ) print(f"Using CounterWithMap deployed earlier at {contract_address}") else: - contract, _ = web3_client.deploy_and_get_contract("common/Counter", "0.8.10", contract_name="CounterWithMap", account=accounts[0]) + contract, _ = web3_client.deploy_and_get_contract( + "common/Counter", "0.8.10", contract_name="CounterWithMap", account=accounts[0] + ) print(f"CounterWithMap deployed at address: {contract.address}") return contract - @pytest.fixture(scope="class") def erc20(web3_client, faucet, sol_client, solana_account, bob): contract_address = os.environ.get("ERC20_ADDRESS") @@ -134,26 +136,26 @@ def erc721(web3_client, faucet, bob): return erc721 -def check_counter(sender, contract, web3_client, sol_client): +def check_counter(sender, contract, web3_client, sol_client): tx = web3_client.make_raw_tx(sender.address) - solana_accounts = get_solana_accounts_by_emulation(web3_client, sender, contract.address, - "inc()") - print_solana_accounts_info(sol_client, solana_accounts,"before inc") + solana_accounts = get_solana_accounts_by_emulation(web3_client, sender, contract.address, "inc()") + print_solana_accounts_info(sol_client, solana_accounts, "before inc") value_before = contract.functions.get().call({"from": sender.address}) print("Value before:", value_before) tx = contract.functions.inc().build_transaction(tx) receipt = web3_client.send_transaction(sender, tx) - print_solana_accounts_info(sol_client, solana_accounts,"after inc") + print_solana_accounts_info(sol_client, solana_accounts, "after inc") print("Transaction receipt:", receipt) value_after = contract.functions.get().call({"from": sender.address}) print("Value after:", value_after) assert value_after == value_before + 1 + def get_solana_accounts_by_emulation(web3_client, sender, contract, function_signature, params=None): data = abi.function_signature_to_4byte_selector(function_signature) @@ -161,13 +163,12 @@ def get_solana_accounts_by_emulation(web3_client, sender, contract, function_sig types = function_signature.split("(")[1].split(")")[0].split(",") data += eth_abi.encode(types, params) tx = web3_client.make_raw_tx(sender.address, contract, data=data, estimate_gas=True) - signed_tx = web3_client.eth.account.sign_transaction( - tx, sender.key) - result = web3_client.get_neon_emulate( - str(signed_tx.rawTransaction.hex())[2:]) + signed_tx = web3_client.eth.account.sign_transaction(tx, sender.key) + result = web3_client.get_neon_emulate(str(signed_tx.rawTransaction.hex())[2:]) print(result) return [item["pubkey"] for item in result["result"]["solana_accounts"]] + def print_solana_accounts_info(sol_client, accounts, action): print("Solana accounts info", action) for acc in accounts: @@ -195,7 +196,6 @@ def check_operator_balance( token_diff = web3_client.to_main_currency(token_balance_after - token_balance_before) assert_profit(sol_diff, sol_price, token_diff, neon_price, web3_client.native_token_name) - def test_transfers(self, alice, bob, accounts, web3_client, trx_list, check_operator_balance): web3_client.send_neon(alice, bob, 5) for i in range(5): @@ -249,36 +249,45 @@ def test_economics_for_erc721_mint(self, erc721, web3_client, check_operator_bal seed = web3_client.text_to_bytes32(gen_hash_of_block(8)) erc721.mint(seed, erc721.account.address, "uri") - - def test_erc721_interaction(self, erc721, web3_client, sol_client, bob, alice, accounts, trx_list, check_operator_balance): + def test_erc721_interaction( + self, erc721, web3_client, sol_client, bob, alice, accounts, trx_list, check_operator_balance + ): seed = web3_client.text_to_bytes32(gen_hash_of_block(8)) - solana_accounts = get_solana_accounts_by_emulation(web3_client, erc721.account, erc721.contract.address, - "mint(bytes32,address,string)", - [seed, erc721.account.address, "uri"]) - print_solana_accounts_info(sol_client, solana_accounts,"before mint") + solana_accounts = get_solana_accounts_by_emulation( + web3_client, + erc721.account, + erc721.contract.address, + "mint(bytes32,address,string)", + [seed, erc721.account.address, "uri"], + ) + print_solana_accounts_info(sol_client, solana_accounts, "before mint") token_id = erc721.mint(seed, erc721.account.address, "uri") - print_solana_accounts_info(sol_client, solana_accounts,"after mint") + print_solana_accounts_info(sol_client, solana_accounts, "after mint") balance_usr1_before = erc721.contract.functions.balanceOf(erc721.account.address).call() balance_usr2_before = erc721.contract.functions.balanceOf(alice.address).call() - solana_accounts = get_solana_accounts_by_emulation(web3_client, erc721.account, erc721.contract.address, - "approve(address,uint256)", - [alice.address, token_id]) - print_solana_accounts_info(sol_client, solana_accounts,"before approve") + solana_accounts = get_solana_accounts_by_emulation( + web3_client, erc721.account, erc721.contract.address, "approve(address,uint256)", [alice.address, token_id] + ) + print_solana_accounts_info(sol_client, solana_accounts, "before approve") resp = erc721.approve(alice.address, token_id, erc721.account) - print_solana_accounts_info(sol_client, solana_accounts,"after approve") + print_solana_accounts_info(sol_client, solana_accounts, "after approve") trx_list.append(resp["transactionHash"]) - solana_accounts = get_solana_accounts_by_emulation(web3_client, alice, erc721.contract.address, - "transferFrom(address,address,uint256)", - [erc721.account.address, alice.address, token_id]) - print_solana_accounts_info(sol_client, solana_accounts,"before transfer_from") + solana_accounts = get_solana_accounts_by_emulation( + web3_client, + alice, + erc721.contract.address, + "transferFrom(address,address,uint256)", + [erc721.account.address, alice.address, token_id], + ) + print_solana_accounts_info(sol_client, solana_accounts, "before transfer_from") resp = erc721.transfer_from(erc721.account.address, alice.address, token_id, alice) - print_solana_accounts_info(sol_client, solana_accounts,"after transfer_from") + print_solana_accounts_info(sol_client, solana_accounts, "after transfer_from") trx_list.append(resp["transactionHash"]) @@ -368,8 +377,6 @@ def test_erc20_interaction(self, erc20, web3_client, bob, alice, accounts, trx_l assert balance_acc2_after == balance_acc2_before + amount assert total_before == total_after - - def test_simple_counter(self, web3_client, accounts, counter, sol_client): # the data fits into contract account sender = accounts[7] @@ -378,5 +385,3 @@ def test_simple_counter(self, web3_client, accounts, counter, sol_client): def test_counter_with_map(self, web3_client, accounts, counter_with_map, sol_client): sender = accounts[9] check_counter(sender, counter_with_map, web3_client, sol_client) - - diff --git a/integration/tests/neon_evm/conftest.py b/integration/tests/neon_evm/conftest.py index f224af463b..0bb0e85c77 100644 --- a/integration/tests/neon_evm/conftest.py +++ b/integration/tests/neon_evm/conftest.py @@ -4,9 +4,9 @@ import eth_abi import pytest from eth_utils import abi -from solana.keypair import Keypair +from solders.keypair import Keypair from eth_keys import keys as eth_keys -from solana.publickey import PublicKey +from solders.pubkey import Pubkey from solana.rpc.commitment import Confirmed from utils.consts import OPERATOR_KEYPAIR_PATH @@ -26,14 +26,14 @@ def evm_loader() -> EvmLoader: return loader -def prepare_operator(key_file, evm_loader): +def prepare_operator(key_file, evm_loader: EvmLoader): with open(key_file, "r") as key: - secret_key = json.load(key)[:32] - account = Keypair.from_secret_key(secret_key) + secret_key = json.load(key) + account = Keypair.from_bytes(secret_key) - evm_loader.request_airdrop(account.public_key, 1000 * 10**9, commitment=Confirmed) + evm_loader.request_airdrop(account.pubkey(), 1000 * 10**9, commitment=Confirmed) - operator_ether = eth_keys.PrivateKey(account.secret_key[:32]).public_key.to_canonical_address() + operator_ether = eth_keys.PrivateKey(account.secret()[:32]).public_key.to_canonical_address() ether_balance_pubkey = evm_loader.ether2operator_balance(account, operator_ether) acc_info = evm_loader.get_account_info(ether_balance_pubkey, commitment=Confirmed) @@ -44,7 +44,7 @@ def prepare_operator(key_file, evm_loader): @pytest.fixture(scope="session") -def default_operator_keypair(evm_loader) -> Keypair: +def default_operator_keypair(evm_loader: EvmLoader) -> Keypair: """ Initialized solana keypair with balance. Get private keys from ci/operator-keypairs/id.json """ @@ -110,12 +110,12 @@ def sender_with_tokens(evm_loader, operator_keypair) -> Caller: @pytest.fixture(scope="session") -def holder_acc(operator_keypair, evm_loader) -> PublicKey: +def holder_acc(operator_keypair, evm_loader) -> Pubkey: return create_holder(operator_keypair, evm_loader) @pytest.fixture(scope="function") -def new_holder_acc(operator_keypair, evm_loader) -> PublicKey: +def new_holder_acc(operator_keypair, evm_loader) -> Pubkey: return create_holder(operator_keypair, evm_loader) @@ -225,7 +225,7 @@ def erc20_for_spl( "deploy(string,string,string,uint8)", ["Test", "TTT", "http://uri.com", 9], ) - additional_accounts = [PublicKey(item["pubkey"]) for item in emulate_result["solana_accounts"]] + additional_accounts = [Pubkey.from_string(item["pubkey"]) for item in emulate_result["solana_accounts"]] signed_tx = make_contract_call_trx( evm_loader, sender_with_tokens, diff --git a/integration/tests/neon_evm/test_execute_trx_from_instruction.py b/integration/tests/neon_evm/test_execute_trx_from_instruction.py index 27e17fa31c..c02277b634 100644 --- a/integration/tests/neon_evm/test_execute_trx_from_instruction.py +++ b/integration/tests/neon_evm/test_execute_trx_from_instruction.py @@ -2,15 +2,14 @@ import string import pytest -import solana import eth_abi from eth_account.datastructures import SignedTransaction from eth_keys import keys as eth_keys from eth_utils import abi, to_text from hexbytes import HexBytes -from solana.keypair import Keypair -from solana.publickey import PublicKey -from solana.rpc.commitment import Confirmed +from solders.keypair import Keypair +from solders.pubkey import Pubkey +from solana.rpc.core import RPCException as SolanaRPCException from spl.token.instructions import get_associated_token_address from utils.types import Caller, Contract @@ -54,9 +53,9 @@ def test_transfer_transaction_with_non_existing_recipient( self, operator_keypair, treasury_pool, sender_with_tokens: Caller, evm_loader, holder_acc ): # recipient account should be created - recipient = Keypair.generate() + recipient = Keypair() - recipient_ether = eth_keys.PrivateKey(recipient.secret_key[:32]).public_key.to_canonical_address() + recipient_ether = eth_keys.PrivateKey(recipient.secret()[:32]).public_key.to_canonical_address() recipient_solana_address, _ = evm_loader.ether2program(recipient_ether) recipient_balance_address = evm_loader.ether2balance(recipient_ether) amount = 10 @@ -70,7 +69,7 @@ def test_transfer_transaction_with_non_existing_recipient( [ sender_with_tokens.balance_account_address, recipient_balance_address, - PublicKey(recipient_solana_address), + Pubkey.from_string(recipient_solana_address), ], ) @@ -160,7 +159,7 @@ def test_incorrect_chain_id( signed_tx = make_eth_transaction( evm_loader, session_user.eth_address, None, sender_with_tokens, amount, chain_id=1 ) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INVALID_CHAIN_ID): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.INVALID_CHAIN_ID): evm_loader.execute_trx_from_instruction( operator_keypair, holder_acc, @@ -191,7 +190,7 @@ def test_incorrect_nonce( session_user.solana_account_address, ], ) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INVALID_NONCE): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.INVALID_NONCE): evm_loader.execute_trx_from_instruction( operator_keypair, holder_acc, @@ -214,7 +213,7 @@ def test_insufficient_funds( evm_loader, sender_with_tokens.eth_address, None, session_user, user_balance + 1 ) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INSUFFICIENT_FUNDS): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.INSUFFICIENT_FUNDS): evm_loader.execute_trx_from_instruction( operator_keypair, holder_acc, @@ -235,7 +234,7 @@ def test_gas_limit_reached( amount = 10 signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, amount, gas=1) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.OUT_OF_GAS): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.OUT_OF_GAS): evm_loader.execute_trx_from_instruction( operator_keypair, holder_acc, @@ -253,7 +252,7 @@ def test_sender_missed_in_remaining_accounts( self, operator_keypair, treasury_pool, session_user: Caller, sender_with_tokens: Caller, evm_loader, holder_acc ): signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): evm_loader.execute_trx_from_instruction( operator_keypair, holder_acc, @@ -267,7 +266,7 @@ def test_recipient_missed_in_remaining_accounts( self, operator_keypair, treasury_pool, sender_with_tokens: Caller, session_user: Caller, evm_loader, holder_acc ): signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): evm_loader.execute_trx_from_instruction( operator_keypair, holder_acc, @@ -283,10 +282,10 @@ def test_incorrect_treasure_pool( signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) treasury_buffer = b"\x02\x00\x00\x00" - treasury_pool = Keypair().public_key + treasury_pool = Keypair().pubkey() error = str.format(InstructionAsserts.INVALID_ACCOUNT, treasury_pool) - with pytest.raises(solana.rpc.core.RPCException, match=error): + with pytest.raises(SolanaRPCException, match=error): evm_loader.execute_trx_from_instruction(operator_keypair, holder_acc, treasury_pool, treasury_buffer, signed_tx, []) def test_incorrect_treasure_index( @@ -296,7 +295,7 @@ def test_incorrect_treasure_index( treasury_buffer = b"\x03\x00\x00\x00" error = str.format(InstructionAsserts.INVALID_ACCOUNT, treasury_pool.account) - with pytest.raises(solana.rpc.core.RPCException, match=error): + with pytest.raises(SolanaRPCException, match=error): evm_loader.execute_trx_from_instruction( operator_keypair, holder_acc, treasury_pool.account, treasury_buffer, signed_tx, [] ) @@ -306,7 +305,7 @@ def test_incorrect_operator_account( ): signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) fake_operator = Keypair() - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ACC_NOT_FOUND): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.ACC_NOT_FOUND): evm_loader.execute_trx_from_instruction( fake_operator, holder_acc, @@ -345,9 +344,9 @@ def test_incorrect_system_program( self, sender_with_tokens, operator_keypair, evm_loader, treasury_pool, session_user, holder_acc ): signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) - fake_sys_program_id = Keypair().public_key + fake_sys_program_id = Keypair().pubkey() with pytest.raises( - solana.rpc.core.RPCException, match=str.format(InstructionAsserts.NOT_SYSTEM_PROGRAM, fake_sys_program_id) + SolanaRPCException, match=str.format(InstructionAsserts.NOT_SYSTEM_PROGRAM, fake_sys_program_id) ): evm_loader.execute_trx_from_instruction( operator_keypair, @@ -363,16 +362,16 @@ def test_incorrect_system_program( def test_operator_does_not_have_enough_founds( self, evm_loader, treasury_pool, session_user: Caller, sender_with_tokens: Caller, operator_keypair, holder_acc ): - key = Keypair.generate() - caller_ether = eth_keys.PrivateKey(key.secret_key[:32]).public_key.to_canonical_address() + key = Keypair() + caller_ether = eth_keys.PrivateKey(key.secret()[:32]).public_key.to_canonical_address() caller, caller_nonce = evm_loader.ether2program(caller_ether) - caller_token = get_associated_token_address(PublicKey(caller), NEON_TOKEN_MINT_ID) + caller_token = get_associated_token_address(Pubkey.from_string(caller), NEON_TOKEN_MINT_ID) - operator_without_money = Caller(key, PublicKey(caller), caller_ether, caller_nonce, caller_token) + operator_without_money = Caller(key, Pubkey.from_string(caller), caller_ether, caller_nonce, caller_token) signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) with pytest.raises( - solana.rpc.core.RPCException, match="Attempt to debit an account but found no record of a prior credit" + SolanaRPCException, match="Attempt to debit an account but found no record of a prior credit" ): evm_loader.execute_trx_from_instruction( operator_without_money.solana_account, diff --git a/integration/tests/neon_evm/test_external_call_for_instructions.py b/integration/tests/neon_evm/test_external_call_for_instructions.py new file mode 100644 index 0000000000..13f67c1bc3 --- /dev/null +++ b/integration/tests/neon_evm/test_external_call_for_instructions.py @@ -0,0 +1,105 @@ +import pytest +from solana.rpc.core import RPCException +from solana.transaction import AccountMeta, Transaction, Instruction + +from utils.consts import TEST_INVOKE_ID +from utils.instructions import ( + TransactionWithComputeBudget, + make_ExecuteTrxFromInstruction, +) +from utils.solana_client import SolanaClient +from utils.web3client import NeonChainWeb3Client + +from .utils.ethereum import make_eth_transaction + + +@pytest.mark.usefixtures("sol_client", "web3_client") +class TestExternalCall: + sol_client: SolanaClient + web3_client: NeonChainWeb3Client + + def test_execute_from_instruction( + self, operator_keypair, evm_loader, treasury_pool, sender_with_tokens, session_user, holder_acc + ): + operator_balance = evm_loader.get_operator_balance_pubkey(operator_keypair) + amount = 1 + + msg = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, amount) + accounts = [ + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address, + session_user.solana_account_address, + session_user.balance_account_address, + ] + + sender_initial_balance = evm_loader.get_neon_balance(sender_with_tokens.eth_address) + receiver_initial_balance = evm_loader.get_neon_balance(session_user.eth_address) + + instruction = make_ExecuteTrxFromInstruction( + operator_keypair, + operator_balance, + holder_acc, + evm_loader.loader_id, + treasury_pool.account, + treasury_pool.buffer, + msg.rawTransaction, + accounts, + ) + upd_instruction = Instruction( + accounts=[AccountMeta(evm_loader.loader_id, is_signer=False, is_writable=True)] + instruction.accounts, + program_id=TEST_INVOKE_ID, + data=instruction.data, + ) + trx = Transaction() + + trx.add(upd_instruction) + self.sol_client.send_tx(trx, operator_keypair) + + assert sender_initial_balance == evm_loader.get_neon_balance(sender_with_tokens.eth_address) + amount + assert receiver_initial_balance == evm_loader.get_neon_balance(session_user.eth_address) - amount + + def test_execute_from_instruction_eip_1559( + self, operator_keypair, evm_loader, treasury_pool, sender_with_tokens, session_user, holder_acc + ): + operator_balance = evm_loader.get_operator_balance_pubkey(operator_keypair) + amount = 1 + compute_unit_price = 1000000 + max_fee_per_gas = 100 + max_priority_fee_per_gas = 10 + + msg = make_eth_transaction( + evm_loader, + session_user.eth_address, + None, + sender_with_tokens, + amount, + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas + ) + + accounts = [ + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address, + session_user.solana_account_address, + session_user.balance_account_address, + ] + + instruction = make_ExecuteTrxFromInstruction( + operator_keypair, + operator_balance, + holder_acc, + evm_loader.loader_id, + treasury_pool.account, + treasury_pool.buffer, + msg.rawTransaction, + accounts, + ) + upd_instruction = Instruction( + accounts=[AccountMeta(evm_loader.loader_id, is_signer=False, is_writable=True)] + instruction.accounts, + program_id=TEST_INVOKE_ID, + data=instruction.data, + ) + trx = TransactionWithComputeBudget(operator_keypair, compute_unit_price) + trx.add(upd_instruction) + with pytest.raises(RPCException, match="CPI calls of Neon EVM are forbidden for DynamicFee transaction type"): + self.sol_client.send_tx(trx, operator_keypair) diff --git a/integration/tests/neon_evm/test_holder_account.py b/integration/tests/neon_evm/test_holder_account.py index 0344a58e95..c24060da94 100644 --- a/integration/tests/neon_evm/test_holder_account.py +++ b/integration/tests/neon_evm/test_holder_account.py @@ -2,12 +2,12 @@ from random import randrange import pytest -import solana from eth_keys import keys as eth_keys -from solana.publickey import PublicKey +from solders.pubkey import Pubkey from solana.rpc.commitment import Confirmed from solana.transaction import Transaction -import solana.system_program as sp +import solders.system_program as sp +from solana.rpc.core import RPCException as SolanaRPCException from utils.evm_loader import EvmLoader @@ -29,11 +29,11 @@ from .utils.transaction_checks import check_transaction_logs_have_text -def transaction_from_holder(evm_loader: EvmLoader, key: PublicKey): +def transaction_from_holder(evm_loader: EvmLoader, key: Pubkey): data = evm_loader.get_account_info(key, commitment=Confirmed).value.data header = HOLDER_ACCOUNT_INFO_LAYOUT.parse(data) - return data[HOLDER_ACCOUNT_INFO_LAYOUT.sizeof() :][: header.len] + return data[HOLDER_ACCOUNT_INFO_LAYOUT.sizeof():][: header.len] def test_create_holder_account(operator_keypair, evm_loader): @@ -45,28 +45,28 @@ def test_create_holder_account(operator_keypair, evm_loader): def test_create_the_same_holder_account_by_another_user(operator_keypair, session_user, evm_loader): seed = str(randrange(1000000)) - storage = PublicKey( - sha256(bytes(operator_keypair.public_key) + bytes(seed, "utf8") + bytes(evm_loader.loader_id)).digest() + storage = Pubkey( + sha256(bytes(operator_keypair.pubkey()) + bytes(seed, "utf8") + bytes(evm_loader.loader_id)).digest() ) create_holder(operator_keypair, evm_loader, seed=seed, storage=storage) trx = Transaction() trx.add( make_CreateAccountWithSeed( - session_user.solana_account.public_key, - session_user.solana_account.public_key, + session_user.solana_account.pubkey(), + session_user.solana_account.pubkey(), seed, 10**9, 128 * 1024, evm_loader.loader_id, ), make_CreateHolderAccount( - storage, session_user.solana_account.public_key, bytes(seed, "utf8"), evm_loader.loader_id + storage, session_user.solana_account.pubkey(), bytes(seed, "utf8"), evm_loader.loader_id ), ) error = str.format(InstructionAsserts.INVALID_ACCOUNT, storage) - with pytest.raises(solana.rpc.core.RPCException, match=error): + with pytest.raises(SolanaRPCException, match=error): evm_loader.send_tx(trx, session_user.solana_account) @@ -91,7 +91,7 @@ def test_write_tx_to_holder_by_no_owner(operator_keypair, session_user, second_s holder_acc = create_holder(operator_keypair, evm_loader) signed_tx = make_eth_transaction(evm_loader, second_session_user.eth_address, None, session_user, 10) - with pytest.raises(solana.rpc.core.RPCException, match="invalid owner"): + with pytest.raises(SolanaRPCException, match="invalid owner"): evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, session_user.solana_account) @@ -106,18 +106,18 @@ def test_success_refund_after_holder_deleting(operator_keypair, evm_loader): holder_acc = create_holder(operator_keypair, evm_loader) pre_storage = evm_loader.get_solana_balance(holder_acc) - pre_acc = evm_loader.get_solana_balance(operator_keypair.public_key) + pre_acc = evm_loader.get_solana_balance(operator_keypair.pubkey()) delete_holder(holder_acc, operator_keypair, operator_keypair, evm_loader) - post_acc = evm_loader.get_solana_balance(operator_keypair.public_key) + post_acc = evm_loader.get_solana_balance(operator_keypair.pubkey()) assert pre_storage + pre_acc, post_acc + 5000 def test_delete_holder_by_no_owner(operator_keypair, user_account, evm_loader): holder_acc = create_holder(operator_keypair, evm_loader) - with pytest.raises(solana.rpc.core.RPCException, match="invalid owner"): + with pytest.raises(SolanaRPCException, match="invalid owner"): delete_holder(holder_acc, user_account.solana_account, user_account.solana_account, evm_loader) @@ -143,7 +143,7 @@ def test_write_to_not_finalized_holder( evm_loader, user_account, rw_lock_contract, "unchange_storage(uint8,uint8)", [1, 1] ) - with pytest.raises(solana.rpc.core.RPCException, match="invalid tag"): + with pytest.raises(SolanaRPCException, match="invalid tag"): evm_loader.write_transaction_to_holder_account(signed_tx2, new_holder_acc, operator_keypair) @@ -177,18 +177,18 @@ def test_holder_write_integer_overflow(operator_keypair, holder_acc, evm_loader) trx = Transaction() trx.add( make_WriteHolder( - operator_keypair.public_key, evm_loader.loader_id, holder_acc, b"\x00" * 32, overflow_offset, b"\x00" * 1 + operator_keypair.pubkey(), evm_loader.loader_id, holder_acc, b"\x00" * 32, overflow_offset, b"\x00" * 1 ) ) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.HOLDER_OVERFLOW): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.HOLDER_OVERFLOW): evm_loader.send_tx(trx, operator_keypair) def test_holder_write_account_size_overflow(operator_keypair, holder_acc, evm_loader): overflow_offset = int(0xFFFFFFFF) trx = Transaction() - trx.add(make_WriteHolder(operator_keypair.public_key, evm_loader.loader_id, holder_acc, b"\x00" * 32, overflow_offset, b"\x00" * 1)) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.HOLDER_INSUFFICIENT_SIZE): + trx.add(make_WriteHolder(operator_keypair.pubkey(), evm_loader.loader_id, holder_acc, b"\x00" * 32, overflow_offset, b"\x00" * 1)) + with pytest.raises(SolanaRPCException, match=InstructionAsserts.HOLDER_INSUFFICIENT_SIZE): evm_loader.send_tx(trx, operator_keypair) @@ -198,33 +198,32 @@ def test_temporary_holder_acc_is_free(treasury_pool, sender_with_tokens, evm_loa user_as_operator = sender_with_tokens.solana_account amount = 10 - operator_ether = eth_keys.PrivateKey(user_as_operator.secret_key[:32]).public_key.to_canonical_address() + operator_ether = eth_keys.PrivateKey(user_as_operator.secret()[:32]).public_key.to_canonical_address() evm_loader.create_operator_balance_account(user_as_operator, operator_ether) - operator_balance_before = evm_loader.get_solana_balance(user_as_operator.public_key) + operator_balance_before = evm_loader.get_solana_balance(user_as_operator.pubkey()) trx = TransactionWithComputeBudget(user_as_operator) seed = str(randrange(1000000)) - holder_pubkey = PublicKey(sha256(bytes(user_as_operator.public_key) + bytes(seed, "utf8") + bytes(evm_loader.loader_id)).digest()) - create_acc_with_seed_instr = sp.create_account_with_seed( + holder_pubkey = Pubkey(sha256(bytes(user_as_operator.pubkey()) + bytes(seed, "utf8") + bytes(evm_loader.loader_id)).digest()) + create_acc_with_seed_instr = sp.create_account_with_seed( sp.CreateAccountWithSeedParams( - from_pubkey=user_as_operator.public_key, - new_account_pubkey=holder_pubkey , - base_pubkey=user_as_operator.public_key, + from_pubkey=user_as_operator.pubkey(), + to_pubkey=holder_pubkey, + base=user_as_operator.pubkey(), seed=seed, lamports=0, space=128 * 1024, - program_id=evm_loader.loader_id, + owner=evm_loader.loader_id, ) ) - create_holder_instruction = make_CreateHolderAccount(holder_pubkey, user_as_operator.public_key, bytes(seed, 'utf8'), evm_loader.loader_id) + create_holder_instruction = make_CreateHolderAccount(holder_pubkey, user_as_operator.pubkey(), bytes(seed, 'utf8'), evm_loader.loader_id) trx.add(create_acc_with_seed_instr) trx.add(create_holder_instruction) signed_tx = make_eth_transaction(evm_loader, sender_with_tokens.eth_address, None, sender_with_tokens, amount) operator_balance_account = evm_loader.get_operator_balance_pubkey(user_as_operator) - trx.add( make_ExecuteTrxFromInstruction( user_as_operator, @@ -242,13 +241,13 @@ def test_temporary_holder_acc_is_free(treasury_pool, sender_with_tokens, evm_loa ) resp = evm_loader.send_tx(trx, user_as_operator) check_transaction_logs_have_text(resp, "exit_status=0x11") - operator_balance_after = evm_loader.get_solana_balance(user_as_operator.public_key) + operator_balance_after = evm_loader.get_solana_balance(user_as_operator.pubkey()) operator_gas_paid_with_holder = operator_balance_before - operator_balance_after signed_tx = make_eth_transaction(evm_loader, sender_with_tokens.eth_address, None, sender_with_tokens, amount) holder_acc = create_holder(sender_with_tokens.solana_account, evm_loader, seed=str(randrange(1000000))) - operator_balance_before = evm_loader.get_solana_balance(user_as_operator.public_key) + operator_balance_before = evm_loader.get_solana_balance(user_as_operator.pubkey()) resp = evm_loader.execute_trx_from_instruction( user_as_operator, @@ -262,7 +261,7 @@ def test_temporary_holder_acc_is_free(treasury_pool, sender_with_tokens, evm_loa ], ) check_transaction_logs_have_text(resp, "exit_status=0x11") - operator_balance_after = evm_loader.get_solana_balance(user_as_operator.public_key) + operator_balance_after = evm_loader.get_solana_balance(user_as_operator.pubkey()) operator_gas_paid_without_holder = operator_balance_before - operator_balance_after assert operator_gas_paid_without_holder == operator_gas_paid_with_holder, "Gas paid differs!" diff --git a/integration/tests/neon_evm/test_interoperability.py b/integration/tests/neon_evm/test_interoperability.py index 96d91d6e32..3e3b0c22f0 100644 --- a/integration/tests/neon_evm/test_interoperability.py +++ b/integration/tests/neon_evm/test_interoperability.py @@ -5,14 +5,15 @@ from eth_utils import abi from eth_keys import keys as eth_keys -from solana.keypair import Keypair -from solana.publickey import PublicKey +from solders.keypair import Keypair +from solders.pubkey import Pubkey from solana.rpc.commitment import Confirmed from solana.rpc.core import RPCException from solana.rpc.types import TxOpts import spl.token.client -from solana.system_program import SYS_PROGRAM_ID -from solana.transaction import TransactionInstruction, AccountMeta +from spl.token.client import Token +from solders.system_program import ID as SYS_PROGRAM_ID +from solana.transaction import Instruction, AccountMeta from spl.token.instructions import create_associated_token_account, TransferParams, transfer @@ -43,17 +44,17 @@ from utils.types import Caller -def _create_mint_and_accounts(evm_loader, from_wallet, to_wallet, amount): +def _create_mint_and_accounts(evm_loader, from_wallet, to_wallet, amount) -> tuple[Token, Pubkey, Pubkey]: mint = spl.token.client.Token.create_mint( conn=evm_loader, payer=from_wallet, - mint_authority=from_wallet.public_key, + mint_authority=from_wallet.pubkey(), decimals=9, program_id=TOKEN_PROGRAM_ID, ) mint.payer = from_wallet - from_token_account = mint.create_associated_token_account(from_wallet.public_key) - to_token_account = mint.create_associated_token_account(to_wallet.public_key) + from_token_account = mint.create_associated_token_account(from_wallet.pubkey()) + to_token_account = mint.create_associated_token_account(to_wallet.pubkey()) mint.mint_to( dest=from_token_account, mint_authority=from_wallet, @@ -79,7 +80,7 @@ def test_get_payer(self, solana_caller): def test_get_solana_PDA(self, solana_caller): addr = solana_caller.get_solana_PDA(COUNTER_ID, b"123") - assert addr == (PublicKey.find_program_address([b"123"], COUNTER_ID))[0] + assert addr == (Pubkey.find_program_address([b"123"], COUNTER_ID))[0] def test_get_eth_ext_authority(self, solana_caller, sender_with_tokens): addr = solana_caller.get_eth_ext_authority(b"123", sender_with_tokens) @@ -96,9 +97,9 @@ def test_create_resource(self, sender_with_tokens, solana_caller, evm_loader): assert str(acc_info.value.owner) == str(MEMO_PROGRAM_ID) def test_execute_from_instruction_for_compute_budget(self, sender_with_tokens, solana_caller): - instruction = TransactionInstruction( + instruction = Instruction( program_id=COMPUTE_BUDGET_ID, - keys=[AccountMeta(sender_with_tokens.solana_account_address, is_signer=False, is_writable=False)], + accounts=[AccountMeta(sender_with_tokens.solana_account_address, is_signer=False, is_writable=False)], data=bytes.fromhex("02") + DEFAULT_UNITS.to_bytes(4, "little"), ) resp = solana_caller.execute(COMPUTE_BUDGET_ID, instruction, sender=sender_with_tokens) @@ -151,9 +152,9 @@ def test_execute_several_instr_in_one_trx(self, sender_with_tokens, solana_calle instruction_count = 10 resource_addr = solana_caller.create_resource(sender_with_tokens, b"123", 8, 1000000000, COUNTER_ID) - instruction = TransactionInstruction( + instruction = Instruction( program_id=COUNTER_ID, - keys=[ + accounts=[ AccountMeta(resource_addr, is_signer=False, is_writable=True), ], data=bytes([0x1]), @@ -173,9 +174,9 @@ def test_limit_of_simple_instr_in_one_trx(self, sender_with_tokens, solana_calle instruction_count = 24 resource_addr = solana_caller.create_resource(sender_with_tokens, b"123", 8, 1000000000, COUNTER_ID) - instruction = TransactionInstruction( + instruction = Instruction( program_id=COUNTER_ID, - keys=[ + accounts=[ AccountMeta(resource_addr, is_signer=False, is_writable=True), ], data=bytes([0x1]), @@ -192,22 +193,22 @@ def test_limit_of_simple_instr_in_one_trx(self, sender_with_tokens, solana_calle def test_transfer_sol_with_cpi(self, sender_with_tokens, solana_caller, evm_loader): recipient = evm_loader.create_account(sender_with_tokens.solana_account, 0, TRANSFER_SOL_ID) amount = random.randint(1, 1000000) - instruction = TransactionInstruction( + instruction = Instruction( program_id=TRANSFER_SOL_ID, - keys=[ - AccountMeta(sender_with_tokens.solana_account.public_key, is_signer=True, is_writable=True), - AccountMeta(recipient.public_key, is_signer=False, is_writable=True), + accounts=[ + AccountMeta(sender_with_tokens.solana_account.pubkey(), is_signer=True, is_writable=True), + AccountMeta(recipient.pubkey(), is_signer=False, is_writable=True), AccountMeta(SYS_PROGRAM_ID, is_signer=False, is_writable=False), ], data=bytes([0x0]) + amount.to_bytes(8, "little"), ) call_params = [(TRANSFER_SOL_ID, 0, instruction)] - balance_before = evm_loader.get_balance(recipient.public_key, commitment=Confirmed).value + balance_before = evm_loader.get_balance(recipient.pubkey(), commitment=Confirmed).value resp = solana_caller.batch_execute( call_params, sender_with_tokens, additional_signers=[sender_with_tokens.solana_account] ) check_transaction_logs_have_text(resp, "exit_status=0x11") - balance_after = evm_loader.get_balance(recipient.public_key, commitment=Confirmed).value + balance_after = evm_loader.get_balance(recipient.pubkey(), commitment=Confirmed).value assert balance_after == balance_before + amount def test_transfer_sol_without_cpi(self, solana_caller, sender_with_tokens, evm_loader): @@ -217,26 +218,26 @@ def test_transfer_sol_without_cpi(self, solana_caller, sender_with_tokens, evm_l ) recipient = evm_loader.create_account(sender_with_tokens.solana_account, 0, TRANSFER_SOL_ID) - instruction = TransactionInstruction( + instruction = Instruction( program_id=TRANSFER_SOL_ID, - keys=[ - AccountMeta(sender.public_key, is_signer=True, is_writable=True), - AccountMeta(recipient.public_key, is_signer=False, is_writable=True), + accounts=[ + AccountMeta(sender.pubkey(), is_signer=True, is_writable=True), + AccountMeta(recipient.pubkey(), is_signer=False, is_writable=True), ], data=bytes([0x1]) + amount.to_bytes(8, "little"), ) call_params = [(TRANSFER_SOL_ID, 0, instruction)] - balance_before = evm_loader.get_balance(recipient.public_key, Confirmed).value + balance_before = evm_loader.get_balance(recipient.pubkey(), Confirmed).value resp = solana_caller.batch_execute(call_params, sender_with_tokens, additional_signers=[sender]) - balance_after = evm_loader.get_balance(recipient.public_key, Confirmed).value + balance_after = evm_loader.get_balance(recipient.pubkey(), Confirmed).value check_transaction_logs_have_text(resp, "exit_status=0x11") assert balance_after == balance_before + amount def test_transfer_with_PDA_signature(self, solana_caller, sender_with_tokens, evm_loader): - from_wallet = Keypair.generate() - to_wallet = Keypair.generate() + from_wallet = Keypair() + to_wallet = Keypair() amount = 100000 - evm_loader.request_airdrop(from_wallet.public_key, 1000 * 10**9, commitment=Confirmed) + evm_loader.request_airdrop(from_wallet.pubkey(), 1000 * 10**9, commitment=Confirmed) mint, from_token_account, to_token_account = _create_mint_and_accounts( evm_loader, from_wallet, to_wallet, amount @@ -251,14 +252,14 @@ def test_transfer_with_PDA_signature(self, solana_caller, sender_with_tokens, ev opts=TxOpts(skip_confirmation=False, skip_preflight=True), ) - instruction = TransactionInstruction( + instruction = Instruction( program_id=TRANSFER_TOKENS_ID, - keys=[ + accounts=[ AccountMeta(from_token_account, is_signer=False, is_writable=True), AccountMeta(mint.pubkey, is_signer=False, is_writable=True), AccountMeta(to_token_account, is_signer=False, is_writable=True), AccountMeta(authority_pubkey, is_signer=False, is_writable=True), - AccountMeta(PublicKey(TOKEN_PROGRAM_ID), is_signer=False, is_writable=True), + AccountMeta(TOKEN_PROGRAM_ID, is_signer=False, is_writable=True), ], data=bytes([0x0]), ) @@ -269,7 +270,7 @@ def test_transfer_with_PDA_signature(self, solana_caller, sender_with_tokens, ev def test_transfer_tokens_with_ext_authority(self, evm_loader, sender_with_tokens, solana_caller): from_wallet = sender_with_tokens - to_wallet = Keypair.generate() + to_wallet = Keypair() amount = 100000 mint, from_token_account, to_token_account = _create_mint_and_accounts( evm_loader, from_wallet.solana_account, to_wallet, amount @@ -296,7 +297,7 @@ def test_transfer_tokens_with_ext_authority(self, evm_loader, sender_with_tokens def test_transfer_tokens_with_unauthorized_signer(self, solana_caller, sender_with_tokens, evm_loader): from_wallet = sender_with_tokens - to_wallet = Keypair.generate() + to_wallet = Keypair() amount = 100000 mint, from_token_account, to_token_account = _create_mint_and_accounts( evm_loader, from_wallet.solana_account, to_wallet, amount @@ -333,9 +334,9 @@ def test_staticcall_does_not_support_external_call( resource_addr = solana_caller.create_resource(sender_with_tokens, b"123", 8, 1000000000, COUNTER_ID) - instruction = TransactionInstruction( + instruction = Instruction( program_id=COUNTER_ID, - keys=[ + accounts=[ AccountMeta(resource_addr, is_signer=False, is_writable=True), ], data=bytes([0x1]), @@ -387,18 +388,18 @@ def test_call_neon_instruction_by_neon_instruction( treasury_pool, new_holder_acc, ): - key = Keypair.generate() - caller_ether = eth_keys.PrivateKey(key.secret_key[:32]).public_key.to_canonical_address() + key = Keypair() + caller_ether = eth_keys.PrivateKey(key.secret()[:32]).public_key.to_canonical_address() account_pubkey = evm_loader.ether2balance(caller_ether) - contract_pubkey = PublicKey(evm_loader.ether2program(caller_ether)[0]) + contract_pubkey = Pubkey.from_string(evm_loader.ether2program(caller_ether)[0]) data = bytes([0x30]) + evm_loader.ether2bytes(caller_ether) + CHAIN_ID.to_bytes(8, "little") - neon_instruction = TransactionInstruction( + neon_instruction = Instruction( program_id=evm_loader.loader_id, data=data, - keys=[ - AccountMeta(pubkey=sender_with_tokens.solana_account.public_key, is_signer=True, is_writable=True), + accounts=[ + AccountMeta(pubkey=sender_with_tokens.solana_account.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), AccountMeta(pubkey=account_pubkey, is_signer=False, is_writable=True), AccountMeta(pubkey=contract_pubkey, is_signer=False, is_writable=True), diff --git a/integration/tests/neon_evm/test_neon_core_rest_api.py b/integration/tests/neon_evm/test_neon_core_rest_api.py index 870b59f8e2..ceaaa90667 100644 --- a/integration/tests/neon_evm/test_neon_core_rest_api.py +++ b/integration/tests/neon_evm/test_neon_core_rest_api.py @@ -17,7 +17,7 @@ def test_get_storage_at(neon_api_client, operator_keypair, user_account, evm_loa def test_get_balance(neon_api_client, user_account, evm_loader): result = neon_api_client.get_balance(user_account.eth_address.hex())['value'] assert str(user_account.balance_account_address) == result[0]["solana_address"] - assert evm_loader.get_account_info(user_account.solana_account.public_key).value is not None + assert evm_loader.get_account_info(user_account.solana_account.pubkey()).value is not None def test_emulate_transfer(neon_api_client, user_account, session_user): @@ -55,5 +55,4 @@ def test_emulate_with_small_amount_of_steps(neon_api_client, evm_loader, user_ac contract_code = get_contract_bin("hello_world") result = neon_api_client.emulate(user_account.eth_address.hex(), contract=None, data=contract_code, max_steps_to_execute=10) - assert result['exit_status'] == 'revert', f"The 'exit_status' field is not revert. Result: {result}" diff --git a/integration/tests/neon_evm/test_revision.py b/integration/tests/neon_evm/test_revision.py index aec461994f..f40294b99f 100644 --- a/integration/tests/neon_evm/test_revision.py +++ b/integration/tests/neon_evm/test_revision.py @@ -1,6 +1,6 @@ import pytest -import solana -from solana.publickey import PublicKey +from solana.rpc.core import RPCException as SolanaRPCException +from solders.pubkey import Pubkey from utils.evm_loader import EVM_STEPS from utils.layouts import FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT @@ -42,7 +42,7 @@ def test_call_contract_with_changing_data( "update_storage_map(uint256)", [data_storage_acc_count], ) - acc_from_emulation = [PublicKey(item["pubkey"]) for item in emulate_result["solana_accounts"]] + acc_from_emulation = [Pubkey.from_string(item["pubkey"]) for item in emulate_result["solana_accounts"]] for i in range(trx_count): signed_tx = make_contract_call_trx( @@ -104,7 +104,7 @@ def send_transaction_steps(holder_account, accounts): "update_storage_map(uint256)", [data_storage_acc_count], ) - acc_from_emulation1 = [PublicKey(item["pubkey"]) for item in emulate_result1["solana_accounts"]] + acc_from_emulation1 = [Pubkey.from_string(item["pubkey"]) for item in emulate_result1["solana_accounts"]] data_accounts1 = list( set(acc_from_emulation1) - {user1.balance_account_address, rw_lock_contract.solana_address} ) @@ -118,7 +118,7 @@ def send_transaction_steps(holder_account, accounts): "update_storage_map(uint256)", [data_storage_acc_count], ) - acc_from_emulation2 = [PublicKey(item["pubkey"]) for item in emulate_result2["solana_accounts"]] + acc_from_emulation2 = [Pubkey.from_string(item["pubkey"]) for item in emulate_result2["solana_accounts"]] data_accounts2 = list( set(acc_from_emulation2) - {user2.balance_account_address, rw_lock_contract.solana_address} ) @@ -180,7 +180,7 @@ def send_transaction_steps(holder_account, accounts): user1.eth_address.hex(), rw_lock_contract.eth_address.hex(), "update_storage_str(string)", [text1] ) - acc_from_emulation1 = [PublicKey(item["pubkey"]) for item in emulate_result1["solana_accounts"]] + acc_from_emulation1 = [Pubkey.from_string(item["pubkey"]) for item in emulate_result1["solana_accounts"]] signed_tx1 = make_contract_call_trx(evm_loader, user1, rw_lock_contract, "update_storage_str(string)", [text1]) evm_loader.write_transaction_to_holder_account(signed_tx1, holder1, operator_keypair) @@ -188,7 +188,7 @@ def send_transaction_steps(holder_account, accounts): emulate_result2 = neon_api_client.emulate_contract_call( user2.eth_address.hex(), rw_lock_contract.eth_address.hex(), "update_storage_str(string)", [text2] ) - acc_from_emulation2 = [PublicKey(item["pubkey"]) for item in emulate_result2["solana_accounts"]] + acc_from_emulation2 = [Pubkey.from_string(item["pubkey"]) for item in emulate_result2["solana_accounts"]] signed_tx2 = make_contract_call_trx(evm_loader, user2, rw_lock_contract, "update_storage_str(string)", [text2]) evm_loader.write_transaction_to_holder_account(signed_tx2, holder2, operator_keypair) @@ -321,7 +321,7 @@ def test_1_user_2_parallel_trx_with_data_change( emulate_result = neon_api_client.emulate_contract_call( session_user.eth_address.hex(), rw_lock_contract.eth_address.hex(), "update_storage_map(uint256)", [3] ) - acc_from_emulation = [PublicKey(item["pubkey"]) for item in emulate_result["solana_accounts"]] + acc_from_emulation = [Pubkey.from_string(item["pubkey"]) for item in emulate_result["solana_accounts"]] data_accounts = set(acc_from_emulation) - set(additional_accounts) signed_tx1 = make_contract_call_trx( evm_loader, session_user, rw_lock_contract, "update_storage_map(uint256)", [3] @@ -511,7 +511,7 @@ def test_insufficient_balance_for_2_parallel_trx( operator_keypair, holder_acc, treasury_pool.account, treasury_pool.buffer, signed_tx2, accounts ) check_transaction_logs_have_text(resp, "exit_status=0x11") - with pytest.raises(solana.rpc.core.RPCException, match=ErrorMessage.INSUFFICIENT_BALANCE.value): + with pytest.raises(SolanaRPCException, match=ErrorMessage.INSUFFICIENT_BALANCE.value): evm_loader.send_transaction_step_from_account( operator_keypair, operator_balance_pubkey, diff --git a/integration/tests/neon_evm/test_transaction_step_from_account.py b/integration/tests/neon_evm/test_transaction_step_from_account.py index 60aa85edf5..7901ba2386 100644 --- a/integration/tests/neon_evm/test_transaction_step_from_account.py +++ b/integration/tests/neon_evm/test_transaction_step_from_account.py @@ -3,12 +3,11 @@ import eth_abi import pytest -import solana from eth_keys import keys as eth_keys from eth_utils import abi, to_text, to_int -from solana.keypair import Keypair -from solana.publickey import PublicKey -from solana.rpc.commitment import Processed +from solana.rpc.core import RPCException as SolanaRPCException +from solders.keypair import Keypair +from solders.pubkey import Pubkey from utils.evm_loader import EVM_STEPS from utils.helpers import gen_hash_of_block from utils.instructions import TransactionWithComputeBudget, make_ExecuteTrxFromAccountDataIterativeOrContinue @@ -171,8 +170,8 @@ def test_transfer_transaction_with_non_existing_recipient( self, operator_keypair, holder_acc, treasury_pool, sender_with_tokens, evm_loader ): # recipient account should be created - recipient = Keypair.generate() - recipient_ether = eth_keys.PrivateKey(recipient.secret_key[:32]).public_key.to_canonical_address() + recipient = Keypair() + recipient_ether = eth_keys.PrivateKey(recipient.secret()[:32]).public_key.to_canonical_address() recipient_solana_address, _ = evm_loader.ether2program(recipient_ether) recipient_balance_address = evm_loader.ether2balance(recipient_ether) amount = 10 @@ -184,7 +183,7 @@ def test_transfer_transaction_with_non_existing_recipient( treasury_pool, holder_acc, [ - PublicKey(recipient_solana_address), + Pubkey.from_string(recipient_solana_address), recipient_balance_address, sender_with_tokens.solana_account_address, sender_with_tokens.balance_account_address, @@ -202,7 +201,7 @@ def test_incorrect_chain_id( signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1, chain_id=1) evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INVALID_CHAIN_ID): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.INVALID_CHAIN_ID): evm_loader.execute_transaction_steps_from_account( operator_keypair, treasury_pool, @@ -234,7 +233,7 @@ def test_incorrect_nonce( ) evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INVALID_NONCE): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.INVALID_NONCE): evm_loader.execute_transaction_steps_from_account( operator_keypair, treasury_pool, @@ -264,7 +263,7 @@ def test_run_finalized_transaction( sender_with_tokens.balance_account_address, ], ) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.TRX_ALREADY_FINALIZED): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.TRX_ALREADY_FINALIZED): evm_loader.execute_transaction_steps_from_account( operator_keypair, treasury_pool, @@ -287,7 +286,7 @@ def test_insufficient_funds( ) evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INSUFFICIENT_FUNDS): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.INSUFFICIENT_FUNDS): evm_loader.execute_transaction_steps_from_account( operator_keypair, treasury_pool, @@ -306,7 +305,7 @@ def test_gas_limit_reached( signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 10, gas=1) evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.OUT_OF_GAS): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.OUT_OF_GAS): evm_loader.execute_transaction_steps_from_account( operator_keypair, treasury_pool, @@ -325,7 +324,7 @@ def test_sender_missed_in_remaining_accounts( signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): evm_loader.execute_transaction_steps_from_account( operator_keypair, treasury_pool, @@ -339,7 +338,7 @@ def test_recipient_missed_in_remaining_accounts( signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): evm_loader.execute_transaction_steps_from_account( operator_keypair, treasury_pool, @@ -351,10 +350,10 @@ def test_incorrect_treasure_pool(self, operator_keypair, sender_with_tokens, evm signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) index = 2 - treasury = TreasuryPool(index, Keypair().generate().public_key, index.to_bytes(4, "little")) + treasury = TreasuryPool(index, Keypair().pubkey(), index.to_bytes(4, "little")) error = str.format(InstructionAsserts.INVALID_ACCOUNT, treasury.account) - with pytest.raises(solana.rpc.core.RPCException, match=error): + with pytest.raises(SolanaRPCException, match=error): evm_loader.execute_transaction_steps_from_account(operator_keypair, treasury, holder_acc, []) def test_incorrect_treasure_index(self, operator_keypair, sender_with_tokens, evm_loader, session_user, holder_acc): @@ -367,7 +366,7 @@ def test_incorrect_treasure_index(self, operator_keypair, sender_with_tokens, ev ) error = str.format(InstructionAsserts.INVALID_ACCOUNT, treasury.account) - with pytest.raises(solana.rpc.core.RPCException, match=error): + with pytest.raises(SolanaRPCException, match=error): evm_loader.execute_transaction_steps_from_account(operator_keypair, treasury, holder_acc, []) def test_incorrect_operator_account( @@ -377,7 +376,7 @@ def test_incorrect_operator_account( evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) fake_operator = Keypair() - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ACC_NOT_FOUND): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.ACC_NOT_FOUND): evm_loader.execute_transaction_steps_from_account( fake_operator, treasury_pool, @@ -395,7 +394,7 @@ def test_operator_is_not_in_white_list( ): signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) - with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.NOT_AUTHORIZED_OPERATOR): + with pytest.raises(SolanaRPCException, match=InstructionAsserts.NOT_AUTHORIZED_OPERATOR): evm_loader.execute_transaction_steps_from_account( sender_with_tokens.solana_account, treasury_pool, @@ -413,12 +412,12 @@ def test_incorrect_system_program( self, sender_with_tokens, operator_keypair, evm_loader, treasury_pool, session_user, holder_acc ): signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) - fake_sys_program_id = Keypair().public_key + fake_sys_program_id = Keypair().pubkey() evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) error = str.format(InstructionAsserts.NOT_SYSTEM_PROGRAM, fake_sys_program_id) operator_balance = evm_loader.get_operator_balance_pubkey(operator_keypair) - with pytest.raises(solana.rpc.core.RPCException, match=error): + with pytest.raises(SolanaRPCException, match=error): evm_loader.send_transaction_step_from_account( operator_keypair, operator_balance, @@ -431,10 +430,10 @@ def test_incorrect_system_program( ) def test_incorrect_holder_account(self, operator_keypair, evm_loader, treasury_pool): - fake_holder_acc = Keypair.generate().public_key + fake_holder_acc = Keypair().pubkey() operator_balance = evm_loader.get_operator_balance_pubkey(operator_keypair) error = str.format(InstructionAsserts.NOT_PROGRAM_OWNED, fake_holder_acc) - with pytest.raises(solana.rpc.core.RPCException, match=error): + with pytest.raises(SolanaRPCException, match=error): evm_loader.send_transaction_step_from_account( operator_keypair, operator_balance, treasury_pool, fake_holder_acc, [], 1, operator_keypair ) @@ -624,7 +623,7 @@ def test_contract_call_update_storage_map_function( rw_lock_caller.solana_address, ] for acc in result["solana_accounts"]: - additional_accounts.append(PublicKey(acc["pubkey"])) + additional_accounts.append(Pubkey.from_string(acc["pubkey"])) resp = evm_loader.execute_transaction_steps_from_account( operator_keypair, treasury_pool, holder_acc, additional_accounts @@ -778,32 +777,61 @@ def send_transaction_steps(user, holder_acc, contract): check_holder_account_tag(new_holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) check_holder_account_tag(holder_acc2, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) - def test_changing_order_of_accounts_for_each_iteration(self, rw_lock_contract, user_account, - session_user, evm_loader, operator_keypair, - treasury_pool, holder_acc, neon_api_client): + def test_changing_order_of_accounts_for_each_iteration( + self, + rw_lock_contract, + user_account, + session_user, + evm_loader, + operator_keypair, + treasury_pool, + holder_acc, + neon_api_client, + ): emulate_result = neon_api_client.emulate_contract_call( session_user.eth_address.hex(), rw_lock_contract.eth_address.hex(), "update_storage_map(uint256)", [3] ) - acc_from_emulation = [PublicKey(item["pubkey"]) for item in emulate_result["solana_accounts"]] - signed_tx = make_contract_call_trx(evm_loader, session_user, rw_lock_contract, "update_storage_map(uint256)", [3]) + acc_from_emulation = [Pubkey.from_string(item["pubkey"]) for item in emulate_result["solana_accounts"]] + signed_tx = make_contract_call_trx( + evm_loader, session_user, rw_lock_contract, "update_storage_map(uint256)", [3] + ) evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) operator_balance_account = evm_loader.get_operator_balance_pubkey(operator_keypair) evm_loader.send_transaction_step_from_account( - operator_keypair, operator_balance_account, treasury_pool, holder_acc, acc_from_emulation, EVM_STEPS, operator_keypair + operator_keypair, + operator_balance_account, + treasury_pool, + holder_acc, + acc_from_emulation, + EVM_STEPS, + operator_keypair, ) random.shuffle(acc_from_emulation) evm_loader.send_transaction_step_from_account( - operator_keypair, operator_balance_account, treasury_pool, holder_acc, acc_from_emulation, EVM_STEPS, operator_keypair + operator_keypair, + operator_balance_account, + treasury_pool, + holder_acc, + acc_from_emulation, + EVM_STEPS, + operator_keypair, ) random.shuffle(acc_from_emulation) resp = evm_loader.send_transaction_step_from_account( - operator_keypair, operator_balance_account, treasury_pool, holder_acc, acc_from_emulation, EVM_STEPS, operator_keypair + operator_keypair, + operator_balance_account, + treasury_pool, + holder_acc, + acc_from_emulation, + EVM_STEPS, + operator_keypair, ) check_transaction_logs_have_text(resp, "exit_status=0x11") check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + class TestStepFromAccountChangingOperatorsDuringTrxRun: def test_next_operator_can_continue_trx( self, @@ -887,7 +915,7 @@ def test_next_operator_can_continue_trx_with_created_spl( func_signature, func_args, ) - additional_accounts = [PublicKey(item["pubkey"]) for item in emulate_result["solana_accounts"]] + additional_accounts = [Pubkey.from_string(item["pubkey"]) for item in emulate_result["solana_accounts"]] signed_tx = make_contract_call_trx( evm_loader, sender_with_tokens, erc20_for_spl_proxy_contract, func_signature, func_args ) diff --git a/integration/tests/neon_evm/test_transaction_step_from_instruction.py b/integration/tests/neon_evm/test_transaction_step_from_instruction.py index 63cda1e952..7bdeb4ab72 100644 --- a/integration/tests/neon_evm/test_transaction_step_from_instruction.py +++ b/integration/tests/neon_evm/test_transaction_step_from_instruction.py @@ -9,8 +9,8 @@ from eth_keys import keys as eth_keys from eth_utils import abi, to_text, to_int from hexbytes import HexBytes -from solana.keypair import Keypair -from solana.publickey import PublicKey +from solders.keypair import Keypair +from solders.pubkey import Pubkey from solana.rpc.commitment import Confirmed from solana.rpc.core import RPCException @@ -166,8 +166,8 @@ def test_transfer_transaction_with_non_existing_recipient( self, operator_keypair, treasury_pool, sender_with_tokens, evm_loader, holder_acc ): # recipient account should be created - recipient = Keypair.generate() - recipient_ether = eth_keys.PrivateKey(recipient.secret_key[:32]).public_key.to_canonical_address() + recipient = Keypair() + recipient_ether = eth_keys.PrivateKey(recipient.secret()[:32]).public_key.to_canonical_address() recipient_solana_address, _ = evm_loader.ether2program(recipient_ether) recipient_balance_address = evm_loader.ether2balance(recipient_ether) amount = 10 @@ -179,7 +179,7 @@ def test_transfer_transaction_with_non_existing_recipient( holder_acc, signed_tx, [ - PublicKey(recipient_solana_address), + Pubkey.from_string(recipient_solana_address), recipient_balance_address, sender_with_tokens.solana_account_address, sender_with_tokens.balance_account_address, @@ -345,7 +345,7 @@ def test_recipient_missed_in_remaining_accounts( def test_incorrect_treasure_pool(self, operator_keypair, sender_with_tokens, evm_loader, session_user, holder_acc): signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) index = 2 - treasury = TreasuryPool(index, Keypair().generate().public_key, index.to_bytes(4, "little")) + treasury = TreasuryPool(index, Keypair().pubkey(), index.to_bytes(4, "little")) error = str.format(InstructionAsserts.INVALID_ACCOUNT, treasury.account) with pytest.raises(solana.rpc.core.RPCException, match=error): @@ -384,7 +384,7 @@ def test_incorrect_treasure_index(self, operator_keypair, sender_with_tokens, ev def test_incorrect_operator_account(self, sender_with_tokens, evm_loader, treasury_pool, session_user, holder_acc): signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) - fake_operator = Keypair().generate() + fake_operator = Keypair() with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ACC_NOT_FOUND): evm_loader.execute_transaction_steps_from_instruction( fake_operator, @@ -421,7 +421,7 @@ def test_incorrect_system_program( self, sender_with_tokens, operator_keypair, evm_loader, treasury_pool, session_user, holder_acc ): signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) - fake_sys_program_id = Keypair().generate().public_key + fake_sys_program_id = Keypair().pubkey() operator_balance = evm_loader.get_operator_balance_pubkey(operator_keypair) with pytest.raises( solana.rpc.core.RPCException, match=str.format(InstructionAsserts.NOT_SYSTEM_PROGRAM, fake_sys_program_id) @@ -447,7 +447,7 @@ def test_incorrect_holder_account( self, sender_with_tokens, operator_keypair, evm_loader, treasury_pool, session_user ): signed_tx = make_eth_transaction(evm_loader, session_user.eth_address, None, sender_with_tokens, 1) - fake_holder_acc = Keypair.generate().public_key + fake_holder_acc = Keypair().pubkey() operator_balance = evm_loader.get_operator_balance_pubkey(operator_keypair) with pytest.raises( solana.rpc.core.RPCException, match=str.format(InstructionAsserts.NOT_PROGRAM_OWNED, fake_holder_acc) @@ -638,7 +638,7 @@ def test_contract_call_update_storage_map_function( rw_lock_caller.solana_address, ] for acc in result["solana_accounts"]: - additional_accounts.append(PublicKey(acc["pubkey"])) + additional_accounts.append(Pubkey.from_string(acc["pubkey"])) resp = evm_loader.execute_transaction_steps_from_instruction( operator_keypair, treasury_pool, holder_acc, signed_tx, additional_accounts diff --git a/integration/tests/neon_evm/test_transactions_eip1559.py b/integration/tests/neon_evm/test_transactions_eip1559.py new file mode 100644 index 0000000000..dc23152d93 --- /dev/null +++ b/integration/tests/neon_evm/test_transactions_eip1559.py @@ -0,0 +1,160 @@ +import pytest +import solana + +from integration.tests.neon_evm.utils.constants import TAG_FINALIZED_STATE +from integration.tests.neon_evm.utils.contract import make_contract_call_trx, make_deployment_transaction +from integration.tests.neon_evm.utils.ethereum import create_contract_address, make_eth_transaction +from integration.tests.neon_evm.utils.transaction_checks import ( + check_holder_account_tag, + check_transaction_logs_have_text, +) +from utils.layouts import FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT +from utils.types import Caller + + +class TestEIP1559Transactions: + def test_contract_interaction_iterative_transactions( + self, + operator_keypair, + holder_acc, + treasury_pool, + sender_with_tokens, + evm_loader, + calculator_contract, + calculator_caller_contract, + ): + signed_tx = make_contract_call_trx( + evm_loader, + sender_with_tokens, + calculator_caller_contract, + "callCalculator()", + max_fee_per_gas=10000, + max_priority_fee_per_gas=10, + trx_type=2, + ) + evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + resp = evm_loader.execute_transaction_steps_from_account( + operator_keypair, + treasury_pool, + holder_acc, + [ + calculator_caller_contract.solana_address, + calculator_contract.solana_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address, + ], + compute_unit_price=3929, + ) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp, "exit_status=0x12") + + def test_contract_deploy_iterative_transaction( + self, operator_keypair, holder_acc, treasury_pool, sender_with_tokens, evm_loader + ): + contract_filename = "hello_world" + contract = create_contract_address(sender_with_tokens, evm_loader) + + signed_tx = make_deployment_transaction( + evm_loader, sender_with_tokens, contract_filename, max_fee_per_gas=10000, max_priority_fee_per_gas=10 + ) + + evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + resp = evm_loader.execute_transaction_steps_from_account( + operator_keypair, + treasury_pool, + holder_acc, + [ + contract.solana_address, + contract.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address, + ], + compute_unit_price=5000, + ) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp, "exit_status=0x12") + + def test_max_fee_less_then_max_priority_fee( + self, + operator_keypair, + holder_acc, + treasury_pool, + sender_with_tokens, + evm_loader, + calculator_contract, + calculator_caller_contract, + ): + signed_tx = make_contract_call_trx( + evm_loader, + sender_with_tokens, + calculator_caller_contract, + "callCalculator()", + max_fee_per_gas=10, + max_priority_fee_per_gas=1000, + trx_type=2, + ) + evm_loader.write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + with pytest.raises(solana.rpc.core.RPCException, match="max_fee_per_gas < max_priority_fee_per_gas"): + evm_loader.execute_transaction_steps_from_account( + operator_keypair, + treasury_pool, + holder_acc, + [ + calculator_caller_contract.solana_address, + calculator_contract.solana_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address, + ], + compute_unit_price=3929, + ) + + def test_simple_transfer_non_iterative_transaction( + self, + operator_keypair, + treasury_pool, + sender_with_tokens: Caller, + session_user: Caller, + evm_loader, + holder_acc + ): + amount = 10 + max_fee_per_gas = 50000 + max_priority_fee_per_gas = 100 + + sender_balance_before = evm_loader.get_neon_balance(sender_with_tokens.eth_address) + recipient_balance_before = evm_loader.get_neon_balance(session_user.eth_address) + + signed_tx = make_eth_transaction( + evm_loader, + session_user.eth_address, + None, + sender_with_tokens, + amount, + max_fee_per_gas=max_fee_per_gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + gas=10000, + ) + + resp = evm_loader.execute_trx_from_instruction( + operator_keypair, + holder_acc, + treasury_pool.account, + treasury_pool.buffer, + signed_tx, + [ + sender_with_tokens.balance_account_address, + session_user.balance_account_address, + session_user.solana_account_address, + ], + compute_unit_price=5000, + ) + + sender_balance_after = evm_loader.get_neon_balance(sender_with_tokens.eth_address) + recipient_balance_after = evm_loader.get_neon_balance(session_user.eth_address) + + additional_fee = max_priority_fee_per_gas * 5000 * 1 + assert sender_balance_before - amount - sender_balance_after > additional_fee + assert recipient_balance_before + amount == recipient_balance_after + check_transaction_logs_have_text(resp, "exit_status=0x11") diff --git a/integration/tests/neon_evm/utils/call_solana.py b/integration/tests/neon_evm/utils/call_solana.py index b4665e8d10..dea120b769 100644 --- a/integration/tests/neon_evm/utils/call_solana.py +++ b/integration/tests/neon_evm/utils/call_solana.py @@ -1,5 +1,6 @@ import eth_abi from eth_utils import keccak +from solders.pubkey import Pubkey from integration.tests.neon_evm.utils.constants import NEON_CORE_API_URL from integration.tests.neon_evm.utils.contract import deploy_contract, make_contract_call_trx @@ -42,14 +43,14 @@ def get_solana_address_by_neon_address(self, neon_address): ) return bytes32_to_solana_pubkey(sol_addr) - def get_solana_PDA(self, program_id, seeds): + def get_solana_PDA(self, program_id, seeds) -> Pubkey: args = eth_abi.encode(["bytes32", "bytes"], [bytes(program_id), seeds]) addr = self.neon_api_client.call_contract_get_function( self.owner, self.contract, "getSolanaPDA(bytes32,bytes)", args ) return bytes32_to_solana_pubkey(addr) - def get_eth_ext_authority(self, salt, sender): + def get_eth_ext_authority(self, salt, sender) -> Pubkey: args = eth_abi.encode(["bytes32"], [salt]) addr = self.neon_api_client.call_contract_get_function( sender, self.contract, "getExtAuthority(bytes32)", args @@ -181,5 +182,5 @@ def create_resource(self, sender, salt, space, lamports, owner): def _get_all_pubkeys_from_instructions(instructions): all_keys = [] for item in instructions: - all_keys += item.keys + all_keys += item.accounts return [acc.pubkey for acc in all_keys] diff --git a/integration/tests/neon_evm/utils/constants.py b/integration/tests/neon_evm/utils/constants.py index f7537fc632..0bfff01a61 100644 --- a/integration/tests/neon_evm/utils/constants.py +++ b/integration/tests/neon_evm/utils/constants.py @@ -1,10 +1,10 @@ import os -from solana.publickey import PublicKey +from solders.pubkey import Pubkey TREASURY_POOL_SEED = os.environ.get("NEON_TREASURY_POOL_SEED", "treasury_pool") TREASURY_POOL_COUNT = os.environ.get("NEON_TREASURY_POOL_COUNT", 128) -ACCOUNT_SEED_VERSION = b'\3' +ACCOUNT_SEED_VERSION = b"\3" TAG_EMPTY = 0 @@ -18,5 +18,7 @@ NEON_CORE_API_RPC_URL = os.environ.get("NEON_CORE_API_RPC_URL", "http://neon_core_rpc:3100") EVM_LOADER = os.environ.get("EVM_LOADER", "53DfF883gyixYNXnM7s5xhdeyV8mVk9T4i2hGV9vG9io") -NEON_TOKEN_MINT_ID: PublicKey = PublicKey(os.environ.get("NEON_TOKEN_MINT") or "HPsV9Deocecw3GeZv1FkAPNCBRfuVyfw9MMwjwRe1xaU") +NEON_TOKEN_MINT_ID: Pubkey = Pubkey.from_string( + os.environ.get("NEON_TOKEN_MINT") or "HPsV9Deocecw3GeZv1FkAPNCBRfuVyfw9MMwjwRe1xaU" +) CHAIN_ID = int(os.environ.get("NEON_CHAIN_ID", 111)) diff --git a/integration/tests/neon_evm/utils/contract.py b/integration/tests/neon_evm/utils/contract.py index 0ba7771ad0..07214117d0 100644 --- a/integration/tests/neon_evm/utils/contract.py +++ b/integration/tests/neon_evm/utils/contract.py @@ -5,8 +5,8 @@ import solcx from eth_account.datastructures import SignedTransaction from eth_utils import abi -from solana.keypair import Keypair -from solana.publickey import PublicKey +from solders.keypair import Keypair +from solders.pubkey import Pubkey from utils.evm_loader import EvmLoader from utils.types import Caller, TreasuryPool, Contract @@ -68,6 +68,8 @@ def make_deployment_transaction( gas: int = 999999999, chain_id=111, access_list=None, + max_priority_fee_per_gas=None, + max_fee_per_gas=None, version: str = "0.7.6", ) -> SignedTransaction: data = get_contract_bin(contract_file_name, contract_name, version) @@ -83,12 +85,27 @@ def make_deployment_transaction( tx["type"] = 1 if value: tx["value"] = value + if max_priority_fee_per_gas: + tx["maxPriorityFeePerGas"] = max_priority_fee_per_gas + if max_fee_per_gas: + tx["maxFeePerGas"] = max_fee_per_gas + tx.pop("gasPrice") - return w3.eth.account.sign_transaction(tx, user.solana_account.secret_key[:32]) + return w3.eth.account.sign_transaction(tx, user.solana_account.secret()[:32]) def make_contract_call_trx( - evm_loader, user, contract, function_signature, params=None, value=0, chain_id=111, access_list=None, trx_type=None + evm_loader, + user, + contract, + function_signature, + params=None, + value=0, + chain_id=111, + access_list=None, + max_priority_fee_per_gas=None, + max_fee_per_gas=None, + trx_type=None, ): # does not work for tuple in params data = abi.function_signature_to_4byte_selector(function_signature) @@ -109,7 +126,9 @@ def make_contract_call_trx( value=value, chain_id=chain_id, access_list=access_list, - type=trx_type, + max_priority_fee_per_gas=max_priority_fee_per_gas, + max_fee_per_gas=max_fee_per_gas, + type_=trx_type, ) return signed_tx @@ -134,7 +153,7 @@ def deploy_contract( emulate_result = neon_api_client.emulate( user.eth_address.hex(), contract=None, data=contract_code + encoded_args.hex() ) - additional_accounts = [PublicKey(item["pubkey"]) for item in emulate_result["solana_accounts"]] + additional_accounts = [Pubkey.from_string(item["pubkey"]) for item in emulate_result["solana_accounts"]] contract: Contract = create_contract_address(user, evm_loader) holder_acc = create_holder(operator, evm_loader) diff --git a/integration/tests/neon_evm/utils/ethereum.py b/integration/tests/neon_evm/utils/ethereum.py index e57970b9c0..3ced3515d1 100644 --- a/integration/tests/neon_evm/utils/ethereum.py +++ b/integration/tests/neon_evm/utils/ethereum.py @@ -1,7 +1,7 @@ from typing import Union from Crypto.Hash import keccak -from solana.publickey import PublicKey +from solders.pubkey import Pubkey from web3.auto import w3 from utils.evm_loader import CHAIN_ID, EvmLoader @@ -12,33 +12,50 @@ def create_contract_address(user: Caller, evm_loader: EvmLoader) -> Contract: # Create contract address from (caller_address, nonce) user_nonce = evm_loader.get_neon_nonce(user.eth_address) - contract_eth_address = keccak.new(digest_bits=256).update(pack([user.eth_address, - user_nonce or None])).digest()[-20:] + contract_eth_address = ( + keccak.new(digest_bits=256).update(pack([user.eth_address, user_nonce or None])).digest()[-20:] + ) contract_solana_address, _ = evm_loader.ether2program(contract_eth_address) contract_neon_address = evm_loader.ether2balance(contract_eth_address) - print(f"Contract addresses: " - f" eth {contract_eth_address.hex()}, " - f" solana {contract_solana_address}") + print(f"Contract addresses: " f" eth {contract_eth_address.hex()}, " f" solana {contract_solana_address}") - return Contract(contract_eth_address, PublicKey(contract_solana_address), contract_neon_address) + return Contract(contract_eth_address, Pubkey.from_string(contract_solana_address), contract_neon_address) -def make_eth_transaction(evm_loader: EvmLoader, to_addr: bytes, data: Union[bytes, None], caller: Caller, - value: int = 0, chain_id=CHAIN_ID, gas=9999999999, access_list=None, type=None): +def make_eth_transaction( + evm_loader: EvmLoader, + to_addr: bytes, + data: Union[bytes, None], + caller: Caller, + value: int = 0, + chain_id=CHAIN_ID, + gas=9999999999, + max_priority_fee_per_gas=None, + max_fee_per_gas=None, + access_list=None, + type_=None, +): nonce = evm_loader.get_neon_nonce(caller.eth_address) - tx = {'to': to_addr, 'value': value, 'gas': gas, 'gasPrice': 0, - 'nonce': nonce} + tx = {"to": to_addr, "value": value, "gas": gas, "gasPrice": 0, "nonce": nonce} if chain_id is not None: - tx['chainId'] = chain_id + tx["chainId"] = chain_id if data is not None: - tx['data'] = data + tx["data"] = data if access_list is not None: - tx['accessList'] = access_list - if type is not None: - tx['type'] = type - return w3.eth.account.sign_transaction(tx, caller.solana_account.secret_key[:32]) + tx["accessList"] = access_list + + if max_priority_fee_per_gas is not None: + tx["maxPriorityFeePerGas"] = max_priority_fee_per_gas + + if max_fee_per_gas is not None: + tx["maxFeePerGas"] = max_fee_per_gas + tx.pop("gasPrice") + + if type_ is not None: + tx["type"] = type_ + return w3.eth.account.sign_transaction(tx, caller.solana_account.secret()[:32]) diff --git a/integration/tests/neon_evm/utils/neon_api_client.py b/integration/tests/neon_evm/utils/neon_api_client.py index d0b9a07ad0..b98b845c5e 100644 --- a/integration/tests/neon_evm/utils/neon_api_client.py +++ b/integration/tests/neon_evm/utils/neon_api_client.py @@ -23,7 +23,8 @@ def emulate(self, sender, contract, data=bytes(), chain_id=CHAIN_ID, value='0x0' "chain_id": chain_id, "value": value }, - "accounts": [] + "accounts": [], + "provide_account_info": None } resp = requests.post(url=f"{self.url}/emulate", json=body, headers=self.headers) if resp.status_code == 200: diff --git a/integration/tests/neon_evm/utils/storage.py b/integration/tests/neon_evm/utils/storage.py index b0294e4860..8b8ef5c527 100644 --- a/integration/tests/neon_evm/utils/storage.py +++ b/integration/tests/neon_evm/utils/storage.py @@ -1,48 +1,50 @@ from hashlib import sha256 from random import randrange -from solana.publickey import PublicKey -from solana.keypair import Keypair +from solders.pubkey import Pubkey +from solders.keypair import Keypair -from solana.transaction import Transaction, TransactionInstruction, AccountMeta +from solana.transaction import Transaction, Instruction, AccountMeta from utils.evm_loader import EvmLoader from utils.instructions import make_CreateAccountWithSeed, make_CreateHolderAccount -def create_holder(signer: Keypair, evm_loader:EvmLoader, seed: str = None, size: int = None, fund: int = None, - storage: PublicKey = None, ) -> PublicKey: +def create_holder(signer: Keypair, evm_loader: EvmLoader, seed: str = None, size: int = None, fund: int = None, + storage: Pubkey = None, ) -> Pubkey: if size is None: size = 128 * 1024 if fund is None: fund = 10 ** 9 if seed is None: - seed = str(randrange(1000000)) + seed = str(randrange(100000000000)) if storage is None: - storage = PublicKey( - sha256(bytes(signer.public_key) + bytes(seed, 'utf8') + bytes(evm_loader.loader_id)).digest()) + storage = Pubkey.from_bytes( + sha256(bytes(signer.pubkey()) + bytes(seed, 'utf8') + bytes(evm_loader.loader_id)).digest() + ) print(f"Create holder account with seed: {seed}") if evm_loader.get_solana_balance(storage) == 0: trx = Transaction() trx.add( - make_CreateAccountWithSeed(signer.public_key, signer.public_key, seed, fund, size, evm_loader.loader_id), - make_CreateHolderAccount(storage, signer.public_key, bytes(seed, 'utf8'), evm_loader.loader_id) + make_CreateAccountWithSeed(signer.pubkey(), signer.pubkey(), seed, fund, size, evm_loader.loader_id), + make_CreateHolderAccount(storage, signer.pubkey(), bytes(seed, 'utf8'), evm_loader.loader_id) ) evm_loader.send_tx(trx, signer) - print(f"Created holder account: {storage}") - return storage + return storage + else: + create_holder(signer, evm_loader, seed, size, fund, storage) -def delete_holder(del_key: PublicKey, acc: Keypair, signer: Keypair, evm_loader: EvmLoader): +def delete_holder(del_key: Pubkey, acc: Keypair, signer: Keypair, evm_loader: EvmLoader): trx = Transaction() - trx.add(TransactionInstruction( + trx.add(Instruction( program_id=evm_loader.loader_id, data=bytes.fromhex("25"), - keys=[ + accounts=[ AccountMeta(pubkey=del_key, is_signer=False, is_writable=True), - AccountMeta(pubkey=acc.public_key, is_signer=(signer == acc), is_writable=True), + AccountMeta(pubkey=acc.pubkey(), is_signer=(signer == acc), is_writable=True), ])) return evm_loader.send_tx(trx, signer) diff --git a/integration/tests/tracer/conftest.py b/integration/tests/tracer/conftest.py index 2d6ba11045..2d7681db99 100644 --- a/integration/tests/tracer/conftest.py +++ b/integration/tests/tracer/conftest.py @@ -4,6 +4,7 @@ from _pytest.config import Config from utils.tracer_client import TracerClient +from utils.storage_contract import StorageContract @pytest.fixture(scope="session") @@ -16,3 +17,8 @@ def tracer_api(tracer_json_rpc_client_session, request): if inspect.isclass(request.cls): request.cls.tracer_api = tracer_json_rpc_client_session yield tracer_json_rpc_client_session + + +@pytest.fixture(scope="class") +def storage_object(web3_client, storage_contract): + return StorageContract(web3_client, storage_contract) \ No newline at end of file diff --git a/integration/tests/tracer/test_override_params.py b/integration/tests/tracer/test_override_params.py new file mode 100644 index 0000000000..75265afb95 --- /dev/null +++ b/integration/tests/tracer/test_override_params.py @@ -0,0 +1,927 @@ +import time +import allure +import pytest +import random + +from deepdiff import DeepDiff +from utils.web3client import NeonChainWeb3Client +from utils.accounts import EthAccounts +from utils.tracer_client import TracerClient +from utils.helpers import padhex + + +# CODE_OVERRIDED is a bytecode of the storage_contract with retrieve() function which returns (number + 1) instead of number +CODE_OVERRIDED = "0x608060405234801561001057600080fd5b506004361061004c5760003560e01c80632e64cec1146100515780635e383d211461006c5780636057361d1461007f578063dce4a44714610094575b600080fd5b6100596100b4565b6040519081526020015b60405180910390f35b61005961007a36600461019b565b6100c8565b61009261008d36600461019b565b6100e9565b005b6100a76100a23660046101b4565b61010d565b60405161006391906101e4565b600080546100c3906001610239565b905090565b600181815481106100d857600080fd5b600091825260209091200154905081565b60008190556040805160208101909152818152610109906001908161013b565b5050565b6060816001600160a01b0316803b806020016040519081016040528181526000908060200190933c92915050565b828054828255906000526020600020908101928215610176579160200282015b8281111561017657825182559160200191906001019061015b565b50610182929150610186565b5090565b5b808211156101825760008155600101610187565b6000602082840312156101ad57600080fd5b5035919050565b6000602082840312156101c657600080fd5b81356001600160a01b03811681146101dd57600080fd5b9392505050565b600060208083528351808285015260005b81811015610211578581018301518582016040015282016101f5565b81811115610223576000604083870101525b50601f01601f1916929092016040019392505050565b6000821982111561025a57634e487b7160e01b600052601160045260246000fd5b50019056fea264697066735822122027ccfc0daba8d2d69d8a56122f60c379952cad9600de2be04409fc7cb4c51c5c64736f6c63430008080033" + +index_0 = padhex(hex(0), 64) +index_1 = padhex(hex(1), 64) +index_2 = padhex(hex(2), 64) +index_5 = padhex(hex(5), 64) + + +@allure.feature("Tracer API") +@allure.story("Tracer API RPC calls debug methods with stateOverrides and/or blockOverrides params check") +@pytest.mark.skip(reason="Feature is not implemented yet in tracer/neon-api") +@pytest.mark.usefixtures("accounts", "web3_client", "tracer_api") +class TestTracerOverrideParams: + web3_client: NeonChainWeb3Client + accounts: EthAccounts + tracer_api: TracerClient + storage_value = random.randint(0, 100) + + @pytest.fixture(scope="class") + def retrieve_block_tx(self, storage_object): + receipt = storage_object.retrieve_block(self.accounts[0]) + return self.web3_client.get_transaction_by_hash(receipt["transactionHash"].hex()) + + @pytest.fixture(scope="class") + def retrieve_block_timestamp_tx(self, storage_object): + receipt = storage_object.retrieve_block_timestamp(self.accounts[0]) + return self.web3_client.get_transaction_by_hash(receipt["transactionHash"].hex()) + + @pytest.fixture(scope="class") + def call_storage_tx(self, storage_object): + _, _, receipt = storage_object.call_storage(self.accounts[0], self.storage_value, "blockNumber") + return self.web3_client.get_transaction_by_hash(receipt["transactionHash"].hex()) + + @pytest.fixture(scope="class") + def call_store_value_tx(self, storage_object): + receipt = storage_object.store_value(self.accounts[0], self.storage_value) + return self.web3_client.get_transaction_by_hash(receipt["transactionHash"].hex()) + + @pytest.fixture(scope="class") + def call_sum_of_values_tx(self, storage_object): + receipt = storage_object.retrieve_sum_of_values(self.accounts[0], self.storage_value, self.storage_value) + return self.web3_client.get_transaction_by_hash(receipt["transactionHash"].hex()) + + def retrieve_block_info_tx(self, storage_object): + receipt = storage_object.retrieve_block_info(self.accounts[0]) + return self.web3_client.get_transaction_by_hash(receipt["transactionHash"].hex()) + + def fill_params_for_storage_contract_trace_call(self, tx, is_prestate=False, is_tx_block=False): + if is_tx_block: + block_number = hex(tx["blockNumber"]) + else: + block_number = hex(tx["blockNumber"] - 1) + + params = [ + { + "to": tx["to"], + "from": tx["from"], + "gas": hex(tx["gas"]), + "gasPrice": hex(tx["gasPrice"]), + "value": hex(tx["value"]), + "data": tx["input"].hex(), + }, + block_number, + ] + + if is_prestate: + params.append({"tracer": "prestateTracer"}) + + return params + + def test_stateOverrides_debug_traceCall_override_nonce(self, call_storage_tx): + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + address_from = call_storage_tx["from"].lower() + params[2]["stateOverrides"] = {address_from: {"nonce": 17}} + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response_overrided["result"][address_from]["nonce"] != response["result"][address_from]["nonce"] + assert response_overrided["result"][address_from]["nonce"] == 17 + + def test_stateOverrides_debug_traceCall_override_nonce_to_lower_value(self, call_storage_tx): + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + nonce = response["result"][call_storage_tx["from"].lower()]["nonce"] + + nonce_overrided = 0 + if nonce - 1 > 0: + nonce_overrided = nonce - 1 + + address_from = call_storage_tx["from"].lower() + params[2]["stateOverrides"] = {address_from: {"nonce": nonce_overrided}} + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response_overrided["result"][address_from]["nonce"] != response["result"][address_from]["nonce"] + assert response_overrided["result"][address_from]["nonce"] == nonce_overrided + + def test_stateOverrides_debug_traceCall_override_nonce_invalid_param(self, call_storage_tx): + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx) + self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + address_from = call_storage_tx["from"].lower() + override_params = {"stateOverrides": {address_from: {"nonce": -1}}, "tracer": "prestateTracer"} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" in response_overrided, "No errors in response" + assert response_overrided["error"]["code"] == -32602, "Invalid error code" + assert response_overrided["error"]["message"] == "Invalid params" + + @pytest.mark.parametrize("nonce", [[17, 9], [14, 14], [1, 5], [0, 0]]) + def test_stateOverrides_debug_traceCall_override_nonce_both_accounts(self, call_storage_tx, nonce): + address_from = call_storage_tx["from"].lower() + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + params[2]["stateOverrides"] = {address_from: {"nonce": nonce[0]}, address_to: {"nonce": nonce[1]}} + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response_overrided["result"][address_to]["nonce"] != response["result"][address_to]["nonce"] + assert response_overrided["result"][address_to]["nonce"] == nonce[1] + assert response_overrided["result"][address_from]["nonce"] != response["result"][address_from]["nonce"] + assert response_overrided["result"][address_from]["nonce"] == nonce[0] + + def test_stateOverrides_debug_traceCall_override_balance_of_contract_account(self, call_storage_tx): + address_from = call_storage_tx["from"].lower() + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + params[2]["stateOverrides"] = {address_to: {"balance": "0x1"}} + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response_overrided["result"][address_from]["balance"] == response["result"][address_from]["balance"] + assert response_overrided["result"][address_to]["balance"] != response["result"][address_to]["balance"] + assert response_overrided["result"][address_to]["balance"] == "0x1" + + def test_stateOverrides_debug_traceCall_override_balance_both_accounts(self, storage_contract): + sender_account = self.accounts[0] + tx_raw = self.web3_client.make_raw_tx(sender_account) + instruction_tx = storage_contract.functions.retrieveSenderBalance().build_transaction(tx_raw) + receipt = self.web3_client.send_transaction(sender_account, instruction_tx) + assert receipt["status"] == 1 + + tx = self.web3_client.get_transaction_by_hash(receipt["transactionHash"].hex()) + address_from = tx["from"].lower() + address_to = tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(tx) + + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + params_prestate = self.fill_params_for_storage_contract_trace_call(tx, is_prestate=True) + response_prestate = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + override_params = {"stateOverrides": {address_from: {"balance": "0xd8d726b7177a80001"}, + address_to: {"balance": "0x1aa535d3d0c"}}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + params_prestate[2]["stateOverrides"] = override_params["stateOverrides"] + response_prestate_overrided = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_prestate_overrided, "Error in response" + assert response_prestate_overrided["result"][address_from]["balance"] != response_prestate["result"][address_from]["balance"] + assert response_prestate_overrided["result"][address_from]["balance"] == "0xd8d726b7177a80001" + assert response_prestate_overrided["result"][address_to]["balance"] != response_prestate["result"][address_to]["balance"] + assert response_prestate_overrided["result"][address_to]["balance"] == "0x1aa535d3d0c" + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] != response_overrided["result"]["returnValue"] + assert response_overrided["result"]["returnValue"] == padhex("0xd8d726b7177a80001", 64)[2:] + + # NDEV-3009 + def test_stateOverrides_debug_traceCall_override_balance_insufficient_for_tx(self, call_storage_tx): + address_from = call_storage_tx["from"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + + self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + params[2]["stateOverrides"] = {address_from: {"balance": "0x1aa535d3d0c"}} + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" in response_overrided, "No errors in response" + assert response_overrided["error"]["code"] == -32603, "Invalid error code" + assert response_overrided["error"]["message"] == "neon_api::trace failed" + + def test_stateOverrides_debug_traceCall_override_balance_invalid_format(self, call_storage_tx): + address_from = call_storage_tx["from"].lower() + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx) + self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + override_params = {"stateOverrides": {address_from: {"balance": "0x25bf6196bd1"}, + address_to: {"balance": 1700000000000000000}}, + "tracer": "prestateTracer"} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" in response_overrided, "No errors in response" + assert response_overrided["error"]["code"] == -32602, "Invalid error code" + assert response_overrided["error"]["message"] == "Invalid params" + + @pytest.mark.parametrize("code", [[CODE_OVERRIDED, CODE_OVERRIDED], ["0x3485", CODE_OVERRIDED]]) + def test_stateOverrides_debug_traceCall_override_code_of_contract_and_external_accounts(self, call_storage_tx, code): + address_from = call_storage_tx["from"].lower() + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx) + + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + params_prestate = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + response_prestate = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + override_params = {"stateOverrides": {address_from: {"code": code[0]}, + address_to: {"code": code[1]}}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + params_prestate[2]["stateOverrides"] = override_params["stateOverrides"] + response_prestate_overrided = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_prestate_overrided, "Error in response" + assert "code" not in response_prestate["result"][address_from], "Code has not to be in response" + assert response_prestate_overrided["result"][address_from]["code"] == code[0] + assert response_prestate_overrided["result"][address_to]["code"] != response_prestate["result"][address_to]["code"] + assert response_prestate_overrided["result"][address_to]["code"] == code[1] + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] == padhex(hex(self.storage_value), 64)[2:] + assert response_overrided["result"]["returnValue"] == padhex(hex(self.storage_value + 1), 64)[2:] + + def test_stateOverrides_debug_traceCall_override_code_of_contract_account_invalid(self, call_storage_tx): + address_from = call_storage_tx["from"].lower() + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx) + + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + params_prestate = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + response_prestate = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + override_params = {"stateOverrides": {address_to: {"code": "0x43"}}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + params_prestate[2]["stateOverrides"] = override_params["stateOverrides"] + response_prestate_overrided = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_prestate_overrided, "Error in response" + assert "code" not in response_prestate["result"][address_from], "Code has not to be in response" + assert "code" not in response_prestate_overrided["result"][address_from], "Code has not to be in response" + assert response_prestate_overrided["result"][address_to]["code"] != response_prestate["result"][address_to]["code"] + assert response_prestate_overrided["result"][address_to]["code"] == "0x43" + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] == padhex(hex(self.storage_value), 64)[2:] + assert response_overrided["result"]["returnValue"] == "" + + def test_stateOverrides_debug_traceCall_override_code_of_contract_account(self, call_storage_tx): + address_from = call_storage_tx["from"].lower() + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx) + + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + params_prestate = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + response_prestate = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + override_params = {"stateOverrides": {address_to: {"code": CODE_OVERRIDED}}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + params_prestate[2]["stateOverrides"] = override_params["stateOverrides"] + response_prestate_overrided = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_prestate_overrided, "Error in response" + assert "code" not in response_prestate["result"][address_from], "Code has not to be in response" + assert "code" not in response_prestate_overrided["result"][address_from], "Code has not to be in response" + assert response_prestate_overrided["result"][address_to]["code"] != response_prestate["result"][address_to]["code"] + assert response_prestate_overrided["result"][address_to]["code"] == CODE_OVERRIDED + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] == padhex(hex(self.storage_value), 64)[2:] + assert response_overrided["result"]["returnValue"] == padhex(hex(self.storage_value + 1), 64)[2:] + + def test_stateOverrides_debug_traceCall_override_code_of_externally_owned_account(self, call_storage_tx): + address_from = call_storage_tx["from"].lower() + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx) + + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + params_prestate = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + response_prestate = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + override_params = {"stateOverrides": {address_from: {"code": "0x92a3"}}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + params_prestate[2]["stateOverrides"] = override_params["stateOverrides"] + response_prestate_overrided = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_prestate_overrided, "Error in response" + assert "code" not in response_prestate["result"][address_from], "Code has not to be in response" + assert response_prestate_overrided["result"][address_from]["code"] == "0x92a3" + assert response_prestate_overrided["result"][address_to]["code"] == response_prestate["result"][address_to]["code"] + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] == response_overrided["result"]["returnValue"] + + @pytest.mark.parametrize("code", ["92a3", "0x92a34"]) + def test_stateOverrides_debug_traceCall_override_code_invalid_format(self, call_storage_tx, code): + address_from = call_storage_tx["from"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx) + self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + override_params = {"stateOverrides": {address_from: {"code": code}}, "tracer": "prestateTracer"} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" in response_overrided, "No errors in response" + assert response_overrided["error"]["code"] == -32602, "Invalid error code" + assert response_overrided["error"]["message"] == "Invalid params" + + def test_stateOverrides_debug_traceCall_override_state(self, storage_object): + _, receipt = storage_object.retrieve_doubled_value(self.accounts[0], self.storage_value) + tx = self.web3_client.get_transaction_by_hash(receipt["transactionHash"].hex()) + address_to = tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(tx, is_tx_block=True) + params_prestate = self.fill_params_for_storage_contract_trace_call(tx, is_prestate=True, is_tx_block=True) + + response_prestate = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_prestate) + + params_prestate[2]["stateOverrides"] = {address_to: {"code": response_prestate["result"][address_to]["code"], + "state": {index_0: padhex(hex(102), 64)}}} + response_overrided_prestate = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + response = self.tracer_api.send_rpc("debug_traceCall", params) + + override_params = {"stateOverrides": params_prestate[2]["stateOverrides"]} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_overrided_prestate, "Error in response" + assert response_prestate["result"][address_to]["storage"][index_0] != \ + response_overrided_prestate["result"][address_to]["storage"][index_0] + assert response_overrided_prestate["result"][address_to]["storage"][index_0] == padhex(hex(102), 64) + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] == padhex(hex(2*self.storage_value), 64)[2:] + assert response_overrided["result"]["returnValue"] == padhex(hex(20), 64)[2:] + + def test_stateOverrides_debug_traceCall_override_state_without_code(self, storage_object): + _, receipt = storage_object.retrieve_doubled_value(self.accounts[0], self.storage_value) + tx = self.web3_client.get_transaction_by_hash(receipt["transactionHash"].hex()) + address_to = tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(tx, is_tx_block=True) + params_prestate = self.fill_params_for_storage_contract_trace_call(tx, is_prestate=True, is_tx_block=True) + + response_prestate = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_prestate) + + params_prestate[2]["stateOverrides"] = {address_to: {"state": {index_0: padhex(hex(102), 64)}}} + response_overrided_prestate = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + response = self.tracer_api.send_rpc("debug_traceCall", params) + + override_params = {"stateOverrides": params_prestate[2]["stateOverrides"]} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_overrided_prestate, "Error in response" + assert "storage" not in response_overrided_prestate["result"][address_to] + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] == padhex(hex(2*self.storage_value), 64)[2:] + assert response_overrided["result"]["returnValue"] == "" + + def test_stateOverrides_debug_traceCall_override_state_non_existent_index(self, call_storage_tx): + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx) + params_prestate = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + + response_prestate = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_prestate) + + params_prestate[2]["stateOverrides"] = {address_to: {"code": response_prestate["result"][address_to]["code"], + "state": {index_5: padhex(hex(self.storage_value + 2), 64)}}} + response_overrided_prestate = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + response = self.tracer_api.send_rpc("debug_traceCall", params) + + override_params = {"stateOverrides": params_prestate[2]["stateOverrides"]} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_overrided_prestate, "Error in response" + assert response_prestate["result"][address_to]["storage"][index_0] == padhex(hex(self.storage_value), 64) + assert response_overrided_prestate["result"][address_to]["storage"][index_0] == index_0 + assert index_5 not in response_overrided_prestate["result"][address_to]["storage"] + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] == padhex(hex(self.storage_value), 64)[2:] + assert response_overrided["result"]["returnValue"] == index_0[2:] + + def test_stateOverrides_debug_traceCall_override_state_invalid_index(self, call_storage_tx): + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + params[2]["stateOverrides"] = {address_to: {"code": response["result"][address_to]["code"], + "state": {"index" : "0x863"}}} + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response, "Error in response" + assert "error" in response_overrided, "No errors in response" + assert response_overrided["error"]["code"] == -32602, "Invalid error code" + assert response_overrided["error"]["message"] == "Invalid params" + + def test_stateOverrides_debug_traceCall_override_stateDiff_one_index(self, call_storage_tx): + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx) + params_prestate = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + + response_prestate = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_prestate) + + params_prestate[2]["stateOverrides"] = {address_to: {"stateDiff": {index_0 : padhex(hex(self.storage_value + 1), 64)}}} + response_overrided_prestate = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + response = self.tracer_api.send_rpc("debug_traceCall", params) + + override_params = {"stateOverrides": params_prestate[2]["stateOverrides"]} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_overrided_prestate, "Error in response" + assert response_overrided_prestate["result"][address_to]["storage"][index_0] == padhex(hex(self.storage_value + 1), 64) + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] == padhex(hex(self.storage_value), 64)[2:] + assert response_overrided["result"]["returnValue"] == padhex(hex(self.storage_value + 1), 64)[2:] + + def test_stateOverrides_debug_traceCall_override_stateDiff_two_indexes(self, call_sum_of_values_tx): + address_to = call_sum_of_values_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_sum_of_values_tx) + params_prestate = self.fill_params_for_storage_contract_trace_call(call_sum_of_values_tx, is_prestate=True) + + response_prestate = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_prestate) + + params_prestate[2]["stateOverrides"] = {address_to: {"stateDiff": {index_0: padhex(hex(101), 64), + index_1: padhex(hex(103), 64)}}} + + response_overrided_prestate = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + response = self.tracer_api.send_rpc("debug_traceCall", params) + + override_params = {"stateOverrides": params_prestate[2]["stateOverrides"]} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_overrided_prestate, "Error in response" + assert response_overrided_prestate["result"][address_to]["storage"][index_0] == padhex(hex(101), 64) + assert response_overrided_prestate["result"][address_to]["storage"][index_1] == padhex(hex(103), 64) + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] == padhex(hex(2*self.storage_value), 64)[2:] + assert response_overrided["result"]["returnValue"] == index_5[2:] + + def test_stateOverrides_debug_traceCall_override_stateDiff_add_non_existent_index_and_existed_one(self, call_sum_of_values_tx): + address_to = call_sum_of_values_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_sum_of_values_tx) + params_prestate = self.fill_params_for_storage_contract_trace_call(call_sum_of_values_tx, is_prestate=True) + + response_prestate = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_prestate) + + params_prestate[2]["stateOverrides"] = {address_to: {"stateDiff": {index_0: padhex(hex(101), 64), + index_5: padhex("0x12", 64)}}} + response_overrided_prestate = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + response = self.tracer_api.send_rpc("debug_traceCall", params) + + override_params = {"stateOverrides": params_prestate[2]["stateOverrides"]} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_overrided_prestate, "Error in response" + assert response_overrided_prestate["result"][address_to]["storage"][index_0] == padhex(hex(101), 64) + assert response_overrided_prestate["result"][address_to]["storage"][index_1] == response_prestate["result"][address_to]["storage"][index_1] + assert index_5 not in response_overrided_prestate["result"][address_to]["storage"] + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] == padhex(hex(2*self.storage_value), 64)[2:] + assert response_overrided["result"]["returnValue"] == padhex(hex(self.storage_value), 64)[2:] + + def test_stateOverrides_debug_traceCall_override_stateDiff_add_non_existent_index(self, call_store_value_tx): + address_to = call_store_value_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_store_value_tx) + params_prestate = self.fill_params_for_storage_contract_trace_call(call_store_value_tx, is_prestate=True) + + response_prestate = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_prestate) + + params_prestate[2]["stateOverrides"] = {address_to: {"stateDiff": {index_5: padhex("0x12", 64)}}} + response_prestate_overrided = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + response = self.tracer_api.send_rpc("debug_traceCall", params) + + override_params = {"stateOverrides": params_prestate[2]["stateOverrides"]} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_prestate_overrided, "Error in response" + assert response_prestate_overrided["result"][address_to]["storage"] == \ + response_prestate_overrided["result"][address_to]["storage"] + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] == response_overrided["result"]["returnValue"] + + def test_stateOverrides_debug_traceCall_override_all_params_without_storage(self, call_storage_tx): + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + params[2]["stateOverrides"] = {address_to: {"code": "0x9029", "nonce": 5, "balance": "0x20"}} + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + #code + assert response_overrided["result"][address_to]["code"] != response["result"][address_to]["code"] + assert response_overrided["result"][address_to]["code"] == "0x9029" + #nonce + assert response_overrided["result"][address_to]["nonce"] != response["result"][address_to]["nonce"] + assert response_overrided["result"][address_to]["nonce"] == 5 + #balance + assert response_overrided["result"][address_to]["balance"] != response["result"][address_to]["balance"] + assert response_overrided["result"][address_to]["balance"] == "0x20" + + def test_stateOverrides_debug_traceCall_override_all_params_with_stateDiff(self, call_storage_tx): + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx) + params_prestate = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + + response_prestate = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_prestate) + + params_prestate[2]["stateOverrides"] = {address_to: {"code": CODE_OVERRIDED, + "nonce": 5, + "balance": "0xd8d726b7177a80000", + "stateDiff": {index_0 : padhex(hex(self.storage_value + 2), 64)}}} + + response_overrided_prestate = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + response = self.tracer_api.send_rpc("debug_traceCall", params) + + override_params = {"stateOverrides": params_prestate[2]["stateOverrides"]} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_overrided_prestate, "Error in response" + #code + assert response_overrided_prestate["result"][address_to]["code"] != response_prestate["result"][address_to]["code"] + assert response_overrided_prestate["result"][address_to]["code"] == CODE_OVERRIDED + #nonce + assert response_overrided_prestate["result"][address_to]["nonce"] != response_prestate["result"][address_to]["nonce"] + assert response_overrided_prestate["result"][address_to]["nonce"] == 5 + #balance + assert response_overrided_prestate["result"][address_to]["balance"] != response_prestate["result"][address_to]["balance"] + assert response_overrided_prestate["result"][address_to]["balance"] == "0xd8d726b7177a80000" + #storage + assert response_prestate["result"][address_to]["storage"][index_0] == padhex(hex(self.storage_value), 64) + assert response_overrided_prestate["result"][address_to]["storage"][index_0] == padhex(hex(self.storage_value + 2), 64) + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] == padhex(hex(self.storage_value), 64)[2:] + assert response_overrided["result"]["returnValue"] == padhex(hex(self.storage_value + 3), 64)[2:] + + def test_stateOverrides_debug_traceCall_override_all_params_with_state(self, call_storage_tx): + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx) + params_prestate = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + + response_prestate = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_prestate) + + params_prestate[2]["stateOverrides"] = {address_to: {"code": CODE_OVERRIDED, + "nonce": 25, + "state": {index_0 : padhex(hex(self.storage_value + 1), 64)}, + "balance": "0xd8d726b7177a80001"}} + + response_overrided_prestate = self.tracer_api.send_rpc("debug_traceCall", params_prestate) + + response = self.tracer_api.send_rpc("debug_traceCall", params) + + override_params = {"stateOverrides": params_prestate[2]["stateOverrides"]} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response_prestate, "Error in response" + assert "error" not in response_overrided_prestate, "Error in response" + #code + assert response_overrided_prestate["result"][address_to]["code"] != response_prestate["result"][address_to]["code"] + assert response_overrided_prestate["result"][address_to]["code"] == CODE_OVERRIDED + #nonce + assert response_overrided_prestate["result"][address_to]["nonce"] != response_prestate["result"][address_to]["nonce"] + assert response_overrided_prestate["result"][address_to]["nonce"] == 25 + #storage + assert response_prestate["result"][address_to]["storage"][index_0] == padhex(hex(self.storage_value), 64) + assert response_overrided_prestate["result"][address_to]["storage"][index_0] == padhex(hex(self.storage_value + 1), 64) + #balance + assert response_overrided_prestate["result"][address_to]["balance"] != response_prestate["result"][address_to]["balance"] + assert response_overrided_prestate["result"][address_to]["balance"] == "0xd8d726b7177a80001" + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert response["result"]["returnValue"] == padhex(hex(self.storage_value), 64)[2:] + assert response_overrided["result"]["returnValue"] == padhex(hex(self.storage_value + 2), 64)[2:] + + def test_stateOverrides_debug_traceCall_do_not_override_storage(self, call_storage_tx): + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + params[2]["stateOverrides"] = {address_to: {"storage": {index_0: padhex("0x58", 64)}}} + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" not in response, "Error in response" + assert "error" not in response_overrided, "Error in response" + assert int(response["result"][address_to]["storage"][index_0], 0) == self.storage_value + assert int(response_overrided["result"][address_to]["storage"][index_0], 0) == self.storage_value + + @pytest.mark.skip("NDEV-3001") + def test_stateOverrides_debug_traceCall_override_with_state_and_stateDiff(self, call_store_value_tx): + address_to = call_store_value_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_store_value_tx) + self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + override_params = {"stateOverrides": {address_to: {"stateDiff": {"0x3" : "0x11"}, "state": {"0x0": "0x15"}}}, + "tracer": "prestateTracer"} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + assert "error" in response_overrided, "State and stateDiff are not allowed to be used together" + + def test_stateOverrides_debug_traceCall_wrong_override_format(self, call_storage_tx): + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx, is_prestate=True) + self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + params[2]["stateOverrides"] = {"nonce": 17} + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" in response_overrided, "No errors in response" + assert response_overrided["error"]["code"] == -32602, "Invalid error code" + assert response_overrided["error"]["message"] == "Invalid params" + + @pytest.mark.skip("NDEV-3003") + def test_stateOverrides_eth_call_override_code(self, call_storage_tx): + address_to = call_storage_tx["to"].lower() + params = self.fill_params_for_storage_contract_trace_call(call_storage_tx) + response = self.tracer_api.send_rpc_and_wait_response("eth_call", params) + + override_params = {address_to: {"code": CODE_OVERRIDED}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("eth_call", params) + + assert response["result"] == padhex(hex(self.storage_value), 64) + assert response_overrided["result"] == padhex(hex(self.storage_value + 1), 64) + + def test_blockOverrides_debug_traceCall_override_block(self, retrieve_block_tx, call_storage_tx): + params = self.fill_params_for_storage_contract_trace_call(retrieve_block_tx, is_tx_block=True) + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + override_params = {"blockOverrides": {"number": call_storage_tx["blockNumber"]}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + diff = DeepDiff(response["result"], response_overrided["result"]) + + diff_storage = {"new_value": padhex(hex(call_storage_tx["blockNumber"]), 64)[2:], + "old_value": padhex(hex(retrieve_block_tx["blockNumber"]), 64)[2:]} + diff_block = {"new_value": hex(call_storage_tx["blockNumber"]), + "old_value": hex(retrieve_block_tx["blockNumber"])} + + for _,v in diff["values_changed"].items(): + assert v == diff_block or v == diff_storage + + def test_blockOverrides_debug_traceCall_override_block_invalid(self, retrieve_block_tx): + params = self.fill_params_for_storage_contract_trace_call(retrieve_block_tx, is_tx_block=True) + self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + override_params = {"blockOverrides": {"number": 0.1}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" in response_overrided, "No errors in response" + assert response_overrided["error"]["code"] == -32602, "Invalid error code" + assert response_overrided["error"]["message"] == "Invalid params" + + # NDEV-3009 + def test_blockOverrides_debug_traceCall_override_block_future(self, retrieve_block_tx): + params = self.fill_params_for_storage_contract_trace_call(retrieve_block_tx, is_tx_block=True) + self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + block = self.web3_client.get_block_number() + override_params = {"blockOverrides": {"number": block + 3}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" in response_overrided, "No errors in response" + assert response_overrided["error"]["code"] == -32603, "Invalid error code" + assert response_overrided["error"]["message"] == "neon_api::trace failed" + + @pytest.mark.skip("NDEV-3010") + def test_blockOverrides_debug_traceCall_wrong_override_format(self, retrieve_block_tx, call_storage_tx): + params = self.fill_params_for_storage_contract_trace_call(retrieve_block_tx, is_tx_block=True) + self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + override_params = {"blockOverrides": {retrieve_block_tx["to"].lower(): {"number": call_storage_tx["blockNumber"]}}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" in response_overrided, "No errors in response" + assert response_overrided["error"]["code"] == -32602, "Invalid error code" + assert response_overrided["error"]["message"] == "Invalid params" + + def test_blockOverrides_debug_traceCall_override_block_timestamp(self, storage_contract, retrieve_block_timestamp_tx): + params_prestate = self.fill_params_for_storage_contract_trace_call(retrieve_block_timestamp_tx, + is_prestate=True, + is_tx_block=True) + response_prestate = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_prestate) + address_to = retrieve_block_timestamp_tx["to"].lower() + timestamp_new = int(response_prestate["result"][address_to]["storage"][index_0], 0) + + sender_account = self.accounts[0] + tx = self.web3_client.make_raw_tx(sender_account) + instruction_tx = storage_contract.functions.storeBlockTimestamp().build_transaction(tx) + receipt = self.web3_client.send_transaction(sender_account, instruction_tx) + assert receipt["status"] == 1 + retrieve_block_timestamp_tx_new = self.web3_client.get_transaction_by_hash(receipt["transactionHash"].hex()) + + params_new_prestate = self.fill_params_for_storage_contract_trace_call(retrieve_block_timestamp_tx_new, + is_prestate=True, + is_tx_block=True) + response_new_prestate = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_new_prestate) + address_to = retrieve_block_timestamp_tx_new["to"].lower() + timestamp = int(response_new_prestate["result"][address_to]["storage"][index_0], 0) + + params = self.fill_params_for_storage_contract_trace_call(retrieve_block_timestamp_tx_new, is_tx_block=True) + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + override_params = {"blockOverrides": {"time": timestamp_new}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + diff = DeepDiff(response["result"], response_overrided["result"]) + + diff_storage = {"new_value": padhex(hex(timestamp_new), 64)[2:], + "old_value": padhex(hex(timestamp), 64)[2:]} + diff_block_timestamp = {"new_value": hex(timestamp_new), "old_value": hex(timestamp)} + + for _,v in diff["values_changed"].items(): + assert v == diff_block_timestamp or v == diff_storage + + def test_blockOverrides_debug_traceCall_override_block_timestamp_invalid_one(self, retrieve_block_timestamp_tx): + params = self.fill_params_for_storage_contract_trace_call(retrieve_block_timestamp_tx, is_tx_block=True) + self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + override_params = {"blockOverrides": {"time": "1715360635"}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" in response_overrided, "No errors in response" + assert response_overrided["error"]["code"] == -32602, "Invalid error code" + assert response_overrided["error"]["message"] == "Invalid params" + + def test_blockOverrides_debug_traceCall_override_block_timestamp_to_timestamp_now(self, retrieve_block_timestamp_tx): + params_prestate = self.fill_params_for_storage_contract_trace_call(retrieve_block_timestamp_tx, + is_prestate=True, + is_tx_block=True) + response_prestate = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_prestate) + address_to = retrieve_block_timestamp_tx["to"].lower() + timestamp = int(response_prestate["result"][address_to]["storage"][index_0], 0) + + params = self.fill_params_for_storage_contract_trace_call(retrieve_block_timestamp_tx, is_tx_block=True) + response = self.tracer_api.send_rpc("debug_traceCall", params) + + timestamp_new = int(time.time()) + override_params = {"blockOverrides": {"time": timestamp_new}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + diff = DeepDiff(response["result"], response_overrided["result"]) + + diff_storage = {"new_value": padhex(hex(timestamp_new), 64)[2:], + "old_value": padhex(hex(timestamp), 64)[2:]} + diff_block_timestamp = {"new_value": hex(timestamp_new), "old_value": hex(timestamp)} + + for _,v in diff["values_changed"].items(): + assert v == diff_block_timestamp or v == diff_storage + + def test_blockOverrides_debug_traceCall_override_block_number_and_timestamp(self, storage_object): + block_info_tx_1 = self.retrieve_block_info_tx(storage_object) + params_1_prestate = self.fill_params_for_storage_contract_trace_call(block_info_tx_1, + is_prestate=True, + is_tx_block=True) + response_prestate = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_1_prestate) + + address_to_1 = block_info_tx_1["to"].lower() + timestamp_1 = int(response_prestate["result"][address_to_1]["storage"][index_2], 0) + + params = self.fill_params_for_storage_contract_trace_call(block_info_tx_1, is_tx_block=True) + response = self.tracer_api.send_rpc("debug_traceCall", params) + + block_info_tx_2 = self.retrieve_block_info_tx(storage_object) + params_2_prestate = self.fill_params_for_storage_contract_trace_call(block_info_tx_2, + is_prestate=True, + is_tx_block=True) + response_prestate_2 = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params_2_prestate) + address_to_2 = block_info_tx_2["to"].lower() + timestamp_2 = int(response_prestate_2["result"][address_to_2]["storage"][index_2], 0) + + override_params = {"blockOverrides": {"number": block_info_tx_2["blockNumber"], "time": timestamp_2}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + diff = DeepDiff(response["result"], response_overrided["result"]) + + diff_storage_block = {"new_value": padhex(hex(block_info_tx_2["blockNumber"]), 64)[2:], + "old_value": padhex(hex(block_info_tx_1["blockNumber"]), 64)[2:]} + diff_block = {"new_value": hex(block_info_tx_2["blockNumber"]), + "old_value": hex(block_info_tx_1["blockNumber"])} + + diff_storage_time = {"new_value": padhex(hex(timestamp_2), 64)[2:], + "old_value": padhex(hex(timestamp_1), 64)[2:]} + diff_time = {"new_value": hex(timestamp_2), + "old_value": hex(timestamp_1)} + + for _,v in diff["values_changed"].items(): + assert v == diff_block or v == diff_storage_block or v == diff_time or v == diff_storage_time + + def test_blockOverrides_debug_traceCall_override_block_number_and_invalid_timestamp(self, storage_object): + block_info_tx = self.retrieve_block_info_tx(storage_object) + params = self.fill_params_for_storage_contract_trace_call(block_info_tx, is_tx_block=True) + self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + block_info_tx_override = self.retrieve_block_info_tx(storage_object) + override_params = {"blockOverrides": {"number": block_info_tx_override["blockNumber"], "time": "1715360635"}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + assert "error" in response_overrided, "No errors in response" + assert response_overrided["error"]["code"] == -32602, "Invalid error code" + assert response_overrided["error"]["message"] == "Invalid params" + + def test_blockOverrides_and_stateOverrides_debug_traceCall(self, call_storage_tx, retrieve_block_tx): + address_from = retrieve_block_tx["from"].lower() + params = self.fill_params_for_storage_contract_trace_call(retrieve_block_tx, is_tx_block=True) + response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) + + override_params = {"blockOverrides": {"number": call_storage_tx["blockNumber"]}, + "stateOverrides": {address_from: {"nonce": 12, "balance": "0xd8d726b7177a80000"}}} + params.append(override_params) + response_overrided = self.tracer_api.send_rpc("debug_traceCall", params) + + params[2]["tracer"] = "prestateTracer" + response_overrided_prestate = self.tracer_api.send_rpc("debug_traceCall", params) + + diff = DeepDiff(response["result"], response_overrided["result"]) + + diff_storage = {"new_value": padhex(hex(call_storage_tx["blockNumber"]), 64)[2:], + "old_value": padhex(hex(retrieve_block_tx["blockNumber"]), 64)[2:]} + diff_block = {"new_value": hex(call_storage_tx["blockNumber"]), + "old_value": hex(retrieve_block_tx["blockNumber"])} + + for _,v in diff["values_changed"].items(): + assert v == diff_block or v == diff_storage + + assert "error" not in response_overrided_prestate, "Error in response" + assert response_overrided_prestate["result"][address_from]["nonce"] == 12 + assert response_overrided_prestate["result"][address_from]["balance"] == "0xd8d726b7177a80000" \ No newline at end of file diff --git a/integration/tests/tracer/test_tracer_debug_methods.py b/integration/tests/tracer/test_tracer_debug_methods.py index 42b86cb1bb..3354683986 100644 --- a/integration/tests/tracer/test_tracer_debug_methods.py +++ b/integration/tests/tracer/test_tracer_debug_methods.py @@ -14,11 +14,13 @@ from utils.web3client import NeonChainWeb3Client from utils.accounts import EthAccounts from utils.tracer_client import TracerClient -from integration.tests.tracer.test_tracer_historical_methods import call_storage +from utils.helpers import padhex + SCHEMAS = "./integration/tests/tracer/schemas/" GOOD_CALLDATA = ["0x60fe60005360016000f3"] + @allure.feature("Tracer API") @allure.story("Tracer API RPC calls debug methods check") @pytest.mark.usefixtures("accounts", "web3_client", "tracer_api") @@ -37,12 +39,13 @@ def validate_response_result(self, response): validator = Draft4Validator(schema) assert validator.is_valid(response["result"]) + # NDEV-3009 def test_debug_trace_call_invalid_params(self): response = self.tracer_api.send_rpc(method="debug_traceCall", params=[{}, "0x0"]) assert "error" in response, "No errors in response" assert response["error"]["code"] == -32603, "Invalid error code" assert response["error"]["message"] == "neon_api::trace failed" - + def test_debug_trace_call_empty_params_valid_block(self): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -63,9 +66,8 @@ def test_debug_trace_call_zero_eth_call(self): recipient_account = self.accounts[1] receipt = self.web3_client.send_neon(sender_account, recipient_account, 0.1) assert receipt["status"] == 1 - tx_hash = receipt["transactionHash"].hex() - tx_info = self.web3_client.get_transaction_by_hash(tx_hash) + tx_info = self.web3_client.get_transaction_by_hash(receipt["transactionHash"].hex()) params = [ { @@ -85,13 +87,12 @@ def test_debug_trace_call_zero_eth_call(self): assert response["result"]["returnValue"] == "" self.validate_response_result(response) - def test_debug_trace_call_non_zero_eth_call(self, storage_contract, web3_client): + def test_debug_trace_call_non_zero_eth_call(self, storage_object): sender_account = self.accounts[0] store_value = random.randint(1, 100) - _, _, receipt = call_storage(sender_account, storage_contract, store_value, "blockNumber", web3_client) - tx_hash = receipt["transactionHash"].hex() - - tx_info = self.web3_client.get_transaction_by_hash(tx_hash) + _, _, receipt = storage_object.call_storage(sender_account, store_value, "blockNumber") + + tx_info = self.web3_client.get_transaction_by_hash(receipt["transactionHash"].hex()) params = [ { @@ -107,7 +108,7 @@ def test_debug_trace_call_non_zero_eth_call(self, storage_contract, web3_client) response = self.tracer_api.send_rpc_and_wait_response("debug_traceCall", params) assert "error" not in response, "Error in response" - assert 1 <= int(response["result"]["returnValue"], 16) <= 100 + assert response["result"]["returnValue"] == padhex(hex(store_value), 64)[2:] self.validate_response_result(response) def test_debug_trace_transaction(self): @@ -120,27 +121,31 @@ def test_debug_trace_transaction(self): assert "error" not in response, "Error in response" self.validate_response_result(response) - def test_debug_trace_transaction_non_zero_trace(self, web3_client, storage_contract): + def test_debug_trace_transaction_non_zero_trace(self, storage_object): sender_account = self.accounts[0] store_value = random.randint(1, 100) - _, _, receipt = call_storage(sender_account, storage_contract, store_value, "blockNumber", web3_client) + _, _, receipt = storage_object.call_storage(sender_account, store_value, "blockNumber") response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", [receipt["transactionHash"].hex()]) + assert "error" not in response, "Error in response" - assert 1 <= int(response["result"]["returnValue"], 16) <= 100 + assert response["result"]["returnValue"] == padhex(hex(store_value), 64)[2:] self.validate_response_result(response) - def test_debug_trace_transaction_hash_without_prefix(self, storage_contract, web3_client): + # GETH: NDEV-3251 + def test_debug_trace_transaction_hash_without_prefix(self, storage_object): sender_account = self.accounts[0] store_value = random.randint(1, 100) - _, _, receipt = call_storage(sender_account, storage_contract, store_value, "blockNumber", web3_client) + _, _, receipt = storage_object.call_storage(sender_account, store_value, "blockNumber") response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", [receipt["transactionHash"].hex()[2:]]) + assert "error" not in response, "Error in response" - assert 1 <= int(response["result"]["returnValue"], 16) <= 100 + assert response["result"]["returnValue"] == padhex(hex(store_value), 64)[2:] self.validate_response_result(response) @pytest.mark.parametrize("hash", [6, "0x0", "", "f23e554"]) + # GETH: NDEV-3250 def test_debug_trace_transaction_invalid_hash(self, hash): response = self.tracer_api.send_rpc(method="debug_traceTransaction", params=[hash]) assert "error" in response, "No errors in response" @@ -160,6 +165,7 @@ def test_debug_trace_block_by_number(self): self.validate_response_result(response["result"][0]) @pytest.mark.parametrize("number", [190, "", "3f08", "num", "0x"]) + # GETH: NDEV-3250 def test_debug_trace_block_by_invalid_number(self, number): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -171,6 +177,8 @@ def test_debug_trace_block_by_invalid_number(self, number): assert response["error"]["code"] == -32602, "Invalid error code" assert response["error"]["message"] == "Invalid params" + # GETH: NDEV-3249 + @pytest.mark.skip(reason="NDEV-3249") def test_debug_trace_block_by_zero_number(self): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -179,8 +187,8 @@ def test_debug_trace_block_by_zero_number(self): response = self.tracer_api.send_rpc(method="debug_traceBlockByNumber", params=["0x0"]) assert "error" in response, "No errors in response" - assert response["error"]["code"] == -32603, "Invalid error code" - assert response["error"]["message"] == "Genesis block is not traceable" + assert response["error"]["code"] == -32000, "Invalid error code" + assert response["error"]["message"] == "genesis is not traceable" def test_debug_trace_block_by_non_zero_early_number(self): sender_account = self.accounts[0] @@ -212,6 +220,7 @@ def test_debug_trace_block_by_hash(self): self.validate_response_result(response["result"][0]) @pytest.mark.parametrize("hash", [190, "0x0", "", "0x2ee1", "num", "f0918e"]) + # GETH: NDEV-3250 def test_debug_trace_block_by_invalid_hash(self, hash): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -223,6 +232,7 @@ def test_debug_trace_block_by_invalid_hash(self, hash): assert response["error"]["code"] == -32602, "Invalid error code" assert response["error"]["message"] == "Invalid params" + # GETH: NDEV-3249 def test_debug_trace_block_by_non_existent_hash(self): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -244,6 +254,7 @@ def decode_raw_header(self, header: bytes): sedes = List([big_endian_int, binary, binary, binary, binary]) return decode(header, sedes) + # NDEV-3261: incomplete header in response def test_debug_getRawHeader_by_block_number(self): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -253,7 +264,6 @@ def test_debug_getRawHeader_by_block_number(self): response = self.tracer_api.send_rpc_and_wait_response("debug_getRawHeader", [hex(receipt["blockNumber"])]) assert "error" not in response, "Error in response" assert "result" in response and response["result"] is not None - header = self.decode_raw_header(bytes.fromhex(response["result"])) block_info = self.web3_client.eth.get_block(receipt["blockNumber"]) assert header[0] == block_info["number"] @@ -262,12 +272,14 @@ def test_debug_getRawHeader_by_block_number(self): assert header[3].hex() == block_info["stateRoot"].hex()[2:] assert header[4].hex() == block_info["receiptsRoot"].hex()[2:] + # GETH: NDEV-3250 def test_debug_getRawHeader_by_invalid_block_number(self): response = self.tracer_api.send_rpc(method="debug_getRawHeader", params=["0f98e"]) assert "error" in response, "No errors in response" assert response["error"]["code"] == -32602, "Invalid error code" assert response["error"]["message"] == "Invalid params" + # NDEV-3261: incomplete header in response def test_debug_getRawHeader_by_block_hash(self): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -286,6 +298,7 @@ def test_debug_getRawHeader_by_block_hash(self): assert header[3].hex() == block_info["stateRoot"].hex()[2:] assert header[4].hex() == block_info["receiptsRoot"].hex()[2:] + # GETH: NDEV-3250 def test_debug_getRawHeader_by_invalid_block_hash(self): response = self.tracer_api.send_rpc(method="debug_getRawHeader", params=["0f98e"]) assert "error" in response, "No errors in response" @@ -303,6 +316,7 @@ def check_modified_accounts_response(self, response, expected_accounts=[]): for item in response["result"]: assert re.match(r"\b0x[a-f0-9]{40}\b", item) + # GETH: NDEV-3248 def test_debug_get_modified_accounts_by_same_number(self): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -315,6 +329,7 @@ def test_debug_get_modified_accounts_by_same_number(self): ) self.check_modified_accounts_response(response, [sender_account.address, recipient_account.address]) + # GETH: NDEV-3248 def test_debug_get_modified_accounts_by_only_one_number(self): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -328,6 +343,7 @@ def test_debug_get_modified_accounts_by_only_one_number(self): self.check_modified_accounts_response(response, [sender_account.address, recipient_account.address]) @pytest.mark.parametrize("difference", [1, 25, 49, 50]) + # GETH: NDEV-3248 def test_debug_get_modified_accounts_by_number_blocks_difference_less_or_equal_50(self, difference): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -343,6 +359,7 @@ def test_debug_get_modified_accounts_by_number_blocks_difference_less_or_equal_5 ) self.check_modified_accounts_response(response, [sender_account.address, recipient_account.address]) + # GETH: NDEV-3248 def test_debug_get_modified_accounts_by_number_51_blocks_difference(self): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -359,12 +376,14 @@ def test_debug_get_modified_accounts_by_number_51_blocks_difference(self): assert response["error"]["message"] == "Requested range (51) is too big, maximum allowed range is 50 blocks" @pytest.mark.parametrize("params", [[1, 124], ["94f3e", 12], ["1a456", "0x0"], ["183b8e", "183b8e"]]) + # GETH: NDEV-3248 def test_debug_get_modified_accounts_by_invalid_numbers(self, params): response = self.tracer_api.send_rpc(method="debug_getModifiedAccountsByNumber", params=params) assert "error" in response, "No errors in response" assert response["error"]["code"] == -32602, "Invalid error code" assert response["error"]["message"] == "Invalid params" + # GETH: NDEV-3248 def test_debug_get_modified_accounts_by_same_hash(self): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -377,6 +396,7 @@ def test_debug_get_modified_accounts_by_same_hash(self): ) self.check_modified_accounts_response(response, [sender_account.address, recipient_account.address]) + # GETH: NDEV-3248 def test_debug_get_modified_accounts_by_hash(self): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -391,6 +411,7 @@ def test_debug_get_modified_accounts_by_hash(self): ) self.check_modified_accounts_response(response, [sender_account.address, recipient_account.address]) + # GETH: NDEV-3248 def test_debug_get_modified_accounts_by_hash_contract_deployment(self, storage_contract_with_deploy_tx): contract = storage_contract_with_deploy_tx[0] receipt = storage_contract_with_deploy_tx[1] @@ -403,6 +424,7 @@ def test_debug_get_modified_accounts_by_hash_contract_deployment(self, storage_c @pytest.mark.parametrize( "params", [[1, 124], ["0x94f3e00000000800000000", 12], ["0x1a456", "0x000000000001"], ["0x183b8e", "183b8e"]] ) + # GETH: NDEV-3248 def test_debug_get_modified_accounts_by_invalid_hash(self, params): response = self.tracer_api.send_rpc(method="debug_getModifiedAccountsByHash", params=params) assert "error" in response, "No errors in response" @@ -422,6 +444,7 @@ def test_debug_get_raw_transaction(self): assert "error" not in response, "Error in response" assert "result" in response and response["result"] == signed_tx.rawTransaction.hex() + # GETH: NDEV-3252 def test_debug_get_raw_transaction_invalid_tx_hash(self): sender_account = self.accounts[0] recipient_account = self.accounts[1] @@ -432,6 +455,7 @@ def test_debug_get_raw_transaction_invalid_tx_hash(self): assert response["error"]["code"] == -32603, "Invalid error code" assert response["error"]["message"] == f'Empty Neon transaction receipt for {receipt["blockHash"].hex()}' + # GETH: NDEV-3252 def test_debug_get_raw_transaction_non_existent_tx_hash(self): response = self.tracer_api.send_rpc( method="debug_getRawTransaction", diff --git a/integration/tests/tracer/test_tracer_debug_trace_transactions_callTracer.py b/integration/tests/tracer/test_tracer_debug_trace_transactions_callTracer.py index bd2c6aa3b0..2e5b877464 100644 --- a/integration/tests/tracer/test_tracer_debug_trace_transactions_callTracer.py +++ b/integration/tests/tracer/test_tracer_debug_trace_transactions_callTracer.py @@ -1,15 +1,13 @@ import random - import allure import pytest -import json from deepdiff import DeepDiff from integration.tests.basic.helpers.basic import AccountData from utils.web3client import NeonChainWeb3Client from utils.accounts import EthAccounts from utils.tracer_client import TracerClient -from integration.tests.tracer.test_tracer_historical_methods import call_storage + @allure.feature("Tracer API") @allure.story("Tracer API RPC calls debug method trace_transaction callTracer check") @@ -19,12 +17,14 @@ class TestDebugTraceTransactionCallTracer: accounts: EthAccounts tracer_api: TracerClient - def fill_expected_response(self, instruction_tx, receipt, + def fill_expected_response(self, + instruction_tx, receipt, type="CALL", logs=False, calls=True, revert=False, - revert_reason=None, + revert_reason=None, + error=None, calls_value="0x1", calls_type="CALL", calls_logs_append=False @@ -50,8 +50,7 @@ def fill_expected_response(self, instruction_tx, receipt, }) if calls_type == "DELEGATECALL": - expected_response["calls"][0]["from"] = instruction_tx["from"].lower() - expected_response["calls"][0]["to"] = address_to + expected_response["calls"][0]["from"] = instruction_tx["to"].lower() if calls_logs_append: for log in receipt["logs"]: @@ -78,23 +77,43 @@ def fill_expected_response(self, instruction_tx, receipt, }] if revert: - expected_response["calls"][0]["error"] = "execution reverted" - expected_response["calls"][0]["revertReason"] = revert_reason + if error: + expected_response["calls"][0]["error"] = revert_reason + else: + expected_response["calls"][0]["error"] = "execution reverted" + expected_response["calls"][0]["revertReason"] = revert_reason return expected_response - def assert_response_contains_expected(self, expected_response, response, sort_calls=False): + def assert_response_contains_expected(self, pytestconfig, expected_response, response, sort_calls=False): if sort_calls: expected_response["calls"] = sorted(expected_response["calls"], key=lambda d: d['type']) response["result"]["calls"] = sorted(response["result"]["calls"], key=lambda d: d['type']) - diff = DeepDiff(expected_response, response["result"]) + if pytestconfig.getoption("--network") == "geth": + # we do not fill whole response, that is why we skip some of fields + # we can build compare function for each field in the future if it needed + exclude_list = ["root['gas']", "root['output']", "root['value']"] + if "calls" in expected_response: + for i in range(len(expected_response["calls"])): + exclude_list.append(f"root['calls'][{i}]['to']") + exclude_list.append(f"root['calls'][{i}]['gas']") + exclude_list.append(f"root['calls'][{i}]['gasUsed']") + exclude_list.append(f"root['calls'][{i}]['input']") + exclude_list.append(f"root['calls'][{i}]['output']") + exclude_list.append(f"root['calls'][{i}]['value']") + exclude_list.append(f"root['calls'][{i}]['logs'][0]['address']") + exclude_list.append(f"root['calls'][{i}]['logs'][0]['position']") + else: + exclude_list = [] + + diff = DeepDiff(expected_response, response["result"], exclude_paths=exclude_list) # check if expected_response is subset of response assert "dictionary_item_removed" not in diff # check if expected_response and response match in identical keys assert "values_changed" not in diff - def test_callTracer_type_create(self, storage_contract_with_deploy_tx): + def test_callTracer_type_create(self, pytestconfig, storage_contract_with_deploy_tx): receipt = storage_contract_with_deploy_tx[1] expected_response = {} @@ -107,9 +126,9 @@ def test_callTracer_type_create(self, storage_contract_with_deploy_tx): expected_response["gasUsed"] = hex(receipt["gasUsed"]) expected_response["type"] = "CREATE" - self.assert_response_contains_expected(expected_response, response) + self.assert_response_contains_expected(pytestconfig, expected_response, response) - def test_callTracer_type_create2(self, events_checker_contract): + def test_callTracer_type_create2(self, pytestconfig, events_checker_contract): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(from_=sender_account) @@ -121,22 +140,22 @@ def test_callTracer_type_create2(self, events_checker_contract): response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", params) expected_response = self.fill_expected_response(instruction_tx, receipt, calls_value="0x0", calls_type="CREATE2") - self.assert_response_contains_expected(expected_response, response) + self.assert_response_contains_expected(pytestconfig, expected_response, response) - def test_callTracer_type_call(self, storage_contract): + def test_callTracer_type_call(self, pytestconfig, storage_object): sender_account = self.accounts[0] store_value = random.randint(1, 100) - tx_obj, _, receipt = call_storage(sender_account, storage_contract, store_value, "blockNumber", self.web3_client) + tx_obj, _, receipt = storage_object.call_storage(sender_account, store_value, "blockNumber") tracer_params = { "tracer": "callTracer", "tracerConfig": { "onlyTopCall": True } } params = [receipt["transactionHash"].hex(), tracer_params] response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", params) expected_response = self.fill_expected_response(tx_obj, receipt, calls=False) - self.assert_response_contains_expected(expected_response, response) + self.assert_response_contains_expected(pytestconfig, expected_response, response) - def test_callTracer_withLog_check(self, event_caller_contract): + def test_callTracer_withLog_check(self, pytestconfig, event_caller_contract): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(from_=sender_account) @@ -148,7 +167,7 @@ def test_callTracer_withLog_check(self, event_caller_contract): response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", params) expected_response = self.fill_expected_response(instruction_tx, receipt, calls=False, logs=True) - self.assert_response_contains_expected(expected_response, response) + self.assert_response_contains_expected(pytestconfig, expected_response, response) tracer_params = { "tracer": "callTracer", "tracerConfig": { "withLog": False } } response = self.tracer_api.send_rpc( @@ -166,8 +185,8 @@ def test_callTracer_onlyTopCall_check(self, events_checker_contract, event_check receipt = self.web3_client.send_transaction(sender_account, instruction_tx) tracer_params = { "tracer": "callTracer", "tracerConfig": { "OnlyTopCall": False } } - params = [receipt["transactionHash"].hex(), tracer_params] - response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", params) + response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", + [receipt["transactionHash"].hex(), tracer_params]) assert len(response["result"]["calls"]) == 2 assert len(response["result"]["calls"][1]["calls"]) == 2 @@ -175,10 +194,14 @@ def test_callTracer_onlyTopCall_check(self, events_checker_contract, event_check assert response["result"]["calls"][1]["calls"][1]["type"] == "STATICCALL" tracer_params = { "tracer": "callTracer", "tracerConfig": { "OnlyTopCall": True } } - response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", params) + response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", + [receipt["transactionHash"].hex(), tracer_params]) assert "calls" not in response["result"] - def test_callTracer_call_contract_from_contract_type_static_call(self, events_checker_contract, event_checker_callee_address): + def test_callTracer_call_contract_from_contract_type_static_call(self, + pytestconfig, + events_checker_contract, + event_checker_callee_address): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(from_=sender_account) @@ -186,14 +209,17 @@ def test_callTracer_call_contract_from_contract_type_static_call(self, events_ch event_checker_callee_address).build_transaction(tx) receipt = self.web3_client.send_transaction(sender_account, instruction_tx) - tracer_params = { "tracer": "callTracer", "tracerConfig": { "OnlyTopCall": True } } + tracer_params = { "tracer": "callTracer", "tracerConfig": { "OnlyTopCall": False } } params = [receipt["transactionHash"].hex(), tracer_params] response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", params) expected_response = self.fill_expected_response(instruction_tx, receipt, calls_value="0x0", calls_type="STATICCALL") - self.assert_response_contains_expected(expected_response, response) + self.assert_response_contains_expected(pytestconfig, expected_response, response) - def test_callTracer_call_contract_from_contract_type_static_call_with_events(self, events_checker_contract, event_checker_callee_address): + def test_callTracer_call_contract_from_contract_type_static_call_with_events(self, + pytestconfig, + events_checker_contract, + event_checker_callee_address): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(from_=sender_account) @@ -211,9 +237,12 @@ def test_callTracer_call_contract_from_contract_type_static_call_with_events(sel calls_value="0x0", calls_type="STATICCALL", calls_logs_append=True) - self.assert_response_contains_expected(expected_response, response, sort_calls=True) + self.assert_response_contains_expected(pytestconfig, expected_response, response, sort_calls=True) - def test_callTracer_call_contract_from_contract_type_call_with_events(self, events_checker_contract, event_checker_callee_address): + def test_callTracer_call_contract_from_contract_type_call_with_events(self, + pytestconfig, + events_checker_contract, + event_checker_callee_address): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(from_=sender_account) @@ -230,9 +259,9 @@ def test_callTracer_call_contract_from_contract_type_call_with_events(self, even logs=True, calls_value="0x0", calls_logs_append=True) - self.assert_response_contains_expected(expected_response, response, sort_calls=True) + self.assert_response_contains_expected(pytestconfig, expected_response, response, sort_calls=True) - def test_callTracer_call_contract_from_contract_type_call(self, events_checker_contract, event_checker_callee_address): + def test_callTracer_call_contract_from_contract_type_call(self, pytestconfig, events_checker_contract, event_checker_callee_address): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(from_=sender_account) @@ -245,9 +274,10 @@ def test_callTracer_call_contract_from_contract_type_call(self, events_checker_c response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", params) expected_response = self.fill_expected_response(instruction_tx, receipt, calls_value="0x0") - self.assert_response_contains_expected(expected_response, response) + self.assert_response_contains_expected(pytestconfig, expected_response, response) - def test_callTracer_call_contract_from_contract_type_delegate_call(self, events_checker_contract, event_checker_callee_address): + @pytest.mark.skip(reason="SLA-119") + def test_callTracer_call_contract_from_contract_type_delegate_call(self, pytestconfig, events_checker_contract, event_checker_callee_address): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(from_=sender_account) @@ -260,9 +290,9 @@ def test_callTracer_call_contract_from_contract_type_delegate_call(self, events_ response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", params) expected_response = self.fill_expected_response(instruction_tx, receipt, calls_value="0x0", calls_type="DELEGATECALL") - self.assert_response_contains_expected(expected_response, response) + self.assert_response_contains_expected(pytestconfig, expected_response, response) - def test_callTracer_call_contract_from_contract_type_callcode(self, opcodes_checker): + def test_callTracer_call_contract_from_contract_type_callcode(self, pytestconfig, opcodes_checker): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(from_=sender_account) @@ -274,9 +304,9 @@ def test_callTracer_call_contract_from_contract_type_callcode(self, opcodes_chec response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", params) expected_response = self.fill_expected_response(instruction_tx, receipt, calls_value="0x0", calls_type="CALLCODE") - self.assert_response_contains_expected(expected_response, response) + self.assert_response_contains_expected(pytestconfig, expected_response, response) - def test_callTracer_call_contract_with_zero_division(self, events_checker_contract, event_checker_callee_address): + def test_callTracer_call_contract_with_zero_division(self, pytestconfig, events_checker_contract, event_checker_callee_address): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(from_=sender_account) @@ -293,9 +323,10 @@ def test_callTracer_call_contract_with_zero_division(self, events_checker_contra revert=True, revert_reason="division or modulo by zero", calls_value="0x0") - self.assert_response_contains_expected(expected_response, response) + + self.assert_response_contains_expected(pytestconfig, expected_response, response) - def test_callTracer_call_contract_from_other_contract_revert_with_assert(self, events_checker_contract, event_checker_callee_address): + def test_callTracer_call_contract_from_other_contract_revert_with_assert(self, pytestconfig, events_checker_contract, event_checker_callee_address): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(from_=sender_account) @@ -313,9 +344,10 @@ def test_callTracer_call_contract_from_other_contract_revert_with_assert(self, e revert=True, revert_reason="assert(false)", calls_value="0x0") - self.assert_response_contains_expected(expected_response, response) - def test_callTracer_call_contract_from_other_contract_trivial_revert(self, events_checker_contract, event_checker_callee_address): + self.assert_response_contains_expected(pytestconfig, expected_response, response) + + def test_callTracer_call_contract_from_other_contract_trivial_revert(self, pytestconfig, events_checker_contract, event_checker_callee_address): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(from_=sender_account) @@ -333,9 +365,10 @@ def test_callTracer_call_contract_from_other_contract_trivial_revert(self, event revert=True, revert_reason="Revert Contract", calls_value="0x0") - self.assert_response_contains_expected(expected_response, response) + self.assert_response_contains_expected(pytestconfig, expected_response, response) - def test_callTracer_call_contract_from_other_contract_revert(self, events_checker_contract, event_checker_callee_address): + @pytest.mark.skip(reason="NDEV-3260") + def test_callTracer_call_contract_from_other_contract_revert(self, pytestconfig, events_checker_contract, event_checker_callee_address): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(from_=sender_account) @@ -348,11 +381,14 @@ def test_callTracer_call_contract_from_other_contract_revert(self, events_checke response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", params) address_to = instruction_tx["to"].lower() - reason = f"Insufficient balance for transfer, account = {address_to}, chain = 111, required = 1" - expected_response = self.fill_expected_response(instruction_tx, receipt, logs=True, revert=True, revert_reason=reason) - self.assert_response_contains_expected(expected_response, response) + if pytestconfig.getoption("--network") == "geth": + reason = "insufficient balance for transfer" + else: + reason = f"Insufficient balance for transfer, account = {address_to}, chain = {self.web3_client.eth.chain_id}, required = 1" + expected_response = self.fill_expected_response(instruction_tx, receipt, logs=True, revert=True, error=reason) + self.assert_response_contains_expected(pytestconfig, expected_response, response) - def test_callTracer_call_contract_from_other_contract_revert_with_require(self, events_checker_contract, event_checker_callee_address): + def test_callTracer_call_contract_from_other_contract_revert_with_require(self, pytestconfig, events_checker_contract, event_checker_callee_address): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(from_=sender_account) @@ -370,9 +406,9 @@ def test_callTracer_call_contract_from_other_contract_revert_with_require(self, revert=True, revert_reason="require False", calls_value="0x0") - self.assert_response_contains_expected(expected_response, response) + self.assert_response_contains_expected(pytestconfig, expected_response, response) - def test_callTracer_call_to_precompiled_contract(self, eip1052_checker): + def test_callTracer_call_to_precompiled_contract(self, pytestconfig, eip1052_checker): sender_account = self.accounts[0] tx = self.web3_client.make_raw_tx(sender_account) precompiled_acc = AccountData(address="0xFf00000000000000000000000000000000000004") @@ -386,21 +422,21 @@ def test_callTracer_call_to_precompiled_contract(self, eip1052_checker): response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", params) expected_response = self.fill_expected_response(instruction_tx, receipt, calls=False) - self.assert_response_contains_expected(expected_response, response) + self.assert_response_contains_expected(pytestconfig, expected_response, response) @pytest.mark.skip(reason="NDEV-2934") - def test_callTracer_without_tracerConfig(self, storage_contract): + def test_callTracer_without_tracerConfig(self, pytestconfig, storage_object): sender_account = self.accounts[0] store_value = random.randint(1, 100) - tx_obj, _, receipt = call_storage(sender_account, storage_contract, store_value, "blockNumber", self.web3_client) + tx_obj, _, receipt = storage_object.call_storage(sender_account, store_value, "blockNumber") tracer_params = { "tracer": "callTracer" } params = [receipt["transactionHash"].hex(), tracer_params] response = self.tracer_api.send_rpc_and_wait_response("debug_traceTransaction", params) expected_response = self.fill_expected_response(tx_obj, receipt, calls=False) - self.assert_response_contains_expected(expected_response, response) + self.assert_response_contains_expected(pytestconfig, expected_response, response) def test_callTracer_call_contract_with_event_from_other_one_with_two_events(self, events_checker_contract, event_checker_callee_address): sender_account = self.accounts[0] diff --git a/integration/tests/tracer/test_tracer_historical_methods.py b/integration/tests/tracer/test_tracer_historical_methods.py index f0411b6f67..2208b06f20 100644 --- a/integration/tests/tracer/test_tracer_historical_methods.py +++ b/integration/tests/tracer/test_tracer_historical_methods.py @@ -11,55 +11,6 @@ from utils.apiclient import JsonRPCSession from utils.helpers import wait_condition -def store_value(sender_account, value, storage_contract, web3_client): - nonce = web3_client.eth.get_transaction_count(sender_account.address) - instruction_tx = storage_contract.functions.store(value).build_transaction( - { - "nonce": nonce, - "gasPrice": web3_client.gas_price(), - } - ) - receipt = web3_client.send_transaction(sender_account, instruction_tx) - assert receipt["status"] == 1 - - -def retrieve_value(sender_account, storage_contract, web3_client): - nonce = web3_client.eth.get_transaction_count(sender_account.address) - instruction_tx = storage_contract.functions.retrieve().build_transaction( - { - "nonce": nonce, - "gasPrice": web3_client.gas_price(), - } - ) - receipt = web3_client.send_transaction(sender_account, instruction_tx) - - assert receipt["status"] == 1 - return instruction_tx, receipt - - -def call_storage(sender_account, storage_contract, storage_value, request_type, web3_client): - request_value = None - store_value(sender_account, storage_value, storage_contract, web3_client) - tx, receipt = retrieve_value(sender_account, storage_contract, web3_client) - - tx_obj = web3_client.make_raw_tx( - from_=sender_account.address, - to=storage_contract.address, - amount=tx["value"], - gas=hex(tx["gas"]), - gas_price=hex(tx["gasPrice"]), - data=tx["data"], - estimate_gas=False, - ) - del tx_obj["chainId"] - del tx_obj["nonce"] - - if request_type == "blockNumber": - request_value = hex(receipt[request_type]) - else: - request_value = receipt[request_type].hex() - return tx_obj, request_value, receipt - @allure.feature("Tracer API") @allure.story("Tracer API RPC calls historical methods check") @@ -78,17 +29,17 @@ def assert_invalid_params(self, response): assert response["error"]["code"] == -32602, "Invalid error code" assert response["error"]["message"] == "Invalid params" + # GETH: NDEV-3252 def test_eth_call_without_params(self): response = self.tracer_api.send_rpc(method="eth_call", params=[None]) assert "error" in response, "Error not in response" @pytest.mark.parametrize("request_type", ["blockNumber", "blockHash"]) - def test_eth_call(self, storage_contract, request_type): + def test_eth_call(self, storage_object, request_type): sender_account = self.accounts[0] store_value_1 = random.randint(0, 100) - tx_obj, request_value, _ = call_storage( - sender_account, storage_contract, store_value_1, request_type, self.web3_client - ) + tx_obj, request_value, _ = storage_object.call_storage(sender_account, store_value_1, request_type) + wait_condition( lambda: int( self.tracer_api.send_rpc( @@ -101,9 +52,8 @@ def test_eth_call(self, storage_contract, request_type): ) store_value_2 = random.randint(0, 100) - tx_obj_2, request_value_2, _ = call_storage( - sender_account, storage_contract, store_value_2, request_type, self.web3_client - ) + tx_obj_2, request_value_2, _ = storage_object.call_storage(sender_account, store_value_2, request_type) + wait_condition( lambda: int( self.tracer_api.send_rpc( @@ -116,7 +66,7 @@ def test_eth_call(self, storage_contract, request_type): ) store_value_3 = random.randint(0, 100) - tx_obj_3, request_value_3, _ = call_storage(sender_account, storage_contract, store_value_3, request_type, self.web3_client) + tx_obj_3, request_value_3, _ = storage_object.call_storage(sender_account, store_value_3, request_type) wait_condition( lambda: int( self.tracer_api.send_rpc( @@ -128,21 +78,20 @@ def test_eth_call(self, storage_contract, request_type): timeout_sec=120, ) - def test_eth_call_invalid_params(self, storage_contract, web3_client): + # GETH: NDEV-3250 + def test_eth_call_invalid_params(self, storage_object): sender_account = self.accounts[0] store_value_1 = random.randint(0, 100) - tx_obj, _, _ = call_storage(sender_account, storage_contract, store_value_1, "blockHash", web3_client) + tx_obj, _, _ = storage_object.call_storage(sender_account, store_value_1, "blockHash") response = self.tracer_api.send_rpc( method="eth_call", req_type="blockHash", params=[tx_obj, {"blockHash": "0x0000"}] ) self.assert_invalid_params(response) - def test_eth_call_by_block_and_hash(self, storage_contract): + def test_eth_call_by_block_and_hash(self, storage_object): sender_account = self.accounts[0] store_value_1 = random.randint(0, 100) - tx_obj, _, receipt = call_storage( - sender_account, storage_contract, store_value_1, "blockNumber", self.web3_client - ) + tx_obj, _, receipt = storage_object.call_storage(sender_account, store_value_1, "blockNumber") request_value_block = hex(receipt["blockNumber"]) request_value_hash = receipt["blockHash"].hex() @@ -169,19 +118,17 @@ def test_eth_call_by_block_and_hash(self, storage_contract): ) @pytest.mark.parametrize("request_type", ["blockNumber", "blockHash"]) - def test_eth_get_storage_at(self, storage_contract, request_type): + def test_eth_get_storage_at(self, storage_object, request_type): sender_account = self.accounts[0] store_value_1 = random.randint(0, 100) - _, request_value_1, _ = call_storage( - sender_account, storage_contract, store_value_1, request_type, self.web3_client - ) + _, request_value_1, _ = storage_object.call_storage(sender_account, store_value_1, request_type) wait_condition( lambda: int( self.tracer_api.send_rpc( method="eth_getStorageAt", req_type=request_type, - params=[storage_contract.address, "0x0", {request_type: request_value_1}], + params=[storage_object.contract_address, "0x0", {request_type: request_value_1}], )["result"], 0, ) @@ -190,16 +137,14 @@ def test_eth_get_storage_at(self, storage_contract, request_type): ) store_value_2 = random.randint(0, 100) - _, request_value_2, _ = call_storage( - sender_account, storage_contract, store_value_2, request_type, self.web3_client - ) + _, request_value_2, _ = storage_object.call_storage(sender_account, store_value_2, request_type) wait_condition( lambda: int( self.tracer_api.send_rpc( method="eth_getStorageAt", req_type=request_type, - params=[storage_contract.address, "0x0", {request_type: request_value_2}], + params=[storage_object.contract_address, "0x0", {request_type: request_value_2}], )["result"], 0, ) @@ -207,6 +152,7 @@ def test_eth_get_storage_at(self, storage_contract, request_type): timeout_sec=120, ) + # GETH: NDEV-3250 def test_eth_get_storage_at_invalid_params(self): response = self.tracer_api.send_rpc( method="eth_getTransactionCount", req_type="blockNumber", params=["0x0", {"blockNumber": "0x001"}] @@ -214,13 +160,11 @@ def test_eth_get_storage_at_invalid_params(self): self.assert_invalid_params(response) @pytest.mark.parametrize("request_type", ["blockNumber", "blockHash"]) - def test_eth_get_transaction_count(self, storage_contract, request_type): + def test_eth_get_transaction_count(self, storage_object, request_type): sender_account = self.accounts[0] nonce = self.web3_client.eth.get_transaction_count(sender_account.address) store_value_1 = random.randint(0, 100) - _, request_value_1, _ = call_storage( - sender_account, storage_contract, store_value_1, request_type, self.web3_client - ) + _, request_value_1, _ = storage_object.call_storage(sender_account, store_value_1, request_type) wait_condition( lambda: int( @@ -236,7 +180,7 @@ def test_eth_get_transaction_count(self, storage_contract, request_type): ) request_value_2 = None - _, receipt = retrieve_value(sender_account, storage_contract, self.web3_client) + _, receipt = storage_object.retrieve_value(sender_account) if request_type == "blockNumber": request_value_2 = hex(receipt[request_type]) @@ -256,6 +200,7 @@ def test_eth_get_transaction_count(self, storage_contract, request_type): timeout_sec=120, ) + # GETH: NDEV-3250 def test_eth_get_transaction_count_invalid_params(self): response = self.tracer_api.send_rpc( method="eth_getTransactionCount", req_type="blockNumber", params=["0x0", {"blockNumber": "0x001"}] @@ -338,6 +283,7 @@ def test_eth_get_balance(self, request_type): timeout_sec=120, ) + # GETH: NDEV-3250 def test_eth_get_balance_invalid_params(self): sender_account = self.accounts[0] response = self.tracer_api.send_rpc( @@ -345,6 +291,7 @@ def test_eth_get_balance_invalid_params(self): ) self.assert_invalid_params(response) + # GETH: NDEV-3251, NDEV-3252 def test_eth_get_code(self, storage_contract_with_deploy_tx): storage_contract_code = storage_contract_with_deploy_tx[0].functions.at(storage_contract_with_deploy_tx[1]["contractAddress"]).call().hex() request_type = "blockNumber" @@ -367,8 +314,8 @@ def test_eth_get_code(self, storage_contract_with_deploy_tx): self.tracer_api.send_rpc( method="eth_getCode", req_type="blockHash", - params=[storage_contract_with_deploy_tx[0].address, - {request_type: hex(storage_contract_with_deploy_tx[1]['blockNumber'])}], + params=[storage_contract_with_deploy_tx[0].address, + {request_type: hex(storage_contract_with_deploy_tx[1]['blockNumber'])}], ) )["result"] == storage_contract_code, @@ -388,6 +335,7 @@ def test_eth_get_code(self, storage_contract_with_deploy_tx): timeout_sec=120, ) + # GETH: NDEV-3250 def test_eth_get_code_invalid_params(self, storage_contract_with_deploy_tx): response = self.tracer_api.send_rpc( method="eth_getCode", req_type="blockHash", diff --git a/loadtesting/k6/main.js b/loadtesting/k6/main.js new file mode 100644 index 0000000000..3ae9b3d364 --- /dev/null +++ b/loadtesting/k6/main.js @@ -0,0 +1,20 @@ +import { sleep } from 'k6'; + +let ENVIRONMENT = {}; +ENVIRONMENT.execution = "local"; +if (__ENV.EXECUTION) { + ENVIRONMENT.execution = __ENV.EXECUTION; +} + +ENVIRONMENT.optionsSet = "load"; +if (__ENV.OPTIONS_SET) { + ENVIRONMENT.optionsSet = `.${__ENV.OPTIONS_SET}`; +} + +import { sendNeon } from './tests/sendNeon.test.js'; +let TESTS = [ ...sendNeon ]; + +let optionsFile = `./env/options.json`; +export let options = JSON.parse(open(optionsFile)); + +//TO DO: add basic scenario here \ No newline at end of file diff --git a/loadtesting/k6/monitoring/.docker/dashboards/neonlabs_proxy.json b/loadtesting/k6/monitoring/.docker/dashboards/neonlabs_proxy.json new file mode 100644 index 0000000000..69d969928c --- /dev/null +++ b/loadtesting/k6/monitoring/.docker/dashboards/neonlabs_proxy.json @@ -0,0 +1,694 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 6, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 17, + "panels": [], + "title": "Aggregated Params", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "sum by (testrun) (k6_vus_gauge{testrun=\"$testrun\"})", + "legendFormat": "VUs", + "range": true, + "refId": "A" + } + ], + "title": "Users", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "sum by (testrun) (k6_vus_gauge{testrun=\"$testrun\"})", + "legendFormat": "VUs", + "range": true, + "refId": "A" + } + ], + "title": "Users", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 10 + }, + "id": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "irate(k6_send_neon_requests_counter{testrun=\"$testrun\"}[$__rate_interval])", + "hide": false, + "legendFormat": "send neon", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "irate(k6_nonce_requests_counter{testrun=\"$testrun\"}[$__rate_interval])", + "hide": false, + "legendFormat": "nonce", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "irate(k6_block_number_requests_counter{testrun=\"$testrun\"}[$__rate_interval])", + "hide": false, + "legendFormat": "block number", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "irate(k6_gas_price_requests_counter{testrun=\"$testrun\"}[$__rate_interval])", + "hide": false, + "legendFormat": "gas price", + "range": true, + "refId": "D" + } + ], + "title": "Aggregated RPS", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 10 + }, + "id": 41, + "interval": "5", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "max by (testrun) (k6_send_neon_request_time_0.95{testrun=\"$testrun\"})", + "legendFormat": "send neon", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "max by (testrun) (k6_nonce_request_time_0.95{testrun=\"$testrun\"})", + "hide": false, + "legendFormat": "nonce", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "max by (testrun) (k6_block_number_request_time_0.95{testrun=\"$testrun\"})", + "hide": false, + "legendFormat": "block", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "max by (testrun) (k6_gas_price_request_time_0.95{testrun=\"$testrun\"})", + "hide": false, + "legendFormat": "gas", + "range": true, + "refId": "D" + } + ], + "title": "Aggregated Requests Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "nonce" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "super-light-red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "block number" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "dark-red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 19 + }, + "id": 42, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.7", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "irate(k6_send_neon_errors_counter{testrun=\"$testrun\"}[$__rate_interval])", + "legendFormat": "send neon", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "irate(k6_nonce_errors_counter{testrun=\"$testrun\"}[$__rate_interval])", + "hide": false, + "legendFormat": "nonce", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "irate(k6_gas_price_errors_counter{testrun=\"$testrun\"}[$__rate_interval])", + "hide": false, + "legendFormat": "gas price", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "editorMode": "code", + "expr": "irate(k6_block_number_errors_counter{testrun=\"$testrun\"}[$__rate_interval])", + "hide": false, + "legendFormat": "block number", + "range": true, + "refId": "D" + } + ], + "title": "Requests Errors", + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 37, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "isNone": true, + "selected": false, + "text": "None", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "P4169E866C3094E38" + }, + "definition": "label_values(k6_vus_gauge,testrun)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "testrun", + "options": [], + "query": { + "query": "label_values(k6_vus_gauge,testrun)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "2023-12-12T08:47:21.284Z", + "to": "2023-12-12T09:00:09.102Z" + }, + "timepicker": {}, + "timezone": "", + "title": "Neonlabs performance test", + "uid": "-PUaX7vIk", + "version": 27, + "weekStart": "" + } \ No newline at end of file diff --git a/loadtesting/k6/monitoring/.docker/prometheus.yml b/loadtesting/k6/monitoring/.docker/prometheus.yml new file mode 100644 index 0000000000..451ed70ae3 --- /dev/null +++ b/loadtesting/k6/monitoring/.docker/prometheus.yml @@ -0,0 +1,10 @@ +global: + scrape_interval: 10s + +scrape_configs: + - job_name: 'vmagent' + static_configs: + - targets: ['vmagent:8429'] + - job_name: 'victoriametrics' + static_configs: + - targets: ['victoriametrics:8428'] diff --git a/loadtesting/k6/monitoring/.docker/provisioning/dashboards/dashboard.yml b/loadtesting/k6/monitoring/.docker/provisioning/dashboards/dashboard.yml new file mode 100644 index 0000000000..d5eb4b71a5 --- /dev/null +++ b/loadtesting/k6/monitoring/.docker/provisioning/dashboards/dashboard.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +providers: + - name: Prometheus + orgId: 1 + folder: '' + type: file + options: + path: /var/lib/grafana/dashboards diff --git a/loadtesting/k6/monitoring/.docker/provisioning/datasources/datasource.yml b/loadtesting/k6/monitoring/.docker/provisioning/datasources/datasource.yml new file mode 100644 index 0000000000..ecf27a8b83 --- /dev/null +++ b/loadtesting/k6/monitoring/.docker/provisioning/datasources/datasource.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: VictoriaMetrics + type: prometheus + access: proxy + url: http://victoriametrics:8428 + isDefault: true \ No newline at end of file diff --git a/loadtesting/k6/monitoring/.docker/telegraf.conf b/loadtesting/k6/monitoring/.docker/telegraf.conf new file mode 100644 index 0000000000..d445ba02e4 --- /dev/null +++ b/loadtesting/k6/monitoring/.docker/telegraf.conf @@ -0,0 +1,61 @@ +# Telegraf Configuration +# +# Telegraf is entirely plugin driven. All metrics are gathered from the +# declared inputs, and sent to the declared outputs. +# +# Plugins must be declared in here to be active. +# To deactivate a plugin, comment out the name and any variables. +# +# Use 'telegraf -config telegraf.conf -test' to see what metrics a config +# file would generate. +# +# Environment variables can be used anywhere in this config file, simply surround +# them with ${}. For strings the variable must be within quotes (ie, "${STR_VAR}"), +# for numbers and booleans they should be plain (ie, ${INT_VAR}, ${BOOL_VAR}) + + +# Global tags can be specified here in key="value" format. +[global_tags] + ## dc = "us-east-1" # will tag all metrics with dc=us-east-1 + ## rack = "1a" + ## Environment variables can be used as tags, and throughout the config file + ## user = "$USER" + + +# Configuration for telegraf agent +[agent] + ## Default data collection interval for all inputs + interval = "5s" + ## Rounds collection interva to 'interval' + ## ie, if interval="10s" then always collect on :00, :10, :20, etc. + round_interval = true + + metric_batch_size = 4000 + + metric_buffer_limit = 100000 + + collection_jitter = "0s" + + flush_interval = "4s" + + flush_jitter = "0s" + + precision = "0s" + + hostname = "" + omit_hostname = false + + +## MY CONFIG + +## input + +[[inputs.prometheus]] + ## An array of urls to scrape metrics from. + ## http://localhost:5656 - is default for k6 plugin 'xk6-prometheus' + urls = ["http://host.docker.internal:5656"] + +## output + +[[outputs.influxdb]] + urls = ["http://victoriametrics:8428"] diff --git a/loadtesting/k6/monitoring/compose.yml b/loadtesting/k6/monitoring/compose.yml new file mode 100644 index 0000000000..879e6a160b --- /dev/null +++ b/loadtesting/k6/monitoring/compose.yml @@ -0,0 +1,65 @@ +version: '3.5' +services: + + vmagent: + container_name: vmagent + image: victoriametrics/vmagent:v1.87.0 + depends_on: + - 'victoriametrics' + ports: + - 8429:8429 + volumes: + - vmagentdata:/vmagentdata + - ./.docker/prometheus.yml:/etc/prometheus/prometheus.yml + command: + - '--promscrape.config=/etc/prometheus/prometheus.yml' + - '--remoteWrite.url=http://victoriametrics:8428/api/v1/write' + networks: + - neonlabs + restart: always + + victoriametrics: + container_name: victoriametrics + image: victoriametrics/victoria-metrics:v1.87.0 + ports: + - 8428:8428 + - 8089:8089 + - 8089:8089/udp + - 2003:2003 + - 2003:2003/udp + - 4242:4242 + volumes: + - vmdata:/storage + command: + - '--storageDataPath=/storage' + - '--graphiteListenAddr=:2003' + - '--opentsdbListenAddr=:4242' + - '--httpListenAddr=:8428' + - '--influxListenAddr=:8089' + networks: + - neonlabs + restart: always + + grafana: + container_name: grafana + image: grafana/grafana:9.2.7 + depends_on: + - 'victoriametrics' + ports: + - 3000:3000 + volumes: + - grafanadata:/var/lib/grafana + - ./.docker/provisioning/:/etc/grafana/provisioning/ + - ./.docker/dashboards/:/var/lib/grafana/dashboards/ + networks: + - neonlabs + restart: always + +volumes: + vmagentdata: {} + vmdata: {} + grafanadata: {} + +networks: + neonlabs: + driver: bridge \ No newline at end of file diff --git a/loadtesting/k6/monitoring/telegraf.conf b/loadtesting/k6/monitoring/telegraf.conf new file mode 100644 index 0000000000..d9670c953f --- /dev/null +++ b/loadtesting/k6/monitoring/telegraf.conf @@ -0,0 +1,71 @@ +# Telegraf Configuration +# +# Telegraf is entirely plugin driven. All metrics are gathered from the +# declared inputs, and sent to the declared outputs. +# +# Plugins must be declared in here to be active. +# To deactivate a plugin, comment out the name and any variables. +# +# Use 'telegraf -config telegraf.conf -test' to see what metrics a config +# file would generate. +# +# Environment variables can be used anywhere in this config file, simply surround +# them with ${}. For strings the variable must be within quotes (ie, "${STR_VAR}"), +# for numbers and booleans they should be plain (ie, ${INT_VAR}, ${BOOL_VAR}) + + +# Global tags can be specified here in key="value" format. +[global_tags] + ## dc = "us-east-1" # will tag all metrics with dc=us-east-1 + ## rack = "1a" + ## Environment variables can be used as tags, and throughout the config file + ## user = "$USER" + + +# Configuration for telegraf agent +[agent] + ## Default data collection interval for all inputs + interval = "5s" + ## Rounds collection interva to 'interval' + ## ie, if interval="10s" then always collect on :00, :10, :20, etc. + round_interval = true + + metric_batch_size = 4000 + + metric_buffer_limit = 100000 + + collection_jitter = "0s" + + flush_interval = "4s" + + flush_jitter = "0s" + + precision = "0s" + + hostname = "" + omit_hostname = false + + +## MY CONFIG + +## input + +[[inputs.prometheus]] + ## An array of urls to scrape metrics from. + ## http://localhost:5656 - is default for k6 plugin 'xk6-prometheus' + urls = ["http://localhost:5656"] + +## output + +# [[outputs.http]] +# ## URL is the address to send metrics to (should end with '/api/prom/push') +# url = "${PROM_URL}" + +# ## Data format to output. +# data_format = "prometheusremotewrite" + +# username = "${PROM_USERNAME}" +# password = "${PROM_PASSWORD}" + +[[outputs.influxdb]] + urls = ["http://localhost:8428"] \ No newline at end of file diff --git a/loadtesting/k6/options/options.js b/loadtesting/k6/options/options.js new file mode 100644 index 0000000000..187362b106 --- /dev/null +++ b/loadtesting/k6/options/options.js @@ -0,0 +1,16 @@ +import { usersNumber } from "../tests/utils/consts.js"; + +export const sendNeonOptions = { + scenarios: { + sendNeon: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '30s', target: usersNumber }, + { duration: '1200s', target: usersNumber }, + ], + gracefulRampDown: '30s', + }, + }, + noConnectionReuse: true, +}; \ No newline at end of file diff --git a/loadtesting/k6/readme.md b/loadtesting/k6/readme.md new file mode 100644 index 0000000000..42812b8bee --- /dev/null +++ b/loadtesting/k6/readme.md @@ -0,0 +1,92 @@ +# How to build a k6 bin + +Make sure your Golang version >= 1.19. + +Make sure you have Golang bin path in PATH, otherwise +```bash +export PATH=$PATH:$(go env GOPATH)/bin +``` + + +### Run performance test using clickfile: +Install xk6 and build an executable file, tag is a neonlabsorg forked xk6-ethereum plugin tag, it can be a fixed version of a forked xk6-ethereum plugin or commit sha (ex. ```05e0ce5```) +```bash +./clickfile.py k6 build --tag 05e0ce5 +``` + +Run load scenario: +```bash +./clickfile.py k6 run --network local --script ./loadtesting/k6/tests/sendNeon.test.js --users 100 --balance 200 +``` + +It is mandatory either set up `K6_USERS_NUMBER` and `K6_INITIAL_BALANCE` envs or pass `--users` and `--balance` flags to run the command. We need these values to prepare test accounts for further transfers transactions. + +### Native commands to build and run k6: +Install k6 +```bash +go install go.k6.io/xk6/cmd/xk6@latest +``` +Build a k6 binary file with the following command, BUILD_TAG can be a fixed version of a forked xk6-ethereum plugin or commit sha (ex. ```05e0ce5```) + ```bash +xk6 build --with github.com/szkiba/xk6-prometheus --with github.com/neonlabsorg/xk6-ethereum@${BUILD_TAG} +``` +where: + +- github.com/szkiba/xk6-prometheus - a plugin for working with metrics + +- neonlabsorg/xk6-ethereum - the ethereum plugin forked by neonlabsorg + +Run test scenario: +```bash +./k6 run -e K6_USERS_NUMBER=100 -e K6_INITIAL_BALANCE=200 ./loadtesting/k6/tests/sendNeon.test.js +``` + +### Local test run with local version of the xk6-ethereum plugin +It is common approach to do some changes in the plugin and test it locally before pushing changes to github. +Pass the xk6-ethereum plugin repository path (on your local machine) as a parameter to the build command: +```bash +xk6 build --with github.com/szkiba/xk6-prometheus --with github.com/neonlabsorg/xk6-ethereum="" +``` +Use an executable file builded with command above to run test scenario (see 'Run performance test using clickfile' or 'Native commands to build and run k6' sections). + +### Run infrastructure +go to the monitoring folder and run +```bash +docker-compose -f compose.yml up -d --build +``` +It is needed to include telegraf tool in the infrastructure. Install it if do not have one (```brew install telegraf``` or similar for your system) + +run telegraf with config +```bash +telegraf --config telegraf.conf +``` +### Monitoring +Run test scenario and go to the default grafana host/port ```localhost:3000```. Open dashboard: ``` Neonlabs performance test```. + +## Scenario options +Send Neon scenario settings: +```js +export const sendNeonOptions = { + scenarios: { + sendNeon: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '30s', target: usersNumber }, + { duration: '1200s', target: usersNumber }, + ], + gracefulRampDown: '30s', + }, + }, + noConnectionReuse: true, +}; +``` +```executor: 'ramping-vus'``` - VUs execute as many iterations as possible for a specified amount of time + +```startVUs: 0``` - number of VUs to start the run with + +```stages``` - an array of objects that specify the target number of VUs to ramp up or down to, in our case: number of VUs is increased from 0 to `usersNumber` value during 30 seconds, then `usersNumber` VUs execute the scenario during 1200 seconds + +```gracefulRampDown: '30s'``` - time to wait for an already started iteration to finish before stopping it during a ramp down + +```noConnectionReuse: true``` - determines whether a connection is reused throughout different actions of the same virtual user and in the same iteration \ No newline at end of file diff --git a/loadtesting/k6/scenarios/sendNeon.js b/loadtesting/k6/scenarios/sendNeon.js new file mode 100644 index 0000000000..e4ea5bebc5 --- /dev/null +++ b/loadtesting/k6/scenarios/sendNeon.js @@ -0,0 +1,28 @@ +import { default as sendNeonTest } from '../tests/sendNeon.test.js'; + +// TODO: we can combine multiple scenarios here +export const options = { + scenarios: { + scriptSendNeonScenario: { + exec: 'sendNeon', + executor: 'constant-vus', + vus: 100, + duration: '1m', + }, + // scriptSecondScenario: { + // exec: 'mySecondScenario', + // executor: 'constant-vus', + // vus, + // duration, + // }, + }, +}; + +export function sendNeon() { + sendNeonTest(); +} + +// the second scenario can be added here +// export function secondScenario() { +// secondScenario(); +//} \ No newline at end of file diff --git a/loadtesting/k6/tests/sendNeon.test.js b/loadtesting/k6/tests/sendNeon.test.js new file mode 100644 index 0000000000..70bfc09e75 --- /dev/null +++ b/loadtesting/k6/tests/sendNeon.test.js @@ -0,0 +1,57 @@ +import { randomItem } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; +import { ethClient, sendNeon } from './utils/ethClient.js'; +import { sendNeonOptions } from '../options/options.js'; +import { Trend, Counter } from 'k6/metrics'; +import { SharedArray } from 'k6/data'; +import exec from 'k6/execution'; +import { check } from 'k6'; + +const sendNeonRequests = new Counter('send_neon_requests'); +const sendNeonErrorCounter = new Counter('send_neon_errors'); +const sendNeonRequestTime = new Trend('send_neon_request_time', true); + +export const options = sendNeonOptions; + +const usersArray = new SharedArray('Users accounts', function () { + const accounts = JSON.parse(open("../data/accounts.json")); + let data = []; + for (let i = 0; i < Object.keys(accounts).length; i++) { + data[i] = accounts[i]; + } + return data; +}); + +const transferAmountRange = [0.01, 0.02, 0.03, 0.04, 0.05]; + +export default function sendNeonTest() { + const vuID = exec.vu.idInTest + const index = vuID % usersArray.length; + + const accountSenderAddress = usersArray[index].sender_address; + const accountSenderPrivateKey = usersArray[index].sender_key; + const accountReceiverAddress = usersArray[index].receiver_address; + + const client = ethClient(accountSenderPrivateKey); + + const startTime = new Date(); + + try { + let receipt = sendNeon( + client, + accountSenderAddress, + accountReceiverAddress, + randomItem(transferAmountRange), + null, + null, + null + ); + check(receipt, { + 'receipt status is 1': (r) => r.status === 1, + }); + } catch (e) { + console.log('Error sendNeon: ' + e); + sendNeonErrorCounter.add(1); + } + sendNeonRequestTime.add(new Date() - startTime); + sendNeonRequests.add(1); +} diff --git a/loadtesting/k6/tests/utils/consts.js b/loadtesting/k6/tests/utils/consts.js new file mode 100644 index 0000000000..047e9bfe47 --- /dev/null +++ b/loadtesting/k6/tests/utils/consts.js @@ -0,0 +1,28 @@ +// Parse envs.json +const network = __ENV.K6_NETWORK; +const envConfig = JSON.parse(open("../../../../envs.json")); +if (network == undefined) { + throw new Error("Network is not defined, please set K6_NETWORK env variable."); +} + +let env; +switch (network) { + case "local": + env = envConfig.local; + break; + case "devnet": + env = envConfig.devnet; + break; + default: + env = envConfig.local; +} + +// Set Chain Id +export const networkId = parseInt(env.network_ids.neon); + +// Set Proxy URL +export const proxyUrl = env.proxy_url; + +// Accounts data +export const initialAccountBalance = parseInt(__ENV.K6_INITIAL_BALANCE); +export const usersNumber = parseInt(__ENV.K6_USERS_NUMBER); diff --git a/loadtesting/k6/tests/utils/ethClient.js b/loadtesting/k6/tests/utils/ethClient.js new file mode 100644 index 0000000000..86f5e5f742 --- /dev/null +++ b/loadtesting/k6/tests/utils/ethClient.js @@ -0,0 +1,46 @@ +import eth from 'k6/x/ethereum'; +import { proxyUrl, networkId } from './consts.js'; + + +export function ethClient(privateKey) { + const client = new eth.Client({ + url: proxyUrl, + chainID: networkId, + privateKey: privateKey, + }); + return client; +} + +export function sendNeon(client, from, to, amount, gas, gasPrice, nonce) { + const value = amount; + return sendTokens(client, from, to, value, gas, gasPrice, nonce) +} + +export function sendTokens(client, from, to, value, gas, gasPrice, nonce) { + if (nonce == null) { + nonce = client.getNonce(from); + } + + if (gasPrice == null) { + gasPrice = client.gasPrice(); + } + + let transaction = { + "from": from, + "to": to, + "value": value, + "gas_price": gasPrice, + "gas": gas, + "nonce": nonce, + "chain_id": client.chainID, + }; + + if (gas == null) { + transaction["gas"] = client.estimateGas(transaction); + } + + const txh = client.sendRawTransaction(transaction); + const receipt = client.waitForTransactionReceipt(txh); + + return receipt; +} diff --git a/loadtesting/k6/tests/utils/helpers.js b/loadtesting/k6/tests/utils/helpers.js new file mode 100644 index 0000000000..5451ee5ce3 --- /dev/null +++ b/loadtesting/k6/tests/utils/helpers.js @@ -0,0 +1,18 @@ +export async function waiter({ func, args, value, retries = 10 }) { + return smartWaiter({ func, args, check: (r) => r == value, retries: retries }); +} + +export async function smartWaiter({ func, args, check, retries = 10 }) { + let result; + let counter = 0; + while (!check(result) && counter < retries) { + await new Promise(r => setTimeout(r, 500)); + if (args != undefined) + result = await func(...args); + else { + result = await func(); + } + counter++; + } + return result; +} \ No newline at end of file diff --git a/loadtesting/proxy/common/base.py b/loadtesting/proxy/common/base.py index 7c2d1fea2c..bd2fac2194 100644 --- a/loadtesting/proxy/common/base.py +++ b/loadtesting/proxy/common/base.py @@ -9,8 +9,9 @@ import web3.types import requests import gevent +from eth_account.signers.local import LocalAccount from gevent.pool import Pool -from locust import TaskSet, events +from locust import TaskSet, events, env from utils import helpers from utils.faucet import Faucet @@ -24,7 +25,7 @@ @events.test_stop.add_listener -def save_transactions_list(environment: "locust.env.Environment", **kwargs): +def save_transactions_list(environment: env.Environment, **kwargs): if "SAVE_TRANSACTIONS" in os.environ: web3_client = NeonWeb3ClientExt( environment.credentials["proxy_url"]) @@ -81,7 +82,7 @@ class NeonProxyTasksSet(TaskSet): """Implements base initialization, creates data requirements and helpers""" faucet: tp.Optional[Faucet] = None - account: tp.Optional["eth_account.signers.local.LocalAccount"] = None + account: tp.Optional[LocalAccount] = None web3_client: tp.Optional[NeonWeb3ClientExt] = None def setup(self) -> None: @@ -137,7 +138,7 @@ def deploy_contract( self, name: str, version: str, - account: "eth_account.signers.local.LocalAccount", + account: LocalAccount, constructor_args: tp.Optional[tp.Any] = None, gas: tp.Optional[int] = 0, contract_name: tp.Optional[str] = None, diff --git a/loadtesting/proxy/tests/erc20spl.py b/loadtesting/proxy/tests/erc20spl.py index 11e7fb5ed8..5f399a2937 100644 --- a/loadtesting/proxy/tests/erc20spl.py +++ b/loadtesting/proxy/tests/erc20spl.py @@ -2,8 +2,8 @@ import random import string -from locust import User, events, tag, task -from solana.keypair import Keypair +from locust import User, events, tag, task, env +from solders.keypair import Keypair from loadtesting.proxy.common.base import NeonProxyTasksSet from utils.erc20wrapper import ERC20Wrapper @@ -14,7 +14,7 @@ @events.test_start.add_listener -def prepare_one_contract_for_erc20(environment: "locust.env.Environment", **kwargs): +def prepare_one_contract_for_erc20(environment: env.Environment, **kwargs): neon_client = NeonChainWeb3Client(environment.credentials["proxy_url"]) faucet = Faucet(environment.credentials["faucet_url"], neon_client) @@ -31,7 +31,7 @@ def prepare_one_contract_for_erc20(environment: "locust.env.Environment", **kwar name, symbol, None, - solana_account=Keypair.generate(), + solana_account=Keypair(), account=eth_account, mintable=True, ) diff --git a/loadtesting/proxy/tests/swaps.py b/loadtesting/proxy/tests/swaps.py index e7e1927fcb..84666017d4 100644 --- a/loadtesting/proxy/tests/swaps.py +++ b/loadtesting/proxy/tests/swaps.py @@ -1,16 +1,14 @@ import os -import copy import pathlib import shutil import subprocess import logging import time import json -import random import web3 -from locust import tag, task, User, events -from solana.keypair import Keypair +from locust import task, User, events, env +from solders.keypair import Keypair from utils.web3client import NeonChainWeb3Client from utils.solana_client import SolanaClient @@ -18,7 +16,6 @@ from utils import helpers from utils.erc20wrapper import ERC20Wrapper -from loadtesting.proxy.common import env LOG = logging.getLogger(__name__) @@ -29,7 +26,7 @@ @events.test_start.add_listener -def deploy_uniswap_contracts(environment: "locust.env.Environment", **kwargs): +def deploy_uniswap_contracts(environment: env.Environment, **kwargs): """ Deploy next pairs: 1. wNEON -> USDC @@ -92,7 +89,7 @@ def deploy_uniswap_contracts(environment: "locust.env.Environment", **kwargs): if "token_contracts" in data and data["token_contracts"]: LOG.info("Load token contracts from saved data") - solana_account = Keypair.from_secret_key(bytes.fromhex(data["solana_account"])) + solana_account = Keypair.from_bytes(bytes.fromhex(data["solana_account"])) token_contracts["wNEON"] = neon_client.get_deployed_contract( data["token_contracts"]["wNEON"], "common/WNeon.sol", "WNEON", "0.4.26" ) @@ -150,7 +147,7 @@ def deploy_uniswap_contracts(environment: "locust.env.Environment", **kwargs): token_contracts[token] = erc_contract LOG.info("Deploy SPL tokens") - solana_account = Keypair.generate() + solana_account = Keypair() for token in ("USDC_SPL", "USDT_SPL"): name = f"Test {token}" @@ -177,8 +174,6 @@ def deploy_uniswap_contracts(environment: "locust.env.Environment", **kwargs): contract_name="UniswapV2Factory" ) else: - - uniswap2_factory, _ = neon_client.deploy_and_get_contract( str(uniswap_path / "contracts/v2-core/UniswapV2Factory.sol"), account=eth_account, @@ -284,13 +279,13 @@ def deploy_uniswap_contracts(environment: "locust.env.Environment", **kwargs): token_contracts[token2].contract.address if "SPL" in token2 else token_contracts[token2].address ) - token1_amount = web3.Web3.to_wei(2000, "ether") - token2_amount = web3.Web3.to_wei(2000, "ether") + token1_amount = web3.Web3.to_wei(20, "ether") + token2_amount = web3.Web3.to_wei(20, "ether") if "SPL" in token1: - token1_amount = web3.Web3.to_wei(2000, "gwei") + token1_amount = web3.Web3.to_wei(20, "gwei") if "SPL" in token2: - token2_amount = web3.Web3.to_wei(2000, "gwei") + token2_amount = web3.Web3.to_wei(20, "gwei") for t in (token1, token2): c = token_contracts[t] @@ -332,7 +327,7 @@ def deploy_uniswap_contracts(environment: "locust.env.Environment", **kwargs): with open("uniswap_contracts.json", "w") as f: saved_data = { "signer": eth_account.key.hex(), - "solana_account": solana_account.secret_key.hex(), + "solana_account": solana_account.secret().hex(), "router": uniswap2_router.address, "factory": uniswap2_factory.address, "token_contracts": {k: v.address for k, v in token_contracts.items()}, @@ -340,7 +335,7 @@ def deploy_uniswap_contracts(environment: "locust.env.Environment", **kwargs): } json.dump(saved_data, f, indent=4) LOG.info(f"Signer user: {eth_account.address}") - LOG.info(f"Solana account: {solana_account.public_key}") + LOG.info(f"Solana account: {solana_account.pubkey()}") environment.uniswap = data users = {i: neon_client.eth.account.from_key(key) for i, key in data.get("users", {}).items()} @@ -348,7 +343,7 @@ def deploy_uniswap_contracts(environment: "locust.env.Environment", **kwargs): @events.test_stopping.add_listener -def save_users(environment: "locust.env.Environment", **kwargs): +def save_users(environment: env.Environment, **kwargs): if environment.parsed_options.exclude_tags and "uniswap" in environment.parsed_options.exclude_tags: return diff --git a/loadtesting/proxy/tests/withdraw.py b/loadtesting/proxy/tests/withdraw.py index 2d34e6b5b7..2146587733 100644 --- a/loadtesting/proxy/tests/withdraw.py +++ b/loadtesting/proxy/tests/withdraw.py @@ -1,3 +1,9 @@ +from locust import tag, task +from solders.keypair import Keypair + +from loadtesting.proxy.common.base import NeonProxyTasksSet +from loadtesting.proxy.common.events import execute_before + @tag("withdraw") class WithDrawTasksSet(NeonProxyTasksSet): @@ -10,14 +16,14 @@ class WithDrawTasksSet(NeonProxyTasksSet): @execute_before("task_block_number", "task_keeps_balance") def task_withdraw_tokens(self) -> None: """withdraw Ethereum tokens to Solana""" - keys = Keypair.generate() + keys = Keypair() contract_interface = self._compile_contract_interface(self.contract_name, self.version) erc20wrapper_address = self.credentials.get("neon_erc20wrapper_address") if erc20wrapper_address: self.log.info(f"withdraw tokens to Solana from {self.account.address[:8]}") contract = self.web3_client.eth.contract(address=erc20wrapper_address, abi=contract_interface["abi"]) amount = self.web3_client._web3.to_wei(1, "ether") - instruction_tx = contract.functions.withdraw(bytes(keys.public_key)).build_transaction( + instruction_tx = contract.functions.withdraw(bytes(keys.pubkey())).build_transaction( { "from": self.account.address, "nonce": self.web3_client.eth.get_transaction_count(self.account.address), diff --git a/pyproject.toml b/pyproject.toml index 8afce34908..a834faad1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,10 @@ markers = [ "slow: these tests are slow and should be run separately", "proxy_version(version): thee tests work only on specified proxy version and all the following higher versions", "bug: mark for tests containing bugs which need to be fixed or refactored", - "neon_only: tests for Neon functionality" + "neon_only: tests for Neon functionality", + "need_eip1559_blocks: values passed to this marker will be used by fixture eip1559_setup", + "execute_in_the_end_of_session: moves the test to the end of the session", + "cost_report: enables Cost Report data collection for this test" ] asyncio_mode = "auto" addopts = "--alluredir=allure-results" diff --git a/scripts/check_lost_trx.py b/scripts/check_lost_trx.py index 0a2215f0ac..006bb67dcc 100755 --- a/scripts/check_lost_trx.py +++ b/scripts/check_lost_trx.py @@ -1,7 +1,6 @@ import itertools import json import pathlib -import glob from concurrent.futures import ThreadPoolExecutor from solana.transaction import Signature diff --git a/scripts/eclipce.py b/scripts/eclipce.py index 1932836b28..a0ce53693d 100644 --- a/scripts/eclipce.py +++ b/scripts/eclipce.py @@ -5,15 +5,15 @@ import typing as tp import solana.rpc.api -from solana.keypair import Keypair -from solana.publickey import PublicKey +from solders.keypair import Keypair +from solders.pubkey import Pubkey from solana.rpc.commitment import Finalized from solana.rpc.types import TxOpts -from solana.transaction import Transaction, AccountMeta, TransactionInstruction +from solana.transaction import Transaction, AccountMeta, Instruction from solders.rpc.errors import InternalErrorMessage from solders.rpc.responses import RequestAirdropResp from spl.token.instructions import get_associated_token_address, create_associated_token_account, approve, ApproveParams -import solana.system_program as sp +import solders.system_program as sp from spl.token.constants import TOKEN_PROGRAM_ID @@ -23,9 +23,9 @@ PROXY_URL = "http://localhost:9090/solana" SOLANA_URL = "http://localhost:8899/" -LOADER_ID = PublicKey("6HALMT8ZvZz3p9zigdUdn7bZ4ae3nrSSNNDV8qRZm22q") +LOADER_ID = Pubkey.from_string("6HALMT8ZvZz3p9zigdUdn7bZ4ae3nrSSNNDV8qRZm22q") AMOUNT = 1_000_000_000 -MINT_PUBKEY = PublicKey("So11111111111111111111111111111111111111112") +MINT_PUBKEY = Pubkey.from_string("So11111111111111111111111111111111111111112") RECIPIENT = "" # New account will be created if empty @@ -55,7 +55,7 @@ def __init__(self, endpoint, account_seed_version="\3"): bytes(account_seed_version, encoding="utf-8").decode("unicode-escape").encode("utf-8") ) - def request_airdrop(self, pubkey: PublicKey, lamports: int) -> RequestAirdropResp: + def request_airdrop(self, pubkey: Pubkey, lamports: int) -> RequestAirdropResp: airdrop_resp = None for _ in range(5): airdrop_resp = super().request_airdrop(pubkey, lamports, commitment=Finalized) @@ -83,31 +83,31 @@ def send_tx_and_check_status_ok(self, tx, *signers): sig_status = json.loads((self.confirm_transaction(sig)).to_json()) assert sig_status["result"]["value"][0]["status"] == {"Ok": None}, f"error:{sig_status}" - def create_ata(self, solana_account, token_mint): + def create_ata(self, solana_account: Keypair, token_mint): trx = Transaction() - trx.add(create_associated_token_account(solana_account.public_key, solana_account.public_key, token_mint)) + trx.add(create_associated_token_account(solana_account.pubkey(), solana_account.pubkey(), token_mint)) opts = TxOpts(skip_preflight=True, skip_confirmation=False) self.send_transaction(trx, solana_account, opts=opts) def ether2program(self, ether: tp.Union[str, bytes]) -> tp.Tuple[str, int]: - items = PublicKey.find_program_address([self.account_seed_version, self.ether2bytes(ether)], LOADER_ID) + items = Pubkey.find_program_address([self.account_seed_version, self.ether2bytes(ether)], LOADER_ID) return str(items[0]), items[1] - def sent_token_from_solana_to_neon(self, solana_account, mint, neon_account, amount): - contract_pubkey = PublicKey(self.ether2program(neon_account)[0]) - associated_token_address = get_associated_token_address(solana_account.public_key, mint) - authority_pool = PublicKey.find_program_address([b"Deposit"], LOADER_ID)[0] + def sent_token_from_solana_to_neon(self, solana_account: Keypair, mint, neon_account, amount): + contract_pubkey = Pubkey.from_string(self.ether2program(neon_account)[0]) + associated_token_address = get_associated_token_address(solana_account.pubkey(), mint) + authority_pool = Pubkey.find_program_address([b"Deposit"], LOADER_ID)[0] pool = get_associated_token_address(authority_pool, mint) - tx = Transaction(fee_payer=solana_account.public_key) + tx = Transaction(fee_payer=solana_account.pubkey()) tx.add( approve( ApproveParams( program_id=TOKEN_PROGRAM_ID, source=associated_token_address, delegate=contract_pubkey, - owner=solana_account.public_key, + owner=solana_account.pubkey(), amount=amount, ) ) @@ -120,7 +120,7 @@ def sent_token_from_solana_to_neon(self, solana_account, mint, neon_account, amo associated_token_address, pool, TOKEN_PROGRAM_ID, - solana_account.public_key, + solana_account.pubkey(), LOADER_ID, ) ) @@ -129,10 +129,14 @@ def sent_token_from_solana_to_neon(self, solana_account, mint, neon_account, amo def make_wsol(amount, solana_wallet, associated_address): tx = Transaction(fee_payer=solana_wallet) - tx.add(sp.transfer(sp.TransferParams(solana_wallet, associated_address, amount))) - - sync_native_instr = TransactionInstruction( - keys=[AccountMeta(pubkey=associated_address, is_signer=False, is_writable=True)], + tx.add(sp.transfer(sp.TransferParams( + from_pubkey=solana_wallet, + to_pubkey=associated_address, + lamports=amount), + )) + + sync_native_instr = Instruction( + accounts=[AccountMeta(pubkey=associated_address, is_signer=False, is_writable=True)], program_id=TOKEN_PROGRAM_ID, data=bytes.fromhex("11"), ) @@ -142,13 +146,13 @@ def make_wsol(amount, solana_wallet, associated_address): def make_deposit( ether_address: bytes, - solana_account: PublicKey, - source: PublicKey, - pool: PublicKey, - token_program: PublicKey, - operator_pubkey: PublicKey, - evm_loader_id: PublicKey, -) -> TransactionInstruction: + solana_account: Pubkey, + source: Pubkey, + pool: Pubkey, + token_program: Pubkey, + operator_pubkey: Pubkey, + evm_loader_id: Pubkey, +) -> Instruction: data = bytes.fromhex("27") + ether_address accounts = [ @@ -157,10 +161,10 @@ def make_deposit( AccountMeta(pubkey=solana_account, is_signer=False, is_writable=True), AccountMeta(pubkey=token_program, is_signer=False, is_writable=False), AccountMeta(pubkey=operator_pubkey, is_signer=True, is_writable=True), - AccountMeta(pubkey=sp.SYS_PROGRAM_ID, is_signer=False, is_writable=False), + AccountMeta(pubkey=sp.ID, is_signer=False, is_writable=False), ] - return TransactionInstruction(program_id=evm_loader_id, data=data, keys=accounts) + return Instruction(program_id=evm_loader_id, data=data, accounts=accounts) solana_client = SolanaClient(endpoint=SOLANA_URL) @@ -174,20 +178,20 @@ def make_deposit( recipient = RECIPIENT -sol_account = Keypair.generate() -solana_client.request_airdrop(sol_account.public_key, 10 * AMOUNT) +sol_account = Keypair() +solana_client.request_airdrop(sol_account.pubkey(), 10 * AMOUNT) # with open("sol_account_with_tokens-keypair.json", "r") as key: # secret_key = json.load(key)[:32] # sol_account = Keypair.from_secret_key(secret_key) -ata_address = get_associated_token_address(sol_account.public_key, MINT_PUBKEY) +ata_address = get_associated_token_address(sol_account.pubkey(), MINT_PUBKEY) solana_client.create_ata(sol_account, MINT_PUBKEY) # wrap SOL -wrap_sol_tx = make_wsol(AMOUNT, sol_account.public_key, ata_address) +wrap_sol_tx = make_wsol(AMOUNT, sol_account.pubkey(), ata_address) solana_client.send_tx_and_check_status_ok(wrap_sol_tx, sol_account) solana_client.sent_token_from_solana_to_neon(sol_account, MINT_PUBKEY, recipient, AMOUNT) -print(f"Final balance: {web3_client.eth.get_balance(recipient)}") \ No newline at end of file +print(f"Final balance: {web3_client.eth.get_balance(recipient)}") diff --git a/utils/accounts.py b/utils/accounts.py index 56563f245f..76c7fecf5b 100644 --- a/utils/accounts.py +++ b/utils/accounts.py @@ -1,4 +1,5 @@ import allure +from eth_account.signers.local import LocalAccount from utils.consts import InputTestConstants from .web3client import NeonChainWeb3Client @@ -9,10 +10,10 @@ def __init__(self, web3_client: NeonChainWeb3Client, faucet, eth_bank_account): self._web3_client = web3_client self._faucet = faucet self._bank_account = eth_bank_account - self._accounts = [] + self._accounts: list[LocalAccount] = [] self.accounts_collector = [] - def __getitem__(self, item): + def __getitem__(self, item) -> LocalAccount: if len(self._accounts) < (item + 1): for _ in range(item + 1 - len(self._accounts)): with allure.step("Create new account with default balance"): diff --git a/utils/consts.py b/utils/consts.py index 290f388394..2552d91b8c 100644 --- a/utils/consts.py +++ b/utils/consts.py @@ -1,6 +1,6 @@ from enum import Enum -from solana.publickey import PublicKey +from solders.pubkey import Pubkey OPERATOR_KEYPAIR_PATH = "deploy/operator-keypairs" LAMPORT_PER_SOL = 1_000_000_000 @@ -10,12 +10,13 @@ MAX_UINT_256 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF MAX_UINT_64 = 2 ** 64 -COMPUTE_BUDGET_ID: PublicKey = PublicKey("ComputeBudget111111111111111111111111111111") -MEMO_PROGRAM_ID: PublicKey = PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") -SOLANA_CALL_PRECOMPILED_ID: PublicKey = PublicKey("83fAnx3LLG612mHbEh4HzXEpYwvSB5fqpwUS3sZkRuUB") -COUNTER_ID: PublicKey = PublicKey("4RJAXLPq1HrXWP4zFrMhvB5drrzqrRFwaRVNUnALcaeh") -TRANSFER_SOL_ID: PublicKey = PublicKey("6x9dAYQehxXLh16EHAKXevnQADTZPKP6ZT4t8BfNDxtB") -TRANSFER_TOKENS_ID: PublicKey = PublicKey("BFsGPJUwgE1rz4eoL322HaKZYNZ5wDLafwYtKwomv2XF") +COMPUTE_BUDGET_ID: Pubkey = Pubkey.from_string("ComputeBudget111111111111111111111111111111") +MEMO_PROGRAM_ID: Pubkey = Pubkey.from_string("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") +SOLANA_CALL_PRECOMPILED_ID: Pubkey = Pubkey.from_string("83fAnx3LLG612mHbEh4HzXEpYwvSB5fqpwUS3sZkRuUB") +COUNTER_ID: Pubkey = Pubkey.from_string("4RJAXLPq1HrXWP4zFrMhvB5drrzqrRFwaRVNUnALcaeh") +TRANSFER_SOL_ID: Pubkey = Pubkey.from_string("6x9dAYQehxXLh16EHAKXevnQADTZPKP6ZT4t8BfNDxtB") +TRANSFER_TOKENS_ID: Pubkey = Pubkey.from_string("BFsGPJUwgE1rz4eoL322HaKZYNZ5wDLafwYtKwomv2XF") +TEST_INVOKE_ID: Pubkey = Pubkey.from_string("8iqgWRzFNXmpUHLffB6uJD1ER3NLLq2ND3BioNgG9ZpZ") class Time: @@ -49,12 +50,12 @@ class InputTestConstants(Enum): wSOL = { "chain_id": 111, - "address_spl": PublicKey("So11111111111111111111111111111111111111112"), + "address_spl": Pubkey.from_string("So11111111111111111111111111111111111111112"), "address": "0x16869acc45BA20abEFB2DdE2096F66373fDe364F", "decimals": 9, "name": "Wrapped SOL", "symbol": "wSOL", - "logo_uri": "https://raw.githubusercontent.com/neonlabsorg/token-list/master/assets/solana-wsol-logo.svg", + "logo_uri": "", } MULTITOKEN_MINTS = { diff --git a/utils/erc20wrapper.py b/utils/erc20wrapper.py index a2f9595e80..89e77fef4d 100644 --- a/utils/erc20wrapper.py +++ b/utils/erc20wrapper.py @@ -1,9 +1,13 @@ +from spl.token.client import Token from eth_account.signers.local import LocalAccount from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts from solana.transaction import Transaction +from solders.keypair import Keypair +from solders.pubkey import Pubkey +from web3.types import TxReceipt -from . import web3client +from . import web3client, stats_collector from .metaplex import create_metadata_instruction_data, create_metadata_instruction INIT_TOKEN_AMOUNT = 1000000000000000 @@ -17,7 +21,7 @@ def __init__( name, symbol, sol_client, - solana_account, + solana_account: Keypair, decimals=9, evm_loader_id=None, account=None, @@ -45,6 +49,8 @@ def __init__( self.decimals = decimals self.sol_client = sol_client self.contract_address = contract_address + self.token_mint: Token + self.solana_associated_token_acc: Pubkey if not contract_address: self.contract_address = self.deploy_wrapper(mintable) @@ -139,10 +145,10 @@ def _prepare_spl_token(self): txn.add( create_metadata_instruction( metadata, - self.solana_acc.public_key, + self.solana_acc.pubkey(), self.token_mint.pubkey, - self.solana_acc.public_key, - self.solana_acc.public_key, + self.solana_acc.pubkey(), + self.solana_acc.pubkey(), ) ) self.sol_client.send_transaction( @@ -171,43 +177,48 @@ def deploy_wrapper(self, mintable: bool): return instruction_receipt # TODO: In all this methods verify if exist self.account - def mint_tokens(self, signer, to_address, amount: int = INIT_TOKEN_AMOUNT, gas_price=None, gas=None): + @stats_collector.cost_report_from_receipt + def mint_tokens(self, signer, to_address, amount: int = INIT_TOKEN_AMOUNT, gas_price=None, gas=None) -> TxReceipt: tx = self.web3_client.make_raw_tx(signer.address, gas_price=gas_price, gas=gas) instruction_tx = self.contract.functions.mint(to_address, amount).build_transaction(tx) resp = self.web3_client.send_transaction(signer, instruction_tx) return resp - def claim(self, signer, from_address, amount: int = INIT_TOKEN_AMOUNT, gas_price=None, gas=None): + @stats_collector.cost_report_from_receipt + def claim(self, signer, from_address, amount: int = INIT_TOKEN_AMOUNT, gas_price=None, gas=None) -> TxReceipt: tx = self.web3_client.make_raw_tx(signer.address, gas_price=gas_price, gas=gas) instruction_tx = self.contract.functions.claim(from_address, amount).build_transaction(tx) resp = self.web3_client.send_transaction(signer, instruction_tx) return resp - def claim_to(self, signer, from_address, to_address, amount, gas_price=None, gas=None): + def claim_to(self, signer, from_address, to_address, amount, gas_price=None, gas=None) -> TxReceipt: tx = self.web3_client.make_raw_tx(signer.address, gas_price=gas_price, gas=gas) instruction_tx = self.contract.functions.claimTo(from_address, to_address, amount).build_transaction(tx) resp = self.web3_client.send_transaction(signer, instruction_tx) return resp - def burn(self, signer, amount, gas_price=None, gas=None): + @stats_collector.cost_report_from_receipt + def burn(self, signer, amount, gas_price=None, gas=None) -> TxReceipt: tx = self.web3_client.make_raw_tx(signer.address, gas_price=gas_price, gas=gas) instruction_tx = self.contract.functions.burn(amount).build_transaction(tx) resp = self.web3_client.send_transaction(signer, instruction_tx) return resp - def burn_from(self, signer, from_address, amount, gas_price=None, gas=None): + def burn_from(self, signer, from_address, amount, gas_price=None, gas=None) -> TxReceipt: tx = self.web3_client.make_raw_tx(signer.address, gas_price=gas_price, gas=gas) instruction_tx = self.contract.functions.burnFrom(from_address, amount).build_transaction(tx) resp = self.web3_client.send_transaction(signer, instruction_tx) return resp - def approve(self, signer, spender_address, amount, gas_price=None, gas=None): + @stats_collector.cost_report_from_receipt + def approve(self, signer, spender_address, amount, gas_price=None, gas=None) -> TxReceipt: tx = self.web3_client.make_raw_tx(signer.address, gas_price=gas_price, gas=gas) instruction_tx = self.contract.functions.approve(spender_address, amount).build_transaction(tx) resp = self.web3_client.send_transaction(signer, instruction_tx) return resp - def transfer(self, signer, address_to, amount, gas_price=None, gas=None): + @stats_collector.cost_report_from_receipt + def transfer(self, signer, address_to, amount, gas_price=None, gas=None) -> TxReceipt: tx = self.web3_client.make_raw_tx(signer.address, gas_price=gas_price, gas=gas) if isinstance(address_to, LocalAccount): address_to = address_to.address @@ -215,19 +226,19 @@ def transfer(self, signer, address_to, amount, gas_price=None, gas=None): resp = self.web3_client.send_transaction(signer, instruction_tx) return resp - def transfer_from(self, signer, address_from, address_to, amount, gas_price=None, gas=None): + def transfer_from(self, signer, address_from, address_to, amount, gas_price=None, gas=None) -> TxReceipt: tx = self.web3_client.make_raw_tx(signer.address, gas_price=gas_price, gas=gas) instruction_tx = self.contract.functions.transferFrom(address_from, address_to, amount).build_transaction(tx) resp = self.web3_client.send_transaction(signer, instruction_tx) return resp - def transfer_solana(self, signer, address_to, amount, gas_price=None, gas=None): + def transfer_solana(self, signer, address_to, amount, gas_price=None, gas=None) -> TxReceipt: tx = self.web3_client.make_raw_tx(signer.address, gas_price=gas_price, gas=gas) instruction_tx = self.contract.functions.transferSolana(address_to, amount).build_transaction(tx) resp = self.web3_client.send_transaction(signer, instruction_tx) return resp - def approve_solana(self, signer, spender, amount, gas_price=None, gas=None): + def approve_solana(self, signer, spender, amount, gas_price=None, gas=None) -> TxReceipt: tx = self.web3_client.make_raw_tx(signer.address, gas_price=gas_price, gas=gas) instruction_tx = self.contract.functions.approveSolana(spender, amount).build_transaction(tx) resp = self.web3_client.send_transaction(signer, instruction_tx) diff --git a/utils/erc721ForMetaplex.py b/utils/erc721ForMetaplex.py index d18a305e1f..d4781088b9 100644 --- a/utils/erc721ForMetaplex.py +++ b/utils/erc721ForMetaplex.py @@ -1,8 +1,9 @@ import logging import allure +from web3.types import TxReceipt -from utils import web3client +from utils import web3client, stats_collector LOGGER = logging.getLogger(__name__) @@ -46,11 +47,13 @@ def deploy(self, contract, contract_name): return contract @allure.step("Mint") - def mint(self, seed, to_address, uri, gas_price=None, gas=None, signer=None): + @stats_collector.cost_report_from_receipt + def mint(self, seed, to_address, uri, gas_price=None, gas=None, signer=None) -> int: signer = self.account if signer is None else signer tx = self.make_tx_object(signer.address, gas_price, gas) instruction_tx = self.contract.functions.mint(seed, to_address, uri).build_transaction(tx) resp = self.web3_client.send_transaction(signer, instruction_tx) + ERC721ForMetaplex.receipt = resp # save for @stats_collector.cost_report_from_receipt logs = self.contract.events.Transfer().process_receipt(resp) LOGGER.info(f"Event logs: {logs}") return logs[0]["args"]["tokenId"] @@ -69,14 +72,15 @@ def safe_mint(self, seed, to_address, uri, data=None, gas_price=None, gas=None, return logs[0]["args"]["tokenId"] @allure.step("Transfer from") - def transfer_from(self, address_from, address_to, token_id, signer, gas_price=None, gas=None): + @stats_collector.cost_report_from_receipt + def transfer_from(self, address_from, address_to, token_id, signer, gas_price=None, gas=None) -> TxReceipt: tx = self.make_tx_object(signer.address, gas_price, gas) instruction_tx = self.contract.functions.transferFrom(address_from, address_to, token_id).build_transaction(tx) resp = self.web3_client.send_transaction(signer, instruction_tx) return resp @allure.step("Safe transfer from") - def safe_transfer_from(self, address_from, address_to, token_id, signer, data=None, gas_price=None, gas=None): + def safe_transfer_from(self, address_from, address_to, token_id, signer, data=None, gas_price=None, gas=None) -> TxReceipt: tx = self.make_tx_object(signer.address, gas_price, gas) if data is None: instruction_tx = self.contract.functions.safeTransferFrom( @@ -90,21 +94,22 @@ def safe_transfer_from(self, address_from, address_to, token_id, signer, data=No return resp @allure.step("Approve") - def approve(self, address_to, token_id, signer, gas_price=None, gas=None): + @stats_collector.cost_report_from_receipt + def approve(self, address_to, token_id, signer, gas_price=None, gas=None) -> TxReceipt: tx = self.make_tx_object(signer.address, gas_price, gas) instruction_tx = self.contract.functions.approve(address_to, token_id).build_transaction(tx) resp = self.web3_client.send_transaction(signer, instruction_tx) return resp @allure.step("Set approval for all") - def set_approval_for_all(self, operator, approved, signer, gas_price=None, gas=None): + def set_approval_for_all(self, operator, approved, signer, gas_price=None, gas=None) -> TxReceipt: tx = self.make_tx_object(signer.address, gas_price, gas) instruction_tx = self.contract.functions.setApprovalForAll(operator, approved).build_transaction(tx) resp = self.web3_client.send_transaction(signer, instruction_tx) return resp @allure.step("Transfer solana from") - def transfer_solana_from(self, from_address, to_address, token_id, signer, gas_price=None, gas=None): + def transfer_solana_from(self, from_address, to_address, token_id, signer, gas_price=None, gas=None) -> TxReceipt: tx = self.make_tx_object(signer.address, gas_price, gas) instruction_tx = self.contract.functions.transferSolanaFrom( from_address, to_address, token_id diff --git a/utils/error_log.py b/utils/error_log.py index 5f42095a3f..381d7d2ad3 100644 --- a/utils/error_log.py +++ b/utils/error_log.py @@ -2,7 +2,7 @@ import json from pathlib import Path from collections import defaultdict -from typing import Optional, Type, Generator +from typing import Type, Generator import pydantic from filelock import FileLock diff --git a/utils/evm_loader.py b/utils/evm_loader.py index 89895ed905..701c605a94 100644 --- a/utils/evm_loader.py +++ b/utils/evm_loader.py @@ -7,9 +7,9 @@ from eth_keys import keys as eth_keys from eth_account.datastructures import SignedTransaction -from solana.keypair import Keypair -from solana.publickey import PublicKey -import solana.system_program as sp +from solders.keypair import Keypair +from solders.pubkey import Pubkey +import solders.system_program as sp from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts from solana.transaction import Transaction @@ -46,23 +46,23 @@ class EvmLoader(SolanaClient): def __init__(self, program_id, endpoint): super().__init__(endpoint) - EvmLoader.loader_id = PublicKey(program_id) + EvmLoader.loader_id = Pubkey.from_string(program_id) self.loader_id = EvmLoader.loader_id - def create_balance_account(self, ether: Union[str, bytes], sender, chain_id=CHAIN_ID) -> PublicKey: + def create_balance_account(self, ether: Union[str, bytes], sender, chain_id=CHAIN_ID) -> Pubkey: account_pubkey = self.ether2balance(ether, chain_id) - contract_pubkey = PublicKey(self.ether2program(ether)[0]) + contract_pubkey = Pubkey.from_string(self.ether2program(ether)[0]) trx = Transaction() trx.add( make_CreateBalanceAccount( - self.loader_id, sender.public_key, self.ether2bytes(ether), account_pubkey, contract_pubkey, chain_id + self.loader_id, sender.pubkey(), self.ether2bytes(ether), account_pubkey, contract_pubkey, chain_id ) ) self.send_tx(trx, sender) return account_pubkey def create_treasury_pool_address(self, pool_index): - return PublicKey.find_program_address( + return Pubkey.find_program_address( [bytes(TREASURY_POOL_SEED, "utf8"), pool_index.to_bytes(4, "little")], self.loader_id )[0] @@ -84,11 +84,11 @@ def ether2hex(ether: Union[str, bytes]): def ether2operator_balance( self, keypair: Keypair, ether_address: Union[str, bytes], chain_id=CHAIN_ID - ) -> PublicKey: + ) -> Pubkey: address_bytes = self.ether2bytes(ether_address) - key = bytes(keypair.public_key) + key = bytes(keypair.pubkey()) chain_id_bytes = chain_id.to_bytes(32, "big") - return PublicKey.find_program_address( + return Pubkey.find_program_address( [self.account_seed_version, key, address_bytes, chain_id_bytes], self.loader_id )[0] @@ -100,9 +100,9 @@ def get_neon_nonce(self, account: Union[str, bytes], chain_id=CHAIN_ID) -> int: return layout.trx_count - def get_solana_account_data(self, account: Union[str, PublicKey, Keypair], expected_length: int) -> bytes: + def get_solana_account_data(self, account: Union[str, Pubkey, Keypair], expected_length: int) -> bytes: if isinstance(account, Keypair): - account = account.public_key + account = account.pubkey() info = self.get_account_info(account, commitment=Confirmed) info = info.value if info is None: @@ -131,7 +131,7 @@ def get_data_account_revision(self, address): def write_transaction_to_holder_account( self, signed_tx: SignedTransaction, - holder_account: PublicKey, + holder_account: Pubkey, operator: Keypair, ): offset = 0 @@ -140,7 +140,7 @@ def write_transaction_to_holder_account( while len(rest): (part, rest) = (rest[:920], rest[920:]) trx = Transaction() - trx.add(make_WriteHolder(operator.public_key, self.loader_id, holder_account, signed_tx.hash, offset, part)) + trx.add(make_WriteHolder(operator.pubkey(), self.loader_id, holder_account, signed_tx.hash, offset, part)) receipts.append( self.send_transaction( trx, @@ -154,35 +154,36 @@ def write_transaction_to_holder_account( self.confirm_transaction(rcpt.value, commitment=Confirmed) def ether2program(self, ether: tp.Union[str, bytes]) -> tp.Tuple[str, int]: - items = PublicKey.find_program_address([self.account_seed_version, self.ether2bytes(ether)], self.loader_id) + items = Pubkey.find_program_address([self.account_seed_version, self.ether2bytes(ether)], self.loader_id) return str(items[0]), items[1] - def ether2balance(self, address: tp.Union[str, bytes], chain_id=CHAIN_ID) -> PublicKey: + def ether2balance(self, address: tp.Union[str, bytes], chain_id=CHAIN_ID) -> Pubkey: # get public key associated with chain_id for an address address_bytes = self.ether2bytes(address) chain_id_bytes = chain_id.to_bytes(32, "big") - return PublicKey.find_program_address( + return Pubkey.find_program_address( [self.account_seed_version, address_bytes, chain_id_bytes], self.loader_id )[0] def get_operator_balance_pubkey(self, operator: Keypair): - operator_ether = eth_keys.PrivateKey(operator.secret_key[:32]).public_key.to_canonical_address() + operator_ether = eth_keys.PrivateKey(operator.secret()[:32]).public_key.to_canonical_address() return self.ether2operator_balance(operator, operator_ether) def execute_trx_from_instruction( self, operator: Keypair, - holder_acc: PublicKey, - treasury_address: PublicKey, + holder_acc: Pubkey, + treasury_address: Pubkey, treasury_buffer: bytes, instruction: SignedTransaction, additional_accounts, signer: Keypair = None, - system_program=sp.SYS_PROGRAM_ID, + system_program=sp.ID, + compute_unit_price=None ) -> SendTransactionResp: signer = operator if signer is None else signer - trx = TransactionWithComputeBudget(operator) + trx = TransactionWithComputeBudget(operator, compute_unit_price=compute_unit_price) operator_balance = self.get_operator_balance_pubkey(operator) trx.add( @@ -195,8 +196,7 @@ def execute_trx_from_instruction( treasury_buffer, instruction.rawTransaction, additional_accounts, - system_program, - ) + system_program) ) return self.send_tx(trx, signer) @@ -204,12 +204,12 @@ def execute_trx_from_instruction( def execute_trx_from_account( self, operator: Keypair, - holder_acc: PublicKey, - treasury_address: PublicKey, + holder_acc: Pubkey, + treasury_address: Pubkey, treasury_buffer: bytes, additional_accounts, signer: Keypair, - system_program=sp.SYS_PROGRAM_ID, + system_program=sp.ID, ) -> SendTransactionResp: operator_balance = self.get_operator_balance_pubkey(operator) @@ -232,13 +232,13 @@ def execute_trx_from_account( def execute_trx_from_instruction_with_solana_call( self, operator: Keypair, - holder_address: PublicKey, - treasury_address: PublicKey, + holder_address: Pubkey, + treasury_address: Pubkey, treasury_buffer: bytes, instruction: SignedTransaction, additional_accounts, signer: Keypair = None, - system_program=sp.SYS_PROGRAM_ID, + system_program=sp.ID, ) -> SendTransactionResp: signer = operator if signer is None else signer operator_balance_pubkey = self.get_operator_balance_pubkey(operator) @@ -263,12 +263,12 @@ def execute_trx_from_account_with_solana_call( self, operator: Keypair, holder_address, - treasury_address: PublicKey, + treasury_address: Pubkey, treasury_buffer: bytes, additional_accounts, signer: Keypair = None, additional_signers: typing.List[Keypair] = None, - system_program=sp.SYS_PROGRAM_ID, + system_program=sp.ID, ) -> SendTransactionResp: signer = operator if signer is None else signer operator_balance_pubkey = self.get_operator_balance_pubkey(operator) @@ -301,7 +301,7 @@ def send_transaction_step_from_instruction( additional_accounts, steps_count, signer: Keypair, - system_program=sp.SYS_PROGRAM_ID, + system_program=sp.ID, index=0, tag=0x34, ) -> GetTransactionResp: @@ -319,7 +319,7 @@ def send_transaction_step_from_instruction( treasury, additional_accounts, system_program, - tag, + tag ) ) @@ -373,11 +373,12 @@ def send_transaction_step_from_account( additional_accounts, steps_count, signer: Keypair, - system_program=sp.SYS_PROGRAM_ID, + system_program=sp.ID, + compute_unit_price=None, tag=0x35, index=0, ) -> GetTransactionResp: - trx = TransactionWithComputeBudget(operator) + trx = TransactionWithComputeBudget(operator, compute_unit_price=compute_unit_price) trx.add( make_ExecuteTrxFromAccountDataIterativeOrContinue( index, @@ -389,13 +390,19 @@ def send_transaction_step_from_account( treasury, additional_accounts, system_program, - tag, + tag ) ) return self.send_tx(trx, signer) def execute_transaction_steps_from_account( - self, operator: Keypair, treasury, storage_account, additional_accounts, signer: Keypair = None + self, + operator: Keypair, + treasury, + storage_account, + additional_accounts, + signer: Keypair = None, + compute_unit_price=None ) -> GetTransactionResp: signer = operator if signer is None else signer operator_balance_pubkey = self.get_operator_balance_pubkey(operator) @@ -413,6 +420,7 @@ def execute_transaction_steps_from_account( EVM_STEPS, signer, index=index, + compute_unit_price=compute_unit_price ) index += 1 @@ -445,7 +453,7 @@ def execute_transaction_steps_from_account_no_chain_id( EVM_STEPS, signer, tag=0x36, - index=index, + index=index ) index += 1 @@ -462,28 +470,28 @@ def execute_transaction_steps_from_account_no_chain_id( def deposit_neon(self, operator_keypair: Keypair, ether_address: Union[str, bytes], amount: int): balance_pubkey = self.ether2balance(ether_address) - contract_pubkey = PublicKey(self.ether2program(ether_address)[0]) + contract_pubkey = Pubkey.from_string(self.ether2program(ether_address)[0]) - evm_token_authority = PublicKey.find_program_address([b"Deposit"], self.loader_id)[0] + evm_token_authority = Pubkey.find_program_address([b"Deposit"], self.loader_id)[0] evm_pool_key = get_associated_token_address(evm_token_authority, NEON_TOKEN_MINT_ID) - token_pubkey = get_associated_token_address(operator_keypair.public_key, NEON_TOKEN_MINT_ID) + token_pubkey = get_associated_token_address(operator_keypair.pubkey(), NEON_TOKEN_MINT_ID) with open("evm_loader-keypair.json", "r") as key: - secret_key = json.load(key)[:32] - mint_authority = Keypair.from_secret_key(secret_key) + secret_key = json.load(key) + mint_authority = Keypair.from_bytes(secret_key) trx = Transaction() trx.add( make_CreateAssociatedTokenIdempotent( - operator_keypair.public_key, operator_keypair.public_key, NEON_TOKEN_MINT_ID + operator_keypair.pubkey(), operator_keypair.pubkey(), NEON_TOKEN_MINT_ID ), spl.token.instructions.mint_to( MintToParams( TOKEN_PROGRAM_ID, NEON_TOKEN_MINT_ID, token_pubkey, - mint_authority.public_key, + mint_authority.pubkey(), amount, ) ), @@ -492,7 +500,7 @@ def deposit_neon(self, operator_keypair: Keypair, ether_address: Union[str, byte spl.token.constants.TOKEN_PROGRAM_ID, token_pubkey, balance_pubkey, - operator_keypair.public_key, + operator_keypair.pubkey(), amount, ) ), @@ -505,7 +513,7 @@ def deposit_neon(self, operator_keypair: Keypair, ether_address: Union[str, byte token_pubkey, evm_pool_key, spl.token.constants.TOKEN_PROGRAM_ID, - operator_keypair.public_key, + operator_keypair.pubkey(), self.loader_id, ), ) @@ -515,10 +523,10 @@ def deposit_neon(self, operator_keypair: Keypair, ether_address: Union[str, byte return receipt def make_new_user(self, sender: Keypair) -> Caller: - key = Keypair.generate() - if self.get_solana_balance(key.public_key) == 0: - self.request_airdrop(key.public_key, 1000 * 10**9, commitment=Confirmed) - caller_ether = eth_keys.PrivateKey(key.secret_key[:32]).public_key.to_canonical_address() + key = Keypair() + if self.get_solana_balance(key.pubkey()) == 0: + self.request_airdrop(key.pubkey(), 1000 * 10**9, commitment=Confirmed) + caller_ether = eth_keys.PrivateKey(key.secret()[:32]).public_key.to_canonical_address() caller_solana = self.ether2program(caller_ether)[0] caller_balance = self.ether2balance(caller_ether) caller_token = get_associated_token_address(caller_balance, NEON_TOKEN_MINT_ID) @@ -527,30 +535,30 @@ def make_new_user(self, sender: Keypair) -> Caller: print(f"Create Neon account {caller_ether} for user {caller_balance}") self.create_balance_account(caller_ether, sender) - print("Account solana address:", key.public_key) + print("Account solana address:", key.pubkey()) print( f"Account ether address: {caller_ether.hex()}", ) print(f"Account solana address: {caller_balance}") - return Caller(key, PublicKey(caller_solana), caller_balance, caller_ether, caller_token) + return Caller(key, Pubkey.from_string(caller_solana), caller_balance, caller_ether, caller_token) def sent_token_from_solana_to_neon(self, solana_account, mint, neon_account, amount, chain_id): """Transfer any token from solana to neon transaction""" balance_pubkey = self.ether2balance(neon_account.address, chain_id) - contract_pubkey = PublicKey(self.ether2program(neon_account.address)[0]) - associated_token_address = get_associated_token_address(solana_account.public_key, mint) - authority_pool = PublicKey.find_program_address([b"Deposit"], self.loader_id)[0] + contract_pubkey = Pubkey.from_string(self.ether2program(neon_account.address)[0]) + associated_token_address = get_associated_token_address(solana_account.pubkey(), mint) + authority_pool = Pubkey.find_program_address([b"Deposit"], self.loader_id)[0] pool = get_associated_token_address(authority_pool, mint) - tx = Transaction(fee_payer=solana_account.public_key) + tx = Transaction(fee_payer=solana_account.pubkey()) tx.add( approve( ApproveParams( program_id=TOKEN_PROGRAM_ID, source=associated_token_address, delegate=balance_pubkey, - owner=solana_account.public_key, + owner=solana_account.pubkey(), amount=amount, ) ) @@ -566,7 +574,7 @@ def sent_token_from_solana_to_neon(self, solana_account, mint, neon_account, amo associated_token_address, pool, TOKEN_PROGRAM_ID, - solana_account.public_key, + solana_account.pubkey(), self.loader_id, ) ) @@ -576,12 +584,12 @@ def deposit_wrapped_sol_from_solana_to_neon(self, solana_account, neon_account, if not full_amount: full_amount = int(0.1 * LAMPORT_PER_SOL) mint_pubkey = wSOL["address_spl"] - ata_address = get_associated_token_address(solana_account.public_key, mint_pubkey) + ata_address = get_associated_token_address(solana_account.pubkey(), mint_pubkey) self.create_associate_token_acc(solana_account, solana_account, mint_pubkey) # wrap SOL - wrap_sol_tx = make_wSOL(full_amount, solana_account.public_key, ata_address) + wrap_sol_tx = make_wSOL(full_amount, solana_account.pubkey(), ata_address) self.send_tx_and_check_status_ok(wrap_sol_tx, solana_account) self.sent_token_from_solana_to_neon(solana_account, wSOL["address_spl"], neon_account, full_amount, chain_id) diff --git a/utils/helpers.py b/utils/helpers.py index 58a9ee1b3f..9510d62053 100644 --- a/utils/helpers.py +++ b/utils/helpers.py @@ -2,10 +2,10 @@ import pathlib import random import string -import time import typing import typing as tp - +import logging +from queue import Queue import allure import base58 @@ -13,9 +13,13 @@ import web3 from eth_abi import abi from eth_utils import keccak -from solana.publickey import PublicKey +from solders.pubkey import Pubkey from solcx import link_code +import polling2 +from semantic_version import Version + +T = tp.TypeVar('T') @allure.step("Get contract abi") @@ -54,7 +58,7 @@ def get_contract_interface( compiled = solcx.compile_files( [contract_path], output_values=["abi", "bin"], - solc_version=version, + solc_version=Version(version), import_remappings=import_remapping, allow_paths=["."], optimize=True, @@ -90,19 +94,36 @@ def generate_text(min_len: int = 2, max_len: int = 200, simple: bool = True) -> @allure.step("Wait condition") -def wait_condition(func_cond, timeout_sec=15, delay=0.5): - start_time = time.time() - while True: - if time.time() - start_time > timeout_sec: - raise TimeoutError(f"The condition not reached within {timeout_sec} sec") - try: - if func_cond(): - break - - except Exception as e: - print(f"Error during waiting: {e}") - time.sleep(delay) - return True +def wait_condition( + func_cond: tp.Callable[..., T], + timeout_sec: float = 15, + delay: float = 0.5, + args: tp.Tuple = (), + kwargs: tp.Optional[dict[str, tp.Any]] = None, + max_tries: tp.Optional[int] = None, + check_success: tp.Callable[[T], bool] = polling2.is_truthy, + step_function: tp.Callable[[float], float] = polling2.step_constant, + ignore_exceptions: tp.Tuple[Exception, ...] = (KeyError,), + poll_forever: bool = False, + collect_values: tp.Optional[Queue] = None, + log: int = logging.NOTSET, + log_error: int = logging.NOTSET +): + return polling2.poll( + target=func_cond, + timeout=timeout_sec, + step=delay, + args=args, + kwargs=kwargs, + max_tries=max_tries, + check_success=check_success, + step_function=step_function, + ignore_exceptions=ignore_exceptions, + poll_forever=poll_forever, + collect_values=collect_values, + log=log, + log_error=log_error, + ) @allure.step("Decode function signature") @@ -115,18 +136,18 @@ def decode_function_signature(function_name: str, args=None) -> str: @allure.step("Get functions signatures with params as keccak256 from contract abi") -def get_selectors(abi): +def get_selectors(abi_): """Get functions signatures with params as keccak256 from contract abi""" selectors = [] - for function in filter(lambda item: item["type"] == "function", abi): + for function in filter(lambda item: item["type"] == "function", abi_): input_types = "" - for input in function["inputs"]: - if "struct" in input["internalType"]: - struct_name = input["name"] - struct_types = ",".join(i["type"] for i in input["components"] if i["name"] != struct_name) + for input_ in function["inputs"]: + if "struct" in input_["internalType"]: + struct_name = input_["name"] + struct_types = ",".join(i["type"] for i in input_["components"] if i["name"] != struct_name) input_types += "," + f"({struct_types})[]" else: - input_types += "," + input["type"] + input_types += "," + input_["type"] input_types = input_types[1:] encoded_selector = f"{function['name']}({input_types})" @@ -143,7 +164,6 @@ def create_invalid_address(length=20) -> str: return address - def cryptohex(text: str): return "0x" + keccak(text=text).hex() @@ -163,24 +183,35 @@ def hasattr_recursive(obj: typing.Any, attribute: str) -> bool: return True -def bytes32_to_solana_pubkey(bytes32_data): + +def bytes32_to_solana_pubkey(bytes32_data: str) -> Pubkey: byte_data = bytes.fromhex(bytes32_data) - base58_data = base58.b58encode(byte_data) - return PublicKey(base58_data.decode('utf-8')) + return Pubkey(byte_data) def solana_pubkey_to_bytes32(solana_pubkey): byte_data = base58.b58decode(str(solana_pubkey)) return byte_data -def serialize_instruction(program_id, instruction) -> bytes: - program_id_bytes = solana_pubkey_to_bytes32(PublicKey(program_id)) - serialized = program_id_bytes + len(instruction.keys).to_bytes(8, "little") - for key in instruction.keys: +def serialize_instruction(program_id: Pubkey, instruction) -> bytes: + program_id_bytes = solana_pubkey_to_bytes32(program_id) + serialized = program_id_bytes + len(instruction.accounts).to_bytes(8, "little") + + for key in instruction.accounts: serialized += bytes(key.pubkey) serialized += key.is_signer.to_bytes(1, "little") serialized += key.is_writable.to_bytes(1, "little") serialized += len(instruction.data).to_bytes(8, "little") + instruction.data - return serialized \ No newline at end of file + return serialized + + +def case_snake_to_camel(snake_str: str) -> str: + components = snake_str.split('_') + camel_case = components[0].lower() + ''.join(x.title() for x in components[1:]) + return camel_case + + +def padhex(s, size): + return '0x' + s[2:].zfill(size) diff --git a/utils/instructions.py b/utils/instructions.py index 288929bc5e..61fd8a2af2 100644 --- a/utils/instructions.py +++ b/utils/instructions.py @@ -1,14 +1,14 @@ import typing as tp from hashlib import sha256 -from solana.keypair import Keypair -from solana.publickey import PublicKey -import solana.system_program as sp -from solana.transaction import AccountMeta, TransactionInstruction, Transaction +from solders.keypair import Keypair +from solders.pubkey import Pubkey +import solders.system_program as sp +from solana.transaction import AccountMeta, Instruction, Transaction from utils.consts import COMPUTE_BUDGET_ID -from solana.system_program import SYS_PROGRAM_ID -from solana.sysvar import SYSVAR_RENT_PUBKEY +from solders.system_program import ID as SYS_PROGRAM_ID +from .metaplex import SYSVAR_RENT_PUBKEY from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID from spl.token.instructions import get_associated_token_address @@ -21,19 +21,27 @@ class ComputeBudget: @staticmethod - def request_units(operator, units, additional_fee): - return TransactionInstruction( + def request_units(operator: Keypair, units): + return Instruction( program_id=COMPUTE_BUDGET_ID, - keys=[AccountMeta(PublicKey(operator.public_key), is_signer=True, is_writable=False)], - data=bytes.fromhex("02") + units.to_bytes(4, "little"), # + additional_fee.to_bytes(4, "little") + accounts=[AccountMeta(operator.pubkey(), is_signer=True, is_writable=False)], + data=bytes.fromhex("02") + units.to_bytes(4, "little") ) @staticmethod - def request_heap_frame(operator, heap_frame): - return TransactionInstruction( + def request_heap_frame(operator: Keypair, heap_frame): + return Instruction( program_id=COMPUTE_BUDGET_ID, - keys=[AccountMeta(PublicKey(operator.public_key), is_signer=True, is_writable=False)], - data=bytes.fromhex("01") + heap_frame.to_bytes(4, "little"), + accounts=[AccountMeta(operator.pubkey(), is_signer=True, is_writable=False)], + data=bytes.fromhex("01") + heap_frame.to_bytes(4, "little") + ) + + @staticmethod + def set_compute_units_price(price, operator: Keypair): + return Instruction( + program_id=COMPUTE_BUDGET_ID, + accounts=[AccountMeta(operator.pubkey(), is_signer=True, is_writable=False)], + data=bytes.fromhex("03") + price.to_bytes(8, "little") ) @@ -42,27 +50,30 @@ def __init__( self, operator: Keypair, units=DEFAULT_UNITS, - additional_fee=DEFAULT_ADDITIONAL_FEE, heap_frame=DEFAULT_HEAP_FRAME, + compute_unit_price=None, *args, **kwargs, ): super().__init__(*args, **kwargs) if units: - self.add(ComputeBudget.request_units(operator, units, additional_fee)) + self.add(ComputeBudget.request_units(operator, units)) + if heap_frame: self.add(ComputeBudget.request_heap_frame(operator, heap_frame)) + if compute_unit_price: + self.add(ComputeBudget.set_compute_units_price(compute_unit_price, operator)) def make_WriteHolder( - operator: PublicKey, evm_loader_id: PublicKey, holder_account: PublicKey, hash: bytes, offset: int, payload: bytes + operator: Pubkey, evm_loader_id: Pubkey, holder_account: Pubkey, hash_: bytes, offset: int, payload: bytes ): - d = bytes([0x26]) + hash + offset.to_bytes(8, byteorder="little") + payload + d = bytes([0x26]) + hash_ + offset.to_bytes(8, byteorder="little") + payload - return TransactionInstruction( + return Instruction( program_id=evm_loader_id, data=d, - keys=[ + accounts=[ AccountMeta(pubkey=holder_account, is_signer=False, is_writable=True), AccountMeta(pubkey=operator, is_signer=True, is_writable=False), ], @@ -71,28 +82,28 @@ def make_WriteHolder( def make_ExecuteTrxFromInstruction( operator: Keypair, - operator_balance: PublicKey, - holder_address: PublicKey, - evm_loader_id: PublicKey, - treasury_address: PublicKey, + operator_balance: Pubkey, + holder_address: Pubkey, + evm_loader_id: Pubkey, + treasury_address: Pubkey, treasury_buffer: bytes, message: bytes, - additional_accounts: tp.List[PublicKey], - system_program=sp.SYS_PROGRAM_ID, - tag=0x3D, + additional_accounts: tp.List[Pubkey], + system_program=sp.ID, + tag=0x3D ): data = bytes([tag]) + treasury_buffer + message print("make_ExecuteTrxFromInstruction accounts") print("Holder: ", holder_address) - print("Operator: ", operator.public_key) + print("Operator: ", operator.pubkey()) print("Treasury: ", treasury_address) print("Operator balance: ", operator_balance) accounts = [ AccountMeta(pubkey=holder_address, is_signer=False, is_writable=True), - AccountMeta(pubkey=operator.public_key, is_signer=True, is_writable=True), + AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=treasury_address, is_signer=False, is_writable=True), AccountMeta(pubkey=operator_balance, is_signer=False, is_writable=True), - AccountMeta(system_program, is_signer=False, is_writable=True), + AccountMeta(system_program, is_signer=False, is_writable=True) ] for acc in additional_accounts: print("Additional acc ", acc) @@ -100,32 +111,32 @@ def make_ExecuteTrxFromInstruction( AccountMeta(acc, is_signer=False, is_writable=True), ) - return TransactionInstruction(program_id=evm_loader_id, data=data, keys=accounts) + return Instruction(program_id=evm_loader_id, data=data, accounts=accounts) def make_ExecuteTrxFromAccount( operator: Keypair, - operator_balance: PublicKey, - evm_loader_id: PublicKey, - holder_address: PublicKey, - treasury_address: PublicKey, + operator_balance: Pubkey, + evm_loader_id: Pubkey, + holder_address: Pubkey, + treasury_address: Pubkey, treasury_buffer: bytes, - additional_accounts: tp.List[PublicKey], + additional_accounts: tp.List[Pubkey], additional_signers: tp.List[Keypair] = None, - system_program=sp.SYS_PROGRAM_ID, - tag=0x33, + system_program=sp.ID, + tag=0x33 ): data = bytes([tag]) + treasury_buffer print("make_ExecuteTrxFromInstruction accounts") - print("Operator: ", operator.public_key) + print("Operator: ", operator.pubkey()) print("Treasury: ", treasury_address) print("Operator eth solana: ", operator_balance) accounts = [ AccountMeta(pubkey=holder_address, is_signer=False, is_writable=True), - AccountMeta(pubkey=operator.public_key, is_signer=True, is_writable=True), + AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=treasury_address, is_signer=False, is_writable=True), AccountMeta(pubkey=operator_balance, is_signer=False, is_writable=True), - AccountMeta(system_program, is_signer=False, is_writable=True), + AccountMeta(system_program, is_signer=False, is_writable=True) ] for acc in additional_accounts: print("Additional acc ", acc) @@ -135,37 +146,37 @@ def make_ExecuteTrxFromAccount( if additional_signers: for acc in additional_signers: accounts.append( - AccountMeta(acc.public_key, is_signer=True, is_writable=True), + AccountMeta(acc.pubkey(), is_signer=True, is_writable=True), ) - return TransactionInstruction(program_id=evm_loader_id, data=data, keys=accounts) + return Instruction(program_id=evm_loader_id, data=data, accounts=accounts) def make_ExecuteTrxFromAccountDataIterativeOrContinue( index: int, step_count: int, operator: Keypair, - operator_balance: PublicKey, - evm_loader_id: PublicKey, - holder_address: PublicKey, - treasury, #: TreasuryPool, - additional_accounts: tp.List[PublicKey], - sys_program_id=sp.SYS_PROGRAM_ID, - tag=0x35, + operator_balance: Pubkey, + evm_loader_id: Pubkey, + holder_address: Pubkey, + treasury, + additional_accounts: tp.List[Pubkey], + sys_program_id=sp.ID, + tag=0x35 ): # 0x35 - TransactionStepFromAccount # 0x36 - TransactionStepFromAccountNoChainId data = tag.to_bytes(1, "little") + treasury.buffer + step_count.to_bytes(4, "little") + index.to_bytes(4, "little") print("make_ExecuteTrxFromAccountDataIterativeOrContinue accounts") print("Holder: ", holder_address) - print("Operator: ", operator.public_key) + print("Operator: ", operator.pubkey()) print("Treasury: ", treasury.account) print("Operator eth solana: ", operator_balance) accounts = [ AccountMeta(pubkey=holder_address, is_signer=False, is_writable=True), - AccountMeta(pubkey=operator.public_key, is_signer=True, is_writable=True), + AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=treasury.account, is_signer=False, is_writable=True), AccountMeta(pubkey=operator_balance, is_signer=False, is_writable=True), - AccountMeta(sys_program_id, is_signer=False, is_writable=True), + AccountMeta(sys_program_id, is_signer=False, is_writable=True) ] for acc in additional_accounts: @@ -174,7 +185,7 @@ def make_ExecuteTrxFromAccountDataIterativeOrContinue( AccountMeta(acc, is_signer=False, is_writable=True), ) - return TransactionInstruction(program_id=evm_loader_id, data=data, keys=accounts) + return Instruction(program_id=evm_loader_id, data=data, accounts=accounts) def make_PartialCallOrContinueFromRawEthereumTX( @@ -182,44 +193,44 @@ def make_PartialCallOrContinueFromRawEthereumTX( step_count: int, instruction: bytes, operator: Keypair, - operator_balance: PublicKey, - evm_loader_id: PublicKey, - storage_address: PublicKey, + operator_balance: Pubkey, + evm_loader_id: Pubkey, + storage_address: Pubkey, treasury: TreasuryPool, - additional_accounts: tp.List[PublicKey], - system_program=sp.SYS_PROGRAM_ID, - tag=0x34, # TransactionStepFromInstruction + additional_accounts: tp.List[Pubkey], + system_program=sp.ID, + tag=0x34 # TransactionStepFromInstruction ): data = bytes([tag]) + treasury.buffer + step_count.to_bytes(4, "little") + index.to_bytes(4, "little") + instruction accounts = [ AccountMeta(pubkey=storage_address, is_signer=False, is_writable=True), - AccountMeta(pubkey=operator.public_key, is_signer=True, is_writable=True), + AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=treasury.account, is_signer=False, is_writable=True), AccountMeta(pubkey=operator_balance, is_signer=False, is_writable=True), - AccountMeta(system_program, is_signer=False, is_writable=True), + AccountMeta(system_program, is_signer=False, is_writable=True) ] for acc in additional_accounts: accounts.append( AccountMeta(acc, is_signer=False, is_writable=True), ) - return TransactionInstruction(program_id=evm_loader_id, data=data, keys=accounts) + return Instruction(program_id=evm_loader_id, data=data, accounts=accounts) def make_Cancel( - evm_loader_id: PublicKey, - storage_address: PublicKey, + evm_loader_id: Pubkey, + storage_address: Pubkey, operator: Keypair, - operator_balance: PublicKey, - hash: bytes, - additional_accounts: tp.List[PublicKey], + operator_balance: Pubkey, + hash_: bytes, + additional_accounts: tp.List[Pubkey], ): - data = bytes([0x37]) + hash + data = bytes([0x37]) + hash_ accounts = [ AccountMeta(pubkey=storage_address, is_signer=False, is_writable=True), - AccountMeta(pubkey=operator.public_key, is_signer=True, is_writable=True), + AccountMeta(pubkey=operator.pubkey(), is_signer=True, is_writable=True), AccountMeta(pubkey=operator_balance, is_signer=False, is_writable=True), ] @@ -228,21 +239,21 @@ def make_Cancel( AccountMeta(acc, is_signer=False, is_writable=True), ) - return TransactionInstruction(program_id=evm_loader_id, data=data, keys=accounts) + return Instruction(program_id=evm_loader_id, data=data, accounts=accounts) def make_DepositV03( ether_address: bytes, chain_id: int, - balance_account: PublicKey, - contract_account: PublicKey, - mint: PublicKey, - source: PublicKey, - pool: PublicKey, - token_program: PublicKey, - operator_pubkey: PublicKey, - evm_loader_id: PublicKey, -) -> TransactionInstruction: + balance_account: Pubkey, + contract_account: Pubkey, + mint: Pubkey, + source: Pubkey, + pool: Pubkey, + token_program: Pubkey, + operator_pubkey: Pubkey, + evm_loader_id: Pubkey, +) -> Instruction: data = bytes([0x31]) + ether_address + chain_id.to_bytes(8, "little") accounts = [ @@ -253,82 +264,82 @@ def make_DepositV03( AccountMeta(pubkey=contract_account, is_signer=False, is_writable=True), AccountMeta(pubkey=token_program, is_signer=False, is_writable=False), AccountMeta(pubkey=operator_pubkey, is_signer=True, is_writable=True), - AccountMeta(pubkey=sp.SYS_PROGRAM_ID, is_signer=False, is_writable=False), + AccountMeta(pubkey=sp.ID, is_signer=False, is_writable=False), ] - return TransactionInstruction(program_id=evm_loader_id, data=data, keys=accounts) + return Instruction(program_id=evm_loader_id, data=data, accounts=accounts) -def make_CreateAssociatedTokenIdempotent(payer: PublicKey, owner: PublicKey, mint: PublicKey) -> TransactionInstruction: +def make_CreateAssociatedTokenIdempotent(payer: Pubkey, owner: Pubkey, mint: Pubkey) -> Instruction: """Creates a transaction instruction to create an associated token account. Returns: The instruction to create the associated token account. """ associated_token_address = get_associated_token_address(owner, mint) - return TransactionInstruction( + return Instruction( data=bytes([1]), - keys=[ + accounts=[ AccountMeta(pubkey=payer, is_signer=True, is_writable=True), AccountMeta(pubkey=associated_token_address, is_signer=False, is_writable=True), AccountMeta(pubkey=owner, is_signer=False, is_writable=False), AccountMeta(pubkey=mint, is_signer=False, is_writable=False), AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), - AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False) ], program_id=ASSOCIATED_TOKEN_PROGRAM_ID, ) def make_CreateBalanceAccount( - evm_loader_id: PublicKey, - sender_pubkey: PublicKey, + evm_loader_id: Pubkey, + sender_pubkey: Pubkey, ether_address: bytes, - account_pubkey: PublicKey, - contract_pubkey: PublicKey, + account_pubkey: Pubkey, + contract_pubkey: Pubkey, chain_id, -) -> TransactionInstruction: +) -> Instruction: print("createBalanceAccount: {}".format(account_pubkey)) data = bytes([0x30]) + ether_address + chain_id.to_bytes(8, "little") - return TransactionInstruction( + return Instruction( program_id=evm_loader_id, data=data, - keys=[ + accounts=[ AccountMeta(pubkey=sender_pubkey, is_signer=True, is_writable=True), - AccountMeta(pubkey=sp.SYS_PROGRAM_ID, is_signer=False, is_writable=False), + AccountMeta(pubkey=sp.ID, is_signer=False, is_writable=False), AccountMeta(pubkey=account_pubkey, is_signer=False, is_writable=True), - AccountMeta(pubkey=contract_pubkey, is_signer=False, is_writable=True), + AccountMeta(pubkey=contract_pubkey, is_signer=False, is_writable=True) ], ) -def make_SyncNative(account: PublicKey): +def make_SyncNative(account: Pubkey): keys = [AccountMeta(pubkey=account, is_signer=False, is_writable=True)] data = bytes.fromhex("11") - return TransactionInstruction(keys=keys, program_id=TOKEN_PROGRAM_ID, data=data) + return Instruction(accounts=keys, program_id=TOKEN_PROGRAM_ID, data=data) def make_CreateAccountWithSeed(funding, base, seed, lamports, space, program): - created = PublicKey(sha256(bytes(base) + bytes(seed, "utf8") + bytes(program)).digest()) + created = Pubkey(sha256(bytes(base) + bytes(seed, "utf8") + bytes(program)).digest()) print(f"Created: {created}") return sp.create_account_with_seed( sp.CreateAccountWithSeedParams( from_pubkey=funding, - new_account_pubkey=created, - base_pubkey=base, + to_pubkey=created, + base=base, seed=seed, lamports=lamports, space=space, - program_id=program, + owner=program, ) ) def make_CreateHolderAccount(account, operator, seed, evm_loader_id): - return TransactionInstruction( - keys=[ + return Instruction( + accounts=[ AccountMeta(pubkey=account, is_signer=False, is_writable=True), AccountMeta(pubkey=operator, is_signer=True, is_writable=False), ], @@ -336,22 +347,37 @@ def make_CreateHolderAccount(account, operator, seed, evm_loader_id): data=bytes.fromhex("24") + len(seed).to_bytes(8, "little") + seed, ) + def make_wSOL(amount, solana_wallet, ata_address): tx = Transaction(fee_payer=solana_wallet) - tx.add(sp.transfer(sp.TransferParams(solana_wallet, ata_address, amount))) + tx.add(sp.transfer(sp.TransferParams( + from_pubkey=solana_wallet, to_pubkey=ata_address, lamports=amount) + )) tx.add(make_SyncNative(ata_address)) return tx + def make_OperatorBalanceAccount(operator_keypair, operator_balance_pubkey, ether_bytes, chain_id, evm_loader_id): trx = Transaction() - trx.add(TransactionInstruction( - keys=[ - AccountMeta(pubkey=operator_keypair.public_key, is_signer=True, is_writable=True), - AccountMeta(pubkey=sp.SYS_PROGRAM_ID, is_signer=False, is_writable=True), + trx.add(Instruction( + accounts=[ + AccountMeta(pubkey=operator_keypair.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=sp.ID, is_signer=False, is_writable=True), AccountMeta(pubkey=operator_balance_pubkey, is_signer=False, is_writable=True) ], program_id=evm_loader_id, data=bytes.fromhex("3A") + ether_bytes + chain_id.to_bytes(8, 'little') )) - return trx \ No newline at end of file + return trx + + +def get_compute_unit_price_eip_1559( + gas_price: int, + max_priority_fee_per_gas: int, +) -> int: + """ + :return: micro lamports + """ + cu_price = max(1, int(max_priority_fee_per_gas * 1_000_000 * 5000.0 / (gas_price * DEFAULT_UNITS))) + return cu_price diff --git a/utils/k6_helpers.py b/utils/k6_helpers.py new file mode 100644 index 0000000000..6d5965fe5d --- /dev/null +++ b/utils/k6_helpers.py @@ -0,0 +1,37 @@ +import json +import os +from utils.accounts import EthAccounts +from utils.faucet import Faucet +from utils.web3client import NeonChainWeb3Client + +def k6_prepare_accounts(network, network_object, users, balance, bank_account): + print("Setting k6 envs...") + k6_set_envs(network, users, balance, bank_account) + + print("Creating accounts...") + accounts = {} + web3_client = NeonChainWeb3Client(proxy_url=network_object["proxy_url"]) + faucet = Faucet(faucet_url=network_object['faucet_url'], web3_client=web3_client) + account_manager = EthAccounts(web3_client, faucet, None) + + # We create 2 accounts for each k6 virtual user: sender and receiver. + # We need to create 2*VU accounts cause we want to make sure account's nonces is not overlapping. + for i in range(2*int(users)): + account_sender = account_manager.create_account(balance=int(balance)) + account_receiver = account_manager.create_account(balance=0) + print(f"Account {str(i)} sender: {account_sender.address}, initial balance: {balance} Neon.") + print(f"Account {str(i)} receiver: {account_receiver.address}") + accounts[i] = {"sender_address": str(account_sender.address), + "sender_key": str(account_sender.key.hex())[2:], + "receiver_address": str(account_receiver.address)} + + with open('./loadtesting/k6/data/accounts.json', 'w', encoding='utf-8') as f: + json.dump(accounts, f) + +def k6_set_envs(network, users_number=None, initial_balance=None, bank_account=None): + os.environ["K6_NETWORK"] = network + os.environ["K6_USERS_NUMBER"] = users_number + os.environ["K6_INITIAL_BALANCE"] = initial_balance + + if bank_account is not None: + os.environ["K6_BANK_ACCOUNT"] = bank_account diff --git a/utils/metaplex.py b/utils/metaplex.py index 02375ef179..a9acb6dc50 100644 --- a/utils/metaplex.py +++ b/utils/metaplex.py @@ -6,21 +6,19 @@ Subconstruct, Enum ) -from solana.publickey import PublicKey -from solana.transaction import AccountMeta, TransactionInstruction +from solders.pubkey import Pubkey +from solana.transaction import AccountMeta, Instruction import enum -import base64 -import base58 from utils.helpers import wait_condition -METADATA_PROGRAM_ID = PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s') -SYSTEM_PROGRAM_ID = PublicKey('11111111111111111111111111111111') -SYSVAR_RENT_PUBKEY = PublicKey('SysvarRent111111111111111111111111111111111') -ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL') -TOKEN_PROGRAM_ID = PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') +METADATA_PROGRAM_ID = Pubkey.from_string('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s') +SYSTEM_PROGRAM_ID = Pubkey.from_string('11111111111111111111111111111111') +SYSVAR_RENT_PUBKEY = Pubkey.from_string('SysvarRent111111111111111111111111111111111') +ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = Pubkey.from_string('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL') +TOKEN_PROGRAM_ID = Pubkey.from_string('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') class MetadataLimit(enum.IntEnum): @@ -190,19 +188,19 @@ def _build(self, obj, stream, context, path): ) -def get_metadata_account(mint_key): - return PublicKey.find_program_address( - [b'metadata', bytes(METADATA_PROGRAM_ID), bytes(PublicKey(mint_key))], +def get_metadata_account(mint_key: Pubkey): + return Pubkey.find_program_address( + [b'metadata', bytes(METADATA_PROGRAM_ID), bytes(mint_key)], METADATA_PROGRAM_ID )[0] -def get_edition(mint_key): - return PublicKey.find_program_address( - [b'metadata', bytes(METADATA_PROGRAM_ID), bytes(PublicKey(mint_key)), b"edition"], +def get_edition(mint_key: Pubkey): + return Pubkey.find_program_address( + [b'metadata', bytes(METADATA_PROGRAM_ID), bytes(mint_key), b"edition"], METADATA_PROGRAM_ID )[0] - + def create_associated_token_account_instruction(associated_token_account, payer, wallet_address, token_mint_address): keys = [ @@ -214,7 +212,7 @@ def create_associated_token_account_instruction(associated_token_account, payer, AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False), ] - return TransactionInstruction(keys=keys, program_id=ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, data=b'') + return Instruction(accounts=keys, program_id=ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID, data=b'') def create_metadata_instruction_data(name: str, symbol: str, uri='', fee=0): @@ -273,10 +271,10 @@ def create_metadata_instruction(data, update_authority, mint_key, mint_authority AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False) # AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False) ] - return TransactionInstruction(keys=keys, program_id=METADATA_PROGRAM_ID, data=data) + return Instruction(accounts=keys, program_id=METADATA_PROGRAM_ID, data=data) -def get_metadata(client, mint_key): +def get_metadata(client, mint_key: Pubkey): metadata_account = get_metadata_account(mint_key) data = client.get_account_info(metadata_account).value.data @@ -285,7 +283,7 @@ def get_metadata(client, mint_key): def _strip_utf8(value) -> str: return value.strip("\x00") - metadata.mint = str(PublicKey(metadata.mint)).encode('utf-8') + metadata.mint = str(Pubkey(metadata.mint)).encode('utf-8') metadata.data.name = _strip_utf8(metadata.data.name) metadata.data.symbol = _strip_utf8(metadata.data.symbol) @@ -298,7 +296,6 @@ def _strip_utf8(value) -> str: return metadata -def wait_account_info(client, mint_key): +def wait_account_info(client, mint_key: Pubkey): metadata_account = get_metadata_account(mint_key) wait_condition(lambda: client.get_account_info(metadata_account).value is not None, timeout_sec=30) - diff --git a/utils/models/cost_report_model.py b/utils/models/cost_report_model.py new file mode 100644 index 0000000000..0d614e35d1 --- /dev/null +++ b/utils/models/cost_report_model.py @@ -0,0 +1,15 @@ +import pydantic + +from utils.models.model_types import HexString + + +class CostReportAction(pydantic.BaseModel): + name: str + usedGas: int + gasPrice: int + tx: HexString + + +class CostReportModel(pydantic.BaseModel): + name: str + actions: list[CostReportAction] = [] diff --git a/utils/models/error.py b/utils/models/error.py index 78ea3ca6ec..02ffe37e01 100644 --- a/utils/models/error.py +++ b/utils/models/error.py @@ -1,8 +1,9 @@ from typing import List, Optional -from pydantic import BaseModel, ConfigDict, field_validator +from pydantic import BaseModel, field_validator from integration.tests.basic.helpers.errors import Error32602 +from utils.models.mixins import ForbidExtra from utils.models.model_types import ( ErrorCodeField, IdField, @@ -12,10 +13,6 @@ ) -class ForbidExtra(BaseModel): - model_config = ConfigDict(extra="forbid") - - class EthErrorData(ForbidExtra): errors: List[str] diff --git a/utils/models/fee_history_model.py b/utils/models/fee_history_model.py new file mode 100644 index 0000000000..1f46e1d832 --- /dev/null +++ b/utils/models/fee_history_model.py @@ -0,0 +1,11 @@ +import typing as tp + +from utils.models.mixins import ForbidExtra +from utils.models.model_types import HexString + + +class EthFeeHistoryResult(ForbidExtra): + baseFeePerGas: list[HexString] + gasUsedRatio: list[tp.Union[int, float]] + oldestBlock: HexString + reward: tp.Optional[list[list[HexString]]] = None diff --git a/utils/models/mixins.py b/utils/models/mixins.py new file mode 100644 index 0000000000..e629aef30c --- /dev/null +++ b/utils/models/mixins.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel, ConfigDict + + +class ForbidExtra(BaseModel): + model_config = ConfigDict(extra="forbid") diff --git a/utils/models/model_types.py b/utils/models/model_types.py index 01c7ea481e..d3450a6c71 100644 --- a/utils/models/model_types.py +++ b/utils/models/model_types.py @@ -3,6 +3,7 @@ import utils.models.model_type_validators as mtv + HexString = Annotated[str, AfterValidator(mtv.validate_hex_string)] JsonRPCString = Annotated[str, AfterValidator(mtv.validate_jsonrpc)] IdField = Annotated[int, AfterValidator(mtv.validate_id)] diff --git a/utils/models/result.py b/utils/models/result.py index 6a52ba39a7..b854e35551 100644 --- a/utils/models/result.py +++ b/utils/models/result.py @@ -1,8 +1,10 @@ import typing as tp from typing import List, Union -from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic import BaseModel, Field, model_validator +from utils.models.mixins import ForbidExtra +from integration.tests.basic.helpers.basic import NeonEventType from integration.tests.basic.helpers.basic import NeonEventType, SolanaInstruction from utils.models.model_types import ( BalanceString, @@ -19,10 +21,6 @@ ) -class ForbidExtra(BaseModel): - model_config = ConfigDict(extra="forbid") - - class EthResult(ForbidExtra): jsonrpc: JsonRPCString id: IdField diff --git a/utils/operator.py b/utils/operator.py index ae41b6bbc5..989287ac2c 100644 --- a/utils/operator.py +++ b/utils/operator.py @@ -4,8 +4,8 @@ import typing as tp import solana.rpc.api -from solana.keypair import Keypair -from solana.publickey import PublicKey +from solders.keypair import Keypair +from solders.pubkey import Pubkey from solana.rpc.commitment import Confirmed, Commitment from eth_keys import keys as eth_keys @@ -21,7 +21,7 @@ def __init__( solana_url: str, neon_token_mint: str, web3_client: tp.Optional[NeonChainWeb3Client] = None, - evm_loader: tp.Optional[tp.Any] = None + evm_loader: tp.Optional[str] = None ): self._proxy_url = proxy_url self._solana_url = solana_url @@ -33,34 +33,35 @@ def __init__( self.sol = solana.rpc.api.Client(self._solana_url) self.evm_loader = evm_loader - def get_operator_keypairs(self): + @staticmethod + def get_operator_keypairs(): directory = OPERATOR_KEYPAIR_PATH operator_keys = [] for key in os.listdir(directory): key_file = pathlib.Path(f"{directory}/{key}") - with open(key_file, "r") as key: - secret_key = json.load(key)[:32] - account = Keypair.from_secret_key(secret_key) + with open(key_file, "r") as f: + secret_key = json.load(f) + account = Keypair.from_bytes(secret_key) operator_keys.append(account) return operator_keys - def get_operator_balance_account(self, operator, w3_client): - operator_ether = eth_keys.PrivateKey(operator.secret_key[:32]).public_key.to_canonical_address() + def get_operator_balance_account(self, operator: Keypair, w3_client): + operator_ether = eth_keys.PrivateKey(operator.secret()[:32]).public_key.to_canonical_address() seed_version = bytes("\3", encoding="utf-8").decode("unicode-escape").encode("utf-8") - operator_pubkey_bytes = bytes(operator.public_key) + operator_pubkey_bytes = bytes(operator.pubkey()) seed_list = ( seed_version, operator_pubkey_bytes, operator_ether, - w3_client.chain_id.to_bytes(32, byteorder="big"), ) - balance_account, _ = PublicKey.find_program_address(seed_list, PublicKey(self.evm_loader)) + w3_client.chain_id.to_bytes(32, byteorder="big")) + balance_account, _ = Pubkey.find_program_address(seed_list, Pubkey.from_string(self.evm_loader)) return balance_account def get_solana_balance(self): balances = [] for keypair in self.operator_keypairs: - balance = self.sol.get_balance(keypair.public_key, commitment=Confirmed) + balance = self.sol.get_balance(keypair.pubkey(), commitment=Confirmed) if isinstance(balance, dict): balance = balance["result"]["value"] else: @@ -72,13 +73,10 @@ def get_token_balance(self, w3_client=None): if w3_client is None: w3_client = self.web3 balances = [] - for operator in self.operator_keypairs: + for operator in self.operator_keypairs: token_addr = self.get_operator_balance_account(operator, w3_client) info: bytes = self.sol.get_account_info(token_addr, commitment=Commitment("confirmed")).value.data layout = OPERATOR_BALANCE_ACCOUNT_LAYOUT.parse(info) - amount = int.from_bytes(layout.balance, byteorder="little") + amount = int.from_bytes(layout.balance, byteorder="little") balances.append(amount) return sum(balances) - - - diff --git a/utils/prices.py b/utils/prices.py index 7581170db9..4eb94ccd9c 100644 --- a/utils/prices.py +++ b/utils/prices.py @@ -60,6 +60,7 @@ async def get_price(solana_address: str, feed_address: str): def get_sol_price() -> float: """Get SOL price from Solana mainnet""" + result = None for network in SOL_FEED_ADDRESSES: try: result = asyncio.run( @@ -72,11 +73,22 @@ def get_sol_price() -> float: except Exception as e: LOG.warning(f"Get error when try to get SOL price from: {network}: {e}") time.sleep(5) - else: - raise AssertionError("Can't get SOL price for all networks") return result +def get_sol_price_with_retry(timeout: int = 120) -> float: + price = get_sol_price() + started = time.time() + while price is None and (time.time() - started) < timeout: + print("Can't get SOL price") + time.sleep(3) + price = get_sol_price() + return price + if price is not None: + return price + raise TimeoutError("Failed to get SOL price within the timeout period.") + + def get_btc_price_detailed() -> PythPriceAccount: for network in BTC_FEED_ADDRESSES: try: diff --git a/utils/solana_client.py b/utils/solana_client.py index 2836d1df11..ee9651b727 100644 --- a/utils/solana_client.py +++ b/utils/solana_client.py @@ -2,19 +2,21 @@ import time import typing as tp import uuid + +import allure import requests import pathlib - import solana.rpc.api import spl.token.client -from solana.keypair import Keypair -from solana.publickey import PublicKey +from spl.token.client import Token +from solders.keypair import Keypair +from solders.pubkey import Pubkey from solana.rpc.commitment import Commitment, Finalized, Confirmed from solana.rpc.types import TxOpts from solders.rpc.responses import GetTransactionResp from solders.signature import Signature -from solana.system_program import TransferParams, transfer, create_account, CreateAccountParams +from solders.system_program import TransferParams, transfer, create_account, CreateAccountParams from solana.transaction import Transaction from solders.rpc.errors import InternalErrorMessage from solders.rpc.responses import RequestAirdropResp @@ -33,10 +35,10 @@ def __init__(self, endpoint, account_seed_version="\3"): ) def request_airdrop( - self, - pubkey: PublicKey, - lamports: int, - commitment: tp.Optional[Commitment] = None, + self, + pubkey: Pubkey, + lamports: int, + commitment: tp.Optional[Commitment] = None, ) -> RequestAirdropResp: airdrop_resp = None for _ in range(5): @@ -51,13 +53,12 @@ def request_airdrop( wait_condition(lambda: self.get_balance(pubkey).value >= lamports, timeout_sec=30) return airdrop_resp - def send_sol(self, from_: Keypair, to: PublicKey, amount_lamports: int): + def send_sol(self, from_: Keypair, to: Pubkey, amount_lamports: int): tx = Transaction().add( - transfer(TransferParams(from_pubkey=from_.public_key, to_pubkey=to, lamports=amount_lamports)) + transfer(TransferParams(from_pubkey=from_.pubkey(), to_pubkey=to, lamports=amount_lamports)) ) self.send_tx_and_check_status_ok(tx, from_) - @staticmethod def ether2bytes(ether: tp.Union[str, bytes]): if isinstance(ether, str): @@ -66,31 +67,30 @@ def ether2bytes(ether: tp.Union[str, bytes]): return bytes.fromhex(ether) return ether - def get_erc_auth_address(self, neon_account_address: str, token_address: str, evm_loader_id: str): neon_account_addressbytes = bytes(12) + bytes.fromhex(neon_account_address[2:]) if token_address.startswith("0x"): token_address = token_address[2:] neon_contract_addressbytes = bytes.fromhex(token_address) - return PublicKey.find_program_address( + return Pubkey.find_program_address( [ self.account_seed_version, b"AUTH", neon_contract_addressbytes, neon_account_addressbytes, ], - PublicKey(evm_loader_id), + Pubkey.from_string(evm_loader_id), )[0] - def create_spl(self, owner: Keypair, decimals: int = 9): + def create_spl(self, owner: Keypair, decimals: int = 9) -> tuple[Token, Pubkey]: token_mint = spl.token.client.Token.create_mint( conn=self, payer=owner, - mint_authority=owner.public_key, + mint_authority=owner.pubkey(), decimals=decimals, program_id=TOKEN_PROGRAM_ID, ) - assoc_addr = token_mint.create_associated_token_account(owner.public_key) + assoc_addr = token_mint.create_associated_token_account(owner.pubkey()) token_mint.mint_to( dest=assoc_addr, mint_authority=owner, @@ -109,14 +109,15 @@ def send_tx_and_check_status_ok(self, tx, *signers): def send_tx(self, trx: Transaction, *signers: Keypair, wait_status=Confirmed): result = self.send_transaction(trx, *signers, - opts=TxOpts(skip_confirmation=True, preflight_commitment=wait_status)) + opts=TxOpts(skip_confirmation=True, preflight_commitment=wait_status)) self.confirm_transaction(result.value, commitment=Confirmed) return self.get_transaction(result.value, commitment=Confirmed) - def create_associate_token_acc(self, payer, owner, token_mint): - if not self.account_exists(get_associated_token_address(owner.public_key, token_mint)): + def create_associate_token_acc(self, payer: Keypair, owner: Keypair, token_mint: Pubkey): + ata: Pubkey = get_associated_token_address(owner.pubkey(), token_mint) + if not self.account_exists(ata): trx = Transaction() - trx.add(create_associated_token_account(payer.public_key, owner.public_key, token_mint)) + trx.add(create_associated_token_account(payer.pubkey(), owner.pubkey(), token_mint)) self.send_tx_and_check_status_ok(trx, payer) def wait_transaction(self, tx): @@ -129,9 +130,9 @@ def wait_transaction(self, tx): return None return self.get_transaction(Signature.from_string(tx), max_supported_transaction_version=0) - def account_exists(self, account_address) -> bool: + def account_exists(self, account_address: Pubkey) -> bool: try: - account_info = self.get_account_info(PublicKey(account_address)) + account_info = self.get_account_info(account_address) if account_info.value is not None: return True else: @@ -140,8 +141,8 @@ def account_exists(self, account_address) -> bool: print(f"An error occurred: {e}") def get_account_whole_info( - self, - pubkey: PublicKey, + self, + pubkey: Pubkey, ): # get_account_info method returns cut data @@ -154,15 +155,15 @@ def get_account_whole_info( response = requests.post(self.endpoint, json=body, headers={"Content-Type": "application/json"}) return response.json() - def mint_spl_to(self, mint: PublicKey, dest: Keypair, amount: int, authority: tp.Optional[Keypair] = None): - token_account = get_associated_token_address(dest.public_key, mint) + def mint_spl_to(self, mint: Pubkey, dest: Keypair, amount: int, authority: tp.Optional[Keypair] = None): + token_account = get_associated_token_address(dest.pubkey(), mint) self.create_associate_token_acc(dest, dest, mint) if authority is None: operator_path = pathlib.Path(__file__).parent.parent / "operator-keypair.json" with open(operator_path, "r") as f: - authority = Keypair.from_seed(json.load(f)[:32]) + authority = Keypair.from_bytes(json.load(f)) token = spl.token.client.Token(self, mint, TOKEN_PROGRAM_ID, authority) token.payer = authority @@ -172,18 +173,35 @@ def get_solana_balance(self, account): return self.get_balance(account, commitment=Confirmed).value def create_account(self, payer, size, owner, account=None, lamports=None): - account = account or Keypair.generate() + account = account or Keypair() lamports = lamports or self.get_minimum_balance_for_rent_exemption(size).value trx = Transaction() - trx.fee_payer=payer.public_key + trx.fee_payer = payer.pubkey() instr = create_account( CreateAccountParams( - payer.public_key, - account.public_key, - lamports, - size, - owner)) + from_pubkey=payer.pubkey(), + to_pubkey=account.pubkey(), + lamports=lamports, + space=size, + owner=owner)) self.send_tx(trx.add(instr), payer, account) return account - + @allure.step("Get Solana transaction with wait") + def get_transaction_with_wait( + self, + tx_sig: Signature, + encoding: str = "json", + commitment: tp.Optional[Commitment] = None, + max_supported_transaction_version: tp.Optional[int] = None, + ) -> GetTransactionResp: + tx = wait_condition( + func_cond=lambda: super(SolanaClient, self).get_transaction( + tx_sig=tx_sig, + encoding=encoding, + commitment=commitment, + max_supported_transaction_version=max_supported_transaction_version, + ), + check_success=lambda trx: trx.value is not None + ) + return tx diff --git a/utils/stats_collector.py b/utils/stats_collector.py new file mode 100644 index 0000000000..ab3c7ee3c3 --- /dev/null +++ b/utils/stats_collector.py @@ -0,0 +1,110 @@ +import contextlib +import inspect +import json +from pathlib import Path +from typing import Callable, Generator +from functools import wraps + +from _pytest.mark import Mark +from filelock import FileLock +from web3.datastructures import AttributeDict +from web3.types import TxReceipt + +import conftest +from utils.models.cost_report_model import CostReportModel, CostReportAction + + +class StatisticsCollector: + def __init__(self, report_file: Path): + self.report_file: Path = report_file + self.model = CostReportModel(name=report_file.stem.removesuffix("-report")) + self.lock = FileLock(self.report_file.with_suffix(self.report_file.suffix + ".lock"), is_singleton=True) + + def __create(self): + data = self.model.model_dump_json(indent=4) + self.report_file.write_text(data) + + def read(self) -> CostReportModel: + with self.report_file.open() as f: + data = json.load(f) + return CostReportModel(**data) + + @contextlib.contextmanager + def _update(self) -> Generator[CostReportModel, None, None]: + with self.lock: + if not self.report_file.exists(): + self.__create() + + report = self.read() + + yield report + + data = report.model_dump_json(indent=4) + self.report_file.write_text(data) + + +def cost_report_from_receipt(func: Callable[..., TxReceipt]) -> Callable[..., TxReceipt]: + @wraps(func) + def wrapper(*args, **kwargs) -> TxReceipt: + result = func(*args, **kwargs) + + if conftest.COST_REPORT_DIR != Path(): + stack = inspect.stack() + if is_called_from_test_marked_for_collection(stack=stack): + if (isinstance(result, (dict, AttributeDict)) and + set(result.keys()).issubset(TxReceipt.__required_keys__)): + receipt = result + else: + try: + # if func does not return a TxReceipt - then TxReceipt must be set as attr "receipt" to func + receipt = wrapper.receipt + except AttributeError: + # ... or to class that func belongs to + receipt = args[0].__class__.receipt + + file_path = Path(inspect.getfile(func)).resolve() + neon_tests_index = file_path.parts.index("neon-tests") + relative_path = Path(*file_path.parts[neon_tests_index + 1:]).with_suffix("") + class_name = ".".join(func.__qualname__.split(".")[:-1]) if "." in func.__qualname__ else "" + report_file_name = ".".join(relative_path.parts) + f".{class_name}" + "-report.json" + report_file = conftest.COST_REPORT_DIR / report_file_name + + collector = StatisticsCollector(report_file) + + used_gas = receipt["gasUsed"] + gas_price = receipt["effectiveGasPrice"] + tx_hash = receipt["transactionHash"].hex() + + action = CostReportAction( + name=func.__name__, + usedGas=used_gas, + gasPrice=gas_price, + tx=tx_hash, + ) + + with collector._update() as report: + report.actions.append(action) + + return result + + return wrapper + + +def is_called_from_test_marked_for_collection(stack: list[inspect.FrameInfo]) -> bool: + for frame_info in stack: + func_name = frame_info.function + if func_name.startswith("test_"): + frame = frame_info.frame + module = inspect.getmodule(frame) + + if "self" in frame.f_locals: + function = getattr(frame.f_locals["self"].__class__, func_name) + else: + function = getattr(module, func_name) + + if hasattr(function, "pytestmark"): + mark: Mark + for mark in function.pytestmark: + if mark.name == "cost_report": + return True + return False diff --git a/utils/storage_contract.py b/utils/storage_contract.py new file mode 100644 index 0000000000..1ef99a5311 --- /dev/null +++ b/utils/storage_contract.py @@ -0,0 +1,83 @@ +import typing +from utils.web3client import NeonChainWeb3Client + +class StorageContract: + web3_client: NeonChainWeb3Client + storage_contract: typing.Any + + def __init__(self, web3_client, storage_contract): + self._web3_client = web3_client + self._storage_contract = storage_contract + self.contract_address = storage_contract.address + + def store_value(self, sender_account, value): + tx = self._web3_client.make_raw_tx(sender_account) + instruction_tx = self._storage_contract.functions.store(value).build_transaction(tx) + receipt = self._web3_client.send_transaction(sender_account, instruction_tx) + assert receipt["status"] == 1 + return receipt + + def retrieve_value(self, sender_account): + tx = self._web3_client.make_raw_tx(sender_account) + instruction_tx = self._storage_contract.functions.retrieve().build_transaction(tx) + receipt = self._web3_client.send_transaction(sender_account, instruction_tx) + assert receipt["status"] == 1 + return instruction_tx, receipt + + def retrieve_doubled_value(self, sender_account, value): + tx = self._web3_client.make_raw_tx(sender_account) + instruction_tx = self._storage_contract.functions.returnDoubledNumber(value).build_transaction(tx) + receipt = self._web3_client.send_transaction(sender_account, instruction_tx) + assert receipt["status"] == 1 + return instruction_tx, receipt + + def call_storage(self, sender_account, storage_value, request_type): + request_value = None + self.store_value(sender_account, storage_value) + tx, receipt = self.retrieve_value(sender_account) + + tx_obj = self._web3_client.make_raw_tx( + from_=sender_account.address, + to=self._storage_contract.address, + amount=tx["value"], + gas=hex(tx["gas"]), + gas_price=hex(tx["gasPrice"]), + data=tx["data"], + estimate_gas=False, + ) + del tx_obj["chainId"] + del tx_obj["nonce"] + + if request_type == "blockNumber": + request_value = hex(receipt[request_type]) + else: + request_value = receipt[request_type].hex() + return tx_obj, request_value, receipt + + def retrieve_block(self, sender_account): + tx = self._web3_client.make_raw_tx(sender_account) + instruction_tx = self._storage_contract.functions.storeBlock().build_transaction(tx) + receipt = self._web3_client.send_transaction(sender_account, instruction_tx) + assert receipt["status"] == 1 + return receipt + + def retrieve_block_timestamp(self, sender_account): + tx = self._web3_client.make_raw_tx(sender_account) + instruction_tx = self._storage_contract.functions.storeBlockTimestamp().build_transaction(tx) + receipt = self._web3_client.send_transaction(sender_account, instruction_tx) + assert receipt["status"] == 1 + return receipt + + def retrieve_block_info(self, sender_account): + tx = self._web3_client.make_raw_tx(sender_account) + instruction_tx = self._storage_contract.functions.storeBlockInfo().build_transaction(tx) + receipt = self._web3_client.send_transaction(sender_account, instruction_tx) + assert receipt["status"] == 1 + return receipt + + def retrieve_sum_of_values(self, sender_account, value_1, value_2): + tx = self._web3_client.make_raw_tx(sender_account) + instruction_tx = self._storage_contract.functions.storeSumOfNumbers(value_1, value_2).build_transaction(tx) + receipt = self._web3_client.send_transaction(sender_account, instruction_tx) + assert receipt["status"] == 1 + return receipt diff --git a/utils/tracer_client.py b/utils/tracer_client.py index a3eca1b1b4..f976413370 100644 --- a/utils/tracer_client.py +++ b/utils/tracer_client.py @@ -6,9 +6,9 @@ def __init__(self, url): self.url = url self.tracer_api = JsonRPCSession(url) - def send_rpc_and_wait_response(self, method_name, params): + def send_rpc_and_wait_response(self, method_name, params, req_type=None): wait_condition( - lambda: self.tracer_api.send_rpc(method=method_name, params=params)["result"] + lambda: self.tracer_api.send_rpc(method=method_name, params=params, req_type=req_type)["result"] is not None, timeout_sec=120, ) diff --git a/utils/types.py b/utils/types.py index 2185c089ec..6a7fb514a4 100644 --- a/utils/types.py +++ b/utils/types.py @@ -1,35 +1,38 @@ import typing as tp +import enum from dataclasses import dataclass -from solana.publickey import PublicKey -from solana.keypair import Keypair + +from solders.pubkey import Pubkey +from solders.keypair import Keypair @dataclass class TreasuryPool: index: int - account: PublicKey + account: Pubkey buffer: bytes @dataclass class Caller: solana_account: Keypair - solana_account_address: PublicKey - balance_account_address: PublicKey + solana_account_address: Pubkey + balance_account_address: Pubkey eth_address: bytes - token_address: PublicKey + token_address: Pubkey @dataclass class Contract: eth_address: bytes - solana_address: PublicKey - balance_account_address: PublicKey + solana_address: Pubkey + balance_account_address: Pubkey + @dataclass class TreasuryPool: index: int - account: PublicKey + account: Pubkey buffer: bytes @@ -43,3 +46,11 @@ class TreasuryPool: "evm", "compiler_compatibility", ] + + +class TransactionType(enum.IntEnum): + LEGACY = 0 + EIP_1559 = 2 + + +RepoType = tp.Literal["proxy", "evm", "tests"] diff --git a/utils/version.py b/utils/version.py new file mode 100644 index 0000000000..a0b7fc469e --- /dev/null +++ b/utils/version.py @@ -0,0 +1,6 @@ +import re + + +def remove_heading_chars_till_first_digit(input_string: str) -> str: + result = re.sub(r'^[^\d]*', '', input_string) + return result diff --git a/utils/web3client.py b/utils/web3client.py index 7f4df96430..3f52b097ff 100644 --- a/utils/web3client.py +++ b/utils/web3client.py @@ -1,5 +1,6 @@ import json import pathlib +import sys import time import typing as tp from decimal import Decimal @@ -11,11 +12,14 @@ import web3 import web3.types from eth_abi import abi +from eth_typing import BlockIdentifier from web3.exceptions import TransactionNotFound +from utils.types import TransactionType from utils import helpers from utils.consts import InputTestConstants, Unit -from utils.helpers import decode_function_signature +from utils.helpers import decode_function_signature, case_snake_to_camel + LOG = logging.getLogger(__name__) @@ -115,8 +119,14 @@ def gas_price(self): gas = self._web3.eth.gas_price return gas + @allure.step("Get base fee per gas") + def base_fee_per_gas(self) -> int: + latest_block: web3.types.BlockData = self._web3.eth.get_block(block_identifier="latest") # noqa + base_fee = latest_block.baseFeePerGas # noqa + return base_fee + @allure.step("Create account") - def create_account(self): + def create_account(self) -> eth_account.signers.local.LocalAccount: return self._web3.eth.account.create() @allure.step("Get block number") @@ -131,7 +141,7 @@ def get_block_number_by_id(self, block_identifier): def get_nonce( self, address: tp.Union[eth_account.signers.local.LocalAccount, str], - block: str = "pending", + block: BlockIdentifier = "pending", ): address = address if isinstance(address, str) else address.address return self._web3.eth.get_transaction_count(address, block) @@ -150,22 +160,23 @@ def deploy_contract( gas_price: tp.Optional[int] = None, constructor_args: tp.Optional[tp.List] = None, value=0, + tx_type: TransactionType = 0, ) -> web3.types.TxReceipt: """Proxy doesn't support send_transaction""" - gas_price = gas_price or self.gas_price() constructor_args = constructor_args or [] contract = self._web3.eth.contract(abi=abi, bytecode=bytecode) - transaction = contract.constructor(*constructor_args).build_transaction( - { - "from": from_.address, - "gas": gas, - "gasPrice": gas_price, - "nonce": self.get_nonce(from_), - "value": value, - "chainId": self.chain_id, - } - ) + tx_params = { + "from": from_.address, + "gas": gas, + "nonce": self.get_nonce(from_), + "value": value, + "chainId": self.chain_id, + } + if tx_type is TransactionType.LEGACY: + tx_params["gasPrice"] = gas_price or self.gas_price() + + transaction = contract.constructor(*constructor_args).build_transaction(tx_params) if transaction["gas"] == 0: transaction["gas"] = self._web3.eth.estimate_gas(transaction) @@ -176,48 +187,68 @@ def deploy_contract( @allure.step("Make raw tx") def make_raw_tx( - self, - from_: tp.Union[str, eth_account.signers.local.LocalAccount], - to: tp.Optional[tp.Union[str, eth_account.signers.local.LocalAccount]] = None, - amount: tp.Optional[tp.Union[int, float, Decimal]] = None, - gas: tp.Optional[int] = None, - gas_price: tp.Optional[int] = None, - nonce: tp.Optional[int] = None, - chain_id: tp.Optional[int] = None, - data: tp.Optional[tp.Union[str, bytes]] = None, - estimate_gas=False, + self, + from_: tp.Union[str, eth_account.signers.local.LocalAccount], + to: tp.Optional[tp.Union[str, eth_account.signers.local.LocalAccount]] = None, + amount: tp.Optional[tp.Union[int, float, Decimal]] = None, + gas: tp.Optional[int] = None, + gas_price: tp.Optional[int] = None, + nonce: tp.Optional[int] = None, + chain_id: tp.Optional[int] = None, + data: tp.Optional[tp.Union[str, bytes]] = None, + estimate_gas=False, + tx_type: TransactionType = TransactionType.LEGACY, ) -> dict: - if isinstance(from_, eth_account.signers.local.LocalAccount): - transaction = {"from": from_.address} + if tx_type is TransactionType.LEGACY: + if isinstance(from_, eth_account.signers.local.LocalAccount): + transaction = {"from": from_.address} + else: + transaction = {"from": from_} + + if to: + if isinstance(to, eth_account.signers.local.LocalAccount): + transaction["to"] = to.address + if isinstance(to, str): + transaction["to"] = to + if amount: + transaction["value"] = amount + if data: + transaction["data"] = data + if nonce is None: + transaction["nonce"] = self.get_nonce(from_) + else: + transaction["nonce"] = nonce + + if chain_id is None: + transaction["chainId"] = self.chain_id + elif chain_id: + transaction["chainId"] = chain_id + + if gas_price is None: + gas_price = self.gas_price() + transaction["gasPrice"] = gas_price + if estimate_gas and not gas: + gas = self._web3.eth.estimate_gas(transaction) + if gas: + transaction["gas"] = gas else: - transaction = {"from": from_} - - if to: - if isinstance(to, eth_account.signers.local.LocalAccount): - transaction["to"] = to.address - if isinstance(to, str): - transaction["to"] = to - if amount: - transaction["value"] = amount - if data: - transaction["data"] = data - if nonce is None: - transaction["nonce"] = self.get_nonce(from_) - else: - transaction["nonce"] = nonce - - if chain_id is None: - transaction["chainId"] = self.chain_id - elif chain_id: - transaction["chainId"] = chain_id - - if gas_price is None: - gas_price = self.gas_price() - transaction["gasPrice"] = gas_price - if estimate_gas and not gas: - gas = self._web3.eth.estimate_gas(transaction) - if gas: - transaction["gas"] = gas + if gas_price is not None and gas is not None: + max_priority_fee_per_gas, max_fee_per_gas = self.gas_price_to_eip1559_params(gas_price=gas_price) + else: + max_priority_fee_per_gas = max_fee_per_gas = "auto" + + transaction = self.make_raw_tx_eip_1559( + chain_id="auto" if chain_id is None else chain_id, + from_=from_, + to=to, + value=amount, + nonce="auto" if nonce is None else nonce, + data=data, + access_list=None, + gas="auto" if estimate_gas else gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + max_fee_per_gas=max_fee_per_gas, + ) return transaction @allure.step("Send transaction") @@ -232,6 +263,76 @@ def send_transaction( signature = self._web3.eth.send_raw_transaction(instruction_tx.rawTransaction) return self._web3.eth.wait_for_transaction_receipt(signature, timeout=timeout) + @allure.step("Create raw transaction EIP-1559") + def make_raw_tx_eip_1559( + self, + *, + chain_id: tp.Union[int, tp.Literal["auto"], None], + from_: tp.Union[str, eth_account.signers.local.LocalAccount], + to: tp.Optional[tp.Union[str, eth_account.signers.local.LocalAccount]], + value: tp.Union[int, float, Decimal, str, None], + nonce: tp.Union[int, tp.Literal["auto"], None], + data: tp.Union[str, bytes, None], + access_list: tp.Union[tp.List[web3.types.AccessListEntry], None], + gas: tp.Union[int, tp.Literal["auto"], None], + max_priority_fee_per_gas: tp.Union[int, tp.Literal["auto"], None], + max_fee_per_gas: tp.Union[int, tp.Literal["auto"], None], + base_fee_multiplier: float = 1.1, + ) -> web3.types.TxParams: + + # Handle addresses + if isinstance(from_, eth_account.signers.local.LocalAccount): + from_ = from_.address + + if isinstance(to, eth_account.signers.local.LocalAccount): + to = to.address + + # Create a copy of local variables and remove the redundant ones + kwargs = locals().copy() + del kwargs["self"] + del kwargs["base_fee_multiplier"] + + # Move parameters related to gas to the end as they should be handled last + for arg_name in ("gas", "max_priority_fee_per_gas", "max_fee_per_gas"): + arg_value = kwargs[arg_name] + del kwargs[arg_name] + kwargs.update({arg_name: arg_value}) + + # Initialize parameters with a default value "type": 2 for EIP-1559 transactions + params = {"type": TransactionType.EIP_1559} + + # Map parameters with 'auto' value to their corresponding values + base_fee_per_gas = 10 + + if max_priority_fee_per_gas == "auto" or max_fee_per_gas == "auto": + base_fee_per_gas = self.base_fee_per_gas() + + auto_map = { + "chain_id": lambda: self.chain_id, + "nonce": lambda: self.get_nonce(from_), + "gas": lambda: self._web3.eth.estimate_gas(params), + "max_priority_fee_per_gas": self._web3.eth._max_priority_fee, # noqa + "max_fee_per_gas": lambda: int((base_fee_per_gas * base_fee_multiplier) + params["maxPriorityFeePerGas"]), + } + + # Iterate over parameters and add them to the params dictionary + for param_name, param_value in kwargs.items(): + if param_value is None: + continue + + if param_value == "auto": + # get the auto value + param_value = auto_map[param_name]() + + # Convert parameter name from snake_case to camelCase + camel_case = case_snake_to_camel(param_name) + + # Add parameter to the params dictionary + params[camel_case] = param_value + + # params keys validation happens here + return web3.types.TxParams(params) + @allure.step("Deploy and get contract") def deploy_and_get_contract( self, @@ -244,6 +345,7 @@ def deploy_and_get_contract( libraries: tp.Optional[dict] = None, gas: tp.Optional[int] = 0, value=0, + tx_type: TransactionType = TransactionType.LEGACY, ) -> tp.Tuple[tp.Any, web3.types.TxReceipt]: contract_interface = helpers.get_contract_interface( contract, @@ -260,6 +362,7 @@ def deploy_and_get_contract( constructor_args=constructor_args, gas=gas, value=value, + tx_type=tx_type, ) contract = self.eth.contract(address=contract_deploy_tx["contractAddress"], abi=contract_interface["abi"]) @@ -335,15 +438,68 @@ def send_tokens( gas: tp.Optional[int] = None, gas_price: tp.Optional[int] = None, nonce: int = None, + tx_type: TransactionType = TransactionType.LEGACY, ) -> web3.types.TxReceipt: - transaction = self.make_raw_tx( - from_, to, amount=value, gas=gas, gas_price=gas_price, nonce=nonce, estimate_gas=True - ) - + if tx_type is TransactionType.LEGACY: + transaction = self.make_raw_tx( + from_, to, amount=value, gas=gas, gas_price=gas_price, nonce=nonce, estimate_gas=True + ) + else: + if gas_price is not None: + max_priority_fee_per_gas, max_fee_per_gas = self.gas_price_to_eip1559_params(gas_price=gas_price) + else: + max_priority_fee_per_gas = max_fee_per_gas = "auto" + + transaction = self.make_raw_tx_eip_1559( + chain_id="auto", + from_=from_, + to=to, + value=value, + nonce="auto" if nonce is None else nonce, + data=None, + access_list=None, + gas="auto" if gas is None else gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + max_fee_per_gas=max_fee_per_gas, + ) signed_tx = self.eth.account.sign_transaction(transaction, from_.key) tx = self.eth.send_raw_transaction(signed_tx.rawTransaction) return self.eth.wait_for_transaction_receipt(tx) + @allure.step("Send tokens under EIP-1559") + def send_tokens_eip_1559( + self, + *, + from_: eth_account.signers.local.LocalAccount, + to: tp.Union[str, eth_account.signers.local.LocalAccount], + value: tp.Union[int, float, Decimal, str, None], + chain_id: tp.Union[int, tp.Literal["auto"], None] = "auto", + nonce: tp.Union[int, tp.Literal["auto"], None] = "auto", + gas: tp.Union[int, tp.Literal["auto"], None] = "auto", + max_priority_fee_per_gas: tp.Union[int, tp.Literal["auto"], None] = "auto", + max_fee_per_gas: tp.Union[int, tp.Literal["auto"], None] = "auto", + base_fee_multiplier: float = 1.1, + access_list: tp.Optional[tp.List[web3.types.AccessListEntry]] = None, + timeout: int = 120, + ) -> web3.types.TxReceipt: + + tx_params = self.make_raw_tx_eip_1559( + chain_id=chain_id, + from_=from_.address, + to=to, + value=value, + nonce=nonce, + gas=gas, + max_priority_fee_per_gas=max_priority_fee_per_gas, + max_fee_per_gas=max_fee_per_gas, + data=None, + access_list=access_list, + base_fee_multiplier=base_fee_multiplier, + ) + + receipt = self.send_transaction(account=from_, transaction=tx_params, timeout=timeout) + return receipt + @allure.step("Send all neons from one account to another") def send_all_neons( self, @@ -394,6 +550,16 @@ def get_token_usd_gas_price(self): ).json() return int(resp["result"]["tokenPriceUsd"], 16) / 100000 + def gas_price_to_eip1559_params(self, gas_price: int) -> tuple[int, int]: + base_fee_per_gas = self.base_fee_per_gas() + + msg = f"gas_price {gas_price} is lower than the baseFeePerGas {base_fee_per_gas}" + assert gas_price >= base_fee_per_gas, msg + + max_fee_per_gas = gas_price + max_priority_fee_per_gas = max_fee_per_gas - base_fee_per_gas + return max_priority_fee_per_gas, max_fee_per_gas + class NeonChainWeb3Client(Web3Client): def __init__( @@ -410,7 +576,7 @@ def create_account_with_balance( faucet, amount: int = InputTestConstants.NEW_USER_REQUEST_AMOUNT.value, bank_account=None, - ): + ) -> eth_account.signers.local.LocalAccount: """Creates a new account with balance""" account = self.create_account() if bank_account is not None: