diff --git a/.github/actions/destroy-tf-stand/action.yml b/.github/actions/destroy-tf-stand/action.yml new file mode 100644 index 0000000000..0209273667 --- /dev/null +++ b/.github/actions/destroy-tf-stand/action.yml @@ -0,0 +1,38 @@ +name: "Destroy stand" +description: "Destroy stand created by terraform" +inputs: + ci_stands_key_hcloud: + description: 'Private key for hcloud' + required: true + devnet_solana_url: + description: 'Solana url for devnet' + required: true +runs: + using: composite + steps: + - name: Install python requirements + if: always() + id: requirements + uses: ./.github/actions/python-requirements + - name: Prepare server for destroy terraform stand + if: always() + shell: bash + run: | + ssh_key=/tmp/ci-stands + echo "${{ inputs.ci_stands_key_hcloud }}" >> ${ssh_key} && chmod 400 ${ssh_key} + - name: Download docker logs + shell: bash + run: python3 ./clickfile.py infra download-logs + - name: Destroy stand with terraform + shell: bash + id: destroy + env: + TF_VAR_ci_pp_solana_url: ${{inputs.devnet_solana_url}} + if: always() + run: | + python3 ./clickfile.py infra destroy + - uses: actions/upload-artifact@v3 + if: always() + with: + name: AWS docker logs + path: ./logs/* \ No newline at end of file diff --git a/.github/actions/dockerize-neon-tests/action.yml b/.github/actions/dockerize-neon-tests/action.yml index 19f262a1bd..d86047baf2 100644 --- a/.github/actions/dockerize-neon-tests/action.yml +++ b/.github/actions/dockerize-neon-tests/action.yml @@ -8,8 +8,8 @@ inputs: description: 'branch name for oz tests' required: true default: master - version_tag: - description: 'neon-tests version tag' + branch_tag: + description: 'tag for feature or version branch' required: false default: '' docker_username: @@ -30,8 +30,8 @@ runs: 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_BRANCH='${{ inputs.oz_branch }}' - if [[ "${{ inputs.version_tag }}" != '' ]]; then - docker tag ${image_id}:${{ inputs.image_tag }} ${image_id}:${{ inputs.version_tag }} + if [[ "${{ inputs.branch_tag }}" != '' ]]; then + docker tag ${image_id}:${{ inputs.image_tag }} ${image_id}:${{ inputs.branch_tag }} fi; echo "${delimeter} Login into Docker registry as ${{ inputs.docker_username }} ${delimeter}" echo "${{ inputs.docker_password }}" | docker login -u ${{ inputs.docker_username }} --password-stdin diff --git a/.github/actions/prepare-stand/action.yml b/.github/actions/prepare-stand/action.yml new file mode 100644 index 0000000000..4a457c91ca --- /dev/null +++ b/.github/actions/prepare-stand/action.yml @@ -0,0 +1,120 @@ +name: "Prepare stand" +description: "Prepare environments for running tests and create terraform stand if it needed" +outputs: + solana_url: + description: "solana url" + value: ${{ steps.share.outputs.solana_url }} + proxy_url: + description: "proxy url" + value: ${{ steps.share.outputs.proxy_url }} + faucet_url: + description: "faucet url" + value: ${{ steps.share.outputs.faucet_url }} + network_id: + description: "network id" + value: ${{ steps.share.outputs.network_id }} + proxy_ip: + description: "proxy ip" + value: ${{ steps.share.outputs.proxy_ip }} + solana_ip: + description: "solana ip" + value: ${{ steps.share.outputs.solana_ip }} +inputs: + network: + description: 'Stand name' + required: true + ci_stands_key_hcloud: + description: 'Private key for hcloud' + required: false + devnet_solana_url: + description: 'Solana url for devnet' + required: false + tfstate_bucket: + description: 'Terraform state bucket' + required: false + tfstate_key_prefix: + description: 'Terraform state key prefix' + required: false + tfstate_region: + description: 'Terraform state region' + required: false + proxy_url: + description: 'Proxy url if network is custom' + required: false + faucet_url: + description: 'Faucet url if network is custom' + required: false + solana_url: + description: 'Solana url if network is custom' + required: false + network_id: + description: 'Network id if network is custom' + required: false +runs: + using: composite + + steps: + - name: Install python requirements + id: requirements + uses: ./.github/actions/python-requirements + - name: Deploy stand with terraform + id: deploy + env: + TF_VAR_ci_pp_solana_url: ${{inputs.devnet_solana_url}} + shell: bash + if: inputs.network == 'terraform' + run: | + ssh_key='/tmp/ci-stands' + echo "${{ inputs.ci_stands_key_hcloud }}" >> ${ssh_key} && chmod 400 ${ssh_key} + python3 ./clickfile.py infra deploy + - uses: actions/upload-artifact@v3 + if: inputs.network == 'terraform' + with: + name: tf-state + path: deploy/hetzner/ + + - name: Set outputs + id: share + env: + SOLANA_IP: ${{ env.SOLANA_IP }} + PROXY_IP: ${{ env.PROXY_IP }} + TFSTATE_BUCKET: ${{ inputs.tfstate_bucket }} + TFSTATE_KEY_PREFIX: ${{ inputs.tfstate_key_prefix }} + TFSTATE_REGION: ${{ inputs.tfstate_region }} + shell: bash + run: | + if [[ "${{inputs.network}}" == "custom" ]]; then + proxy_url=${{ inputs.proxy_url }} + faucet_url=${{ inputs.faucet_url }} + solana_url=${{ inputs.solana_url }} + network_id=${{ inputs.network_id }} + else + proxy_url=`python ./clickfile.py infra print-network-param -n '${{inputs.network}}' -p 'proxy_url'` + solana_url=`python ./clickfile.py infra print-network-param -n '${{inputs.network}}' -p 'solana_url'` + faucet_url=`python ./clickfile.py infra print-network-param -n '${{inputs.network}}' -p 'faucet_url'` + network_id=`python ./clickfile.py infra print-network-param -n '${{inputs.network}}' -p 'network_ids.neon'` + fi; + + if [[ "${{ inputs.network }}" == "devnet" ]]; then + solana_url="${{ inputs.devnet_solana_url }}" + fi; + echo "proxy_url=$proxy_url" + echo "solana_url=$solana_url" + echo "faucet_url=$faucet_url" + echo "network_id=$network_id" + echo "solana_ip=${{ env.SOLANA_IP }}" + echo "proxy_ip=${{ env.PROXY_IP }}" + echo "proxy_url=$proxy_url" >> $GITHUB_OUTPUT + echo "solana_url=$solana_url" >> $GITHUB_OUTPUT + echo "faucet_url=$faucet_url" >> $GITHUB_OUTPUT + echo "network_id=$network_id" >> $GITHUB_OUTPUT + echo "proxy_ip=${{ env.PROXY_IP }}" >> $GITHUB_OUTPUT + echo "solana_ip=${{ env.SOLANA_IP }}" >> $GITHUB_OUTPUT + + - name: Wait until proxy is ready + shell: bash + run: | + while [[ "$(curl -s -X POST -o /dev/null -w ''%{http_code}'' ${{ steps.share.outputs.proxy_url }})" != "200" ]]; + do echo "Proxy is not ready yet. Waiting 5 seconds..."; + sleep 5; + done diff --git a/.github/workflows/dapps.yml b/.github/workflows/dapps.yml index 41d7593bf0..dd81db6841 100644 --- a/.github/workflows/dapps.yml +++ b/.github/workflows/dapps.yml @@ -14,7 +14,7 @@ on: options: - night-stand - devnet - - aws + - terraform - custom dapps: type: string @@ -41,6 +41,7 @@ on: required: false description: "Url to send the report as comment for PR" env: + NETWORK: ${{ github.event.inputs.network || 'night-stand' }} AWS_ACCESS_KEY_ID: ${{ secrets.DAPPS_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.DAPPS_AWS_SECRET_ACCESS_KEY }} AWS_REGION: us-east-2 @@ -63,95 +64,39 @@ jobs: echo "list=${list}" echo "list=${list}" >> $GITHUB_OUTPUT - - name: Define network - id: stand - run: | - if [[ "${{ github.event.inputs.network }}" != "" ]]; then - network=${{ github.event.inputs.network }} - else - network=aws - fi; - echo "network=${network}" - echo "network=${network}" >> $GITHUB_OUTPUT - - - name: Install terraform - id: terraform - if: steps.stand.outputs.network == 'aws' - run: | - sudo apt-get update && sudo apt-get install -y python-dev gnupg software-properties-common wget jq - wget -O- https://apt.releases.hashicorp.com/gpg | \ - gpg --dearmor | \ - sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg - gpg --no-default-keyring \ - --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg \ - --fingerprint - echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \ - https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \ - sudo tee /etc/apt/sources.list.d/hashicorp.list - sudo apt update - sudo apt-get install terraform - - - - name: Install python requirements - id: requirements - uses: ./.github/actions/python-requirements - - - name: Deploy stand with terraform - id: deploy - if: steps.stand.outputs.network == 'aws' - run: | - ssh_key=/tmp/dapps-stand - echo "${{ secrets.SSH_DAPPS_PRIVATE_KEY }}" >> ${ssh_key} && chmod 400 ${ssh_key} - python3 ./clickfile.py infra deploy - - uses: actions/upload-artifact@v3 - if: steps.stand.outputs.network == 'aws' + - name: "Prepare stand" + id: prepare_stand + env: + AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}} + AWS_DEFAULT_REGION: ${{secrets.AWS_DEFAULT_REGION}} + AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}} + HCLOUD_TOKEN: ${{secrets.HCLOUD_TOKEN}} + TFSTATE_BUCKET: ${{vars.TFSTATE_BUCKET}} + TFSTATE_KEY_PREFIX: ${{vars.TFSTATE_KEY_PREFIX}} + TFSTATE_REGION: ${{vars.TFSTATE_REGION}} + uses: ./.github/actions/prepare-stand with: - name: tf-state - path: deploy/aws/ - + network: ${{ env.NETWORK }} + ci_stands_key_hcloud: ${{ secrets.CI_STANDS_KEY_HCLOUD }} + devnet_solana_url: ${{ secrets.SOLANA_URL }} + proxy_url: ${{ github.event.inputs.proxy_url }} + solana_url: ${{ github.event.inputs.solana_url }} + faucet_url: ${{ github.event.inputs.faucet_url }} + network_id: ${{ github.event.inputs.network_id }} - name: Set outputs - id: share - env: - SOLANA_IP: ${{ env.SOLANA_IP }} - PROXY_IP: ${{ env.PROXY_IP }} - run: | - if [[ "${{steps.stand.outputs.network}}" == "custom" ]]; then - proxy_url=${{ github.event.inputs.proxy_url }} - faucet_url=${{ github.event.inputs.faucet_url }} - solana_url=${{ github.event.inputs.solana_url }} - network_id=${{ github.event.inputs.network_id }} - else - proxy_url=`python ./clickfile.py infra print-network-param -n '${{steps.stand.outputs.network}}' -p 'proxy_url'` - solana_url=`python ./clickfile.py infra print-network-param -n '${{steps.stand.outputs.network}}' -p 'solana_url'` - faucet_url=`python ./clickfile.py infra print-network-param -n '${{steps.stand.outputs.network}}' -p 'faucet_url'` - network_id=`python ./clickfile.py infra print-network-param -n '${{steps.stand.outputs.network}}' -p 'network_id'` - fi; - - if [[ "${{steps.stand.outputs.network}}" == "devnet" ]]; then - solana_url="${{ secrets.SOLANA_URL }}" - fi; - echo "proxy_url=$proxy_url" - echo "solana_url=$solana_url" - echo "faucet_url=$faucet_url" - echo "network_id=$network_id" - echo "proxy_url=$proxy_url" >> $GITHUB_OUTPUT - echo "solana_url=$solana_url" >> $GITHUB_OUTPUT - echo "faucet_url=$faucet_url" >> $GITHUB_OUTPUT - echo "network_id=$network_id" >> $GITHUB_OUTPUT - - - name: Wait until proxy is ready - timeout-minutes: 5 + id: vars run: | - while [[ "$(curl -s -X POST -o /dev/null -w ''%{http_code}'' ${{ steps.share.outputs.proxy_url }})" != "200" ]]; - do echo "Proxy is not ready yet. Waiting 5 seconds..."; - sleep 5; - done + echo "runner=${{ env.RUNNER }}" >> $GITHUB_OUTPUT + # echo "network=${{ env.NETWORK }}" >> $GITHUB_OUTPUT outputs: - solana_url: ${{ steps.share.outputs.solana_url }} - proxy_url: ${{ steps.share.outputs.proxy_url }} - faucet_url: ${{ steps.share.outputs.faucet_url }} - network_id: ${{ steps.share.outputs.network_id }} - network: ${{ steps.stand.outputs.network }} + runner: ${{ steps.vars.outputs.runner }} + # network: ${{ steps.vars.outputs.network }} + solana_url: ${{ steps.prepare_stand.outputs.solana_url }} + proxy_url: ${{ steps.prepare_stand.outputs.proxy_url }} + faucet_url: ${{ steps.prepare_stand.outputs.faucet_url }} + network_id: ${{ steps.prepare_stand.outputs.network_id }} + proxy_ip: ${{ steps.prepare_stand.outputs.proxy_ip }} + solana_ip: ${{ steps.prepare_stand.outputs.solana_ip }} dapps: ${{ steps.dapps.outputs.list }} uniswap-v2: @@ -164,7 +109,8 @@ jobs: PROXY_URL: ${{ needs.prepare.outputs.proxy_url }} FAUCET_URL: ${{ needs.prepare.outputs.faucet_url }} NETWORK_ID: ${{ needs.prepare.outputs.network_id }} - NETWORK_NAME: ${{ needs.prepare.outputs.network }} + SOLANA_IP: ${{ needs.prepare.outputs.solana_ip }} + PROXY_IP: ${{ needs.prepare.outputs.proxy_ip }} DUMP_ENVS: True steps: - uses: actions/checkout@v3 @@ -175,7 +121,7 @@ jobs: - name: Prepare accounts id: accounts run: | - python3 ./clickfile.py infra gen-accounts -c 3 -a 10000 -n ${{env.NETWORK_NAME}} + python3 ./clickfile.py infra gen-accounts -c 3 -a 10000 -n ${{env.NETWORK}} - name: Launch uniswap v2 tests if: ${{ steps.accounts.outcome == 'success' }} @@ -208,11 +154,12 @@ jobs: needs: - prepare env: + SOLANA_IP: ${{ needs.prepare.outputs.solana_ip }} + PROXY_IP: ${{ needs.prepare.outputs.proxy_ip }} SOLANA_URL: ${{ needs.prepare.outputs.solana_url }} PROXY_URL: ${{ needs.prepare.outputs.proxy_url }} FAUCET_URL: ${{ needs.prepare.outputs.faucet_url }} NETWORK_ID: ${{ needs.prepare.outputs.network_id }} - NETWORK_NAME: ${{ needs.prepare.outputs.network }} DUMP_ENVS: True steps: - uses: actions/checkout@v3 @@ -223,7 +170,7 @@ jobs: - name: Prepare accounts id: accounts run: | - python3 ./clickfile.py infra gen-accounts -c 3 -a 10000 -n ${{env.NETWORK_NAME}} + python3 ./clickfile.py infra gen-accounts -c 3 -a 10000 -n ${{env.NETWORK}} - name: Launch uniswap v3 tests if: ${{ steps.accounts.outcome == 'success'}} @@ -259,7 +206,8 @@ jobs: PROXY_URL: ${{ needs.prepare.outputs.proxy_url }} FAUCET_URL: ${{ needs.prepare.outputs.faucet_url }} NETWORK_ID: ${{ needs.prepare.outputs.network_id }} - NETWORK_NAME: ${{ needs.prepare.outputs.network }} + SOLANA_IP: ${{ needs.prepare.outputs.solana_ip }} + PROXY_IP: ${{ needs.prepare.outputs.proxy_ip }} DUMP_ENVS: True steps: - uses: actions/checkout@v3 @@ -270,7 +218,7 @@ jobs: - name: Prepare accounts id: accounts run: | - python3 ./clickfile.py infra gen-accounts -c 4 -a 10000 -n ${{env.NETWORK_NAME}} + python3 ./clickfile.py infra gen-accounts -c 4 -a 10000 -n ${{env.NETWORK}} - name: Launch saddle tests if: ${{ steps.accounts.outcome == 'success' }} @@ -307,7 +255,8 @@ jobs: PROXY_URL: ${{ needs.prepare.outputs.proxy_url }} FAUCET_URL: ${{ needs.prepare.outputs.faucet_url }} NETWORK_ID: ${{ needs.prepare.outputs.network_id }} - NETWORK_NAME: ${{ needs.prepare.outputs.network }} + SOLANA_IP: ${{ needs.prepare.outputs.solana_ip }} + PROXY_IP: ${{ needs.prepare.outputs.proxy_ip }} DUMP_ENVS: True steps: - uses: actions/checkout@v3 @@ -318,7 +267,7 @@ jobs: - name: Prepare accounts id: accounts run: | - python3 ./clickfile.py infra gen-accounts -c 5 -a 20000 -n ${{env.NETWORK_NAME}} + python3 ./clickfile.py infra gen-accounts -c 5 -a 20000 -n ${{env.NETWORK}} - name: Launch aave tests if: ${{ steps.accounts.outcome == 'success' }} @@ -348,7 +297,8 @@ jobs: PROXY_URL: ${{ needs.prepare.outputs.proxy_url }} FAUCET_URL: ${{ needs.prepare.outputs.faucet_url }} NETWORK_ID: ${{ needs.prepare.outputs.network_id }} - NETWORK_NAME: ${{ needs.prepare.outputs.network }} + SOLANA_IP: ${{ needs.prepare.outputs.solana_ip }} + PROXY_IP: ${{ needs.prepare.outputs.proxy_ip }} DUMP_ENVS: True steps: - uses: actions/checkout@v3 @@ -359,7 +309,7 @@ jobs: - name: Prepare accounts id: accounts run: | - python3 ./clickfile.py infra gen-accounts -c 8 -a 10000 -n ${{env.NETWORK_NAME}} + python3 ./clickfile.py infra gen-accounts -c 8 -a 10000 -n ${{env.NETWORK}} - name: Launch curve tests id: curve @@ -394,7 +344,8 @@ jobs: PROXY_URL: ${{ needs.prepare.outputs.proxy_url }} FAUCET_URL: ${{ needs.prepare.outputs.faucet_url }} NETWORK_ID: ${{ needs.prepare.outputs.network_id }} - NETWORK_NAME: ${{ needs.prepare.outputs.network }} + SOLANA_IP: ${{ needs.prepare.outputs.solana_ip }} + PROXY_IP: ${{ needs.prepare.outputs.proxy_ip }} DUMP_ENVS: True steps: - uses: actions/checkout@v3 @@ -423,7 +374,8 @@ jobs: PROXY_URL: ${{ needs.prepare.outputs.proxy_url }} FAUCET_URL: ${{ needs.prepare.outputs.faucet_url }} NETWORK_ID: ${{ needs.prepare.outputs.network_id }} - NETWORK_NAME: ${{ needs.prepare.outputs.network }} + SOLANA_IP: ${{ needs.prepare.outputs.solana_ip }} + PROXY_IP: ${{ needs.prepare.outputs.proxy_ip }} DUMP_ENVS: True steps: - uses: actions/checkout@v3 @@ -434,7 +386,7 @@ jobs: - name: Prepare accounts id: accounts run: | - python3 ./clickfile.py infra gen-accounts -c 2 -a 10000 -n ${{env.NETWORK_NAME}} + python3 ./clickfile.py infra gen-accounts -c 2 -a 10000 -n ${{env.NETWORK}} - name: Launch compound tests timeout-minutes: 30 @@ -469,7 +421,8 @@ jobs: PROXY_URL: ${{ needs.prepare.outputs.proxy_url }} FAUCET_URL: ${{ needs.prepare.outputs.faucet_url }} NETWORK_ID: ${{ needs.prepare.outputs.network_id }} - NETWORK_NAME: ${{ needs.prepare.outputs.network }} + SOLANA_IP: ${{ needs.prepare.outputs.solana_ip }} + PROXY_IP: ${{ needs.prepare.outputs.proxy_ip }} DUMP_ENVS: True steps: - uses: actions/checkout@v3 @@ -480,7 +433,7 @@ jobs: - name: Prepare accounts id: accounts run: | - python3 ./clickfile.py infra gen-accounts -c 3 -a 10000 -n ${{env.NETWORK_NAME}} + python3 ./clickfile.py infra gen-accounts -c 3 -a 10000 -n ${{env.NETWORK}} - name: Launch robonomics tests if: ${{ steps.accounts.outcome == 'success' }} @@ -513,7 +466,8 @@ jobs: PROXY_URL: ${{ needs.prepare.outputs.proxy_url }} FAUCET_URL: ${{ needs.prepare.outputs.faucet_url }} NETWORK_ID: ${{ needs.prepare.outputs.network_id }} - NETWORK_NAME: ${{ needs.prepare.outputs.network }} + SOLANA_IP: ${{ needs.prepare.outputs.solana_ip }} + PROXY_IP: ${{ needs.prepare.outputs.proxy_ip }} DUMP_ENVS: True steps: - uses: actions/checkout@v3 @@ -524,7 +478,7 @@ jobs: - name: Prepare accounts id: accounts run: | - python3 ./clickfile.py infra gen-accounts -c 10 -a 10000 -n ${{env.NETWORK_NAME}} + python3 ./clickfile.py infra gen-accounts -c 10 -a 10000 -n ${{env.NETWORK}} - name: Launch yearn tests id: yearn @@ -572,7 +526,7 @@ jobs: - name: Prepare accounts id: accounts run: | - python3 ./clickfile.py infra gen-accounts -c 8 -a 10000 -n ${{env.NETWORK_NAME}} + python3 ./clickfile.py infra gen-accounts -c 8 -a 10000 -n ${{env.NETWORK}} - name: Launch Pancake tests if: ${{ steps.accounts.outcome == 'success'}} @@ -601,6 +555,7 @@ jobs: PROXY_URL: ${{ needs.prepare.outputs.proxy_url }} NETWORK_ID: ${{ needs.prepare.outputs.network_id }} NETWORK: ${{ needs.prepare.outputs.network }} + steps: - uses: actions/checkout@v3 - name: Install python requirements @@ -664,46 +619,26 @@ jobs: destroy: runs-on: ubuntu-20.04 - needs: [prepare, uniswap-v2, uniswap-v3, saddle, curve, aave, yearn, robonomics, swap-report, compound, curve-factory, pancake] - if: always() && needs.prepare.outputs.network == 'aws' + needs: [ prepare, uniswap-v2, uniswap-v3, saddle, curve, aave, yearn, robonomics, swap-report, compound, curve-factory, pancake ] + if: always() && needs.prepare.outputs.network == 'terraform' steps: - uses: actions/checkout@v3 - - uses: actions/download-artifact@v3 - with: - name: tf-state - - name: Install python requirements - if: always() - id: requirements - uses: ./.github/actions/python-requirements - - name: Prepare server for destroy terraform stand - if: always() - run: | - sudo apt-get update && sudo apt-get install -y python-dev gnupg software-properties-common wget - wget -O- https://apt.releases.hashicorp.com/gpg | \ - gpg --dearmor | \ - sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg - gpg --no-default-keyring \ - --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg \ - --fingerprint - echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \ - https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \ - sudo tee /etc/apt/sources.list.d/hashicorp.list - sudo apt update - sudo apt-get install terraform - ssh_key=/tmp/dapps-stand - echo "${{ secrets.SSH_DAPPS_PRIVATE_KEY }}" >> ${ssh_key} && chmod 400 ${ssh_key} - - name: Download docker logs - run: python3 ./clickfile.py infra download-logs - - name: Destroy stand with terraform - id: destroy - if: always() - run: | - python3 ./clickfile.py infra destroy - - uses: actions/upload-artifact@v3 - if: always() + - name: "Destroy stand" + env: + AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}} + AWS_DEFAULT_REGION: ${{secrets.AWS_DEFAULT_REGION}} + AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}} + HCLOUD_TOKEN: ${{secrets.HCLOUD_TOKEN}} + TFSTATE_BUCKET: ${{vars.TFSTATE_BUCKET}} + TFSTATE_KEY_PREFIX: ${{vars.TFSTATE_KEY_PREFIX}} + TFSTATE_REGION: ${{vars.TFSTATE_REGION}} + PROXY_IP: ${{ needs.prepare.outputs.proxy_ip }} + SOLANA_IP: ${{ needs.prepare.outputs.solana_ip }} + uses: ./.github/actions/destroy-tf-stand with: - name: AWS docker logs - path: ./logs/* + ci_stands_key_hcloud: ${{ secrets.CI_STANDS_KEY_HCLOUD }} + devnet_solana_url: ${{ secrets.SOLANA_URL }} + notify: runs-on: ubuntu-20.04 diff --git a/.github/workflows/dockerize_neon_tests.yml b/.github/workflows/dockerize_neon_tests.yml index fccbc66fcb..60ba962915 100644 --- a/.github/workflows/dockerize_neon_tests.yml +++ b/.github/workflows/dockerize_neon_tests.yml @@ -2,7 +2,8 @@ name: Docker Image for all neon tests on: push: - branches: ["develop"] + branches: + - "**" workflow_dispatch: inputs: oz_branch: @@ -35,21 +36,18 @@ jobs: echo "oz_branch=${oz_branch}" echo "tag=${tag}" >> $GITHUB_OUTPUT echo "oz_branch=${oz_branch}" >> $GITHUB_OUTPUT - - name: Define version branch - id: version_branch + - name: Define feature or version branch + id: feature_branch + if: github.ref_name !='develop' || github.ref_name !='master' run: | - if [[ "${{ github.ref }}" =~ "refs/heads/"[vt][0-9]+\.[0-9]+\.x ]]; then value=${{ github.ref_name }} - else - value="" - fi echo "value=${value}" echo "value=${value}" >> $GITHUB_OUTPUT - name: "Dockerize neon tests" uses: ./.github/actions/dockerize-neon-tests with: image_tag: ${{ steps.define-env.outputs.tag }} - version_tag: ${{ steps.version_branch.outputs.value }} + branch_tag: ${{ steps.feature_branch.outputs.value }} docker_username: ${{ secrets.DOCKER_USERNAME }} docker_password: ${{ secrets.DOCKER_PASSWORD }} oz_branch: ${{ steps.define-env.outputs.oz_branch }} diff --git a/.github/workflows/openzeppelin.yml b/.github/workflows/openzeppelin.yml index 969c3e34e3..45fc752d75 100644 --- a/.github/workflows/openzeppelin.yml +++ b/.github/workflows/openzeppelin.yml @@ -13,6 +13,7 @@ on: options: - night-stand - devnet + - terraform runner: type: choice default: neon-hosted @@ -26,18 +27,22 @@ on: description: "Count of parallel jobs" required: true default: "8" + oz_branch: + description: "Which OZ branch to use (if it is empty 'master', branch will be used)" + required: false + default: master env: - JOBS_NUMBER: "8" - NETWORK: night-stand - RUNNER: neon-hosted + JOBS_NUMBER: ${{ github.event.inputs.jobsNumber || '8' }} + NETWORK: ${{ github.event.inputs.network || 'night-stand' }} + RUNNER: ${{github.event.inputs.runner || 'neon-hosted' }} IMAGE: neonlabsorg/neon_tests CONTAINER: oz-${{ github.run_id }} BUILD_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" jobs: dockerize: - if: ${{ github.ref_name != 'develop'}} + if: github.ref_name != 'develop' || github.event.inputs.oz_branch !='' runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 @@ -47,50 +52,48 @@ jobs: image_tag: ${{ github.sha }} docker_username: ${{ secrets.DOCKER_USERNAME }} docker_password: ${{ secrets.DOCKER_PASSWORD }} + oz_branch: ${{ github.event.inputs.oz_branch || 'master' }} prepare-env: runs-on: ubuntu-20.04 steps: - - name: Setup env - id: setup + - uses: actions/checkout@v3 + - name: "Prepare stand" + id: prepare_stand + env: + AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}} + AWS_DEFAULT_REGION: ${{secrets.AWS_DEFAULT_REGION}} + AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}} + HCLOUD_TOKEN: ${{secrets.HCLOUD_TOKEN}} + TFSTATE_BUCKET: ${{vars.TFSTATE_BUCKET}} + TFSTATE_KEY_PREFIX: ${{vars.TFSTATE_KEY_PREFIX}} + TFSTATE_REGION: ${{vars.TFSTATE_REGION}} + uses: ./.github/actions/prepare-stand + with: + network: ${{ env.NETWORK }} + ci_stands_key_hcloud: ${{ secrets.CI_STANDS_KEY_HCLOUD }} + devnet_solana_url: ${{ secrets.SOLANA_URL }} + - name: Set outputs + id: vars run: | - # $1 - inputs - # $2 - env.VAR - function setVar { - if [ -z "$1" ] - then - RESULT="$2" - else - RESULT="$1" - fi - echo $RESULT - } - - NETWORK=$( setVar "${{ github.event.inputs.network }}" "${{ env.NETWORK }}" ) - RUNNER=$( setVar "${{ github.event.inputs.runner }}" "${{ env.RUNNER }}" ) - JOBS_NUMBER=$( setVar "${{ github.event.inputs.jobsNumber }}" "${{ env.JOBS_NUMBER }}" ) - - echo "Network: ${NETWORK}" - echo "Runner: ${RUNNER}" - echo "Jobs: ${JOBS_NUMBER}" - - echo "network=${NETWORK}" >> $GITHUB_OUTPUT - echo "runner=${RUNNER}" >> $GITHUB_OUTPUT - echo "jobs=${JOBS_NUMBER}" >> $GITHUB_OUTPUT + echo "runner=${{ env.RUNNER }}" >> $GITHUB_OUTPUT + echo "network=${{ env.NETWORK }}" >> $GITHUB_OUTPUT outputs: - network: ${{ steps.setup.outputs.network }} - runner: ${{ steps.setup.outputs.runner }} - jobs: ${{ steps.setup.outputs.jobs }} + runner: ${{ steps.vars.outputs.runner }} + network: ${{ steps.vars.outputs.network }} + solana_url: ${{ steps.prepare_stand.outputs.solana_url }} + proxy_url: ${{ steps.prepare_stand.outputs.proxy_url }} + faucet_url: ${{ steps.prepare_stand.outputs.faucet_url }} + network_id: ${{ steps.prepare_stand.outputs.network_id }} + proxy_ip: ${{ steps.prepare_stand.outputs.proxy_ip }} + solana_ip: ${{ steps.prepare_stand.outputs.solana_ip }} tests: name: OpenZeppelin tests needs: - prepare-env - dockerize - if: | - always() && - !contains(needs.*.result, 'failure') && - !contains(needs.*.result, 'cancelled') runs-on: ${{ needs.prepare-env.outputs.runner }} + if: always() && contains(fromJSON('["success", "skipped"]'), needs.dockerize.result) env: AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}" AWS_SECRET_ACCESS_KEY: "${{ secrets.AWS_SECRET_ACCESS_KEY }}" @@ -98,7 +101,14 @@ jobs: - uses: actions/checkout@v3 - name: Define image tag id: image_tag - uses: ./.github/actions/define-image-tag + run: | + if [[ "${{ needs.dockerize.result }}" != "skipped" ]]; then + tag=${{ github.sha }} + else + tag='latest' + fi + echo "tag=${tag}" + echo "tag=${tag}" >> $GITHUB_OUTPUT - name: Pull docker image run: docker pull ${{ env.IMAGE }}:${{ steps.image_tag.outputs.tag }} - name: Run docker container @@ -107,9 +117,14 @@ jobs: - name: Run OpenZeppelin tests timeout-minutes: 150 run: | - docker exec -i -e OZ_BALANCES_REPORT_FLAG=1 ${{ env.CONTAINER }} python3 ./clickfile.py run oz \ - --network ${{ needs.prepare-env.outputs.network }} \ - --jobs ${{ needs.prepare-env.outputs.jobs }} \ + env='' + if [ "${{ env.NETWORK }}" == "terraform" ]; then + env="-e PROXY_IP=${{ needs.prepare-env.outputs.proxy_ip }} -e SOLANA_IP=${{ needs.prepare-env.outputs.solana_ip }}" + fi + echo "env=${env}" + docker exec -i -e OZ_BALANCES_REPORT_FLAG=1 ${env} ${{ env.CONTAINER }} python3 ./clickfile.py run oz \ + --network ${{ env.NETWORK }} \ + --jobs ${{ env.JOBS_NUMBER }} \ --users 10 - name: Print OpenZeppelin report run: | @@ -130,7 +145,29 @@ jobs: run: | docker exec -i ${{ env.CONTAINER }} \ python3 ./clickfile.py send-notification -u ${{ secrets.SLACK_QA_CHANNEL_URL }} \ - -b ${{ env.BUILD_URL }} -n ${{ needs.prepare-env.outputs.network }} + -b ${{ env.BUILD_URL }} -n ${{ env.NETWORK }} - name: Remove docker container if: always() run: docker rm -f ${{ env.CONTAINER }} + + destroy: + runs-on: ubuntu-20.04 + needs: [tests, prepare-env] + if: always() && needs.prepare-env.outputs.network == 'terraform' + steps: + - uses: actions/checkout@v3 + - name: "Destroy stand" + env: + AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}} + AWS_DEFAULT_REGION: ${{secrets.AWS_DEFAULT_REGION}} + AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}} + HCLOUD_TOKEN: ${{secrets.HCLOUD_TOKEN}} + TFSTATE_BUCKET: ${{vars.TFSTATE_BUCKET}} + TFSTATE_KEY_PREFIX: ${{vars.TFSTATE_KEY_PREFIX}} + TFSTATE_REGION: ${{vars.TFSTATE_REGION}} + PROXY_IP: ${{ needs.prepare-env.outputs.proxy_ip }} + SOLANA_IP: ${{ needs.prepare-env.outputs.solana_ip }} + uses: ./.github/actions/destroy-tf-stand + with: + ci_stands_key_hcloud: ${{ secrets.CI_STANDS_KEY_HCLOUD }} + devnet_solana_url: ${{ secrets.SOLANA_URL }} diff --git a/Dockerfile b/Dockerfile index 3d9f9a96e7..fb795e1f7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -79,4 +79,4 @@ RUN mkdir -p ${DOWNLOAD_PATH} && \ COPY deploy/infra/compile_contracts.sh compatibility/openzeppelin-contracts RUN cd compatibility/openzeppelin-contracts npm set audit false RUN cd compatibility/openzeppelin-contracts && npm ci -RUN cd compatibility/openzeppelin-contracts && ./compile_contracts.sh \ No newline at end of file +RUN cd compatibility/openzeppelin-contracts && ./compile_contracts.sh diff --git a/clickfile.py b/clickfile.py index 9da0408922..40401a673a 100755 --- a/clickfile.py +++ b/clickfile.py @@ -10,6 +10,7 @@ import subprocess import sys import typing as tp +from pathlib import Path from multiprocessing.dummy import Pool from urllib.parse import urlparse @@ -123,7 +124,7 @@ def wrapper(*args, **kwargs) -> None: def get_tokens_balances(operator: Operator) -> tp.Dict: """Return tokens balances""" return dict( - neon=operator.get_neon_balance(), + neon=operator.get_token_balance(), sol=operator.get_solana_balance() / 1_000_000_000, ) @@ -131,7 +132,7 @@ def float_2_str(d): return dict(map(lambda i: (i[0], str(i[1])), d.items())) if os.environ.get("OZ_BALANCES_REPORT_FLAG") is not None: - network = network_manager.networks[args[0]] + network = network_manager.get_network_object(args[0]) op = Operator( network["proxy_url"], network["solana_url"], @@ -177,17 +178,12 @@ def run_openzeppelin_tests(network, jobs=8, amount=20000, users=8): ) (cwd.parent / "results").mkdir(parents=True, exist_ok=True) keys_env = [ - faucet_cli.prepare_wallets_with_balance( - network_manager.networks[network], count=users, airdrop_amount=amount - ) + dapps_cli.prepare_accounts(network, users, amount) for i in range(jobs) ] - tests = ( - subprocess.check_output("find \"test\" -name '*.test.js'", shell=True, cwd=cwd) - .decode() - .splitlines() - ) + tests = list(Path(f"{cwd}/test").rglob('*.test.js')) + tests = [str(test) for test in tests] def run_oz_file(file_name): print(f"Run {file_name}") @@ -226,7 +222,7 @@ def run_oz_file(file_name): pool.close() pool.join() # Add allure environment - settings = network_manager.networks[network] + settings = network_manager.get_network_object(network) web3_client = web3client.NeonChainWeb3Client( settings["proxy_url"]) opts = { @@ -331,7 +327,7 @@ def create_allure_environment_opts(opts: dict): def generate_allure_environment(network_name: str): - network = network_manager.networks[network_name] + network = network_manager.get_network_object(network_name) env = os.environ.copy() env["NETWORK_ID"] = str(network["network_ids"]["neon"]) @@ -425,6 +421,8 @@ def get_evm_pinned_version(branch): tag = pipeline_file["env"]["NEON_EVM_TAG"] if tag == "latest": return "develop" + if re.match(r"[vt]{1}\d{1,2}\.\d{1,2}.*", tag) is not None: + tag = re.sub(r"\.\d+$", ".x", tag) return tag @@ -488,7 +486,7 @@ def update_contracts(branch): "name", required=True, type=click.Choice( - ["economy", "basic", "tracer", "services", "oz", "ui", "compiler_compatibility"] + ["economy", "basic", "tracer", "services", "oz", "ui", "evm", "compiler_compatibility"] ), ) @catch_traceback @@ -505,7 +503,7 @@ def run(name, jobs, numprocesses, ui_item, amount, users, network): if numprocesses: command = f"{command} --numprocesses {numprocesses} --dist loadgroup" elif name == "tracer": - command = "py.test integration/tests/tracer" + command = "py.test -n 50 integration/tests/tracer" elif name == "services": command = "py.test integration/tests/services" if numprocesses: @@ -514,6 +512,10 @@ def run(name, jobs, numprocesses, ui_item, amount, users, network): command = "py.test integration/tests/compiler_compatibility" if numprocesses: command = f"{command} --numprocesses {numprocesses} --dist loadscope" + elif name == "evm": + command = "py.test integration/tests/neon_evm" + if numprocesses: + command = f"{command} --numprocesses {numprocesses}" elif name == "oz": run_openzeppelin_tests( network, jobs=int(jobs), amount=int(amount), users=int(users) @@ -845,7 +847,7 @@ def send_notification(url, build_url, traceback, network): "-n", "--network", default="night-stand", type=str, help="In which stand run tests" ) def get_operator_balances(network: str): - net = network_manager.networks[network] + net = network_manager.get_network_object(network) operator = Operator( net["proxy_url"], net["solana_url"], @@ -853,7 +855,7 @@ def get_operator_balances(network: str): net["spl_neon_mint"], net["operator_keys"], ) - neon_balance = operator.get_neon_balance() + neon_balance = operator.get_token_balance() sol_balance = operator.get_solana_balance() print( f'Operator balances ({len(net["operator_keys"])}):\n' diff --git a/conftest.py b/conftest.py index 284a218515..8609043f9e 100644 --- a/conftest.py +++ b/conftest.py @@ -8,9 +8,10 @@ import pytest from _pytest.config import Config from _pytest.runner import runtestprotocol +from solana.keypair import Keypair from clickfile import create_allure_environment_opts -from utils.web3client import NeonChainWeb3Client, SolChainWeb3Client +from utils.web3client import NeonChainWeb3Client, Web3Client pytest_plugins = ["ui.plugins.browser"] @@ -35,18 +36,14 @@ class EnvironmentConfig: def pytest_addoption(parser): - parser.addoption( - "--network", action="store", default="night-stand", help="Which stand use" - ) + parser.addoption("--network", action="store", default="local", help="Which stand use") parser.addoption( "--make-report", action="store_true", default=False, help="Store tests result to file", ) - parser.addoption( - "--envs", action="store", default="envs.json", help="Filename with environments" - ) + parser.addoption("--envs", action="store", default="envs.json", help="Filename with environments") def pytest_sessionstart(session): @@ -76,9 +73,7 @@ def pytest_configure(config: Config): envs_file = config.getoption("--envs") with open(pathlib.Path().parent.parent / envs_file, "r+") as f: environments = json.load(f) - assert ( - network_name in environments - ), f"Environment {network_name} doesn't exist in envs.json" + assert network_name in environments, f"Environment {network_name} doesn't exist in envs.json" env = environments[network_name] if network_name == "devnet": for solana_env_var in solana_url_env_vars: @@ -91,23 +86,32 @@ def pytest_configure(config: Config): env["use_bank"] = False if "eth_bank_account" not in env: env["eth_bank_account"] = "" - if network_name == "aws": - env["solana_url"] = env["solana_url"].replace( - "", os.environ.get("SOLANA_IP") - ) - env["proxy_url"] = env["proxy_url"].replace( - "", os.environ.get("PROXY_IP") - ) - env["faucet_url"] = env["faucet_url"].replace( - "", os.environ.get("PROXY_IP") - ) + if network_name == "terraform": + env["solana_url"] = env["solana_url"].replace("", os.environ.get("SOLANA_IP")) + env["proxy_url"] = env["proxy_url"].replace("", os.environ.get("PROXY_IP")) + env["faucet_url"] = env["faucet_url"].replace("", os.environ.get("PROXY_IP")) config.environment = EnvironmentConfig(**env) +@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) + + +@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) + + @pytest.fixture(scope="session", autouse=True) -def allure_environment(pytestconfig: Config, web3_client: NeonChainWeb3Client): +def allure_environment(pytestconfig: Config, web3_client: NeonChainWeb3Client, request): opts = {} - if pytestconfig.getoption("--network") != "geth": + + if pytestconfig.getoption("--network") != "geth" and "neon_evm" not in os.getenv('PYTEST_CURRENT_TEST'): opts = { "Network": pytestconfig.environment.proxy_url, "Proxy.Version": web3_client.get_proxy_version()["result"], @@ -151,8 +155,26 @@ def web3_client(pytestconfig: Config) -> NeonChainWeb3Client: @pytest.fixture(scope="session", autouse=True) -def web3_client_sol(pytestconfig: Config) -> SolChainWeb3Client: - client = SolChainWeb3Client( - pytestconfig.environment.proxy_url, - ) - return client +def web3_client_sol(pytestconfig: Config) -> tp.Union[Web3Client, None]: + if "sol" in pytestconfig.environment.network_ids: + client = Web3Client( + f"{pytestconfig.environment.proxy_url}/sol" + ) + return client + else: + return None + + +@pytest.fixture(scope="session") +def web3_client_abc(pytestconfig: Config) -> tp.Union[Web3Client, None]: + if "abc" in pytestconfig.environment.network_ids: + return Web3Client(f"{pytestconfig.environment.proxy_url}/abc") + else: + return None + +@pytest.fixture(scope="session", autouse=True) +def web3_client_def(pytestconfig: Config) -> tp.Union[Web3Client, None]: + if "def" in pytestconfig.environment.network_ids: + return Web3Client(f"{pytestconfig.environment.proxy_url}/def") + else: + return None \ No newline at end of file diff --git a/contracts/common/Common.sol b/contracts/common/Common.sol index 987d93ead9..b9b1c499c8 100644 --- a/contracts/common/Common.sol +++ b/contracts/common/Common.sol @@ -34,4 +34,27 @@ contract CommonCaller { function getNumber() public view returns (uint256) { return myCommon.getNumber(); } +} + +contract BunchActions { + + function setNumber(address[] memory addresses, uint256[] memory _numbers) public { + for (uint256 i = 0; i < addresses.length; ++i) { + Common(addresses[i]).setNumber(_numbers[i]); + } + } +} + + +contract MappingActions { + mapping(uint256 => string) public stringMapping; + + // Function to replace n values in the mapping + function replaceValues(uint256 n) external { + + for (uint256 i = 0; i < n; i++) { + stringMapping[i] = "UpdatedString"; + } + + } } \ No newline at end of file diff --git a/contracts/common/WNativeChainToken.sol b/contracts/common/WNativeChainToken.sol new file mode 100644 index 0000000000..4e2bd714ea --- /dev/null +++ b/contracts/common/WNativeChainToken.sol @@ -0,0 +1,82 @@ +pragma solidity ^0.8.12; + +contract WNativeChainToken { + + event Approval(address indexed src, address indexed guy, uint wad); + event Transfer(address indexed src, address indexed dst, uint wad); + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + mapping(address => uint) public balanceOf; + mapping(address => mapping(address => uint)) public allowance; + + constructor() payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function withdrawTo(address to, uint wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + payable(to).transfer(wad); + emit Withdrawal(to, wad); + } + + function totalSupply() public view returns (uint) { + return address(this).balance; + } + + function approve(address guy, uint wad) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer(address dst, uint wad) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint wad) + public + returns (bool) + { + require(balanceOf[src] >= wad); + + if (src != msg.sender) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} + +contract WNativeChainTokenCaller { + WNativeChainToken nativeChainToken; + event Log(address indexed addr); + + constructor() payable { + nativeChainToken = new WNativeChainToken{value: msg.value}(); + emit Log(address(nativeChainToken)); + } + + function deposit() public payable { + nativeChainToken.deposit{value: msg.value}(); + } +} \ No newline at end of file diff --git a/contracts/neon_evm/calculator.sol b/contracts/neon_evm/calculator.sol new file mode 100644 index 0000000000..02185b35cc --- /dev/null +++ b/contracts/neon_evm/calculator.sol @@ -0,0 +1,22 @@ +pragma solidity >=0.5.12; + +contract calculator { + uint public x = 20; + uint public y = 20; + + function getSum() public view returns (uint256) { + return x + y; + } +} + +contract calculatorCaller { + calculator calc; + + constructor(address _calc) { + calc = calculator(_calc); + } + + function callCalculator() public view returns (uint sum) { + sum = calc.getSum(); + } +} diff --git a/contracts/neon_evm/hello_world.sol b/contracts/neon_evm/hello_world.sol new file mode 100644 index 0000000000..8248f63a50 --- /dev/null +++ b/contracts/neon_evm/hello_world.sol @@ -0,0 +1,10 @@ +pragma solidity >=0.5.12; + +contract hello_world { + uint public num = 5; + string public text = "Hello World!"; + + function call_hello_world() public view returns (string memory) { + return text; + } +} \ No newline at end of file diff --git a/contracts/neon_evm/rw_lock.sol b/contracts/neon_evm/rw_lock.sol new file mode 100644 index 0000000000..f9cf104c9e --- /dev/null +++ b/contracts/neon_evm/rw_lock.sol @@ -0,0 +1,69 @@ +pragma solidity >=0.5.12; + +import "./hello_world.sol"; +contract rw_lock { + mapping(address => mapping(uint256 => uint256)) public data; + uint len = 0; + string public text; + + function unchange_storage(uint8 x, uint8 y) public pure returns(uint8) { + return x + y; + } + + function update_storage(uint resize) public { + uint n = 0; + + while (n < resize){ + data[msg.sender][len+n] = uint256(len+n); + n = n + 1; + } + len = len + resize; + } + + function update_storage_str(string memory new_text) public { + text = new_text; + } + + function update_storage_map(uint resize) public { + uint n = 0; + while (n < resize){ + data[msg.sender][n] = uint256(n); + n = n + 1; + } + } + + function get_text() public view returns (string memory) { + return text; + } + + function deploy_contract() public returns(address){ + hello_world hello = new hello_world(); + hello.call_hello_world(); + return address(hello); + } +} + + +contract rw_lock_caller { + rw_lock rw; + + constructor(address rw_lock_address) { + rw = rw_lock(rw_lock_address); + } + + function unchange_storage(uint8 x, uint8 y) public view returns(uint8) { + return rw.unchange_storage(x, y); + } + + function update_storage_str(string memory new_text) public { + rw.update_storage_str(new_text); + } + + function update_storage_map(uint resize) public { + rw.update_storage_map(resize); + } + + function get_text() public view returns (string memory) { + return rw.get_text(); + } +} \ No newline at end of file diff --git a/contracts/neon_evm/small.sol b/contracts/neon_evm/small.sol new file mode 100644 index 0000000000..1e39854656 --- /dev/null +++ b/contracts/neon_evm/small.sol @@ -0,0 +1,6 @@ +pragma solidity >=0.5.12; +contract small { + function call_hello() public view returns (string memory) { + return "Hi"; + } +} diff --git a/contracts/neon_evm/string_setter.sol b/contracts/neon_evm/string_setter.sol new file mode 100644 index 0000000000..6c3d03566c --- /dev/null +++ b/contracts/neon_evm/string_setter.sol @@ -0,0 +1,14 @@ +pragma solidity >=0.5.12; + +contract string_setter { + string public text; + + + function get() public view returns (string memory) { + return text; + } + + function set(string memory new_text) public payable { + text = new_text; + } +} \ No newline at end of file diff --git a/contracts/opcodes/ChainIdDependentOpCodes.sol b/contracts/opcodes/ChainIdDependentOpCodes.sol new file mode 100644 index 0000000000..aca2bfe3c3 --- /dev/null +++ b/contracts/opcodes/ChainIdDependentOpCodes.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +contract ChainIdDependentOpCodes { + + function getChainId() public view returns (uint256) { + uint256 id; + assembly { + id := chainid() + } + return id; + } + + function getBalance(address _addr) public view returns (uint) { + return _addr.balance; + } +} + +contract ChainIdDependentOpCodesCaller { + + ChainIdDependentOpCodes public chainIdDependentOpCodes; + + constructor() { + } + + function getChainId(address _addr) public view returns (uint) { + ChainIdDependentOpCodes my = ChainIdDependentOpCodes(_addr); + return my.getChainId(); + } + + function getBalance(address _addr, address account) public view returns (uint) { + ChainIdDependentOpCodes my = ChainIdDependentOpCodes(_addr); + + return my.getBalance(account); + } +} \ No newline at end of file diff --git a/contracts/EIPs/UnsupportedOpcodes.sol b/contracts/opcodes/UnsupportedOpcodes.sol similarity index 100% rename from contracts/EIPs/UnsupportedOpcodes.sol rename to contracts/opcodes/UnsupportedOpcodes.sol diff --git a/deploy/aws/data.tf b/deploy/aws/data.tf new file mode 100644 index 0000000000..b4185846ae --- /dev/null +++ b/deploy/aws/data.tf @@ -0,0 +1,62 @@ +data "hcloud_image" "ci-image" { + name = "ubuntu-22.04" + with_architecture = "x86" +} + +data "hcloud_network" "ci-network" { + name = "ci-network" +} + +data "hcloud_ssh_key" "ci-ssh-key" { + name = "hcloud-ci-stands" +} + +variable "branch" { + type = string +} + + +variable "proxy_model_commit" { + type = string +} + +variable "proxy_image_tag" { + type = string +} + +variable "neon_evm_commit" { + type = string +} + + +variable "faucet_model_commit" { + type = string +} + +data "template_file" "solana_init" { + template = file("solana_init.sh") + + vars = { + branch = "${var.branch}" + proxy_model_commit = "${var.proxy_model_commit}" + proxy_image_tag = "${var.proxy_image_tag}" + neon_evm_commit = "${var.neon_evm_commit}" + faucet_model_commit = "${var.faucet_model_commit}" + dockerhub_org_name = "${var.dockerhub_org_name}" + } +} + +data "template_file" "proxy_init" { + template = file("proxy_init.sh") + + vars = { + branch = "${var.branch}" + proxy_model_commit = "${var.proxy_model_commit}" + proxy_image_tag = "${var.proxy_image_tag}" + solana_ip = hcloud_server.solana.network.*.ip[0] + neon_evm_commit = "${var.neon_evm_commit}" + faucet_model_commit = "${var.faucet_model_commit}" + ci_pp_solana_url = "${var.ci_pp_solana_url}" + dockerhub_org_name = "${var.dockerhub_org_name}" + } +} diff --git a/deploy/aws/provider.tf b/deploy/aws/provider.tf new file mode 100644 index 0000000000..5fbd1132e7 --- /dev/null +++ b/deploy/aws/provider.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + hcloud = { + source = "hetznercloud/hcloud" + version = "1.44.1" + } + } + + backend "s3" { + // Must be set from environment + } +} + +provider "hcloud" { + # Configuration options +} diff --git a/deploy/cli/dapps.py b/deploy/cli/dapps.py index 2fc2b66859..a7a5f6cee5 100644 --- a/deploy/cli/dapps.py +++ b/deploy/cli/dapps.py @@ -1,9 +1,12 @@ import os import glob import json +import re import subprocess +import sys import typing as tp import pathlib +import logging import tabulate from paramiko.client import SSHClient @@ -16,29 +19,25 @@ from utils.web3client import NeonChainWeb3Client from utils.solana_client import SolanaClient from utils.prices import get_neon_price - - -TF_CWD = "deploy/aws" - -TF_ENV = { - "AWS_SECRET_ACCESS_KEY": os.environ.get("AWS_SECRET_ACCESS_KEY"), - "AWS_ACCESS_KEY_ID": os.environ.get("AWS_ACCESS_KEY_ID"), - "AWS_S3_BUCKET": os.environ.get("AWS_S3_BUCKET", "neon-tests-dapps"), - "AWS_REGION": os.environ.get("AWS_REGION", "us-east-2"), - "TF_VAR_branch": "develop", - "TF_VAR_proxy_container_tag": os.environ.get("NEON_PROXY_TAG", "latest"), - "TF_VAR_neon_evm_container_tag": os.environ.get("NEON_EVM_TAG", "latest"), - "TF_VAR_faucet_container_tag": os.environ.get("NEON_FAUCET_TAG", "latest"), - "TF_STATE_KEY": f"neon-tests/{os.environ.get('GITHUB_RUN_ID', '0')}", -} - -TF_ENV.update( - { - "TF_BACKEND_CONFIG": f"-backend-config=\"bucket={TF_ENV['AWS_S3_BUCKET']}\" " - f"-backend-config=\"key={TF_ENV['TF_STATE_KEY']}\" " - f"-backend-config=\"region={TF_ENV['AWS_REGION']}\" ", - } -) +from python_terraform import Terraform + + +TFSTATE_BUCKET = os.environ.get("TFSTATE_BUCKET") +TFSTATE_KEY_PREFIX = os.environ.get("TFSTATE_KEY_PREFIX") +TFSTATE_REGION = os.environ.get("TFSTATE_REGION") +TF_STATE_KEY = f"{TFSTATE_KEY_PREFIX}neon-tests-{os.environ.get('GITHUB_RUN_ID', '0')}" +TF_BACKEND_CONFIG = {"bucket": TFSTATE_BUCKET, + "key": TF_STATE_KEY, "region": TFSTATE_REGION} + +os.environ["TF_VAR_branch"] = 'develop' +os.environ["TF_VAR_proxy_image_tag"] = os.environ.get("NEON_PROXY_TAG", "latest") +os.environ["TF_VAR_proxy_model_commit"] = os.environ.get("PROXY_MODEL_COMMIT", "develop") +os.environ["TF_VAR_run_number"] = os.environ.get("GITHUB_RUN_ID", "0") +os.environ["TF_VAR_neon_evm_commit"] = os.environ.get("NEON_EVM_TAG", "latest") +os.environ["TF_VAR_faucet_model_commit"] = os.environ.get("NEON_FAUCET_TAG", "latest") +os.environ["TF_VAR_dockerhub_org_name"] = os.environ.get("DOCKERHUB_ORG_NAME", "neonlabsorg") +terraform = Terraform(working_dir=pathlib.Path( + __file__).parent.parent / "hetzner") WEB3_CLIENT = NeonChainWeb3Client(os.environ.get("PROXY_URL")) REPORT_HEADERS = ["Action", "Fee", "Cost in $", "Accounts", "TRx", "Estimated Gas", "Used Gas", "Used % of EG"] @@ -52,76 +51,55 @@ def set_github_env(envs: tp.Dict, upper=True) -> None: env_file.write(f"\n{key.upper() if upper else key}={str(value)}") -def get_solana_ip() -> str: - solana_ip = subprocess.run( - "terraform output --json | jq -r '.solana_ip.value'", - shell=True, - env=TF_ENV, - cwd="deploy/aws", - stdout=subprocess.PIPE, - text=True, - ).stdout.strip() - if isinstance(solana_ip, bytes): - solana_ip = solana_ip.decode() - return solana_ip - - -def get_proxy_ip() -> str: - proxy_ip = subprocess.run( - "terraform output --json | jq -r '.proxy_ip.value'", - shell=True, - env=TF_ENV, - cwd="deploy/aws", - stdout=subprocess.PIPE, - text=True, - ).stdout.strip() - if isinstance(proxy_ip, bytes): - proxy_ip = proxy_ip.decode() - return proxy_ip +def deploy_infrastructure() -> dict: + terraform.init(backend_config=TF_BACKEND_CONFIG) + return_code, stdout, stderr = terraform.apply(skip_plan=True) + print(f"code: {return_code}") + print(f"stdout: {stdout}") + print(f"stderr: {stderr}") + with open(f"terraform.log", "w") as file: + file.write(stdout) + file.write(stderr) + if return_code != 0: + print("Terraform infrastructure is not built correctly") + sys.exit(1) + output = terraform.output(json=True) + print(f"output: {output}") + proxy_ip = output["proxy_ip"]["value"] + solana_ip = output["solana_ip"]["value"] -def deploy_infrastructure() -> dict: - subprocess.check_call( - f"terraform init {TF_ENV['TF_BACKEND_CONFIG']}", - shell=True, - env=TF_ENV, - cwd=TF_CWD, - ) - subprocess.check_call( - "terraform apply --auto-approve=true", shell=True, env=TF_ENV, cwd=TF_CWD - ) - proxy_ip = get_proxy_ip() - solana_ip = get_solana_ip() infra = dict(solana_ip=solana_ip, proxy_ip=proxy_ip) set_github_env(infra) return infra def destroy_infrastructure(): - subprocess.run( - f"terraform init {TF_ENV['TF_BACKEND_CONFIG']}", - shell=True, - env=TF_ENV, - cwd=TF_CWD, - ) - subprocess.run( - "terraform destroy --auto-approve=true", shell=True, env=TF_ENV, cwd=TF_CWD - ) + log = logging.getLogger() + log.handlers = [] + handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter( + '%(asctime)4s %(name)4s [%(filename)s:%(lineno)s - %(funcName)s()] %(levelname)4s %(message)4s') + handler.setFormatter(formatter) + log.addHandler(handler) + log.setLevel(logging.INFO) + + def format_tf_output(output): + return re.sub(r'(?m)^', ' ' * TF_OUTPUT_OFFSET, str(output)) + + TF_OUTPUT_OFFSET = 16 + terraform.init(backend_config=TF_BACKEND_CONFIG) + tf_destroy = terraform.apply('-destroy', skip_plan=True) + log.info(format_tf_output(tf_destroy)) def download_remote_docker_logs(): - subprocess.run( - f"terraform init {TF_ENV['TF_BACKEND_CONFIG']}", - shell=True, - env=TF_ENV, - cwd=TF_CWD, - ) - proxy_ip = get_proxy_ip() - solana_ip = get_solana_ip() + proxy_ip = os.environ.get("PROXY_IP") + solana_ip = os.environ.get("SOLANA_IP") home_path = os.environ.get("HOME") artifact_logs = "./logs" - ssh_key = "/tmp/dapps-stand" + ssh_key = "/tmp/ci-stands" os.mkdir(artifact_logs) if not os.path.exists(f"{home_path}/.ssh"): os.mkdir(f"{home_path}/.ssh") @@ -132,13 +110,14 @@ def download_remote_docker_logs(): subprocess.run( f"ssh-keyscan -H {proxy_ip} >> {home_path}/.ssh/known_hosts", shell=True ) + ssh_client = SSHClient() ssh_client.load_system_host_keys() - ssh_client.connect(solana_ip, username="ubuntu", key_filename=ssh_key, timeout=120) + ssh_client.connect(solana_ip, username="root", key_filename=ssh_key, timeout=120) upload_service_logs(ssh_client, "solana", artifact_logs) - ssh_client.connect(proxy_ip, username="ubuntu", key_filename=ssh_key, timeout=120) + ssh_client.connect(proxy_ip, username="root", key_filename=ssh_key, timeout=120) services = ["postgres", "dbcreation", "indexer", "proxy", "faucet"] for service in services: upload_service_logs(ssh_client, service, artifact_logs) @@ -158,15 +137,7 @@ def upload_service_logs(ssh_client, service, artifact_logs): def prepare_accounts(network_name, count, amount) -> tp.List: network_manager = NetworkManager() - if network_name == "aws": - network = { - "proxy_url": os.environ.get("PROXY_URL"), - "solana_url": os.environ.get("SOLANA_URL"), - "faucet_url": os.environ.get("FAUCET_URL"), - "network_ids": network_manager.get_network_param(network_name, "network_ids") - } - else: - network = network_manager.get_network_param(network_name) + network = network_manager.get_network_object(network_name) accounts = faucet_cli.prepare_wallets_with_balance(network, count, amount) if os.environ.get("CI"): set_github_env(dict(accounts=",".join(accounts))) diff --git a/deploy/cli/network_manager.py b/deploy/cli/network_manager.py index 61b117ed84..4a8c166f87 100644 --- a/deploy/cli/network_manager.py +++ b/deploy/cli/network_manager.py @@ -3,7 +3,7 @@ import pathlib from collections import defaultdict -NETWORK_NAME = os.environ.get("NETWORK_NAME", "full_test_suite") +NETWORK_NAME = os.environ.get("NETWORK", "full_test_suite") EXPANDED_ENVS = [ "PROXY_URL", "FAUCET_URL", @@ -13,21 +13,21 @@ class NetworkManager(): def __init__(self): - self.networks = {} + self._networks = {} with open(pathlib.Path.cwd() / "envs.json", "r") as f: - self.networks = json.load(f) - if NETWORK_NAME not in self.networks.keys() and os.environ.get("DUMP_ENVS"): + self._networks = json.load(f) + if NETWORK_NAME not in self._networks.keys() and os.environ.get("DUMP_ENVS"): environments = defaultdict(dict) for var in EXPANDED_ENVS: environments[NETWORK_NAME].update({var.lower(): os.environ.get(var, "")}) environments[NETWORK_NAME]['network_ids'] = {'neon': os.environ.get('NETWORK_ID', "")} - self.networks.update(environments) + self._networks.update(environments) def get_network_param(self, network, params=None): value = "" - if network in self.networks: - value = self.networks[network] + if network in self._networks: + value = self._networks[network] if params: for item in params.split('.'): value = value[item] @@ -37,3 +37,11 @@ def get_network_param(self, network, params=None): if os.environ.get("PROXY_IP"): value = value.replace("", os.environ.get("PROXY_IP")) return value + + def get_network_object(self, network_name): + network = self.get_network_param(network_name) + if network_name == "terraform": + network["proxy_url"] = self.get_network_param(network_name, "proxy_url") + network["solana_url"] = self.get_network_param(network_name, "solana_url") + network["faucet_url"] = self.get_network_param(network_name, "faucet_url") + return network diff --git a/deploy/hetzner/data.tf b/deploy/hetzner/data.tf new file mode 100644 index 0000000000..b4185846ae --- /dev/null +++ b/deploy/hetzner/data.tf @@ -0,0 +1,62 @@ +data "hcloud_image" "ci-image" { + name = "ubuntu-22.04" + with_architecture = "x86" +} + +data "hcloud_network" "ci-network" { + name = "ci-network" +} + +data "hcloud_ssh_key" "ci-ssh-key" { + name = "hcloud-ci-stands" +} + +variable "branch" { + type = string +} + + +variable "proxy_model_commit" { + type = string +} + +variable "proxy_image_tag" { + type = string +} + +variable "neon_evm_commit" { + type = string +} + + +variable "faucet_model_commit" { + type = string +} + +data "template_file" "solana_init" { + template = file("solana_init.sh") + + vars = { + branch = "${var.branch}" + proxy_model_commit = "${var.proxy_model_commit}" + proxy_image_tag = "${var.proxy_image_tag}" + neon_evm_commit = "${var.neon_evm_commit}" + faucet_model_commit = "${var.faucet_model_commit}" + dockerhub_org_name = "${var.dockerhub_org_name}" + } +} + +data "template_file" "proxy_init" { + template = file("proxy_init.sh") + + vars = { + branch = "${var.branch}" + proxy_model_commit = "${var.proxy_model_commit}" + proxy_image_tag = "${var.proxy_image_tag}" + solana_ip = hcloud_server.solana.network.*.ip[0] + neon_evm_commit = "${var.neon_evm_commit}" + faucet_model_commit = "${var.faucet_model_commit}" + ci_pp_solana_url = "${var.ci_pp_solana_url}" + dockerhub_org_name = "${var.dockerhub_org_name}" + } +} diff --git a/deploy/hetzner/main.tf b/deploy/hetzner/main.tf new file mode 100644 index 0000000000..304717267b --- /dev/null +++ b/deploy/hetzner/main.tf @@ -0,0 +1,81 @@ + +resource "hcloud_server" "proxy" { + name = "proxy-${var.run_number}-${var.branch}" + image = data.hcloud_image.ci-image.id + server_type = var.server_type + location = var.location + ssh_keys = [ + data.hcloud_ssh_key.ci-ssh-key.id + ] + + public_net { + ipv4_enabled = true + ipv6_enabled = false + } + + network { + network_id = data.hcloud_network.ci-network.id + } + + provisioner "file" { + content = data.template_file.proxy_init.rendered + destination = "/tmp/proxy_init.sh" + + connection { + type = "ssh" + user = "root" + host = hcloud_server.proxy.ipv4_address + private_key = file("/tmp/ci-stands") + } + + } + + + provisioner "remote-exec" { + inline = [ + "echo '${hcloud_server.solana.network.*.ip[0]}' > /tmp/solana_host", + "chmod a+x /tmp/proxy_init.sh", + "sudo /tmp/proxy_init.sh" + ] + connection { + type = "ssh" + user = "root" + host = hcloud_server.proxy.ipv4_address + private_key = file("/tmp/ci-stands") + } + + } + + labels = { + environment = "ci" + purpose = "ci-oz-full-tests" + } + depends_on = [ + hcloud_server.solana + ] +} + +resource "hcloud_server" "solana" { + name = "solana-${var.run_number}-${var.branch}" + image = data.hcloud_image.ci-image.id + server_type = var.server_type + location = var.location + ssh_keys = [ + data.hcloud_ssh_key.ci-ssh-key.id + ] + + public_net { + ipv4_enabled = true + ipv6_enabled = false + } + + network { + network_id = data.hcloud_network.ci-network.id + } + + user_data = data.template_file.solana_init.rendered + + labels = { + environment = "ci" + } +} diff --git a/deploy/hetzner/output.tf b/deploy/hetzner/output.tf new file mode 100644 index 0000000000..cd61f5fb93 --- /dev/null +++ b/deploy/hetzner/output.tf @@ -0,0 +1,11 @@ +output "solana_ip" { + value = hcloud_server.solana.ipv4_address +} + +output "proxy_ip" { + value = hcloud_server.proxy.ipv4_address +} + +output "branch" { + value = var.branch +} diff --git a/deploy/hetzner/provider.tf b/deploy/hetzner/provider.tf new file mode 100644 index 0000000000..5fbd1132e7 --- /dev/null +++ b/deploy/hetzner/provider.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + hcloud = { + source = "hetznercloud/hcloud" + version = "1.44.1" + } + } + + backend "s3" { + // Must be set from environment + } +} + +provider "hcloud" { + # Configuration options +} diff --git a/deploy/hetzner/proxy_init.sh b/deploy/hetzner/proxy_init.sh new file mode 100644 index 0000000000..f6e0fe5612 --- /dev/null +++ b/deploy/hetzner/proxy_init.sh @@ -0,0 +1,133 @@ +#!/bin/bash + + +# Install docker +sudo apt-get remove docker docker-engine docker.io containerd runc +sudo apt-get update +sudo apt-get -y install ca-certificates curl gnupg lsb-release +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +sudo apt-get update +sudo apt-get -y install docker-ce docker-ce-cli containerd.io + +sudo apt-get -y install pbzip2 + +# Install docker-compose +sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +sudo chmod +x /usr/local/bin/docker-compose + + +# Get docker-compose file +cd /opt +curl -O https://raw.githubusercontent.com/neonlabsorg/proxy-model.py/${proxy_model_commit}/docker-compose/docker-compose-ci.yml + + +# Set required environment variables +export REVISION=${proxy_image_tag} +export SOLANA_URL=http:\/\/${solana_ip}:8899 +export NEON_EVM_COMMIT=${neon_evm_commit} +export FAUCET_COMMIT=${faucet_model_commit} +export CI_PP_SOLANA_URL=${ci_pp_solana_url} +export DOCKERHUB_ORG_NAME=${dockerhub_org_name} + + + +# Generate docker-compose override file +cat > docker-compose-ci.override.yml <&2 + local CHECK_COMMAND_RESULT=$(eval $CHECK_COMMAND) + echo $CHECK_COMMAND_RESULT >> /tmp/output.txt + if [[ "$CHECK_COMMAND_RESULT" == "1" ]]; then + echo "$SERVICE is up" 1>&2 + break + fi + + ((CURRENT_ATTEMPT=CURRENT_ATTEMPT+1)) + sleep 2 + done; +} + +# Check if Solana is available +SOLANA_DATA='{"jsonrpc":"2.0","id":1,"method":"getHealth"}' +SOLANA_RESULT='"ok"' +wait_service "solana" $SOLANA_URL $SOLANA_DATA $SOLANA_RESULT + +# Up all services +docker-compose -f docker-compose-ci.yml -f docker-compose-ci.override.yml up -d $SERVICES + + +# Check if Proxy is available +PROXY_URL="http://localhost:9090/solana" +PROXY_DATA='{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' +PROXY_RESULT='"result"' +wait_service "proxy" $PROXY_URL $PROXY_DATA $PROXY_RESULT + + +docker rm -f opt_solana_1 diff --git a/deploy/hetzner/solana_init.sh b/deploy/hetzner/solana_init.sh new file mode 100644 index 0000000000..9ab027d7b7 --- /dev/null +++ b/deploy/hetzner/solana_init.sh @@ -0,0 +1,64 @@ +#!/bin/bash + + +# Install docker +sudo apt-get remove docker docker-engine docker.io containerd runc +sudo apt-get update +sudo apt-get -y install ca-certificates curl gnupg lsb-release pbzip2 +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +sudo apt-get update + + +# Tune instance for Solana requirements(must be applied before start services) +sudo bash -c "cat >/etc/sysctl.d/20-solana-udp-buffers.conf</etc/sysctl.d/20-solana-mmaps.conf</etc/security/limits.d/90-solana-nofiles.conf< docker-compose-ci.override.yml< --jobs 8 ``` +## Run neon evm tests + +To run neon evm tests: +1. set environment variables: + SOLANA_URL: by default http://solana:8899 + NEON_CORE_API_URL: by default http://neon_api:8085/api +2. run the next command: +```bash +./clickfile.py run evm --numprocesses 6 +``` ## Run tests manual diff --git a/docs/tests/audit/evm/opcodes/test_chain_id_dependent_opcodes.md b/docs/tests/audit/evm/opcodes/test_chain_id_dependent_opcodes.md new file mode 100644 index 0000000000..772196d992 --- /dev/null +++ b/docs/tests/audit/evm/opcodes/test_chain_id_dependent_opcodes.md @@ -0,0 +1,12 @@ +# Overview + +Verify EIP-1052: EXTCODEHASH opcode + +# Tests list + +| Test case | Description | XFailed | +|-------------------------------------------------------------------------------|------------------------------------------------------|---------| +| TestChainIdDependentOpcodes::test_chain_id_sol | Call 'chainid' opcode in sol chain contract | | +| TestChainIdDependentOpcodes::test_chain_id_neon | Call 'chainid' opcode in neon chain contract | | +| TestChainIdDependentOpcodes::test_balance_by_sol_contract | Call 'balance' opcode from sol-native chain contract | | +| TestChainIdDependentOpcodes::test_balance_by_neon_contract | Call 'balance' opcode from neon-native chain contract | | diff --git a/docs/tests/audit/test_deposit.md b/docs/tests/audit/test_deposit.md index d2b40e2b92..70c69869a4 100644 --- a/docs/tests/audit/test_deposit.md +++ b/docs/tests/audit/test_deposit.md @@ -4,11 +4,14 @@ Tests for check transfer tokens from solana->neon and neon->solana # Tests list -| Test case | Description | XFailed | -|-------------------------------------------------------------|---------------------------------------------------------|---------| -| TestDeposit::test_transfer_neon_from_solana_to_neon | Verify transfer neon solana -> neon | | -| TestDeposit::test_transfer_spl_token_from_solana_to_neon | Verify transfer spl token solana -> neon | | -| TestWithdraw::test_success_withdraw_to_non_existing_account | Transfer Neon from Neon -> Solana to unexisting account | | -| TestWithdraw::test_success_withdraw_to_existing_account | Transfer Neon from Neon -> Solana to existing account | | -| TestWithdraw::test_failed_withdraw_non_divisible_amount | Failed case to withdraw with bad amount | | -| TestWithdraw::test_failed_withdraw_insufficient_balance | Failed case to withdraw with bad amount | | +| Test case | Description | XFailed | +|---------------------------------------------------------------------|---------------------------------------------------------|---------| +| TestDeposit::test_transfer_neon_from_solana_to_neon | Verify transfer neon solana -> neon | | +| TestDeposit::test_transfer_spl_token_from_solana_to_neon | Verify transfer spl token solana -> neon | | +| TestDeposit::test_transfer_wrapped_sol_token_from_solana_to_neon | Verify transfer wrapped spl token solana -> neon | | +| TestDeposit::test_transfer_neon_from_solana_to_neon | Verify transfer neon tokens solana -> neon | | +| TestDeposit::test_create_and_transfer_new_token_from_solana_to_neon | Verify transfer created tokens solana -> neon | | +| TestWithdraw::test_success_withdraw_to_non_existing_account | Transfer Neon from Neon -> Solana to unexisting account | | +| TestWithdraw::test_success_withdraw_to_existing_account | Transfer Neon from Neon -> Solana to existing account | | +| TestWithdraw::test_failed_withdraw_non_divisible_amount | Failed case to withdraw with bad amount | | +| TestWithdraw::test_failed_withdraw_insufficient_balance | Failed case to withdraw with bad amount | | diff --git a/docs/tests/audit/test_nonce.md b/docs/tests/audit/test_nonce.md index 1919b72d42..be473622c2 100644 --- a/docs/tests/audit/test_nonce.md +++ b/docs/tests/audit/test_nonce.md @@ -16,3 +16,4 @@ Verify mempool work and how proxy handles it | TestNonce::test_send_the_same_transactions_if_accepted | Send one transaction twice | | | TestNonce::test_send_the_same_transactions_if_not_accepted | Send one transaction twice but first not accepted | | | TestNonce::test_send_transaction_with_old_nonce | Send transaction with old nonce | | +| TestNonce::test_nonce_with_several_chains | Check that nonces on different chains are independent | | diff --git a/docs/tests/audit/test_payment_in_different_tokens.md b/docs/tests/audit/test_payment_in_different_tokens.md new file mode 100644 index 0000000000..89337ced12 --- /dev/null +++ b/docs/tests/audit/test_payment_in_different_tokens.md @@ -0,0 +1,19 @@ +# Overview + +Verify mempool work and how proxy handles it + +# Tests list + +| Test case | Description | XFailed | +|-----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|---------| +| TestMultiplyChains::test_user_to_user_trx | Check transfer tokens between users on second chain | | +| TestMultiplyChains::test_user_to_contract_and_contract_to_user_trx | Check transfer tokens between contracts and users on second chain | | +| TestMultiplyChains::test_contract_to_contract_trx | Check transfer tokens between contracts | | +| TestMultiplyChains::test_user_to_contract_wrong_chain_id_trx | Check transfer tokens to contract deployed on different chain | | +| TestMultiplyChains::test_deploy_contract | Check deploy contracts on second chain | | +| TestMultiplyChains::test_deploy_contract_with_sending_tokens | Check deploy contracts with sending tokens on second chain | | +| TestMultiplyChains::test_deploy_contract_by_one_user_to_different_chains | Check deploy contracts on different chains by one user | | +| TestMultiplyChains::test_interact_with_contract_from_another_chain | Check interracting with contracts deployed on another chain | | +| TestMultiplyChains::test_transfer_neons_in_sol_chain | Check transfer neons through sol chain | | +| TestMultiplyChains::test_transfer_sol_in_neon_chain | Check transfer sol through neon chain | | +| TestMultiplyChains::test_call_different_chains_contracts_in_one_transaction | Check interracting with several contracts deployed on different chains in one transaction | | diff --git a/docs/tests/audit/transfer/test_transaction_validation.md b/docs/tests/audit/transfer/test_transaction_validation.md index 299c7b9f6e..70d8dbe856 100644 --- a/docs/tests/audit/transfer/test_transaction_validation.md +++ b/docs/tests/audit/transfer/test_transaction_validation.md @@ -4,10 +4,10 @@ Tests for transfer transactions validation # Tests list -| Test case | Description | XFailed | -|------------------------------------------------------------------------|-----------------------------------------------------|---------------------| -| TestTransactionsValidation::test_generate_bad_sign | Send transaction with invalid sign and got an error | | -| TestTransactionsValidation::test_send_underpriced_transaction | Send transaction with not enough gas_price | | -| TestTransactionsValidation::test_send_too_big_transaction | Send a big transaction 256*1024 in data | | -| TestTransactionsValidation::test_send_transaction_with_small_gas_price | Send a transaction with small gas price | | -| TestTransactionsValidation::test_big_memory_value | Check memory overflow | | \ No newline at end of file +| Test case | Description | XFailed | +|------------------------------------------------------------------------|-----------------------------------------------------|-----------| +| TestTransactionsValidation::test_generate_bad_sign | Send transaction with invalid sign and got an error | | +| TestTransactionsValidation::test_send_underpriced_transaction | Send transaction with not enough gas_price | | +| TestTransactionsValidation::test_send_too_big_transaction | Send a big transaction 256*1024 in data | | +| TestTransactionsValidation::test_send_transaction_with_small_gas_price | Send a transaction with small gas price | NDEV-2386 | +| TestTransactionsValidation::test_big_memory_value | Check memory overflow | | \ No newline at end of file diff --git a/envs.json b/envs.json index d2230bb0cb..bccd777114 100644 --- a/envs.json +++ b/envs.json @@ -135,7 +135,10 @@ "evm_loader": "53DfF883gyixYNXnM7s5xhdeyV8mVk9T4i2hGV9vG9io", "proxy_url": "http://127.0.0.1:9090/solana", "network_ids": { - "neon": 111 + "neon": 111, + "sol": 112, + "abc": 113, + "def": 114 }, "solana_url": "http://127.0.0.1:8899/", "faucet_url": "http://127.0.0.1:3333/", @@ -207,11 +210,14 @@ "5dyQQATyk4yga4f4m8BCrUF1jdfGQ1mShV4ezFLxyCqW" ] }, - "aws": { + "terraform": { "evm_loader": "53DfF883gyixYNXnM7s5xhdeyV8mVk9T4i2hGV9vG9io", "proxy_url": "http://:9090/solana", "network_ids": { - "neon": 111 + "neon": 111, + "sol": 112, + "abc": 113, + "def": 114 }, "solana_url": "http://:8899/", "faucet_url": "http://:3333/", diff --git a/evm_loader-keypair.json b/evm_loader-keypair.json new file mode 100644 index 0000000000..da4a1d778d --- /dev/null +++ b/evm_loader-keypair.json @@ -0,0 +1 @@ +[127,240,45,117,60,55,248,79,137,128,176,124,177,53,125,213,194,211,20,110,232,87,87,27,194,68,199,200,135,231,13,127,60,0,57,43,120,125,56,168,83,209,36,5,118,52,196,60,113,51,198,18,70,29,116,254,177,127,66,72,21,82,134,192] \ No newline at end of file diff --git a/integration/tests/base.py b/integration/tests/base.py index 2d7fab3327..4af8bf9538 100644 --- a/integration/tests/base.py +++ b/integration/tests/base.py @@ -6,7 +6,7 @@ from utils.faucet import Faucet from utils.operator import Operator from utils.solana_client import SolanaClient -from utils.web3client import NeonChainWeb3Client +from utils.web3client import NeonChainWeb3Client, Web3Client class BaseTests: @@ -14,14 +14,16 @@ class BaseTests: operator: Operator faucet: Faucet web3_client: NeonChainWeb3Client + web3_client_sol: Web3Client sol_client: SolanaClient sol_price: float @pytest.fixture(autouse=True) - def prepare(self, operator: Operator, faucet: Faucet, web3_client, sol_client): + def prepare(self, operator: Operator, faucet: Faucet, web3_client, web3_client_sol, sol_client): self.operator = operator self.faucet = faucet self.web3_client = web3_client + self.web3_client_sol = web3_client_sol self.sol_client = sol_client @pytest.fixture(autouse=True) @@ -29,23 +31,25 @@ def prepare_account(self, prepare_account): self.acc = prepare_account def create_tx_object(self, sender, recipient=None, amount=0, nonce=None, gas=None, gas_price=None, data=None, - estimate_gas=True): + estimate_gas=True, web3_client=None): + if web3_client is None: + web3_client = self.web3_client if gas_price is None: - gas_price = self.web3_client.gas_price() + gas_price = web3_client.gas_price() if nonce is None: - nonce = self.web3_client.eth.get_transaction_count(sender) + nonce = web3_client.eth.get_transaction_count(sender) transaction = { "from": sender, - "chainId": self.web3_client.eth.chain_id, "gasPrice": gas_price, - "nonce": nonce, + "chainId": web3_client.eth.chain_id, + "nonce": nonce } if gas is not None: transaction["gas"] = gas if amount is not None: - transaction["value"] = self.web3_client.to_wei(amount, Unit.ETHER) + transaction["value"] = web3_client.to_atomic_currency(amount) if recipient is not None: transaction["to"] = recipient @@ -54,5 +58,6 @@ def create_tx_object(self, sender, recipient=None, amount=0, nonce=None, gas=Non transaction["data"] = data if estimate_gas: - transaction["gas"] = self.web3_client.eth.estimate_gas(transaction) + transaction["gas"] = web3_client.eth.estimate_gas(transaction) + return transaction diff --git a/integration/tests/basic/evm/opcodes/test_chain_id_dependent_opcodes.py b/integration/tests/basic/evm/opcodes/test_chain_id_dependent_opcodes.py new file mode 100644 index 0000000000..45d8c20025 --- /dev/null +++ b/integration/tests/basic/evm/opcodes/test_chain_id_dependent_opcodes.py @@ -0,0 +1,111 @@ +import pytest + +from integration.tests.basic.helpers.basic import BaseMixin + + +class TestChainIdDependentOpcodes(BaseMixin): + @pytest.fixture(scope="class") + def contract_neon(self, web3_client, class_account): + contract, _ = web3_client.deploy_and_get_contract( + "opcodes/ChainIdDependentOpCodes", "0.8.10", class_account) + return contract + + @pytest.fixture(scope="class") + def contract_neon_caller(self, web3_client_sol, class_account): + contract, _ = web3_client_sol.deploy_and_get_contract( + "opcodes/ChainIdDependentOpCodes", "0.8.10", class_account, + contract_name='ChainIdDependentOpCodesCaller', + constructor_args=[class_account.address], + ) + return contract + + @pytest.fixture(scope="class") + def contract_sol(self, web3_client_sol, class_account_sol_chain): + contract, _ = web3_client_sol.deploy_and_get_contract( + "opcodes/ChainIdDependentOpCodes", "0.8.10", class_account_sol_chain) + return contract + + @pytest.fixture(scope="class") + def contract_caller_sol(self, web3_client_sol, class_account_sol_chain): + contract, _ = web3_client_sol.deploy_and_get_contract( + "opcodes/ChainIdDependentOpCodes", "0.8.10", class_account_sol_chain, + contract_name='ChainIdDependentOpCodesCaller', + ) + return contract + + @pytest.fixture(scope="class") + def contract_caller_neon(self, web3_client, class_account): + contract, _ = web3_client.deploy_and_get_contract( + "opcodes/ChainIdDependentOpCodes", "0.8.10", class_account, + contract_name='ChainIdDependentOpCodesCaller', + ) + return contract + + @pytest.mark.multipletokens + def test_chain_id_sol(self, contract_sol, pytestconfig): + assert contract_sol.functions.getChainId().call() \ + == pytestconfig.environment.network_ids["sol"] + + def test_chain_id_neon(self, contract_neon, pytestconfig): + assert contract_neon.functions.getChainId().call() \ + == pytestconfig.environment.network_ids["neon"] + + @pytest.mark.multipletokens + def test_balance_by_sol_contract(self, contract_sol, class_account_sol_chain, web3_client, + web3_client_sol, contract_caller_sol): + # user calls sol contract in sol chain == sol balance + # user calls sol contract in neon chain == neon balance + # sol contract calls sol contract in sol chain == sol balance + # sol contract calls sol contract in neon chain == sol balance + + expected_balance_sol = web3_client_sol.get_balance(class_account_sol_chain.address) + expected_balance_neon = web3_client.get_balance(class_account_sol_chain.address) + balance = contract_sol.functions.getBalance(class_account_sol_chain.address).call() + + assert balance == expected_balance_sol + + contract_sol_in_neon_network = web3_client.get_deployed_contract(contract_sol.address, + "opcodes/ChainIdDependentOpCodes") + balance = contract_sol_in_neon_network.functions.getBalance(class_account_sol_chain.address).call() + assert balance == expected_balance_neon + + balance = contract_caller_sol.functions.getBalance(contract_sol.address, class_account_sol_chain.address).call() + assert balance == expected_balance_sol + + caller_in_neon_network = web3_client.get_deployed_contract(contract_caller_sol.address, + 'opcodes/ChainIdDependentOpCodes', + 'ChainIdDependentOpCodesCaller') + + balance = caller_in_neon_network.functions.getBalance(contract_sol.address, + class_account_sol_chain.address).call() + assert balance == expected_balance_sol + + @pytest.mark.multipletokens + def test_balance_by_neon_contract(self, contract_neon, sol_client, class_account, web3_client, + web3_client_sol, contract_caller_neon): + # user call neon contract in neon chain == neon balance + # user calls neon contract in sol chain == sol balance + # neon contract calls neon contract in neon chain == neon balance + # neon contract calls neon contract in sol chain == neon balance + + expected_balance_sol = web3_client_sol.get_balance(class_account.address) + expected_balance_neon = web3_client.get_balance(class_account.address) + + balance = contract_neon.functions.getBalance(class_account.address).call() + assert balance == expected_balance_neon + + contract_neon_in_sol_network = web3_client_sol.get_deployed_contract(contract_neon.address, + "opcodes/ChainIdDependentOpCodes") + balance = contract_neon_in_sol_network.functions.getBalance(class_account.address).call() + assert balance == expected_balance_sol + + caller_in_sol_network = web3_client_sol.get_deployed_contract(contract_caller_neon.address, + 'opcodes/ChainIdDependentOpCodes', + 'ChainIdDependentOpCodesCaller') + + balance = caller_in_sol_network.functions.getBalance(contract_neon.address, class_account.address).call() + assert balance == expected_balance_neon + + balance = caller_in_sol_network.functions.getBalance(contract_neon.address, + class_account.address).call() + assert balance == expected_balance_neon diff --git a/integration/tests/basic/evm/opcodes/test_extcodehash.py b/integration/tests/basic/evm/opcodes/test_extcodehash.py index 5da9e24846..f49ec48a1c 100644 --- a/integration/tests/basic/evm/opcodes/test_extcodehash.py +++ b/integration/tests/basic/evm/opcodes/test_extcodehash.py @@ -166,9 +166,9 @@ def test_extcodehash_for_reverted_destroyed_contract(self, eip1052_checker): receipt = self.web3_client.send_transaction(self.sender_account, instruction_tx) neon_logs = self.proxy_api.send_rpc( method="neon_getTransactionReceipt", - params=[receipt["transactionHash"].hex()], + params=[receipt["transactionHash"].hex(), "neon"], )["result"]["logs"] - data = [log["data"] for log in neon_logs if log["topics"] != []] + data = [log["data"] for log in neon_logs if log["neonEventType"] == "Log"] # TODO fix checking # assert data[0] != ZERO_HASH # assert len(data) == 3 diff --git a/integration/tests/basic/evm/opcodes/test_selfdestruct.py b/integration/tests/basic/evm/opcodes/test_selfdestruct.py index d73aa4a712..4299e414fa 100644 --- a/integration/tests/basic/evm/opcodes/test_selfdestruct.py +++ b/integration/tests/basic/evm/opcodes/test_selfdestruct.py @@ -35,12 +35,12 @@ def destroy(self, destroyable_contract, sender, funds_recipient, amount=None): receipt = self.web3_client.send_transaction(sender, instruction_tx) assert receipt["status"] == 1 - def check_contract_code_is_empty(self, contract_address): + def check_contract_code_is_not_empty(self, contract_address): response = self.proxy_api.send_rpc( "eth_getCode", params=[contract_address, "latest"] ) - assert response["result"] == "0x" + assert response["result"] != "0x" def test_destroy(self, destroyable_contract): self.deposit(destroyable_contract, self.sender_account, 1) @@ -59,10 +59,7 @@ def test_destroy(self, destroyable_contract): instruction_tx = destroyable_contract.functions.anyFunction().build_transaction(tx) receipt = self.web3_client.send_transaction(self.sender_account, instruction_tx) assert receipt["status"] == 1 - - event_logs = destroyable_contract.events.FunctionCalled().process_receipt(receipt) - assert event_logs == () - self.check_contract_code_is_empty(destroyable_contract.address) + self.check_contract_code_is_not_empty(destroyable_contract.address) def test_destroy_contract_with_contract_address_as_target(self, destroyable_contract): self.deposit(destroyable_contract, self.recipient_account, 1) @@ -72,7 +69,7 @@ def test_destroy_contract_with_contract_address_as_target(self, destroyable_cont contract_balance_after = self.get_balance_from_wei(destroyable_contract.address) assert contract_balance_after == contract_balance_before - self.check_contract_code_is_empty(destroyable_contract.address) + self.check_contract_code_is_not_empty(destroyable_contract.address) def test_destroy_contract_and_sent_neons_to_contract(self, destroyable_contract): self.deposit(destroyable_contract, self.sender_account, 1) @@ -84,7 +81,7 @@ def test_destroy_contract_and_sent_neons_to_contract(self, destroyable_contract) contract_balance_after = self.get_balance_from_wei(destroyable_contract.address) assert contract_balance_after == amount - self.check_contract_code_is_empty(destroyable_contract.address) + self.check_contract_code_is_not_empty(destroyable_contract.address) def test_destroy_contract_by_call_from_second_contract(self, destroyable_contract, contract_caller): self.deposit(destroyable_contract, self.sender_account, 2) @@ -96,7 +93,7 @@ def test_destroy_contract_by_call_from_second_contract(self, destroyable_contrac recipient_balance_after = self.get_balance_from_wei(self.recipient_account.address) assert receipt["status"] == 1 assert recipient_balance_after - recipient_balance_before == 2 - self.check_contract_code_is_empty(destroyable_contract.address) + self.check_contract_code_is_not_empty(destroyable_contract.address) def test_destroy_contract_and_sent_neon_from_contract_in_one_trx(self, destroyable_contract, contract_caller): self.deposit(destroyable_contract, self.sender_account, 2) @@ -108,7 +105,7 @@ def test_destroy_contract_and_sent_neon_from_contract_in_one_trx(self, destroyab recipient_balance_after = self.get_balance_from_wei(self.recipient_account.address) assert receipt["status"] == 1 assert recipient_balance_after - recipient_balance_before == 2 - self.check_contract_code_is_empty(destroyable_contract.address) + self.check_contract_code_is_not_empty(destroyable_contract.address) def test_sent_neon_from_contract_and_destroy_contract_in_one_trx(self, destroyable_contract, contract_caller): self.deposit(destroyable_contract, self.sender_account, 2) @@ -125,7 +122,7 @@ def test_sent_neon_from_contract_and_destroy_contract_in_one_trx(self, destroyab assert self.get_balance_from_wei(destroyable_contract.address) == 0 assert recipient_balance_after - recipient_balance_before == 2 - self.check_contract_code_is_empty(destroyable_contract.address) + self.check_contract_code_is_not_empty(destroyable_contract.address) def test_destroy_contract_and_sent_neon_to_contract_in_one_trx(self, destroyable_contract): self.deposit(destroyable_contract, self.recipient_account, 1) @@ -134,7 +131,7 @@ def test_destroy_contract_and_sent_neon_to_contract_in_one_trx(self, destroyable self.destroy(destroyable_contract, self.sender_account, self.recipient_account, amount=3) balance_after = self.get_balance_from_wei(self.recipient_account.address) assert balance_after - balance_before == 4 - self.check_contract_code_is_empty(destroyable_contract.address) + self.check_contract_code_is_not_empty(destroyable_contract.address) def test_destroy_contract_2_times_in_one_trx(self, destroyable_contract, contract_caller): self.deposit(destroyable_contract, self.recipient_account, 1) @@ -142,7 +139,7 @@ def test_destroy_contract_2_times_in_one_trx(self, destroyable_contract, contrac instruction_tx = contract_caller.functions.callDestroyTwice(self.sender_account.address).build_transaction(tx) receipt = self.web3_client.send_transaction(self.sender_account, instruction_tx) assert receipt["status"] == 1 - self.check_contract_code_is_empty(destroyable_contract.address) + self.check_contract_code_is_not_empty(destroyable_contract.address) def test_destroy_contract_via_delegatecall(self, destroyable_contract, contract_caller): # contract_caller should be destroyed instead of destroyable_contract @@ -153,7 +150,7 @@ def test_destroy_contract_via_delegatecall(self, destroyable_contract, contract_ assert receipt["status"] == 1 assert self.web3_client.eth.get_code(destroyable_contract.address) != "0x" - self.check_contract_code_is_empty(contract_caller.address) + self.check_contract_code_is_not_empty(contract_caller.address) def test_destroy_contract_via_delegatecall_and_create_new_contract(self, destroyable_contract, contract_caller): tx = self.create_contract_call_tx_object(self.sender_account) @@ -163,4 +160,4 @@ def test_destroy_contract_via_delegatecall_and_create_new_contract(self, destroy assert receipt["status"] == 1 assert self.web3_client.eth.get_code(destroyable_contract.address) != "0x" - self.check_contract_code_is_empty(contract_caller.address) + self.check_contract_code_is_not_empty(contract_caller.address) diff --git a/integration/tests/basic/evm/opcodes/test_unsupported_opcodes.py b/integration/tests/basic/evm/opcodes/test_unsupported_opcodes.py index e8608d8963..5d4985d970 100644 --- a/integration/tests/basic/evm/opcodes/test_unsupported_opcodes.py +++ b/integration/tests/basic/evm/opcodes/test_unsupported_opcodes.py @@ -12,7 +12,7 @@ class TestUnsupportedOpcodes(BaseMixin): @pytest.fixture(scope="class") def contract(self, web3_client, class_account): contract, _ = web3_client.deploy_and_get_contract( - "EIPs/UnsupportedOpcodes", "0.8.10", class_account) + "opcodes/UnsupportedOpcodes", "0.8.10", class_account) return contract def test_basefee(self, contract): diff --git a/integration/tests/basic/helpers/assert_message.py b/integration/tests/basic/helpers/assert_message.py index 0568a85ca5..58777adf97 100644 --- a/integration/tests/basic/helpers/assert_message.py +++ b/integration/tests/basic/helpers/assert_message.py @@ -4,6 +4,7 @@ class ErrorMessage(Enum): NEGATIVE_VALUE = "Resulting wei value must be between 1 and " INSUFFICIENT_FUNDS = "insufficient funds for transfer" + INSUFFICIENT_BALANCE = "Insufficient balance for transfer" GAS_OVERFLOW = "gas uint64 overflow" GAS_LIMIT_REACHED = "gas limit reached" INVALID_FIELDS_GAS = "Transaction had invalid fields: {'gas'" diff --git a/integration/tests/basic/helpers/basic.py b/integration/tests/basic/helpers/basic.py index 4f1eac85eb..af320be23b 100644 --- a/integration/tests/basic/helpers/basic.py +++ b/integration/tests/basic/helpers/basic.py @@ -184,19 +184,21 @@ def wait_finalized_block(self, block_num: int): response = self.proxy_api.send_rpc("neon_finalizedBlockNumber", []) fin_block_num = int(response["result"], 16) - def make_contract_tx_object(self, sender=None, amount=0, estimate_gas=False) -> tp.Dict: + def make_contract_tx_object(self, sender=None, amount=0, estimate_gas=False, web3_client=None) -> tp.Dict: """Can be used with build_transaction method""" if sender is None: sender = self.sender_account.address - return super().create_tx_object(sender, recipient=None, amount=amount, estimate_gas=estimate_gas) + return super().create_tx_object(sender, recipient=None, amount=amount, estimate_gas=estimate_gas, + web3_client=web3_client) def create_tx_object(self, sender=None, recipient=None, amount=2, nonce=None, gas=None, gas_price=None, data=None, - estimate_gas=True): + estimate_gas=True, web3_client=None): if sender is None: sender = self.sender_account.address if recipient is None: recipient = self.recipient_account.address - return super().create_tx_object(sender, recipient, amount, nonce, gas, gas_price, data, estimate_gas) + return super().create_tx_object(sender, recipient, amount, nonce, gas, gas_price, + data, estimate_gas, web3_client) def create_contract_call_tx_object(self, sender=None, amount=None): if sender is None: diff --git a/integration/tests/basic/helpers/chains.py b/integration/tests/basic/helpers/chains.py new file mode 100644 index 0000000000..f7437e7202 --- /dev/null +++ b/integration/tests/basic/helpers/chains.py @@ -0,0 +1,6 @@ +def make_nonce_the_biggest_for_chain(account, client, rest_clients): + # to avoid error "EVM Error. Attempt to deploy to existing account 0x..." + clients = [c for c in rest_clients if c] + new_account = client.create_account() + while client.get_nonce(account.address) < max([c.get_nonce(account.address) for c in clients]): + client.send_tokens(account, new_account.address, 10) diff --git a/integration/tests/basic/helpers/errors.py b/integration/tests/basic/helpers/errors.py index 62542df349..c0d42831ff 100644 --- a/integration/tests/basic/helpers/errors.py +++ b/integration/tests/basic/helpers/errors.py @@ -18,3 +18,4 @@ class Error32602: BAD_FROM_ADDRESS = "bad from-address" BAD_TOPIC = "bad topic" BAD_TRANSACTION_ID_FORMAT = "bad transaction-id format" + INVALID_NONCE = "invalid nonce: value" diff --git a/integration/tests/basic/helpers/rpc_checks.py b/integration/tests/basic/helpers/rpc_checks.py index e501f9e26d..519acde531 100644 --- a/integration/tests/basic/helpers/rpc_checks.py +++ b/integration/tests/basic/helpers/rpc_checks.py @@ -104,11 +104,11 @@ def assert_block_fields(block: dict, full_trx: bool, tx_receipt: tp.Optional[typ def assert_log_field_in_neon_trx_receipt(response, events_count): - expected_event_types = ["ENTER CALL"] + expected_event_types = ["EnterCall"] for i in range(events_count): - expected_event_types.append("LOG") - expected_event_types.append("EXIT STOP") - expected_event_types.append("RETURN") + expected_event_types.append("Log") + expected_event_types.append("ExitStop") + expected_event_types.append("Return") all_logs = [] for trx in response["result"]["solanaTransactions"]: @@ -138,7 +138,7 @@ def assert_log_field_in_neon_trx_receipt(response, events_count): assert neon_logs != [] for log in neon_logs: all_logs.append(log) - event_types = [log["neonEventType"] for log in sorted(all_logs, key=lambda x: x["neonEventOrder"])] + event_types = [log["neonEventType"] for log in sorted(all_logs, key=lambda x: int(x["neonEventOrder"], 16))] assert event_types == expected_event_types, f"Actual: {event_types}; Expected: {expected_event_types}" @@ -178,10 +178,8 @@ def assert_equal_fields(result, comparable_object, comparable_fields, keys_mappi r = comparable_object[keys_mappings.get(field)] else: r = comparable_object[field] - if isinstance(r, str): - r = r.lower() if isinstance(r, int): r = hex(r) if isinstance(r, HexBytes): r = r.hex() - assert l == r, f"{field} from response {l} is not equal to {field} from receipt {r}" + assert l == r, f"The field '{field}' {l} from response is not equal to {field} from receipt {r}" diff --git a/integration/tests/basic/rpc/test_rpc_base_calls.py b/integration/tests/basic/rpc/test_rpc_base_calls.py index cfba570d69..38cd1f1156 100644 --- a/integration/tests/basic/rpc/test_rpc_base_calls.py +++ b/integration/tests/basic/rpc/test_rpc_base_calls.py @@ -328,35 +328,18 @@ def test_check_unsupported_methods(self, method: str): def test_get_evm_params(self): response = self.proxy_api.send_rpc(method="neon_getEvmParams", params=[]) - expected_fields = [ - "NEON_GAS_LIMIT_MULTIPLIER_NO_CHAINID", - "NEON_POOL_SEED", - "NEON_COMPUTE_BUDGET_UNITS", - "NEON_SEED_VERSION", - "NEON_EVM_STEPS_LAST_ITERATION_MAX", - "NEON_PAYMENT_TO_DEPOSIT", - "NEON_COMPUTE_UNITS", - "NEON_REQUEST_UNITS_ADDITIONAL_FEE", - "NEON_PKG_VERSION", - "NEON_HEAP_FRAME", "NEON_ACCOUNT_SEED_VERSION", - "NEON_TOKEN_MINT", - "NEON_TREASURY_POOL_SEED", - "NEON_STORAGE_ENTRIES_IN_CONTRACT_ACCOUNT", + "NEON_EVM_STEPS_LAST_ITERATION_MAX", "NEON_EVM_STEPS_MIN", - "NEON_PAYMENT_TO_TREASURE", - "NEON_OPERATOR_PRIORITY_SLOTS", - "NEON_STATUS_NAME", - "NEON_REVISION", - "NEON_ADDITIONAL_FEE", - "NEON_CHAIN_ID", - "NEON_COMPUTE_BUDGET_HEAP_FRAME", - "NEON_POOL_COUNT", + "NEON_GAS_LIMIT_MULTIPLIER_NO_CHAINID", "NEON_HOLDER_MSG_SIZE", + "NEON_OPERATOR_PRIORITY_SLOTS", + "NEON_PAYMENT_TO_TREASURE", + "NEON_STORAGE_ENTRIES_IN_CONTRACT_ACCOUNT", "NEON_TREASURY_POOL_COUNT", - "NEON_TOKEN_MINT_DECIMALS", - "NEON_EVM_ID", + "NEON_TREASURY_POOL_SEED", + "NEON_EVM_ID" ] for field in expected_fields: assert ( diff --git a/integration/tests/basic/rpc/test_rpc_estimate_gas.py b/integration/tests/basic/rpc/test_rpc_estimate_gas.py index 2704fed84d..e6d58c7bc6 100644 --- a/integration/tests/basic/rpc/test_rpc_estimate_gas.py +++ b/integration/tests/basic/rpc/test_rpc_estimate_gas.py @@ -8,17 +8,6 @@ from integration.tests.basic.helpers.errors import Error32000 -@pytest.fixture(scope="class") -def common_contract(web3_client, class_account) -> tp.Any: - contract, receipt = web3_client.deploy_and_get_contract( - contract="common/Common", - version="0.8.12", - contract_name="Common", - account=class_account, - ) - yield contract, receipt - - @allure.feature("JSON-RPC validation") @allure.story("Verify eth_estimateGas RPC call") class TestRpcEstimateGas(BaseMixin): @@ -45,7 +34,7 @@ def test_eth_estimate_gas_different_block_param(self, block_param: tp.Union[int, assert rpc_checks.is_hex( response["result"] ), f"the result for estimated gas should be in hex, but got'{response['result']}'" - assert int(response["result"], 16) == 30_000 + assert int(response["result"], 16) == 25_000 def test_eth_estimate_gas_negative(self): response = self.proxy_api.send_rpc(method="eth_estimateGas", params=[]) @@ -97,7 +86,7 @@ def test_rpc_estimate_gas_send_neon(self): assert "gas" in transaction estimated_gas = transaction["gas"] - assert estimated_gas == 30_000 + assert estimated_gas == 25_000 def test_rpc_estimate_gas_erc20(self, erc20_simple): tx_receipt = erc20_simple.transfer(erc20_simple.owner, self.recipient_account, 1) @@ -105,7 +94,7 @@ def test_rpc_estimate_gas_erc20(self, erc20_simple): assert "gas" in transaction estimated_gas = transaction["gas"] - assert estimated_gas == 1_552_280 + assert estimated_gas == 1_394_160 def test_rpc_estimate_gas_spl(self, erc20_spl): tx_receipt = erc20_spl.transfer(erc20_spl.account, self.recipient_account, 1) @@ -113,27 +102,36 @@ def test_rpc_estimate_gas_spl(self, erc20_spl): assert "gas" in transaction estimated_gas = transaction["gas"] - assert estimated_gas == 2_084_280 + assert estimated_gas == 2_079_280 def test_rpc_estimate_gas_contract_get_value(self, common_contract): tx = self.make_contract_tx_object() - instruction_tx = common_contract[0].functions.getText().build_transaction(tx) - tx_receipt = self.web3_client.send_transaction(self.sender_account, instruction_tx) - transaction = self.web3_client.get_transaction_by_hash(tx_receipt["transactionHash"]) + instruction_tx = common_contract.functions.getText().build_transaction(tx) + tx_receipt = self.web3_client.send_transaction( + self.sender_account, instruction_tx + ) + transaction = self.web3_client.get_transaction_by_hash( + tx_receipt["transactionHash"] + ) assert "gas" in transaction estimated_gas = transaction["gas"] - assert estimated_gas == 30_000 + assert estimated_gas == 25_000 def test_rpc_estimate_gas_contract_set_value(self, common_contract): tx = self.make_contract_tx_object() - instruction_tx = common_contract[0].functions.setNumber(100).build_transaction(tx) - tx_receipt = self.web3_client.send_transaction(self.sender_account, instruction_tx) - transaction = self.web3_client.get_transaction_by_hash(tx_receipt["transactionHash"]) - + instruction_tx = ( + common_contract.functions.setNumber(100).build_transaction(tx) + ) + tx_receipt = self.web3_client.send_transaction( + self.sender_account, instruction_tx + ) + transaction = self.web3_client.get_transaction_by_hash( + tx_receipt["transactionHash"] + ) assert "gas" in transaction estimated_gas = transaction["gas"] - assert estimated_gas == 30_000 + assert estimated_gas == 25_000 def test_rpc_estimate_gas_contract_calls_another_contract(self, common_contract): caller_contract, _ = self.web3_client.deploy_and_get_contract( @@ -141,7 +139,7 @@ def test_rpc_estimate_gas_contract_calls_another_contract(self, common_contract) "0.8.12", contract_name="CommonCaller", account=self.sender_account, - constructor_args=[common_contract[0].address], + constructor_args=[common_contract.address], ) tx = self.make_contract_tx_object() instruction_tx = caller_contract.functions.getNumber().build_transaction(tx) @@ -151,4 +149,4 @@ def test_rpc_estimate_gas_contract_calls_another_contract(self, common_contract) assert "gas" in transaction estimated_gas = transaction["gas"] - assert estimated_gas == 30_000 + assert estimated_gas == 25_000 diff --git a/integration/tests/basic/rpc/test_rpc_get_logs.py b/integration/tests/basic/rpc/test_rpc_get_logs.py index 53493bccba..73477031b8 100644 --- a/integration/tests/basic/rpc/test_rpc_get_logs.py +++ b/integration/tests/basic/rpc/test_rpc_get_logs.py @@ -209,9 +209,9 @@ def test_get_logs(self, method, event_caller_contract, param_fields, tag1, tag2) if topic: assert topic in result["topics"] if "address" in param_fields: - assert result["address"] == receipt["to"].lower(), ( - f"address from response {result['address']} " - f"is not equal to address from receipt {receipt['to'].lower()}" + assert response["result"][0]["address"] == receipt["to"], ( + f"address from response {response['result'][0]['address']} " + f"is not equal to address from receipt {receipt['to']}" ) assert_fields_are_hex(result, self.ETH_HEX_FIELDS) diff --git a/integration/tests/basic/rpc/test_rpc_get_transaction.py b/integration/tests/basic/rpc/test_rpc_get_transaction.py index d86c987494..fe7dff0aa6 100644 --- a/integration/tests/basic/rpc/test_rpc_get_transaction.py +++ b/integration/tests/basic/rpc/test_rpc_get_transaction.py @@ -202,7 +202,11 @@ def test_get_transaction_receipt(self, method): """Verify implemented rpc calls work with neon_getTransactionReceipt and eth_getTransactionReceipt""" tx_receipt = self.send_neon(self.sender_account, self.recipient_account, 10) transaction_hash = tx_receipt.transactionHash.hex() - response = self.proxy_api.send_rpc(method=method, params=transaction_hash) + params = [transaction_hash] + if method.startswith('neon_'): + params.append('ethereum') + response = self.proxy_api.send_rpc(method=method, params=params) +# response = self.proxy_api.send_rpc(method=method, params=transaction_hash) assert "error" not in response assert "result" in response, AssertMessage.DOES_NOT_CONTAIN_RESULT result = response["result"] @@ -237,8 +241,8 @@ def test_eth_get_transaction_receipt_when_hash_doesnt_exist(self, method): (["0x874E87B5ccb467f07Ca42cF82e11aD44c7be159F"], Error32000.CODE, Error32000.MISSING_ARGUMENT), ([None, 10], Error32602.CODE, Error32602.BAD_ADDRESS), (["123345", 10], Error32602.CODE, Error32602.BAD_ADDRESS), - (["0x874E87B5ccb467f07Ca42cF82e11aD44c7be159F", None], Error32000.CODE, - Error32000.OBJECT_CANT_BE_INTERPRETED_AS_INT) + (["0x874E87B5ccb467f07Ca42cF82e11aD44c7be159F", None], Error32602.CODE, + Error32602.INVALID_NONCE) ], ) def test_neon_get_transaction_by_sender_nonce_negative(self, params, error_code, error_message): diff --git a/integration/tests/basic/test_deposit.py b/integration/tests/basic/test_deposit.py index 3585a0523e..f25dd2a6f6 100644 --- a/integration/tests/basic/test_deposit.py +++ b/integration/tests/basic/test_deposit.py @@ -2,8 +2,8 @@ import pytest import allure +import web3 from _pytest.config import Config -from solana.publickey import PublicKey from solana.keypair import Keypair from solana.rpc.commitment import Commitment from solana.rpc.types import TxOpts @@ -17,22 +17,11 @@ from web3 import exceptions as web3_exceptions from integration.tests.basic.helpers.basic import BaseMixin, BaseTests -from utils.consts import LAMPORT_PER_SOL -from utils.transfers_inter_networks import Transfer +from utils.consts import LAMPORT_PER_SOL, wSOL +from utils.transfers_inter_networks import neon_transfer_tx, wSOL_tx, token_from_solana_to_neon_tx, mint_tx from utils.helpers import wait_condition -wSOL = { - "chain_id": 111, - "address_spl": "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", -} - - @allure.feature("Transfer NEON <-> Solana") @allure.story("Deposit from Solana to NEON") class TestDeposit(BaseMixin): @@ -40,129 +29,133 @@ def withdraw_neon(self, dest_acc, move_amount): contract, _ = self.web3_client.deploy_and_get_contract( "precompiled/NeonToken", "0.8.10", account=self.sender_account ) - - instruction_tx = contract.functions.withdraw( - bytes(dest_acc.public_key) - ).build_transaction( - { - "from": self.sender_account.address, - "nonce": self.web3_client.eth.get_transaction_count( - self.sender_account.address - ), - "gasPrice": self.web3_client.gas_price(), - "value": self.web3_client._web3.to_wei(move_amount, "ether"), - } - ) + tx = self.create_contract_call_tx_object(amount=move_amount) + instruction_tx = contract.functions.withdraw(bytes(dest_acc.public_key)).build_transaction(tx) receipt = self.web3_client.send_transaction(self.sender_account, instruction_tx) assert receipt["status"] == 1 - def create_ata(self, solana_account, neon_mint): - trx = Transaction() - trx.add( - create_associated_token_account( - solana_account.public_key, solana_account.public_key, neon_mint - ) - ) - opts = TxOpts(skip_preflight=True, skip_confirmation=False) - self.sol_client.send_transaction(trx, solana_account, opts=opts) - - def send_tx_and_check_status_ok(self, tx, solana_account): - opts = TxOpts(skip_preflight=True, skip_confirmation=False) - sig = self.sol_client.send_transaction(tx, solana_account, opts=opts).value - sig_status = json.loads((self.sol_client.confirm_transaction(sig)).to_json()) - assert sig_status["result"]["value"][0]["status"] == {"Ok": None} - - def test_transfer_neon_from_solana_to_neon( - self, new_account, solana_account, pytestconfig: Config, neon_mint, erc20_spl - ): + def test_transfer_neon_from_solana_to_neon(self, new_account, solana_account, pytestconfig: Config, neon_mint): """Transfer Neon from Solana -> Neon""" amount = 0.1 full_amount = int(amount * LAMPORT_PER_SOL) evm_loader_id = pytestconfig.environment.evm_loader - neon_wallet = self.sol_client.get_neon_account_address( - new_account.address, evm_loader_id - ) - neon_balance_before = self.get_balance_from_wei(new_account.address) - self.create_ata(solana_account, neon_mint) + self.sol_client.create_ata(solana_account, neon_mint) self.withdraw_neon(solana_account, amount) - - tx = Transfer.neon_from_solana_to_neon_tx( + tx = token_from_solana_to_neon_tx( + self.sol_client, solana_account, - neon_wallet, neon_mint, new_account, full_amount, evm_loader_id, + self.web3_client.eth.chain_id, ) - self.send_tx_and_check_status_ok(tx, solana_account) + self.sol_client.send_tx_and_check_status_ok(tx, solana_account) neon_balance_after = self.get_balance_from_wei(new_account.address) assert neon_balance_after == neon_balance_before + amount - def test_transfer_spl_token_from_solana_to_neon( - self, solana_account, new_account, pytestconfig: Config, erc20_spl + @pytest.mark.multipletokens + def test_create_and_transfer_new_token_from_solana_to_neon( + self, + new_account, + solana_account, + pytestconfig: Config, + neon_mint, + web3_client_abc, + operator_keypair, + evm_loader_keypair, ): + amount = 5000000 evm_loader_id = pytestconfig.environment.evm_loader - response = self.proxy_api.send_rpc(method="neon_getEvmParams", params=[]) - neon_pool_count = response["result"]["NEON_POOL_COUNT"] + new_sol_account = Keypair.generate() + self.sol_client.send_sol(solana_account, new_sol_account.public_key, amount) + + self.sol_client.deposit_neon_like_tokens_from_solana_to_neon( + neon_mint, + new_sol_account, + new_account, + web3_client_abc.eth.chain_id, + operator_keypair, + evm_loader_keypair, + evm_loader_id, + amount, + ) + + abc_balance_after = web3_client_abc.get_balance(new_account) + assert abc_balance_after == amount * 1000000000 + def test_transfer_spl_token_from_solana_to_neon(self, solana_account, new_account, pytestconfig: Config, erc20_spl): + evm_loader_id = pytestconfig.environment.evm_loader amount = 0.1 full_amount = int(amount * LAMPORT_PER_SOL) - mint_pubkey = PublicKey(wSOL["address_spl"]) - ata_address = get_associated_token_address( - solana_account.public_key, mint_pubkey - ) + mint_pubkey = wSOL["address_spl"] + ata_address = get_associated_token_address(solana_account.public_key, mint_pubkey) - self.create_ata(solana_account, mint_pubkey) + self.sol_client.create_ata(solana_account, mint_pubkey) - spl_neon_token = SplToken( - self.sol_client, mint_pubkey, TOKEN_PROGRAM_ID, solana_account - ) - ata_balance_before = spl_neon_token.get_balance( - ata_address, commitment=Commitment("confirmed") - ) + spl_neon_token = SplToken(self.sol_client, mint_pubkey, TOKEN_PROGRAM_ID, solana_account) + ata_balance_before = spl_neon_token.get_balance(ata_address, commitment=Commitment("confirmed")) # wrap SOL - wrap_sol_tx = Transfer.wSOL_tx( - self.sol_client, wSOL, full_amount, solana_account.public_key, ata_address - ) - self.send_tx_and_check_status_ok(wrap_sol_tx, solana_account) + wSOL_account = self.sol_client.get_account_info(ata_address).value + wrap_sol_tx = wSOL_tx(wSOL_account, wSOL, full_amount, solana_account.public_key, ata_address) + self.sol_client.send_tx_and_check_status_ok(wrap_sol_tx, solana_account) # transfer wSOL - transfer_tx = Transfer.neon_transfer_tx( - self.web3_client, + transfer_tx = neon_transfer_tx( + self.web3_client, self.sol_client, full_amount, wSOL, solana_account, new_account, erc20_spl, evm_loader_id + ) + self.sol_client.send_tx_and_check_status_ok(transfer_tx, solana_account) + + ata_balance_after = spl_neon_token.get_balance(ata_address, commitment=Commitment("confirmed")) + + assert int(ata_balance_after.value.amount) == int(ata_balance_before.value.amount) + full_amount + + @pytest.mark.multipletokens + def test_transfer_wrapped_sol_token_from_solana_to_neon( + self, solana_account, pytestconfig: Config, web3_client_sol + ): + new_account = self.web3_client.create_account() + + evm_loader_id = pytestconfig.environment.evm_loader + amount = 0.1 + full_amount = int(amount * LAMPORT_PER_SOL) + + mint_pubkey = wSOL["address_spl"] + ata_address = get_associated_token_address(solana_account.public_key, mint_pubkey) + + self.sol_client.create_ata(solana_account, mint_pubkey) + + # wrap SOL + wSOL_account = self.sol_client.get_account_info(ata_address).value + wrap_sol_tx = wSOL_tx(wSOL_account, wSOL, full_amount, solana_account.public_key, ata_address) + self.sol_client.send_tx_and_check_status_ok(wrap_sol_tx, solana_account) + + tx = token_from_solana_to_neon_tx( self.sol_client, - full_amount, - wSOL, solana_account, + wSOL["address_spl"], new_account, - erc20_spl, + full_amount, evm_loader_id, - neon_pool_count, + web3_client_sol.eth.chain_id, ) - self.send_tx_and_check_status_ok(transfer_tx, solana_account) - ata_balance_after = spl_neon_token.get_balance( - ata_address, commitment=Commitment("confirmed") - ) + self.sol_client.send_tx_and_check_status_ok(tx, solana_account) - assert ( - int(ata_balance_after.value.amount) - == int(ata_balance_before.value.amount) + full_amount - ) + assert web3_client_sol.get_balance(new_account) / LAMPORT_PER_SOL == full_amount @allure.feature("Transfer NEON <-> Solana") @allure.story("Withdraw from NEON to Solana") class TestWithdraw(BaseTests): def withdraw(self, dest_acc, move_amount, withdraw_contract): - instruction_tx = withdraw_contract.functions.withdraw( - bytes(dest_acc.public_key) - ).build_transaction( + instruction_tx = withdraw_contract.functions.withdraw(bytes(dest_acc.public_key)).build_transaction( { "from": self.acc.address, "nonce": self.web3_client.eth.get_transaction_count(self.acc.address), @@ -173,6 +166,32 @@ def withdraw(self, dest_acc, move_amount, withdraw_contract): receipt = self.web3_client.send_transaction(self.acc, instruction_tx) assert receipt["status"] == 1 + @pytest.mark.parametrize("move_amount, error", [(11000, web3.exceptions.ContractLogicError), (10000, ValueError)]) + def test_failed_withdraw_insufficient_balance( + self, + pytestconfig: Config, + move_amount, + error, + withdraw_contract, + neon_mint, + solana_account, + ): + dest_acc = solana_account + + spl_neon_token = SplToken(self.sol_client, neon_mint, TOKEN_PROGRAM_ID, dest_acc.public_key) + + amount = move_amount * pow(10, 18) + dest_token_acc = get_associated_token_address(dest_acc.public_key, neon_mint) + + response = spl_neon_token.get_balance(dest_token_acc, commitment=Commitment("confirmed")) + assert json.loads(response.to_json())["message"] == "Invalid param: could not find account" + + with pytest.raises(error): + self.withdraw(dest_acc, amount, withdraw_contract) + + response = spl_neon_token.get_balance(dest_token_acc, commitment=Commitment("confirmed")) + assert json.loads(response.to_json())["message"] == "Invalid param: could not find account" + @pytest.mark.only_stands def test_success_withdraw_to_non_existing_account( self, pytestconfig: Config, withdraw_contract, neon_mint, solana_account @@ -181,29 +200,21 @@ def test_success_withdraw_to_non_existing_account( dest_acc = Keypair.generate() self.sol_client.request_airdrop(dest_acc.public_key, 1_000_000_000) - spl_neon_token = SplToken( - self.sol_client, neon_mint, TOKEN_PROGRAM_ID, dest_acc - ) + 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) move_amount = self.web3_client._web3.to_wei(5, "ether") - 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.public_key, commitment=Commitment("confirmed")) with pytest.raises(AttributeError): _ = destination_balance_before.value self.withdraw(dest_acc, move_amount, withdraw_contract) - destination_balance_after = spl_neon_token.get_balance( - dest_token_acc, commitment=Commitment("confirmed") - ) + destination_balance_after = spl_neon_token.get_balance(dest_token_acc, commitment=Commitment("confirmed")) - assert int(destination_balance_after.value.amount) == int( - move_amount / 1_000_000_000 - ) + assert int(destination_balance_after.value.amount) == int(move_amount / 1_000_000_000) def test_success_withdraw_to_existing_account( self, pytestconfig: Config, withdraw_contract, neon_mint, solana_account @@ -214,11 +225,7 @@ def test_success_withdraw_to_existing_account( wait_condition(lambda: self.sol_client.get_balance(dest_acc.public_key) != 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.public_key, dest_acc.public_key, neon_mint)) opts = TxOpts(skip_preflight=True, skip_confirmation=False) self.sol_client.send_transaction(trx, dest_acc, opts=opts) @@ -227,76 +234,33 @@ def test_success_withdraw_to_existing_account( move_amount_alan = 2_123_000_321_000_000_000 move_amount_galan = int(move_amount_alan / 1_000_000_000) - spl_neon_token = SplToken( - self.sol_client, neon_mint, TOKEN_PROGRAM_ID, dest_acc - ) + spl_neon_token = SplToken(self.sol_client, neon_mint, TOKEN_PROGRAM_ID, dest_acc) - destination_balance_before = spl_neon_token.get_balance( - dest_token_acc, commitment=Commitment("confirmed") - ) - assert int(destination_balance_before.value.amount) == 0 + destination_balance_before = spl_neon_token.get_balance(dest_token_acc, commitment=Commitment("confirmed")) self.withdraw(dest_acc, move_amount_alan, withdraw_contract) - destination_balance_after = spl_neon_token.get_balance( - dest_token_acc, commitment=Commitment("confirmed") + destination_balance_after = spl_neon_token.get_balance(dest_token_acc, commitment=Commitment("confirmed")) + assert int(destination_balance_after.value.amount) == move_amount_galan + int( + destination_balance_before.value.amount ) - assert int(destination_balance_after.value.amount) == move_amount_galan def test_failed_withdraw_non_divisible_amount( self, pytestconfig: Config, withdraw_contract, neon_mint, solana_account ): 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.public_key) 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.public_key, commitment=Commitment("confirmed")) with pytest.raises(AttributeError): _ = destination_balance_before.value with pytest.raises(web3_exceptions.ContractLogicError): self.withdraw(dest_acc, move_amount, withdraw_contract) - destination_balance_after = spl_neon_token.get_balance( - dest_acc.public_key, commitment=Commitment("confirmed") - ) - with pytest.raises(AttributeError): - _ = destination_balance_after.value - - @pytest.mark.parametrize("move_amount", [11000, 10000]) - def test_failed_withdraw_insufficient_balance( - self, - pytestconfig: Config, - move_amount, - withdraw_contract, - neon_mint, - solana_account, - ): - dest_acc = solana_account - - spl_neon_token = SplToken( - self.sol_client, neon_mint, TOKEN_PROGRAM_ID, dest_acc.public_key - ) - - amount = move_amount * pow(10, 18) - - destination_balance_before = spl_neon_token.get_balance( - dest_acc.public_key, commitment=Commitment("confirmed") - ) - with pytest.raises(AttributeError): - _ = destination_balance_before.value - - with pytest.raises(ValueError): - self.withdraw(dest_acc, 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.public_key, commitment=Commitment("confirmed")) with pytest.raises(AttributeError): _ = destination_balance_after.value diff --git a/integration/tests/basic/test_nonce.py b/integration/tests/basic/test_nonce.py index 498f19339c..38737f1222 100644 --- a/integration/tests/basic/test_nonce.py +++ b/integration/tests/basic/test_nonce.py @@ -2,10 +2,12 @@ import time import allure +import pytest from integration.tests.basic.helpers import rpc_checks from integration.tests.basic.helpers.assert_message import ErrorMessage from integration.tests.basic.helpers.basic import BaseMixin +from utils.consts import LAMPORT_PER_SOL @allure.feature("Ethereum compatibility") @@ -89,7 +91,7 @@ def test_send_transaction_with_low_nonce_after_several_high(self): def test_send_transaction_with_low_nonce_after_high(self): """Check that transaction with a higher nonce is waiting for its turn in the mempool""" nonce = ( - self.web3_client.eth.get_transaction_count(self.sender_account.address) + 1 + self.web3_client.eth.get_transaction_count(self.sender_account.address) + 1 ) transaction = self.create_tx_object(nonce=nonce) signed_tx = self.web3_client.eth.account.sign_transaction( @@ -121,7 +123,7 @@ def test_send_transaction_with_low_nonce_after_high(self): def test_send_transaction_with_the_same_nonce_and_lower_gas(self): """Check that transaction with a low gas and the same nonce can't be sent""" nonce = ( - self.web3_client.eth.get_transaction_count(self.sender_account.address) + 1 + self.web3_client.eth.get_transaction_count(self.sender_account.address) + 1 ) gas = self.web3_client.gas_price() transaction = self.create_tx_object(nonce=nonce, gas_price=gas) @@ -140,14 +142,14 @@ def test_send_transaction_with_the_same_nonce_and_lower_gas(self): ) assert "error" in response, f"Response doesn't has an error: {response}" assert ( - ErrorMessage.REPLACEMENT_UNDERPRICED.value in response["error"]["message"] + ErrorMessage.REPLACEMENT_UNDERPRICED.value in response["error"]["message"] ) assert response["error"]["code"] == -32000 def test_send_transaction_with_the_same_nonce_and_higher_gas(self): """Check that transaction with higher gas and the same nonce can be sent""" nonce = ( - self.web3_client.eth.get_transaction_count(self.sender_account.address) + 1 + self.web3_client.eth.get_transaction_count(self.sender_account.address) + 1 ) gas = self.web3_client.gas_price() transaction = self.create_tx_object(nonce=nonce, gas_price=gas) @@ -218,3 +220,18 @@ def test_send_transaction_with_old_nonce(self): ) assert ErrorMessage.NONCE_TOO_LOW.value in response["error"]["message"] assert response["error"]["code"] == -32002 + + @pytest.mark.multipletokens + def test_nonce_with_several_chains(self, class_account_sol_chain, + web3_client_sol, web3_client, faucet): + sender = class_account_sol_chain + self.faucet.request_neon(sender.address, 100) + neon_chain_nonce = self.web3_client.get_nonce(sender.address) + sol_chain_nonce = self.web3_client_sol.get_nonce(sender.address) + transaction_order_list = ['sol', 'neon', 'sol', 'sol', 'sol', 'neon'] + + for item in transaction_order_list: + client = web3_client_sol if item == 'sol' else web3_client + client.send_tokens(sender, self.recipient_account, 1000) + assert self.web3_client.get_nonce(sender.address) == neon_chain_nonce + 2 + assert self.web3_client_sol.get_nonce(sender.address) == sol_chain_nonce + 4 diff --git a/integration/tests/basic/test_payment_in_different_tokens.py b/integration/tests/basic/test_payment_in_different_tokens.py new file mode 100644 index 0000000000..0dcb290f2c --- /dev/null +++ b/integration/tests/basic/test_payment_in_different_tokens.py @@ -0,0 +1,271 @@ +import random + +import allure +import pytest +import web3 + +from integration.tests.basic.helpers.basic import BaseMixin +from integration.tests.basic.helpers.chains import make_nonce_the_biggest_for_chain + + +@allure.feature("Multiply token") +@allure.story("Payments in different tokens") +class TestMultiplyChains(BaseMixin): + @pytest.fixture(scope="class") + def bob(self, class_account_sol_chain): + return class_account_sol_chain + + @pytest.fixture(scope="class") + def alice(self, sol_client, account_with_all_tokens): + return account_with_all_tokens + + @pytest.fixture(scope="function") + def check_neon_balance_does_not_changed(self, alice, bob, web3_client): + alice_balance_before = web3_client.get_balance(alice) + bob_balance_before = web3_client.get_balance(bob) + yield + alice_balance_after = web3_client.get_balance(alice) + bob_balance_after = web3_client.get_balance(bob) + assert alice_balance_after == alice_balance_before + assert bob_balance_after == bob_balance_before + + @pytest.mark.multipletokens + def test_user_to_user_trx(self, web3_client_sol, alice, bob, check_neon_balance_does_not_changed): + bob_sol_balance_before = web3_client_sol.get_balance(bob) + alice_sol_balance_before = web3_client_sol.get_balance(alice) + value = 1000 + receipt = web3_client_sol.send_tokens(bob, alice, value) + assert receipt["status"] == 1 + bob_sol_balance_after = web3_client_sol.get_balance(bob) + alice_sol_balance_after = web3_client_sol.get_balance(alice) + assert alice_sol_balance_after == alice_sol_balance_before + value + assert bob_sol_balance_after < bob_sol_balance_before - value + + @pytest.mark.multipletokens + def test_user_to_contract_and_contract_to_user_trx( + self, web3_client_sol, bob, check_neon_balance_does_not_changed, wsol + ): + bob_sol_balance_before = web3_client_sol.get_balance(bob) + contract_sol_balance_initial = web3_client_sol.get_balance(wsol.address) + amount = 0.001 + value = web3_client_sol.to_atomic_currency(amount) + tx = self.make_contract_tx_object(bob.address, amount=amount, web3_client=web3_client_sol) + instruction_tx = wsol.functions.deposit().build_transaction(tx) + receipt = self.web3_client_sol.send_transaction(bob, instruction_tx) + assert receipt["status"] == 1 + bob_sol_balance_after_deposit = web3_client_sol.get_balance(bob) + contract_sol_balance_after_deposit = web3_client_sol.get_balance(wsol.address) + assert contract_sol_balance_after_deposit == contract_sol_balance_initial + web3_client_sol.to_atomic_currency( + amount + ) + assert bob_sol_balance_after_deposit < bob_sol_balance_before - value + + tx = self.make_contract_tx_object(bob.address, web3_client=web3_client_sol) + instruction_tx = wsol.functions.withdraw(value).build_transaction(tx) + receipt = self.web3_client_sol.send_transaction(bob, instruction_tx) + assert receipt["status"] == 1 + bob_sol_balance_after_withdraw = web3_client_sol.get_balance(bob) + contract_sol_balance_after_withdraw = web3_client_sol.get_balance(wsol.address) + assert contract_sol_balance_after_withdraw == contract_sol_balance_initial + assert bob_sol_balance_after_withdraw < bob_sol_balance_after_deposit + value + + @pytest.mark.multipletokens + def test_contract_to_contract_trx(self, web3_client_sol, bob): + # contract to new contract + amount = 0.0001 + value = web3_client_sol.to_atomic_currency(amount) + bob_sol_balance_before = web3_client_sol.get_balance(bob) + wsol_contract_caller, resp = web3_client_sol.deploy_and_get_contract( + contract="common/WNativeChainToken", + version="0.8.12", + contract_name="WNativeChainTokenCaller", + account=bob, + value=value, + ) + wrapper_address = wsol_contract_caller.events.Log().process_receipt(resp)[0].args["addr"] + assert web3_client_sol.get_balance(wrapper_address) == value + + # contract to existing contract + tx = self.make_contract_tx_object(bob.address, amount=amount, web3_client=web3_client_sol) + instruction_tx = wsol_contract_caller.functions.deposit().build_transaction(tx) + receipt = self.web3_client_sol.send_transaction(bob, instruction_tx) + assert receipt["status"] == 1 + bob_sol_balance_after = web3_client_sol.get_balance(bob) + + assert web3_client_sol.get_balance(wrapper_address) == value * 2 + assert bob_sol_balance_after < bob_sol_balance_before - value * 2 + + @pytest.mark.multipletokens + def test_user_to_contract_wrong_chain_id_trx( + self, web3_client_sol, bob, check_neon_balance_does_not_changed, event_caller_contract + ): + tx = self.make_contract_tx_object(bob.address, amount=1) + instruction_tx = event_caller_contract.functions.unnamedArg("hello").build_transaction(tx) + with pytest.raises(ValueError, match="wrong chain id"): + self.web3_client_sol.send_transaction(bob, instruction_tx) + + @pytest.mark.multipletokens + def test_deploy_contract(self, web3_client_sol, alice, check_neon_balance_does_not_changed): + sol_balance_before = web3_client_sol.get_balance(alice) + contract, _ = web3_client_sol.deploy_and_get_contract( + contract="common/Common", + version="0.8.12", + contract_name="Common", + account=alice, + ) + sol_balance_after = web3_client_sol.get_balance(alice) + assert sol_balance_after < sol_balance_before + + @pytest.mark.multipletokens + def test_deploy_contract_with_sending_tokens(self, web3_client_sol, alice, check_neon_balance_does_not_changed): + sol_alice_balance_before = web3_client_sol.get_balance(alice) + value = 1000 + contract, receipt = web3_client_sol.deploy_and_get_contract( + contract="common/WNativeChainToken", + version="0.8.12", + contract_name="WNativeChainToken", + account=alice, + value=value, + ) + assert receipt["status"] == 1 + sol_alice_balance_after = web3_client_sol.get_balance(alice) + contract_balance = web3_client_sol.get_balance(contract.address) + assert contract_balance == value + assert sol_alice_balance_after < sol_alice_balance_before - value + + @pytest.mark.multipletokens + def test_deploy_contract_by_one_user_to_different_chains( + self, web3_client_sol, solana_account, web3_client, pytestconfig, alice + ): + def deploy_contract(w3_client): + _, rcpt = w3_client.deploy_and_get_contract( + contract="common/Common", version="0.8.12", contract_name="Common", account=alice + ) + return rcpt + + make_nonce_the_biggest_for_chain(alice, web3_client_sol, [web3_client]) + deploy_contract(web3_client_sol) + + with pytest.raises(web3.exceptions.ContractLogicError, match="Attempt to deploy to existing account"): + deploy_contract(web3_client) + + make_nonce_the_biggest_for_chain(alice, web3_client, [web3_client_sol]) + receipt = deploy_contract(web3_client) + assert receipt["status"] == 1 + + @pytest.mark.multipletokens + def test_interact_with_contract_from_another_chain( + self, web3_client_sol, bob, check_neon_balance_does_not_changed, common_contract + ): + tx = self.make_contract_tx_object(bob.address, web3_client=web3_client_sol) + common_contract_sol_chain = web3_client_sol.get_deployed_contract(common_contract.address, "common/Common") + number = random.randint(0, 1000000) + instruction_tx = common_contract_sol_chain.functions.setNumber(number).build_transaction(tx) + + self.web3_client_sol.send_transaction(bob, instruction_tx) + assert common_contract_sol_chain.functions.getNumber().call() == number + assert common_contract.functions.getNumber().call() == number + + @pytest.mark.multipletokens + def test_transfer_neons_in_sol_chain(self, web3_client_sol, web3_client, bob, alice, wneon): + amount = 1 + value = self.web3_client._web3.to_wei(amount, "ether") + + tx = self.make_contract_tx_object(bob.address, amount=amount) + + instruction_tx = wneon.functions.deposit().build_transaction(tx) + self.web3_client.send_transaction(bob, instruction_tx) + + wneon_sol_chain = web3_client_sol.get_deployed_contract(wneon.address, "common/WNeon", "WNEON", "0.4.26") + + tx = self.make_contract_tx_object(bob.address, web3_client=web3_client_sol) + neon_balance_before = web3_client.get_balance(alice.address) + instruction_tx = wneon_sol_chain.functions.transfer(alice.address, value).build_transaction(tx) + receipt = self.web3_client_sol.send_transaction(bob, instruction_tx) + assert receipt["status"] == 1 + + tx = self.make_contract_tx_object(alice.address, web3_client=web3_client_sol) + instruction_tx = wneon_sol_chain.functions.withdraw(value).build_transaction(tx) + receipt = self.web3_client_sol.send_transaction(alice, instruction_tx) + assert receipt["status"] == 1 + + assert web3_client.get_balance(alice.address) == neon_balance_before + web3_client.to_atomic_currency(amount) + + @pytest.mark.multipletokens + def test_transfer_sol_in_neon_chain(self, web3_client_sol, web3_client, bob, alice, wsol): + amount = 0.001 + value = web3_client_sol.to_atomic_currency(amount) + + tx = self.make_contract_tx_object(bob.address, amount=amount, web3_client=web3_client_sol) + + instruction_tx = wsol.functions.deposit().build_transaction(tx) + self.web3_client_sol.send_transaction(bob, instruction_tx) + + wsol_neon_chain = web3_client.get_deployed_contract(wsol.address, "common/WNativeChainToken") + + tx = self.make_contract_tx_object(bob.address, web3_client=web3_client) + sol_balance_before = web3_client_sol.get_balance(alice.address) + instruction_tx = wsol_neon_chain.functions.transfer(alice.address, value).build_transaction(tx) + receipt = self.web3_client.send_transaction(bob, instruction_tx) + assert receipt["status"] == 1 + + tx = self.make_contract_tx_object(alice.address, web3_client=web3_client) + instruction_tx = wsol_neon_chain.functions.withdraw(value).build_transaction(tx) + receipt = self.web3_client.send_transaction(alice, instruction_tx) + assert receipt["status"] == 1 + + assert web3_client_sol.get_balance(alice.address) == sol_balance_before + value + + @pytest.mark.multipletokens + def test_call_different_chains_contracts_in_one_transaction( + self, + alice, + common_contract, + web3_client, + web3_client_sol, + web3_client_abc, + web3_client_def, + class_account_sol_chain, + ): + chains = { + "neon": {"client": web3_client}, + "sol": {"client": web3_client_sol}, + "abc": {"client": web3_client_abc}, + "def": {"client": web3_client_def}, + } + + make_nonce_the_biggest_for_chain(alice, web3_client, [item["client"] for item in chains.values()]) + + bunch_contract_neon, _ = web3_client.deploy_and_get_contract( + contract="common/Common", version="0.8.12", contract_name="BunchActions", account=alice + ) + + for chain in chains: + bunch_contract = chains[chain]["client"].get_deployed_contract( + bunch_contract_neon.address, "common/Common", contract_name="BunchActions" + ) + chains[chain]["bunch_contract"] = bunch_contract + make_nonce_the_biggest_for_chain( + alice, chains[chain]["client"], [item["client"] for item in chains.values()] + ) + + common_contract, _ = chains[chain]["client"].deploy_and_get_contract( + contract="common/Common", + version="0.8.12", + account=alice, + ) + chains[chain]["common_contract"] = common_contract + + for chain in chains: + tx = self.make_contract_tx_object(alice.address, web3_client=chains[chain]["client"]) + numbers = [random.randint(0, 1000000) for _ in range(len(chains))] + instruction_tx = ( + chains[chain]["bunch_contract"] + .functions.setNumber([item["common_contract"].address for item in chains.values()], numbers) + .build_transaction(tx) + ) + receipt = chains[chain]["client"].send_transaction(alice, instruction_tx) + assert receipt["status"] == 1 + + for i, item in enumerate(chains.values()): + assert item["common_contract"].functions.getNumber().call() == numbers[i] diff --git a/integration/tests/basic/test_trx_rlp_decoding.py b/integration/tests/basic/test_trx_rlp_decoding.py index 779664346f..d11c1e5fd9 100644 --- a/integration/tests/basic/test_trx_rlp_decoding.py +++ b/integration/tests/basic/test_trx_rlp_decoding.py @@ -66,7 +66,7 @@ def test_modify_s(self, signed_tx, new_s, expected_error): @pytest.mark.parametrize("new_r, expected_error", [ (13237258775825350966557245051891674271982401474769237400875435660443279001850, - "failed to recover ECDSA public key"), + "Invalid signature"), (123, "insufficient funds for transfer"), ('', "Invalid signature values")]) def test_modify_r(self, signed_tx, new_r, expected_error): diff --git a/integration/tests/basic/test_wneon.py b/integration/tests/basic/test_wneon.py index fe8a977e98..452f411ff0 100644 --- a/integration/tests/basic/test_wneon.py +++ b/integration/tests/basic/test_wneon.py @@ -18,14 +18,6 @@ from utils.helpers import wait_condition -@pytest.fixture(scope="class") -def wneon(web3_client, faucet, class_account): - contract, _ = web3_client.deploy_and_get_contract( - "common/WNeon", "0.4.26", account=class_account, contract_name="WNEON" - ) - return contract - - @allure.feature("Ethereum compatibility") @allure.story("Wrapped NEON tests") class TestWNeon(BaseMixin): @@ -121,7 +113,7 @@ def test_transfer_and_check_token_does_not_use_spl(self, wneon, new_account): lambda: self.sol_client.get_transaction( Signature.from_string(solana_trx["result"][0]), ) - != GetTransactionResp(None) + != GetTransactionResp(None), timeout_sec=30 ) solana_resp = self.sol_client.get_transaction( Signature.from_string(solana_trx["result"][0]) diff --git a/integration/tests/basic/transfer/test_neon.py b/integration/tests/basic/transfer/test_neon.py index 0b476dbf6c..230ffe883e 100644 --- a/integration/tests/basic/transfer/test_neon.py +++ b/integration/tests/basic/transfer/test_neon.py @@ -42,7 +42,7 @@ def test_send_more_than_exist_on_account_neon(self): self.sender_account, self.recipient_account, amount, - error_message=ErrorMessage.INSUFFICIENT_FUNDS.value, + error_message=ErrorMessage.INSUFFICIENT_BALANCE.value, ) self.assert_balance(self.sender_account.address, sender_balance, rnd_dig=1) diff --git a/integration/tests/basic/transfer/test_transaction_validation.py b/integration/tests/basic/transfer/test_transaction_validation.py index e7111b158b..d92ee27dce 100644 --- a/integration/tests/basic/transfer/test_transaction_validation.py +++ b/integration/tests/basic/transfer/test_transaction_validation.py @@ -1,6 +1,5 @@ import random import re -import time import allure import pytest @@ -88,6 +87,7 @@ def test_send_too_big_transaction(self): assert ErrorMessage.TOO_BIG_TRANSACTION.value in response["error"]["message"] assert response["error"]["code"] == -32000 + @pytest.mark.skip(reason="Test doesn't work with MINIMAL_GAS_PRICE in config. NDEV-2386") def test_send_transaction_with_small_gas_price(self, new_account): """Check that transaction can't be accepted if gas value is too small""" gas_price = self.web3_client.gas_price() @@ -99,7 +99,7 @@ def test_send_transaction_with_small_gas_price(self, new_account): "eth_sendRawTransaction", [signed_tx.rawTransaction.hex()] ) assert is_hex(response['result']) - time.sleep(5) + self.wait_transaction_accepted(response["result"]) receipt = self.proxy_api.send_rpc(method="eth_getTransactionReceipt", params=[response["result"]]) assert receipt['result'] is None diff --git a/integration/tests/conftest.py b/integration/tests/conftest.py index 1e88b4013d..bc333d0b0d 100644 --- a/integration/tests/conftest.py +++ b/integration/tests/conftest.py @@ -1,6 +1,8 @@ import os import random import string +import pathlib +import json import typing import allure @@ -20,6 +22,7 @@ from utils.operator import Operator from utils.solana_client import SolanaClient from utils.web3client import NeonChainWeb3Client +from utils.prices import get_sol_price, get_neon_price NEON_AIRDROP_AMOUNT = 10_000 @@ -27,13 +30,22 @@ def pytest_collection_modifyitems(config, items): deselected_items = [] selected_items = [] - if config.getoption("--network") == "devnet": - deselected_mark = "only_stands" + deselected_marks = [] + network_name = config.getoption("--network") + + if network_name == "devnet": + deselected_marks.append("only_stands") else: - deselected_mark = "only_devnet" + deselected_marks.append("only_devnet") + + envs_file = config.getoption("--envs") + with open(pathlib.Path().parent.parent / envs_file, "r+") as f: + environments = json.load(f) + if len(environments[network_name]["network_ids"]) == 1: + deselected_marks.append("multipletokens") for item in items: - if item.get_closest_marker(deselected_mark): + if any([item.get_closest_marker(mark) for mark in deselected_marks]): deselected_items.append(item) else: selected_items.append(item) @@ -90,21 +102,21 @@ def prepare_account(operator, faucet, web3_client: NeonChainWeb3Client): acc = web3_client.eth.account.create() with allure.step(f"Request {NEON_AIRDROP_AMOUNT} NEON from faucet for {acc.address}"): faucet.request_neon(acc.address, NEON_AIRDROP_AMOUNT) - assert web3_client.get_balance(acc) == NEON_AIRDROP_AMOUNT - start_neon_balance = operator.get_neon_balance() + assert web3_client.get_ether_balance(acc) == NEON_AIRDROP_AMOUNT + start_neon_balance = operator.get_token_balance() start_sol_balance = operator.get_solana_balance() with allure.step( - f"Operator initial balance: {start_neon_balance / LAMPORT_PER_SOL} NEON {start_sol_balance / LAMPORT_PER_SOL} SOL" + f"Operator initial balance: {start_neon_balance / LAMPORT_PER_SOL} NEON {start_sol_balance / LAMPORT_PER_SOL} SOL" ): pass yield acc - end_neon_balance = operator.get_neon_balance() + end_neon_balance = operator.get_token_balance() end_sol_balance = operator.get_solana_balance() with allure.step( - f"Operator end balance: {end_neon_balance / LAMPORT_PER_SOL} NEON {end_sol_balance / LAMPORT_PER_SOL} SOL" + f"Operator end balance: {end_neon_balance / LAMPORT_PER_SOL} NEON {end_sol_balance / LAMPORT_PER_SOL} SOL" ): pass - with allure.step(f"Account end balance: {web3_client.get_balance(acc)} NEON"): + with allure.step(f"Account end balance: {web3_client.get_ether_balance(acc)} NEON"): pass @@ -144,11 +156,11 @@ def solana_account(bank_account, pytestconfig: Config, sol_client): @pytest.fixture(scope="session") def erc20_spl( - web3_client: NeonChainWeb3Client, - faucet, - pytestconfig: Config, - sol_client, - solana_account, + web3_client: NeonChainWeb3Client, + faucet, + pytestconfig: Config, + sol_client, + solana_account, ): symbol = "".join([random.choice(string.ascii_uppercase) for _ in range(3)]) erc20 = ERC20Wrapper( @@ -200,13 +212,50 @@ def erc20_spl_mintable(web3_client: NeonChainWeb3Client, faucet, sol_client, sol @pytest.fixture(scope="function") -def new_account(web3_client, faucet, eth_bank_account): - yield web3_client.create_account_with_balance(faucet, bank_account=eth_bank_account) +def new_account(web3_client, sol_client, faucet, eth_bank_account, web3_client_sol, pytestconfig, solana_account): + account = web3_client.create_account_with_balance(faucet, bank_account=eth_bank_account) + yield account + + +@pytest.fixture(scope="class") +def class_account(web3_client, faucet, eth_bank_account, solana_account, sol_client, web3_client_sol, pytestconfig): + account = web3_client.create_account_with_balance(faucet, bank_account=eth_bank_account) + yield account + + +@pytest.fixture(scope="class") +def class_account_sol_chain( + sol_client, solana_account, web3_client, web3_client_sol, pytestconfig, faucet, eth_bank_account +): + account = web3_client.create_account_with_balance(faucet, bank_account=eth_bank_account) + sol_client.request_airdrop(solana_account.public_key, 1 * LAMPORT_PER_SOL) + sol_client.deposit_wrapped_sol_from_solana_to_neon( + solana_account, account, web3_client_sol.eth.chain_id, pytestconfig.environment.evm_loader, 1 * LAMPORT_PER_SOL + ) + return account @pytest.fixture(scope="class") -def class_account(web3_client, faucet, eth_bank_account): - yield web3_client.create_account_with_balance(faucet, bank_account=eth_bank_account) +def account_with_all_tokens( + sol_client, solana_account, web3_client, web3_client_abc, web3_client_def, web3_client_sol, pytestconfig, faucet, + eth_bank_account, neon_mint, operator_keypair, evm_loader_keypair +): + account = web3_client.create_account_with_balance(faucet, bank_account=eth_bank_account) + if web3_client_sol: + sol_client.request_airdrop(solana_account.public_key, 1 * LAMPORT_PER_SOL) + sol_client.deposit_wrapped_sol_from_solana_to_neon( + solana_account, account, web3_client_sol.eth.chain_id, pytestconfig.environment.evm_loader, 1 * LAMPORT_PER_SOL + ) + for client in [web3_client_abc, web3_client_def]: + if client: + new_sol_account = Keypair.generate() + sol_client.send_sol(solana_account, new_sol_account.public_key, 5000000) + sol_client.deposit_neon_like_tokens_from_solana_to_neon(neon_mint, new_sol_account, account, + client.eth.chain_id, + operator_keypair, + evm_loader_keypair, + pytestconfig.environment.evm_loader, 1000000000000000000) + return account @pytest.fixture(scope="function") @@ -228,7 +277,18 @@ def withdraw_contract(web3_client, faucet, class_account): @pytest.fixture(scope="class") -def meta_proxy_contract(web3_client, faucet, class_account): +def common_contract(web3_client, class_account): + contract, _ = web3_client.deploy_and_get_contract( + contract="common/Common", + version="0.8.12", + contract_name="Common", + account=class_account, + ) + yield contract + + +@pytest.fixture(scope="class") +def meta_proxy_contract(web3_client, class_account): contract, _ = web3_client.deploy_and_get_contract("./EIPs/MetaProxy", "0.8.10", account=class_account) return contract @@ -239,6 +299,25 @@ def event_caller_contract(web3_client, class_account) -> typing.Any: yield event_caller +@pytest.fixture(scope="class") +def wsol(web3_client_sol, class_account_sol_chain): + contract, _ = web3_client_sol.deploy_and_get_contract( + contract="common/WNativeChainToken", + version="0.8.12", + contract_name="WNativeChainToken", + account=class_account_sol_chain, + ) + return contract + + +@pytest.fixture(scope="class") +def wneon(web3_client, faucet, class_account): + contract, _ = web3_client.deploy_and_get_contract( + "common/WNeon", "0.4.26", account=class_account, contract_name="WNEON" + ) + return contract + + @pytest.fixture(scope="class") def storage_contract(web3_client, class_account) -> typing.Any: contract, _ = web3_client.deploy_and_get_contract( @@ -249,3 +328,20 @@ def storage_contract(web3_client, class_account) -> typing.Any: constructor_args=[] ) yield contract + + +@pytest.fixture(scope="session") +def sol_price() -> float: + """Get SOL price from Solana mainnet""" + price = get_sol_price() + with allure.step(f"SOL price {price}$"): + return price + + +@pytest.fixture(scope="session") +def neon_price() -> float: + """Get SOL price from Solana mainnet""" + price = get_neon_price() + with allure.step(f"NEON price {price}$"): + return price + diff --git a/integration/tests/economy/conftest.py b/integration/tests/economy/conftest.py index 8085179ef7..1277b6361f 100644 --- a/integration/tests/economy/conftest.py +++ b/integration/tests/economy/conftest.py @@ -1,26 +1,12 @@ -import allure import pytest from pythclient.solana import SolanaClient from _pytest.config import Config -from utils.prices import get_sol_price, get_neon_price - - -@pytest.fixture(scope="session") -def sol_price() -> float: - """Get SOL price from Solana mainnet""" - price = get_sol_price() - with allure.step(f"SOL price {price}$"): - return price - - -@pytest.fixture(scope="session") -def neon_price() -> float: - """Get SOL price from Solana mainnet""" - price = get_neon_price() - with allure.step(f"NEON price {price}$"): - return price +from integration.tests.basic.helpers.chains import make_nonce_the_biggest_for_chain +from utils.erc20wrapper import ERC20Wrapper +from utils.erc721ForMetaplex import ERC721ForMetaplex +from utils.web3client import NeonChainWeb3Client @pytest.fixture(scope="session") @@ -31,3 +17,67 @@ def sol_client_tx_v2(pytestconfig: Config): pytestconfig.environment.account_seed_version, ) return client + + +@pytest.fixture(scope="class") +def counter_contract(account_with_all_tokens, client_and_price, web3_client_sol, web3_client): + w3_client, _ = client_and_price + 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) + return contract + + +@pytest.fixture(scope="class", params=["neon", "sol"]) +def client_and_price(web3_client, web3_client_sol, sol_price, neon_price, request, pytestconfig): + if request.param == "neon": + return web3_client, neon_price + elif request.param == "sol": + if "sol" in pytestconfig.environment.network_ids: + return web3_client_sol, sol_price + pytest.skip(f"{request.param} chain is not available") + + +@pytest.fixture(scope="class") +def erc20_wrapper( + account_with_all_tokens, + client_and_price, + faucet, + solana_account, + sol_client, + web3_client_sol, + web3_client, +): + client, _ = client_and_price + make_nonce_the_biggest_for_chain(account_with_all_tokens, client, [web3_client, web3_client_sol]) + contract = ERC20Wrapper( + client, + faucet, + "Test AAA", + "AAA", + sol_client, + account=account_with_all_tokens, + solana_account=solana_account, + mintable=True, + ) + contract.mint_tokens(account_with_all_tokens, contract.account.address) + return contract + + +@pytest.fixture(scope="class") +def erc721_neon_chain(web3_client: NeonChainWeb3Client, faucet, pytestconfig: Config, account_with_all_tokens): + contract = ERC721ForMetaplex(web3_client, faucet, account_with_all_tokens) + return contract + + +@pytest.fixture(scope="class") +def erc721( + erc721_neon_chain, + client_and_price, + faucet, + account_with_all_tokens +): + client, _ = client_and_price + contract = ERC721ForMetaplex(client, faucet, account=account_with_all_tokens, + contract_address=erc721_neon_chain.contract.address) + + return contract diff --git a/integration/tests/economy/const.py b/integration/tests/economy/const.py new file mode 100644 index 0000000000..5bffb2264f --- /dev/null +++ b/integration/tests/economy/const.py @@ -0,0 +1,30 @@ +import logging +from decimal import getcontext + +TX_COST = 5000 + +DECIMAL_CONTEXT = getcontext() +DECIMAL_CONTEXT.prec = 9 + +SOLCX_VERSIONS = ["0.6.6", "0.8.6", "0.8.10"] + +INSUFFICIENT_FUNDS_ERROR = "insufficient funds for" +GAS_LIMIT_ERROR = "gas limit reached" +LOGGER = logging.getLogger(__name__) +BIG_STRING = ( + "But I must explain to you how all this mistaken idea of denouncing pleasure and " + "praising pain was born and I will give you a complete account of the system, and " + "expound the actual teachings of the great explorer of the truth, the master-builder " + "of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it" + " is pleasure, but because those who do not know how to pursue pleasure rationally" + " encounter consequences that are extremely painful. Nor again is there anyone who" + " loves or pursues or desires to obtain pain of itself, because it is pain, but" + " because occasionally circumstances occur in which toil and pain can procure him" + " some great pleasure. To take a trivial example, which of us ever undertakes laborious" + " physical exercise, except to obtain some advantage from it? But who has any right to" + " find fault with a man who chooses to enjoy a pleasure that has no annoying consequences," + " or one who avoids a pain that produces no resultant pleasure? On the other hand," + " we denounce with righteous indigna" + " some great pleasure. To take a trivial example, which of us ever undertakes laborious" + " physical exercise, except to obtain some advantage from it? But who has any right to" +) diff --git a/integration/tests/economy/steps.py b/integration/tests/economy/steps.py new file mode 100644 index 0000000000..9a90e52719 --- /dev/null +++ b/integration/tests/economy/steps.py @@ -0,0 +1,85 @@ +import time +from decimal import Decimal + +import allure +from solana.rpc.core import RPCException +from solders.rpc.responses import GetTransactionResp +from solders.signature import Signature + +from integration.tests.economy.const import DECIMAL_CONTEXT, TX_COST +from utils.consts import LAMPORT_PER_SOL +from utils.helpers import wait_condition + + +@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 + 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) + + 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), + ) + with allure.step(msg): + assert token_cost > sol_cost, msg + + +@allure.step("Get single transaction gas") +def get_single_transaction_gas(): + """One TX_COST to verify Solana signature plus another one TX_COST to pay to Governance""" + return TX_COST * 2 + + +@allure.step("Check transaction used ALT") +def check_alt_on(web3_client, sol_client, receipt, accounts_quantity): + solana_trx = web3_client.get_solana_trx_by_neon(receipt["transactionHash"].hex()) + wait_condition( + lambda: sol_client.get_transaction( + Signature.from_string(solana_trx["result"][0]), + max_supported_transaction_version=0, + ) + != GetTransactionResp(None) + ) + trx = sol_client.get_transaction( + Signature.from_string(solana_trx["result"][0]), + max_supported_transaction_version=0, + ) + alt = trx.value.transaction.transaction.message.address_table_lookups + accounts = alt[0].writable_indexes + alt[0].readonly_indexes + assert len(accounts) == accounts_quantity - 2 + + +@allure.step("Check block for not using ALT") +def check_alt_off(block): + txs = block.value.transactions + for tx in txs: + if tx.version == 0 and tx.transaction.message.address_table_lookups: + raise AssertionError("ALT should not be used") + + +@allure.step("Get gas used percent") +def get_gas_used_percent(web3_client, receipt): + trx = web3_client.eth.get_transaction(receipt["transactionHash"]) + estimated_gas = trx["gas"] + percent = round(receipt["gasUsed"] / estimated_gas * 100, 2) + with allure.step(f"Gas used percent: {percent}%"): + pass + + +@allure.step("Wait for block") +def wait_for_block(client, block, timeout=60): + started = time.time() + while (time.time() - started) < timeout: + try: + return client.get_block(block, max_supported_transaction_version=2) + except RPCException: + time.sleep(3) + time.sleep(3) + raise TimeoutError("Block not available for slot") diff --git a/integration/tests/economy/test_economics.py b/integration/tests/economy/test_economics.py index f0d7355ae5..afb0ef84ed 100644 --- a/integration/tests/economy/test_economics.py +++ b/integration/tests/economy/test_economics.py @@ -1,8 +1,6 @@ import json -import logging import random -import time -from decimal import Decimal, getcontext +from decimal import Decimal import allure import pytest @@ -11,10 +9,8 @@ from _pytest.config import Config from solana.keypair import Keypair as SolanaAccount from solana.publickey import PublicKey -from solana.rpc.core import RPCException from solana.rpc.types import Commitment, TxOpts from solana.transaction import Transaction -from solders.rpc.responses import GetTransactionResp from solders.signature import Signature from spl.token.instructions import ( create_associated_token_account, @@ -23,37 +19,12 @@ from utils.consts import LAMPORT_PER_SOL from utils.erc20 import ERC20 -from utils.helpers import wait_condition +from utils.helpers import wait_condition, gen_hash_of_block +from .const import SOLCX_VERSIONS, INSUFFICIENT_FUNDS_ERROR, GAS_LIMIT_ERROR, BIG_STRING, TX_COST +from .steps import wait_for_block, assert_profit, get_gas_used_percent, check_alt_on, check_alt_off from ..base import BaseTests - -TX_COST = 5000 - -DECIMAL_CONTEXT = getcontext() -DECIMAL_CONTEXT.prec = 9 - -SOLCX_VERSIONS = ["0.6.6", "0.8.6", "0.8.10"] - -INSUFFICIENT_FUNDS_ERROR = "insufficient funds for" -GAS_LIMIT_ERROR = "gas limit reached" -LOGGER = logging.getLogger(__name__) -BIG_STRING = ( - "But I must explain to you how all this mistaken idea of denouncing pleasure and " - "praising pain was born and I will give you a complete account of the system, and " - "expound the actual teachings of the great explorer of the truth, the master-builder " - "of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it" - " is pleasure, but because those who do not know how to pursue pleasure rationally" - " encounter consequences that are extremely painful. Nor again is there anyone who" - " loves or pursues or desires to obtain pain of itself, because it is pain, but" - " because occasionally circumstances occur in which toil and pain can procure him" - " some great pleasure. To take a trivial example, which of us ever undertakes laborious" - " physical exercise, except to obtain some advantage from it? But who has any right to" - " find fault with a man who chooses to enjoy a pleasure that has no annoying consequences," - " or one who avoids a pain that produces no resultant pleasure? On the other hand," - " we denounce with righteous indigna" - " some great pleasure. To take a trivial example, which of us ever undertakes laborious" - " physical exercise, except to obtain some advantage from it? But who has any right to" -) +from ..basic.helpers.chains import make_nonce_the_biggest_for_chain # @pytest.fixture(scope="session", autouse=True) @@ -77,189 +48,154 @@ def install_solcx_versions(): class TestEconomics(BaseTests): acc = None - @pytest.fixture(autouse=True) - def save_token_costs(self, sol_price, neon_price): - self.sol_price = sol_price - self.neon_price = neon_price - - @allure.step("Verify operator profit") - def assert_profit(self, sol_diff, neon_diff): - sol_amount = sol_diff / LAMPORT_PER_SOL - if neon_diff < 0: - raise AssertionError(f"NEON has negative difference {neon_diff}") - neon_amount = neon_diff - sol_cost = Decimal(sol_amount, DECIMAL_CONTEXT) * Decimal( - self.sol_price, DECIMAL_CONTEXT - ) - neon_cost = Decimal(neon_amount, DECIMAL_CONTEXT) * Decimal( - self.neon_price, DECIMAL_CONTEXT - ) - - msg = "Operator receive {:.9f} NEON ({:.2f} $) and spend {:.9f} SOL ({:.2f} $), profit - {:.9f}% ".format( - neon_amount, - neon_cost, - sol_amount, - sol_cost, - ((neon_cost - sol_cost) / sol_cost * 100), - ) - with allure.step(msg): - assert neon_cost > sol_cost, msg - - @allure.step("Get single transaction gas") - def get_single_transaction_gas(self): - """One TX_COST to verify Solana signature plus another one TX_COST to pay to Governance""" - return TX_COST * 2 - - @allure.step("Check transaction used ALT") - def check_alt_on(self, sol_client, receipt, accounts_quantity): - solana_trx = self.web3_client.get_solana_trx_by_neon( - receipt["transactionHash"].hex() - ) - wait_condition( - lambda: sol_client.get_transaction( - Signature.from_string(solana_trx["result"][0]), - max_supported_transaction_version=0, - ) - != GetTransactionResp(None) - ) - trx = sol_client.get_transaction( - Signature.from_string(solana_trx["result"][0]), - max_supported_transaction_version=0, - ) - alt = trx.value.transaction.transaction.message.address_table_lookups - accounts = alt[0].writable_indexes + alt[0].readonly_indexes - assert len(accounts) == accounts_quantity - 2 - - @allure.step("Check block for not using ALT") - def check_alt_off(self, block): - txs = block.value.transactions - for tx in txs: - if tx.version == 0 and tx.transaction.message.address_table_lookups: - raise AssertionError("ALT should not be used") - - @allure.step("Get gas used percent") - def get_gas_used_percent(self, receipt): - trx = self.web3_client.eth.get_transaction(receipt["transactionHash"]) - estimated_gas = trx["gas"] - percent = round(receipt["gasUsed"] / estimated_gas * 100, 2) - with allure.step(f"Gas used percent: {percent}%"): - pass - @pytest.mark.only_stands - def test_account_creation(self): + def test_account_creation(self, client_and_price): """Verify account creation spend SOL""" + w3_client, token_price = client_and_price sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() - acc = self.web3_client.eth.account.create() - assert self.web3_client.eth.get_balance(acc.address) == Decimal(0) + neon_balance_before = self.operator.get_token_balance(w3_client) + acc = w3_client.eth.account.create() + assert w3_client.get_balance(acc.address) == Decimal(0) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + neon_balance_after = self.operator.get_token_balance(w3_client) assert neon_balance_after == neon_balance_before assert sol_balance_after == sol_balance_before - def test_send_neon_to_unexist_account(self): - """Verify how many cost neon send to new user""" + def test_send_neon_to_unexist_account(self, account_with_all_tokens, client_and_price, sol_price): + """Verify how many cost transfer of native chain token to new user""" + w3_client, token_price = client_and_price sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() - acc2 = self.web3_client.create_account() - receipt = self.web3_client.send_neon(self.acc, acc2, 5) - - assert self.web3_client.get_balance(acc2) == 5 + token_balance_before = self.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) + assert w3_client.get_balance(acc2) == transfer_value sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.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" - self.assert_profit(sol_diff, neon_balance_after - neon_balance_before) - self.get_gas_used_percent(receipt) + 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_send_neon_to_exist_account(self): - """Verify how many cost neon send to use who was already initialized""" - acc2 = self.web3_client.create_account() - self.web3_client.send_neon(self.acc, acc2, 1) + def test_send_tokens_to_exist_account(self, account_with_all_tokens, client_and_price, sol_price): + """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) - assert self.web3_client.get_balance(acc2) == 1 + assert w3_client.get_balance(acc2) == transfer_value // 2 sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() - receipt = self.web3_client.send_neon(self.acc, acc2, 5) + token_balance_before = self.operator.get_token_balance(w3_client) + receipt = w3_client.send_tokens(account_with_all_tokens, acc2, transfer_value // 2) - assert self.web3_client.get_balance(acc2) == 6 + assert w3_client.get_balance(acc2) == transfer_value sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) sol_diff = sol_balance_before - sol_balance_after - self.get_gas_used_percent(receipt) + get_gas_used_percent(w3_client, receipt) - assert ( - sol_balance_before > sol_balance_after - ), "Operator balance after send tx doesn't changed" - self.assert_profit(sol_diff, neon_balance_after - neon_balance_before) + assert sol_balance_before > sol_balance_after, "Operator balance after send tx doesn't changed" + 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) + + def test_send_tokens_without_chain_id(self, account_with_all_tokens, client_and_price, web3_client, sol_price): + # for transactions without chain_id NEONs would be sent (even for sol chain) + w3_client, token_price = client_and_price + acc2 = w3_client.create_account() + sol_balance_before = self.operator.get_solana_balance() + token_balance_before = self.operator.get_token_balance(web3_client) + + instruction_tx = self.create_tx_object( + account_with_all_tokens.address, acc2.address, 0.1, web3_client=w3_client + ) + instruction_tx.pop("chainId") + + w3_client.send_transaction(account_with_all_tokens, instruction_tx) - def test_send_when_not_enough_neon_to_gas(self): - acc2 = self.web3_client.create_account() + sol_balance_after = self.operator.get_solana_balance() + token_balance_after = self.operator.get_token_balance(web3_client) + sol_diff = sol_balance_before - sol_balance_after - assert self.web3_client.get_balance(acc2) == 0 + 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) - self.web3_client.send_neon(self.acc, acc2, 1) + def test_send_when_not_enough_tokens_to_gas(self, client_and_price, account_with_all_tokens): + 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) sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() + token_balance_before = self.operator.get_token_balance(w3_client) - acc3 = self.web3_client.create_account() + acc3 = w3_client.create_account() with pytest.raises(ValueError, match=INSUFFICIENT_FUNDS_ERROR) as e: - self.web3_client.send_neon(acc2, acc3, 1) + w3_client.send_tokens(acc2, acc3, transfer_amount) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) assert sol_balance_before == sol_balance_after - assert neon_balance_before == neon_balance_after + assert token_balance_before == token_balance_after - def test_erc20wrapper_transfer(self, erc20_spl_mintable): + def test_erc20wrapper_transfer(self, erc20_wrapper, client_and_price, sol_price): + w3_client, token_price = client_and_price sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() - - assert ( - erc20_spl_mintable.contract.functions.balanceOf(self.acc.address).call() - == 0 - ) - transfer_tx = erc20_spl_mintable.transfer(erc20_spl_mintable.account, self.acc, 25) - - assert ( - erc20_spl_mintable.contract.functions.balanceOf(self.acc.address).call() - == 25 - ) + token_balance_before = self.operator.get_token_balance(w3_client) + assert erc20_wrapper.contract.functions.balanceOf(self.acc.address).call() == 0 + transfer_tx = erc20_wrapper.transfer(erc20_wrapper.account, self.acc, 25) + assert erc20_wrapper.contract.functions.balanceOf(self.acc.address).call() == 25 + wait_condition(lambda: sol_balance_before > self.operator.get_solana_balance()) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) sol_diff = sol_balance_before - sol_balance_after assert sol_balance_before > sol_balance_after + 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, transfer_tx) - self.assert_profit(sol_diff, neon_balance_after - neon_balance_before) + def test_erc721_mint(self, erc721, client_and_price, account_with_all_tokens, sol_price): + w3_client, token_price = client_and_price + sol_balance_before = self.operator.get_solana_balance() + token_balance_before = self.operator.get_token_balance(w3_client) + seed = self.web3_client.text_to_bytes32(gen_hash_of_block(8)) + + erc721.mint(seed, account_with_all_tokens.address, "uri") + + wait_condition(lambda: sol_balance_before > self.operator.get_solana_balance()) + sol_balance_after = self.operator.get_solana_balance() + token_balance_after = self.operator.get_token_balance(w3_client) + sol_diff = sol_balance_before - sol_balance_after - self.get_gas_used_percent(transfer_tx) + assert sol_balance_before > sol_balance_after + 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) - def test_withdraw_neon_unexisting_ata(self, pytestconfig: Config): + def test_withdraw_neon_unexisting_ata(self, pytestconfig: Config, neon_price, sol_price): sol_user = SolanaAccount() self.sol_client.request_airdrop(sol_user.public_key, 5 * LAMPORT_PER_SOL) sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() + neon_balance_before = self.operator.get_token_balance(self.web3_client) user_neon_balance_before = self.web3_client.get_balance(self.acc) move_amount = self.web3_client._web3.to_wei(5, "ether") - contract, _ = self.web3_client.deploy_and_get_contract( - "precompiled/NeonToken", "0.8.10", account=self.acc - ) + contract, _ = self.web3_client.deploy_and_get_contract("precompiled/NeonToken", "0.8.10", account=self.acc) - instruction_tx = contract.functions.withdraw( - bytes(sol_user.public_key) - ).build_transaction( + instruction_tx = contract.functions.withdraw(bytes(sol_user.public_key)).build_transaction( { "from": self.acc.address, "nonce": self.web3_client.eth.get_transaction_count(self.acc.address), @@ -272,36 +208,34 @@ def test_withdraw_neon_unexisting_ata(self, pytestconfig: Config): assert (user_neon_balance_before - self.web3_client.get_balance(self.acc)) > 5 - balance = self.sol_client.get_account_info_json_parsed( - sol_user.public_key, commitment=Commitment("confirmed") - ) + balance = self.sol_client.get_account_info_json_parsed(sol_user.public_key, commitment=Commitment("confirmed")) assert int(balance.value.lamports) == int(move_amount / 1_000_000_000) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + neon_balance_after = self.operator.get_token_balance(self.web3_client) assert sol_balance_before > sol_balance_after assert neon_balance_after > neon_balance_before - self.assert_profit( + neon_diff = self.web3_client.to_main_currency(neon_balance_after - neon_balance_before) + assert_profit( sol_balance_before - sol_balance_after, - neon_balance_after - neon_balance_before, + sol_price, + neon_diff, + neon_price, + self.web3_client.native_token_name, ) - self.get_gas_used_percent(receipt) + get_gas_used_percent(self.web3_client, receipt) - def test_withdraw_neon_existing_ata(self, pytestconfig, neon_mint): + def test_withdraw_neon_existing_ata(self, pytestconfig, neon_mint, neon_price, sol_price): sol_user = SolanaAccount() self.sol_client.request_airdrop(sol_user.public_key, 5 * LAMPORT_PER_SOL) wait_condition(lambda: self.sol_client.get_balance(sol_user.public_key) != 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.public_key, sol_user.public_key, neon_mint)) opts = TxOpts(skip_preflight=True, skip_confirmation=False) self.sol_client.send_transaction(trx, sol_user, opts=opts) @@ -309,18 +243,14 @@ def test_withdraw_neon_existing_ata(self, pytestconfig, neon_mint): dest_token_acc = get_associated_token_address(sol_user.public_key, neon_mint) sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() + neon_balance_before = self.operator.get_token_balance(self.web3_client) user_neon_balance_before = self.web3_client.get_balance(self.acc) move_amount = self.web3_client._web3.to_wei(5, "ether") - contract, _ = self.web3_client.deploy_and_get_contract( - "precompiled/NeonToken", "0.8.10", account=self.acc - ) + contract, _ = self.web3_client.deploy_and_get_contract("precompiled/NeonToken", "0.8.10", account=self.acc) - instruction_tx = contract.functions.withdraw( - bytes(sol_user.public_key) - ).build_transaction( + instruction_tx = contract.functions.withdraw(bytes(sol_user.public_key)).build_transaction( { "from": self.acc.address, "nonce": self.web3_client.eth.get_transaction_count(self.acc.address), @@ -334,194 +264,134 @@ def test_withdraw_neon_existing_ata(self, pytestconfig, neon_mint): assert (user_neon_balance_before - self.web3_client.get_balance(self.acc)) > 5 balances = json.loads( - self.sol_client.get_token_account_balance( - dest_token_acc, Commitment("confirmed") - ).to_json() - ) - assert int(balances["result"]["value"]["amount"]) == int( - move_amount / 1_000_000_000 + self.sol_client.get_token_account_balance(dest_token_acc, Commitment("confirmed")).to_json() ) + assert int(balances["result"]["value"]["amount"]) == int(move_amount / 1_000_000_000) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + neon_balance_after = self.operator.get_token_balance(self.web3_client) assert sol_balance_before > sol_balance_after assert neon_balance_after > neon_balance_before - self.assert_profit( + neon_diff = self.web3_client.to_main_currency(neon_balance_after - neon_balance_before) + assert_profit( sol_balance_before - sol_balance_after, - neon_balance_after - neon_balance_before, - ) - self.get_gas_used_percent(receipt) - - def test_erc20_contract(self): - """Verify ERC20 token send""" - sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() - - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "EIPs/ERC20/ERC20.sol", - "0.8.8", - self.acc, - constructor_args=["Test Token", "TT", 1000], + sol_price, + neon_diff, + neon_price, + self.web3_client.native_token_name, ) - assert contract.functions.balanceOf(self.acc.address).call() == 1000 + get_gas_used_percent(self.web3_client, receipt) - sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() - sol_diff = sol_balance_before - sol_balance_after - - assert sol_balance_before > sol_balance_after - - self.assert_profit(sol_diff, neon_balance_after - neon_balance_before) - self.get_gas_used_percent(contract_deploy_tx) - - def test_erc20_transfer(self): + def test_erc20_transfer(self, client_and_price, account_with_all_tokens, web3_client_sol, web3_client, sol_price): """Verify ERC20 token send""" - - contract = ERC20(self.web3_client, self.faucet, owner=self.acc) + w3_client, token_price = client_and_price + make_nonce_the_biggest_for_chain(account_with_all_tokens, w3_client, [web3_client, web3_client_sol]) + contract = ERC20(w3_client, self.faucet, owner=account_with_all_tokens) sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() + token_balance_before = self.operator.get_token_balance(w3_client) - acc2 = self.web3_client.create_account() + acc2 = w3_client.create_account() - transfer_tx = contract.transfer( self.acc, acc2, 25) + transfer_tx = contract.transfer(account_with_all_tokens, acc2, 25) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) sol_diff = sol_balance_before - sol_balance_after assert sol_balance_before > sol_balance_after - assert neon_balance_after > neon_balance_before + assert token_balance_after > token_balance_before - self.assert_profit(sol_diff, neon_balance_after - neon_balance_before) - self.get_gas_used_percent(transfer_tx) + 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, transfer_tx) - def test_deploy_small_contract_less_100tx(self, sol_price): + def test_deploy_small_contract_less_100tx( + self, account_with_all_tokens, client_and_price, web3_client_sol, web3_client, sol_price + ): """Verify we are bill minimum for 100 instruction""" + w3_client, token_price = client_and_price sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() + token_balance_before = self.operator.get_token_balance(w3_client) - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=self.acc - ) + 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) sol_balance_after_deploy = self.operator.get_solana_balance() - neon_balance_after_deploy = self.operator.get_neon_balance() - - inc_tx = contract.functions.inc().build_transaction( - { - "from": self.acc.address, - "nonce": self.web3_client.eth.get_transaction_count(self.acc.address), - "gasPrice": self.web3_client.gas_price(), - } - ) + token_balance_after_deploy = self.operator.get_token_balance(w3_client) + tx = self.create_tx_object(account_with_all_tokens.address, estimate_gas=False, web3_client=w3_client) + inc_tx = contract.functions.inc().build_transaction(tx) assert contract.functions.get().call() == 0 - receipt = self.web3_client.send_transaction(self.acc, inc_tx) + receipt = w3_client.send_transaction(account_with_all_tokens, inc_tx) assert contract.functions.get().call() == 1 sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) assert sol_balance_before > sol_balance_after_deploy > sol_balance_after - assert neon_balance_after > neon_balance_after_deploy > neon_balance_before - self.assert_profit( - sol_balance_before - sol_balance_after, - neon_balance_after - neon_balance_before, + assert token_balance_after > token_balance_after_deploy > token_balance_before + token_diff = w3_client.to_main_currency(token_balance_after - token_balance_before) + assert_profit( + sol_balance_before - sol_balance_after, sol_price, token_diff, token_price, w3_client.native_token_name ) - self.get_gas_used_percent(receipt) - - def test_deploy_small_contract_less_gas(self): - sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() - - with pytest.raises(ValueError, match=GAS_LIMIT_ERROR): - self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", gas=1000, account=self.acc - ) - - sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() - - assert sol_balance_before == sol_balance_after - assert neon_balance_after == neon_balance_before + get_gas_used_percent(w3_client, receipt) - def test_deploy_small_contract_less_neon(self): - acc2 = self.web3_client.create_account() - self.web3_client.send_neon(self.acc, acc2, 0.001) - - sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() - - with pytest.raises(ValueError, match=INSUFFICIENT_FUNDS_ERROR): - self.web3_client.deploy_and_get_contract("common/Counter", "0.8.10", account=acc2) - - sol_balance_after_deploy = self.operator.get_solana_balance() - neon_balance_after_deploy = self.operator.get_neon_balance() - - assert sol_balance_before == sol_balance_after_deploy - assert neon_balance_before == neon_balance_after_deploy - - def test_deploy_to_losted_contract_account(self): + def test_deploy_to_losted_contract_account(self, account_with_all_tokens, client_and_price, sol_price): + w3_client, token_price = client_and_price sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() + token_balance_before = self.operator.get_token_balance(w3_client) - acc2 = self.web3_client.create_account() - self.web3_client.send_neon(self.acc, acc2, 0.001) + acc2 = w3_client.create_account() + w3_client.send_tokens(account_with_all_tokens, acc2, 1) with pytest.raises(ValueError, match=INSUFFICIENT_FUNDS_ERROR): - self.web3_client.deploy_and_get_contract("common/Counter", "0.8.10", account=acc2) - - self.web3_client.send_neon(self.acc, acc2, 50) - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=acc2 - ) + 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) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) assert sol_balance_before > sol_balance_after - assert neon_balance_after > neon_balance_before + assert token_balance_after > token_balance_before - self.assert_profit( - sol_balance_before - sol_balance_after, - neon_balance_after - neon_balance_before, + token_diff = w3_client.to_main_currency(token_balance_after - token_balance_before) + assert_profit( + sol_balance_before - sol_balance_after, sol_price, token_diff, token_price, w3_client.native_token_name ) - self.get_gas_used_percent(contract_deploy_tx) + get_gas_used_percent(w3_client, contract_deploy_tx) - def test_contract_get_is_free(self): + def test_contract_get_is_free(self, counter_contract, client_and_price, account_with_all_tokens): """Verify that get contract calls is free""" - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=self.acc - ) - + w3_client, token_price = client_and_price sol_balance_after_deploy = self.operator.get_solana_balance() - neon_balance_after_deploy = self.operator.get_neon_balance() + token_balance_after_deploy = self.operator.get_token_balance(w3_client) - user_balance_before = self.web3_client.get_balance(self.acc) - assert contract.functions.get().call() == 0 + user_balance_before = w3_client.get_balance(account_with_all_tokens) + assert counter_contract.functions.get().call() == 0 - assert self.web3_client.get_balance(self.acc) == user_balance_before + assert w3_client.get_balance(account_with_all_tokens) == user_balance_before sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) assert sol_balance_after_deploy == sol_balance_after - assert neon_balance_after_deploy == neon_balance_after + assert token_balance_after_deploy == token_balance_after @pytest.mark.xfail(reason="https://neonlabs.atlassian.net/browse/NDEV-699") - def test_cost_resize_account(self): + def test_cost_resize_account(self, neon_price, sol_price): """Verify how much cost account resize""" sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() + neon_balance_before = self.operator.get_token_balance(self.web3_client) contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( "common/IncreaseStorage", "0.8.10", account=self.acc ) sol_balance_before_increase = self.operator.get_solana_balance() - neon_balance_before_increase = self.operator.get_neon_balance() + neon_balance_before_increase = self.operator.get_token_balance(self.web3_client) inc_tx = contract.functions.inc().build_transaction( { @@ -534,470 +404,225 @@ def test_cost_resize_account(self): instruction_receipt = self.web3_client.send_transaction(self.acc, inc_tx) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + neon_balance_after = self.operator.get_token_balance(self.web3_client) - assert ( - sol_balance_before > sol_balance_before_increase > sol_balance_after - ), "SOL Balance not changed" - assert ( - neon_balance_after > neon_balance_before_increase > neon_balance_before - ), "NEON Balance incorrect" - self.assert_profit( + assert sol_balance_before > sol_balance_before_increase > sol_balance_after, "SOL Balance not changed" + assert neon_balance_after > neon_balance_before_increase > neon_balance_before, "NEON Balance incorrect" + neon_diff = self.web3_client.to_main_currency(neon_balance_after - neon_balance_before) + assert_profit( sol_balance_before - sol_balance_after, - neon_balance_after - neon_balance_before, - ) - self.get_gas_used_percent(instruction_receipt) - - def test_cost_resize_account_less_neon(self): - """Verify how much cost account resize""" - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/IncreaseStorage", "0.8.10", account=self.acc - ) - - acc2 = self.web3_client.create_account() - self.web3_client.send_neon(self.acc, acc2, 0.001) - - sol_balance_before_increase = self.operator.get_solana_balance() - neon_balance_before_increase = self.operator.get_neon_balance() - - inc_tx = contract.functions.inc().build_transaction( - { - "from": acc2.address, - "nonce": self.web3_client.eth.get_transaction_count(acc2.address), - "gasPrice": self.web3_client.gas_price(), - } + sol_price, + neon_diff, + neon_price, + self.web3_client.native_token_name, ) + get_gas_used_percent(self.web3_client, instruction_receipt) - with pytest.raises(ValueError, match=INSUFFICIENT_FUNDS_ERROR): - self.web3_client.send_transaction(acc2, inc_tx) - - sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() - - assert ( - sol_balance_before_increase == sol_balance_after - ), "SOL Balance not changed" - assert ( - neon_balance_after == neon_balance_before_increase - ), "NEON Balance incorrect" - - def test_failed_tx_when_less_gas(self): - """Don't get money from user if tx failed""" - sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() - - acc2 = self.web3_client.create_account() - - user_balance_before = self.web3_client.get_balance(self.acc) - with pytest.raises(ValueError, match=GAS_LIMIT_ERROR): - receipt = self.web3_client.send_neon(self.acc, acc2, 5, gas=100) - - assert user_balance_before == self.web3_client.get_balance(self.acc) - - sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() - - assert sol_balance_before == sol_balance_after - assert neon_balance_after == neon_balance_before - # self.assert_profit(sol_balance_before - sol_balance_after, neon_balance_after - neon_balance_before) - - def test_contract_interact_more_500_steps(self): + def test_contract_interact_1000_steps(self, counter_contract, client_and_price, account_with_all_tokens, sol_price): """Deploy a contract with more 500 instructions""" - sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() - - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=self.acc - ) + w3_client, token_price = client_and_price - sol_balance_before_instruction = self.operator.get_solana_balance() - neon_balance_before_instruction = self.operator.get_neon_balance() - - instruction_tx = contract.functions.moreInstruction( - 0, 100 - ).build_transaction( # 1086 steps in evm - { - "from": self.acc.address, - "nonce": self.web3_client.eth.get_transaction_count(self.acc.address), - "gasPrice": self.web3_client.gas_price(), - } - ) - instruction_receipt = self.web3_client.send_transaction( - self.acc, instruction_tx - ) + sol_balance_before = self.operator.get_solana_balance() + token_balance_before = self.operator.get_token_balance(w3_client) + tx = self.create_tx_object(account_with_all_tokens.address, estimate_gas=False, web3_client=w3_client) + 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) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) - assert ( - sol_balance_before > sol_balance_before_instruction > sol_balance_after - ), "SOL Balance not changed" - assert ( - neon_balance_after > neon_balance_before_instruction > neon_balance_before - ), "NEON Balance incorrect" - self.assert_profit( - sol_balance_before_instruction - sol_balance_after, - neon_balance_after - neon_balance_before_instruction, + assert sol_balance_before > sol_balance_after, "SOL Balance not changed" + assert token_balance_after > token_balance_before, "TOKEN Balance incorrect" + token_diff = w3_client.to_main_currency(token_balance_after - token_balance_before) + assert_profit( + sol_balance_before - sol_balance_after, sol_price, token_diff, token_price, w3_client.native_token_name ) - self.get_gas_used_percent(instruction_receipt) + get_gas_used_percent(w3_client, instruction_receipt) - def test_contract_interact_more_steps(self): + def test_contract_interact_500000_steps( + self, counter_contract, client_and_price, account_with_all_tokens, sol_price + ): """Deploy a contract with more 500000 bpf""" - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=self.acc - ) - - sol_balance_before_instruction = self.operator.get_solana_balance() - neon_balance_before_instruction = self.operator.get_neon_balance() + w3_client, token_price = client_and_price - instruction_tx = contract.functions.moreInstruction(0, 1500).build_transaction( - { - "from": self.acc.address, - "nonce": self.web3_client.eth.get_transaction_count(self.acc.address), - "gasPrice": self.web3_client.gas_price(), - } - ) + sol_balance_before = self.operator.get_solana_balance() + token_balance_before = self.operator.get_token_balance(w3_client) + tx = self.create_tx_object(account_with_all_tokens.address, estimate_gas=False, web3_client=w3_client) + instruction_tx = counter_contract.functions.moreInstruction(0, 1500).build_transaction(tx) - instruction_receipt = self.web3_client.send_transaction( - self.acc, instruction_tx - ) + instruction_receipt = w3_client.send_transaction(account_with_all_tokens, instruction_tx) + wait_condition(lambda: sol_balance_before > self.operator.get_solana_balance()) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) - assert ( - sol_balance_before_instruction > sol_balance_after - ), "SOL Balance not changed" - assert ( - neon_balance_after > neon_balance_before_instruction - ), "NEON Balance incorrect" + assert sol_balance_before > sol_balance_after, "SOL Balance not changed" + assert token_balance_after > token_balance_before, "TOKEN Balance incorrect" - self.assert_profit( - sol_balance_before_instruction - sol_balance_after, - neon_balance_after - neon_balance_before_instruction, + token_diff = w3_client.to_main_currency(token_balance_after - token_balance_before) + assert_profit( + sol_balance_before - sol_balance_after, sol_price, token_diff, token_price, w3_client.native_token_name ) - self.get_gas_used_percent(instruction_receipt) + get_gas_used_percent(w3_client, instruction_receipt) - def test_contract_interact_more_steps_less_gas(self): - """Deploy a contract with more 500000 bpf""" - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=self.acc - ) + def test_send_transaction_with_gas_limit_reached(self, counter_contract, client_and_price, account_with_all_tokens): + """Transaction with small amount of gas""" + w3_client, token_price = client_and_price - sol_balance_before_instruction = self.operator.get_solana_balance() - neon_balance_before_instruction = self.operator.get_neon_balance() + sol_balance_before = self.operator.get_solana_balance() + token_balance_before = self.operator.get_token_balance(w3_client) + + tx = self.create_tx_object(account_with_all_tokens.address, estimate_gas=False, web3_client=w3_client, gas=1000) + instruction_tx = counter_contract.functions.moreInstruction(0, 100).build_transaction(tx) - instruction_tx = contract.functions.moreInstruction(0, 1500).build_transaction( - { - "from": self.acc.address, - "nonce": self.web3_client.eth.get_transaction_count(self.acc.address), - "gasPrice": self.web3_client.gas_price(), - "gas": 1000, - } - ) with pytest.raises(ValueError, match=GAS_LIMIT_ERROR): - self.web3_client.send_transaction(self.acc, instruction_tx) + w3_client.send_transaction(account_with_all_tokens, instruction_tx) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() - - assert ( - sol_balance_after == sol_balance_before_instruction - ), "SOL Balance changes" - assert ( - neon_balance_after == neon_balance_before_instruction - ), "NEON Balance incorrect" + token_balance_after = self.operator.get_token_balance(w3_client) - def test_contract_interact_more_steps_less_neon(self): - """Deploy a contract with more 500000 bpf""" - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=self.acc - ) + assert sol_balance_after == sol_balance_before, "SOL Balance changes" + assert token_balance_after == token_balance_before, "TOKEN Balance incorrect" - acc2 = self.web3_client.create_account() - self.web3_client.send_neon(self.acc, acc2, 0.001) + def test_send_transaction_with_insufficient_funds( + self, counter_contract, client_and_price, account_with_all_tokens + ): + """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) - sol_balance_before_instruction = self.operator.get_solana_balance() - neon_balance_before_instruction = self.operator.get_neon_balance() + sol_balance_before = self.operator.get_solana_balance() + token_balance_before = self.operator.get_token_balance(w3_client) - instruction_tx = contract.functions.moreInstruction(0, 1500).build_transaction( - { - "from": acc2.address, - "nonce": self.web3_client.eth.get_transaction_count(acc2.address), - "gasPrice": self.web3_client.gas_price(), - } - ) + tx = self.create_tx_object(acc2.address, estimate_gas=False, web3_client=w3_client) + instruction_tx = counter_contract.functions.moreInstruction(0, 1500).build_transaction(tx) with pytest.raises(ValueError, match=INSUFFICIENT_FUNDS_ERROR): - self.web3_client.send_transaction(acc2, instruction_tx) + w3_client.send_transaction(acc2, instruction_tx) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) - assert ( - sol_balance_before_instruction == sol_balance_after - ), "SOL Balance changed" - assert ( - neon_balance_after == neon_balance_before_instruction - ), "NEON Balance incorrect" + assert sol_balance_before == sol_balance_after, "SOL Balance changed" + assert token_balance_after == token_balance_before, "TOKEN Balance incorrect" - # @pytest.mark.xfail(reason="Unprofitable transaction, because we create account not in evm (will be fixed)") - def test_tx_interact_more_1kb(self): + def test_tx_interact_more_1kb(self, counter_contract, client_and_price, account_with_all_tokens, sol_price): """Send to contract a big text (tx more than 1 kb)""" + w3_client, token_price = client_and_price sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() - - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=self.acc - ) + token_balance_before = self.operator.get_token_balance(w3_client) - sol_balance_before_instruction = self.operator.get_solana_balance() - neon_balance_before_instruction = self.operator.get_neon_balance() + tx = self.create_tx_object(account_with_all_tokens.address, estimate_gas=False, web3_client=w3_client) + instruction_tx = counter_contract.functions.bigString(BIG_STRING).build_transaction(tx) - instruction_tx = contract.functions.bigString(BIG_STRING).build_transaction( - { - "from": self.acc.address, - "nonce": self.web3_client.eth.get_transaction_count(self.acc.address), - "gasPrice": self.web3_client.gas_price(), - } - ) - - instruction_receipt = self.web3_client.send_transaction( - self.acc, instruction_tx - ) + instruction_receipt = w3_client.send_transaction(account_with_all_tokens, instruction_tx) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) - assert ( - sol_balance_before > sol_balance_before_instruction > sol_balance_after - ), "SOL Balance not changed" - assert ( - neon_balance_after > neon_balance_before_instruction > neon_balance_before - ), "NEON Balance incorrect" - - self.assert_profit( - sol_balance_before_instruction - sol_balance_after, - neon_balance_after - neon_balance_before_instruction, - ) - self.get_gas_used_percent(instruction_receipt) - - def test_tx_interact_more_1kb_less_neon(self): - """Send to contract a big text (tx more than 1 kb) when less neon""" - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=self.acc - ) - - acc2 = self.web3_client.create_account() - self.web3_client.send_neon(self.acc, acc2, 0.001) - - sol_balance_before_instruction = self.operator.get_solana_balance() - neon_balance_before_instruction = self.operator.get_neon_balance() - - instruction_tx = contract.functions.bigString(BIG_STRING).build_transaction( - { - "from": acc2.address, - "nonce": self.web3_client.eth.get_transaction_count(acc2.address), - "gasPrice": self.web3_client.gas_price(), - } - ) - with pytest.raises(ValueError, match=INSUFFICIENT_FUNDS_ERROR): - instruction_receipt = self.web3_client.send_transaction( - acc2, instruction_tx - ) - - sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() - - assert ( - sol_balance_before_instruction == sol_balance_after - ), "SOL Balance changed" - assert ( - neon_balance_after == neon_balance_before_instruction - ), "NEON Balance incorrect" - - def test_tx_interact_more_1kb_less_gas(self): - """Send to contract a big text (tx more than 1 kb)""" - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=self.acc - ) - - sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() + assert sol_balance_before > sol_balance_after, "SOL Balance not changed" + assert token_balance_after > token_balance_before, "TOKEN Balance incorrect" - instruction_tx = contract.functions.bigString(BIG_STRING).build_transaction( - { - "from": self.acc.address, - "nonce": self.web3_client.eth.get_transaction_count(self.acc.address), - "gasPrice": self.web3_client.gas_price(), - "gas": 100, - } + token_diff = w3_client.to_main_currency(token_balance_after - token_balance_before) + assert_profit( + sol_balance_before - sol_balance_after, sol_price, token_diff, token_price, w3_client.native_token_name ) - with pytest.raises(ValueError, match=GAS_LIMIT_ERROR): - receipt = self.web3_client.send_transaction(self.acc, instruction_tx) + get_gas_used_percent(w3_client, instruction_receipt) - sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() - - assert sol_balance_before == sol_balance_after, "SOL Balance changed" - assert neon_balance_after == neon_balance_before, "NEON Balance incorrect" + def test_deploy_contract_more_1kb( + self, client_and_price, account_with_all_tokens, web3_client, web3_client_sol, sol_price + ): + w3_client, token_price = client_and_price - def test_deploy_contract_more_1kb(self): + make_nonce_the_biggest_for_chain(account_with_all_tokens, w3_client, [web3_client, web3_client_sol]) sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() + token_balance_before = self.operator.get_token_balance(w3_client) - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Fat", "0.8.10", account=self.acc + contract, contract_deploy_tx = w3_client.deploy_and_get_contract( + "common/Fat", "0.8.10", account=account_with_all_tokens ) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) assert sol_balance_before > sol_balance_after - assert neon_balance_after > neon_balance_before + assert token_balance_after > token_balance_before - self.assert_profit( - sol_balance_before - sol_balance_after, - neon_balance_after - neon_balance_before, + token_diff = w3_client.to_main_currency(token_balance_after - token_balance_before) + assert_profit( + sol_balance_before - sol_balance_after, sol_price, token_diff, token_price, w3_client.native_token_name ) - self.get_gas_used_percent(contract_deploy_tx) + get_gas_used_percent(w3_client, contract_deploy_tx) - def test_deploy_contract_more_1kb_less_neon(self): - acc2 = self.web3_client.create_account() - self.web3_client.send_neon(self.acc, acc2, 0.001) + def test_deploy_contract_to_payed( + self, client_and_price, account_with_all_tokens, web3_client, web3_client_sol, sol_price + ): + w3_client, token_price = client_and_price + make_nonce_the_biggest_for_chain(account_with_all_tokens, w3_client, [web3_client, web3_client_sol]) + nonce = w3_client.eth.get_transaction_count(account_with_all_tokens.address) + contract_address = w3_client.keccak(rlp.encode((bytes.fromhex(self.acc.address[2:]), nonce)))[-20:] - sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() + w3_client.send_tokens(account_with_all_tokens, w3_client.to_checksum_address(contract_address.hex()), 5000) - with pytest.raises(ValueError, match=INSUFFICIENT_FUNDS_ERROR): - _, _ = self.web3_client.deploy_and_get_contract( - "common/Fat", "0.8.10", account=acc2 - ) - - sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() - - assert sol_balance_before == sol_balance_after - assert neon_balance_after == neon_balance_before - - def test_deploy_contract_more_1kb_less_gas(self): sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() - - with pytest.raises(ValueError, match=GAS_LIMIT_ERROR): - self.web3_client.deploy_and_get_contract( - "common/Fat", "0.8.10", account=self.acc, gas=1000 - ) - - sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() - - assert sol_balance_before == sol_balance_after - assert neon_balance_after == neon_balance_before - - def test_deploy_contract_to_payed(self): - acc2 = self.web3_client.create_account() - self.web3_client.send_neon(self.acc, acc2, 10) - - nonce = self.web3_client.eth.get_transaction_count(self.acc.address) - contract_address = self.web3_client.keccak( - rlp.encode((bytes.fromhex(self.acc.address[2:]), nonce)) - )[-20:] + token_balance_before = self.operator.get_token_balance(w3_client) - self.web3_client.send_neon( - acc2, self.web3_client.to_checksum_address(contract_address.hex()), 0.5 - ) - - sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() - - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=self.acc + contract, contract_deploy_tx = w3_client.deploy_and_get_contract( + "common/Counter", "0.8.10", account=account_with_all_tokens ) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) assert sol_balance_before > sol_balance_after, "SOL Balance not changed" - assert neon_balance_after > neon_balance_before, "NEON Balance incorrect" - self.assert_profit( - sol_balance_before - sol_balance_after, - neon_balance_after - neon_balance_before, + assert token_balance_after > token_balance_before, "TOKEN Balance incorrect" + token_diff = w3_client.to_main_currency(token_balance_after - token_balance_before) + assert_profit( + sol_balance_before - sol_balance_after, sol_price, token_diff, token_price, w3_client.native_token_name ) - self.get_gas_used_percent(contract_deploy_tx) + get_gas_used_percent(w3_client, contract_deploy_tx) - def test_deploy_contract_to_exist_unpayed(self): - acc2 = self.web3_client.create_account() - self.web3_client.send_neon(self.acc, acc2, 50) + def test_deploy_contract_to_exist_unpayed( + self, client_and_price, account_with_all_tokens, web3_client, web3_client_sol, sol_price + ): + w3_client, token_price = client_and_price sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() + token_balance_before = self.operator.get_token_balance(w3_client) - nonce = self.web3_client.eth.get_transaction_count(acc2.address) - contract_address = self.web3_client.to_checksum_address( - self.web3_client.keccak( - rlp.encode((bytes.fromhex(acc2.address[2:]), nonce)) - )[-20:].hex() + make_nonce_the_biggest_for_chain(account_with_all_tokens, w3_client, [web3_client, web3_client_sol]) + nonce = w3_client.eth.get_transaction_count(account_with_all_tokens.address) + contract_address = w3_client.to_checksum_address( + w3_client.keccak(rlp.encode((bytes.fromhex(account_with_all_tokens.address[2:]), nonce)))[-20:].hex() ) with pytest.raises(ValueError, match=GAS_LIMIT_ERROR): - self.web3_client.send_neon(acc2, contract_address, 1, gas=1) + w3_client.send_tokens(account_with_all_tokens, contract_address, 100, gas=1) - _, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=acc2 + _, contract_deploy_tx = w3_client.deploy_and_get_contract( + "common/Counter", "0.8.10", account=account_with_all_tokens ) sol_balance_after_deploy = self.operator.get_solana_balance() - neon_balance_after_deploy = self.operator.get_neon_balance() + token_balance_after_deploy = self.operator.get_token_balance(w3_client) assert sol_balance_before > sol_balance_after_deploy - assert neon_balance_after_deploy > neon_balance_before - self.assert_profit( + assert token_balance_after_deploy > token_balance_before + token_diff = w3_client.to_main_currency(token_balance_after_deploy - token_balance_before) + assert_profit( sol_balance_before - sol_balance_after_deploy, - neon_balance_after_deploy - neon_balance_before, - ) - self.get_gas_used_percent(contract_deploy_tx) - - def test_interact_with_contract_from_non_payed_user(self): - acc2 = self.web3_client.create_account() - self.faucet.request_neon(acc2.address, 10) - - sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() - - contract, contract_deploy_tx = self.web3_client.deploy_and_get_contract( - "common/Counter", "0.8.10", account=self.acc - ) - - sol_balance_after_deploy = self.operator.get_solana_balance() - neon_balance_after_deploy = self.operator.get_neon_balance() - - inc_tx = contract.functions.inc().build_transaction( - { - "from": acc2.address, - "nonce": self.web3_client.eth.get_transaction_count(acc2.address), - "gasPrice": self.web3_client.gas_price(), - } - ) - - self.web3_client.send_transaction(acc2, inc_tx) - - assert contract.functions.get().call() == 1 - - sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() - - assert sol_balance_before > sol_balance_after_deploy > sol_balance_after - assert neon_balance_after > neon_balance_after_deploy > neon_balance_before - self.assert_profit( - sol_balance_before - sol_balance_after, - neon_balance_after - neon_balance_before, + sol_price, + token_diff, + token_price, + w3_client.native_token_name, ) + get_gas_used_percent(w3_client, contract_deploy_tx) @pytest.mark.timeout(960) - def test_deploy_contract_alt_on(self, sol_client): + def test_deploy_contract_alt_on(self, sol_client, neon_price, sol_price): """Trigger transaction than requires more than 30 accounts""" accounts_quantity = random.randint(31, 45) sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() + neon_balance_before = self.operator.get_token_balance(self.web3_client) contract, _ = self.web3_client.deploy_and_get_contract( "common/ALT", "0.8.10", account=self.acc, constructor_args=[8] @@ -1011,10 +636,8 @@ def test_deploy_contract_alt_on(self, sol_client): } ) receipt = self.web3_client.send_transaction(self.acc, tx) - self.check_alt_on(sol_client, receipt, accounts_quantity) - solana_trx = self.web3_client.get_solana_trx_by_neon( - receipt["transactionHash"].hex() - ) + check_alt_on(self.web3_client, sol_client, receipt, accounts_quantity) + solana_trx = self.web3_client.get_solana_trx_by_neon(receipt["transactionHash"].hex()) sol_trx_with_alt = None for trx in solana_trx["result"]: @@ -1028,15 +651,9 @@ def test_deploy_contract_alt_on(self, sol_client): if not sol_trx_with_alt: raise ValueError(f"There are no lookup table for {solana_trx}") - operator = PublicKey( - sol_trx_with_alt.value.transaction.transaction.message.account_keys[0] - ) + operator = PublicKey(sol_trx_with_alt.value.transaction.transaction.message.account_keys[0]) - alt_address = ( - sol_trx_with_alt.value.transaction.transaction.message.address_table_lookups[ - 0 - ].account_key - ) + alt_address = sol_trx_with_alt.value.transaction.transaction.message.address_table_lookups[0].account_key alt_balance = sol_client.get_balance(PublicKey(alt_address)).value operator_balance = sol_client.get_balance(operator).value @@ -1045,13 +662,17 @@ def test_deploy_contract_alt_on(self, sol_client): timeout_sec=120, ) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + neon_balance_after = self.operator.get_token_balance(self.web3_client) assert sol_balance_before > sol_balance_after assert neon_balance_after > neon_balance_before - self.assert_profit( + neon_diff = self.web3_client.to_main_currency(neon_balance_after - neon_balance_before) + assert_profit( sol_balance_before - sol_balance_after - alt_balance, - neon_balance_after - neon_balance_before, + sol_price, + neon_diff, + neon_price, + self.web3_client.native_token_name, ) # the charge for alt creating should be returned wait_condition( @@ -1061,55 +682,89 @@ def test_deploy_contract_alt_on(self, sol_client): ) assert ( - operator_balance + alt_balance - TX_COST * 2 - == sol_client.get_balance(operator).value + operator_balance + alt_balance - TX_COST * 2 == sol_client.get_balance(operator).value ), "Operator balance after the return of the alt creation fee is not correct" - self.get_gas_used_percent(receipt) + get_gas_used_percent(self.web3_client, receipt) - @pytest.mark.parametrize("accounts_quantity", [10]) - def test_deploy_contract_alt_off(self, sol_client, accounts_quantity): + def test_deploy_contract_alt_off( + self, sol_client, client_and_price, account_with_all_tokens, web3_client, web3_client_sol, sol_price + ): """Trigger transaction than requires less than 30 accounts""" + accounts_quantity = 10 + w3_client, token_price = client_and_price + make_nonce_the_biggest_for_chain(account_with_all_tokens, w3_client, [web3_client, web3_client_sol]) sol_balance_before = self.operator.get_solana_balance() - neon_balance_before = self.operator.get_neon_balance() + token_balance_before = self.operator.get_token_balance(w3_client) - contract, _ = self.web3_client.deploy_and_get_contract( - "common/ALT", "0.8.10", account=self.acc, constructor_args=[8] + contract, _ = w3_client.deploy_and_get_contract( + "common/ALT", "0.8.10", account=account_with_all_tokens, constructor_args=[8] ) sol_balance_after_deploy = self.operator.get_solana_balance() - neon_balance_after_deploy = self.operator.get_neon_balance() + token_balance_after_deploy = self.operator.get_token_balance(w3_client) - tx = contract.functions.fill(accounts_quantity).build_transaction( - { - "from": self.acc.address, - "nonce": self.web3_client.eth.get_transaction_count(self.acc.address), - "gasPrice": self.web3_client.gas_price(), - } - ) - receipt = self.web3_client.send_transaction(self.acc, tx) + tx = self.create_tx_object(account_with_all_tokens.address, estimate_gas=False, web3_client=w3_client) + instr = contract.functions.fill(accounts_quantity).build_transaction(tx) + receipt = w3_client.send_transaction(account_with_all_tokens, instr) block = int(receipt["blockNumber"]) response = wait_for_block(sol_client, block) - self.check_alt_off(response) + check_alt_off(response) sol_balance_after = self.operator.get_solana_balance() - neon_balance_after = self.operator.get_neon_balance() + token_balance_after = self.operator.get_token_balance(w3_client) assert sol_balance_before > sol_balance_after_deploy > sol_balance_after - assert neon_balance_after > neon_balance_after_deploy > neon_balance_before - self.assert_profit( - sol_balance_before - sol_balance_after, - neon_balance_after - neon_balance_before, + assert token_balance_after > token_balance_after_deploy > token_balance_before + token_diff = w3_client.to_main_currency(token_balance_after - token_balance_after_deploy) + assert_profit( + sol_balance_after_deploy - sol_balance_after, + sol_price, + token_diff, + token_price, + w3_client.native_token_name, + ) + get_gas_used_percent(w3_client, receipt) + + def test_deploy_big_contract_with_structures(self, client_and_price, account_with_all_tokens, sol_price): + w3_client, token_price = client_and_price + + sol_balance_before = self.operator.get_solana_balance() + token_balance_before = self.operator.get_token_balance(w3_client) + + contract, receipt = w3_client.deploy_and_get_contract("EIPs/ERC3475", "0.8.10", account_with_all_tokens) + + sol_balance_after = self.operator.get_solana_balance() + token_balance_after = self.operator.get_token_balance(w3_client) + token_diff = w3_client.to_main_currency(token_balance_after - token_balance_before) + assert_profit( + sol_balance_before - sol_balance_after, sol_price, token_diff, token_price, w3_client.native_token_name + ) + get_gas_used_percent(w3_client, receipt) + + @pytest.mark.parametrize("value", [20, 25, 55]) + def test_call_contract_with_mapping_updating( + self, client_and_price, account_with_all_tokens, sol_price, web3_client, web3_client_sol, value + ): + w3_client, token_price = client_and_price + 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 + ) + + sol_balance_before = self.operator.get_solana_balance() + token_balance_before = self.operator.get_token_balance(w3_client) + + tx = self.create_tx_object(account_with_all_tokens.address, estimate_gas=False, web3_client=w3_client) + + instruction_tx = contract.functions.replaceValues(value).build_transaction(tx) + receipt = w3_client.send_transaction(account_with_all_tokens, instruction_tx) + assert receipt["status"] == 1 + wait_condition(lambda: sol_balance_before != self.operator.get_solana_balance()) + + sol_balance_after = self.operator.get_solana_balance() + token_balance_after = self.operator.get_token_balance(w3_client) + token_diff = w3_client.to_main_currency(token_balance_after - token_balance_before) + assert_profit( + sol_balance_before - sol_balance_after, sol_price, token_diff, token_price, w3_client.native_token_name ) - self.get_gas_used_percent(receipt) - - -def wait_for_block(client, block, timeout=60): - started = time.time() - while (time.time() - started) < timeout: - try: - return client.get_block(block, max_supported_transaction_version=2) - except RPCException: - time.sleep(3) - time.sleep(3) - raise TimeoutError("Block not available for slot") diff --git a/integration/tests/migrations/__init__.py b/integration/tests/migrations/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/integration/tests/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/integration/tests/migrations/test_account_migration.py b/integration/tests/migrations/test_account_migration.py new file mode 100644 index 0000000000..846783f39d --- /dev/null +++ b/integration/tests/migrations/test_account_migration.py @@ -0,0 +1,275 @@ +"""Tests to check old accounts continue work after structure changes +Environment variables ACCOUNTS, ERC20_ADDRESS, ERC721_ADDRESS should be set""" +import os + +import pytest +from web3.logs import DISCARD + +from integration.tests.base import BaseTests +from integration.tests.economy.steps import assert_profit +from utils.erc20wrapper import ERC20Wrapper +from utils.erc721ForMetaplex import ERC721ForMetaplex +from utils.helpers import gen_hash_of_block + + +@pytest.fixture(scope="session") +def accounts(web3_client): + account_keys = os.environ.get("ACCOUNTS").split(",") + accounts = [] + for key in account_keys: + accounts.append(web3_client.eth.account.from_key(key)) + print("Before testing:") + for acc in accounts: + print( + f"Balance for {acc.address}: {web3_client.to_atomic_currency(web3_client.get_balance(acc.address))}, nonce: {web3_client.eth.get_transaction_count(acc.address)}" + ) + + yield accounts + print("After testing:") + for acc in accounts: + print( + f"Balance for {acc.address}: {web3_client.to_atomic_currency(web3_client.get_balance(acc.address))}, nonce: {web3_client.eth.get_transaction_count(acc.address)}" + ) + + +@pytest.fixture(scope="session") +def bob(accounts): + return accounts[0] + + +@pytest.fixture(scope="session") +def alice(accounts): + return accounts[1] + + +@pytest.fixture(scope="session") +def trx_list(): + list = [] + yield list + print("Trx list:") + for trx in list: + print(trx.hex()) + + +@pytest.fixture(scope="session") +def erc20(web3_client, faucet, sol_client, solana_account, bob): + contract_address = os.environ.get("ERC20_ADDRESS") + + if contract_address: + erc20 = ERC20Wrapper( + web3_client, + faucet, + "Test AAA", + "AAA", + sol_client, + account=bob, + solana_account=solana_account, + mintable=True, + contract_address=contract_address, + ) + print(f"Using ERC20 deployed earlier at {contract_address}") + + else: + erc20 = ERC20Wrapper( + web3_client, + faucet, + "Test AAA", + "AAA", + sol_client, + account=bob, + solana_account=solana_account, + mintable=True, + ) + print(f"ERC20 deployed at address: {erc20.contract.address}") + erc20.mint_tokens(erc20.account, erc20.account.address) + return erc20 + + +@pytest.fixture(scope="session") +def erc721(web3_client, faucet, bob): + contract_address = os.environ.get("ERC721_ADDRESS") + if contract_address: + erc721 = ERC721ForMetaplex(web3_client, faucet, account=bob, contract_address=contract_address) + print(f"Using ERC721 deployed earlier at {contract_address}") + else: + erc721 = ERC721ForMetaplex(web3_client, faucet, account=bob) + print(f"ERC721 deployed at address: {erc721.contract.address}") + + return erc721 + + +class TestAccountMigration(BaseTests): + @pytest.fixture(scope="function") + def check_operator_balance( + self, web3_client, sol_client, operator_keypair, solana_account, neon_price, sol_price, request + ): + print("test case name:", request.node.name) + sol_balance_before = self.operator.get_solana_balance() + token_balance_before = self.operator.get_token_balance(web3_client) + + yield + sol_balance_after = self.operator.get_solana_balance() + token_balance_after = self.operator.get_token_balance(web3_client) + sol_diff = sol_balance_before - sol_balance_after + + assert sol_balance_before > sol_balance_after + assert token_balance_after > token_balance_before + + 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): + receipt = web3_client.send_neon(alice, accounts[i + 1], 5) + trx_list.append(receipt["transactionHash"]) + assert receipt["status"] == 1 + receipt = web3_client.send_neon(bob, accounts[i + 2], 1) + trx_list.append(receipt["transactionHash"]) + assert receipt["status"] == 1 + + def test_contract_deploy_economics(self, alice, bob, web3_client, check_operator_balance): + web3_client.deploy_and_get_contract("common/EventCaller", "0.8.12", bob) + + def test_contract_deploy_and_interact(self, web3_client, accounts, trx_list, check_operator_balance): + acc1 = accounts[7] + acc2 = accounts[8] + contract_a, receipt = web3_client.deploy_and_get_contract( + "common/NestedCallsChecker", "0.8.12", acc2, contract_name="A" + ) + trx_list.append(receipt["transactionHash"]) + contract_b, receipt = web3_client.deploy_and_get_contract( + "common/NestedCallsChecker", "0.8.12", acc2, contract_name="B" + ) + trx_list.append(receipt["transactionHash"]) + contract_c, receipt = web3_client.deploy_and_get_contract( + "common/NestedCallsChecker", "0.8.12", acc1, contract_name="C" + ) + trx_list.append(receipt["transactionHash"]) + + tx = { + "from": acc1.address, + "nonce": web3_client.eth.get_transaction_count(acc1.address), + "gasPrice": web3_client.gas_price(), + } + + instruction_tx = contract_a.functions.method1(contract_b.address, contract_c.address).build_transaction(tx) + resp = web3_client.send_transaction(acc1, instruction_tx) + trx_list.append(resp["transactionHash"]) + + event_a1_logs = contract_a.events.EventA1().process_receipt(resp, errors=DISCARD) + assert len(event_a1_logs) == 1 + event_b1_logs = contract_b.events.EventB1().process_receipt(resp, errors=DISCARD) + assert len(event_b1_logs) == 1 + event_b2_logs = contract_b.events.EventB2().process_receipt(resp, errors=DISCARD) + event_c1_logs = contract_c.events.EventC1().process_receipt(resp, errors=DISCARD) + event_c2_logs = contract_c.events.EventC2().process_receipt(resp, errors=DISCARD) + for log in (event_b2_logs, event_c1_logs, event_c2_logs): + assert log == (), f"Trx shouldn't contain logs for the events: eventB2, eventC1, eventC2_log0. Log: {log}" + + def test_economics_for_erc721_mint(self, erc721, web3_client, check_operator_balance): + 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, bob, alice, accounts, trx_list, check_operator_balance): + seed = web3_client.text_to_bytes32(gen_hash_of_block(8)) + token_id = erc721.mint(seed, erc721.account.address, "uri") + + balance_usr1_before = erc721.contract.functions.balanceOf(erc721.account.address).call() + balance_usr2_before = erc721.contract.functions.balanceOf(alice.address).call() + + resp = erc721.approve(alice.address, token_id, erc721.account) + trx_list.append(resp["transactionHash"]) + + resp = erc721.transfer_from(erc721.account.address, alice.address, token_id, alice) + trx_list.append(resp["transactionHash"]) + + balance_usr1_after = erc721.contract.functions.balanceOf(erc721.account.address).call() + balance_usr2_after = erc721.contract.functions.balanceOf(alice.address).call() + + assert balance_usr1_after - balance_usr1_before == -1 + assert balance_usr2_after - balance_usr2_before == 1 + + token_ids = [] + for _ in range(5): + seed = web3_client.text_to_bytes32(gen_hash_of_block(8)) + token_ids.append(erc721.mint(seed, erc721.account.address, "uri")) + + for i in range(5): + recipient = accounts[i + 3] + + balance_usr1_before = erc721.contract.functions.balanceOf(erc721.account.address).call() + balance_usr2_before = erc721.contract.functions.balanceOf(recipient.address).call() + + resp = erc721.transfer_from(erc721.account.address, recipient.address, token_ids[i], erc721.account) + trx_list.append(resp["transactionHash"]) + + balance_usr1_after = erc721.contract.functions.balanceOf(erc721.account.address).call() + balance_usr2_after = erc721.contract.functions.balanceOf(recipient.address).call() + + assert balance_usr1_after - balance_usr1_before == -1 + assert balance_usr2_after - balance_usr2_before == 1 + + def test_erc20_interaction(self, erc20, web3_client, bob, alice, accounts, trx_list, check_operator_balance): + balance_before = erc20.contract.functions.balanceOf(erc20.account.address).call() + amount = 500 + resp = erc20.mint_tokens(erc20.account, erc20.account.address, amount) + trx_list.append(resp["transactionHash"]) + + balance_after = erc20.contract.functions.balanceOf(erc20.account.address).call() + assert balance_after == balance_before + amount + + tom = accounts[9] + balance_before = erc20.contract.functions.balanceOf(tom.address).call() + resp = erc20.mint_tokens(erc20.account, tom.address, amount) + trx_list.append(resp["transactionHash"]) + + balance_after = erc20.contract.functions.balanceOf(tom.address).call() + assert balance_after == amount + balance_before + + balance_before = erc20.contract.functions.balanceOf(tom.address).call() + total_before = erc20.contract.functions.totalSupply().call() + resp = erc20.burn(tom, tom.address, amount) + trx_list.append(resp["transactionHash"]) + + balance_after = erc20.contract.functions.balanceOf(tom.address).call() + total_after = erc20.contract.functions.totalSupply().call() + + assert balance_after == balance_before - amount + assert total_after == total_before - amount + + amount = 1000000000000000 + resp = erc20.mint_tokens(erc20.account, accounts[9].address, amount) + trx_list.append(resp["transactionHash"]) + + amount = 500 + for i in range(8): + recipient = accounts[i] + sender = accounts[9] + balance_acc1_before = erc20.contract.functions.balanceOf(sender.address).call() + balance_acc2_before = erc20.contract.functions.balanceOf(recipient.address).call() + total_before = erc20.contract.functions.totalSupply().call() + resp = erc20.transfer(sender, recipient.address, amount) + trx_list.append(resp["transactionHash"]) + + balance_acc1_after = erc20.contract.functions.balanceOf(sender.address).call() + balance_acc2_after = erc20.contract.functions.balanceOf(recipient.address).call() + total_after = erc20.contract.functions.totalSupply().call() + assert balance_acc1_after == balance_acc1_before - amount + assert balance_acc2_after == balance_acc2_before + amount + assert total_before == total_after + + for i in range(7): + recipient = accounts[8] + sender = accounts[i] + balance_acc1_before = erc20.contract.functions.balanceOf(sender.address).call() + balance_acc2_before = erc20.contract.functions.balanceOf(recipient.address).call() + total_before = erc20.contract.functions.totalSupply().call() + resp = erc20.transfer(sender, recipient.address, amount) + trx_list.append(resp["transactionHash"]) + balance_acc1_after = erc20.contract.functions.balanceOf(sender.address).call() + balance_acc2_after = erc20.contract.functions.balanceOf(recipient.address).call() + total_after = erc20.contract.functions.totalSupply().call() + assert balance_acc1_after == balance_acc1_before - amount + assert balance_acc2_after == balance_acc2_before + amount + assert total_before == total_after diff --git a/integration/tests/neon_evm/README.md b/integration/tests/neon_evm/README.md new file mode 100644 index 0000000000..60806109fe --- /dev/null +++ b/integration/tests/neon_evm/README.md @@ -0,0 +1,102 @@ +Intro +====== + +In this directory are located tests for the evm which sends transactions direct to the evm. +Tests are using python and py.test library for run tests. + + +Installation +------------ + +```bash +pip install -r requirements.txt +py.test ./ -s -v +``` + +Also we can configure some variables from environment variables: + +1. SOLANA_URL - by default http://solana:8899 +2. NEON_CORE_API_URL - by default http://neon_api:8085/api +3. EVM_LOADER - set evm loader address +4. NEON_TOKEN_MINT - ethereum token mint address + + +How to write tests +================== + +Structure +--------- + +Test has a several helper directories and files: + +1. contracts - place for all Solidity contracts used in tests +2. utils - place for utilities which divided by logic parts +3. conftest.py - common fixtures for all tests + + +Common fixtures +--------------- + +For more information about fixtures see [pytest-fixture](https://docs.pytest.org/en/latest/fixture.html). +In several words, fixtures are functions which are called before and after each test and has a scope parameter which setup how often this function will be called. +For example: + +```python +import pytest + +@pytest.fixture(scope="function") +def fixture1(): + print("Call fixture1") + return "fixture1" + + +def test_one(fixture1): + pass + + +def test_two(fixture1): + pass +``` + +In this case "fixture1" will be called before each test. + +```python +import pytest + +@pytest.fixture(scope="session") +def fixture1(): + print("Call fixture1") + return "fixture1" + + +def test_one(fixture1): + pass + + +def test_two(fixture1): + pass +``` + +In this case "fixture1" will be called only once before all tests. + +We have a several common fixtures: + +1. evm_loader - fixture for evm loader object +2. operator_keypair - solana Keypair for operator key +3. treasury_pool - created treasury pool +4. user_account - created user account with ethereum account + + +Tips +==== + +Generate eth contract function call data +--------------------------------------- + +```python +from eth_utils import abi +func_name = abi.function_signature_to_4byte_selector('unchange_storage(uint8,uint8)') +data = (func_name + bytes.fromhex("%064x" % 0x01) + bytes.fromhex("%064x" % 0x01)) +``` + +uint8 parameters must be 64 bytes long diff --git a/integration/tests/neon_evm/__init__.py b/integration/tests/neon_evm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration/tests/neon_evm/conftest.py b/integration/tests/neon_evm/conftest.py new file mode 100644 index 0000000000..feb69afb17 --- /dev/null +++ b/integration/tests/neon_evm/conftest.py @@ -0,0 +1,165 @@ +import json +import pathlib + +import eth_abi +import pytest + +from solana.keypair import Keypair +from eth_keys import keys as eth_keys +from solana.publickey import PublicKey +from solana.rpc.commitment import Confirmed + +from .solana_utils import EvmLoader, create_treasury_pool_address, make_new_user, \ + deposit_neon, solana_client, wait_for_account_to_exists +from .utils.constants import NEON_CORE_API_URL +from .utils.contract import deploy_contract +from .utils.storage import create_holder +from .types.types import TreasuryPool, Caller, Contract +from .utils.neon_api_client import NeonApiClient + +KEY_PATH = pathlib.Path(__file__).parent / "operator-keypairs" + + +@pytest.fixture(scope="session") +def evm_loader(operator_keypair: Keypair) -> EvmLoader: + loader = EvmLoader(operator_keypair) + return loader + + +def prepare_operator(key_file): + with open(key_file, "r") as key: + secret_key = json.load(key)[:32] + account = Keypair.from_secret_key(secret_key) + + solana_client.request_airdrop(account.public_key, 1000 * 10 ** 9, commitment=Confirmed) + wait_for_account_to_exists(solana_client, account.public_key) + + a = solana_client.get_account_info(account.public_key, commitment=Confirmed) + print(f"{a}") + + operator_ether = eth_keys.PrivateKey(account.secret_key[:32]).public_key.to_canonical_address() + + evm_loader = EvmLoader(account) + ether_balance_pubkey = evm_loader.ether2balance(operator_ether) + acc_info = solana_client.get_account_info(ether_balance_pubkey, commitment=Confirmed) + if acc_info.value is None: + evm_loader.create_balance_account(operator_ether) + + return account + + +@pytest.fixture(scope="session") +def default_operator_keypair() -> Keypair: + """ + Initialized solana keypair with balance. Get private keys from ci/operator-keypairs/id.json + """ + key_file = KEY_PATH / "id.json" + return prepare_operator(key_file) + + +@pytest.fixture(scope="session") +def operator_keypair(worker_id) -> Keypair: + """ + Initialized solana keypair with balance. Get private keys from ci/operator-keypairs + """ + if worker_id in ("master", "gw1"): + key_file = KEY_PATH / "id.json" + else: + file_id = int(worker_id[-1]) + 2 + key_file = KEY_PATH / f"id{file_id}.json" + return prepare_operator(key_file) + + +@pytest.fixture(scope="session") +def second_operator_keypair(worker_id) -> Keypair: + """ + Initialized solana keypair with balance. Get private key from cli or ./ci/operator-keypairs + """ + if worker_id in ("master", "gw1"): + key_file = KEY_PATH / "id20.json" + else: + file_id = 20 + int(worker_id[-1]) + 2 + key_file = KEY_PATH / f"id{file_id}.json" + + return prepare_operator(key_file) + + +@pytest.fixture(scope="session") +def treasury_pool(evm_loader) -> TreasuryPool: + index = 2 + address = create_treasury_pool_address(index) + index_buf = index.to_bytes(4, 'little') + return TreasuryPool(index, address, index_buf) + + +@pytest.fixture(scope="function") +def user_account(evm_loader) -> Caller: + return make_new_user(evm_loader) + + +@pytest.fixture(scope="session") +def session_user(evm_loader) -> Caller: + return make_new_user(evm_loader) + + +@pytest.fixture(scope="session") +def second_session_user(evm_loader) -> Caller: + return make_new_user(evm_loader) + + +@pytest.fixture(scope="session") +def sender_with_tokens(evm_loader, operator_keypair) -> Caller: + user = make_new_user(evm_loader) + deposit_neon(evm_loader, operator_keypair, user.eth_address, 100000) + return user + + +@pytest.fixture(scope="session") +def holder_acc(operator_keypair) -> PublicKey: + return create_holder(operator_keypair) + + +@pytest.fixture(scope="function") +def new_holder_acc(operator_keypair) -> PublicKey: + return create_holder(operator_keypair) + + +@pytest.fixture(scope="function") +def rw_lock_contract(evm_loader: EvmLoader, operator_keypair: Keypair, session_user: Caller, + treasury_pool) -> Contract: + return deploy_contract(operator_keypair, session_user, "rw_lock", evm_loader, treasury_pool) + + +@pytest.fixture(scope="function") +def rw_lock_caller(evm_loader: EvmLoader, operator_keypair: Keypair, + session_user: Caller, treasury_pool: TreasuryPool, rw_lock_contract: Contract) -> Contract: + constructor_args = eth_abi.encode(['address'], [rw_lock_contract.eth_address.hex()]) + return deploy_contract(operator_keypair, session_user, "rw_lock", evm_loader, + treasury_pool, encoded_args=constructor_args, contract_name="rw_lock_caller") + + +@pytest.fixture(scope="function") +def string_setter_contract(evm_loader: EvmLoader, operator_keypair: Keypair, session_user: Caller, + treasury_pool) -> Contract: + return deploy_contract(operator_keypair, session_user, "string_setter", evm_loader, treasury_pool) + + +@pytest.fixture(scope="session") +def calculator_contract(evm_loader: EvmLoader, operator_keypair: Keypair, session_user: Caller, + treasury_pool) -> Contract: + return deploy_contract(operator_keypair, session_user, "calculator", evm_loader, treasury_pool) + + +@pytest.fixture(scope="session") +def calculator_caller_contract(evm_loader: EvmLoader, operator_keypair: Keypair, session_user: Caller, + treasury_pool, calculator_contract) -> Contract: + constructor_args = eth_abi.encode(['address'], [calculator_contract.eth_address.hex()]) + + return deploy_contract(operator_keypair, session_user, "calculator", evm_loader, treasury_pool, + encoded_args=constructor_args, contract_name="calculatorCaller") + + +@pytest.fixture(scope="session") +def neon_api_client(): + client = NeonApiClient(url=NEON_CORE_API_URL) + return client diff --git a/integration/tests/neon_evm/operator-keypairs/id.json b/integration/tests/neon_evm/operator-keypairs/id.json new file mode 100644 index 0000000000..54e7bbcc6b --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id.json @@ -0,0 +1 @@ +[161,247,66,57,203,188,141,236,124,123,200,192,255,23,161,34,116,202,70,182,176,94,195,68,185,32,61,42,203,57,245,190,153,233,189,58,187,209,15,232,83,112,145,116,89,187,201,44,36,255,81,102,84,101,62,1,180,217,194,37,147,74,7,170] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id10.json b/integration/tests/neon_evm/operator-keypairs/id10.json new file mode 100644 index 0000000000..0775610ce3 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id10.json @@ -0,0 +1 @@ +[95,1,219,168,183,165,35,48,232,59,7,206,250,251,243,138,175,212,58,154,252,147,193,16,240,56,129,24,104,220,21,244,70,237,248,21,84,35,91,157,114,197,241,253,93,33,93,230,14,255,229,131,73,153,27,16,86,172,34,220,16,128,14,26] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id11.json b/integration/tests/neon_evm/operator-keypairs/id11.json new file mode 100644 index 0000000000..db71430291 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id11.json @@ -0,0 +1 @@ +[112,233,194,170,175,217,98,186,160,118,143,249,88,40,179,77,222,62,121,149,205,155,152,1,34,243,188,44,66,231,63,202,143,44,91,212,211,86,255,54,80,56,164,192,211,49,40,167,211,153,130,207,41,220,90,64,169,45,26,169,42,202,231,252] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id12.json b/integration/tests/neon_evm/operator-keypairs/id12.json new file mode 100644 index 0000000000..3cfb114dc4 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id12.json @@ -0,0 +1 @@ +[107,242,59,236,113,145,60,13,84,146,102,174,36,95,255,207,104,129,118,70,114,139,210,54,17,195,176,206,202,153,82,111,25,231,254,200,204,102,17,163,119,42,14,188,149,225,220,221,195,14,163,251,62,228,171,180,124,174,161,152,231,162,221,54] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id13.json b/integration/tests/neon_evm/operator-keypairs/id13.json new file mode 100644 index 0000000000..e5e633ee39 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id13.json @@ -0,0 +1 @@ +[132,30,178,226,98,41,184,97,30,124,214,218,107,138,88,82,206,12,27,217,25,6,146,118,146,174,17,129,255,96,33,56,204,58,207,78,57,180,19,42,180,42,245,237,110,83,156,150,192,15,157,91,246,49,103,146,168,79,122,156,136,146,247,152] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id14.json b/integration/tests/neon_evm/operator-keypairs/id14.json new file mode 100644 index 0000000000..3258026f32 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id14.json @@ -0,0 +1 @@ +[77,155,208,78,141,49,104,72,43,252,108,43,240,125,166,156,235,146,195,221,21,235,48,39,224,133,233,174,110,10,211,154,123,240,134,8,166,118,208,116,229,35,39,53,203,206,97,95,22,107,236,62,70,200,191,63,221,34,154,91,159,153,180,171] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id15.json b/integration/tests/neon_evm/operator-keypairs/id15.json new file mode 100644 index 0000000000..317b10c65e --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id15.json @@ -0,0 +1 @@ +[176,43,81,75,52,27,223,223,223,150,148,199,16,151,195,121,143,174,242,189,90,208,164,198,92,28,77,104,72,230,11,154,212,64,129,103,42,57,174,119,63,101,167,173,184,97,146,208,207,208,81,239,29,214,66,101,101,151,247,130,197,17,172,170] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id16.json b/integration/tests/neon_evm/operator-keypairs/id16.json new file mode 100644 index 0000000000..a2ea6fa616 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id16.json @@ -0,0 +1 @@ +[56,132,45,132,252,89,229,88,99,111,37,90,53,175,128,227,57,228,18,166,56,5,168,23,33,127,37,69,154,150,67,213,227,9,242,48,173,228,249,65,55,107,84,219,87,138,241,192,218,29,149,67,43,239,9,132,93,209,218,167,27,138,112,46] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id17.json b/integration/tests/neon_evm/operator-keypairs/id17.json new file mode 100644 index 0000000000..5cbbbd54df --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id17.json @@ -0,0 +1 @@ +[169,132,11,32,251,137,85,119,180,62,4,36,213,240,247,29,61,194,252,19,134,98,188,82,22,213,206,195,39,133,36,162,202,14,212,69,151,114,40,66,173,213,100,145,235,70,34,247,101,166,227,86,18,81,56,17,58,103,152,200,117,163,88,236] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id18.json b/integration/tests/neon_evm/operator-keypairs/id18.json new file mode 100644 index 0000000000..5a2ad7abc1 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id18.json @@ -0,0 +1 @@ +[34,136,232,16,199,6,212,178,9,245,202,238,151,70,70,205,173,92,116,43,176,222,250,193,7,242,79,227,3,252,57,47,171,52,75,200,195,215,71,197,31,185,128,5,62,168,237,61,5,25,229,26,19,201,190,9,149,80,104,95,157,123,207,40] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id19.json b/integration/tests/neon_evm/operator-keypairs/id19.json new file mode 100644 index 0000000000..716b611d62 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id19.json @@ -0,0 +1 @@ +[107,48,148,246,46,55,52,252,132,177,71,135,119,118,187,16,236,80,237,198,239,54,112,107,225,26,36,200,242,63,40,71,143,117,14,174,182,27,199,155,93,167,196,216,69,150,100,195,216,48,222,143,240,185,186,37,202,137,179,232,239,26,85,242] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id2.json b/integration/tests/neon_evm/operator-keypairs/id2.json new file mode 100644 index 0000000000..9b1bd57b61 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id2.json @@ -0,0 +1 @@ +[27,180,96,29,186,81,246,40,92,95,84,188,13,66,153,230,110,37,231,213,185,160,128,146,175,193,199,187,131,108,146,211,129,250,211,63,136,192,175,63,138,71,161,34,135,75,55,3,149,61,84,23,252,239,203,80,170,175,222,166,136,217,173,126] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id20.json b/integration/tests/neon_evm/operator-keypairs/id20.json new file mode 100644 index 0000000000..03a27c45eb --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id20.json @@ -0,0 +1 @@ +[203,226,8,46,184,170,110,207,167,123,46,136,82,48,44,123,212,127,198,109,113,6,103,190,9,144,89,120,215,45,75,17,134,73,9,238,232,115,234,164,179,96,197,67,31,175,86,222,149,202,209,164,83,170,133,179,189,178,99,158,15,3,152,135] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id21.json b/integration/tests/neon_evm/operator-keypairs/id21.json new file mode 100644 index 0000000000..caa4fb7332 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id21.json @@ -0,0 +1 @@ +[218,78,39,37,233,34,133,207,6,61,140,213,98,27,8,7,255,50,100,179,83,154,114,120,52,199,178,29,32,131,193,9,46,69,220,188,192,48,86,70,174,193,58,32,171,172,54,169,134,56,245,54,239,253,235,32,155,211,137,90,163,61,54,57] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id22.json b/integration/tests/neon_evm/operator-keypairs/id22.json new file mode 100644 index 0000000000..7966c4ba46 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id22.json @@ -0,0 +1 @@ +[213,38,17,156,63,219,208,33,176,13,185,208,204,212,179,20,63,74,116,6,104,176,68,242,100,10,76,138,124,95,99,168,91,253,251,27,103,97,112,214,38,187,136,19,100,149,58,106,184,17,85,214,7,150,172,139,155,133,93,93,168,107,139,174] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id23.json b/integration/tests/neon_evm/operator-keypairs/id23.json new file mode 100644 index 0000000000..e2c659a57c --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id23.json @@ -0,0 +1 @@ +[161,138,142,31,77,141,153,110,193,211,60,52,240,89,17,162,42,125,158,78,243,214,203,144,85,66,34,231,222,87,106,54,184,10,234,92,109,70,63,141,23,221,150,4,181,90,234,216,132,7,213,115,220,202,207,31,31,191,66,27,76,106,139,28] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id24.json b/integration/tests/neon_evm/operator-keypairs/id24.json new file mode 100644 index 0000000000..ab53a2b489 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id24.json @@ -0,0 +1 @@ +[111,225,67,67,201,129,87,214,46,150,197,209,142,102,243,6,197,103,130,19,12,11,205,90,228,216,232,212,42,87,217,246,20,32,169,144,96,175,129,11,93,237,7,215,62,60,254,34,29,15,133,117,199,162,8,140,144,35,233,179,139,198,54,30] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id25.json b/integration/tests/neon_evm/operator-keypairs/id25.json new file mode 100644 index 0000000000..c3cd09b2a8 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id25.json @@ -0,0 +1 @@ +[124,147,121,70,22,245,195,2,113,226,67,159,168,33,247,202,149,42,5,18,192,26,192,41,21,255,15,1,115,38,184,182,28,114,114,195,60,79,241,252,192,204,205,176,152,170,117,102,255,74,145,69,85,136,64,214,177,134,255,221,184,119,141,125] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id26.json b/integration/tests/neon_evm/operator-keypairs/id26.json new file mode 100644 index 0000000000..11980bcaad --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id26.json @@ -0,0 +1 @@ +[194,43,141,149,241,214,135,212,243,123,229,238,68,168,5,2,148,253,171,107,40,251,133,240,237,248,82,44,45,252,177,157,9,157,56,52,149,119,152,127,172,5,132,184,12,65,237,196,211,249,93,75,246,23,49,187,1,196,138,243,254,148,127,200] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id27.json b/integration/tests/neon_evm/operator-keypairs/id27.json new file mode 100644 index 0000000000..00c30dea20 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id27.json @@ -0,0 +1 @@ +[208,83,61,173,206,206,193,114,39,85,114,190,81,55,97,76,94,156,47,248,193,163,230,202,176,121,89,43,16,210,146,36,208,251,160,86,224,180,79,140,119,14,118,76,60,79,81,96,160,123,179,88,201,27,173,141,126,239,109,213,242,106,36,246] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id28.json b/integration/tests/neon_evm/operator-keypairs/id28.json new file mode 100644 index 0000000000..4b858ba337 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id28.json @@ -0,0 +1 @@ +[209,78,176,59,205,21,225,147,100,16,206,202,223,36,96,43,85,159,238,98,244,52,0,49,98,111,122,225,225,165,9,112,104,103,35,237,229,14,253,80,210,177,222,83,166,131,106,170,212,72,208,227,125,135,92,255,38,250,183,162,139,32,85,50] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id29.json b/integration/tests/neon_evm/operator-keypairs/id29.json new file mode 100644 index 0000000000..454b40b636 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id29.json @@ -0,0 +1 @@ +[139,25,69,250,8,210,132,135,49,210,33,168,172,48,73,239,226,151,2,49,128,127,136,222,160,209,112,172,83,153,57,59,231,21,48,207,152,236,233,187,223,100,82,93,7,113,26,194,124,70,245,140,6,215,63,170,178,46,130,201,93,40,215,178] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id3.json b/integration/tests/neon_evm/operator-keypairs/id3.json new file mode 100644 index 0000000000..7bc96cdf71 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id3.json @@ -0,0 +1 @@ +[109,212,150,39,229,176,85,103,100,217,115,40,197,125,185,154,137,194,7,69,141,239,74,113,237,187,26,120,169,5,255,32,35,79,159,50,228,49,216,5,51,156,105,212,248,146,245,190,201,70,233,200,237,253,215,78,68,113,248,0,238,15,136,200] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id30.json b/integration/tests/neon_evm/operator-keypairs/id30.json new file mode 100644 index 0000000000..b6b4a9b641 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id30.json @@ -0,0 +1 @@ +[164,184,180,94,178,30,255,212,198,57,93,119,61,202,55,97,47,134,31,115,120,201,199,32,163,108,202,131,121,135,128,6,68,231,110,177,230,42,56,26,247,172,232,205,110,76,106,16,153,106,234,56,11,247,17,237,241,11,56,21,145,232,254,153] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id4.json b/integration/tests/neon_evm/operator-keypairs/id4.json new file mode 100644 index 0000000000..f0ca8465aa --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id4.json @@ -0,0 +1 @@ +[214,210,14,130,12,222,73,66,181,175,176,156,48,43,71,4,216,168,159,87,124,129,219,255,7,252,25,161,46,177,45,113,108,56,97,99,254,82,6,60,29,65,233,96,140,158,29,212,193,138,51,31,126,138,226,116,105,127,95,121,156,23,55,54] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id5.json b/integration/tests/neon_evm/operator-keypairs/id5.json new file mode 100644 index 0000000000..1a5e67907c --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id5.json @@ -0,0 +1 @@ +[135,36,157,212,37,148,54,141,6,153,179,125,174,108,39,176,152,84,179,2,79,124,113,176,10,83,101,69,165,219,43,89,7,50,172,103,235,215,62,192,98,108,223,67,30,114,241,40,45,61,189,107,162,9,86,31,11,124,184,245,39,12,101,13] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id6.json b/integration/tests/neon_evm/operator-keypairs/id6.json new file mode 100644 index 0000000000..e367a53739 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id6.json @@ -0,0 +1 @@ +[245,253,29,158,248,242,248,20,31,201,243,72,241,50,136,180,229,63,143,99,92,29,230,5,181,107,230,147,253,103,22,247,1,192,248,239,196,211,107,121,216,2,30,108,163,95,239,105,245,93,161,171,252,153,27,40,132,14,136,73,52,195,251,158] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id7.json b/integration/tests/neon_evm/operator-keypairs/id7.json new file mode 100644 index 0000000000..088191ba97 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id7.json @@ -0,0 +1 @@ +[103,137,235,222,211,158,252,227,63,199,231,174,155,23,246,93,31,194,94,222,156,0,159,205,93,110,243,4,179,21,145,13,236,212,22,139,247,96,23,122,167,37,106,151,101,126,240,202,104,15,176,243,57,143,103,132,64,211,83,237,3,68,189,154] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id8.json b/integration/tests/neon_evm/operator-keypairs/id8.json new file mode 100644 index 0000000000..515cd77c55 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id8.json @@ -0,0 +1 @@ +[43,222,228,254,6,34,211,192,249,60,124,254,19,117,183,192,163,79,160,118,73,194,91,40,166,32,139,101,161,204,226,199,202,125,95,169,241,101,72,248,194,7,102,135,184,165,73,207,166,48,108,180,62,205,193,52,91,86,120,152,102,112,62,105] \ No newline at end of file diff --git a/integration/tests/neon_evm/operator-keypairs/id9.json b/integration/tests/neon_evm/operator-keypairs/id9.json new file mode 100644 index 0000000000..02f1125299 --- /dev/null +++ b/integration/tests/neon_evm/operator-keypairs/id9.json @@ -0,0 +1 @@ +[76,252,69,52,51,62,9,220,165,183,52,47,149,238,49,198,68,226,22,251,245,70,58,59,205,49,212,77,31,191,81,94,131,180,89,79,237,117,30,106,190,97,233,53,235,163,61,130,254,35,88,148,254,230,240,235,118,222,84,106,92,2,89,155] \ No newline at end of file diff --git a/integration/tests/neon_evm/solana_utils.py b/integration/tests/neon_evm/solana_utils.py new file mode 100644 index 0000000000..0530f7a454 --- /dev/null +++ b/integration/tests/neon_evm/solana_utils.py @@ -0,0 +1,600 @@ +import json +import math +import os +import subprocess +import time +import typing +from hashlib import sha256 +from typing import Tuple, Union + +import spl.token.instructions +from base58 import b58encode +from eth_account.datastructures import SignedTransaction +from eth_keys import keys as eth_keys +from hexbytes import HexBytes +import solana.system_program as sp + +from solana.keypair import Keypair +from solana.publickey import PublicKey +from solana.rpc.api import Client +from solana.rpc.commitment import Confirmed +from solana.rpc.types import TxOpts +from solana.transaction import AccountMeta, TransactionInstruction, Transaction +from solders.rpc.responses import SendTransactionResp, GetTransactionResp +from spl.token.constants import TOKEN_PROGRAM_ID +from spl.token.instructions import get_associated_token_address, ApproveParams, MintToParams + +from .utils.constants import CHAIN_ID + +from .utils.constants import EVM_LOADER, SOLANA_URL, SYSTEM_ADDRESS, NEON_TOKEN_MINT_ID, \ + ACCOUNT_SEED_VERSION, TREASURY_POOL_SEED +from .utils.instructions import make_DepositV03, make_Cancel, make_WriteHolder, make_ExecuteTrxFromInstruction, \ + TransactionWithComputeBudget, make_PartialCallOrContinueFromRawEthereumTX, \ + make_ExecuteTrxFromAccountDataIterativeOrContinue, make_CreateAssociatedTokenIdempotent +from .utils.layouts import BALANCE_ACCOUNT_LAYOUT +from .types.types import Caller, Contract + +EVM_LOADER_SO = os.environ.get("EVM_LOADER_SO", 'target/bpfel-unknown-unknown/release/evm_loader.so') +solana_client = Client(SOLANA_URL, commitment=Confirmed) +path_to_solana = 'solana' + +# amount of gas per 1 byte evm_storage +EVM_BYTE_COST = 6960 # 1_000_000_000/ 100 * 365 / (1024*1024) * 2 +# number of evm steps per transaction +EVM_STEPS = 500 +# the message size that is used to holder-account filling +HOLDER_MSG_SIZE = 950 +# Ethereum account allocated data size +ACCOUNT_MAX_SIZE = 256 +# spl-token account allocated data size +SPL_TOKEN_ACCOUNT_SIZE = 165 +# payment to treasure +PAYMENT_TO_TREASURE = 5000 +# payment for solana signature verification +LAMPORTS_PER_SIGNATURE = 5000 +# account storage overhead for calculation of base rent +ACCOUNT_STORAGE_OVERHEAD = 128 + + + +def create_treasury_pool_address(pool_index, evm_loader=EVM_LOADER): + return PublicKey.find_program_address( + [bytes(TREASURY_POOL_SEED, 'utf8'), pool_index.to_bytes(4, 'little')], + PublicKey(evm_loader) + )[0] + + +def wait_for_account_to_exists(http_client: Client, account: PublicKey, timeout = 30, sleep_time = 0.4): + elapsed_time = 0 + while elapsed_time < timeout: + resp = http_client.get_account_info(account, commitment=Confirmed) + if resp.value is not None: + return + + time.sleep(sleep_time) + elapsed_time += sleep_time + + raise RuntimeError(f"Account {account} not exists after {timeout} seconds") + + +def account_with_seed(base, seed, program) -> PublicKey: + return PublicKey(sha256(bytes(base) + bytes(seed, 'utf8') + bytes(program)).digest()) + + +def create_account_with_seed(funding, base, seed, lamports, space, program=PublicKey(EVM_LOADER)): + created = account_with_seed(base, seed, program) + print(f"Created: {created}") + return sp.create_account_with_seed(sp.CreateAccountWithSeedParams( + from_pubkey=funding, + new_account_pubkey=created, + base_pubkey=base, + seed=seed, + lamports=lamports, + space=space, + program_id=program + )) + + +def create_holder_account(account, operator, seed): + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=account, is_signer=False, is_writable=True), + AccountMeta(pubkey=operator, is_signer=True, is_writable=False), + ], + program_id=PublicKey(EVM_LOADER), + data=bytes.fromhex("24") + + len(seed).to_bytes(8, 'little') + seed + ) + + +class solana_cli: + def __init__(self, acc=None): + self.acc = acc + + def call(self, arguments): + if self.acc is None: + cmd = '{} --url {} {}'.format(path_to_solana, SOLANA_URL, arguments) + else: + cmd = '{} --keypair {} --url {} {}'.format(path_to_solana, self.acc.get_path(), SOLANA_URL, arguments) + try: + return subprocess.check_output(cmd, shell=True, universal_newlines=True) + except subprocess.CalledProcessError as err: + print(f"ERR: solana error {err}") + raise + + +class neon_cli: + def __init__(self, verbose_flags=''): + self.verbose_flags = verbose_flags + + def call(self, arguments): + cmd = 'neon-cli {} --loglevel debug --commitment=processed --url {} {}'.format(self.verbose_flags, SOLANA_URL, arguments) + proc_result = subprocess.run(cmd, shell=True, text=True, stdout=subprocess.PIPE, universal_newlines=True) + result = json.loads(proc_result.stdout) + if result["result"] == "error": + error = result["error"] + raise Exception(f"ERR: neon-cli error {error}") + + proc_result.check_returncode() + return result["value"] + + # def emulate(self, loader_id, sender, contract, data): + # cmd = ["neon-cli", + # "--commitment=confirmed", + # "--url", SOLANA_URL, + # f"--evm_loader={loader_id}", + # "emulate" + # ] + # print('cmd:', cmd) + # print("data:", data) + # + # body = json.dumps({ + # "tx": { + # "from": sender, + # "to": contract, + # "data": data + # }, + # "accounts": [] + # }) + # + # proc_result = subprocess.run(cmd, input=body, text=True, stdout=subprocess.PIPE, universal_newlines=True) + # + # result = json.loads(proc_result.stdout) + # print("EMULATOR RESULT: ") + # print(json.dumps(result)) + # + # if result["result"] == "error": + # error = result["error"] + # raise Exception(f"ERR: neon-cli error {error}") + # + # proc_result.check_returncode() + # return result["value"] + + # def call_contract_get_function(self, evm_loader, sender, contract, function_signature: str, constructor_args=None): + # data = abi.function_signature_to_4byte_selector(function_signature) + # if constructor_args is not None: + # data += constructor_args + # result = self.emulate(evm_loader.loader_id, sender.eth_address.hex(), contract.eth_address.hex(), data.hex()) + # return result["result"] + + def get_steps_count(self, evm_loader, from_acc, to, data): + if isinstance(to, (Caller, Contract)): + to = to.eth_address.hex() + + result = neon_cli().emulate( + evm_loader.loader_id, + from_acc.eth_address.hex(), + to, + data + ) + + return result["steps_executed"] + + +class RandomAccount: + def __init__(self, path=None): + if path is None: + self.make_random_path() + print(f"New keypair file: {self.path}") + self.generate_key() + else: + self.path = path + self.retrieve_keys() + print('New Public key:', self.acc.public_key()) + print('Private:', self.acc.secret_key()) + + def make_random_path(self): + self.path = os.urandom(5).hex() + ".json" + + def generate_key(self): + cmd_generate = 'solana-keygen new --no-passphrase --outfile {}'.format(self.path) + try: + return subprocess.check_output(cmd_generate, shell=True, universal_newlines=True) + except subprocess.CalledProcessError as err: + print(f"ERR: solana error {err}") + raise + + def retrieve_keys(self): + with open(self.path) as f: + d = json.load(f) + self.acc = Keypair(d[0:32]) + + def get_path(self): + return self.path + + def get_acc(self): + return self.acc + + +class WalletAccount(RandomAccount): + def __init__(self, path): + self.path = path + self.retrieve_keys() + print('Wallet public key:', self.acc.public_key()) + + +class EvmLoader: + def __init__(self, acc: Keypair, program_id=EVM_LOADER): + if program_id is None: + print(f"EVM Loader program address is empty, deploy it") + result = json.loads(solana_cli(acc).call('deploy {}'.format(EVM_LOADER_SO))) + program_id = result['programId'] + EvmLoader.loader_id = PublicKey(program_id) + print("Done\n") + + self.loader_id = EvmLoader.loader_id + self.acc = acc + print("Evm loader program: {}".format(self.loader_id)) + + def create_balance_account(self, ether: Union[str, bytes]) -> PublicKey: + account_pubkey = self.ether2balance(ether) + contract_pubkey = PublicKey(self.ether2program(ether)[0]) + print('createBalanceAccount: {} => {}'.format(ether, account_pubkey)) + + data = bytes([0x30]) + self.ether2bytes(ether) + CHAIN_ID.to_bytes(8, 'little') + trx = Transaction() + trx.add(TransactionInstruction( + program_id=self.loader_id, + data=data, + keys=[ + AccountMeta(pubkey=self.acc.public_key, is_signer=True, is_writable=True), + AccountMeta(pubkey=PublicKey(SYSTEM_ADDRESS), 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), + ])) + + send_transaction(solana_client, trx, self.acc) + return account_pubkey + + @staticmethod + def ether2hex(ether: Union[str, bytes]): + if isinstance(ether, str): + if ether.startswith('0x'): + return ether[2:] + return ether + return ether.hex() + + @staticmethod + def ether2bytes(ether: Union[str, bytes]): + if isinstance(ether, str): + if ether.startswith('0x'): + return bytes.fromhex(ether[2:]) + return bytes.fromhex(ether) + return ether + + def ether2seed(self, ether: Union[str, bytes]): + seed = b58encode(ACCOUNT_SEED_VERSION + self.ether2bytes(ether)).decode('utf8') + acc = account_with_seed(self.acc.public_key, seed, self.loader_id) + print('ether2program: {} {} => {}'.format(self.ether2hex(ether), 255, acc)) + return acc, 255 + + def ether2program(self, ether: Union[str, bytes]) -> Tuple[str, int]: + items = PublicKey.find_program_address([ACCOUNT_SEED_VERSION, self.ether2bytes(ether)], PublicKey(EVM_LOADER)) + return str(items[0]), items[1] + + def ether2balance(self, address: Union[str, bytes]) -> PublicKey: + address_bytes = self.ether2bytes(address) + chain_id_bytes = CHAIN_ID.to_bytes(32, 'big') + return PublicKey.find_program_address( + [ACCOUNT_SEED_VERSION, address_bytes, chain_id_bytes], + PublicKey(EVM_LOADER) + )[0] + + def check_account(self, solana): + info = solana_client.get_account_info(solana) + print("checkAccount({}): {}".format(solana, info)) + + def get_neon_nonce(self, account: Union[str, bytes, Caller]) -> int: + if isinstance(account, Caller): + return self.get_neon_nonce(account.eth_address) + + solana_address = self.ether2balance(account) + + info: bytes = get_solana_account_data(solana_client, solana_address, BALANCE_ACCOUNT_LAYOUT.sizeof()) + layout = BALANCE_ACCOUNT_LAYOUT.parse(info) + + return layout.trx_count + + def get_neon_balance(self, account: Union[str, bytes, Caller]) -> int: + if isinstance(account, Caller): + return self.get_neon_balance(account.eth_address) + + solana_address = self.ether2balance(account) + + info: bytes = get_solana_account_data(solana_client, solana_address, BALANCE_ACCOUNT_LAYOUT.sizeof()) + layout = BALANCE_ACCOUNT_LAYOUT.parse(info) + + return int.from_bytes(layout.balance, byteorder="little") + + +def get_solana_balance(account): + return solana_client.get_balance(account, commitment=Confirmed).value + + +def get_solana_account_data(solana_client: Client, account: Union[str, PublicKey, Keypair], expected_length: int) -> bytes: + if isinstance(account, Keypair): + account = account.public_key + print(f"Get account data for {account}") + info = solana_client.get_account_info(account, commitment=Confirmed) + print(f"Result: {info}") + info = info.value + if info is None: + raise Exception("Can't get information about {}".format(account)) + if len(info.data) < expected_length: + print("len(data)({}) < expected_length({})".format(len(info.data), expected_length)) + raise Exception("Wrong data length for account data {}".format(account)) + return info.data + +def send_transaction(client: Client, trx: Transaction, *signers: Keypair, wait_status=Confirmed): + print("Send trx") + result = client.send_transaction(trx, *signers, opts=TxOpts(skip_confirmation=True, preflight_commitment=wait_status)) + print("Result: {}".format(result)) + client.confirm_transaction(result.value, commitment=Confirmed) + return client.get_transaction(result.value, commitment=Confirmed) + + +def evm_step_cost(): + operator_expences = PAYMENT_TO_TREASURE + LAMPORTS_PER_SIGNATURE + return math.floor(operator_expences / EVM_STEPS) + + +def make_new_user(evm_loader: EvmLoader) -> Caller: + key = Keypair.generate() + if get_solana_balance(key.public_key) == 0: + solana_client.request_airdrop(key.public_key, 1000 * 10 ** 9, commitment=Confirmed) + wait_for_account_to_exists(solana_client, key.public_key) + + caller_ether = eth_keys.PrivateKey(key.secret_key[:32]).public_key.to_canonical_address() + caller_solana = evm_loader.ether2program(caller_ether)[0] + caller_balance = evm_loader.ether2balance(caller_ether) + caller_token = get_associated_token_address(caller_balance, NEON_TOKEN_MINT_ID) + + if get_solana_balance(caller_balance) == 0: + print(f"Create Neon account {caller_ether} for user {caller_balance}") + evm_loader.create_balance_account(caller_ether) + + print('Account solana address:', key.public_key) + 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) + + +def deposit_neon(evm_loader: EvmLoader, operator_keypair: Keypair, ether_address: Union[str, bytes], amount: int): + balance_pubkey = evm_loader.ether2balance(ether_address) + contract_pubkey = PublicKey(evm_loader.ether2program(ether_address)[0]) + + evm_token_authority = PublicKey.find_program_address([b"Deposit"], evm_loader.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) + + with open("evm_loader-keypair.json", "r") as key: + secret_key = json.load(key)[:32] + mint_authority = Keypair.from_secret_key(secret_key) + + trx = Transaction() + trx.add( + make_CreateAssociatedTokenIdempotent( + operator_keypair.public_key, + operator_keypair.public_key, + NEON_TOKEN_MINT_ID + ), + spl.token.instructions.mint_to( + MintToParams( + spl.token.constants.TOKEN_PROGRAM_ID, + NEON_TOKEN_MINT_ID, + token_pubkey, + mint_authority.public_key, + amount + ) + ), + spl.token.instructions.approve( + ApproveParams( + spl.token.constants.TOKEN_PROGRAM_ID, + token_pubkey, + balance_pubkey, + operator_keypair.public_key, + amount, + ) + ), + make_DepositV03( + evm_loader.ether2bytes(ether_address), + CHAIN_ID, + balance_pubkey, + contract_pubkey, + NEON_TOKEN_MINT_ID, + token_pubkey, + evm_pool_key, + spl.token.constants.TOKEN_PROGRAM_ID, + operator_keypair.public_key, + ) + ) + + receipt = send_transaction(solana_client, trx, operator_keypair, mint_authority) + + return receipt + + +def cancel_transaction( + evm_loader: EvmLoader, + tx_hash: HexBytes, + holder_acc: PublicKey, + operator_keypair: Keypair, + additional_accounts: typing.List[PublicKey], +): + # Cancel deployment transaction: + trx = Transaction() + trx.add( + make_Cancel( + evm_loader, + holder_acc, + operator_keypair, + tx_hash, + additional_accounts, + ) + ) + + cancel_receipt = send_transaction(solana_client, trx, operator_keypair) + + print("Cancel receipt:", cancel_receipt) + assert cancel_receipt.value.transaction.meta.err is None + return cancel_receipt + + +def write_transaction_to_holder_account( + signed_tx: SignedTransaction, + holder_account: PublicKey, + operator: Keypair, +): + # Write transaction to transaction holder account + offset = 0 + receipts = [] + rest = signed_tx.rawTransaction + while len(rest): + (part, rest) = (rest[:920], rest[920:]) + trx = Transaction() + trx.add(make_WriteHolder(operator.public_key, holder_account, signed_tx.hash, offset, part)) + receipts.append( + solana_client.send_transaction( + trx, + operator, + opts=TxOpts(skip_confirmation=True, preflight_commitment=Confirmed), + ) + ) + offset += len(part) + + for rcpt in receipts: + solana_client.confirm_transaction(rcpt.value, commitment=Confirmed) + + +def execute_trx_from_instruction(operator: Keypair, evm_loader: EvmLoader, treasury_address: PublicKey, treasury_buffer: bytes, + instruction: SignedTransaction, + additional_accounts, signer: Keypair, + system_program=sp.SYS_PROGRAM_ID) -> SendTransactionResp: + trx = TransactionWithComputeBudget(operator) + trx.add(make_ExecuteTrxFromInstruction(operator, evm_loader, treasury_address, + treasury_buffer, instruction.rawTransaction, additional_accounts, + system_program)) + + return solana_client.send_transaction(trx, signer, opts=TxOpts(skip_preflight=False, skip_confirmation=False, preflight_commitment=Confirmed)) + + +def send_transaction_step_from_instruction(operator: Keypair, evm_loader: EvmLoader, treasury, storage_account, + instruction: SignedTransaction, + additional_accounts, steps_count, signer: Keypair, + system_program=sp.SYS_PROGRAM_ID, index=0) -> SendTransactionResp: + trx = TransactionWithComputeBudget(operator) + trx.add( + make_PartialCallOrContinueFromRawEthereumTX( + index, steps_count, instruction.rawTransaction, + operator, evm_loader, storage_account, treasury, + additional_accounts, system_program + ) + ) + + return solana_client.send_transaction(trx, signer, opts=TxOpts(skip_preflight=False, skip_confirmation=False, preflight_commitment=Confirmed)) + + +def execute_transaction_steps_from_instruction(operator: Keypair, evm_loader: EvmLoader, treasury, storage_account, + instruction: SignedTransaction, + additional_accounts, steps_count=EVM_STEPS, + signer: Keypair = None) -> SendTransactionResp: + signer = operator if signer is None else signer + + index = 0 + done = False + while not done: + response = send_transaction_step_from_instruction(operator, evm_loader, treasury, storage_account, instruction, additional_accounts, EVM_STEPS, signer, index=index) + index += 1 + + receipt = solana_client.get_transaction(response.value, commitment=Confirmed) + if receipt.value.transaction.meta.err: + raise AssertionError(f"Can't deploy contract: {receipt.value.transaction.meta.err}") + for log in receipt.value.transaction.meta.log_messages: + if "exit_status" in log: + done = True + break + if "ExitError" in log: + raise AssertionError(f"EVM Return error in logs: {receipt}") + + return response + + +def send_transaction_step_from_account(operator: Keypair, evm_loader: EvmLoader, treasury, storage_account, + additional_accounts, steps_count, signer: Keypair, + system_program=sp.SYS_PROGRAM_ID, + tag=0x35, index=0) -> GetTransactionResp: + trx = TransactionWithComputeBudget(operator) + trx.add( + make_ExecuteTrxFromAccountDataIterativeOrContinue( + index, steps_count, + operator, evm_loader, storage_account, treasury, + additional_accounts, system_program, tag + ) + ) + return send_transaction(solana_client, trx, signer) + + +def execute_transaction_steps_from_account(operator: Keypair, evm_loader: EvmLoader, treasury, storage_account, + additional_accounts, steps_count=EVM_STEPS, signer: Keypair = None) -> GetTransactionResp: + signer = operator if signer is None else signer + + index = 0 + done = False + while not done: + receipt = send_transaction_step_from_account(operator, evm_loader, treasury, storage_account, additional_accounts, EVM_STEPS, signer, index=index) + index += 1 + + if receipt.value.transaction.meta.err: + raise AssertionError(f"Can't deploy contract: {receipt.value.transaction.meta.err}") + for log in receipt.value.transaction.meta.log_messages: + if "exit_status" in log: + done = True + break + if "ExitError" in log: + raise AssertionError(f"EVM Return error in logs: {receipt}") + + return receipt + + +def execute_transaction_steps_from_account_no_chain_id(operator: Keypair, evm_loader: EvmLoader, treasury, storage_account, + additional_accounts, steps_count=EVM_STEPS, + signer: Keypair = None) -> GetTransactionResp: + signer = operator if signer is None else signer + + index = 0 + done = False + while not done: + receipt = send_transaction_step_from_account(operator, evm_loader, treasury, storage_account, additional_accounts, EVM_STEPS, signer, tag=0x36, index=index) + index += 1 + + if receipt.value.transaction.meta.err: + raise AssertionError(f"Can't deploy contract: {receipt.value.transaction.meta.err}") + for log in receipt.value.transaction.meta.log_messages: + if "exit_status" in log: + done = True + break + if "ExitError" in log: + raise AssertionError(f"EVM Return error in logs: {receipt}") + + return receipt diff --git a/integration/tests/neon_evm/test_cancel_trx.py b/integration/tests/neon_evm/test_cancel_trx.py new file mode 100644 index 0000000000..cbb683a9a4 --- /dev/null +++ b/integration/tests/neon_evm/test_cancel_trx.py @@ -0,0 +1,42 @@ +from solana.transaction import Transaction + +from .solana_utils import send_transaction, solana_client, \ + send_transaction_step_from_instruction +from .utils.constants import TAG_FINALIZED_STATE, TAG_STATE +from .utils.contract import make_contract_call_trx +from .utils.storage import create_holder +from .utils.instructions import make_Cancel +from .utils.layouts import FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, STORAGE_ACCOUNT_INFO_LAYOUT +from .utils.transaction_checks import check_holder_account_tag + + +# We need test here two types of transaction +class TestCancelTrx: + + def test_cancel_trx(self, operator_keypair, rw_lock_contract, user_account, treasury_pool, evm_loader): + """EVM can cancel transaction and finalize storage account""" + signed_tx = make_contract_call_trx(user_account, rw_lock_contract, "unchange_storage(uint8,uint8)", [1, 1]) + + storage_account = create_holder(operator_keypair) + trx = send_transaction_step_from_instruction(operator_keypair, evm_loader, treasury_pool, storage_account, + signed_tx, + [rw_lock_contract.solana_address, + rw_lock_contract.balance_account_address, + user_account.balance_account_address], + 1, operator_keypair) + + receipt = solana_client.get_transaction(trx.value) + assert receipt.value.transaction.meta.err is None + check_holder_account_tag(storage_account, STORAGE_ACCOUNT_INFO_LAYOUT, TAG_STATE) + + user_nonce = evm_loader.get_neon_nonce(user_account.eth_address) + trx = Transaction() + trx.add( + make_Cancel(evm_loader, storage_account, operator_keypair, signed_tx.hash, + [rw_lock_contract.solana_address, + rw_lock_contract.balance_account_address, + user_account.balance_account_address]) + ) + send_transaction(solana_client, trx, operator_keypair) + check_holder_account_tag(storage_account, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + assert user_nonce < evm_loader.get_neon_nonce(user_account.eth_address) diff --git a/integration/tests/neon_evm/test_execute_trx_from_instruction.py b/integration/tests/neon_evm/test_execute_trx_from_instruction.py new file mode 100644 index 0000000000..abf72ddbed --- /dev/null +++ b/integration/tests/neon_evm/test_execute_trx_from_instruction.py @@ -0,0 +1,328 @@ +import random +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 spl.token.instructions import get_associated_token_address + +from .solana_utils import execute_trx_from_instruction +from .utils.assert_messages import InstructionAsserts +from .utils.constants import NEON_TOKEN_MINT_ID +from .utils.contract import make_contract_call_trx +from .utils.ethereum import make_eth_transaction +from .utils.transaction_checks import check_transaction_logs_have_text +from .types.types import Caller, Contract + + +class TestExecuteTrxFromInstruction: + + def test_simple_transfer_transaction(self, operator_keypair, treasury_pool, + sender_with_tokens: Caller, session_user: Caller, + evm_loader): + amount = 10 + sender_balance_before = evm_loader.get_neon_balance(sender_with_tokens) + recipient_balance_before = evm_loader.get_neon_balance(session_user) + + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, amount) + resp = execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address, + session_user.balance_account_address, + session_user.solana_account_address], + operator_keypair) + sender_balance_after = evm_loader.get_neon_balance(sender_with_tokens) + recipient_balance_after = evm_loader.get_neon_balance(session_user) + assert sender_balance_before - amount == sender_balance_after + assert recipient_balance_before + amount == recipient_balance_after + check_transaction_logs_have_text(resp.value, "exit_status=0x11") + + def test_transfer_transaction_with_non_existing_recipient(self, operator_keypair, treasury_pool, + sender_with_tokens: Caller, + 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_solana_address, _ = evm_loader.ether2program(recipient_ether) + recipient_balance_address = evm_loader.ether2balance(recipient_ether) + amount = 10 + signed_tx = make_eth_transaction(recipient_ether, None, sender_with_tokens, amount) + resp = execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address, + recipient_balance_address, + PublicKey(recipient_solana_address)], + operator_keypair) + + recipient_balance_after = evm_loader.get_neon_balance(recipient_ether) + check_transaction_logs_have_text(resp.value, "exit_status=0x11") + + assert recipient_balance_after == amount + + def test_call_contract_function_without_neon_transfer(self, operator_keypair, treasury_pool, + sender_with_tokens: Caller, string_setter_contract: Contract, + evm_loader, neon_api_client): + text = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + signed_tx = make_contract_call_trx(sender_with_tokens, string_setter_contract, "set(string)", [text]) + + resp = execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address, + string_setter_contract.solana_address], + operator_keypair) + + check_transaction_logs_have_text(resp.value, "exit_status=0x11") + assert text in to_text( + neon_api_client.call_contract_get_function(sender_with_tokens, string_setter_contract, + "get()")) + + def test_call_contract_function_with_neon_transfer(self, operator_keypair, treasury_pool, + sender_with_tokens: Caller, + evm_loader, neon_api_client, string_setter_contract): + transfer_amount = random.randint(1, 1000) + + sender_balance_before = evm_loader.get_neon_balance(sender_with_tokens) + contract_balance_before = evm_loader.get_neon_balance(string_setter_contract.eth_address) + + text = ''.join(random.choice(string.ascii_letters) for i in range(10)) + func_name = abi.function_signature_to_4byte_selector('set(string)') + data = func_name + eth_abi.encode(['string'], [text]) + signed_tx = make_eth_transaction(string_setter_contract.eth_address, data, sender_with_tokens, transfer_amount) + resp = execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address, + string_setter_contract.balance_account_address, + string_setter_contract.solana_address], + operator_keypair) + + check_transaction_logs_have_text(resp.value, "exit_status=0x11") + + assert text in to_text(neon_api_client.call_contract_get_function(sender_with_tokens, + string_setter_contract, + "get()")) + + sender_balance_after = evm_loader.get_neon_balance(sender_with_tokens) + contract_balance_after = evm_loader.get_neon_balance(string_setter_contract.eth_address) + assert sender_balance_before - transfer_amount == sender_balance_after + assert contract_balance_before + transfer_amount == contract_balance_after + + def test_incorrect_chain_id(self, operator_keypair, treasury_pool, + sender_with_tokens: Caller, session_user: Caller, + evm_loader): + amount = 1 + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, amount, chain_id=1) + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INVALID_CHAIN_ID): + execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address, + session_user.balance_account_address, + session_user.solana_account_address], + operator_keypair) + + def test_incorrect_nonce(self, operator_keypair, treasury_pool, + sender_with_tokens: Caller, session_user: Caller, + evm_loader): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + + execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address, + session_user.balance_account_address, + session_user.solana_account_address], + operator_keypair) + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INVALID_NONCE): + execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address, + session_user.balance_account_address, + session_user.solana_account_address], + operator_keypair) + + def test_insufficient_funds(self, operator_keypair, treasury_pool, evm_loader, + sender_with_tokens: Caller, session_user: Caller): + user_balance = evm_loader.get_neon_balance(session_user) + + signed_tx = make_eth_transaction(sender_with_tokens.eth_address, None, session_user, user_balance + 1) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INSUFFICIENT_FUNDS): + execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address, + session_user.balance_account_address, + session_user.solana_account_address], + operator_keypair) + + def test_gas_limit_reached(self, operator_keypair, treasury_pool, + session_user: Caller, sender_with_tokens: Caller, + evm_loader): + amount = 10 + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, amount, gas=1) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.OUT_OF_GAS): + execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address, + session_user.balance_account_address, + session_user.solana_account_address], + operator_keypair) + + def test_sender_missed_in_remaining_accounts(self, operator_keypair, treasury_pool, + session_user: Caller, sender_with_tokens: Caller, + evm_loader): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): + execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx, + [session_user.balance_account_address, + session_user.solana_account_address], + operator_keypair) + + def test_recipient_missed_in_remaining_accounts(self, operator_keypair, treasury_pool, + sender_with_tokens: Caller, session_user: Caller, + evm_loader): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): + execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address], + operator_keypair) + + def test_incorrect_treasure_pool(self, operator_keypair, + sender_with_tokens: Caller, session_user: Caller, + evm_loader): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + + treasury_buffer = b'\x02\x00\x00\x00' + treasury_pool = Keypair().public_key + + error = str.format(InstructionAsserts.INVALID_ACCOUNT, treasury_pool) + with pytest.raises(solana.rpc.core.RPCException, match=error): + execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool, treasury_buffer, + signed_tx, + [], + operator_keypair) + + def test_incorrect_treasure_index(self, operator_keypair, treasury_pool, + sender_with_tokens: Caller, session_user: Caller, + evm_loader): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + 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): + execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_buffer, + signed_tx, + [], + operator_keypair) + + def test_incorrect_operator_account(self, evm_loader, treasury_pool, + session_user: Caller, sender_with_tokens: Caller): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + fake_operator = Keypair() + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ACC_NOT_FOUND): + execute_trx_from_instruction(fake_operator, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address, + session_user.balance_account_address, + session_user.solana_account_address], + fake_operator) + + def test_operator_is_not_in_white_list(self, sender_with_tokens, evm_loader, treasury_pool, + session_user): + # now any user can send transactions through "execute transaction from instruction" instruction + + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + resp = execute_trx_from_instruction(sender_with_tokens.solana_account, evm_loader, + treasury_pool.account, + treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address, + session_user.balance_account_address, + session_user.solana_account_address], + sender_with_tokens.solana_account) + check_transaction_logs_have_text(resp.value, "exit_status=0x11") + + def test_incorrect_system_program(self, sender_with_tokens, operator_keypair, evm_loader, treasury_pool, + session_user): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + fake_sys_program_id = Keypair().public_key + with pytest.raises(solana.rpc.core.RPCException, + match=str.format(InstructionAsserts.NOT_SYSTEM_PROGRAM, fake_sys_program_id)): + execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, + treasury_pool.buffer, + signed_tx, + [], + operator_keypair, system_program=fake_sys_program_id) + + def test_operator_does_not_have_enough_founds(self, evm_loader, treasury_pool, + session_user: Caller, sender_with_tokens: Caller): + key = Keypair.generate() + caller_ether = eth_keys.PrivateKey(key.secret_key[: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) + evm_loader.create_balance_account(caller_ether) + + operator_without_money = Caller(key, PublicKey(caller), caller_ether, caller_nonce, caller_token) + + signed_tx = make_eth_transaction(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"): + execute_trx_from_instruction(operator_without_money.solana_account, evm_loader, treasury_pool.account, + treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address, + session_user.balance_account_address, + session_user.solana_account_address], + operator_without_money.solana_account) + + def test_transaction_with_access_list(self, operator_keypair, treasury_pool, sender_with_tokens, + evm_loader, calculator_contract, calculator_caller_contract): + access_list = ( + { + "address": '0x' + calculator_contract.eth_address.hex(), + "storageKeys": ( + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001", + ) + }, + ) + signed_tx = make_contract_call_trx(sender_with_tokens, calculator_caller_contract, "callCalculator()", [], + access_list=access_list) + + resp = execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx, + [sender_with_tokens.balance_account_address, + calculator_caller_contract.solana_address, + calculator_contract.solana_address], + operator_keypair) + + check_transaction_logs_have_text(resp.value, "exit_status=0x12") + + def test_old_trx_type_with_leading_zeros(self, sender_with_tokens, operator_keypair, evm_loader, + calculator_caller_contract, calculator_contract, treasury_pool): + signed_tx = make_contract_call_trx(sender_with_tokens, calculator_caller_contract, "callCalculator()", []) + new_raw_trx = HexBytes(bytes([0]) + signed_tx.rawTransaction) + + signed_tx_new = SignedTransaction( + rawTransaction=new_raw_trx, + hash=signed_tx.hash, + r=signed_tx.r, + s=signed_tx.s, + v=signed_tx.v, + ) + + resp = execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + signed_tx_new, + [sender_with_tokens.balance_account_address, + calculator_caller_contract.solana_address, + calculator_contract.solana_address], + operator_keypair) + check_transaction_logs_have_text(resp.value, "exit_status=0x12") diff --git a/integration/tests/neon_evm/test_holder_account.py b/integration/tests/neon_evm/test_holder_account.py new file mode 100644 index 0000000000..807b567b92 --- /dev/null +++ b/integration/tests/neon_evm/test_holder_account.py @@ -0,0 +1,163 @@ +from hashlib import sha256 +from random import randrange + +import pytest +import solana +from solana.publickey import PublicKey +from solana.rpc.commitment import Confirmed +from solana.rpc.types import TxOpts +from solana.transaction import Transaction + +from . import solana_utils +from .solana_utils import solana_client, write_transaction_to_holder_account, \ + send_transaction_step_from_account, get_solana_balance, execute_transaction_steps_from_account +from .utils.assert_messages import InstructionAsserts +from .utils.constants import EVM_LOADER, TAG_STATE +from .utils.contract import make_deployment_transaction, make_contract_call_trx +from .utils.ethereum import make_eth_transaction +from .utils.instructions import make_WriteHolder +from .utils.layouts import STORAGE_ACCOUNT_INFO_LAYOUT, HOLDER_ACCOUNT_INFO_LAYOUT +from .utils.storage import create_holder, delete_holder + + +def transaction_from_holder(key: PublicKey): + data = solana_client.get_account_info(key, commitment=Confirmed).value.data + header = HOLDER_ACCOUNT_INFO_LAYOUT.parse(data) + + return data[HOLDER_ACCOUNT_INFO_LAYOUT.sizeof():][:header.len] + + +def test_create_holder_account(operator_keypair): + holder_acc = create_holder(operator_keypair) + info = solana_client.get_account_info(holder_acc, commitment=Confirmed) + assert info.value is not None, "Holder account is not created" + assert info.value.lamports == 1000000000, "Account balance is not correct" + + +def test_create_the_same_holder_account_by_another_user(operator_keypair, session_user): + seed = str(randrange(1000000)) + storage = PublicKey( + sha256(bytes(operator_keypair.public_key) + bytes(seed, 'utf8') + bytes(PublicKey(EVM_LOADER))).digest()) + create_holder(operator_keypair, seed=seed, storage=storage) + + trx = Transaction() + trx.add( + solana_utils.create_account_with_seed(session_user.solana_account.public_key, + session_user.solana_account.public_key, seed, 10 ** 9, 128 * 1024), + solana_utils.create_holder_account(storage, session_user.solana_account.public_key, bytes(seed, 'utf8')) + ) + + error = str.format(InstructionAsserts.INVALID_ACCOUNT, storage) + with pytest.raises(solana.rpc.core.RPCException, match=error): + solana_utils.send_transaction(solana_client, trx, session_user.solana_account) + + +def test_write_tx_to_holder(operator_keypair, session_user, second_session_user, evm_loader): + holder_acc = create_holder(operator_keypair) + signed_tx = make_eth_transaction(second_session_user.eth_address, None, session_user, 10) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + assert signed_tx.rawTransaction == transaction_from_holder(holder_acc), \ + "Account data is not correct" + + +def test_write_tx_to_holder_in_parts(operator_keypair, session_user): + holder_acc = create_holder(operator_keypair) + + signed_tx = make_deployment_transaction(session_user, "erc20_for_spl_factory", "ERC20ForSplFactory") + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + assert signed_tx.rawTransaction == transaction_from_holder(holder_acc), \ + "Account data is not correct" + + +def test_write_tx_to_holder_by_no_owner(operator_keypair, session_user, second_session_user, evm_loader): + holder_acc = create_holder(operator_keypair) + + signed_tx = make_eth_transaction(second_session_user.eth_address, None, session_user, 10) + with pytest.raises(solana.rpc.core.RPCException, match="invalid owner"): + write_transaction_to_holder_account(signed_tx, holder_acc, session_user.solana_account) + + +def test_delete_holder(operator_keypair): + holder_acc = create_holder(operator_keypair) + delete_holder(holder_acc, operator_keypair, operator_keypair) + info = solana_client.get_account_info(holder_acc, commitment=Confirmed) + assert info.value is None, "Holder account isn't deleted" + + +def test_success_refund_after_holder_deliting(operator_keypair): + holder_acc = create_holder(operator_keypair) + + pre_storage = get_solana_balance(holder_acc) + pre_acc = get_solana_balance(operator_keypair.public_key) + + delete_holder(holder_acc, operator_keypair, operator_keypair) + + post_acc = get_solana_balance(operator_keypair.public_key) + + assert pre_storage + pre_acc, post_acc + 5000 + + +def test_delete_holder_by_no_owner(operator_keypair, user_account): + holder_acc = create_holder(operator_keypair) + with pytest.raises(solana.rpc.core.RPCException, match="invalid owner"): + delete_holder(holder_acc, user_account.solana_account, user_account.solana_account) + + +def test_write_to_not_finalized_holder(rw_lock_contract, user_account, evm_loader, operator_keypair, treasury_pool, + new_holder_acc): + signed_tx = make_contract_call_trx(user_account, rw_lock_contract, "unchange_storage(uint8,uint8)", [1, 1]) + write_transaction_to_holder_account(signed_tx, new_holder_acc, operator_keypair) + + send_transaction_step_from_account(operator_keypair, evm_loader, treasury_pool, new_holder_acc, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address], 1, operator_keypair) + account_data = solana_client.get_account_info(new_holder_acc, commitment=Confirmed).value.data + parsed_data = STORAGE_ACCOUNT_INFO_LAYOUT.parse(account_data) + assert parsed_data.tag == TAG_STATE + + signed_tx2 = make_contract_call_trx(user_account, rw_lock_contract, "unchange_storage(uint8,uint8)", [1, 1]) + + with pytest.raises(solana.rpc.core.RPCException, match="invalid tag"): + write_transaction_to_holder_account(signed_tx2, new_holder_acc, operator_keypair) + + +def test_write_to_finalized_holder(rw_lock_contract, session_user, evm_loader, operator_keypair, treasury_pool, + new_holder_acc): + signed_tx = make_contract_call_trx(session_user, rw_lock_contract, "unchange_storage(uint8,uint8)", [1, 1]) + write_transaction_to_holder_account(signed_tx, new_holder_acc, operator_keypair) + + execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, new_holder_acc, + [session_user.solana_account_address, + session_user.balance_account_address, + rw_lock_contract.solana_address]) + signed_tx2 = make_contract_call_trx(session_user, rw_lock_contract, "unchange_storage(uint8,uint8)", [1, 1]) + + write_transaction_to_holder_account(signed_tx2, new_holder_acc, operator_keypair) + assert signed_tx2.rawTransaction == transaction_from_holder(new_holder_acc), \ + "Account data is not correct" + + +def test_holder_write_integer_overflow(operator_keypair, holder_acc): + overflow_offset = int(0xFFFFFFFFFFFFFFFF) + + trx = Transaction() + trx.add(make_WriteHolder(operator_keypair.public_key, holder_acc, b"\x00" * 32, overflow_offset, b"\x00" * 1)) + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.HOLDER_OVERFLOW): + solana_client.send_transaction( + trx, + operator_keypair, + opts=TxOpts(skip_confirmation=True, preflight_commitment=Confirmed), + ) + +def test_holder_write_account_size_overflow(operator_keypair, holder_acc): + overflow_offset = int(0xFFFFFFFF) + + trx = Transaction() + trx.add(make_WriteHolder(operator_keypair.public_key, holder_acc, b"\x00" * 32, overflow_offset, b"\x00" * 1)) + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.HOLDER_INSUFFICIENT_SIZE): + solana_client.send_transaction( + trx, + operator_keypair, + opts=TxOpts(skip_confirmation=True, preflight_commitment=Confirmed), + ) diff --git a/integration/tests/neon_evm/test_neon_core_api.py b/integration/tests/neon_evm/test_neon_core_api.py new file mode 100644 index 0000000000..ec66dd4f67 --- /dev/null +++ b/integration/tests/neon_evm/test_neon_core_api.py @@ -0,0 +1,58 @@ +from eth_utils import abi, to_text + +from .utils.contract import deploy_contract, get_contract_bin +from .solana_utils import solana_client + + +def test_get_storage_at(neon_api_client, operator_keypair, user_account, evm_loader, treasury_pool): + contract = deploy_contract(operator_keypair, user_account, "hello_world", evm_loader, treasury_pool) + storage = neon_api_client.get_storage_at(contract.eth_address.hex())["value"] + zero_array = [0 for _ in range(31)] + assert storage == zero_array + [5] + + storage = neon_api_client.get_storage_at(contract.eth_address.hex(), index='0x2')["value"] + assert storage == zero_array + [0] + + +def test_get_ether_account_data(neon_api_client, user_account): + result = neon_api_client.get_ether_account_data(user_account.eth_address.hex())['value'] + assert str(user_account.balance_account_address) == result[0]["solana_address"] + assert solana_client.get_account_info(user_account.solana_account.public_key).value is not None + + +def test_emulate_transfer(neon_api_client, user_account, session_user): + result = neon_api_client.emulate(user_account.eth_address.hex(), + session_user.eth_address.hex()) + assert result['exit_status'] == 'succeed', f"The 'exit_status' field is not succeed. Result: {result}" + assert result['steps_executed'] == 1, f"Steps executed amount is not 1. Result: {result}" + assert result['used_gas'] > 0, f"Used gas is less than 0. Result: {result}" + + +def test_emulate_contract_deploy(neon_api_client, user_account): + contract_code = get_contract_bin("hello_world") + result = neon_api_client.emulate(user_account.eth_address.hex(), + contract=None, data=contract_code) + assert result['exit_status'] == 'succeed', f"The 'exit_status' field is not succeed. Result: {result}" + assert result['steps_executed'] > 100, f"Steps executed amount is wrong. Result: {result}" + assert result['used_gas'] > 0, f"Used gas is less than 0. Result: {result}" + + +def test_emulate_call_contract_function(neon_api_client, operator_keypair, treasury_pool, evm_loader, user_account): + contract = deploy_contract(operator_keypair, user_account, "hello_world", evm_loader, treasury_pool) + assert contract.eth_address + data = abi.function_signature_to_4byte_selector('call_hello_world()') + + result = neon_api_client.emulate(user_account.eth_address.hex(), + contract=contract.eth_address.hex(), data=data) + + assert result['exit_status'] == 'succeed', f"The 'exit_status' field is not succeed. Result: {result}" + assert result['steps_executed'] > 0, f"Steps executed amount is 0. Result: {result}" + assert result['used_gas'] > 0, f"Used gas is less than 0. Result: {result}" + assert "Hello World" in to_text(result["result"]) + + +def test_emulate_with_small_amount_of_steps(neon_api_client, evm_loader, user_account): + 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['error'] == 'Too many steps' diff --git a/integration/tests/neon_evm/test_parallel.py b/integration/tests/neon_evm/test_parallel.py new file mode 100644 index 0000000000..856ef47369 --- /dev/null +++ b/integration/tests/neon_evm/test_parallel.py @@ -0,0 +1,165 @@ +from typing import Any +from unittest import TestCase + +from _pytest.fixtures import fixture +from solana.keypair import Keypair +from solana.publickey import PublicKey +from solana.rpc.core import RPCException + +from .solana_utils import EvmLoader, solana_client, make_new_user, deposit_neon, \ + cancel_transaction, send_transaction_step_from_account, execute_trx_from_instruction +from .utils.contract import write_transaction_to_holder_account, make_deployment_transaction +from .utils.ethereum import create_contract_address, make_eth_transaction +from .utils.storage import create_holder +from .types.types import Caller, TreasuryPool + +EVM_STEPS_COUNT = 0xFFFFFFFF +ONE_TOKEN = 10 ** 9 +MAX_PERMITTED_DATA_INCREASE = 10240 + + +class ParallelTransactionsTest(TestCase): + @fixture(autouse=True) + def prepare_fixture( + self, + user_account: Caller, + evm_loader: EvmLoader, + operator_keypair: Keypair, + treasury_pool: TreasuryPool, + ): + self.user_account = user_account + self.evm_loader = evm_loader + self.operator_keypair = operator_keypair + self.treasury_pool = treasury_pool + self.second_account = make_new_user(evm_loader) + + def test_create_same_accounts(self): + cases = [ + [2], + [3], + [4], + ] + + for case in cases: + iterations = case[0] + with self.subTest(iterations=iterations): + self.create_same_accounts_subtest(iterations) + + def create_same_accounts_subtest(self, iterations: int): + deposit_neon(self.evm_loader, self.operator_keypair, self.user_account.eth_address, ONE_TOKEN) + deposit_neon(self.evm_loader, self.operator_keypair, self.second_account.eth_address, ONE_TOKEN) + + contract = create_contract_address(self.user_account, self.evm_loader) + holder_acc = create_holder(self.operator_keypair) + deployment_tx = make_deployment_transaction(self.user_account, "big_contract", "BigContract") + write_transaction_to_holder_account(deployment_tx, holder_acc, self.operator_keypair) + + # First N iterations + for i in range(iterations): + deployment_receipt = send_transaction_step_from_account(self.operator_keypair, + self.evm_loader, + self.treasury_pool, + holder_acc, + [contract.balance_account_address, + contract.solana_address, + self.user_account.balance_account_address, + self.user_account.solana_account_address], + EVM_STEPS_COUNT, + self.operator_keypair, + index=i) + + assert not ParallelTransactionsTest.check_iteration_deployed(deployment_receipt) + + # Transferring to the same account in order to break deployment + ParallelTransactionsTest.transfer( + self.second_account, + contract.eth_address, + ONE_TOKEN, + self.evm_loader, + self.operator_keypair, + self.treasury_pool, + ) + + # Trying to finish deployment (expected to fail) + try: + send_transaction_step_from_account(self.operator_keypair, + self.evm_loader, + self.treasury_pool, + holder_acc, + [contract.balance_account_address, + contract.solana_address, + self.user_account.balance_account_address, + self.user_account.solana_account_address], + EVM_STEPS_COUNT, + self.operator_keypair) + + assert False, 'Deployment expected to fail' + except RPCException as e: + ParallelTransactionsTest.check_account_initialized_in_another_trx_exception(e, + contract.balance_account_address) + + # Cancel deployment transaction: + cancel_transaction( + self.evm_loader, + deployment_tx.hash, + holder_acc, + self.operator_keypair, + [contract.balance_account_address, + contract.solana_address, + self.user_account.balance_account_address, + self.user_account.solana_account_address], + ) + + @staticmethod + def check_iteration_deployed(receipt: Any) -> bool: + if receipt.value.transaction.meta.err: + raise AssertionError(f"Can't deploy contract: {receipt.value.transaction.meta.err}") + + for log in receipt.value.transaction.meta.log_messages: + if "exit_status" in log: + return True + if "ExitError" in log: + raise AssertionError(f"EVM Return error in logs: {receipt}") + return False + + @staticmethod + def transfer( + src_account: Caller, + dst_addr: bytes, + value: int, + evm_loader: EvmLoader, + operator_keypair: Keypair, + treasury_pool: TreasuryPool, + ): + message = make_eth_transaction( + dst_addr, + bytes(), + src_account, + value, + ) + + dst_solana_account, _ = evm_loader.ether2program(dst_addr) + dst_balance_account = evm_loader.ether2balance(dst_addr) + + trx = execute_trx_from_instruction(operator_keypair, evm_loader, treasury_pool.account, treasury_pool.buffer, + message, + [dst_balance_account, + PublicKey(dst_solana_account), + src_account.balance_account_address], + operator_keypair) + receipt = solana_client.get_transaction(trx.value) + print("Transfer receipt:", receipt) + assert receipt.value.transaction.meta.err is None + + return receipt + + @staticmethod + def check_account_initialized_in_another_trx_exception(exception: RPCException, solana_address: PublicKey): + error = exception.args[0] + print("error:", error) + + for log in error.data.logs: + if f'Account {solana_address} - was empty, created by another transaction' in log: + return + + assert False, "Search string not found in Solana logs" diff --git a/integration/tests/neon_evm/test_step_instructions_work_the_same.py b/integration/tests/neon_evm/test_step_instructions_work_the_same.py new file mode 100644 index 0000000000..be39c904d8 --- /dev/null +++ b/integration/tests/neon_evm/test_step_instructions_work_the_same.py @@ -0,0 +1,60 @@ +from .solana_utils import solana_client, execute_transaction_steps_from_account, write_transaction_to_holder_account, \ + execute_transaction_steps_from_instruction +from .utils.contract import make_deployment_transaction +from .utils.ethereum import make_eth_transaction, create_contract_address +from .utils.storage import create_holder + + +class TestTransactionStepFromAccount: + def test_simple_transfer_transaction(self, operator_keypair, treasury_pool, evm_loader, + sender_with_tokens, session_user, holder_acc): + amount = 10 + + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, amount) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + resp_from_acc = execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0).value + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, amount) + signature = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], + 0) + resp_from_inst = solana_client.get_transaction(signature.value).value + assert resp_from_acc.transaction.meta.fee == resp_from_inst.transaction.meta.fee + assert resp_from_acc.transaction.meta.inner_instructions == resp_from_inst.transaction.meta.inner_instructions + for i in range(len(resp_from_acc.transaction.meta.post_balances)): + assert resp_from_acc.transaction.meta.post_balances[i] - resp_from_acc.transaction.meta.pre_balances[i] == \ + resp_from_inst.transaction.meta.post_balances[i] - resp_from_inst.transaction.meta.pre_balances[i] + + def test_deploy_contract(self, operator_keypair, holder_acc, treasury_pool, evm_loader, sender_with_tokens): + contract_filename = "small" + contract = create_contract_address(sender_with_tokens, evm_loader) + + signed_tx = make_deployment_transaction(sender_with_tokens, contract_filename) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + resp_from_acc = execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [contract.solana_address, + contract.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address]).value + signed_tx = make_deployment_transaction(sender_with_tokens, contract_filename) + holder_acc = create_holder(operator_keypair) + contract = create_contract_address(sender_with_tokens, evm_loader) + + signature = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, [contract.solana_address, + contract.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address]) + resp_from_inst = solana_client.get_transaction(signature.value).value + assert resp_from_acc.transaction.meta.fee == resp_from_inst.transaction.meta.fee + assert len(resp_from_acc.transaction.meta.inner_instructions) == len( + resp_from_inst.transaction.meta.inner_instructions) + assert len(resp_from_acc.transaction.transaction.message.account_keys) == len( + resp_from_acc.transaction.transaction.message.account_keys) diff --git a/integration/tests/neon_evm/test_transaction_step_from_account.py b/integration/tests/neon_evm/test_transaction_step_from_account.py new file mode 100644 index 0000000000..0dc4e50d76 --- /dev/null +++ b/integration/tests/neon_evm/test_transaction_step_from_account.py @@ -0,0 +1,556 @@ +import random +import string +import time + +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, Confirmed +from solana.rpc.types import TxOpts + +from .solana_utils import solana_client, execute_transaction_steps_from_account, \ + write_transaction_to_holder_account, create_treasury_pool_address, send_transaction_step_from_account +from utils.helpers import gen_hash_of_block +from .utils.assert_messages import InstructionAsserts +from .utils.constants import TAG_FINALIZED_STATE +from .utils.contract import make_deployment_transaction, make_contract_call_trx, deploy_contract, get_contract_bin +from .utils.ethereum import make_eth_transaction, create_contract_address +from .utils.instructions import TransactionWithComputeBudget, make_ExecuteTrxFromAccountDataIterativeOrContinue +from .utils.layouts import FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT +from .utils.storage import create_holder +from .utils.transaction_checks import check_transaction_logs_have_text, check_holder_account_tag +from .types.types import TreasuryPool + + +def generate_access_lists(): + addr1 = gen_hash_of_block(20) + addr2 = gen_hash_of_block(20) + key1, key2, key3, key4 = (f"0x000000000000000000000000000000000000000000000000000000000000000{item}" for item in + (0, 1, 2, 3)) + return (({"address": addr1, "storageKeys": []},), + ({"address": addr1, "storageKeys": (key1, key2, key3, key4)},), + ({"address": addr1, "storageKeys": (key1, key2)}, {"address": addr2, "storageKeys": []}), + ({"address": addr1, "storageKeys": (key1, key2)}, {"address": addr2, "storageKeys": (key3,)})) + + +class TestTransactionStepFromAccount: + + def test_simple_transfer_transaction(self, operator_keypair, treasury_pool, evm_loader, + sender_with_tokens, session_user, holder_acc): + amount = 10 + sender_balance_before = evm_loader.get_neon_balance(sender_with_tokens) + recipient_balance_before = evm_loader.get_neon_balance(session_user) + + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, amount) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + resp = execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + sender_balance_after = evm_loader.get_neon_balance(sender_with_tokens) + recipient_balance_after = evm_loader.get_neon_balance(session_user) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x11") + assert sender_balance_before - amount == sender_balance_after + assert recipient_balance_before + amount == recipient_balance_after + + def test_deploy_contract(self, operator_keypair, holder_acc, treasury_pool, evm_loader, sender_with_tokens, + neon_api_client): + contract_filename = "hello_world" + contract = create_contract_address(sender_with_tokens, evm_loader) + + signed_tx = make_deployment_transaction(sender_with_tokens, contract_filename) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + contract_code = get_contract_bin("hello_world") + + steps_count = neon_api_client.get_steps_count(sender_with_tokens, None, contract_code) + resp = execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [contract.solana_address, + contract.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], + steps_count) + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x12") + + def test_call_contract_function_without_neon_transfer(self, operator_keypair, holder_acc, treasury_pool, + sender_with_tokens, evm_loader, string_setter_contract, + neon_api_client): + text = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + signed_tx = make_contract_call_trx(sender_with_tokens, string_setter_contract, "set(string)", [text]) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + resp = execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [string_setter_contract.solana_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address]) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x11") + + assert text in to_text( + neon_api_client.call_contract_get_function(sender_with_tokens, string_setter_contract, + "get()")) + + def test_call_contract_function_with_neon_transfer(self, operator_keypair, treasury_pool, + sender_with_tokens, string_setter_contract, holder_acc, + evm_loader, neon_api_client): + transfer_amount = random.randint(1, 1000) + + sender_balance_before = evm_loader.get_neon_balance(sender_with_tokens) + contract_balance_before = evm_loader.get_neon_balance(string_setter_contract.eth_address) + + text = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + + signed_tx = make_contract_call_trx(sender_with_tokens, string_setter_contract, "set(string)", [text], + value=transfer_amount) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + resp = execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [string_setter_contract.solana_address, + string_setter_contract.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address]) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x11") + + sender_balance_after = evm_loader.get_neon_balance(sender_with_tokens) + contract_balance_after = evm_loader.get_neon_balance(string_setter_contract.eth_address) + assert sender_balance_before - transfer_amount == sender_balance_after + assert contract_balance_before + transfer_amount == contract_balance_after + + assert text in to_text( + neon_api_client.call_contract_get_function(sender_with_tokens, string_setter_contract, + "get()")) + + 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_solana_address, _ = evm_loader.ether2program(recipient_ether) + recipient_balance_address = evm_loader.ether2balance(recipient_ether) + amount = 10 + signed_tx = make_eth_transaction(recipient_ether, None, sender_with_tokens, amount) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + resp = execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [PublicKey(recipient_solana_address), + recipient_balance_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + recipient_balance_after = evm_loader.get_neon_balance(recipient_ether) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x11") + + assert recipient_balance_after == amount + + def test_incorrect_chain_id(self, operator_keypair, holder_acc, treasury_pool, + sender_with_tokens, session_user, evm_loader): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1, chain_id=1) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INVALID_CHAIN_ID): + execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_incorrect_nonce(self, operator_keypair, treasury_pool, sender_with_tokens, evm_loader, session_user, + holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INVALID_NONCE): + execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_run_finalized_transaction(self, operator_keypair, treasury_pool, sender_with_tokens, evm_loader, + session_user, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.TRX_ALREADY_FINALIZED): + execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_insufficient_funds(self, operator_keypair, treasury_pool, evm_loader, session_user, + holder_acc, sender_with_tokens): + user_balance = evm_loader.get_neon_balance(session_user) + + signed_tx = make_eth_transaction(sender_with_tokens.eth_address, None, session_user, user_balance + 1) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INSUFFICIENT_FUNDS): + execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_gas_limit_reached(self, operator_keypair, treasury_pool, session_user, evm_loader, sender_with_tokens, + holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 10, gas=1) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.OUT_OF_GAS): + execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_sender_missed_in_remaining_accounts(self, operator_keypair, treasury_pool, session_user, + sender_with_tokens, evm_loader, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): + execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [session_user.solana_account_address, + session_user.balance_account_address], 0) + + def test_recipient_missed_in_remaining_accounts(self, operator_keypair, treasury_pool, session_user, + sender_with_tokens, evm_loader, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): + execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_incorrect_treasure_pool(self, operator_keypair, sender_with_tokens, evm_loader, session_user, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + 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')) + + error = str.format(InstructionAsserts.INVALID_ACCOUNT, treasury.account) + with pytest.raises(solana.rpc.core.RPCException, match=error): + execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury, holder_acc, [], 0) + + def test_incorrect_treasure_index(self, operator_keypair, sender_with_tokens, evm_loader, + session_user, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + index = 2 + treasury = TreasuryPool(index, create_treasury_pool_address(index), (index + 1).to_bytes(4, 'little')) + + error = str.format(InstructionAsserts.INVALID_ACCOUNT, treasury.account) + with pytest.raises(solana.rpc.core.RPCException, match=error): + execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury, holder_acc, [], 0) + + def test_incorrect_operator_account(self, operator_keypair, sender_with_tokens, evm_loader, treasury_pool, + session_user, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + 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): + execute_transaction_steps_from_account(fake_operator, evm_loader, treasury_pool, holder_acc, + [sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address, + session_user.solana_account_address, + session_user.balance_account_address], 0) + + def test_operator_is_not_in_white_list(self, sender_with_tokens, operator_keypair, evm_loader, treasury_pool, + session_user, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.NOT_AUTHORIZED_OPERATOR): + execute_transaction_steps_from_account(sender_with_tokens.solana_account, evm_loader, treasury_pool, + holder_acc, + [sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address, + session_user.solana_account_address, + session_user.balance_account_address], 0, + signer=sender_with_tokens.solana_account) + + def test_incorrect_system_program(self, sender_with_tokens, operator_keypair, evm_loader, treasury_pool, + session_user, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + fake_sys_program_id = Keypair().public_key + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + error = str.format(InstructionAsserts.NOT_SYSTEM_PROGRAM, fake_sys_program_id) + with pytest.raises(solana.rpc.core.RPCException, match=error): + send_transaction_step_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [], 1, operator_keypair, system_program=fake_sys_program_id) + + def test_incorrect_holder_account(self, operator_keypair, evm_loader, treasury_pool): + fake_holder_acc = Keypair.generate().public_key + + error = str.format(InstructionAsserts.NOT_PROGRAM_OWNED, fake_holder_acc) + with pytest.raises(solana.rpc.core.RPCException, match=error): + send_transaction_step_from_account(operator_keypair, evm_loader, treasury_pool, fake_holder_acc, [], 1, + operator_keypair) + + def test_transaction_with_access_list(self, operator_keypair, holder_acc, treasury_pool, + sender_with_tokens, evm_loader, calculator_contract, + calculator_caller_contract): + access_list = ( + { + "address": '0x' + calculator_contract.eth_address.hex(), + "storageKeys": ( + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001", + ) + }, + ) + signed_tx = make_contract_call_trx(sender_with_tokens, calculator_caller_contract, "callCalculator()", [], + access_list=access_list) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + resp = execute_transaction_steps_from_account(operator_keypair, evm_loader, 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]) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x12") + + @pytest.mark.parametrize("access_list", generate_access_lists()) + def test_access_list_structure(self, operator_keypair, holder_acc, treasury_pool, evm_loader, + sender_with_tokens, string_setter_contract, access_list, neon_api_client): + text = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + + signed_tx = make_contract_call_trx(sender_with_tokens, string_setter_contract, "set(string)", [text], + value=10, access_list=access_list) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + resp = execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [string_setter_contract.solana_address, + string_setter_contract.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address]) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x11") + + assert text in to_text( + neon_api_client.call_contract_get_function(sender_with_tokens, string_setter_contract, + "get()")) + + +class TestAccountStepContractCallContractInteractions: + def test_contract_call_unchange_storage_function(self, rw_lock_contract, rw_lock_caller, session_user, evm_loader, + operator_keypair, treasury_pool, holder_acc): + signed_tx = make_contract_call_trx(session_user, rw_lock_caller, 'unchange_storage(uint8,uint8)', [1, 1]) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + resp = execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [rw_lock_caller.solana_address, + rw_lock_contract.solana_address, + session_user.solana_account_address, + session_user.balance_account_address]) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x12") + + def test_contract_call_set_function(self, rw_lock_contract, session_user, evm_loader, operator_keypair, + treasury_pool, holder_acc, rw_lock_caller, neon_api_client): + signed_tx = make_contract_call_trx(session_user, rw_lock_caller, 'update_storage_str(string)', ['hello']) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + resp = execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [rw_lock_caller.solana_address, + rw_lock_contract.solana_address, + session_user.solana_account_address, + session_user.balance_account_address], 1000) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x11") + + assert 'hello' in to_text(neon_api_client.call_contract_get_function(session_user, rw_lock_contract, + "get_text()")) + + def test_contract_call_get_function(self, rw_lock_contract, session_user, evm_loader, operator_keypair, + treasury_pool, holder_acc, rw_lock_caller): + signed_tx = make_contract_call_trx(session_user, rw_lock_caller, 'get_text()') + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + resp = execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + [rw_lock_caller.solana_address, + rw_lock_contract.solana_address, + session_user.solana_account_address, + session_user.balance_account_address], 1000) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x12") + + def test_contract_call_update_storage_map_function(self, rw_lock_contract, session_user, evm_loader, + operator_keypair, rw_lock_caller, + treasury_pool, holder_acc, neon_api_client): + signed_tx = make_contract_call_trx(session_user, rw_lock_caller, 'update_storage_map(uint256)', [3]) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + func_name = abi.function_signature_to_4byte_selector('update_storage_map(uint256)') + data = func_name + eth_abi.encode(['uint256'], [3]) + result = neon_api_client.emulate(session_user.eth_address.hex(), + rw_lock_caller.eth_address.hex(), + data.hex()) + additional_accounts = [session_user.solana_account_address, + session_user.balance_account_address, + rw_lock_contract.solana_address, + rw_lock_caller.solana_address] + print(result) + for acc in result['solana_accounts']: + additional_accounts.append(PublicKey(acc['pubkey'])) + + resp = execute_transaction_steps_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc, + additional_accounts) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x11") + + constructor_args = eth_abi.encode(['address', 'uint256'], [rw_lock_caller.eth_address.hex(), 2]) + actual_data = neon_api_client.call_contract_get_function(session_user, rw_lock_contract, + "data(address,uint256)", constructor_args) + assert to_int(hexstr=actual_data) == 2, "Contract data is not correct" + + +class TestTransactionStepFromAccountParallelRuns: + + def test_one_user_call_2_contracts(self, rw_lock_contract, string_setter_contract, user_account, evm_loader, + operator_keypair, treasury_pool, new_holder_acc): + signed_tx = make_contract_call_trx(user_account, rw_lock_contract, 'unchange_storage(uint8,uint8)', [1, 1]) + write_transaction_to_holder_account(signed_tx, new_holder_acc, operator_keypair) + + send_transaction_step_from_account(operator_keypair, evm_loader, treasury_pool, new_holder_acc, + [user_account.balance_account_address, + user_account.solana_account_address, + rw_lock_contract.solana_address], 1, operator_keypair) + + signed_tx2 = make_contract_call_trx(user_account, string_setter_contract, 'get()') + + holder_acc2 = create_holder(operator_keypair) + write_transaction_to_holder_account(signed_tx2, holder_acc2, operator_keypair) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.LOCKED_ACC): + send_transaction_step_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc2, + [user_account.balance_account_address, + user_account.solana_account_address, + string_setter_contract.solana_address], 1, operator_keypair) + + def test_2_users_call_the_same_contract(self, rw_lock_contract, user_account, + session_user, evm_loader, operator_keypair, + treasury_pool, new_holder_acc): + signed_tx = make_contract_call_trx(user_account, rw_lock_contract, 'unchange_storage(uint8,uint8)', [1, 1]) + write_transaction_to_holder_account(signed_tx, new_holder_acc, operator_keypair) + + send_transaction_step_from_account(operator_keypair, evm_loader, treasury_pool, new_holder_acc, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address], 1, operator_keypair) + + signed_tx2 = make_contract_call_trx(session_user, rw_lock_contract, 'get_text()') + + holder_acc2 = create_holder(operator_keypair) + write_transaction_to_holder_account(signed_tx2, holder_acc2, operator_keypair) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.LOCKED_ACC): + send_transaction_step_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc2, + [session_user.solana_account_address, + session_user.balance_account_address, + rw_lock_contract.solana_address], 1, operator_keypair) + + def test_two_contracts_call_same_contract(self, rw_lock_contract, user_account, + session_user, evm_loader, operator_keypair, + treasury_pool, new_holder_acc): + constructor_args = eth_abi.encode(['address'], [rw_lock_contract.eth_address.hex()]) + + contract1 = deploy_contract(operator_keypair, session_user, "rw_lock", evm_loader, treasury_pool, + encoded_args=constructor_args, contract_name="rw_lock_caller") + contract2 = deploy_contract(operator_keypair, session_user, "rw_lock", evm_loader, treasury_pool, + encoded_args=constructor_args, contract_name="rw_lock_caller") + + signed_tx1 = make_contract_call_trx(user_account, contract1, 'unchange_storage(uint8,uint8)', [1, 1]) + signed_tx2 = make_contract_call_trx(session_user, contract2, 'get_text()') + write_transaction_to_holder_account(signed_tx1, new_holder_acc, operator_keypair) + + send_transaction_step_from_account(operator_keypair, evm_loader, treasury_pool, new_holder_acc, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address, + contract1.solana_address], 1, operator_keypair) + + holder_acc2 = create_holder(operator_keypair) + write_transaction_to_holder_account(signed_tx2, holder_acc2, operator_keypair) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.LOCKED_ACC): + send_transaction_step_from_account(operator_keypair, evm_loader, treasury_pool, holder_acc2, + [session_user.solana_account_address, + session_user.balance_account_address, + rw_lock_contract.solana_address, + contract2.solana_address], 1, + operator_keypair) + + +class TestStepFromAccountChangingOperatorsDuringTrxRun: + def test_next_operator_can_continue_trx_after_some_time(self, rw_lock_contract, user_account, evm_loader, + operator_keypair, second_operator_keypair, treasury_pool, + new_holder_acc): + signed_tx = make_contract_call_trx(user_account, rw_lock_contract, 'update_storage_str(string)', ['text']) + write_transaction_to_holder_account(signed_tx, new_holder_acc, operator_keypair) + + trx = TransactionWithComputeBudget(operator_keypair) + trx.add( + make_ExecuteTrxFromAccountDataIterativeOrContinue( + 0, 1, + operator_keypair, evm_loader, new_holder_acc, treasury_pool, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address] + ) + ) + solana_client.send_transaction(trx, operator_keypair, + opts=TxOpts(skip_confirmation=True, preflight_commitment=Confirmed)) + + # next operator can't continue trx during OPERATOR_PRIORITY_SLOTS*0.4 + error = rf"{InstructionAsserts.INVALID_OPERATOR_KEY}|{InstructionAsserts.INVALID_HOLDER_OWNER}" + with pytest.raises(solana.rpc.core.RPCException, match=error): + send_transaction_step_from_account( + second_operator_keypair, evm_loader, treasury_pool, new_holder_acc, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address], + 500, second_operator_keypair + ) + + time.sleep(15) + send_transaction_step_from_account(second_operator_keypair, evm_loader, treasury_pool, new_holder_acc, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address], 500, second_operator_keypair) + resp = send_transaction_step_from_account(second_operator_keypair, evm_loader, treasury_pool, new_holder_acc, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address], 1, second_operator_keypair) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x11") diff --git a/integration/tests/neon_evm/test_transaction_step_from_account_no_chainid.py b/integration/tests/neon_evm/test_transaction_step_from_account_no_chainid.py new file mode 100644 index 0000000000..d9115e5360 --- /dev/null +++ b/integration/tests/neon_evm/test_transaction_step_from_account_no_chainid.py @@ -0,0 +1,120 @@ +import random +import re +import string +import solana + +import pytest +from eth_utils import to_text + +from .solana_utils import write_transaction_to_holder_account, \ + execute_transaction_steps_from_account_no_chain_id +from .utils.constants import TAG_FINALIZED_STATE +from .utils.contract import make_deployment_transaction, make_contract_call_trx, get_contract_bin +from .utils.ethereum import make_eth_transaction, create_contract_address +from .utils.layouts import FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT +from .utils.transaction_checks import check_holder_account_tag, check_transaction_logs_have_text + + +class TestTransactionStepFromAccountNoChainId: + + def test_simple_transfer_transaction(self, operator_keypair, treasury_pool, evm_loader, + sender_with_tokens, session_user, holder_acc): + amount = 10 + sender_balance_before = evm_loader.get_neon_balance(sender_with_tokens) + recipient_balance_before = evm_loader.get_neon_balance(session_user) + + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, amount, chain_id=None) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + resp = execute_transaction_steps_from_account_no_chain_id(operator_keypair, evm_loader, treasury_pool, + holder_acc, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.balance_account_address, + sender_with_tokens.solana_account_address], 0) + + sender_balance_after = evm_loader.get_neon_balance(sender_with_tokens) + recipient_balance_after = evm_loader.get_neon_balance(session_user) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x11") + assert sender_balance_before - amount == sender_balance_after + assert recipient_balance_before + amount == recipient_balance_after + + def test_deploy_contract(self, operator_keypair, holder_acc, treasury_pool, evm_loader, sender_with_tokens, + neon_api_client): + contract_filename = "hello_world" + contract = create_contract_address(sender_with_tokens, evm_loader) + + signed_tx = make_deployment_transaction(sender_with_tokens, contract_filename, chain_id=None) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + contract_code = get_contract_bin(contract_filename) + + steps_count = neon_api_client.get_steps_count(sender_with_tokens, None, contract_code) + resp = execute_transaction_steps_from_account_no_chain_id(operator_keypair, evm_loader, treasury_pool, + holder_acc, + [contract.solana_address, + contract.balance_account_address, + sender_with_tokens.balance_account_address, + sender_with_tokens.solana_account_address], + steps_count) + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x12") + + def test_call_contract_function_with_neon_transfer(self, operator_keypair, treasury_pool, + sender_with_tokens, string_setter_contract, holder_acc, + evm_loader, neon_api_client): + transfer_amount = random.randint(1, 1000) + + sender_balance_before = evm_loader.get_neon_balance(sender_with_tokens) + contract_balance_before = evm_loader.get_neon_balance(string_setter_contract.eth_address) + + text = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + + signed_tx = make_contract_call_trx(sender_with_tokens, string_setter_contract, "set(string)", [text], + value=transfer_amount, chain_id=None) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + resp = execute_transaction_steps_from_account_no_chain_id(operator_keypair, evm_loader, treasury_pool, + holder_acc, + [string_setter_contract.solana_address, + string_setter_contract.balance_account_address, + sender_with_tokens.balance_account_address, + sender_with_tokens.solana_account_address] + ) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value.transaction.transaction.signatures[0], "exit_status=0x11") + + sender_balance_after = evm_loader.get_neon_balance(sender_with_tokens) + contract_balance_after = evm_loader.get_neon_balance(string_setter_contract.eth_address) + assert sender_balance_before - transfer_amount == sender_balance_after + assert contract_balance_before + transfer_amount == contract_balance_after + + assert text in to_text( + neon_api_client.call_contract_get_function(sender_with_tokens, string_setter_contract, "get()") + ) + + def test_transaction_with_access_list(self, operator_keypair, treasury_pool, + sender_with_tokens, calculator_contract, calculator_caller_contract, + holder_acc, evm_loader): + access_list = ( + { + "address": '0x' + calculator_contract.eth_address.hex(), + "storageKeys": ( + "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + }, + ) + signed_tx = make_contract_call_trx(sender_with_tokens, calculator_caller_contract, "callCalculator()", [], + chain_id=None, access_list=access_list) + write_transaction_to_holder_account(signed_tx, holder_acc, operator_keypair) + + error = re.escape("assertion failed: trx.chain_id().is_none()") + with pytest.raises(solana.rpc.core.RPCException, match=error): + execute_transaction_steps_from_account_no_chain_id(operator_keypair, evm_loader, treasury_pool, + holder_acc, + [calculator_contract.solana_address, + calculator_caller_contract.solana_address, + sender_with_tokens.balance_account_address, + sender_with_tokens.solana_account_address] + ) diff --git a/integration/tests/neon_evm/test_transaction_step_from_instruction.py b/integration/tests/neon_evm/test_transaction_step_from_instruction.py new file mode 100644 index 0000000000..65ad2ccb44 --- /dev/null +++ b/integration/tests/neon_evm/test_transaction_step_from_instruction.py @@ -0,0 +1,609 @@ +import random +import string +import time + +import eth_abi +import pytest +import rlp +import solana +from eth_account.datastructures import SignedTransaction +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 solana.rpc.commitment import Confirmed +from solana.rpc.core import RPCException + +from .solana_utils import execute_transaction_steps_from_instruction, \ + create_treasury_pool_address, send_transaction_step_from_instruction +from .utils.assert_messages import InstructionAsserts +from .utils.constants import TAG_FINALIZED_STATE +from .utils.contract import make_deployment_transaction, make_contract_call_trx, deploy_contract, get_contract_bin +from .utils.ethereum import make_eth_transaction, create_contract_address +from .utils.layouts import FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT +from .utils.storage import create_holder +from .utils.transaction_checks import check_transaction_logs_have_text, check_holder_account_tag +from .types.types import TreasuryPool + + +class TestTransactionStepFromInstruction: + + def test_simple_transfer_transaction(self, operator_keypair, treasury_pool, evm_loader, + sender_with_tokens, session_user, holder_acc): + amount = 10 + sender_balance_before = evm_loader.get_neon_balance(sender_with_tokens) + recipient_balance_before = evm_loader.get_neon_balance(session_user) + + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, amount) + + resp = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + sender_balance_after = evm_loader.get_neon_balance(sender_with_tokens) + recipient_balance_after = evm_loader.get_neon_balance(session_user) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value, "exit_status=0x11") + assert sender_balance_before - amount == sender_balance_after + assert recipient_balance_before + amount == recipient_balance_after + + @pytest.mark.parametrize("chain_id", [None, 111]) + def test_deploy_contract(self, operator_keypair, holder_acc, treasury_pool, evm_loader, sender_with_tokens, + chain_id, neon_api_client): + contract_filename = "small" + + signed_tx = make_deployment_transaction(sender_with_tokens, contract_filename, chain_id=chain_id) + contract = create_contract_address(sender_with_tokens, evm_loader) + + contract_code = get_contract_bin("small") + + steps_count = neon_api_client.get_steps_count(sender_with_tokens, None, contract_code) + resp = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, [contract.solana_address, + contract.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], + steps_count) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value, "exit_status=0x12") + + def test_call_contract_function_without_neon_transfer(self, operator_keypair, treasury_pool, sender_with_tokens, + evm_loader, holder_acc, string_setter_contract, + neon_api_client): + text = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + signed_tx = make_contract_call_trx(sender_with_tokens, string_setter_contract, "set(string)", [text]) + + resp = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, [string_setter_contract.solana_address, + string_setter_contract.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address] + ) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value, "exit_status=0x11") + + assert text in to_text( + neon_api_client.call_contract_get_function(sender_with_tokens, string_setter_contract, + "get()")) + + def test_call_contract_function_with_neon_transfer(self, operator_keypair, treasury_pool, sender_with_tokens, + evm_loader, holder_acc, string_setter_contract, + neon_api_client): + transfer_amount = random.randint(1, 1000) + + sender_balance_before = evm_loader.get_neon_balance(sender_with_tokens) + contract_balance_before = evm_loader.get_neon_balance(string_setter_contract.eth_address) + + text = ''.join(random.choice(string.ascii_letters) for i in range(10)) + signed_tx = make_contract_call_trx(sender_with_tokens, string_setter_contract, "set(string)", [text], + value=transfer_amount) + + resp = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, [string_setter_contract.solana_address, + string_setter_contract.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address] + ) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value, "exit_status=0x11") + + sender_balance_after = evm_loader.get_neon_balance(sender_with_tokens) + contract_balance_after = evm_loader.get_neon_balance(string_setter_contract.eth_address) + assert sender_balance_before - transfer_amount == sender_balance_after + assert contract_balance_before + transfer_amount == contract_balance_after + + assert text in to_text( + neon_api_client.call_contract_get_function(sender_with_tokens, string_setter_contract, + "get()")) + + 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_solana_address, _ = evm_loader.ether2program(recipient_ether) + recipient_balance_address = evm_loader.ether2balance(recipient_ether) + amount = 10 + signed_tx = make_eth_transaction(recipient_ether, None, sender_with_tokens, amount) + + resp = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, [PublicKey(recipient_solana_address), + recipient_balance_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + recipient_balance_after = evm_loader.get_neon_balance(recipient_ether) + check_transaction_logs_have_text(resp.value, "exit_status=0x11") + + assert recipient_balance_after == amount + + def test_incorrect_chain_id(self, operator_keypair, holder_acc, treasury_pool, + sender_with_tokens, session_user, evm_loader): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1, chain_id=1) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INVALID_CHAIN_ID): + execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_incorrect_nonce(self, operator_keypair, treasury_pool, sender_with_tokens, evm_loader, session_user, + holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + new_holder_acc = create_holder(operator_keypair) + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INVALID_NONCE): + execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, new_holder_acc, + signed_tx, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_run_finalized_transaction(self, operator_keypair, treasury_pool, sender_with_tokens, evm_loader, + session_user, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.TRX_ALREADY_FINALIZED): + execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_insufficient_funds(self, operator_keypair, treasury_pool, evm_loader, session_user, + holder_acc, sender_with_tokens): + user_balance = evm_loader.get_neon_balance(session_user) + + signed_tx = make_eth_transaction(sender_with_tokens.eth_address, None, session_user, user_balance + 1) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.INSUFFICIENT_FUNDS): + execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_gas_limit_reached(self, operator_keypair, treasury_pool, session_user, evm_loader, sender_with_tokens, + holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 10, gas=1) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.OUT_OF_GAS): + execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_sender_missed_in_remaining_accounts(self, operator_keypair, treasury_pool, session_user, + sender_with_tokens, evm_loader, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): + execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, + [session_user.solana_account_address, + session_user.balance_account_address], 0) + + def test_recipient_missed_in_remaining_accounts(self, operator_keypair, treasury_pool, session_user, + sender_with_tokens, evm_loader, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ADDRESS_MUST_BE_PRESENT): + execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, + [session_user.solana_account_address, + session_user.balance_account_address], 0) + + def test_incorrect_treasure_pool(self, operator_keypair, sender_with_tokens, evm_loader, session_user, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + index = 2 + treasury = TreasuryPool(index, Keypair().generate().public_key, index.to_bytes(4, 'little')) + + error = str.format(InstructionAsserts.INVALID_ACCOUNT, treasury.account) + with pytest.raises(solana.rpc.core.RPCException, match=error): + execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury, holder_acc, + signed_tx, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_incorrect_treasure_index(self, operator_keypair, sender_with_tokens, evm_loader, + session_user, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + index = 2 + treasury = TreasuryPool(index, create_treasury_pool_address(index), (index + 1).to_bytes(4, 'little')) + + error = str.format(InstructionAsserts.INVALID_ACCOUNT, treasury.account) + with pytest.raises(solana.rpc.core.RPCException, match=error): + execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury, holder_acc, + signed_tx, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_incorrect_operator_account(self, sender_with_tokens, evm_loader, treasury_pool, session_user, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + fake_operator = Keypair().generate() + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.ACC_NOT_FOUND): + execute_transaction_steps_from_instruction(fake_operator, evm_loader, treasury_pool, holder_acc, + signed_tx, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0) + + def test_operator_is_not_in_white_list(self, sender_with_tokens, evm_loader, treasury_pool, + session_user, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.NOT_AUTHORIZED_OPERATOR): + execute_transaction_steps_from_instruction(sender_with_tokens.solana_account, evm_loader, treasury_pool, + holder_acc, + signed_tx, + [session_user.solana_account_address, + session_user.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], 0, + signer=sender_with_tokens.solana_account) + + def test_incorrect_system_program(self, sender_with_tokens, operator_keypair, evm_loader, treasury_pool, + session_user, holder_acc): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + fake_sys_program_id = Keypair().generate().public_key + + with pytest.raises(solana.rpc.core.RPCException, + match=str.format(InstructionAsserts.NOT_SYSTEM_PROGRAM, fake_sys_program_id)): + send_transaction_step_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, + [sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address, + session_user.solana_account_address, + session_user.balance_account_address], 1, operator_keypair, + system_program=fake_sys_program_id) + + def test_incorrect_holder_account(self, sender_with_tokens, operator_keypair, evm_loader, treasury_pool, + session_user): + signed_tx = make_eth_transaction(session_user.eth_address, None, sender_with_tokens, 1) + fake_holder_acc = Keypair.generate().public_key + with pytest.raises(solana.rpc.core.RPCException, + match=str.format(InstructionAsserts.NOT_PROGRAM_OWNED, fake_holder_acc)): + send_transaction_step_from_instruction(operator_keypair, evm_loader, treasury_pool, fake_holder_acc, + signed_tx, + [sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address, + session_user.solana_account_address, + session_user.balance_account_address], 1, operator_keypair) + + @pytest.mark.parametrize("value", [0, 10]) + def test_transaction_with_access_list(self, operator_keypair, treasury_pool, sender_with_tokens, + evm_loader, holder_acc, + string_setter_contract, value): + access_list = ( + { + "address": '0x' + string_setter_contract.eth_address.hex(), + "storageKeys": ( + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001", + ) + }, + ) + signed_tx = make_contract_call_trx(sender_with_tokens, string_setter_contract, "set(string)", ["text"], + value=value, access_list=access_list) + resp = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, [string_setter_contract.solana_address, + string_setter_contract.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address] + ) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value, "exit_status=0x11") + + def test_deploy_contract_with_access_list(self, operator_keypair, holder_acc, treasury_pool, evm_loader, + sender_with_tokens, neon_api_client): + contract_filename = "small" + contract = create_contract_address(sender_with_tokens, evm_loader) + + access_list = ( + { + "address": contract.eth_address.hex(), + "storageKeys": ( + "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + }, + ) + signed_tx = make_deployment_transaction(sender_with_tokens, contract_filename, access_list=access_list) + + contract_code = get_contract_bin(contract_filename) + + steps_count = neon_api_client.get_steps_count(sender_with_tokens, None, contract_code) + resp = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, [contract.solana_address, + contract.balance_account_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address], + steps_count) + check_transaction_logs_have_text(resp.value, "exit_status=0x12") + + +class TestInstructionStepContractCallContractInteractions: + def test_contract_call_unchange_storage_function(self, rw_lock_contract, session_user, evm_loader, operator_keypair, + treasury_pool, holder_acc, rw_lock_caller): + signed_tx = make_contract_call_trx(session_user, rw_lock_caller, 'unchange_storage(uint8,uint8)', [1, 1]) + resp = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, + [rw_lock_caller.solana_address, + rw_lock_contract.solana_address, + session_user.solana_account_address, + session_user.balance_account_address]) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value, "exit_status=0x12") + + def test_contract_call_set_function(self, rw_lock_contract, session_user, evm_loader, operator_keypair, + treasury_pool, holder_acc, rw_lock_caller, neon_api_client): + signed_tx = make_contract_call_trx(session_user, rw_lock_caller, 'update_storage_str(string)', ['hello']) + + resp = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, + [rw_lock_caller.solana_address, + rw_lock_contract.solana_address, + session_user.solana_account_address, + session_user.balance_account_address], 1000) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value, "exit_status=0x11") + + assert 'hello' in to_text(neon_api_client.call_contract_get_function(session_user, rw_lock_contract, + "get_text()")) + + def test_contract_call_get_function(self, rw_lock_contract, session_user, evm_loader, operator_keypair, + treasury_pool, holder_acc, rw_lock_caller): + signed_tx = make_contract_call_trx(session_user, rw_lock_caller, 'get_text()') + resp = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, + [rw_lock_caller.solana_address, + rw_lock_contract.solana_address, + session_user.solana_account_address, + session_user.balance_account_address], 1000) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value, "exit_status=0x12") + + def test_contract_call_update_storage_map_function(self, rw_lock_contract, session_user, evm_loader, + operator_keypair, rw_lock_caller, + treasury_pool, holder_acc, neon_api_client): + signed_tx = make_contract_call_trx(session_user, rw_lock_caller, 'update_storage_map(uint256)', [3]) + + func_name = abi.function_signature_to_4byte_selector('update_storage_map(uint256)') + data = func_name + eth_abi.encode(['uint256'], [3]) + result = neon_api_client.emulate(session_user.eth_address.hex(), + rw_lock_caller.eth_address.hex(), + data) + additional_accounts = [session_user.solana_account_address, + session_user.balance_account_address, + rw_lock_contract.solana_address, + rw_lock_caller.solana_address] + for acc in result['solana_accounts']: + additional_accounts.append(PublicKey(acc['pubkey'])) + + resp = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx, additional_accounts) + + check_holder_account_tag(holder_acc, FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT, TAG_FINALIZED_STATE) + check_transaction_logs_have_text(resp.value, "exit_status=0x11") + + constructor_args = eth_abi.encode(['address', 'uint256'], [rw_lock_caller.eth_address.hex(), 2]) + actual_data = neon_api_client.call_contract_get_function(session_user, rw_lock_contract, + "data(address,uint256)", constructor_args) + assert to_int(hexstr=actual_data) == 2, "Contract data is not correct" + + +class TestTransactionStepFromInstructionParallelRuns: + + def test_one_user_call_2_contracts(self, rw_lock_contract, string_setter_contract, user_account, evm_loader, + operator_keypair, treasury_pool, new_holder_acc): + signed_tx = make_contract_call_trx(user_account, rw_lock_contract, 'unchange_storage(uint8,uint8)', [1, 1]) + send_transaction_step_from_instruction(operator_keypair, evm_loader, treasury_pool, new_holder_acc, signed_tx, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address], 1, operator_keypair) + + signed_tx2 = make_contract_call_trx(user_account, string_setter_contract, 'get()') + holder_acc2 = create_holder(operator_keypair) + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.LOCKED_ACC): + send_transaction_step_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc2, signed_tx2, + [user_account.solana_account_address, + user_account.balance_account_address, + string_setter_contract.solana_address], 1, operator_keypair) + + def test_2_users_call_the_same_contract(self, rw_lock_contract, user_account, + session_user, evm_loader, operator_keypair, + treasury_pool, new_holder_acc): + signed_tx = make_contract_call_trx(user_account, rw_lock_contract, 'unchange_storage(uint8,uint8)', [1, 1]) + + send_transaction_step_from_instruction(operator_keypair, evm_loader, treasury_pool, new_holder_acc, signed_tx, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address], 1, operator_keypair) + + signed_tx2 = make_contract_call_trx(session_user, rw_lock_contract, 'get_text()') + holder_acc2 = create_holder(operator_keypair) + + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.LOCKED_ACC): + send_transaction_step_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc2, signed_tx2, + [session_user.solana_account_address, + session_user.balance_account_address, + rw_lock_contract.solana_address], 1, operator_keypair) + + def test_two_contracts_call_same_contract(self, rw_lock_contract, user_account, + session_user, evm_loader, operator_keypair, + treasury_pool, new_holder_acc): + constructor_args = eth_abi.encode(['address'], [rw_lock_contract.eth_address.hex()]) + + contract1 = deploy_contract(operator_keypair, session_user, "rw_lock", evm_loader, treasury_pool, + encoded_args=constructor_args, contract_name="rw_lock_caller") + contract2 = deploy_contract(operator_keypair, session_user, "rw_lock", evm_loader, treasury_pool, + encoded_args=constructor_args, contract_name="rw_lock_caller") + + signed_tx1 = make_contract_call_trx(user_account, contract1, 'unchange_storage(uint8,uint8)', [1, 1]) + signed_tx2 = make_contract_call_trx(session_user, contract2, 'get_text()') + + send_transaction_step_from_instruction(operator_keypair, evm_loader, treasury_pool, new_holder_acc, signed_tx1, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address, + contract1.solana_address], 1, operator_keypair) + + holder_acc2 = create_holder(operator_keypair) + with pytest.raises(solana.rpc.core.RPCException, match=InstructionAsserts.LOCKED_ACC): + send_transaction_step_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc2, signed_tx2, + [session_user.solana_account_address, + session_user.balance_account_address, + rw_lock_contract.solana_address, + contract2.solana_address], 1, + operator_keypair) + + +class TestStepFromInstructionChangingOperatorsDuringTrxRun: + def test_next_operator_can_continue_trx_after_some_time(self, rw_lock_contract, user_account, evm_loader, + operator_keypair, second_operator_keypair, treasury_pool, + new_holder_acc): + signed_tx = make_contract_call_trx(user_account, rw_lock_contract, 'update_storage_str(string)', ['text']) + + send_transaction_step_from_instruction(operator_keypair, evm_loader, treasury_pool, new_holder_acc, + signed_tx, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address], 1, operator_keypair) + # next operator can't continue trx during OPERATOR_PRIORITY_SLOTS*0.4 + with pytest.raises(solana.rpc.core.RPCException, + match=rf"{InstructionAsserts.INVALID_OPERATOR_KEY}|{InstructionAsserts.INVALID_HOLDER_OWNER}"): + send_transaction_step_from_instruction(second_operator_keypair, evm_loader, treasury_pool, new_holder_acc, + signed_tx, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address], 500, second_operator_keypair) + + time.sleep(15) + send_transaction_step_from_instruction(second_operator_keypair, evm_loader, treasury_pool, new_holder_acc, + signed_tx, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address], 500, second_operator_keypair) + resp = send_transaction_step_from_instruction(second_operator_keypair, evm_loader, treasury_pool, + new_holder_acc, signed_tx, + [user_account.solana_account_address, + user_account.balance_account_address, + rw_lock_contract.solana_address], 1, second_operator_keypair) + check_transaction_logs_have_text(resp.value, "exit_status=0x11") + + +class TestStepFromInstructionWithChangedRLPTrx: + def test_add_waste_to_trx(self, sender_with_tokens, operator_keypair, treasury_pool, evm_loader, holder_acc, + string_setter_contract): + text = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + signed_tx = make_contract_call_trx(sender_with_tokens, string_setter_contract, "set(string)", [text]) + decoded_tx = rlp.decode(signed_tx.rawTransaction) + decoded_tx.insert(6, HexBytes(b'\x19p\x16l\xc0')) + new_trx = HexBytes(rlp.encode(decoded_tx)) + + signed_tx_new = SignedTransaction( + rawTransaction=new_trx, + hash=signed_tx.hash, + r=signed_tx.r, + s=signed_tx.s, + v=signed_tx.v, + ) + with pytest.raises(RPCException, match="Program log: RLP error: RlpIncorrectListLen"): + execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx_new, + [sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address, + string_setter_contract.solana_address]) + + def test_add_waste_to_trx_without_decoding(self, sender_with_tokens, operator_keypair, treasury_pool, evm_loader, + holder_acc, + string_setter_contract): + text = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + signed_tx = make_contract_call_trx(sender_with_tokens, string_setter_contract, "set(string)", [text]) + signed_tx_new = SignedTransaction( + rawTransaction=signed_tx.rawTransaction + HexBytes(b'\x19p\x16l\xc0'), + hash=signed_tx.hash, + r=signed_tx.r, + s=signed_tx.s, + v=signed_tx.v, + ) + with pytest.raises(RPCException, match="Program log: RLP error: RlpInconsistentLengthAndData"): + execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx_new, + [sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address, + string_setter_contract.solana_address]) + + def test_old_trx_type_with_leading_zeros(self, sender_with_tokens, operator_keypair, evm_loader, + string_setter_contract, treasury_pool, holder_acc): + text = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + + signed_tx = make_contract_call_trx(sender_with_tokens, string_setter_contract, "set(string)", [text]) + new_raw_trx = HexBytes(bytes([0]) + signed_tx.rawTransaction) + + signed_tx_new = SignedTransaction( + rawTransaction=new_raw_trx, + hash=signed_tx.hash, + r=signed_tx.r, + s=signed_tx.s, + v=signed_tx.v, + ) + + resp = execute_transaction_steps_from_instruction(operator_keypair, evm_loader, treasury_pool, holder_acc, + signed_tx_new, [string_setter_contract.solana_address, + sender_with_tokens.solana_account_address, + sender_with_tokens.balance_account_address] + ) + check_transaction_logs_have_text(resp.value, "exit_status=0x11") diff --git a/integration/tests/neon_evm/types/__init__.py b/integration/tests/neon_evm/types/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration/tests/neon_evm/types/types.py b/integration/tests/neon_evm/types/types.py new file mode 100644 index 0000000000..e1001f6e58 --- /dev/null +++ b/integration/tests/neon_evm/types/types.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass +from solana.publickey import PublicKey +from solana.keypair import Keypair + + +@dataclass +class TreasuryPool: + index: int + account: PublicKey + buffer: bytes + + +@dataclass +class Caller: + solana_account: Keypair + solana_account_address: PublicKey + balance_account_address: PublicKey + eth_address: bytes + token_address: PublicKey + + +@dataclass +class Contract: + eth_address: bytes + solana_address: PublicKey + balance_account_address: PublicKey diff --git a/integration/tests/neon_evm/utils/__init__.py b/integration/tests/neon_evm/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/integration/tests/neon_evm/utils/assert_messages.py b/integration/tests/neon_evm/utils/assert_messages.py new file mode 100644 index 0000000000..43511cd81c --- /dev/null +++ b/integration/tests/neon_evm/utils/assert_messages.py @@ -0,0 +1,18 @@ +class InstructionAsserts: + LOCKED_ACC = "trying to execute transaction on rw locked account" + INVALID_CHAIN_ID = "Invalid Chain ID" + INVALID_NONCE = "Invalid Nonce" + TRX_ALREADY_FINALIZED = "Transaction already finalized" + INSUFFICIENT_FUNDS = "Insufficient balance" + OUT_OF_GAS = "Out of Gas" + ADDRESS_MUST_BE_PRESENT = r"address .* must be present in the transaction" + INVALID_ACCOUNT = "Account {} - invalid public key" + ACC_NOT_FOUND = "AccountNotFound" + NOT_AUTHORIZED_OPERATOR = "Operator is not authorized" + NOT_SYSTEM_PROGRAM = "Account {} - is not system program" + NOT_NEON_PROGRAM = "Account {} - is not Neon program" + NOT_PROGRAM_OWNED = "Account {} - invalid owner" + INVALID_HOLDER_OWNER = "Holder Account - invalid owner" + INVALID_OPERATOR_KEY = "operator.key != storage.operator" + HOLDER_OVERFLOW = "Checked Integer Math Overflow" + HOLDER_INSUFFICIENT_SIZE = "Holder Account - insufficient size" diff --git a/integration/tests/neon_evm/utils/constants.py b/integration/tests/neon_evm/utils/constants.py new file mode 100644 index 0000000000..af9c1119fc --- /dev/null +++ b/integration/tests/neon_evm/utils/constants.py @@ -0,0 +1,27 @@ +import os +from solana.publickey import PublicKey + + +SYSTEM_ADDRESS = "11111111111111111111111111111111" +TOKEN_KEG_ADDRESS = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" +SYSVAR_CLOCK_ADDRESS = "SysvarC1ock11111111111111111111111111111111" +SYS_INSTRUCT_ADDRESS = "Sysvar1nstructions1111111111111111111111111" +KECCAKPROG_ADDRESS = "KeccakSecp256k11111111111111111111111111111" +RENT_ID_ADDRESS = "SysvarRent111111111111111111111111111111111" +INCINERATOR_ADDRESS = "1nc1nerator11111111111111111111111111111111" +TREASURY_POOL_SEED = os.environ.get("NEON_TREASURY_POOL_SEED", "treasury_pool") +TREASURY_POOL_COUNT = os.environ.get("NEON_TREASURY_POOL_COUNT", 128) +COMPUTE_BUDGET_ID: PublicKey = PublicKey("ComputeBudget111111111111111111111111111111") + +ACCOUNT_SEED_VERSION = b'\3' + +TAG_EMPTY = 0 +TAG_STATE = 23 +TAG_FINALIZED_STATE = 32 +TAG_HOLDER = 52 + +SOLANA_URL = os.environ.get("SOLANA_URL", "http://solana:8899") +NEON_CORE_API_URL = os.environ.get("NEON_CORE_API_URL", "http://neon_api:8085/api") +EVM_LOADER = os.environ.get("EVM_LOADER", "53DfF883gyixYNXnM7s5xhdeyV8mVk9T4i2hGV9vG9io") +NEON_TOKEN_MINT_ID: PublicKey = PublicKey(os.environ.get("NEON_TOKEN_MINT", "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 new file mode 100644 index 0000000000..963e2c9337 --- /dev/null +++ b/integration/tests/neon_evm/utils/contract.py @@ -0,0 +1,137 @@ +import typing as tp +import pathlib + +import eth_abi +import solcx +from eth_account.datastructures import SignedTransaction +from eth_utils import abi +from solana.keypair import Keypair + +from ..types.types import Caller, Contract, TreasuryPool +from ..solana_utils import \ + EvmLoader, write_transaction_to_holder_account, \ + send_transaction_step_from_account +from .storage import create_holder +from .ethereum import create_contract_address, make_eth_transaction + +from web3.auto import w3 + + +def get_contract_bin( + contract: str, + contract_name: tp.Optional[str] = None, +): + version = '0.7.6' + if not contract.endswith(".sol"): + contract += ".sol" + if contract_name is None: + if "/" in contract: + contract_name = contract.rsplit("/", 1)[1].rsplit(".", 1)[0] + else: + contract_name = contract.rsplit(".", 1)[0] + + if version not in [str(v) for v in solcx.get_installed_solc_versions()]: + solcx.install_solc(version) + + contract_path = (pathlib.Path.cwd() / "contracts" / "neon_evm" / f"{contract}").absolute() + if not contract_path.exists(): + contract_path = (pathlib.Path.cwd() / "contracts" / "external" / f"{contract}").absolute() + + assert contract_path.exists(), f"Can't found contract: {contract_path}" + + compiled = solcx.compile_files( + [contract_path], + output_values=["abi", "bin"], + solc_version=version, + allow_paths=["."], + optimize=True, + ) + contract_abi = None + for key in compiled.keys(): + if contract_name == key.rsplit(":")[-1]: + contract_abi = compiled[key] + break + + return contract_abi['bin'] + + +def make_deployment_transaction( + user: Caller, + contract_file_name: tp.Union[pathlib.Path, str], + contract_name: tp.Optional[str] = None, + encoded_args=None, + gas: int = 999999999, chain_id=111, access_list=None +) -> SignedTransaction: + data = get_contract_bin(contract_file_name, contract_name) + if encoded_args is not None: + data = data + encoded_args.hex() + + nonce = EvmLoader(user.solana_account).get_neon_nonce(user.eth_address) + tx = { + 'to': None, + 'value': 0, + 'gas': gas, + 'gasPrice': 0, + 'nonce': nonce, + 'data': data + } + if chain_id: + tx['chainId'] = chain_id + if access_list: + tx['accessList'] = access_list + tx['type'] = 1 + + return w3.eth.account.sign_transaction(tx, user.solana_account.secret_key[:32]) + + +def make_contract_call_trx(user, contract, function_signature, params=None, value=0, chain_id=111, access_list=None, + trx_type=None): + data = abi.function_signature_to_4byte_selector(function_signature) + + if params is not None: + for param in params: + if isinstance(param, int): + data += eth_abi.encode(['uint256'], [param]) + elif isinstance(param, str): + data += eth_abi.encode(['string'], [param]) + + signed_tx = make_eth_transaction(contract.eth_address, data, user, value=value, + chain_id=chain_id, access_list=access_list, type=trx_type) + return signed_tx + + +def deploy_contract( + operator: Keypair, + user: Caller, + contract_file_name: tp.Union[pathlib.Path, str], + evm_loader: EvmLoader, + treasury_pool: TreasuryPool, + step_count: int = 1000, + encoded_args=None, + contract_name: tp.Optional[str] = None +): + print("Deploying contract") + contract: Contract = create_contract_address(user, evm_loader) + holder_acc = create_holder(operator) + signed_tx = make_deployment_transaction(user, contract_file_name, contract_name, encoded_args=encoded_args) + write_transaction_to_holder_account(signed_tx, holder_acc, operator) + + index = 0 + contract_deployed = False + while not contract_deployed: + receipt = send_transaction_step_from_account(operator, evm_loader, treasury_pool, holder_acc, + [contract.solana_address, + contract.balance_account_address, + user.balance_account_address], + step_count, operator, index=index) + index += 1 + + if receipt.value.transaction.meta.err: + raise AssertionError(f"Can't deploy contract: {receipt.value.transaction.meta.err}") + for log in receipt.value.transaction.meta.log_messages: + if "exit_status" in log: + contract_deployed = True + break + if "ExitError" in log: + raise AssertionError(f"EVM Return error in logs: {receipt}") + return contract diff --git a/integration/tests/neon_evm/utils/eth_tx_utils.py b/integration/tests/neon_evm/utils/eth_tx_utils.py new file mode 100644 index 0000000000..5c32846ec7 --- /dev/null +++ b/integration/tests/neon_evm/utils/eth_tx_utils.py @@ -0,0 +1,225 @@ +from Crypto.Hash import keccak +import json +from web3.auto import w3 +from eth_keys import keys +import struct + + +def unpack(data): + ch = data[0] + if ch <= 0x7F: + return ch, data[1:] + elif ch == 0x80: + return None, data[1:] + elif ch <= 0xB7: + l = ch - 0x80 + return data[1:1 + l].tobytes(), data[1 + l:] + elif ch <= 0xBF: + lLen = ch - 0xB7 + l = int.from_bytes(data[1:1 + lLen], byteorder='big') + return data[1 + lLen:1 + lLen + l].tobytes(), data[1 + lLen + l:] + elif ch == 0xC0: + return (), data[1:] + elif ch <= 0xF7: + l = ch - 0xC0 + lst = list() + sub = data[1:1 + l] + while len(sub): + (item, sub) = unpack(sub) + lst.append(item) + return lst, data[1 + l:] + else: + lLen = ch - 0xF7 + l = int.from_bytes(data[1:1 + lLen], byteorder='big') + lst = list() + sub = data[1 + lLen:1 + lLen + l] + while len(sub): + (item, sub) = unpack(sub) + lst.append(item) + return lst, data[1 + lLen + l:] + + +def pack(data): + if data is None: + return (0x80).to_bytes(1, 'big') + if isinstance(data, str): + return pack(data.encode('utf8')) + elif isinstance(data, bytes): + if len(data) <= 55: + return (len(data) + 0x80).to_bytes(1, 'big') + data + else: + l = len(data) + lLen = (l.bit_length() + 7) // 8 + return (0xB7 + lLen).to_bytes(1, 'big') + l.to_bytes(lLen, 'big') + data + elif isinstance(data, int): + if data < 0x80: + return data.to_bytes(1, 'big') + else: + l = (data.bit_length() + 7) // 8 + return (l + 0x80).to_bytes(1, 'big') + data.to_bytes(l, 'big') + pass + elif isinstance(data, list) or isinstance(data, tuple): + if len(data) == 0: + return (0xC0).to_bytes(1, 'big') + else: + res = bytearray() + for d in data: + res += pack(d) + l = len(res) + if l <= 55: + return (l + 0xC0).to_bytes(1, 'big') + res + else: + lLen = (l.bit_length() + 7) // 8 + return (lLen + 0xF7).to_bytes(1, 'big') + l.to_bytes(lLen, 'big') + res + else: + raise Exception("Unknown type {} of data".format(str(type(data)))) + + +def get_int(a): + if isinstance(a, int): + return a + if isinstance(a, bytes): + return int.from_bytes(a, 'big') + if a is None: + return a + raise Exception("Invalid convertion from {} to int".format(a)) + + +class Trx: + def __init__(self): + self.nonce = None + self.gasPrice = None + self.gasLimit = None + self.toAddress = None + self.value = None + self.callData = None + self.v = None + self.r = None + self.s = None + + @classmethod + def from_string(cls, s): + t = Trx() + (unpacked, data) = unpack(memoryview(s)) + (nonce, gasPrice, gasLimit, toAddress, value, callData, v, r, s) = unpacked + t.nonce = get_int(nonce) + t.gasPrice = get_int(gasPrice) + t.gasLimit = get_int(gasLimit) + t.toAddress = toAddress + t.value = get_int(value) + t.callData = callData + t.v = get_int(v) + t.r = get_int(r) + t.s = get_int(s) + return t + + def chain_id(self): + # chainid*2 + 35 xxxxx0 + 100011 xxxx0 + 100010 +1 + # chainid*2 + 36 xxxxx0 + 100100 xxxx0 + 100011 +1 + return (self.v - 1) // 2 - 17 + + def __str__(self): + return pack(( + self.nonce, + self.gasPrice, + self.gasLimit, + self.toAddress, + self.value, + self.callData, + self.v, + self.r.to_bytes(32, 'big') if self.r else None, + self.s.to_bytes(32, 'big') if self.s else None) + ).hex() + + def get_msg(self, chain_id=None): + return pack(( + self.nonce, + self.gasPrice, + self.gasLimit, + self.toAddress, + self.value, + self.callData, + chain_id or self.chain_id(), None, None)) + + def hash(self, chain_id=None): + trx = pack(( + self.nonce, + self.gasPrice, + self.gasLimit, + self.toAddress, + self.value, + self.callData, + chain_id or self.chain_id(), None, None)) + return keccak.new(digest_bits=256).update(trx).digest() + + def sender(self): + msg_hash = self.hash() + sig = keys.Signature(vrs=[1 if self.v % 2 == 0 else 0, self.r, self.s]) + pub = sig.recover_public_key_from_msg_hash(msg_hash) + return pub.to_canonical_address().hex() + + +class JsonEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, bytes): + return obj.hex() + return json.JSONEncoder.default(obj) + + +def make_instruction_data_from_tx(instruction, private_key=None): + if isinstance(instruction, dict): + if instruction['chainId'] is None: + raise Exception("chainId value is needed in input dict") + if private_key is None: + raise Exception("Needed private key for transaction creation from fields") + + signed_tx = w3.eth.account.sign_transaction(instruction, private_key) + _trx = Trx.from_string(signed_tx.rawTransaction) + + raw_msg = _trx.get_msg(instruction['chainId']) + sig = keys.Signature(vrs=[1 if _trx.v % 2 == 0 else 0, _trx.r, _trx.s]) + pub = sig.recover_public_key_from_msg_hash(_trx.hash()) + + + return pub.to_canonical_address(), sig.to_bytes(), raw_msg + elif isinstance(instruction, str): + if instruction[:2] == "0x": + instruction = instruction[2:] + + _trx = Trx.from_string(bytearray.fromhex(instruction)) + # print(json.dumps(_trx.__dict__, cls=JsonEncoder, indent=3)) + + raw_msg = _trx.get_msg() + sig = keys.Signature(vrs=[1 if _trx.v % 2 == 0 else 0, _trx.r, _trx.s]) + pub = sig.recover_public_key_from_msg_hash(_trx.hash()) + + data = pub.to_canonical_address() + data += sig.to_bytes() + data += raw_msg + + return pub.to_canonical_address(), sig.to_bytes(), raw_msg + else: + raise Exception("function gets ") + + +def make_keccak_instruction_data(check_instruction_index, msg_len, data_start): + if 255 < check_instruction_index < 0: + raise Exception("Invalid index for instruction - {}".format(check_instruction_index)) + + check_count = 1 + eth_address_size = 20 + signature_size = 65 + eth_address_offset = data_start + signature_offset = eth_address_offset + eth_address_size + message_data_offset = signature_offset + signature_size + + data = struct.pack("B", check_count) + data += struct.pack(" 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_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}") + + return Contract(contract_eth_address, PublicKey(contract_solana_address), contract_neon_address) + + +def make_eth_transaction(to_addr: bytes, data: Union[bytes, None], caller: Caller, + value: int = 0, chain_id=CHAIN_ID, gas=9999999999, access_list=None, type=None): + nonce = EvmLoader(caller.solana_account).get_neon_nonce(caller.eth_address) + tx = {'to': to_addr, 'value': value, 'gas': gas, 'gasPrice': 0, + 'nonce': nonce} + + if chain_id is not None: + tx['chainId'] = chain_id + + if data is not None: + 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]) diff --git a/integration/tests/neon_evm/utils/instructions.py b/integration/tests/neon_evm/utils/instructions.py new file mode 100644 index 0000000000..982192843c --- /dev/null +++ b/integration/tests/neon_evm/utils/instructions.py @@ -0,0 +1,244 @@ +import typing as tp + +from eth_keys import keys as eth_keys +from solana.keypair import Keypair +from solana.publickey import PublicKey +import solana.system_program as sp +from solana.transaction import AccountMeta, TransactionInstruction, Transaction + +from .constants import EVM_LOADER +from solana.system_program import SYS_PROGRAM_ID +from solana.sysvar 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 + +from ..types.types import TreasuryPool + +DEFAULT_UNITS = 1_400_000 +DEFAULT_HEAP_FRAME = 256 * 1024 +DEFAULT_ADDITIONAL_FEE = 0 +COMPUTE_BUDGET_ID: PublicKey = PublicKey("ComputeBudget111111111111111111111111111111") + + +class ComputeBudget: + @staticmethod + def request_units(operator, units, additional_fee): + return TransactionInstruction( + 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") + ) + + @staticmethod + def request_heap_frame(operator, heap_frame): + return TransactionInstruction( + 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") + ) + + +class TransactionWithComputeBudget(Transaction): + def __init__(self, + operator: Keypair, + units=DEFAULT_UNITS, + additional_fee=DEFAULT_ADDITIONAL_FEE, + heap_frame=DEFAULT_HEAP_FRAME, + *args, **kwargs): + super().__init__(*args, **kwargs) + if units: + self.add(ComputeBudget.request_units(operator, units, additional_fee)) + if heap_frame: + self.add(ComputeBudget.request_heap_frame(operator, heap_frame)) + + +def write_holder_layout(hash: bytes, offset: int, data: bytes): + assert (len(hash) == 32) + return ( + bytes([0x26]) + + hash + + offset.to_bytes(8, byteorder="little") + + data + ) + + +def make_WriteHolder(operator: PublicKey, holder_account: PublicKey, hash: bytes, offset: int, payload: bytes): + d = write_holder_layout(hash, offset, payload) + + return TransactionInstruction( + program_id=PublicKey(EVM_LOADER), + data=d, + keys=[ + AccountMeta(pubkey=holder_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=operator, is_signer=True, is_writable=False), + ]) + + +def make_ExecuteTrxFromInstruction( + operator: Keypair, + evm_loader: "EvmLoader", + treasury_address: PublicKey, + treasury_buffer: bytes, + message: bytes, + additional_accounts: tp.List[PublicKey], + system_program=sp.SYS_PROGRAM_ID, +): + data = bytes([0x32]) + treasury_buffer + message + operator_ether = eth_keys.PrivateKey(operator.secret_key[:32]).public_key.to_canonical_address() + print("make_ExecuteTrxFromInstruction accounts") + print("Operator: ", operator.public_key) + print("Treasury: ", treasury_address) + print("Operator ether: ", operator_ether.hex()) + print("Operator eth solana: ", evm_loader.ether2balance(operator_ether)) + accounts = [ + AccountMeta(pubkey=operator.public_key, is_signer=True, is_writable=True), + AccountMeta(pubkey=treasury_address, is_signer=False, is_writable=True), + AccountMeta(pubkey=PublicKey(evm_loader.ether2balance(operator_ether)), is_signer=False, is_writable=True), + AccountMeta(system_program, is_signer=False, is_writable=True), + ] + for acc in additional_accounts: + print("Additional acc ", acc) + accounts.append(AccountMeta(acc, is_signer=False, is_writable=True), ) + + return TransactionInstruction( + program_id=PublicKey(EVM_LOADER), + data=data, + keys=accounts + ) + + +def make_ExecuteTrxFromAccountDataIterativeOrContinue( + index: int, + step_count: int, + operator: Keypair, + evm_loader: "EvmLoader", + holder_address: PublicKey, + treasury: TreasuryPool, + additional_accounts: tp.List[PublicKey], + sys_program_id=sp.SYS_PROGRAM_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") + operator_ether = eth_keys.PrivateKey(operator.secret_key[:32]).public_key.to_canonical_address() + print("make_ExecuteTrxFromAccountDataIterativeOrContinue accounts") + print("Holder: ", holder_address) + print("Operator: ", operator.public_key) + print("Treasury: ", treasury.account) + print("Operator ether: ", operator_ether.hex()) + print("Operator eth solana: ", evm_loader.ether2balance(operator_ether)) + accounts = [ + AccountMeta(pubkey=holder_address, is_signer=False, is_writable=True), + AccountMeta(pubkey=operator.public_key, is_signer=True, is_writable=True), + AccountMeta(pubkey=treasury.account, is_signer=False, is_writable=True), + AccountMeta(pubkey=PublicKey(evm_loader.ether2balance(operator_ether)), is_signer=False, is_writable=True), + AccountMeta(sys_program_id, is_signer=False, is_writable=True), + ] + + for acc in additional_accounts: + print("Additional acc ", acc) + accounts.append(AccountMeta(acc, is_signer=False, is_writable=True), ) + + return TransactionInstruction( + program_id=PublicKey(EVM_LOADER), + data=data, + keys=accounts + ) + + +def make_PartialCallOrContinueFromRawEthereumTX( + index: int, + step_count: int, + instruction: bytes, + operator: Keypair, + evm_loader: "EvmLoader", + storage_address: PublicKey, + treasury: TreasuryPool, + additional_accounts: tp.List[PublicKey], + system_program=sp.SYS_PROGRAM_ID): + data = bytes([0x34]) + treasury.buffer + step_count.to_bytes(4, "little") + index.to_bytes(4, "little") + instruction + operator_ether = eth_keys.PrivateKey(operator.secret_key[:32]).public_key.to_canonical_address() + + accounts = [ + AccountMeta(pubkey=storage_address, is_signer=False, is_writable=True), + AccountMeta(pubkey=operator.public_key, is_signer=True, is_writable=True), + AccountMeta(pubkey=treasury.account, is_signer=False, is_writable=True), + AccountMeta(pubkey=evm_loader.ether2balance(operator_ether), 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=PublicKey(EVM_LOADER), + data=data, + keys=accounts + ) + + +def make_Cancel(evm_loader: "EvmLoader", storage_address: PublicKey, operator: Keypair, hash: bytes, additional_accounts: tp.List[PublicKey]): + data = bytes([0x37]) + hash + operator_ether = eth_keys.PrivateKey(operator.secret_key[:32]).public_key.to_canonical_address() + + accounts = [ + AccountMeta(pubkey=storage_address, is_signer=False, is_writable=True), + AccountMeta(pubkey=operator.public_key, is_signer=True, is_writable=True), + AccountMeta(pubkey=evm_loader.ether2balance(operator_ether), 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=PublicKey(EVM_LOADER), + data=data, + keys=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, +) -> TransactionInstruction: + data = bytes([0x31]) + ether_address + chain_id.to_bytes(8, 'little') + + accounts = [ + AccountMeta(pubkey=mint, is_signer=False, is_writable=True), + AccountMeta(pubkey=source, is_signer=False, is_writable=True), + AccountMeta(pubkey=pool, is_signer=False, is_writable=True), + AccountMeta(pubkey=balance_account, is_signer=False, is_writable=True), + 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), + ] + + return TransactionInstruction(program_id=PublicKey(EVM_LOADER), data=data, keys=accounts) + +def make_CreateAssociatedTokenIdempotent(payer: PublicKey, owner: PublicKey, mint: PublicKey) -> TransactionInstruction: + """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( + data=bytes([1]), + keys=[ + 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), + ], + program_id=ASSOCIATED_TOKEN_PROGRAM_ID, + ) \ No newline at end of file diff --git a/integration/tests/neon_evm/utils/layouts.py b/integration/tests/neon_evm/utils/layouts.py new file mode 100644 index 0000000000..461358bd0b --- /dev/null +++ b/integration/tests/neon_evm/utils/layouts.py @@ -0,0 +1,50 @@ +from construct import Bytes, Int8ul, Struct, Int64ul, Int32ul + +STORAGE_ACCOUNT_INFO_LAYOUT = Struct( + "tag" / Int8ul, + "blocked" / Int8ul, + "owner" / Bytes(32), + "hash" / Bytes(32), + "caller" / Bytes(20), + "chain_id" / Int64ul, + "gas_limit" / Bytes(32), + "gas_price" / Bytes(32), + "gas_used" / Bytes(32), + "operator" / Bytes(32), + "slot" / Int64ul, + "account_list_len" / Int64ul, +) + +HOLDER_ACCOUNT_INFO_LAYOUT = Struct( + "tag" / Int8ul, + "blocked" / Int8ul, + "owner" / Bytes(32), + "hash" / Bytes(32), + "len" / Int64ul +) + + +FINALIZED_STORAGE_ACCOUNT_INFO_LAYOUT = Struct( + "tag" / Int8ul, + "blocked" / Int8ul, + "owner" / Bytes(32), + "hash" / Bytes(32), +) + + +CONTRACT_ACCOUNT_LAYOUT = Struct( + "type" / Int8ul, + "blocked" / Int8ul, + "address" / Bytes(20), + "chain_id" / Int64ul, + "generation" / Int32ul, +) + +BALANCE_ACCOUNT_LAYOUT = Struct( + "type" / Int8ul, + "blocked" / Int8ul, + "address" / Bytes(20), + "chain_id" / Int64ul, + "trx_count" / Int64ul, + "balance" / Bytes(32), +) \ No newline at end of file diff --git a/integration/tests/neon_evm/utils/neon_api_client.py b/integration/tests/neon_evm/utils/neon_api_client.py new file mode 100644 index 0000000000..716c117b1d --- /dev/null +++ b/integration/tests/neon_evm/utils/neon_api_client.py @@ -0,0 +1,68 @@ +import requests +from eth_utils import abi + +from .constants import CHAIN_ID +from ..types.types import Caller, Contract + + +class NeonApiClient: + def __init__(self, url): + self.url = url + self.headers = {"Content-Type": "application/json"} + + def emulate(self, sender, contract, data=bytes(), chain_id=CHAIN_ID, value='0x0', max_steps_to_execute=500000): + if isinstance(data, bytes): + data = data.hex() + body = { + "step_limit": max_steps_to_execute, + "tx": { + "from": sender, + "to": contract, + "data": data, + "chain_id": chain_id, + "value": value + }, + "accounts": [] + } + print(body) + resp = requests.post(url=f"{self.url}/emulate", json=body, headers=self.headers) + print(resp.text) + if resp.status_code == 200: + return resp.json()["value"] + else: + return resp.json() + + + def get_storage_at(self, contract_id, index="0x0"): + body = { + "contract": contract_id, + "index": index + } + return requests.post(url=f"{self.url}/storage", json=body, headers=self.headers).json() + + def get_ether_account_data(self, ether, chain_id = CHAIN_ID): + body = { + "account": [ + { "address": ether, "chain_id": chain_id } + ] + } + return requests.post(url=f"{self.url}/balance", json=body, headers=self.headers).json() + + def call_contract_get_function(self, sender, contract, function_signature: str, + constructor_args=None): + data = abi.function_signature_to_4byte_selector(function_signature) + if constructor_args is not None: + data += constructor_args + result = self.emulate(sender.eth_address.hex(), contract.eth_address.hex(), data) + print(result) + return result["result"] + + def get_steps_count(self, from_acc, to, data): + if isinstance(to, (Caller, Contract)): + to = to.eth_address.hex() + result = self.emulate( + from_acc.eth_address.hex(), + to, + data + ) + return result["steps_executed"] diff --git a/integration/tests/neon_evm/utils/storage.py b/integration/tests/neon_evm/utils/storage.py new file mode 100644 index 0000000000..4876d459bb --- /dev/null +++ b/integration/tests/neon_evm/utils/storage.py @@ -0,0 +1,47 @@ +from hashlib import sha256 +from random import randrange + +from solana.publickey import PublicKey +from solana.keypair import Keypair +from ..solana_utils import create_holder_account, get_solana_balance, create_account_with_seed, \ + send_transaction, solana_client +from solana.transaction import Transaction, TransactionInstruction, AccountMeta +from .constants import EVM_LOADER + + +def create_holder(signer: Keypair, seed: str = None, size: int = None, fund: int = None, + storage: PublicKey = None) -> PublicKey: + if size is None: + size = 128 * 1024 + if fund is None: + fund = 10 ** 9 + if seed is None: + seed = str(randrange(1000000)) + if storage is None: + storage = PublicKey( + sha256(bytes(signer.public_key) + bytes(seed, 'utf8') + bytes(PublicKey(EVM_LOADER))).digest()) + + print(f"Create holder account with seed: {seed}") + + if get_solana_balance(storage) == 0: + trx = Transaction() + trx.add( + create_account_with_seed(signer.public_key, signer.public_key, seed, fund, size), + create_holder_account(storage, signer.public_key, bytes(seed, 'utf8')) + ) + send_transaction(solana_client, trx, signer) + print(f"Created holder account: {storage}") + return storage + + +def delete_holder(del_key: PublicKey, acc: Keypair, signer: Keypair): + trx = Transaction() + + trx.add(TransactionInstruction( + program_id=PublicKey(EVM_LOADER), + data=bytes.fromhex("25"), + keys=[ + AccountMeta(pubkey=del_key, is_signer=False, is_writable=True), + AccountMeta(pubkey=acc.public_key, is_signer=(signer == acc), is_writable=True), + ])) + return send_transaction(solana_client, trx, signer) diff --git a/integration/tests/neon_evm/utils/transaction_checks.py b/integration/tests/neon_evm/utils/transaction_checks.py new file mode 100644 index 0000000000..60e2b8f9c4 --- /dev/null +++ b/integration/tests/neon_evm/utils/transaction_checks.py @@ -0,0 +1,28 @@ +import base64 + +from solana.rpc.commitment import Confirmed + +from ..solana_utils import solana_client + + +def check_transaction_logs_have_text(trx_hash, text): + + receipt = solana_client.get_transaction(trx_hash) + logs = "" + for log in receipt.value.transaction.meta.log_messages: + if "Program data:" in log: + logs += "Program data: " + encoded_part = log.replace("Program data: ", "") + for item in encoded_part.split(" "): + logs += " " + str(base64.b64decode(item)) + else: + logs += log + logs += " " + assert text in logs, f"Transaction logs don't contain '{text}'. Logs: {logs}" + + +def check_holder_account_tag(storage_account, layout, expected_tag): + account_data = solana_client.get_account_info(storage_account, commitment=Confirmed).value.data + parsed_data = layout.parse(account_data) + assert parsed_data.tag == expected_tag, f"Account tag {account_data[0]} != expected {expected_tag}" + diff --git a/integration/tests/tracer/schemas/debug_traceCall.json b/integration/tests/tracer/schemas/debug_traceCall.json index b204a461dd..b8841578e3 100644 --- a/integration/tests/tracer/schemas/debug_traceCall.json +++ b/integration/tests/tracer/schemas/debug_traceCall.json @@ -46,7 +46,8 @@ "type": [ "string", "null" - ] + ], + "items": {} }, "stack": { "type": [ @@ -80,9 +81,7 @@ "gas", "gasCost", "op", - "pc", - "returnData", - "stack" + "pc" ] } ] @@ -94,4 +93,4 @@ "returnValue", "structLogs" ] -} \ 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 9f830c4061..8592c07e1a 100644 --- a/integration/tests/tracer/test_tracer_debug_methods.py +++ b/integration/tests/tracer/test_tracer_debug_methods.py @@ -1,8 +1,11 @@ import json import pathlib import random +import re from jsonschema import Draft4Validator +from rlp import decode +from rlp.sedes import List, big_endian_int, binary import allure import pytest @@ -254,4 +257,183 @@ def test_debug_trace_block_by_non_existent_hash(self): method="debug_traceBlockByHash", params=['0xd97ff4869d52c4add6f5bcb1ba96020dd7877244b4cbf49044f49f002015ea85']) assert "error" in response, "No errors in response" assert response["error"]["code"] == -32603, "Invalid error code" - assert response["error"]["message"] == "eth_getBlockByHash returns None for '\"0xd97ff4869d52c4add6f5bcb1ba96020dd7877244b4cbf49044f49f002015ea85\"' block" \ No newline at end of file + assert response["error"]["message"] == "eth_getBlockByHash returns None for '\"0xd97ff4869d52c4add6f5bcb1ba96020dd7877244b4cbf49044f49f002015ea85\"' block" + + def decode_raw_header(self, header: bytes): + sedes = List([big_endian_int, binary, binary, binary, binary]) + return decode(header, sedes) + + def test_getRawHeader_by_block_number(self): + receipt = self.send_neon( + self.sender_account, self.recipient_account, 0.1) + assert receipt["status"] == 1 + wait_condition(lambda: self.tracer_api.send_rpc(method="debug_getRawHeader", + params=[hex(receipt["blockNumber"])])["result"] is not None, + timeout_sec=120) + + response = self.tracer_api.send_rpc( + method="debug_getRawHeader", params=[hex(receipt["blockNumber"])]) + assert "error" not in response, "Error in response" + assert 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"] + assert header[1].hex() == '' + assert header[2].hex() == block_info["parentHash"].hex()[2:] + assert header[3].hex() == block_info["stateRoot"].hex()[2:] + assert header[4].hex() == block_info["receiptsRoot"].hex()[2:] + + + def test_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" + + def test_getRawHeader_by_block_hash(self): + receipt = self.send_neon( + self.sender_account, self.recipient_account, 0.1) + assert receipt["status"] == 1 + wait_condition(lambda: self.tracer_api.send_rpc(method="debug_getRawHeader", + params=[receipt["blockHash"].hex()])["result"] is not None, + timeout_sec=120) + + response = self.tracer_api.send_rpc( + method="debug_getRawHeader", params=[receipt["blockHash"].hex()]) + assert "error" not in response, "Error in response" + assert 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"] + assert header[1].hex() == '' + assert header[2].hex() == block_info["parentHash"].hex()[2:] + assert header[3].hex() == block_info["stateRoot"].hex()[2:] + assert header[4].hex() == block_info["receiptsRoot"].hex()[2:] + + def test_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" + assert response["error"]["code"] == -32602, "Invalid error code" + assert response["error"]["message"] == "Invalid params" + + def check_modified_accounts_response(self, response): + assert "error" not in response, "Error in response" + assert response["result"] is not None and response["result"] != [] + assert isinstance(response["result"], list) + assert self.sender_account.address in response["result"] + for item in response["result"]: + assert re.match('^0x[a-fA-F\d]{64}$', item) + + @pytest.mark.skip(reason="bug NDEV-2375") + def test_debug_get_modified_accounts_by_number(self): + receipt_start = self.send_neon( + self.sender_account, self.recipient_account, 0.1) + receipt_end = self.send_neon( + self.sender_account, self.recipient_account, 0.1) + assert receipt_start["status"] == 1 + assert receipt_end["status"] == 1 + + wait_condition(lambda: self.tracer_api.send_rpc(method="debug_getModifiedAccountsByNumber", + params=[hex(receipt_start["blockNumber"]), hex(receipt_end["blockNumber"])])["result"] is not None, + timeout_sec=120) + + response = self.tracer_api.send_rpc( + method="debug_getModifiedAccountsByNumber", params=[hex(receipt_start["blockNumber"]), hex(receipt_end["blockNumber"])]) + self.check_modified_accounts_response(response) + + @pytest.mark.skip(reason="bug NDEV-2375") + def test_debug_get_modified_accounts_by_same_number(self): + receipt = self.send_neon( + self.sender_account, self.recipient_account, 0.1) + assert receipt["status"] == 1 + + wait_condition(lambda: self.tracer_api.send_rpc(method="debug_getModifiedAccountsByNumber", + params=[hex(receipt["blockNumber"]), hex(receipt["blockNumber"])])["result"] is not None, + timeout_sec=120) + + response = self.tracer_api.send_rpc( + method="debug_getModifiedAccountsByNumber", params=[hex(receipt["blockNumber"]), hex(receipt["blockNumber"])]) + self.check_modified_accounts_response(response) + + @pytest.mark.skip(reason="bug NDEV-2375") + def test_debug_get_modified_accounts_by_only_one_number(self): + receipt = self.send_neon( + self.sender_account, self.recipient_account, 0.1) + assert receipt["status"] == 1 + + wait_condition(lambda: self.tracer_api.send_rpc(method="debug_getModifiedAccountsByNumber", + params=[hex(receipt["blockNumber"])])["result"] is not None, + timeout_sec=120) + + response = self.tracer_api.send_rpc( + method="debug_getModifiedAccountsByNumber", params=[hex(receipt["blockNumber"])]) + self.check_modified_accounts_response(response) + + @pytest.mark.skip(reason="bug NDEV-2375") + @pytest.mark.parametrize("difference", [1, 50, 199, 200]) + def test_debug_get_modified_accounts_by_number_blocks_difference_less_or_equal_200(self, difference): + receipt = self.send_neon( + self.sender_account, self.recipient_account, 0.1) + assert receipt["status"] == 1 + start_number = hex(receipt["blockNumber"] - difference) + end_number= hex(receipt["blockNumber"]) + wait_condition(lambda: self.tracer_api.send_rpc(method="debug_getModifiedAccountsByNumber", + params=[start_number, end_number])["result"] is not None, + timeout_sec=120) + + response = self.tracer_api.send_rpc( + method="debug_getModifiedAccountsByNumber", params=[start_number, end_number]) + self.check_modified_accounts_response(response) + + @pytest.mark.skip(reason="bug NDEV-2375") + def test_debug_get_modified_accounts_by_number_201_blocks_difference(self): + receipt = self.send_neon( + self.sender_account, self.recipient_account, 0.1) + assert receipt["status"] == 1 + start_number = hex(receipt["blockNumber"] - 201) + end_number= hex(receipt["blockNumber"]) + + response = self.tracer_api.send_rpc( + method="debug_getModifiedAccountsByNumber", params=[start_number, end_number]) + assert "error" in response, "No errors in response" + assert response["error"]["code"] == -32603, "Invalid error code" + assert response["error"]["message"] == "Requested range (201) is too big, maximum allowed range is 200 blocks" + + @pytest.mark.skip(reason="bug NDEV-2375") + @pytest.mark.parametrize("params", [[1, 124], ["94f3e", 12], ["1a456", "0x0"], ["183b8e", "183b8e"]]) + 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" + + @pytest.mark.skip(reason="bug NDEV-2375") + def test_debug_get_modified_accounts_by_hash(self): + receipt_start = self.send_neon( + self.sender_account, self.recipient_account, 0.1) + receipt_end = self.send_neon( + self.sender_account, self.recipient_account, 0.1) + assert receipt_start["status"] == 1 + assert receipt_end["status"] == 1 + + wait_condition(lambda: self.tracer_api.send_rpc(method="debug_getModifiedAccountsByHash", + params=[receipt_start["blockHash"].hex(), receipt_end["blockHash"].hex()])["result"] is not None, + timeout_sec=120) + + response = self.tracer_api.send_rpc( + method="debug_getModifiedAccountsByHash", params=[receipt_start["blockHash"].hex(), receipt_end["blockHash"].hex()]) + self.check_modified_accounts_response(response) + + @pytest.mark.skip(reason="bug NDEV-2375") + @pytest.mark.parametrize("params", [[1, 124], ["0x94f3e00000000800000000", 12], ["0x1a456", "0x000000000001"], ["0x183b8e", "183b8e"]]) + 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" + assert response["error"]["code"] == -32602, "Invalid error code" + assert response["error"]["message"] == "Invalid params" diff --git a/loadtesting/proxy/common/events.py b/loadtesting/proxy/common/events.py index f7606d04a9..57c289461f 100644 --- a/loadtesting/proxy/common/events.py +++ b/loadtesting/proxy/common/events.py @@ -28,7 +28,7 @@ def get_token_balance(op: operator.Operator) -> tp.Dict: """Return tokens balance""" - return dict(neon=op.get_neon_balance(), sol=op.get_solana_balance()) + return dict(neon=op.get_token_balance(), sol=op.get_solana_balance()) def execute_before(*attrs) -> tp.Callable: diff --git a/operator-keypair.json b/operator-keypair.json new file mode 100644 index 0000000000..54e7bbcc6b --- /dev/null +++ b/operator-keypair.json @@ -0,0 +1 @@ +[161,247,66,57,203,188,141,236,124,123,200,192,255,23,161,34,116,202,70,182,176,94,195,68,185,32,61,42,203,57,245,190,153,233,189,58,187,209,15,232,83,112,145,116,89,187,201,44,36,255,81,102,84,101,62,1,180,217,194,37,147,74,7,170] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7e751e804a..7cd2b264a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,8 @@ profile="black" timeout = 300 markers = [ "only_stands: this tests works only on test stands", - "only_devnet: this tests works only on devnet env" + "only_devnet: this tests works only on devnet env", + "multipletokens : this tests works only on stands with several chains" ] asyncio_mode = "auto" addopts = "--alluredir=allure-results" diff --git a/utils/consts.py b/utils/consts.py index 6a823ba170..7aca95cb02 100644 --- a/utils/consts.py +++ b/utils/consts.py @@ -1,5 +1,6 @@ from enum import Enum +from solana.publickey import PublicKey LAMPORT_PER_SOL = 1_000_000_000 ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" @@ -7,6 +8,7 @@ INITIAL_ACCOUNT_AMOUNT = 100 MAX_UINT_256 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + class Unit(Enum): WEI = "wei" KWEI = "kwei" @@ -32,3 +34,14 @@ def get_transfer_amount(self) -> float: def get_default_initial_amount(self) -> int: return self.FAUCET_1ST_REQUEST_AMOUNT.value + + +wSOL = { + "chain_id": 111, + "address_spl": PublicKey("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", +} diff --git a/utils/erc20wrapper.py b/utils/erc20wrapper.py index 7a21cf91f0..e26851c9ed 100644 --- a/utils/erc20wrapper.py +++ b/utils/erc20wrapper.py @@ -25,6 +25,7 @@ def __init__( evm_loader_id=None, account=None, mintable=True, + contract_address=None ): self.solana_associated_token_acc = None self.token_mint = None @@ -39,8 +40,14 @@ def __init__( self.symbol = symbol self.decimals = decimals self.sol_client = sol_client - self.contract_address = self.deploy_wrapper(mintable) - self.contract = self.get_wrapper_contract() + + if contract_address: + self.contract = web3_client.get_deployed_contract(contract_address, + contract_file = "EIPs/ERC20/IERC20ForSpl") + else: + self.contract_address = self.deploy_wrapper(mintable) + self.contract = self.web3_client.get_deployed_contract(self.contract_address, + "EIPs/ERC20/IERC20ForSpl") def make_tx_object(self, from_address, gas_price=None, gas=None): tx = { @@ -90,18 +97,6 @@ def deploy_wrapper(self, mintable: bool): return logs[0]["args"]["pair"] return instruction_receipt - def get_wrapper_contract(self): - contract_path = (pathlib.Path.cwd() / "contracts" / "EIPs" / "ERC20" / "IERC20ForSpl.sol").absolute() - - with open(contract_path, "r") as s: - source = s.read() - - compiled = solcx.compile_source(source, output_values=["abi", "bin"], solc_version="0.8.10") - contract_interface = compiled[list(compiled.keys())[0]] - - contract = self.web3_client.eth.contract(address=self.contract_address, abi=contract_interface["abi"]) - return contract - # 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): tx = self.make_tx_object(signer.address, gas_price, gas) diff --git a/utils/erc721ForMetaplex.py b/utils/erc721ForMetaplex.py index cd80cc088a..4f88e5390f 100644 --- a/utils/erc721ForMetaplex.py +++ b/utils/erc721ForMetaplex.py @@ -7,11 +7,15 @@ class ERC721ForMetaplex: def __init__(self, web3_client: web3client.NeonChainWeb3Client, faucet, account=None, contract="erc721_for_metaplex.sol", - contract_name="ERC721ForMetaplex"): + contract_name="ERC721ForMetaplex", contract_address=None): self.web3_client = web3_client self.account = account or web3_client.create_account() - faucet.request_neon(self.account.address, 300) - self.contract = self.deploy(contract, contract_name) + faucet.request_neon(self.account.address, 600) + if contract_address: + self.contract = web3_client.get_deployed_contract(contract_address, + contract_file = contract, contract_name=contract_name) + else: + self.contract = self.deploy(contract, contract_name) def make_tx_object(self, from_address, gasPrice=None, gas=None): tx = {"from": from_address, "nonce": self.web3_client.eth.get_transaction_count(from_address), diff --git a/utils/instructions.py b/utils/instructions.py index ab89faf550..ae22ce2381 100644 --- a/utils/instructions.py +++ b/utils/instructions.py @@ -1,14 +1,11 @@ import hashlib import json -import math -import random import base58 from solana.publickey import PublicKey from solana.system_program import SYS_PROGRAM_ID from solana.transaction import AccountMeta, TransactionInstruction from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID -from spl.token.instructions import get_associated_token_address COMPUTE_BUDGET_ID: PublicKey = PublicKey( "ComputeBudget111111111111111111111111111111") @@ -18,18 +15,21 @@ class Instruction: @staticmethod - def account_v3(solana_wallet, neon_wallet_pda, - neon_wallet, evm_loader_id) -> TransactionInstruction: + def balance_account(solana_wallet, account_pubkey, contract_pubkey, + neon_wallet, evm_loader_id, chain_id) -> TransactionInstruction: + keys = [ AccountMeta(pubkey=solana_wallet, is_signer=True, is_writable=True), AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), - AccountMeta(pubkey=neon_wallet_pda, + AccountMeta(pubkey=account_pubkey, + is_signer=False, is_writable=True), + AccountMeta(pubkey=contract_pubkey, is_signer=False, is_writable=True), ] - data = bytes.fromhex('28') + bytes.fromhex(str(neon_wallet)[2:]) + data = bytes.fromhex('30') + bytes.fromhex(str(neon_wallet)[2:]) + chain_id.to_bytes(8, 'little') return TransactionInstruction( program_id=PublicKey(evm_loader_id), keys=keys, @@ -41,31 +41,30 @@ def sync_native(account: PublicKey): data = bytes.fromhex('11') return TransactionInstruction(keys=keys, program_id=TOKEN_PROGRAM_ID, data=data) - @staticmethod - def deposit(solana_pubkey, neon_pubkey, deposit_pubkey, - neon_wallet_address, neon_mint, evm_loader_id) -> TransactionInstruction: - associated_token_address = get_associated_token_address( - solana_pubkey, neon_mint) - pool_key = get_associated_token_address(deposit_pubkey, neon_mint) - keys = [ - AccountMeta(pubkey=associated_token_address, - is_signer=False, is_writable=True), - AccountMeta(pubkey=pool_key, is_signer=False, is_writable=True), - AccountMeta(pubkey=neon_pubkey, is_signer=False, is_writable=True), - AccountMeta(pubkey=TOKEN_PROGRAM_ID, - is_signer=False, is_writable=False), - AccountMeta(pubkey=solana_pubkey, - is_signer=True, is_writable=True), - AccountMeta(pubkey=SYS_PROGRAM_ID, - is_signer=False, is_writable=False), + def deposit( + ether_address: bytes, + chain_id: int, + balance_account: PublicKey, + contract_account: PublicKey, + mint: PublicKey, + source: PublicKey, + pool: PublicKey, + operator_pubkey: PublicKey, + evm_loader_id + ) -> TransactionInstruction: + data = bytes.fromhex('31') + ether_address + chain_id.to_bytes(8, 'little') + accounts = [ + AccountMeta(pubkey=mint, is_signer=False, is_writable=True), + AccountMeta(pubkey=source, is_signer=False, is_writable=True), + AccountMeta(pubkey=pool, is_signer=False, is_writable=True), + AccountMeta(pubkey=balance_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=contract_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), + AccountMeta(pubkey=operator_pubkey, is_signer=True, is_writable=True), + AccountMeta(pubkey=SYS_PROGRAM_ID, is_signer=False, is_writable=False), ] - - data = bytes.fromhex('27') + bytes.fromhex(neon_wallet_address[2:]) - return TransactionInstruction( - program_id=PublicKey(evm_loader_id), - keys=keys, - data=data) + return TransactionInstruction(program_id=PublicKey(evm_loader_id), data=data, keys=accounts) @staticmethod def compute_budget_utils(operator, units=DEFAULT_UNITS) -> TransactionInstruction: @@ -133,14 +132,12 @@ def claim(_from, to, amount, web3_client, ata_address, signed_tx = web3_client._web3.eth.account.sign_transaction( tx, _from.key) - if signed_tx.rawTransaction is not None: emulated_tx = web3_client.get_neon_emulate( str(signed_tx.rawTransaction.hex())[2:]) - if emulated_tx is not None: - for account in emulated_tx['result']['accounts']: - key = account['account'] + for account in emulated_tx['result']['solana_accounts']: + key = account['pubkey'] result[key] = AccountMeta(pubkey=PublicKey( key), is_signer=False, is_writable=True) if 'contract' in account: @@ -156,16 +153,15 @@ def claim(_from, to, amount, web3_client, ata_address, return signed_tx, result @staticmethod - def buld_tx_instruction(solana_wallet, neon_wallet, neon_raw_transaction, - neon_keys, evm_loader_id, neon_pool_count): + def build_tx_instruction(solana_wallet, neon_wallet, neon_raw_transaction, + neon_keys, evm_loader_id): program_id = PublicKey(evm_loader_id) - treasure_pool_index = math.floor(random.randint( - 0, 1) * int(neon_pool_count)) % int(neon_pool_count) + treasure_pool_index = 2 treasure_pool_address = get_collateral_pool_address( treasure_pool_index, evm_loader_id) - data = bytes.fromhex('1f') + treasure_pool_index.to_bytes(4, 'little') + \ - bytes.fromhex(str(neon_raw_transaction.hex())[2:]) + data = bytes.fromhex('32') + treasure_pool_index.to_bytes(4, 'little') + \ + bytes.fromhex(str(neon_raw_transaction.hex())[2:]) keys = [AccountMeta(pubkey=solana_wallet, is_signer=True, is_writable=True), AccountMeta(pubkey=treasure_pool_address, is_signer=False, is_writable=True), @@ -199,4 +195,4 @@ def get_solana_wallet_signer(solana_account, neon_account, web3_client): neon_wallet = bytes(neon_account.address, 'utf-8') new_wallet = hashlib.sha256(solana_wallet + neon_wallet).hexdigest() emulate_signer_private_key = f'0x{new_wallet}' - return web3_client._web3.eth.account.from_key(emulate_signer_private_key) + return web3_client.eth.account.from_key(emulate_signer_private_key) diff --git a/utils/operator.py b/utils/operator.py index 2969e29c7d..b4d6c417db 100644 --- a/utils/operator.py +++ b/utils/operator.py @@ -1,10 +1,8 @@ -import time import typing as tp import solana.rpc.api from solana.publickey import PublicKey from solana.rpc.commitment import Confirmed -from solana.rpc.types import TokenAccountOpts from utils.web3client import NeonChainWeb3Client @@ -40,45 +38,11 @@ def get_solana_balance(self): balances.append(balance) return sum(balances) - def get_neon_balance(self): + def get_token_balance(self, w3_client=None): + if w3_client is None: + w3_client = self.web3 balances = [] if len(self._operator_neon_rewards_address) > 0: for addr in self._operator_neon_rewards_address: - balances.append(self.web3.get_balance(self.web3.to_checksum_address(addr.lower()))) - else: - for key in self._operator_keys: - if self._operator_keys[key] is None: - accounts = self.sol.get_token_accounts_by_owner_json_parsed( - PublicKey(key), TokenAccountOpts(mint=PublicKey(self._neon_token_mint)) - ) - self._operator_keys[key] = accounts.value[0]["pubkey"] - balances.append( - int( - self.sol.get_token_account_balance(PublicKey(self._operator_keys[key]), commitment=Confirmed).to_json()[ - "result" - ]["value"]["amount"] - ) - ) + balances.append(w3_client.get_balance(w3_client.to_checksum_address(addr.lower()))) return sum(balances) - - def wait_solana_balance_changed(self, current_balance, timeout=90): - """solana change balance only when blocks confirmed""" - started = time.time() - - while (time.time() - started) < timeout: - balance = self.get_solana_balance() - if balance != current_balance: - return balance - time.sleep(5) - raise TimeoutError(f"Operator solana balance didn't change for {timeout} seconds") - - def wait_neon_balance_changed(self, current_balance, timeout=90): - """solana change balance only when blocks confirmed""" - started = time.time() - - while (time.time() - started) < timeout: - balance = self.get_neon_balance() - if balance != current_balance: - return balance - time.sleep(5) - raise TimeoutError(f"Operator neon balance didn't change for {timeout} seconds") diff --git a/utils/solana_client.py b/utils/solana_client.py index 4997b3f6bf..62e5d3c342 100644 --- a/utils/solana_client.py +++ b/utils/solana_client.py @@ -1,3 +1,4 @@ +import json import time import typing as tp @@ -11,18 +12,21 @@ from solana.transaction import Transaction 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, mint_to, MintToParams +from spl.token.client import Token as SplToken +from utils.consts import LAMPORT_PER_SOL, wSOL from utils.helpers import wait_condition from spl.token.constants import TOKEN_PROGRAM_ID +from utils.transfers_inter_networks import wSOL_tx, token_from_solana_to_neon_tx, mint_tx + class SolanaClient(solana.rpc.api.Client): def __init__(self, endpoint, account_seed_version="\3"): super().__init__(endpoint=endpoint, timeout=60) self.account_seed_version = ( - bytes(account_seed_version, encoding="utf-8") - .decode("unicode-escape") - .encode("utf-8") + bytes(account_seed_version, encoding="utf-8").decode("unicode-escape").encode("utf-8") ) def request_airdrop( @@ -33,9 +37,7 @@ def request_airdrop( ) -> RequestAirdropResp: airdrop_resp = None for _ in range(5): - airdrop_resp = super().request_airdrop( - pubkey, lamports, commitment=Finalized - ) + airdrop_resp = super().request_airdrop(pubkey, lamports, commitment=Finalized) if isinstance(airdrop_resp, InternalErrorMessage): time.sleep(10) print(f"Get error from solana airdrop: {airdrop_resp}") @@ -43,18 +45,12 @@ def request_airdrop( break else: raise AssertionError(f"Can't get airdrop from solana: {airdrop_resp}") - wait_condition( - lambda: self.get_balance(pubkey).value >= lamports, timeout_sec=30 - ) + 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): tx = Transaction().add( - transfer( - TransferParams( - from_pubkey=from_.public_key, to_pubkey=to, lamports=amount_lamports - ) - ) + transfer(TransferParams(from_pubkey=from_.public_key, to_pubkey=to, lamports=amount_lamports)) ) balance_before = self.get_balance(to).value self.send_transaction(tx, from_) @@ -65,18 +61,29 @@ def send_sol(self, from_: Keypair, to: PublicKey, amount_lamports: int): else: raise AssertionError(f"Balance not changed in account {to}") - def get_neon_account_address( - self, neon_account_address: str, evm_loader_id: str - ) -> PublicKey: - neon_account_addressbytes = bytes.fromhex(neon_account_address[2:]) + def ether2balance(self, address: tp.Union[str, bytes], chain_id: int, evm_loader_id: str) -> PublicKey: + # get public key associated with chain_id for an address + address_bytes = bytes.fromhex(address[2:]) + chain_id_bytes = chain_id.to_bytes(32, "big") return PublicKey.find_program_address( - [self.account_seed_version, neon_account_addressbytes], - PublicKey(evm_loader_id), + [self.account_seed_version, address_bytes, chain_id_bytes], PublicKey(evm_loader_id) )[0] - def get_erc_auth_address( - self, neon_account_address: str, token_address: str, evm_loader_id: str - ): + @staticmethod + def ether2bytes(ether: tp.Union[str, bytes]): + if isinstance(ether, str): + if ether.startswith("0x"): + return bytes.fromhex(ether[2:]) + return bytes.fromhex(ether) + return ether + + def ether2program(self, ether: tp.Union[str, bytes], evm_loader_id: str) -> tp.Tuple[str, int]: + items = PublicKey.find_program_address( + [self.account_seed_version, self.ether2bytes(ether)], PublicKey(evm_loader_id) + ) + return str(items[0]), items[1] + + 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:] @@ -108,3 +115,72 @@ def create_spl(self, owner: Keypair, decimals: int = 9): ) return token_mint, assoc_addr + + def send_tx_and_check_status_ok(self, tx, *signers): + opts = TxOpts(skip_preflight=True, skip_confirmation=False) + sig = self.send_transaction(tx, *signers, opts=opts).value + 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, neon_mint): + trx = Transaction() + trx.add(create_associated_token_account(solana_account.public_key, solana_account.public_key, neon_mint)) + opts = TxOpts(skip_preflight=True, skip_confirmation=False) + self.send_transaction(trx, solana_account, opts=opts) + + def deposit_wrapped_sol_from_solana_to_neon( + self, solana_account, neon_account, chain_id, evm_loader_id, full_amount=None + ): + 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) + + self.create_ata(solana_account, mint_pubkey) + + # wrap SOL + wSOL_account = self.get_account_info(ata_address).value + wrap_sol_tx = wSOL_tx(wSOL_account, wSOL, full_amount, solana_account.public_key, ata_address) + self.send_tx_and_check_status_ok(wrap_sol_tx, solana_account) + + tx = token_from_solana_to_neon_tx( + self, solana_account, wSOL["address_spl"], neon_account, full_amount, evm_loader_id, chain_id + ) + + self.send_tx_and_check_status_ok(tx, solana_account) + + def deposit_neon_like_tokens_from_solana_to_neon( + self, + neon_mint, + solana_account, + neon_account, + chain_id, + operator_keypair, + evm_loader_keypair, + evm_loader_id, + amount, + ): + + spl_neon_token = SplToken(self, neon_mint, TOKEN_PROGRAM_ID, payer=operator_keypair) + associated_token_address = spl_neon_token.create_associated_token_account(solana_account.public_key) + + tx = mint_tx( + amount=amount, + dest=associated_token_address, + neon_mint=neon_mint, + mint_authority=evm_loader_keypair.public_key, + ) + tx.fee_payer = operator_keypair.public_key + + self.send_tx_and_check_status_ok(tx, operator_keypair, evm_loader_keypair) + + tx = token_from_solana_to_neon_tx( + self, + solana_account, + neon_mint, + neon_account, + amount, + evm_loader_id, + chain_id, + ) + self.send_tx_and_check_status_ok(tx, solana_account) diff --git a/utils/transfers_inter_networks.py b/utils/transfers_inter_networks.py index 6424663278..8ce960dd84 100644 --- a/utils/transfers_inter_networks.py +++ b/utils/transfers_inter_networks.py @@ -1,119 +1,130 @@ - from solana.publickey import PublicKey from solana.system_program import TransferParams, transfer from solana.transaction import Transaction from spl.token.constants import TOKEN_PROGRAM_ID -from spl.token.instructions import (ApproveParams, approve, - get_associated_token_address) +from spl.token.instructions import ApproveParams, approve, get_associated_token_address, MintToParams, mint_to from utils.instructions import Instruction, get_solana_wallet_signer -class Transfer: - @staticmethod - def neon_from_solana_to_neon_tx(solana_account, neon_wallet, neon_mint, neon_account, - amount, evm_loader_id): - '''Transfer NEON from solana to neon transaction''' - tx = Transaction(fee_payer=solana_account.public_key) - associated_token_address = get_associated_token_address( - solana_account.public_key, neon_mint) +def token_from_solana_to_neon_tx(sol_client, solana_account, mint, neon_account, amount, evm_loader_id, chain_id): + """Transfer any token from solana to neon transaction""" + balance_pubkey = sol_client.ether2balance(neon_account.address, chain_id, evm_loader_id) + contract_pubkey = PublicKey(sol_client.ether2program(neon_account.address, evm_loader_id)[0]) - tx.add(approve( + tx = Transaction(fee_payer=solana_account.public_key) + associated_token_address = get_associated_token_address(solana_account.public_key, mint) + tx.add( + approve( ApproveParams( program_id=TOKEN_PROGRAM_ID, source=associated_token_address, - delegate=neon_wallet, + delegate=balance_pubkey, owner=solana_account.public_key, - amount=amount))) + amount=amount, + ) + ) + ) + + authority_pool = get_authority_pool_address(evm_loader_id) + + pool = get_associated_token_address(authority_pool, mint) + tx.add( + Instruction.deposit( + bytes.fromhex(neon_account.address[2:]), + chain_id, + balance_pubkey, + contract_pubkey, + mint, + associated_token_address, + pool, + solana_account.public_key, + evm_loader_id, + ) + ) + return tx - authority_pool = get_authority_pool_address( - evm_loader_id) - tx.add(Instruction.deposit( - solana_account.public_key, - neon_wallet, - authority_pool, - neon_account.address, - neon_mint, - evm_loader_id)) - - return tx - - def wSOL_tx(sol_client, spl_token, amount, solana_wallet, ata_address): - mint_pubkey = PublicKey(spl_token['address_spl']) - wSOL_account = sol_client.get_account_info(ata_address).value - - tx = Transaction(fee_payer=solana_wallet) - if (wSOL_account is None): - tx.add(Instruction.associated_token_account( - solana_wallet, ata_address, solana_wallet, mint_pubkey, instruction_data=bytes(0))) - tx.add(transfer(TransferParams(solana_wallet, ata_address, amount))) - tx.add(Instruction.sync_native(ata_address)) - - return tx - - def neon_transfer_tx(web3_client, sol_client, amount, spl_token, solana_account, - neon_account, erc20_spl, evm_loader_id, neon_pool_count): - - neon_wallet_pda = sol_client.get_neon_account_address( - neon_account.address, evm_loader_id) - neon_wallet_account = sol_client.get_account_info( - neon_wallet_pda).value - delegate_pda = sol_client.get_erc_auth_address( - neon_account.address, spl_token['address'], evm_loader_id) - - emulate_signer = get_solana_wallet_signer( - solana_account, neon_account, web3_client) - emulated_signer_pda = sol_client.get_neon_account_address( - emulate_signer.address, evm_loader_id) - emulate_signer_pda_account = sol_client.get_account_info( - emulated_signer_pda).value - - solana_wallet = solana_account.public_key - - ata_address = get_associated_token_address( - solana_wallet, PublicKey(spl_token['address_spl'])) - - neon_transaction, neon_keys = Instruction.claim(neon_account, - spl_token['address'], - amount, - web3_client, - ata_address, - emulate_signer, - erc20_spl) - tx = Transaction(fee_payer=solana_wallet) - - compute_budget_instruction = Instruction.compute_budget_utils( - solana_account) - tx.add(compute_budget_instruction) - - heap_frame_instruction = Instruction.request_heap_frame(solana_account) - tx.add(heap_frame_instruction) - - tx.add(approve( +def wSOL_tx(wSOL_account, spl_token, amount, solana_wallet, ata_address): + mint_pubkey = PublicKey(spl_token["address_spl"]) + + tx = Transaction(fee_payer=solana_wallet) + if wSOL_account is None: + tx.add( + Instruction.associated_token_account( + solana_wallet, ata_address, solana_wallet, mint_pubkey, instruction_data=bytes(0) + ) + ) + tx.add(transfer(TransferParams(solana_wallet, ata_address, amount))) + tx.add(Instruction.sync_native(ata_address)) + + return tx + + +def mint_tx(amount, dest, neon_mint, mint_authority): + trx = Transaction() + params = MintToParams( + amount=amount, dest=dest, mint=neon_mint, mint_authority=mint_authority, program_id=TOKEN_PROGRAM_ID + ) + trx.add(mint_to(params)) + return trx + + +def neon_transfer_tx( + web3_client, sol_client, amount, spl_token, solana_account, neon_account, erc20_spl, evm_loader_id +): + chain_id = web3_client.eth.chain_id + delegate_pda = sol_client.ether2balance(neon_account.address, chain_id, evm_loader_id) + contract_pubkey = PublicKey(sol_client.ether2program(neon_account.address, evm_loader_id)[0]) + + emulate_signer = get_solana_wallet_signer(solana_account, neon_account, web3_client) + emulated_signer_pda = sol_client.ether2balance(emulate_signer.address, chain_id, evm_loader_id) + emulated_contract_pubkey = PublicKey(sol_client.ether2program(emulate_signer.address, evm_loader_id)[0]) + + solana_wallet = solana_account.public_key + + ata_address = get_associated_token_address(solana_wallet, PublicKey(spl_token["address_spl"])) + + neon_transaction, neon_keys = Instruction.claim( + neon_account, spl_token["address"], amount, web3_client, ata_address, emulate_signer, erc20_spl + ) + tx = Transaction(fee_payer=solana_wallet) + + compute_budget_instruction = Instruction.compute_budget_utils(solana_account) + tx.add(compute_budget_instruction) + + heap_frame_instruction = Instruction.request_heap_frame(solana_account) + tx.add(heap_frame_instruction) + + tx.add( + approve( ApproveParams( program_id=TOKEN_PROGRAM_ID, source=ata_address, delegate=delegate_pda, owner=solana_account.public_key, - amount=amount))) - - if neon_wallet_account is None: - tx.add(Instruction.account_v3(solana_wallet, neon_wallet_pda, - neon_account.address, evm_loader_id)) + amount=amount, + ) + ) + ) - if emulate_signer_pda_account is None: - tx.add(Instruction.account_v3(solana_wallet, emulated_signer_pda, - emulate_signer.address, evm_loader_id)) + tx.add( + Instruction.balance_account(solana_wallet, delegate_pda, contract_pubkey, neon_account.address, evm_loader_id, + chain_id)) - if neon_transaction.rawTransaction is not None: - tx.add(Instruction.buld_tx_instruction(solana_wallet, neon_wallet_pda, - neon_transaction.rawTransaction, neon_keys, - evm_loader_id, neon_pool_count)) + tx.add( + Instruction.balance_account(solana_wallet, emulated_signer_pda, emulated_contract_pubkey, + emulate_signer.address, evm_loader_id, chain_id) + ) - return tx + tx.add( + Instruction.build_tx_instruction( + solana_wallet, delegate_pda, neon_transaction.rawTransaction, neon_keys, evm_loader_id + ) + ) + return tx def get_authority_pool_address(evm_loader_id: str): - text = 'Deposit' + text = "Deposit" return PublicKey.find_program_address([text.encode()], PublicKey(evm_loader_id))[0] diff --git a/utils/web3client.py b/utils/web3client.py index 06d4e46826..bc892a7b56 100644 --- a/utils/web3client.py +++ b/utils/web3client.py @@ -26,15 +26,18 @@ def __init__( self._proxy_url = proxy_url self._tracer_url = tracer_url self._chain_id = None - self._web3 = web3.Web3( - web3.HTTPProvider( - proxy_url, session=session, request_kwargs={"timeout": 30} - ) - ) + self._web3 = web3.Web3(web3.HTTPProvider(proxy_url, session=session, request_kwargs={"timeout": 30})) def __getattr__(self, item): return getattr(self._web3, item) + @property + def native_token_name(self): + if self._proxy_url.split("/")[-1] != "solana": + return self._proxy_url.split("/")[-1].upper() + else: + return "NEON" + @property def chain_id(self): if self._chain_id is None: @@ -119,6 +122,7 @@ def deploy_contract( gas: tp.Optional[int] = 0, gas_price: tp.Optional[int] = None, constructor_args: tp.Optional[tp.List] = None, + value=0, ) -> web3.types.TxReceipt: """Proxy doesn't support send_transaction""" gas_price = gas_price or self.gas_price() @@ -131,6 +135,7 @@ def deploy_contract( "gas": gas, "gasPrice": gas_price, "nonce": self.get_nonce(from_), + "value": value, "chainId": self.chain_id, } ) @@ -146,19 +151,17 @@ def send_transaction( self, account: eth_account.signers.local.LocalAccount, transaction: tp.Dict, - gas_multiplier: tp.Optional[ - float - ] = None, # fix for some event depends transactions + gas_multiplier: tp.Optional[float] = None, # fix for some event depends transactions ) -> web3.types.TxReceipt: if "gasPrice" not in transaction: transaction["gasPrice"] = self.gas_price() if "gas" not in transaction: transaction["gas"] = self._web3.eth.estimate_gas(transaction) + if "nonce" not in transaction: + transaction["nonce"] = self.get_nonce(account) if gas_multiplier is not None: transaction["gas"] = int(transaction["gas"] * gas_multiplier) - instruction_tx = self._web3.eth.account.sign_transaction( - transaction, account.key - ) + instruction_tx = self._web3.eth.account.sign_transaction(transaction, account.key) signature = self._web3.eth.send_raw_transaction(instruction_tx.rawTransaction) return self._web3.eth.wait_for_transaction_receipt(signature) @@ -171,6 +174,7 @@ def deploy_and_get_contract( constructor_args: tp.Optional[tp.Any] = None, import_remapping: tp.Optional[dict] = None, gas: tp.Optional[int] = 0, + value=0, ) -> tp.Tuple[tp.Any, web3.types.TxReceipt]: contract_interface = helpers.get_contract_interface( contract, @@ -185,25 +189,20 @@ def deploy_and_get_contract( bytecode=contract_interface["bin"], constructor_args=constructor_args, gas=gas, + value=value, ) - contract = self.eth.contract( - address=contract_deploy_tx["contractAddress"], abi=contract_interface["abi"] - ) + contract = self.eth.contract(address=contract_deploy_tx["contractAddress"], abi=contract_interface["abi"]) return contract, contract_deploy_tx - def compile_by_vyper_and_deploy( - self, account, contract_name, constructor_args=None - ): + def compile_by_vyper_and_deploy(self, account, contract_name, constructor_args=None): import vyper # Import here because vyper prevent override decimal precision (uses in economy tests) contract_path = pathlib.Path.cwd() / "contracts" / "vyper" with open(contract_path / f"{contract_name}.vy") as f: contract_code = f.read() - contract_interface = vyper.compile_code( - contract_code, output_formats=["abi", "bytecode"] - ) + contract_interface = vyper.compile_code(contract_code, output_formats=["abi", "bytecode"]) contract_deploy_tx = self.deploy_contract( account, @@ -211,9 +210,7 @@ def compile_by_vyper_and_deploy( bytecode=contract_interface["bytecode"], constructor_args=constructor_args, ) - return self.eth.contract( - address=contract_deploy_tx["contractAddress"], abi=contract_interface["abi"] - ) + return self.eth.contract(address=contract_deploy_tx["contractAddress"], abi=contract_interface["abi"]) @staticmethod def text_to_bytes32(text: str) -> bytes: @@ -228,6 +225,50 @@ def call_function_at_address(self, contract_address, signature, args, result_typ result = self._web3.eth.call(tx) return abi.decode(result_types, result)[0] + def get_balance(self, address: tp.Union[str, eth_account.signers.local.LocalAccount]): + if not isinstance(address, str): + address = address.address + return self._web3.eth.get_balance(address, "pending") + + def get_deployed_contract(self, address, contract_file, contract_name=None, solc_version="0.8.12"): + contract_interface = helpers.get_contract_interface(contract_file, solc_version, contract_name) + contract = self.eth.contract(address=address, abi=contract_interface["abi"]) + return contract + + def send_tokens( + self, + from_: eth_account.signers.local.LocalAccount, + to: tp.Union[str, eth_account.signers.local.LocalAccount], + value: int, + gas: tp.Optional[int] = 0, + gas_price: tp.Optional[int] = None, + nonce: int = None, + ) -> web3.types.TxReceipt: + to_addr = to if isinstance(to, str) else to.address + if nonce is None: + nonce = self.get_nonce(from_) + transaction = { + "from": from_.address, + "to": to_addr, + "value": value, + "gasPrice": gas_price or self.gas_price(), + "gas": gas, + "nonce": nonce, + "chainId": self.eth.chain_id, + } + if transaction["gas"] == 0: + transaction["gas"] = self.eth.estimate_gas(transaction) + 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) + + @staticmethod + def to_atomic_currency(amount): + return web3.Web3.to_wei(amount, "ether") + + def to_main_currency(self, value): + return web3.Web3.from_wei(value, "ether") + class NeonChainWeb3Client(Web3Client): def __init__( @@ -246,24 +287,18 @@ def create_account_with_balance( ): """Creates a new account with balance""" account = self.create_account() - balance_before = float( - self.from_wei(self.eth.get_balance(account.address), Unit.ETHER) - ) + balance_before = float(self.from_wei(self.eth.get_balance(account.address), Unit.ETHER)) if bank_account is not None: self.send_neon(bank_account, account, amount) else: faucet.request_neon(account.address, amount=amount) for _ in range(20): - if float( - self.from_wei(self.eth.get_balance(account.address), Unit.ETHER) - ) >= (balance_before + amount): + if float(self.from_wei(self.eth.get_balance(account.address), Unit.ETHER)) >= (balance_before + amount): break time.sleep(1) else: - raise AssertionError( - f"Balance didn't changed after 20 seconds ({account.address})" - ) + raise AssertionError(f"Balance didn't changed after 20 seconds ({account.address})") return account def send_neon( @@ -275,38 +310,8 @@ def send_neon( gas_price: tp.Optional[int] = None, nonce: int = None, ) -> web3.types.TxReceipt: - to_addr = to if isinstance(to, str) else to.address - if nonce is None: - nonce = self.get_nonce(from_) - transaction = { - "from": from_.address, - "to": to_addr, - "chainId": self.chain_id, - "value": web3.Web3.to_wei(amount, "ether"), - "gasPrice": gas_price or self.gas_price(), - "gas": gas, - "nonce": nonce, - } - if transaction["gas"] == 0: - transaction["gas"] = self._web3.eth.estimate_gas(transaction) - - signed_tx = self._web3.eth.account.sign_transaction(transaction, from_.key) - tx = self._web3.eth.send_raw_transaction(signed_tx.rawTransaction) - return self._web3.eth.wait_for_transaction_receipt(tx) - - def get_balance( - self, address: tp.Union[str, eth_account.signers.local.LocalAccount] - ): - if not isinstance(address, str): - address = address.address - return web3.Web3.from_wei( - self._web3.eth.get_balance(address, "pending"), "ether" - ) - - -class SolChainWeb3Client(Web3Client): - def __init__(self, proxy_url: str): - super().__init__(f"{proxy_url}/sol") + value = web3.Web3.to_wei(amount, "ether") + return self.send_tokens(from_, to, value, gas, gas_price, nonce) - def create_account_with_balance(self): - pass + def get_ether_balance(self, address: tp.Union[str, eth_account.signers.local.LocalAccount]): + return web3.Web3.from_wei(super().get_balance(address), "ether")