From a238bf32070ac1f27376ca100f2f28eb6226fdcd Mon Sep 17 00:00:00 2001 From: Daniel Shiposha Date: Tue, 30 Aug 2022 13:13:10 +0000 Subject: [PATCH] Merge branch 'develop' into feature/multi-assets-redone --- --- a/.docker/Dockerfile-chain-dev-unit +++ b/.docker/Dockerfile-chain-dev-unit @@ -23,4 +23,4 @@ WORKDIR /dev_chain -RUN cargo test --features=limit-testing +CMD cargo test --features=limit-testing --- /dev/null +++ b/.docker/Dockerfile-parachain-node-only @@ -0,0 +1,103 @@ +# ===== Rust builder ===== +FROM ubuntu:20.04 as rust-builder +LABEL maintainer="Unique.Network" + +ARG RUST_TOOLCHAIN= + +ENV RUST_TOOLCHAIN $RUST_TOOLCHAIN +ENV CARGO_HOME="/cargo-home" +ENV PATH="/cargo-home/bin:$PATH" +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN apt-get update && \ + apt-get install -y curl cmake pkg-config libssl-dev git clang && \ + apt-get clean && \ + rm -r /var/lib/apt/lists/* + +RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain none + +RUN rustup toolchain uninstall $(rustup toolchain list) && \ + rustup toolchain install $RUST_TOOLCHAIN && \ + rustup default $RUST_TOOLCHAIN && \ + rustup target list --installed && \ + rustup show +RUN rustup target add wasm32-unknown-unknown --toolchain $RUST_TOOLCHAIN + +RUN mkdir /unique_parachain +WORKDIR /unique_parachain + + +# ===== BUILD current version ====== +FROM rust-builder as builder-unique-current + +ARG PROFILE=release +ARG FEATURE= +ARG MAINNET_BRANCH= +ARG REPO_URL= + +WORKDIR /unique_parachain + +RUN git clone $REPO_URL -b $MAINNET_BRANCH . && \ + cargo build --features=$FEATURE --$PROFILE + +# ===== BUILD target version ====== +FROM rust-builder as builder-unique-target + +ARG PROFILE=release +ARG FEATURE= + +COPY . /unique_parachain +WORKDIR /unique_parachain + +RUN cargo build --features=$FEATURE --$PROFILE + +# ===== BUILD POLKADOT ===== +FROM rust-builder as builder-polkadot + +ARG POLKADOT_BUILD_BRANCH= +ENV POLKADOT_BUILD_BRANCH $POLKADOT_BUILD_BRANCH + + +WORKDIR /unique_parachain + +RUN git clone -b $POLKADOT_BUILD_BRANCH --depth 1 https://github.com/paritytech/polkadot.git && \ + cd polkadot && \ + cargo build --release + +# ===== RUN ====== + +FROM ubuntu:20.04 + +ARG RUNTIME= +ENV RUNTIME $RUNTIME + +RUN apt-get -y update && \ + apt-get -y install curl git && \ + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash && \ + export NVM_DIR="$HOME/.nvm" && \ + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ + nvm install v16.16.0 && \ + nvm use v16.16.0 + +RUN git clone https://github.com/uniquenetwork/polkadot-launch -b feature/runtime-upgrade-testing + +RUN export NVM_DIR="$HOME/.nvm" && \ + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ + cd /polkadot-launch && \ + npm install --global yarn && \ + yarn install + +RUN echo "$RUNTIME" + +COPY --from=builder-unique-current /unique_parachain/target/release/unique-collator /unique-chain/current/release/ +COPY --from=builder-unique-target /unique_parachain/target/release/unique-collator /unique-chain/target/release/ +COPY --from=builder-unique-target /unique_parachain/target/release/wbuild/"$RUNTIME"-runtime/"$RUNTIME"_runtime.compact.compressed.wasm /unique-chain/target/release/wbuild/"$RUNTIME"-runtime/"$RUNTIME"_runtime.compact.compressed.wasm + +COPY --from=builder-polkadot /unique_parachain/polkadot/target/release/polkadot /polkadot/target/release/ +COPY --from=builder-polkadot /unique_parachain/polkadot/target/release/wbuild/westend-runtime/westend_runtime.compact.compressed.wasm /polkadot/target/release/wbuild/westend-runtime/westend_runtime.compact.compressed.wasm + +CMD export NVM_DIR="$HOME/.nvm" && \ + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ + cd /polkadot-launch && \ + yarn start launch-config.json --test-upgrade-parachains -w -n --- /dev/null +++ b/.docker/docker-compose.tmp-node.j2 @@ -0,0 +1,33 @@ +version: "3.5" + +services: + node-parachain: + build: + args: + - "RUST_TOOLCHAIN={{ RUST_TOOLCHAIN }}" + - "BRANCH={{ BRANCH }}" + - "REPO_URL={{ REPO_URL }}" + - "FEATURE={{ FEATURE }}" + - "RUNTIME={{ RUNTIME }}" + - "POLKADOT_BUILD_BRANCH={{ POLKADOT_BUILD_BRANCH }}" + - "MAINNET_TAG={{ MAINNET_TAG }}" + - "MAINNET_BRANCH={{ MAINNET_BRANCH }}" + context: ../ + dockerfile: .docker/Dockerfile-parachain-node-only + image: node-parachain + container_name: node-parachain + volumes: + - type: bind + source: ./launch-config-forkless-nodata.json + target: /polkadot-launch/launch-config.json + read_only: true + expose: + - 9944 + - 9933 + ports: + - 127.0.0.1:9944:9944 + - 127.0.0.1:9933:9933 + logging: + options: + max-size: "1m" + max-file: "3" --- a/.docker/forkless-config/launch-config-forkless-data.j2 +++ b/.docker/forkless-config/launch-config-forkless-data.j2 @@ -86,7 +86,7 @@ }, "parachains": [ { - "bin": "/unique-chain/target/release/unique-collator", + "bin": "/unique-chain/current/release/unique-collator", "upgradeBin": "/unique-chain/target/release/unique-collator", "upgradeWasm": "/unique-chain/target/release/wbuild/{{ FEATURE }}/{{ RUNTIME }}_runtime.compact.compressed.wasm", "id": "1000", --- a/.docker/forkless-config/launch-config-forkless-nodata.j2 +++ b/.docker/forkless-config/launch-config-forkless-nodata.j2 @@ -101,7 +101,8 @@ "--rpc-cors=all", "--unsafe-rpc-external", "--unsafe-ws-external", - "-lxcm=trace" + "-lxcm=trace", + "--ws-max-connections=1000" ] }, { @@ -113,7 +114,8 @@ "--rpc-cors=all", "--unsafe-rpc-external", "--unsafe-ws-external", - "-lxcm=trace" + "-lxcm=trace", + "--ws-max-connections=1000" ] } ] --- a/.github/workflows/build-test-master.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: yarn test para - -# Controls when the action will run. -on: - # Triggers the workflow on push or pull request events but only for the master branch - pull_request: - branches: - - master - types: - - opened - - reopened - - synchronize #commit(s) pushed to the pull request - - ready_for_review - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -#Define Workflow variables -env: - REPO_URL: ${{ github.server_url }}/${{ github.repository }} - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - - master-build-and-test: - # The type of runner that the job will run on - runs-on: [self-hosted-ci,large] - timeout-minutes: 1380 - - name: ${{ matrix.network }} - - continue-on-error: true #Do not stop testing of matrix runs failed. - - strategy: - matrix: - include: - - network: "opal" - features: "opal-runtime" - - network: "quartz" - features: "quartz-runtime" - - network: "unique" - features: "unique-runtime" - - steps: - - name: Skip if pull request is in Draft - # `if: github.event.pull_request.draft == true` should be kept here, at - # the step level, rather than at the job level. The latter is not - # recommended because when the PR is moved from "Draft" to "Ready to - # review" the workflow will immediately be passing (since it was skipped), - # even though it hasn't actually ran, since it takes a few seconds for - # the workflow to start. This is also disclosed in: - # https://github.community/t/dont-run-actions-on-draft-pull-requests/16817/17 - # That scenario would open an opportunity for the check to be bypassed: - # 1. Get your PR approved - # 2. Move it to Draft - # 3. Push whatever commits you want - # 4. Move it to "Ready for review"; now the workflow is passing (it was - # skipped) and "Check reviews" is also passing (it won't be updated - # until the workflow is finished) - if: github.event.pull_request.draft == true - run: exit 1 - - - name: Clean Workspace - uses: AutoModality/action-clean@v1.1.0 - - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 - with: - ref: ${{ github.head_ref }} - - - name: Read .env file - uses: xom9ikk/dotenv@v1.0.2 - - - name: Generate ENV related extend file for docker-compose - uses: cuchi/jinja2-action@v1.2.0 - with: - template: .docker/docker-compose.tmp-master.j2 - output_file: .docker/docker-compose.${{ matrix.network }}.yml - variables: | - REPO_URL=${{ github.server_url }}/${{ github.repository }}.git - RUST_TOOLCHAIN=${{ env.RUST_TOOLCHAIN }} - POLKADOT_BUILD_BRANCH=${{ env.POLKADOT_BUILD_BRANCH }} - FEATURE=${{ matrix.features }} - BRANCH=${{ github.head_ref }} - - - name: Show build configuration - run: cat .docker/docker-compose.${{ matrix.network }}.yml - - - name: Build the stack - run: docker-compose -f ".docker/docker-compose-master.yml" -f ".docker/docker-compose.${{ matrix.network }}.yml" up -d --build - - - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: Run tests - run: | - cd tests - yarn install - yarn add mochawesome - echo "Ready to start tests" - node scripts/readyness.js - NOW=$(date +%s) && yarn test --reporter mochawesome --reporter-options reportFilename=test-${NOW} - env: - RPC_URL: http://127.0.0.1:9933/ - - - name: Test Report - uses: phoenix-actions/test-reporting@v8 - id: test-report - if: success() || failure() # run this step even if previous step failed - with: - name: Tests ${{ matrix.network }} # Name of the check run which will be created - path: tests/mochawesome-report/test-*.json # Path to test results - reporter: mochawesome-json - fail-on-error: 'false' - - - name: Read output variables - run: | - echo "url is ${{ steps.test-report.outputs.runHtmlUrl }}" - - - name: Stop running containers - if: always() # run this step always - run: docker-compose -f ".docker/docker-compose-master.yml" -f ".docker/docker-compose.${{ matrix.network }}.yml" down --- a/.github/workflows/codestyle.yml +++ b/.github/workflows/codestyle.yml @@ -49,7 +49,7 @@ run: cargo fmt -- --check # In that mode it returns only exit code. - name: Cargo fmt state if: success() - run: echo "Nothing to do. Cargo fmt returned exit code 0." + run: echo "Nothing to do. Command 'cargo fmt -- --check' returned exit code 0." clippy: --- a/.github/workflows/dev-build-tests.yml +++ b/.github/workflows/dev-build-tests.yml @@ -123,66 +123,3 @@ - name: Stop running containers if: always() # run this step always run: docker-compose -f ".docker/docker-compose-dev.yaml" -f ".docker/docker-compose.${{ matrix.network }}.yml" down - - - dev_build_unit_test: - # The type of runner that the job will run on - runs-on: [self-hosted-ci,medium] - timeout-minutes: 1380 - - name: unit tests - - - continue-on-error: true #Do not stop testing of matrix runs failed. As it decided during PR review - it required 50/50& Let's check it with false. - - - steps: - - name: Skip if pull request is in Draft - # `if: github.event.pull_request.draft == true` should be kept here, at - # the step level, rather than at the job level. The latter is not - # recommended because when the PR is moved from "Draft" to "Ready to - # review" the workflow will immediately be passing (since it was skipped), - # even though it hasn't actually ran, since it takes a few seconds for - # the workflow to start. This is also disclosed in: - # https://github.community/t/dont-run-actions-on-draft-pull-requests/16817/17 - # That scenario would open an opportunity for the check to be bypassed: - # 1. Get your PR approved - # 2. Move it to Draft - # 3. Push whatever commits you want - # 4. Move it to "Ready for review"; now the workflow is passing (it was - # skipped) and "Check reviews" is also passing (it won't be updated - # until the workflow is finished) - if: github.event.pull_request.draft == true - run: exit 1 - - - name: Clean Workspace - uses: AutoModality/action-clean@v1.1.0 - - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 - with: - ref: ${{ github.head_ref }} #Checking out head commit - - - name: Read .env file - uses: xom9ikk/dotenv@v1.0.2 - - - name: Generate ENV related extend file for docker-compose - uses: cuchi/jinja2-action@v1.2.0 - with: - template: .docker/docker-compose.tmp-unit.j2 - output_file: .docker/docker-compose.unit.yml - variables: | - RUST_TOOLCHAIN=${{ env.RUST_TOOLCHAIN }} - FEATURE=${{ matrix.features }} - - - - name: Show build configuration - run: cat .docker/docker-compose.unit.yml - - - name: Build the stack - run: docker-compose -f ".docker/docker-compose-dev.yaml" -f ".docker/docker-compose.unit.yml" up -d --build --remove-orphans - - - - name: Stop running containers - if: always() # run this step always - run: docker-compose -f ".docker/docker-compose-dev.yaml" -f ".docker/docker-compose.unit.yml" down --- /dev/null +++ b/.github/workflows/nodes-only-update.yml @@ -0,0 +1,301 @@ +name: nodes-only update + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + pull_request: + branches: + - master + types: + - opened + - reopened + - synchronize #commit(s) pushed to the pull request + - ready_for_review + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +#Define Workflow variables +env: + REPO_URL: ${{ github.server_url }}/${{ github.repository }} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + + prepare-execution-marix: + + name: Prepare execution matrix + + runs-on: self-hosted-ci + outputs: + matrix: ${{ steps.create_matrix.outputs.matrix }} + + steps: + + - name: Clean Workspace + uses: AutoModality/action-clean@v1.1.0 + + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} #Checking out head commit + + - name: Read .env file + uses: xom9ikk/dotenv@v1.0.2 + + - name: Create Execution matrix + uses: fabiocaccamo/create-matrix-action@v2 + id: create_matrix + with: + matrix: | + network {opal}, runtime {opal}, features {opal-runtime}, mainnet_branch {${{ env.QUARTZ_MAINNET_TAG }}} + network {quartz}, runtime {quartz}, features {quartz-runtime}, mainnet_branch {${{ env.QUARTZ_MAINNET_TAG }}} + network {unique}, runtime {unique}, features {unique-runtime}, mainnet_branch {${{ env.UNIQUE_MAINNET_TAG }}} + + + + forkless-update-nodata: + needs: prepare-execution-marix + # The type of runner that the job will run on + runs-on: [self-hosted-ci,large] + + + + timeout-minutes: 1380 + + name: ${{ matrix.network }} + + continue-on-error: true #Do not stop testing of matrix runs failed. As it decided during PR review - it required 50/50& Let's check it with false. + + strategy: + matrix: + include: ${{fromJson(needs.prepare-execution-marix.outputs.matrix)}} + + + steps: + - name: Skip if pull request is in Draft + # `if: github.event.pull_request.draft == true` should be kept here, at + # the step level, rather than at the job level. The latter is not + # recommended because when the PR is moved from "Draft" to "Ready to + # review" the workflow will immediately be passing (since it was skipped), + # even though it hasn't actually ran, since it takes a few seconds for + # the workflow to start. This is also disclosed in: + # https://github.community/t/dont-run-actions-on-draft-pull-requests/16817/17 + # That scenario would open an opportunity for the check to be bypassed: + # 1. Get your PR approved + # 2. Move it to Draft + # 3. Push whatever commits you want + # 4. Move it to "Ready for review"; now the workflow is passing (it was + # skipped) and "Check reviews" is also passing (it won't be updated + # until the workflow is finished) + if: github.event.pull_request.draft == true + run: exit 1 + + - name: Clean Workspace + uses: AutoModality/action-clean@v1.1.0 + + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} #Checking out head commit + + - name: Read .env file + uses: xom9ikk/dotenv@v1.0.2 + + - name: Generate ENV related extend file for docker-compose + uses: cuchi/jinja2-action@v1.2.0 + with: + template: .docker/docker-compose.tmp-node.j2 + output_file: .docker/docker-compose.node.${{ matrix.network }}.yml + variables: | + REPO_URL=${{ github.server_url }}/${{ github.repository }}.git + RUST_TOOLCHAIN=${{ env.RUST_TOOLCHAIN }} + POLKADOT_BUILD_BRANCH=${{ env.POLKADOT_BUILD_BRANCH }} + POLKADOT_MAINNET_BRANCH=${{ env.POLKADOT_MAINNET_BRANCH }} + MAINNET_TAG=${{ matrix.mainnet_tag }} + MAINNET_BRANCH=${{ matrix.mainnet_branch }} + FEATURE=${{ matrix.features }} + RUNTIME=${{ matrix.runtime }} + BRANCH=${{ github.head_ref }} + + - name: Show build configuration + run: cat .docker/docker-compose.node.${{ matrix.network }}.yml + + - name: Generate launch-config-forkless-nodata.json + uses: cuchi/jinja2-action@v1.2.0 + with: + template: .docker/forkless-config/launch-config-forkless-nodata.j2 + output_file: .docker/launch-config-forkless-nodata.json + variables: | + FEATURE=${{ matrix.features }} + RUNTIME=${{ matrix.runtime }} + + - name: Show launch-config-forkless configuration + run: cat .docker/launch-config-forkless-nodata.json + + - uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: Build the stack + run: docker-compose -f ".docker/docker-compose-forkless.yml" -f ".docker/docker-compose.node.${{ matrix.network }}.yml" up -d --build --remove-orphans --force-recreate --timeout 300 + + # 🚀 POLKADOT LAUNCH COMPLETE 🚀 + - name: Check if docker logs consist messages related to testing of Node Parachain Upgrade. + if: success() + run: | + counter=160 + function check_container_status { + docker inspect -f {{.State.Running}} node-parachain + } + function do_docker_logs { + docker logs --details node-parachain 2>&1 + } + function is_started { + if [ "$(check_container_status)" == "true" ]; then + echo "Container: node-parachain RUNNING"; + echo "Check Docker logs" + DOCKER_LOGS=$(do_docker_logs) + if [[ ${DOCKER_LOGS} = *"POLKADOT LAUNCH COMPLETE"* ]];then + echo "🚀 POLKADOT LAUNCH COMPLETE 🚀" + return 0 + else + echo "Message not found in logs output, repeating..." + return 1 + fi + else + echo "Container node-parachain NOT RUNNING" + echo "Halting all future checks" + exit 1 + fi + echo "something goes wrong" + exit 1 + } + while ! is_started; do + echo "Waiting for special message in log files " + sleep 30s + counter=$(( $counter - 1 )) + echo "Counter: $counter" + if [ "$counter" -gt "0" ]; then + continue + else + break + fi + done + echo "Halting script" + exit 0 + shell: bash + + - name: Run tests before Node Parachain upgrade + working-directory: tests + run: | + yarn install + yarn add mochawesome + echo "Ready to start tests" + NOW=$(date +%s) && yarn test --reporter mochawesome --reporter-options reportFilename=test-${NOW} + env: + RPC_URL: http://127.0.0.1:9933/ + + - name: Test Report Before Node upgrade + uses: phoenix-actions/test-reporting@v8 + id: test-report-before + if: success() || failure() # run this step even if previous step failed + with: + name: Tests before node upgrade ${{ matrix.network }} # Name of the check run which will be created + path: tests/mochawesome-report/test-*.json # Path to test results + reporter: mochawesome-json + fail-on-error: 'false' + + - name: Send SIGUSR1 to polkadotlaunch process + if: success() || failure() + run: | + #Get PID of polkadot-launch + PID=$(docker exec node-parachain pidof 'polkadot-launch') + echo "Polkadot-launch PID: $PID" + #Send SIGUSR1 signal to $PID + docker exec node-parachain kill -SIGUSR1 ${PID} + + # 🌗 All parachain collators restarted with the new binaries. + - name: Check if docker logs consist messages related to testing of Node Parachain Upgrade. + if: success() + run: | + counter=160 + function check_container_status { + docker inspect -f {{.State.Running}} node-parachain + } + function do_docker_logs { + docker logs --details node-parachain 2>&1 + } + function is_started { + if [ "$(check_container_status)" == "true" ]; then + echo "Container: node-parachain RUNNING"; + echo "Check Docker logs" + DOCKER_LOGS=$(do_docker_logs) + if [[ ${DOCKER_LOGS} = *"All parachain collators restarted with the new binaries."* ]];then + echo "🌗 All parachain collators restarted with the new binaries." + return 0 + else + echo "Message not found in logs output, repeating..." + return 1 + fi + else + echo "Container node-parachain NOT RUNNING" + echo "Halting all future checks" + exit 1 + fi + echo "something goes wrong" + exit 1 + } + while ! is_started; do + echo "Waiting for special message in log files " + sleep 30s + counter=$(( $counter - 1 )) + echo "Counter: $counter" + if [ "$counter" -gt "0" ]; then + continue + else + break + fi + done + echo "Halting script" + exit 0 + shell: bash + + - name: Run tests after Node Parachain upgrade + working-directory: tests + run: | + yarn install + yarn add mochawesome + echo "Ready to start tests" + NOW=$(date +%s) && yarn test --reporter mochawesome --reporter-options reportFilename=test-${NOW} + env: + RPC_URL: http://127.0.0.1:9933/ + + - name: Test Report After Node upgrade + uses: phoenix-actions/test-reporting@v8 + id: test-report-after + if: success() || failure() # run this step even if previous step failed + with: + name: Tests after node upgrade ${{ matrix.network }} # Name of the check run which will be created + path: tests/mochawesome-report/test-*.json # Path to test results + reporter: mochawesome-json + fail-on-error: 'false' + + + - name: Stop running containers + if: always() # run this step always + run: docker-compose -f ".docker/docker-compose-forkless.yml" -f ".docker/docker-compose.node.${{ matrix.network }}.yml" down --volumes + + - name: Remove builder cache + if: always() # run this step always + run: | + docker builder prune -f + docker system prune -f + + - name: Clean Workspace + if: always() + uses: AutoModality/action-clean@v1.1.0 --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,84 @@ +name: unit tests + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + pull_request: + branches: + - develop + - master + types: + - opened + - reopened + - synchronize #commit(s) pushed to the pull request + - ready_for_review + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + + unit_tests: + # The type of runner that the job will run on + runs-on: [self-hosted-ci,medium] + timeout-minutes: 1380 + + name: ${{ github.base_ref }} + + continue-on-error: true #Do not stop testing of matrix runs failed. As it decided during PR review - it required 50/50& Let's check it with false. + + + steps: + - name: Skip if pull request is in Draft + # `if: github.event.pull_request.draft == true` should be kept here, at + # the step level, rather than at the job level. The latter is not + # recommended because when the PR is moved from "Draft" to "Ready to + # review" the workflow will immediately be passing (since it was skipped), + # even though it hasn't actually ran, since it takes a few seconds for + # the workflow to start. This is also disclosed in: + # https://github.community/t/dont-run-actions-on-draft-pull-requests/16817/17 + # That scenario would open an opportunity for the check to be bypassed: + # 1. Get your PR approved + # 2. Move it to Draft + # 3. Push whatever commits you want + # 4. Move it to "Ready for review"; now the workflow is passing (it was + # skipped) and "Check reviews" is also passing (it won't be updated + # until the workflow is finished) + if: github.event.pull_request.draft == true + run: exit 1 + + - name: Clean Workspace + uses: AutoModality/action-clean@v1.1.0 + + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} #Checking out head commit + + - name: Read .env file + uses: xom9ikk/dotenv@v1.0.2 + + - name: Generate ENV related extend file for docker-compose + uses: cuchi/jinja2-action@v1.2.0 + with: + template: .docker/docker-compose.tmp-unit.j2 + output_file: .docker/docker-compose.unit.yml + variables: | + RUST_TOOLCHAIN=${{ env.RUST_TOOLCHAIN }} + FEATURE=${{ matrix.features }} + + + - name: Show build configuration + run: cat .docker/docker-compose.unit.yml + + - name: Build the stack + run: docker-compose -f ".docker/docker-compose-dev.yaml" -f ".docker/docker-compose.unit.yml" up --build --force-recreate --timeout 300 --remove-orphans --exit-code-from node-dev + + - name: Stop running containers + if: always() # run this step always + run: docker-compose -f ".docker/docker-compose-dev.yaml" -f ".docker/docker-compose.unit.yml" down --- a/.maintain/frame-weight-template.hbs +++ b/.maintain/frame-weight-template.hbs @@ -14,6 +14,7 @@ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] #![allow(unused_imports)] +#![allow(missing_docs)] #![allow(clippy::unnecessary_cast)] use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; --- a/Cargo.lock +++ b/Cargo.lock @@ -1772,41 +1772,6 @@ ] [[package]] -name = "darling" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] name = "data-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2204,27 +2169,27 @@ [[package]] name = "evm-coder" -version = "0.1.1" +version = "0.1.3" dependencies = [ "ethereum", - "evm-coder-macros", + "evm-coder-procedural", "evm-core", "hex", "hex-literal", "impl-trait-for-tuples", "primitive-types", + "sp-std", ] [[package]] -name = "evm-coder-macros" -version = "0.1.0" +name = "evm-coder-procedural" +version = "0.2.0" dependencies = [ "Inflector", - "darling", "hex", "proc-macro2", "quote", - "sha3 0.9.1", + "sha3 0.10.2", "syn", ] @@ -2345,7 +2310,7 @@ [[package]] name = "fc-consensus" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "async-trait", "fc-db", @@ -2364,7 +2329,7 @@ [[package]] name = "fc-db" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "fp-storage", "kvdb-rocksdb", @@ -2380,7 +2345,7 @@ [[package]] name = "fc-mapping-sync" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "fc-db", "fp-consensus", @@ -2397,7 +2362,7 @@ [[package]] name = "fc-rpc" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "ethereum", "ethereum-types", @@ -2437,7 +2402,7 @@ [[package]] name = "fc-rpc-core" version = "1.1.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "ethereum", "ethereum-types", @@ -2578,7 +2543,7 @@ [[package]] name = "fp-consensus" version = "2.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "ethereum", "parity-scale-codec 3.1.5", @@ -2590,7 +2555,7 @@ [[package]] name = "fp-evm" version = "3.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "evm", "frame-support", @@ -2604,7 +2569,7 @@ [[package]] name = "fp-evm-mapping" version = "0.1.0" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "frame-support", "sp-core", @@ -2613,7 +2578,7 @@ [[package]] name = "fp-rpc" version = "3.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "ethereum", "ethereum-types", @@ -2630,7 +2595,7 @@ [[package]] name = "fp-self-contained" version = "1.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "ethereum", "frame-support", @@ -2646,7 +2611,7 @@ [[package]] name = "fp-storage" version = "2.0.0" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "parity-scale-codec 3.1.5", ] @@ -3429,12 +3394,6 @@ ] [[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] name = "idna" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5531,7 +5490,7 @@ [[package]] name = "pallet-base-fee" version = "1.0.0" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "fp-evm", "frame-support", @@ -5638,7 +5597,7 @@ [[package]] name = "pallet-common" -version = "0.1.5" +version = "0.1.8" dependencies = [ "ethereum", "evm-coder", @@ -5746,7 +5705,7 @@ [[package]] name = "pallet-ethereum" version = "4.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "ethereum", "ethereum-types", @@ -5775,7 +5734,7 @@ [[package]] name = "pallet-evm" version = "6.0.0-dev" -source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#64915c0986fcf2bbe942794b838c0cf359c93c21" +source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.27#69945f20e8dc06ab9e0dd636848a8db6a408c71c" dependencies = [ "evm", "fp-evm", @@ -5819,7 +5778,7 @@ [[package]] name = "pallet-evm-contract-helpers" -version = "0.1.2" +version = "0.2.0" dependencies = [ "evm-coder", "fp-evm-mapping", @@ -5903,7 +5862,7 @@ [[package]] name = "pallet-fungible" -version = "0.1.3" +version = "0.1.5" dependencies = [ "ethereum", "evm-coder", @@ -6145,7 +6104,7 @@ [[package]] name = "pallet-nonfungible" -version = "0.1.4" +version = "0.1.5" dependencies = [ "ethereum", "evm-coder", @@ -6267,7 +6226,7 @@ [[package]] name = "pallet-refungible" -version = "0.2.3" +version = "0.2.4" dependencies = [ "derivative", "ethereum", --- a/crates/evm-coder-macros/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "evm-coder-macros" -version = "0.1.0" -license = "GPLv3" -edition = "2021" - -[lib] -proc-macro = true - -[dependencies] -sha3 = "0.9.1" -quote = "1.0" -proc-macro2 = "1.0" -syn = { version = "1.0", features = ["full"] } -hex = "0.4.3" -Inflector = "0.11.4" -darling = "0.13.0" --- a/crates/evm-coder-macros/src/lib.rs +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -#![allow(dead_code)] - -use darling::FromMeta; -use inflector::cases; -use proc_macro::TokenStream; -use quote::quote; -use sha3::{Digest, Keccak256}; -use syn::{ - AttributeArgs, DeriveInput, GenericArgument, Ident, ItemImpl, Pat, Path, PathArguments, - PathSegment, Type, parse_macro_input, spanned::Spanned, -}; - -mod solidity_interface; -mod to_log; - -fn fn_selector_str(input: &str) -> u32 { - let mut hasher = Keccak256::new(); - hasher.update(input.as_bytes()); - let result = hasher.finalize(); - - let mut selector_bytes = [0; 4]; - selector_bytes.copy_from_slice(&result[0..4]); - - u32::from_be_bytes(selector_bytes) -} - -/// Returns solidity function selector (first 4 bytes of hash) by its -/// textual representation -/// -/// ```ignore -/// use evm_coder_macros::fn_selector; -/// -/// assert_eq!(fn_selector!(transfer(address, uint256)), 0xa9059cbb); -/// ``` -#[proc_macro] -pub fn fn_selector(input: TokenStream) -> TokenStream { - let input = input.to_string().replace(' ', ""); - let selector = fn_selector_str(&input); - - (quote! { - #selector - }) - .into() -} - -fn event_selector_str(input: &str) -> [u8; 32] { - let mut hasher = Keccak256::new(); - hasher.update(input.as_bytes()); - let result = hasher.finalize(); - - let mut selector_bytes = [0; 32]; - selector_bytes.copy_from_slice(&result[0..32]); - selector_bytes -} - -/// Returns solidity topic (hash) by its textual representation -/// -/// ```ignore -/// use evm_coder_macros::event_topic; -/// -/// assert_eq!( -/// format!("{:x}", event_topic!(Transfer(address, address, uint256))), -/// "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", -/// ); -/// ``` -#[proc_macro] -pub fn event_topic(stream: TokenStream) -> TokenStream { - let input = stream.to_string().replace(' ', ""); - let selector_bytes = event_selector_str(&input); - - (quote! { - ::primitive_types::H256([#( - #selector_bytes, - )*]) - }) - .into() -} - -fn parse_path(ty: &Type) -> syn::Result<&Path> { - match &ty { - syn::Type::Path(pat) => { - if let Some(qself) = &pat.qself { - return Err(syn::Error::new(qself.ty.span(), "no receiver expected")); - } - Ok(&pat.path) - } - _ => Err(syn::Error::new(ty.span(), "expected ty to be path")), - } -} - -fn parse_path_segment(path: &Path) -> syn::Result<&PathSegment> { - if path.segments.len() != 1 { - return Err(syn::Error::new( - path.span(), - "expected path to have only segment", - )); - } - let last_segment = &path.segments.last().unwrap(); - Ok(last_segment) -} - -fn parse_ident_from_pat(pat: &Pat) -> syn::Result<&Ident> { - match pat { - Pat::Ident(i) => Ok(&i.ident), - _ => Err(syn::Error::new(pat.span(), "expected pat ident")), - } -} - -fn parse_ident_from_segment(segment: &PathSegment, allow_generics: bool) -> syn::Result<&Ident> { - if segment.arguments != PathArguments::None && !allow_generics { - return Err(syn::Error::new( - segment.arguments.span(), - "unexpected generic type", - )); - } - Ok(&segment.ident) -} - -fn parse_ident_from_path(path: &Path, allow_generics: bool) -> syn::Result<&Ident> { - let segment = parse_path_segment(path)?; - parse_ident_from_segment(segment, allow_generics) -} - -fn parse_ident_from_type(ty: &Type, allow_generics: bool) -> syn::Result<&Ident> { - let path = parse_path(ty)?; - parse_ident_from_path(path, allow_generics) -} - -// Gets T out of Result -fn parse_result_ok(ty: &Type) -> syn::Result<&Type> { - let path = parse_path(ty)?; - let segment = parse_path_segment(path)?; - - if segment.ident != "Result" { - return Err(syn::Error::new( - ty.span(), - "expected Result as return type (no renamed aliases allowed)", - )); - } - let args = match &segment.arguments { - PathArguments::AngleBracketed(e) => e, - _ => { - return Err(syn::Error::new( - segment.arguments.span(), - "missing Result generics", - )) - } - }; - - let args = &args.args; - let arg = args.first().unwrap(); - - let ty = match arg { - GenericArgument::Type(ty) => ty, - _ => { - return Err(syn::Error::new( - arg.span(), - "expected first generic to be type", - )) - } - }; - - Ok(ty) -} - -fn pascal_ident_to_call(ident: &Ident) -> Ident { - let name = format!("{}Call", ident); - Ident::new(&name, ident.span()) -} -fn snake_ident_to_pascal(ident: &Ident) -> Ident { - let name = ident.to_string(); - let name = cases::pascalcase::to_pascal_case(&name); - Ident::new(&name, ident.span()) -} -fn snake_ident_to_screaming(ident: &Ident) -> Ident { - let name = ident.to_string(); - let name = cases::screamingsnakecase::to_screaming_snake_case(&name); - Ident::new(&name, ident.span()) -} -fn pascal_ident_to_snake_call(ident: &Ident) -> Ident { - let name = ident.to_string(); - let name = cases::snakecase::to_snake_case(&name); - let name = format!("call_{}", name); - Ident::new(&name, ident.span()) -} - -/// Derives call enum implementing [`evm_coder::Callable`], [`evm_coder::Weighted`] -/// and [`evm_coder::Call`] from impl block -/// -/// ## Macro syntax -/// -/// `#[solidity_interface(name, is, inline_is, events)]` -/// - *name*: used in generated code, and for Call enum name -/// - *is*: used to provide call inheritance, not found methods will be delegated to all contracts -/// specified in is/inline_is -/// - *inline_is*: same as is, but selectors for passed contracts will be used by derived ERC165 -/// implementation -/// -/// `#[weight(value)]` -/// Can be added to every method of impl block, used for deriving [`evm_coder::Weighted`], which -/// is used by substrate bridge -/// - *value*: expression, which evaluates to weight required to call this method. -/// This expression can use call arguments to calculate non-constant execution time. -/// This expression should evaluate faster than actual execution does, and may provide worser case -/// than one is called -/// -/// `#[solidity_interface(rename_selector)]` -/// - *rename_selector*: by default, selector name will be generated by transforming method name -/// from snake_case to camelCase. Use this option, if other naming convention is required. -/// I.e: method `token_uri` will be automatically renamed to `tokenUri` in selector, but name -/// required by ERC721 standard is `tokenURI`, thus we need to specify `rename_selector = "tokenURI"` -/// explicitly -/// -/// Also, any contract method may have doc comments, which will be automatically added to generated -/// solidity interface definitions -/// -/// ## Example -/// -/// ```ignore -/// struct SuperContract; -/// struct InlineContract; -/// struct Contract; -/// -/// #[derive(ToLog)] -/// enum ContractEvents { -/// Event(#[indexed] uint32), -/// } -/// -/// #[solidity_interface(name = "MyContract", is(SuperContract), inline_is(InlineContract))] -/// impl Contract { -/// /// Multiply two numbers -/// #[weight(200 + a + b)] -/// #[solidity_interface(rename_selector = "mul")] -/// fn mul(&mut self, a: uint32, b: uint32) -> Result { -/// Ok(a.checked_mul(b).ok_or("overflow")?) -/// } -/// } -/// ``` -#[proc_macro_attribute] -pub fn solidity_interface(args: TokenStream, stream: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as AttributeArgs); - let args = solidity_interface::InterfaceInfo::from_list(&args).unwrap(); - - let input: ItemImpl = match syn::parse(stream) { - Ok(t) => t, - Err(e) => return e.to_compile_error().into(), - }; - - let expanded = match solidity_interface::SolidityInterface::try_from(args, &input) { - Ok(v) => v.expand(), - Err(e) => e.to_compile_error(), - }; - - (quote! { - #input - - #expanded - }) - .into() -} - -#[proc_macro_attribute] -pub fn solidity(_args: TokenStream, stream: TokenStream) -> TokenStream { - stream -} -#[proc_macro_attribute] -pub fn weight(_args: TokenStream, stream: TokenStream) -> TokenStream { - stream -} - -/// ## Syntax -/// -/// `#[indexed]` -/// Marks this field as indexed, so it will appear in [`ethereum::Log`] topics instead of data -#[proc_macro_derive(ToLog, attributes(indexed))] -pub fn to_log(value: TokenStream) -> TokenStream { - let input = parse_macro_input!(value as DeriveInput); - - match to_log::Events::try_from(&input) { - Ok(e) => e.expand(), - Err(e) => e.to_compile_error(), - } - .into() -} --- a/crates/evm-coder-macros/src/solidity_interface.rs +++ /dev/null @@ -1,1005 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -#![allow(dead_code)] - -use quote::quote; -use darling::{FromMeta, ToTokens}; -use inflector::cases; -use std::fmt::Write; -use syn::{ - Expr, FnArg, GenericArgument, Generics, Ident, ImplItem, ImplItemMethod, ItemImpl, Lit, Meta, - MetaNameValue, NestedMeta, PatType, Path, PathArguments, ReturnType, Type, spanned::Spanned, - parse_str, -}; - -use crate::{ - fn_selector_str, parse_ident_from_pat, parse_ident_from_path, parse_path, parse_path_segment, - parse_result_ok, pascal_ident_to_call, pascal_ident_to_snake_call, snake_ident_to_pascal, - snake_ident_to_screaming, -}; - -struct Is { - name: Ident, - pascal_call_name: Ident, - snake_call_name: Ident, - via: Option<(Type, Ident)>, -} -impl Is { - fn new_via(path: &Path, via: Option<(Type, Ident)>) -> syn::Result { - let name = parse_ident_from_path(path, false)?.clone(); - Ok(Self { - pascal_call_name: pascal_ident_to_call(&name), - snake_call_name: pascal_ident_to_snake_call(&name), - name, - via, - }) - } - fn new(path: &Path) -> syn::Result { - Self::new_via(path, None) - } - - fn expand_call_def(&self, gen_ref: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { - let name = &self.name; - let pascal_call_name = &self.pascal_call_name; - quote! { - #name(#pascal_call_name #gen_ref) - } - } - - fn expand_interface_id(&self) -> proc_macro2::TokenStream { - let pascal_call_name = &self.pascal_call_name; - quote! { - interface_id ^= u32::from_be_bytes(#pascal_call_name::interface_id()); - } - } - - fn expand_supports_interface( - &self, - generics: &proc_macro2::TokenStream, - ) -> proc_macro2::TokenStream { - let pascal_call_name = &self.pascal_call_name; - quote! { - <#pascal_call_name #generics>::supports_interface(interface_id) - } - } - - fn expand_variant_weight(&self) -> proc_macro2::TokenStream { - let name = &self.name; - quote! { - Self::#name(call) => call.weight() - } - } - - fn expand_variant_call( - &self, - call_name: &proc_macro2::Ident, - generics: &proc_macro2::TokenStream, - ) -> proc_macro2::TokenStream { - let name = &self.name; - let pascal_call_name = &self.pascal_call_name; - let via_typ = self - .via - .as_ref() - .map(|(t, _)| quote! {#t}) - .unwrap_or_else(|| quote! {Self}); - let via_map = self - .via - .as_ref() - .map(|(_, i)| quote! {.#i()}) - .unwrap_or_default(); - quote! { - #call_name::#name(call) => return <#via_typ as ::evm_coder::Callable<#pascal_call_name #generics>>::call(self #via_map, Msg { - call, - caller: c.caller, - value: c.value, - }) - } - } - - fn expand_parse(&self, generics: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { - let name = &self.name; - let pascal_call_name = &self.pascal_call_name; - quote! { - if let Some(parsed_call) = <#pascal_call_name #generics>::parse(method_id, reader)? { - return Ok(Some(Self::#name(parsed_call))) - } - } - } - - fn expand_generator(&self, generics: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { - let pascal_call_name = &self.pascal_call_name; - quote! { - <#pascal_call_name #generics>::generate_solidity_interface(tc, is_impl); - } - } - - fn expand_event_generator(&self) -> proc_macro2::TokenStream { - let name = &self.name; - quote! { - #name::generate_solidity_interface(tc, is_impl); - } - } -} - -#[derive(Default)] -struct IsList(Vec); -impl FromMeta for IsList { - fn from_list(items: &[NestedMeta]) -> darling::Result { - let mut out = Vec::new(); - for item in items { - match item { - NestedMeta::Meta(Meta::Path(path)) => out.push(Is::new(path)?), - // TODO: replace meta parsing with manual - NestedMeta::Meta(Meta::List(list)) - if list.path.is_ident("via") && list.nested.len() == 3 => - { - let mut data = list.nested.iter(); - let typ = match data.next().expect("len == 3") { - NestedMeta::Lit(Lit::Str(s)) => { - let v = s.value(); - let typ: Type = parse_str(&v)?; - typ - } - _ => { - return Err(syn::Error::new( - item.span(), - "via typ should be type in string", - ) - .into()) - } - }; - let via = match data.next().expect("len == 3") { - NestedMeta::Meta(Meta::Path(path)) => path - .get_ident() - .ok_or_else(|| syn::Error::new(item.span(), "via should be ident"))?, - _ => return Err(syn::Error::new(item.span(), "via should be ident").into()), - }; - let path = match data.next().expect("len == 3") { - NestedMeta::Meta(Meta::Path(path)) => path, - _ => return Err(syn::Error::new(item.span(), "path should be path").into()), - }; - - out.push(Is::new_via(path, Some((typ, via.clone())))?) - } - _ => { - return Err(syn::Error::new( - item.span(), - "expected either Name or via(\"Type\", getter, Name)", - ) - .into()) - } - } - } - Ok(Self(out)) - } -} - -#[derive(FromMeta)] -pub struct InterfaceInfo { - name: Ident, - #[darling(default)] - is: IsList, - #[darling(default)] - inline_is: IsList, - #[darling(default)] - events: IsList, -} - -#[derive(FromMeta)] -struct MethodInfo { - #[darling(default)] - rename_selector: Option, -} - -enum AbiType { - // type - Plain(Ident), - // (type1,type2) - Tuple(Vec), - // type[] - Vec(Box), - // type[20] - Array(Box, usize), -} -impl AbiType { - fn try_from(value: &Type) -> syn::Result { - let value = Self::try_maybe_special_from(value)?; - if value.is_special() { - return Err(syn::Error::new(value.span(), "unexpected special type")); - } - Ok(value) - } - fn try_maybe_special_from(value: &Type) -> syn::Result { - match value { - Type::Array(arr) => { - let wrapped = AbiType::try_from(&arr.elem)?; - match &arr.len { - Expr::Lit(l) => match &l.lit { - Lit::Int(i) => { - let num = i.base10_parse::()?; - Ok(AbiType::Array(Box::new(wrapped), num as usize)) - } - _ => Err(syn::Error::new(arr.len.span(), "should be int literal")), - }, - _ => Err(syn::Error::new(arr.len.span(), "should be literal")), - } - } - Type::Path(_) => { - let path = parse_path(value)?; - let segment = parse_path_segment(path)?; - if segment.ident == "Vec" { - let args = match &segment.arguments { - PathArguments::AngleBracketed(e) => e, - _ => { - return Err(syn::Error::new( - segment.arguments.span(), - "missing Vec generic", - )) - } - }; - let args = &args.args; - if args.len() != 1 { - return Err(syn::Error::new( - args.span(), - "expected only one generic for vec", - )); - } - let arg = args.first().unwrap(); - - let ty = match arg { - GenericArgument::Type(ty) => ty, - _ => { - return Err(syn::Error::new( - arg.span(), - "expected first generic to be type", - )) - } - }; - - let wrapped = AbiType::try_from(ty)?; - Ok(Self::Vec(Box::new(wrapped))) - } else { - if !segment.arguments.is_empty() { - return Err(syn::Error::new( - segment.arguments.span(), - "unexpected generic arguments for non-vec type", - )); - } - Ok(Self::Plain(segment.ident.clone())) - } - } - Type::Tuple(t) => { - let mut out = Vec::with_capacity(t.elems.len()); - for el in t.elems.iter() { - out.push(AbiType::try_from(el)?) - } - Ok(Self::Tuple(out)) - } - _ => Err(syn::Error::new( - value.span(), - "unexpected type, only arrays, plain types and tuples are supported", - )), - } - } - fn is_value(&self) -> bool { - matches!(self, Self::Plain(v) if v == "value") - } - fn is_caller(&self) -> bool { - matches!(self, Self::Plain(v) if v == "caller") - } - fn is_special(&self) -> bool { - self.is_caller() || self.is_value() - } - fn selector_ty_buf(&self, buf: &mut String) -> std::fmt::Result { - match self { - AbiType::Plain(t) => { - write!(buf, "{}", t) - } - AbiType::Tuple(t) => { - write!(buf, "(")?; - for (i, t) in t.iter().enumerate() { - if i != 0 { - write!(buf, ",")?; - } - t.selector_ty_buf(buf)?; - } - write!(buf, ")") - } - AbiType::Vec(v) => { - v.selector_ty_buf(buf)?; - write!(buf, "[]") - } - AbiType::Array(v, len) => { - v.selector_ty_buf(buf)?; - write!(buf, "[{}]", len) - } - } - } - fn selector_ty(&self) -> String { - let mut out = String::new(); - self.selector_ty_buf(&mut out).expect("no fmt error"); - out - } -} -impl ToTokens for AbiType { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - match self { - AbiType::Plain(t) => tokens.extend(quote! {#t}), - AbiType::Tuple(t) => { - tokens.extend(quote! {( - #(#t),* - )}); - } - AbiType::Vec(v) => tokens.extend(quote! {Vec<#v>}), - AbiType::Array(v, l) => tokens.extend(quote! {[#v; #l]}), - } - } -} - -struct MethodArg { - name: Ident, - camel_name: String, - ty: AbiType, -} -impl MethodArg { - fn try_from(value: &PatType) -> syn::Result { - let name = parse_ident_from_pat(&value.pat)?.clone(); - Ok(Self { - camel_name: cases::camelcase::to_camel_case(&name.to_string()), - name, - ty: AbiType::try_maybe_special_from(&value.ty)?, - }) - } - fn is_value(&self) -> bool { - self.ty.is_value() - } - fn is_caller(&self) -> bool { - self.ty.is_caller() - } - fn is_special(&self) -> bool { - self.ty.is_special() - } - fn selector_ty(&self) -> String { - assert!(!self.is_special()); - self.ty.selector_ty() - } - - fn expand_call_def(&self) -> proc_macro2::TokenStream { - assert!(!self.is_special()); - let name = &self.name; - let ty = &self.ty; - - quote! { - #name: #ty - } - } - - fn expand_parse(&self) -> proc_macro2::TokenStream { - assert!(!self.is_special()); - let name = &self.name; - quote! { - #name: reader.abi_read()? - } - } - - fn expand_call_arg(&self) -> proc_macro2::TokenStream { - if self.is_value() { - quote! { - c.value.clone() - } - } else if self.is_caller() { - quote! { - c.caller.clone() - } - } else { - let name = &self.name; - quote! { - #name - } - } - } - - fn expand_solidity_argument(&self) -> proc_macro2::TokenStream { - let camel_name = &self.camel_name.to_string(); - let ty = &self.ty; - quote! { - >::new(#camel_name) - } - } -} - -#[derive(PartialEq)] -enum Mutability { - Mutable, - View, - Pure, -} - -pub struct WeightAttr(syn::Expr); - -mod keyword { - syn::custom_keyword!(weight); -} - -impl syn::parse::Parse for WeightAttr { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - input.parse::()?; - let content; - syn::bracketed!(content in input); - content.parse::()?; - - let weight_content; - syn::parenthesized!(weight_content in content); - Ok(WeightAttr(weight_content.parse::()?)) - } -} - -struct Method { - name: Ident, - camel_name: String, - pascal_name: Ident, - screaming_name: Ident, - selector_str: String, - selector: u32, - args: Vec, - has_normal_args: bool, - mutability: Mutability, - result: Type, - weight: Option, - docs: Vec, -} -impl Method { - fn try_from(value: &ImplItemMethod) -> syn::Result { - let mut info = MethodInfo { - rename_selector: None, - }; - let mut docs = Vec::new(); - let mut weight = None; - for attr in &value.attrs { - let ident = parse_ident_from_path(&attr.path, false)?; - if ident == "solidity" { - let args = attr.parse_meta().unwrap(); - info = MethodInfo::from_meta(&args).unwrap(); - } else if ident == "doc" { - let args = attr.parse_meta().unwrap(); - let value = match args { - Meta::NameValue(MetaNameValue { - lit: Lit::Str(str), .. - }) => str.value(), - _ => unreachable!(), - }; - docs.push(value); - } else if ident == "weight" { - weight = Some(syn::parse2::(attr.to_token_stream())?.0); - } - } - let ident = &value.sig.ident; - let ident_str = ident.to_string(); - if !cases::snakecase::is_snake_case(&ident_str) { - return Err(syn::Error::new(ident.span(), "method name should be snake_cased\nif alternative solidity name needs to be set - use #[solidity] attribute")); - } - - let mut mutability = Mutability::Pure; - - if let Some(FnArg::Receiver(receiver)) = value - .sig - .inputs - .iter() - .find(|arg| matches!(arg, FnArg::Receiver(_))) - { - if receiver.reference.is_none() { - return Err(syn::Error::new( - receiver.span(), - "receiver should be by ref", - )); - } - if receiver.mutability.is_some() { - mutability = Mutability::Mutable; - } else { - mutability = Mutability::View; - } - } - let mut args = Vec::new(); - for typ in value - .sig - .inputs - .iter() - .filter(|arg| matches!(arg, FnArg::Typed(_))) - { - let typ = match typ { - FnArg::Typed(typ) => typ, - _ => unreachable!(), - }; - args.push(MethodArg::try_from(typ)?); - } - - if mutability != Mutability::Mutable && args.iter().any(|arg| arg.is_value()) { - return Err(syn::Error::new( - args.iter().find(|arg| arg.is_value()).unwrap().ty.span(), - "payable function should be mutable", - )); - } - - let result = match &value.sig.output { - ReturnType::Type(_, ty) => ty, - _ => return Err(syn::Error::new(value.sig.output.span(), "interface method should return Result\nif there is no value to return - specify void (which is alias to unit)")), - }; - let result = parse_result_ok(result)?; - - let camel_name = info - .rename_selector - .unwrap_or_else(|| cases::camelcase::to_camel_case(&ident.to_string())); - let mut selector_str = camel_name.clone(); - selector_str.push('('); - let mut has_normal_args = false; - for (i, arg) in args.iter().filter(|arg| !arg.is_special()).enumerate() { - if i != 0 { - selector_str.push(','); - } - write!(selector_str, "{}", arg.selector_ty()).unwrap(); - has_normal_args = true; - } - selector_str.push(')'); - let selector = fn_selector_str(&selector_str); - - Ok(Self { - name: ident.clone(), - camel_name, - pascal_name: snake_ident_to_pascal(ident), - screaming_name: snake_ident_to_screaming(ident), - selector_str, - selector, - args, - has_normal_args, - mutability, - result: result.clone(), - weight, - docs, - }) - } - fn expand_call_def(&self) -> proc_macro2::TokenStream { - let defs = self - .args - .iter() - .filter(|a| !a.is_special()) - .map(|a| a.expand_call_def()); - let pascal_name = &self.pascal_name; - - if self.has_normal_args { - quote! { - #pascal_name { - #( - #defs, - )* - } - } - } else { - quote! {#pascal_name} - } - } - - fn expand_const(&self) -> proc_macro2::TokenStream { - let screaming_name = &self.screaming_name; - let selector = u32::to_be_bytes(self.selector); - let selector_str = &self.selector_str; - quote! { - #[doc = #selector_str] - const #screaming_name: ::evm_coder::types::bytes4 = [#(#selector,)*]; - } - } - - fn expand_interface_id(&self) -> proc_macro2::TokenStream { - let screaming_name = &self.screaming_name; - quote! { - interface_id ^= u32::from_be_bytes(Self::#screaming_name); - } - } - - fn expand_parse(&self) -> proc_macro2::TokenStream { - let pascal_name = &self.pascal_name; - let screaming_name = &self.screaming_name; - if self.has_normal_args { - let parsers = self - .args - .iter() - .filter(|a| !a.is_special()) - .map(|a| a.expand_parse()); - quote! { - Self::#screaming_name => return Ok(Some(Self::#pascal_name { - #( - #parsers, - )* - })) - } - } else { - quote! { Self::#screaming_name => return Ok(Some(Self::#pascal_name)) } - } - } - - fn expand_variant_call(&self, call_name: &proc_macro2::Ident) -> proc_macro2::TokenStream { - let pascal_name = &self.pascal_name; - let name = &self.name; - - let matcher = if self.has_normal_args { - let names = self - .args - .iter() - .filter(|a| !a.is_special()) - .map(|a| &a.name); - - quote! {{ - #( - #names, - )* - }} - } else { - quote! {} - }; - - let receiver = match self.mutability { - Mutability::Mutable | Mutability::View => quote! {self.}, - Mutability::Pure => quote! {Self::}, - }; - let args = self.args.iter().map(|a| a.expand_call_arg()); - - quote! { - #call_name::#pascal_name #matcher => { - let result = #receiver #name( - #( - #args, - )* - )?; - (&result).to_result() - } - } - } - - fn expand_variant_weight(&self) -> proc_macro2::TokenStream { - let pascal_name = &self.pascal_name; - if let Some(weight) = &self.weight { - let matcher = if self.has_normal_args { - let names = self - .args - .iter() - .filter(|a| !a.is_special()) - .map(|a| &a.name); - - quote! {{ - #( - #names, - )* - }} - } else { - quote! {} - }; - quote! { - Self::#pascal_name #matcher => (#weight).into() - } - } else { - let matcher = if self.has_normal_args { - quote! {{..}} - } else { - quote! {} - }; - quote! { - Self::#pascal_name #matcher => ().into() - } - } - } - - fn expand_solidity_function(&self) -> proc_macro2::TokenStream { - let camel_name = &self.camel_name; - let mutability = match self.mutability { - Mutability::Mutable => quote! {SolidityMutability::Mutable}, - Mutability::View => quote! { SolidityMutability::View }, - Mutability::Pure => quote! {SolidityMutability::Pure}, - }; - let result = &self.result; - - let args = self - .args - .iter() - .filter(|a| !a.is_special()) - .map(MethodArg::expand_solidity_argument); - let docs = self.docs.iter(); - let selector = format!("{} {:0>8x}", self.selector_str, self.selector); - - quote! { - SolidityFunction { - docs: &[#(#docs),*], - selector: #selector, - name: #camel_name, - mutability: #mutability, - args: ( - #( - #args, - )* - ), - result: >::default(), - } - } - } -} - -fn generics_list(gen: &Generics) -> proc_macro2::TokenStream { - if gen.params.is_empty() { - return quote! {}; - } - let params = gen.params.iter().map(|p| match p { - syn::GenericParam::Type(id) => { - let v = &id.ident; - quote! {#v} - } - syn::GenericParam::Lifetime(lt) => { - let v = <.lifetime; - quote! {#v} - } - syn::GenericParam::Const(c) => { - let i = &c.ident; - quote! {#i} - } - }); - quote! { #(#params),* } -} -fn generics_reference(gen: &Generics) -> proc_macro2::TokenStream { - if gen.params.is_empty() { - return quote! {}; - } - let list = generics_list(gen); - quote! { <#list> } -} -fn generics_data(gen: &Generics) -> proc_macro2::TokenStream { - let list = generics_list(gen); - if gen.params.len() == 1 { - quote! {#list} - } else { - quote! { (#list) } - } -} - -pub struct SolidityInterface { - generics: Generics, - name: Box, - info: InterfaceInfo, - methods: Vec, -} -impl SolidityInterface { - pub fn try_from(info: InterfaceInfo, value: &ItemImpl) -> syn::Result { - let mut methods = Vec::new(); - - for item in &value.items { - if let ImplItem::Method(method) = item { - methods.push(Method::try_from(method)?) - } - } - Ok(Self { - generics: value.generics.clone(), - name: value.self_ty.clone(), - info, - methods, - }) - } - pub fn expand(self) -> proc_macro2::TokenStream { - let name = self.name; - - let solidity_name = self.info.name.to_string(); - let call_name = pascal_ident_to_call(&self.info.name); - let generics = self.generics; - let gen_ref = generics_reference(&generics); - let gen_data = generics_data(&generics); - let gen_where = &generics.where_clause; - - let call_sub = self - .info - .inline_is - .0 - .iter() - .chain(self.info.is.0.iter()) - .map(|c| Is::expand_call_def(c, &gen_ref)); - let call_parse = self - .info - .inline_is - .0 - .iter() - .chain(self.info.is.0.iter()) - .map(|is| Is::expand_parse(is, &gen_ref)); - let call_variants = self - .info - .inline_is - .0 - .iter() - .chain(self.info.is.0.iter()) - .map(|c| Is::expand_variant_call(c, &call_name, &gen_ref)); - let weight_variants = self - .info - .inline_is - .0 - .iter() - .chain(self.info.is.0.iter()) - .map(Is::expand_variant_weight); - - let inline_interface_id = self.info.inline_is.0.iter().map(Is::expand_interface_id); - let supports_interface = self - .info - .is - .0 - .iter() - .map(|is| Is::expand_supports_interface(is, &gen_ref)); - - let calls = self.methods.iter().map(Method::expand_call_def); - let consts = self.methods.iter().map(Method::expand_const); - let interface_id = self.methods.iter().map(Method::expand_interface_id); - let parsers = self.methods.iter().map(Method::expand_parse); - let call_variants_this = self - .methods - .iter() - .map(|m| Method::expand_variant_call(m, &call_name)); - let weight_variants_this = self.methods.iter().map(Method::expand_variant_weight); - let solidity_functions = self.methods.iter().map(Method::expand_solidity_function); - - // TODO: Inline inline_is - let solidity_is = self - .info - .is - .0 - .iter() - .chain(self.info.inline_is.0.iter()) - .map(|is| is.name.to_string()); - let solidity_events_is = self.info.events.0.iter().map(|is| is.name.to_string()); - let solidity_generators = self - .info - .is - .0 - .iter() - .chain(self.info.inline_is.0.iter()) - .map(|is| Is::expand_generator(is, &gen_ref)); - let solidity_event_generators = self.info.events.0.iter().map(Is::expand_event_generator); - - // let methods = self.methods.iter().map(Method::solidity_def); - - quote! { - #[derive(Debug)] - pub enum #call_name #gen_ref { - ERC165Call(::evm_coder::ERC165Call, ::core::marker::PhantomData<#gen_data>), - #( - #calls, - )* - #( - #call_sub, - )* - } - impl #gen_ref #call_name #gen_ref { - #( - #consts - )* - pub fn interface_id() -> ::evm_coder::types::bytes4 { - let mut interface_id = 0; - #(#interface_id)* - #(#inline_interface_id)* - u32::to_be_bytes(interface_id) - } - pub fn supports_interface(interface_id: ::evm_coder::types::bytes4) -> bool { - interface_id != u32::to_be_bytes(0xffffff) && ( - interface_id == ::evm_coder::ERC165Call::INTERFACE_ID || - interface_id == Self::interface_id() - #( - || #supports_interface - )* - ) - } - pub fn generate_solidity_interface(tc: &evm_coder::solidity::TypeCollector, is_impl: bool) { - use evm_coder::solidity::*; - use core::fmt::Write; - let interface = SolidityInterface { - name: #solidity_name, - selector: Self::interface_id(), - is: &["Dummy", "ERC165", #( - #solidity_is, - )* #( - #solidity_events_is, - )* ], - functions: (#( - #solidity_functions, - )*), - }; - if is_impl { - tc.collect("// Common stubs holder\ncontract Dummy {\n\tuint8 dummy;\n\tstring stub_error = \"this contract is implemented in native\";\n}\ncontract ERC165 is Dummy {\n\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool) {\n\t\trequire(false, stub_error);\n\t\tinterfaceID;\n\t\treturn true;\n\t}\n}\n".into()); - } else { - tc.collect("// Common stubs holder\ninterface Dummy {\n}\ninterface ERC165 is Dummy {\n\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool);\n}\n".into()); - } - #( - #solidity_generators - )* - #( - #solidity_event_generators - )* - - let mut out = string::new(); - // In solidity interface usage (is) should be preceeded by interface definition - // This comment helps to sort it in a set - if #solidity_name.starts_with("Inline") { - out.push_str("// Inline\n"); - } - let _ = interface.format(is_impl, &mut out, tc); - tc.collect(out); - } - } - impl #gen_ref ::evm_coder::Call for #call_name #gen_ref { - fn parse(method_id: ::evm_coder::types::bytes4, reader: &mut ::evm_coder::abi::AbiReader) -> ::evm_coder::execution::Result> { - use ::evm_coder::abi::AbiRead; - match method_id { - ::evm_coder::ERC165Call::INTERFACE_ID => return Ok( - ::evm_coder::ERC165Call::parse(method_id, reader)? - .map(|c| Self::ERC165Call(c, ::core::marker::PhantomData)) - ), - #( - #parsers, - )* - _ => {}, - } - #( - #call_parse - )else* - return Ok(None); - } - } - impl #generics ::evm_coder::Weighted for #call_name #gen_ref - #gen_where - { - #[allow(unused_variables)] - fn weight(&self) -> ::evm_coder::execution::DispatchInfo { - match self { - #( - #weight_variants, - )* - // TODO: It should be very cheap, but not free - Self::ERC165Call(::evm_coder::ERC165Call::SupportsInterface {..}, _) => 100u64.into(), - #( - #weight_variants_this, - )* - } - } - } - impl #generics ::evm_coder::Callable<#call_name #gen_ref> for #name - #gen_where - { - #[allow(unreachable_code)] // In case of no inner calls - fn call(&mut self, c: Msg<#call_name #gen_ref>) -> ::evm_coder::execution::ResultWithPostInfo<::evm_coder::abi::AbiWriter> { - use ::evm_coder::abi::AbiWrite; - match c.call { - #( - #call_variants, - )* - #call_name::ERC165Call(::evm_coder::ERC165Call::SupportsInterface {interface_id}, _) => { - let mut writer = ::evm_coder::abi::AbiWriter::default(); - writer.bool(&<#call_name #gen_ref>::supports_interface(interface_id)); - return Ok(writer.into()); - } - _ => {}, - } - let mut writer = ::evm_coder::abi::AbiWriter::default(); - match c.call { - #( - #call_variants_this, - )* - _ => unreachable!() - } - } - } - } - } -} --- a/crates/evm-coder-macros/src/to_log.rs +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -use inflector::cases; -use syn::{Data, DeriveInput, Field, Fields, Ident, Variant, spanned::Spanned}; -use std::fmt::Write; -use quote::quote; - -use crate::{parse_ident_from_path, parse_ident_from_type, snake_ident_to_screaming}; - -struct EventField { - name: Ident, - camel_name: String, - ty: Ident, - indexed: bool, -} - -impl EventField { - fn try_from(field: &Field) -> syn::Result { - let name = field.ident.as_ref().unwrap(); - let ty = parse_ident_from_type(&field.ty, false)?; - let mut indexed = false; - for attr in &field.attrs { - if let Ok(ident) = parse_ident_from_path(&attr.path, false) { - if ident == "indexed" { - indexed = true; - } - } - } - Ok(Self { - name: name.to_owned(), - camel_name: cases::camelcase::to_camel_case(&name.to_string()), - ty: ty.to_owned(), - indexed, - }) - } - fn expand_solidity_argument(&self) -> proc_macro2::TokenStream { - let camel_name = &self.camel_name; - let ty = &self.ty; - let indexed = self.indexed; - quote! { - >::new(#indexed, #camel_name) - } - } -} - -struct Event { - name: Ident, - name_screaming: Ident, - fields: Vec, - selector: [u8; 32], - selector_str: String, -} - -impl Event { - fn try_from(variant: &Variant) -> syn::Result { - let name = &variant.ident; - let name_screaming = snake_ident_to_screaming(name); - - let named = match &variant.fields { - Fields::Named(named) => named, - _ => { - return Err(syn::Error::new( - variant.fields.span(), - "expected named fields", - )) - } - }; - let mut fields = Vec::new(); - for field in &named.named { - fields.push(EventField::try_from(field)?); - } - if fields.iter().filter(|f| f.indexed).count() > 3 { - return Err(syn::Error::new( - variant.fields.span(), - "events can have at most 4 indexed fields (1 indexed field is reserved for event signature)" - )); - } - let mut selector_str = format!("{}(", name); - for (i, arg) in fields.iter().enumerate() { - if i != 0 { - write!(selector_str, ",").unwrap(); - } - write!(selector_str, "{}", arg.ty).unwrap(); - } - selector_str.push(')'); - let selector = crate::event_selector_str(&selector_str); - - Ok(Self { - name: name.to_owned(), - name_screaming, - fields, - selector, - selector_str, - }) - } - - fn expand_serializers(&self) -> proc_macro2::TokenStream { - let name = &self.name; - let name_screaming = &self.name_screaming; - let fields = self.fields.iter().map(|f| &f.name); - - let indexed = self.fields.iter().filter(|f| f.indexed).map(|f| &f.name); - let plain = self.fields.iter().filter(|f| !f.indexed).map(|f| &f.name); - - quote! { - Self::#name {#( - #fields, - )*} => { - topics.push(topic::from(Self::#name_screaming)); - #( - topics.push(#indexed.to_topic()); - )* - #( - #plain.abi_write(&mut writer); - )* - } - } - } - - fn expand_consts(&self) -> proc_macro2::TokenStream { - let name_screaming = &self.name_screaming; - let selector_str = &self.selector_str; - let selector = &self.selector; - - quote! { - #[doc = #selector_str] - const #name_screaming: [u8; 32] = [#( - #selector, - )*]; - } - } - - fn expand_solidity_function(&self) -> proc_macro2::TokenStream { - let name = self.name.to_string(); - let args = self.fields.iter().map(EventField::expand_solidity_argument); - quote! { - SolidityEvent { - name: #name, - args: ( - #( - #args, - )* - ), - } - } - } -} - -pub struct Events { - name: Ident, - events: Vec, -} - -impl Events { - pub fn try_from(data: &DeriveInput) -> syn::Result { - let name = &data.ident; - let en = match &data.data { - Data::Enum(en) => en, - _ => return Err(syn::Error::new(data.span(), "expected enum")), - }; - let mut events = Vec::new(); - for variant in &en.variants { - events.push(Event::try_from(variant)?); - } - Ok(Self { - name: name.to_owned(), - events, - }) - } - pub fn expand(&self) -> proc_macro2::TokenStream { - let name = &self.name; - - let consts = self.events.iter().map(Event::expand_consts); - let serializers = self.events.iter().map(Event::expand_serializers); - let solidity_name = self.name.to_string(); - let solidity_functions = self.events.iter().map(Event::expand_solidity_function); - - quote! { - impl #name { - #( - #consts - )* - - pub fn generate_solidity_interface(tc: &evm_coder::solidity::TypeCollector, is_impl: bool) { - use evm_coder::solidity::*; - use core::fmt::Write; - let interface = SolidityInterface { - selector: [0; 4], - name: #solidity_name, - is: &[], - functions: (#( - #solidity_functions, - )*), - }; - let mut out = string::new(); - out.push_str("// Inline\n"); - let _ = interface.format(is_impl, &mut out, tc); - tc.collect(out); - } - } - - #[automatically_derived] - impl ::evm_coder::events::ToLog for #name { - fn to_log(&self, contract: address) -> ::ethereum::Log { - use ::evm_coder::events::ToTopic; - use ::evm_coder::abi::AbiWrite; - let mut writer = ::evm_coder::abi::AbiWriter::new(); - let mut topics = Vec::new(); - match self { - #( - #serializers, - )* - } - ::ethereum::Log { - address: contract, - topics, - data: writer.finish(), - } - } - } - } - } -} --- a/crates/evm-coder/CHANGELOG.md +++ b/crates/evm-coder/CHANGELOG.md @@ -1,4 +1,24 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## [0.1.3] - 2022-08-29 + +### Fixed + + - Parsing simple values. + +## [v0.1.2] 2022-08-19 + +### Added + + - Implementation `AbiWrite` for tuples. + + ### Fixes + + - Tuple generation for solidity. + ## [v0.1.1] 2022-08-16 ### Other changes @@ -7,4 +27,4 @@ - build: Upgrade polkadot to v0.9.26 85515e54c4ca1b82a2630034e55dcc804c643bf8 -- build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b \ No newline at end of file +- build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b --- a/crates/evm-coder/Cargo.toml +++ b/crates/evm-coder/Cargo.toml @@ -1,19 +1,26 @@ [package] name = "evm-coder" -version = "0.1.1" +version = "0.1.3" license = "GPLv3" edition = "2021" [dependencies] -evm-coder-macros = { path = "../evm-coder-macros" } +# evm-coder reexports those proc-macro +evm-coder-procedural = { path = "./procedural" } +# Evm uses primitive-types for H160, H256 and others primitive-types = { version = "0.11.1", default-features = false } -hex-literal = "0.3.3" +# Evm doesn't have reexports for log and others ethereum = { version = "0.12.0", default-features = false } -evm-core = { default-features = false, git = "https://github.com/uniquenetwork/evm", branch = "unique-polkadot-v0.9.27" } -impl-trait-for-tuples = "0.2.1" +sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.27" } +# Error types for execution +evm-core = { default-features = false , git = "https://github.com/uniquenetwork/evm", branch = "unique-polkadot-v0.9.27" } +# We have tuple-heavy code in solidity.rs +impl-trait-for-tuples = "0.2.2" [dev-dependencies] +# We want to assert some large binary blobs equality in tests hex = "0.4.3" +hex-literal = "0.3.4" [features] default = ["std"] --- /dev/null +++ b/crates/evm-coder/README.md @@ -0,0 +1,15 @@ +# evm-coder + +Library for seamless call translation between Rust and Solidity code + +By encoding solidity definitions in Rust, this library also provides generation of +solidity interfaces for ethereum developers + +## Overview + +Most of this library functionality shouldn't be used directly, but via macros + +- [`solidity_interface`] +- [`ToLog`] + + \ No newline at end of file --- /dev/null +++ b/crates/evm-coder/procedural/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "evm-coder-procedural" +version = "0.2.0" +license = "GPLv3" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +# Ethereum uses keccak (=sha3) for selectors +sha3 = "0.10.1" +# Value formatting +hex = "0.4.3" +Inflector = "0.11.4" +# General proc-macro utilities +quote = "1.0" +proc-macro2 = "1.0" +syn = { version = "1.0", features = ["full"] } --- /dev/null +++ b/crates/evm-coder/procedural/src/lib.rs @@ -0,0 +1,244 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +#![allow(dead_code)] + +use inflector::cases; +use proc_macro::TokenStream; +use quote::quote; +use sha3::{Digest, Keccak256}; +use syn::{ + DeriveInput, GenericArgument, Ident, ItemImpl, Pat, Path, PathArguments, PathSegment, Type, + parse_macro_input, spanned::Spanned, +}; + +mod solidity_interface; +mod to_log; + +fn fn_selector_str(input: &str) -> u32 { + let mut hasher = Keccak256::new(); + hasher.update(input.as_bytes()); + let result = hasher.finalize(); + + let mut selector_bytes = [0; 4]; + selector_bytes.copy_from_slice(&result[0..4]); + + u32::from_be_bytes(selector_bytes) +} + +/// Returns solidity function selector (first 4 bytes of hash) by its +/// textual representation +/// +/// ```ignore +/// use evm_coder_macros::fn_selector; +/// +/// assert_eq!(fn_selector!(transfer(address, uint256)), 0xa9059cbb); +/// ``` +#[proc_macro] +pub fn fn_selector(input: TokenStream) -> TokenStream { + let input = input.to_string().replace(' ', ""); + let selector = fn_selector_str(&input); + + (quote! { + #selector + }) + .into() +} + +fn event_selector_str(input: &str) -> [u8; 32] { + let mut hasher = Keccak256::new(); + hasher.update(input.as_bytes()); + let result = hasher.finalize(); + + let mut selector_bytes = [0; 32]; + selector_bytes.copy_from_slice(&result[0..32]); + selector_bytes +} + +/// Returns solidity topic (hash) by its textual representation +/// +/// ```ignore +/// use evm_coder_macros::event_topic; +/// +/// assert_eq!( +/// format!("{:x}", event_topic!(Transfer(address, address, uint256))), +/// "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", +/// ); +/// ``` +#[proc_macro] +pub fn event_topic(stream: TokenStream) -> TokenStream { + let input = stream.to_string().replace(' ', ""); + let selector_bytes = event_selector_str(&input); + + (quote! { + ::primitive_types::H256([#( + #selector_bytes, + )*]) + }) + .into() +} + +fn parse_path(ty: &Type) -> syn::Result<&Path> { + match &ty { + syn::Type::Path(pat) => { + if let Some(qself) = &pat.qself { + return Err(syn::Error::new(qself.ty.span(), "no receiver expected")); + } + Ok(&pat.path) + } + _ => Err(syn::Error::new(ty.span(), "expected ty to be path")), + } +} + +fn parse_path_segment(path: &Path) -> syn::Result<&PathSegment> { + if path.segments.len() != 1 { + return Err(syn::Error::new( + path.span(), + "expected path to have only segment", + )); + } + let last_segment = &path.segments.last().unwrap(); + Ok(last_segment) +} + +fn parse_ident_from_pat(pat: &Pat) -> syn::Result<&Ident> { + match pat { + Pat::Ident(i) => Ok(&i.ident), + _ => Err(syn::Error::new(pat.span(), "expected pat ident")), + } +} + +fn parse_ident_from_segment(segment: &PathSegment, allow_generics: bool) -> syn::Result<&Ident> { + if segment.arguments != PathArguments::None && !allow_generics { + return Err(syn::Error::new( + segment.arguments.span(), + "unexpected generic type", + )); + } + Ok(&segment.ident) +} + +fn parse_ident_from_path(path: &Path, allow_generics: bool) -> syn::Result<&Ident> { + let segment = parse_path_segment(path)?; + parse_ident_from_segment(segment, allow_generics) +} + +fn parse_ident_from_type(ty: &Type, allow_generics: bool) -> syn::Result<&Ident> { + let path = parse_path(ty)?; + parse_ident_from_path(path, allow_generics) +} + +// Gets T out of Result +fn parse_result_ok(ty: &Type) -> syn::Result<&Type> { + let path = parse_path(ty)?; + let segment = parse_path_segment(path)?; + + if segment.ident != "Result" { + return Err(syn::Error::new( + ty.span(), + "expected Result as return type (no renamed aliases allowed)", + )); + } + let args = match &segment.arguments { + PathArguments::AngleBracketed(e) => e, + _ => { + return Err(syn::Error::new( + segment.arguments.span(), + "missing Result generics", + )) + } + }; + + let args = &args.args; + let arg = args.first().unwrap(); + + let ty = match arg { + GenericArgument::Type(ty) => ty, + _ => { + return Err(syn::Error::new( + arg.span(), + "expected first generic to be type", + )) + } + }; + + Ok(ty) +} + +fn pascal_ident_to_call(ident: &Ident) -> Ident { + let name = format!("{}Call", ident); + Ident::new(&name, ident.span()) +} +fn snake_ident_to_pascal(ident: &Ident) -> Ident { + let name = ident.to_string(); + let name = cases::pascalcase::to_pascal_case(&name); + Ident::new(&name, ident.span()) +} +fn snake_ident_to_screaming(ident: &Ident) -> Ident { + let name = ident.to_string(); + let name = cases::screamingsnakecase::to_screaming_snake_case(&name); + Ident::new(&name, ident.span()) +} +fn pascal_ident_to_snake_call(ident: &Ident) -> Ident { + let name = ident.to_string(); + let name = cases::snakecase::to_snake_case(&name); + let name = format!("call_{}", name); + Ident::new(&name, ident.span()) +} + +/// See documentation for this proc-macro reexported in `evm-coder` crate +#[proc_macro_attribute] +pub fn solidity_interface(args: TokenStream, stream: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as solidity_interface::InterfaceInfo); + + let input: ItemImpl = match syn::parse(stream) { + Ok(t) => t, + Err(e) => return e.to_compile_error().into(), + }; + + let expanded = match solidity_interface::SolidityInterface::try_from(args, &input) { + Ok(v) => v.expand(), + Err(e) => e.to_compile_error(), + }; + + (quote! { + #input + + #expanded + }) + .into() +} + +#[proc_macro_attribute] +pub fn solidity(_args: TokenStream, stream: TokenStream) -> TokenStream { + stream +} +#[proc_macro_attribute] +pub fn weight(_args: TokenStream, stream: TokenStream) -> TokenStream { + stream +} + +/// See documentation for this proc-macro reexported in `evm-coder` crate +#[proc_macro_derive(ToLog, attributes(indexed))] +pub fn to_log(value: TokenStream) -> TokenStream { + let input = parse_macro_input!(value as DeriveInput); + + match to_log::Events::try_from(&input) { + Ok(e) => e.expand(), + Err(e) => e.to_compile_error(), + } + .into() +} --- /dev/null +++ b/crates/evm-coder/procedural/src/solidity_interface.rs @@ -0,0 +1,1110 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +#![allow(dead_code)] + +// NOTE: In order to understand this Rust macro better, first read this chapter +// about Procedural Macros in Rust book: +// https://doc.rust-lang.org/reference/procedural-macros.html + +use quote::{quote, ToTokens}; +use inflector::cases; +use std::fmt::Write; +use syn::{ + Expr, FnArg, GenericArgument, Generics, Ident, ImplItem, ImplItemMethod, ItemImpl, Lit, Meta, + MetaNameValue, PatType, PathArguments, ReturnType, Type, + spanned::Spanned, + parse::{Parse, ParseStream}, + parenthesized, Token, LitInt, LitStr, +}; + +use crate::{ + fn_selector_str, parse_ident_from_pat, parse_ident_from_path, parse_path, parse_path_segment, + parse_result_ok, pascal_ident_to_call, pascal_ident_to_snake_call, snake_ident_to_pascal, + snake_ident_to_screaming, +}; + +struct Is { + name: Ident, + pascal_call_name: Ident, + snake_call_name: Ident, + via: Option<(Type, Ident)>, +} +impl Is { + fn expand_call_def(&self, gen_ref: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { + let name = &self.name; + let pascal_call_name = &self.pascal_call_name; + quote! { + #name(#pascal_call_name #gen_ref) + } + } + + fn expand_interface_id(&self) -> proc_macro2::TokenStream { + let pascal_call_name = &self.pascal_call_name; + quote! { + interface_id ^= u32::from_be_bytes(#pascal_call_name::interface_id()); + } + } + + fn expand_supports_interface( + &self, + generics: &proc_macro2::TokenStream, + ) -> proc_macro2::TokenStream { + let pascal_call_name = &self.pascal_call_name; + quote! { + <#pascal_call_name #generics>::supports_interface(interface_id) + } + } + + fn expand_variant_weight(&self) -> proc_macro2::TokenStream { + let name = &self.name; + quote! { + Self::#name(call) => call.weight() + } + } + + fn expand_variant_call( + &self, + call_name: &proc_macro2::Ident, + generics: &proc_macro2::TokenStream, + ) -> proc_macro2::TokenStream { + let name = &self.name; + let pascal_call_name = &self.pascal_call_name; + let via_typ = self + .via + .as_ref() + .map(|(t, _)| quote! {#t}) + .unwrap_or_else(|| quote! {Self}); + let via_map = self + .via + .as_ref() + .map(|(_, i)| quote! {.#i()}) + .unwrap_or_default(); + quote! { + #call_name::#name(call) => return <#via_typ as ::evm_coder::Callable<#pascal_call_name #generics>>::call(self #via_map, Msg { + call, + caller: c.caller, + value: c.value, + }) + } + } + + fn expand_parse(&self, generics: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { + let name = &self.name; + let pascal_call_name = &self.pascal_call_name; + quote! { + if let Some(parsed_call) = <#pascal_call_name #generics>::parse(method_id, reader)? { + return Ok(Some(Self::#name(parsed_call))) + } + } + } + + fn expand_generator(&self, generics: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { + let pascal_call_name = &self.pascal_call_name; + quote! { + <#pascal_call_name #generics>::generate_solidity_interface(tc, is_impl); + } + } + + fn expand_event_generator(&self) -> proc_macro2::TokenStream { + let name = &self.name; + quote! { + #name::generate_solidity_interface(tc, is_impl); + } + } +} + +#[derive(Default)] +struct IsList(Vec); +impl Parse for IsList { + fn parse(input: ParseStream) -> syn::Result { + let mut out = vec![]; + loop { + if input.is_empty() { + break; + } + let name = input.parse::()?; + let lookahead = input.lookahead1(); + let via = if lookahead.peek(syn::token::Paren) { + let contents; + parenthesized!(contents in input); + let method = contents.parse::()?; + contents.parse::()?; + let ty = contents.parse::()?; + Some((ty, method)) + } else if lookahead.peek(Token![,]) { + None + } else if input.is_empty() { + None + } else { + return Err(lookahead.error()); + }; + out.push(Is { + pascal_call_name: pascal_ident_to_call(&name), + snake_call_name: pascal_ident_to_snake_call(&name), + name, + via, + }); + if input.peek(Token![,]) { + input.parse::()?; + continue; + } else { + break; + } + } + Ok(Self(out)) + } +} + +pub struct InterfaceInfo { + name: Ident, + is: IsList, + inline_is: IsList, + events: IsList, + expect_selector: Option, +} +impl Parse for InterfaceInfo { + fn parse(input: ParseStream) -> syn::Result { + let mut name = None; + let mut is = None; + let mut inline_is = None; + let mut events = None; + let mut expect_selector = None; + // TODO: create proc-macro to optimize proc-macro boilerplate? :D + loop { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::name) { + let k = input.parse::()?; + input.parse::()?; + if name.replace(input.parse::()?).is_some() { + return Err(syn::Error::new(k.span(), "name is already set")); + } + } else if lookahead.peek(kw::is) { + let k = input.parse::()?; + let contents; + parenthesized!(contents in input); + if is.replace(contents.parse::()?).is_some() { + return Err(syn::Error::new(k.span(), "is is already set")); + } + } else if lookahead.peek(kw::inline_is) { + let k = input.parse::()?; + let contents; + parenthesized!(contents in input); + if inline_is.replace(contents.parse::()?).is_some() { + return Err(syn::Error::new(k.span(), "inline_is is already set")); + } + } else if lookahead.peek(kw::events) { + let k = input.parse::()?; + let contents; + parenthesized!(contents in input); + if events.replace(contents.parse::()?).is_some() { + return Err(syn::Error::new(k.span(), "events is already set")); + } + } else if lookahead.peek(kw::expect_selector) { + let k = input.parse::()?; + input.parse::()?; + let value = input.parse::()?; + if expect_selector + .replace(value.base10_parse::()?) + .is_some() + { + return Err(syn::Error::new(k.span(), "expect_selector is already set")); + } + } else if input.is_empty() { + break; + } else { + return Err(lookahead.error()); + } + if input.peek(Token![,]) { + input.parse::()?; + } else { + break; + } + } + Ok(Self { + name: name.ok_or_else(|| syn::Error::new(input.span(), "missing name"))?, + is: is.unwrap_or_default(), + inline_is: inline_is.unwrap_or_default(), + events: events.unwrap_or_default(), + expect_selector, + }) + } +} + +struct MethodInfo { + rename_selector: Option, +} +impl Parse for MethodInfo { + fn parse(input: ParseStream) -> syn::Result { + let mut rename_selector = None; + let lookahead = input.lookahead1(); + if lookahead.peek(kw::rename_selector) { + let k = input.parse::()?; + input.parse::()?; + if rename_selector + .replace(input.parse::()?.value()) + .is_some() + { + return Err(syn::Error::new(k.span(), "rename_selector is already set")); + } + } + Ok(Self { rename_selector }) + } +} + +enum AbiType { + // type + Plain(Ident), + // (type1,type2) + Tuple(Vec), + // type[] + Vec(Box), + // type[20] + Array(Box, usize), +} +impl AbiType { + fn try_from(value: &Type) -> syn::Result { + let value = Self::try_maybe_special_from(value)?; + if value.is_special() { + return Err(syn::Error::new(value.span(), "unexpected special type")); + } + Ok(value) + } + fn try_maybe_special_from(value: &Type) -> syn::Result { + match value { + Type::Array(arr) => { + let wrapped = AbiType::try_from(&arr.elem)?; + match &arr.len { + Expr::Lit(l) => match &l.lit { + Lit::Int(i) => { + let num = i.base10_parse::()?; + Ok(AbiType::Array(Box::new(wrapped), num as usize)) + } + _ => Err(syn::Error::new(arr.len.span(), "should be int literal")), + }, + _ => Err(syn::Error::new(arr.len.span(), "should be literal")), + } + } + Type::Path(_) => { + let path = parse_path(value)?; + let segment = parse_path_segment(path)?; + if segment.ident == "Vec" { + let args = match &segment.arguments { + PathArguments::AngleBracketed(e) => e, + _ => { + return Err(syn::Error::new( + segment.arguments.span(), + "missing Vec generic", + )) + } + }; + let args = &args.args; + if args.len() != 1 { + return Err(syn::Error::new( + args.span(), + "expected only one generic for vec", + )); + } + let arg = args.first().expect("first arg"); + + let ty = match arg { + GenericArgument::Type(ty) => ty, + _ => { + return Err(syn::Error::new( + arg.span(), + "expected first generic to be type", + )) + } + }; + + let wrapped = AbiType::try_from(ty)?; + Ok(Self::Vec(Box::new(wrapped))) + } else { + if !segment.arguments.is_empty() { + return Err(syn::Error::new( + segment.arguments.span(), + "unexpected generic arguments for non-vec type", + )); + } + Ok(Self::Plain(segment.ident.clone())) + } + } + Type::Tuple(t) => { + let mut out = Vec::with_capacity(t.elems.len()); + for el in t.elems.iter() { + out.push(AbiType::try_from(el)?) + } + Ok(Self::Tuple(out)) + } + _ => Err(syn::Error::new( + value.span(), + "unexpected type, only arrays, plain types and tuples are supported", + )), + } + } + fn is_value(&self) -> bool { + matches!(self, Self::Plain(v) if v == "value") + } + fn is_caller(&self) -> bool { + matches!(self, Self::Plain(v) if v == "caller") + } + fn is_special(&self) -> bool { + self.is_caller() || self.is_value() + } + fn selector_ty_buf(&self, buf: &mut String) -> std::fmt::Result { + match self { + AbiType::Plain(t) => { + write!(buf, "{}", t) + } + AbiType::Tuple(t) => { + write!(buf, "(")?; + for (i, t) in t.iter().enumerate() { + if i != 0 { + write!(buf, ",")?; + } + t.selector_ty_buf(buf)?; + } + write!(buf, ")") + } + AbiType::Vec(v) => { + v.selector_ty_buf(buf)?; + write!(buf, "[]") + } + AbiType::Array(v, len) => { + v.selector_ty_buf(buf)?; + write!(buf, "[{}]", len) + } + } + } + fn selector_ty(&self) -> String { + let mut out = String::new(); + self.selector_ty_buf(&mut out).expect("no fmt error"); + out + } +} +impl ToTokens for AbiType { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + AbiType::Plain(t) => tokens.extend(quote! {#t}), + AbiType::Tuple(t) => { + tokens.extend(quote! {( + #(#t),* + )}); + } + AbiType::Vec(v) => tokens.extend(quote! {Vec<#v>}), + AbiType::Array(v, l) => tokens.extend(quote! {[#v; #l]}), + } + } +} + +struct MethodArg { + name: Ident, + camel_name: String, + ty: AbiType, +} +impl MethodArg { + fn try_from(value: &PatType) -> syn::Result { + let name = parse_ident_from_pat(&value.pat)?.clone(); + Ok(Self { + camel_name: cases::camelcase::to_camel_case(&name.to_string()), + name, + ty: AbiType::try_maybe_special_from(&value.ty)?, + }) + } + fn is_value(&self) -> bool { + self.ty.is_value() + } + fn is_caller(&self) -> bool { + self.ty.is_caller() + } + fn is_special(&self) -> bool { + self.ty.is_special() + } + fn selector_ty(&self) -> String { + assert!(!self.is_special()); + self.ty.selector_ty() + } + + fn expand_call_def(&self) -> proc_macro2::TokenStream { + assert!(!self.is_special()); + let name = &self.name; + let ty = &self.ty; + + quote! { + #name: #ty + } + } + + fn expand_parse(&self) -> proc_macro2::TokenStream { + assert!(!self.is_special()); + let name = &self.name; + quote! { + #name: reader.abi_read()? + } + } + + fn expand_call_arg(&self) -> proc_macro2::TokenStream { + if self.is_value() { + quote! { + c.value.clone() + } + } else if self.is_caller() { + quote! { + c.caller.clone() + } + } else { + let name = &self.name; + quote! { + #name + } + } + } + + fn expand_solidity_argument(&self) -> proc_macro2::TokenStream { + let camel_name = &self.camel_name.to_string(); + let ty = &self.ty; + quote! { + >::new(#camel_name) + } + } +} + +#[derive(PartialEq)] +enum Mutability { + Mutable, + View, + Pure, +} + +/// Group all keywords for this macro. Usage example: +/// #[solidity_interface(name = "B", inline_is(A))] +mod kw { + syn::custom_keyword!(weight); + + syn::custom_keyword!(via); + syn::custom_keyword!(name); + syn::custom_keyword!(is); + syn::custom_keyword!(inline_is); + syn::custom_keyword!(events); + syn::custom_keyword!(expect_selector); + + syn::custom_keyword!(rename_selector); +} + +/// Rust methods are parsed into this structure when Solidity code is generated +struct Method { + name: Ident, + camel_name: String, + pascal_name: Ident, + screaming_name: Ident, + selector_str: String, + selector: u32, + args: Vec, + has_normal_args: bool, + mutability: Mutability, + result: Type, + weight: Option, + docs: Vec, +} +impl Method { + fn try_from(value: &ImplItemMethod) -> syn::Result { + let mut info = MethodInfo { + rename_selector: None, + }; + let mut docs = Vec::new(); + let mut weight = None; + for attr in &value.attrs { + let ident = parse_ident_from_path(&attr.path, false)?; + if ident == "solidity" { + info = attr.parse_args::()?; + } else if ident == "doc" { + let args = attr.parse_meta().unwrap(); + let value = match args { + Meta::NameValue(MetaNameValue { + lit: Lit::Str(str), .. + }) => str.value(), + _ => unreachable!(), + }; + docs.push(value); + } else if ident == "weight" { + weight = Some(attr.parse_args::()?); + } + } + let ident = &value.sig.ident; + let ident_str = ident.to_string(); + if !cases::snakecase::is_snake_case(&ident_str) { + return Err(syn::Error::new(ident.span(), "method name should be snake_cased\nif alternative solidity name needs to be set - use #[solidity] attribute")); + } + + let mut mutability = Mutability::Pure; + + if let Some(FnArg::Receiver(receiver)) = value + .sig + .inputs + .iter() + .find(|arg| matches!(arg, FnArg::Receiver(_))) + { + if receiver.reference.is_none() { + return Err(syn::Error::new( + receiver.span(), + "receiver should be by ref", + )); + } + if receiver.mutability.is_some() { + mutability = Mutability::Mutable; + } else { + mutability = Mutability::View; + } + } + let mut args = Vec::new(); + for typ in value + .sig + .inputs + .iter() + .filter(|arg| matches!(arg, FnArg::Typed(_))) + { + let typ = match typ { + FnArg::Typed(typ) => typ, + _ => unreachable!(), + }; + args.push(MethodArg::try_from(typ)?); + } + + if mutability != Mutability::Mutable && args.iter().any(|arg| arg.is_value()) { + return Err(syn::Error::new( + args.iter().find(|arg| arg.is_value()).unwrap().ty.span(), + "payable function should be mutable", + )); + } + + let result = match &value.sig.output { + ReturnType::Type(_, ty) => ty, + _ => return Err(syn::Error::new(value.sig.output.span(), "interface method should return Result\nif there is no value to return - specify void (which is alias to unit)")), + }; + let result = parse_result_ok(result)?; + + let camel_name = info + .rename_selector + .unwrap_or_else(|| cases::camelcase::to_camel_case(&ident.to_string())); + let mut selector_str = camel_name.clone(); + selector_str.push('('); + let mut has_normal_args = false; + for (i, arg) in args.iter().filter(|arg| !arg.is_special()).enumerate() { + if i != 0 { + selector_str.push(','); + } + write!(selector_str, "{}", arg.selector_ty()).unwrap(); + has_normal_args = true; + } + selector_str.push(')'); + let selector = fn_selector_str(&selector_str); + + Ok(Self { + name: ident.clone(), + camel_name, + pascal_name: snake_ident_to_pascal(ident), + screaming_name: snake_ident_to_screaming(ident), + selector_str, + selector, + args, + has_normal_args, + mutability, + result: result.clone(), + weight, + docs, + }) + } + fn expand_call_def(&self) -> proc_macro2::TokenStream { + let defs = self + .args + .iter() + .filter(|a| !a.is_special()) + .map(|a| a.expand_call_def()); + let pascal_name = &self.pascal_name; + let docs = &self.docs; + + if self.has_normal_args { + quote! { + #(#[doc = #docs])* + #[allow(missing_docs)] + #pascal_name { + #( + #defs, + )* + } + } + } else { + quote! {#pascal_name} + } + } + + fn expand_const(&self) -> proc_macro2::TokenStream { + let screaming_name = &self.screaming_name; + let selector = u32::to_be_bytes(self.selector); + let selector_str = &self.selector_str; + quote! { + #[doc = #selector_str] + const #screaming_name: ::evm_coder::types::bytes4 = [#(#selector,)*]; + } + } + + fn expand_interface_id(&self) -> proc_macro2::TokenStream { + let screaming_name = &self.screaming_name; + quote! { + interface_id ^= u32::from_be_bytes(Self::#screaming_name); + } + } + + fn expand_parse(&self) -> proc_macro2::TokenStream { + let pascal_name = &self.pascal_name; + let screaming_name = &self.screaming_name; + if self.has_normal_args { + let parsers = self + .args + .iter() + .filter(|a| !a.is_special()) + .map(|a| a.expand_parse()); + quote! { + Self::#screaming_name => return Ok(Some(Self::#pascal_name { + #( + #parsers, + )* + })) + } + } else { + quote! { Self::#screaming_name => return Ok(Some(Self::#pascal_name)) } + } + } + + fn expand_variant_call(&self, call_name: &proc_macro2::Ident) -> proc_macro2::TokenStream { + let pascal_name = &self.pascal_name; + let name = &self.name; + + let matcher = if self.has_normal_args { + let names = self + .args + .iter() + .filter(|a| !a.is_special()) + .map(|a| &a.name); + + quote! {{ + #( + #names, + )* + }} + } else { + quote! {} + }; + + let receiver = match self.mutability { + Mutability::Mutable | Mutability::View => quote! {self.}, + Mutability::Pure => quote! {Self::}, + }; + let args = self.args.iter().map(|a| a.expand_call_arg()); + + quote! { + #call_name::#pascal_name #matcher => { + let result = #receiver #name( + #( + #args, + )* + )?; + (&result).to_result() + } + } + } + + fn expand_variant_weight(&self) -> proc_macro2::TokenStream { + let pascal_name = &self.pascal_name; + if let Some(weight) = &self.weight { + let matcher = if self.has_normal_args { + let names = self + .args + .iter() + .filter(|a| !a.is_special()) + .map(|a| &a.name); + + quote! {{ + #( + #names, + )* + }} + } else { + quote! {} + }; + quote! { + Self::#pascal_name #matcher => (#weight).into() + } + } else { + let matcher = if self.has_normal_args { + quote! {{..}} + } else { + quote! {} + }; + quote! { + Self::#pascal_name #matcher => ().into() + } + } + } + + fn expand_solidity_function(&self) -> proc_macro2::TokenStream { + let camel_name = &self.camel_name; + let mutability = match self.mutability { + Mutability::Mutable => quote! {SolidityMutability::Mutable}, + Mutability::View => quote! { SolidityMutability::View }, + Mutability::Pure => quote! {SolidityMutability::Pure}, + }; + let result = &self.result; + + let args = self + .args + .iter() + .filter(|a| !a.is_special()) + .map(MethodArg::expand_solidity_argument); + let docs = &self.docs; + let selector_str = &self.selector_str; + let selector = self.selector; + + quote! { + SolidityFunction { + docs: &[#(#docs),*], + selector_str: #selector_str, + selector: #selector, + name: #camel_name, + mutability: #mutability, + args: ( + #( + #args, + )* + ), + result: >::default(), + } + } + } +} + +fn generics_list(gen: &Generics) -> proc_macro2::TokenStream { + if gen.params.is_empty() { + return quote! {}; + } + let params = gen.params.iter().map(|p| match p { + syn::GenericParam::Type(id) => { + let v = &id.ident; + quote! {#v} + } + syn::GenericParam::Lifetime(lt) => { + let v = <.lifetime; + quote! {#v} + } + syn::GenericParam::Const(c) => { + let i = &c.ident; + quote! {#i} + } + }); + quote! { #(#params),* } +} +fn generics_reference(gen: &Generics) -> proc_macro2::TokenStream { + if gen.params.is_empty() { + return quote! {}; + } + let list = generics_list(gen); + quote! { <#list> } +} +fn generics_data(gen: &Generics) -> proc_macro2::TokenStream { + let list = generics_list(gen); + if gen.params.len() == 1 { + quote! {#list} + } else { + quote! { (#list) } + } +} + +pub struct SolidityInterface { + generics: Generics, + name: Box, + info: InterfaceInfo, + methods: Vec, + docs: Vec, +} +impl SolidityInterface { + pub fn try_from(info: InterfaceInfo, value: &ItemImpl) -> syn::Result { + let mut methods = Vec::new(); + + for item in &value.items { + if let ImplItem::Method(method) = item { + methods.push(Method::try_from(method)?) + } + } + let mut docs = vec![]; + for attr in &value.attrs { + let ident = parse_ident_from_path(&attr.path, false)?; + if ident == "doc" { + let args = attr.parse_meta().unwrap(); + let value = match args { + Meta::NameValue(MetaNameValue { + lit: Lit::Str(str), .. + }) => str.value(), + _ => unreachable!(), + }; + docs.push(value); + } + } + Ok(Self { + generics: value.generics.clone(), + name: value.self_ty.clone(), + info, + methods, + docs, + }) + } + pub fn expand(self) -> proc_macro2::TokenStream { + let name = self.name; + + let solidity_name = self.info.name.to_string(); + let call_name = pascal_ident_to_call(&self.info.name); + let generics = self.generics; + let gen_ref = generics_reference(&generics); + let gen_data = generics_data(&generics); + let gen_where = &generics.where_clause; + + let call_sub = self + .info + .inline_is + .0 + .iter() + .chain(self.info.is.0.iter()) + .map(|c| Is::expand_call_def(c, &gen_ref)); + let call_parse = self + .info + .inline_is + .0 + .iter() + .chain(self.info.is.0.iter()) + .map(|is| Is::expand_parse(is, &gen_ref)); + let call_variants = self + .info + .inline_is + .0 + .iter() + .chain(self.info.is.0.iter()) + .map(|c| Is::expand_variant_call(c, &call_name, &gen_ref)); + let weight_variants = self + .info + .inline_is + .0 + .iter() + .chain(self.info.is.0.iter()) + .map(Is::expand_variant_weight); + + let inline_interface_id = self.info.inline_is.0.iter().map(Is::expand_interface_id); + let supports_interface = self + .info + .is + .0 + .iter() + .map(|is| Is::expand_supports_interface(is, &gen_ref)); + + let calls = self.methods.iter().map(Method::expand_call_def); + let consts = self.methods.iter().map(Method::expand_const); + let interface_id = self.methods.iter().map(Method::expand_interface_id); + let parsers = self.methods.iter().map(Method::expand_parse); + let call_variants_this = self + .methods + .iter() + .map(|m| Method::expand_variant_call(m, &call_name)); + let weight_variants_this = self.methods.iter().map(Method::expand_variant_weight); + let solidity_functions = self.methods.iter().map(Method::expand_solidity_function); + + // TODO: Inline inline_is + let solidity_is = self + .info + .is + .0 + .iter() + .chain(self.info.inline_is.0.iter()) + .map(|is| is.name.to_string()); + let solidity_events_is = self.info.events.0.iter().map(|is| is.name.to_string()); + let solidity_generators = self + .info + .is + .0 + .iter() + .chain(self.info.inline_is.0.iter()) + .map(|is| Is::expand_generator(is, &gen_ref)); + let solidity_event_generators = self.info.events.0.iter().map(Is::expand_event_generator); + + let docs = &self.docs; + + if let Some(expect_selector) = &self.info.expect_selector { + if !self.info.inline_is.0.is_empty() { + return syn::Error::new( + name.span(), + "expect_selector is not compatible with inline_is", + ) + .to_compile_error(); + } + let selector = self + .methods + .iter() + .map(|m| m.selector) + .fold(0, |a, b| a ^ b); + + if *expect_selector != selector { + let mut methods = String::new(); + for meth in self.methods.iter() { + write!(methods, "\n- {}", meth.selector_str).expect("write to string"); + } + return syn::Error::new(name.span(), format!("expected selector mismatch, expected {expect_selector:0>8x}, but implementation has {selector:0>8x}{methods}")).to_compile_error(); + } + } + // let methods = self.methods.iter().map(Method::solidity_def); + + quote! { + #[derive(Debug)] + #(#[doc = #docs])* + pub enum #call_name #gen_ref { + /// Inherited method + ERC165Call(::evm_coder::ERC165Call, ::core::marker::PhantomData<#gen_data>), + #( + #calls, + )* + #( + #call_sub, + )* + } + impl #gen_ref #call_name #gen_ref { + #( + #consts + )* + /// Return this call ERC165 selector + pub fn interface_id() -> ::evm_coder::types::bytes4 { + let mut interface_id = 0; + #(#interface_id)* + #(#inline_interface_id)* + u32::to_be_bytes(interface_id) + } + /// Is this contract implements specified ERC165 selector + pub fn supports_interface(interface_id: ::evm_coder::types::bytes4) -> bool { + interface_id != u32::to_be_bytes(0xffffff) && ( + interface_id == ::evm_coder::ERC165Call::INTERFACE_ID || + interface_id == Self::interface_id() + #( + || #supports_interface + )* + ) + } + /// Generate solidity definitions for methods described in this interface + pub fn generate_solidity_interface(tc: &evm_coder::solidity::TypeCollector, is_impl: bool) { + use evm_coder::solidity::*; + use core::fmt::Write; + let interface = SolidityInterface { + docs: &[#(#docs),*], + name: #solidity_name, + selector: Self::interface_id(), + is: &["Dummy", "ERC165", #( + #solidity_is, + )* #( + #solidity_events_is, + )* ], + functions: (#( + #solidity_functions, + )*), + }; + + let mut out = string::new(); + if #solidity_name.starts_with("Inline") { + out.push_str("/// @dev inlined interface\n"); + } + let _ = interface.format(is_impl, &mut out, tc); + tc.collect(out); + #( + #solidity_event_generators + )* + #( + #solidity_generators + )* + if is_impl { + tc.collect("/// @dev common stubs holder\ncontract Dummy {\n\tuint8 dummy;\n\tstring stub_error = \"this contract is implemented in native\";\n}\ncontract ERC165 is Dummy {\n\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool) {\n\t\trequire(false, stub_error);\n\t\tinterfaceID;\n\t\treturn true;\n\t}\n}\n".into()); + } else { + tc.collect("/// @dev common stubs holder\ninterface Dummy {\n}\ninterface ERC165 is Dummy {\n\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool);\n}\n".into()); + } + } + } + impl #gen_ref ::evm_coder::Call for #call_name #gen_ref { + fn parse(method_id: ::evm_coder::types::bytes4, reader: &mut ::evm_coder::abi::AbiReader) -> ::evm_coder::execution::Result> { + use ::evm_coder::abi::AbiRead; + match method_id { + ::evm_coder::ERC165Call::INTERFACE_ID => return Ok( + ::evm_coder::ERC165Call::parse(method_id, reader)? + .map(|c| Self::ERC165Call(c, ::core::marker::PhantomData)) + ), + #( + #parsers, + )* + _ => {}, + } + #( + #call_parse + )else* + return Ok(None); + } + } + impl #generics ::evm_coder::Weighted for #call_name #gen_ref + #gen_where + { + #[allow(unused_variables)] + fn weight(&self) -> ::evm_coder::execution::DispatchInfo { + match self { + #( + #weight_variants, + )* + // TODO: It should be very cheap, but not free + Self::ERC165Call(::evm_coder::ERC165Call::SupportsInterface {..}, _) => 100u64.into(), + #( + #weight_variants_this, + )* + } + } + } + impl #generics ::evm_coder::Callable<#call_name #gen_ref> for #name + #gen_where + { + #[allow(unreachable_code)] // In case of no inner calls + fn call(&mut self, c: Msg<#call_name #gen_ref>) -> ::evm_coder::execution::ResultWithPostInfo<::evm_coder::abi::AbiWriter> { + use ::evm_coder::abi::AbiWrite; + match c.call { + #( + #call_variants, + )* + #call_name::ERC165Call(::evm_coder::ERC165Call::SupportsInterface {interface_id}, _) => { + let mut writer = ::evm_coder::abi::AbiWriter::default(); + writer.bool(&<#call_name #gen_ref>::supports_interface(interface_id)); + return Ok(writer.into()); + } + _ => {}, + } + let mut writer = ::evm_coder::abi::AbiWriter::default(); + match c.call { + #( + #call_variants_this, + )* + _ => unreachable!() + } + } + } + } + } +} --- /dev/null +++ b/crates/evm-coder/procedural/src/to_log.rs @@ -0,0 +1,238 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +use inflector::cases; +use syn::{Data, DeriveInput, Field, Fields, Ident, Variant, spanned::Spanned}; +use std::fmt::Write; +use quote::quote; + +use crate::{parse_ident_from_path, parse_ident_from_type, snake_ident_to_screaming}; + +struct EventField { + name: Ident, + camel_name: String, + ty: Ident, + indexed: bool, +} + +impl EventField { + fn try_from(field: &Field) -> syn::Result { + let name = field.ident.as_ref().unwrap(); + let ty = parse_ident_from_type(&field.ty, false)?; + let mut indexed = false; + for attr in &field.attrs { + if let Ok(ident) = parse_ident_from_path(&attr.path, false) { + if ident == "indexed" { + indexed = true; + } + } + } + Ok(Self { + name: name.to_owned(), + camel_name: cases::camelcase::to_camel_case(&name.to_string()), + ty: ty.to_owned(), + indexed, + }) + } + fn expand_solidity_argument(&self) -> proc_macro2::TokenStream { + let camel_name = &self.camel_name; + let ty = &self.ty; + let indexed = self.indexed; + quote! { + >::new(#indexed, #camel_name) + } + } +} + +struct Event { + name: Ident, + name_screaming: Ident, + fields: Vec, + selector: [u8; 32], + selector_str: String, +} + +impl Event { + fn try_from(variant: &Variant) -> syn::Result { + let name = &variant.ident; + let name_screaming = snake_ident_to_screaming(name); + + let named = match &variant.fields { + Fields::Named(named) => named, + _ => { + return Err(syn::Error::new( + variant.fields.span(), + "expected named fields", + )) + } + }; + let mut fields = Vec::new(); + for field in &named.named { + fields.push(EventField::try_from(field)?); + } + if fields.iter().filter(|f| f.indexed).count() > 3 { + return Err(syn::Error::new( + variant.fields.span(), + "events can have at most 4 indexed fields (1 indexed field is reserved for event signature)" + )); + } + let mut selector_str = format!("{}(", name); + for (i, arg) in fields.iter().enumerate() { + if i != 0 { + write!(selector_str, ",").unwrap(); + } + write!(selector_str, "{}", arg.ty).unwrap(); + } + selector_str.push(')'); + let selector = crate::event_selector_str(&selector_str); + + Ok(Self { + name: name.to_owned(), + name_screaming, + fields, + selector, + selector_str, + }) + } + + fn expand_serializers(&self) -> proc_macro2::TokenStream { + let name = &self.name; + let name_screaming = &self.name_screaming; + let fields = self.fields.iter().map(|f| &f.name); + + let indexed = self.fields.iter().filter(|f| f.indexed).map(|f| &f.name); + let plain = self.fields.iter().filter(|f| !f.indexed).map(|f| &f.name); + + quote! { + Self::#name {#( + #fields, + )*} => { + topics.push(topic::from(Self::#name_screaming)); + #( + topics.push(#indexed.to_topic()); + )* + #( + #plain.abi_write(&mut writer); + )* + } + } + } + + fn expand_consts(&self) -> proc_macro2::TokenStream { + let name_screaming = &self.name_screaming; + let selector_str = &self.selector_str; + let selector = &self.selector; + + quote! { + #[doc = #selector_str] + const #name_screaming: [u8; 32] = [#( + #selector, + )*]; + } + } + + fn expand_solidity_function(&self) -> proc_macro2::TokenStream { + let name = self.name.to_string(); + let args = self.fields.iter().map(EventField::expand_solidity_argument); + quote! { + SolidityEvent { + name: #name, + args: ( + #( + #args, + )* + ), + } + } + } +} + +pub struct Events { + name: Ident, + events: Vec, +} + +impl Events { + pub fn try_from(data: &DeriveInput) -> syn::Result { + let name = &data.ident; + let en = match &data.data { + Data::Enum(en) => en, + _ => return Err(syn::Error::new(data.span(), "expected enum")), + }; + let mut events = Vec::new(); + for variant in &en.variants { + events.push(Event::try_from(variant)?); + } + Ok(Self { + name: name.to_owned(), + events, + }) + } + pub fn expand(&self) -> proc_macro2::TokenStream { + let name = &self.name; + + let consts = self.events.iter().map(Event::expand_consts); + let serializers = self.events.iter().map(Event::expand_serializers); + let solidity_name = self.name.to_string(); + let solidity_functions = self.events.iter().map(Event::expand_solidity_function); + + quote! { + impl #name { + #( + #consts + )* + + pub fn generate_solidity_interface(tc: &evm_coder::solidity::TypeCollector, is_impl: bool) { + use evm_coder::solidity::*; + use core::fmt::Write; + let interface = SolidityInterface { + docs: &[], + selector: [0; 4], + name: #solidity_name, + is: &[], + functions: (#( + #solidity_functions, + )*), + }; + let mut out = string::new(); + out.push_str("/// @dev inlined interface\n"); + let _ = interface.format(is_impl, &mut out, tc); + tc.collect(out); + } + } + + #[automatically_derived] + impl ::evm_coder::events::ToLog for #name { + fn to_log(&self, contract: address) -> ::ethereum::Log { + use ::evm_coder::events::ToTopic; + use ::evm_coder::abi::AbiWrite; + let mut writer = ::evm_coder::abi::AbiWriter::new(); + let mut topics = Vec::new(); + match self { + #( + #serializers, + )* + } + ::ethereum::Log { + address: contract, + topics, + data: writer.finish(), + } + } + } + } + } +} --- a/crates/evm-coder/src/abi.rs +++ b/crates/evm-coder/src/abi.rs @@ -14,8 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -//! TODO: I misunterstood therminology, abi IS rlp encoded, so -//! this module should be replaced with rlp crate +//! Implementation of EVM RLP reader/writer #![allow(dead_code)] @@ -32,6 +31,11 @@ const ABI_ALIGNMENT: usize = 32; +trait TypeHelper { + fn is_dynamic() -> bool; +} + +/// View into RLP data, which provides method to read typed items from it #[derive(Clone)] pub struct AbiReader<'i> { buf: &'i [u8], @@ -39,6 +43,7 @@ offset: usize, } impl<'i> AbiReader<'i> { + /// Start reading RLP buffer, assuming there is no padding bytes pub fn new(buf: &'i [u8]) -> Self { Self { buf, @@ -46,6 +51,7 @@ offset: 0, } } + /// Start reading RLP buffer, parsing first 4 bytes as selector pub fn new_call(buf: &'i [u8]) -> Result<(types::bytes4, Self)> { if buf.len() < 4 { return Err(Error::Error(ExitError::OutOfOffset)); @@ -75,8 +81,8 @@ return Err(Error::Error(ExitError::OutOfOffset)); } let mut block = [0; S]; - // Verify padding is empty - if !buf[pad_start..pad_size].iter().all(|&v| v == 0) { + let is_pad_zeroed = buf[pad_start..pad_size].iter().all(|&v| v == 0); + if !is_pad_zeroed { return Err(Error::Error(ExitError::InvalidRange)); } block.copy_from_slice(&buf[block_start..block_size]); @@ -109,10 +115,12 @@ ) } + /// Read [`H160`] at current position, then advance pub fn address(&mut self) -> Result { Ok(H160(self.read_padleft()?)) } + /// Read [`bool`] at current position, then advance pub fn bool(&mut self) -> Result { let data: [u8; 1] = self.read_padleft()?; match data[0] { @@ -122,64 +130,89 @@ } } + /// Read [`[u8; 4]`] at current position, then advance pub fn bytes4(&mut self) -> Result<[u8; 4]> { self.read_padright() } + /// Read [`Vec`] at current position, then advance pub fn bytes(&mut self) -> Result> { - let mut subresult = self.subresult()?; - let length = subresult.read_usize()?; + let mut subresult = self.subresult(None)?; + let length = subresult.uint32()? as usize; if subresult.buf.len() < subresult.offset + length { return Err(Error::Error(ExitError::OutOfOffset)); } Ok(subresult.buf[subresult.offset..subresult.offset + length].into()) } + + /// Read [`string`] at current position, then advance pub fn string(&mut self) -> Result { string::from_utf8(self.bytes()?).map_err(|_| Error::Error(ExitError::InvalidRange)) } + /// Read [`u8`] at current position, then advance pub fn uint8(&mut self) -> Result { Ok(self.read_padleft::<1>()?[0]) } + /// Read [`u32`] at current position, then advance pub fn uint32(&mut self) -> Result { Ok(u32::from_be_bytes(self.read_padleft()?)) } + /// Read [`u128`] at current position, then advance pub fn uint128(&mut self) -> Result { Ok(u128::from_be_bytes(self.read_padleft()?)) } + /// Read [`U256`] at current position, then advance pub fn uint256(&mut self) -> Result { let buf: [u8; 32] = self.read_padleft()?; Ok(U256::from_big_endian(&buf)) } + /// Read [`u64`] at current position, then advance pub fn uint64(&mut self) -> Result { Ok(u64::from_be_bytes(self.read_padleft()?)) } + /// Read [`usize`] at current position, then advance + #[deprecated = "dangerous, as usize may have different width in wasm and native execution"] pub fn read_usize(&mut self) -> Result { Ok(usize::from_be_bytes(self.read_padleft()?)) } - fn subresult(&mut self) -> Result> { - let offset = self.read_usize()?; + /// Slice recursive buffer, advance one word for buffer offset + /// If `size` is [`None`] then [`Self::offset`] and [`Self::subresult_offset`] evals from [`Self::buf`]. + fn subresult(&mut self, size: Option) -> Result> { + let subresult_offset = self.subresult_offset; + let offset = if let Some(size) = size { + self.offset += size; + self.subresult_offset += size; + 0 + } else { + self.uint32()? as usize + }; + if offset + self.subresult_offset > self.buf.len() { return Err(Error::Error(ExitError::InvalidRange)); } + + let new_offset = offset + subresult_offset; Ok(AbiReader { buf: self.buf, - subresult_offset: offset + self.subresult_offset, - offset: offset + self.subresult_offset, + subresult_offset: new_offset, + offset: new_offset, }) } + /// Is this parser reached end of buffer? pub fn is_finished(&self) -> bool { self.buf.len() == self.offset } } +/// Writer for RLP encoded data #[derive(Default)] pub struct AbiWriter { static_part: Vec, @@ -187,9 +220,11 @@ had_call: bool, } impl AbiWriter { + /// Initialize internal buffers for output data, assuming no padding required pub fn new() -> Self { Self::default() } + /// Initialize internal buffers, inserting method selector at beginning pub fn new_call(method_id: u32) -> Self { let mut val = Self::new(); val.static_part.extend(&method_id.to_be_bytes()); @@ -211,59 +246,71 @@ .extend(&[0; ABI_ALIGNMENT][0..ABI_ALIGNMENT - bytes.len()]); } + /// Write [`H160`] to end of buffer pub fn address(&mut self, address: &H160) { self.write_padleft(&address.0) } + /// Write [`bool`] to end of buffer pub fn bool(&mut self, value: &bool) { self.write_padleft(&[if *value { 1 } else { 0 }]) } + /// Write [`u8`] to end of buffer pub fn uint8(&mut self, value: &u8) { self.write_padleft(&[*value]) } + /// Write [`u32`] to end of buffer pub fn uint32(&mut self, value: &u32) { self.write_padleft(&u32::to_be_bytes(*value)) } + /// Write [`u128`] to end of buffer pub fn uint128(&mut self, value: &u128) { self.write_padleft(&u128::to_be_bytes(*value)) } + /// Write [`U256`] to end of buffer pub fn uint256(&mut self, value: &U256) { let mut out = [0; 32]; value.to_big_endian(&mut out); self.write_padleft(&out) } + /// Write [`usize`] to end of buffer + #[deprecated = "dangerous, as usize may have different width in wasm and native execution"] pub fn write_usize(&mut self, value: &usize) { self.write_padleft(&usize::to_be_bytes(*value)) } + /// Append recursive data, writing pending offset at end of buffer pub fn write_subresult(&mut self, result: Self) { self.dynamic_part.push((self.static_part.len(), result)); // Empty block, to be filled later self.write_padleft(&[]); } - pub fn memory(&mut self, value: &[u8]) { + fn memory(&mut self, value: &[u8]) { let mut sub = Self::new(); - sub.write_usize(&value.len()); + sub.uint32(&(value.len() as u32)); for chunk in value.chunks(ABI_ALIGNMENT) { sub.write_padright(chunk); } self.write_subresult(sub); } + /// Append recursive [`str`] at end of buffer pub fn string(&mut self, value: &str) { self.memory(value.as_bytes()) } + /// Append recursive [`[u8]`] at end of buffer pub fn bytes(&mut self, value: &[u8]) { self.memory(value) } + /// Finish writer, concatenating all internal buffers pub fn finish(mut self) -> Vec { for (static_offset, part) in self.dynamic_part { let part_offset = self.static_part.len() - self.had_call.then(|| 4).unwrap_or(0); @@ -278,30 +325,48 @@ } } +/// [`AbiReader`] implements reading of many types, but it should +/// be limited to types defined in spec +/// +/// As this trait can't be made sealed, +/// instead of having `impl AbiRead for T`, we have `impl AbiRead for AbiReader` pub trait AbiRead { + /// Read item from current position, advanding decoder fn abi_read(&mut self) -> Result; + + /// Size for type aligned to [`ABI_ALIGNMENT`]. + fn size() -> usize; } macro_rules! impl_abi_readable { - ($ty:ty, $method:ident) => { + ($ty:ty, $method:ident, $dynamic:literal) => { + impl TypeHelper for $ty { + fn is_dynamic() -> bool { + $dynamic + } + } impl AbiRead<$ty> for AbiReader<'_> { fn abi_read(&mut self) -> Result<$ty> { self.$method() } + + fn size() -> usize { + ABI_ALIGNMENT + } } }; } -impl_abi_readable!(u8, uint8); -impl_abi_readable!(u32, uint32); -impl_abi_readable!(u64, uint64); -impl_abi_readable!(u128, uint128); -impl_abi_readable!(U256, uint256); -impl_abi_readable!([u8; 4], bytes4); -impl_abi_readable!(H160, address); -impl_abi_readable!(Vec, bytes); -impl_abi_readable!(bool, bool); -impl_abi_readable!(string, string); +impl_abi_readable!(u8, uint8, false); +impl_abi_readable!(u32, uint32, false); +impl_abi_readable!(u64, uint64, false); +impl_abi_readable!(u128, uint128, false); +impl_abi_readable!(U256, uint256, false); +impl_abi_readable!([u8; 4], bytes4, false); +impl_abi_readable!(H160, address, false); +impl_abi_readable!(Vec, bytes, true); +impl_abi_readable!(bool, bool, true); +impl_abi_readable!(string, string, true); mod sealed { /// Not all types can be placed in vec, i.e `Vec` is restricted, `bytes` should be used instead @@ -317,8 +382,8 @@ Self: AbiRead, { fn abi_read(&mut self) -> Result> { - let mut sub = self.subresult()?; - let size = sub.read_usize()?; + let mut sub = self.subresult(None)?; + let size = sub.uint32()? as usize; sub.subresult_offset = sub.offset; let mut out = Vec::with_capacity(size); for _ in 0..size { @@ -326,21 +391,51 @@ } Ok(out) } + + fn size() -> usize { + ABI_ALIGNMENT + } } macro_rules! impl_tuples { ($($ident:ident)+) => { + impl<$($ident: TypeHelper,)+> TypeHelper for ($($ident,)+) { + fn is_dynamic() -> bool { + false + $( + || <$ident>::is_dynamic() + )* + } + } impl<$($ident),+> sealed::CanBePlacedInVec for ($($ident,)+) {} impl<$($ident),+> AbiRead<($($ident,)+)> for AbiReader<'_> where - $(Self: AbiRead<$ident>),+ + $( + Self: AbiRead<$ident>, + )+ + ($($ident,)+): TypeHelper, { fn abi_read(&mut self) -> Result<($($ident,)+)> { - let mut subresult = self.subresult()?; + let size = if !<($($ident,)+)>::is_dynamic() { Some(>::size()) } else { None }; + let mut subresult = self.subresult(size)?; Ok(( $(>::abi_read(&mut subresult)?,)+ )) } + + fn size() -> usize { + 0 $(+ as AbiRead<$ident>>::size())+ + } + } + #[allow(non_snake_case)] + impl<$($ident),+> AbiWrite for &($($ident,)+) + where + $($ident: AbiWrite,)+ + { + fn abi_write(&self, writer: &mut AbiWriter) { + let ($($ident,)+) = self; + $($ident.abi_write(writer);)+ + } } }; } @@ -356,8 +451,13 @@ impl_tuples! {A B C D E F G H I} impl_tuples! {A B C D E F G H I J} +/// For questions about inability to provide custom implementations, +/// see [`AbiRead`] pub trait AbiWrite { + /// Write value to end of specified encoder fn abi_write(&self, writer: &mut AbiWriter); + /// Specialization for [`crate::solidity_interface`] implementation, + /// see comment in `impl AbiWrite for ResultWithPostInfo` fn to_result(&self) -> ResultWithPostInfo { let mut writer = AbiWriter::new(); self.abi_write(&mut writer); @@ -365,13 +465,11 @@ } } +/// This particular AbiWrite implementation should be split to another trait, +/// which only implements `to_result`, but due to lack of specialization feature +/// in stable Rust, we can't have blanket impl of this trait `for T where T: AbiWrite`, +/// so here we abusing default trait methods for it impl AbiWrite for ResultWithPostInfo { - // this particular AbiWrite implementation should be split to another trait, - // which only implements [`to_result`] - // - // But due to lack of specialization feature in stable Rust, we can't have - // blanket impl of this trait `for T where T: AbiWrite`, so here we abusing - // default trait methods for it fn abi_write(&self, _writer: &mut AbiWriter) { debug_assert!(false, "shouldn't be called, see comment") } @@ -422,6 +520,8 @@ fn abi_write(&self, _writer: &mut AbiWriter) {} } +/// Helper macros to parse reader into variables +#[deprecated] #[macro_export] macro_rules! abi_decode { ($reader:expr, $($name:ident: $typ:ident),+ $(,)?) => { @@ -430,6 +530,9 @@ )+ } } + +/// Helper macros to construct RLP-encoded buffer +#[deprecated] #[macro_export] macro_rules! abi_encode { ($($typ:ident($value:expr)),* $(,)?) => {{ @@ -479,7 +582,7 @@ assert_eq!(encoded, alternative_encoded); let mut decoder = AbiReader::new(&encoded); - assert_eq!(decoder.bool().unwrap(), true); + assert!(decoder.bool().unwrap()); assert_eq!(decoder.string().unwrap(), "test"); } @@ -548,4 +651,49 @@ ] ); } + + #[test] + fn parse_vec_with_simple_type() { + use crate::types::address; + use primitive_types::{H160, U256}; + + let (call, mut decoder) = AbiReader::new_call(&hex!( + " + 1ACF2D55 + 0000000000000000000000000000000000000000000000000000000000000020 // offset of (address, uint256)[] + 0000000000000000000000000000000000000000000000000000000000000003 // length of (address, uint256)[] + + 0000000000000000000000002D2FF76104B7BACB2E8F6731D5BFC184EBECDDBC // address + 000000000000000000000000000000000000000000000000000000000000000A // uint256 + + 000000000000000000000000AB8E3D9134955566483B11E6825C9223B6737B10 // address + 0000000000000000000000000000000000000000000000000000000000000014 // uint256 + + 0000000000000000000000008C582BDF2953046705FC56F189385255EFC1BE18 // address + 000000000000000000000000000000000000000000000000000000000000001E // uint256 + " + )) + .unwrap(); + assert_eq!(call, u32::to_be_bytes(0x1ACF2D55)); + let data = + as AbiRead>>::abi_read(&mut decoder).unwrap(); + assert_eq!(data.len(), 3); + assert_eq!( + data, + vec![ + ( + H160(hex!("2D2FF76104B7BACB2E8F6731D5BFC184EBECDDBC")), + U256([10, 0, 0, 0]) + ), + ( + H160(hex!("AB8E3D9134955566483B11E6825C9223B6737B10")), + U256([20, 0, 0, 0]) + ), + ( + H160(hex!("8C582BDF2953046705FC56F189385255EFC1BE18")), + U256([30, 0, 0, 0]) + ), + ] + ); + } } --- a/crates/evm-coder/src/events.rs +++ b/crates/evm-coder/src/events.rs @@ -19,11 +19,23 @@ use crate::types::*; +/// Implementation of this trait should not be written manually, +/// instead use [`crate::ToLog`] proc macros. +/// +/// See also [`evm_coder_procedural::ToLog`], [solidity docs on events](https://docs.soliditylang.org/en/develop/contracts.html#events) pub trait ToLog { + /// Convert event to [`ethereum::Log`]. + /// Because event by itself doesn't contains current contract + /// address, it should be specified manually. fn to_log(&self, contract: H160) -> Log; } +/// Only items implementing `ToTopic` may be used as `#[indexed]` field +/// in [`crate::ToLog`] macro usage. +/// +/// See also (solidity docs on events)[] pub trait ToTopic { + /// Convert value to topic to be used in [`ethereum::Log`] fn to_topic(&self) -> H256; } --- a/crates/evm-coder/src/execution.rs +++ b/crates/evm-coder/src/execution.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! Contract execution related types + #[cfg(not(feature = "std"))] use alloc::string::{String, ToString}; use evm_core::{ExitError, ExitFatal}; @@ -22,10 +24,14 @@ use crate::Weight; +/// Execution error, should be convertible between EVM and Substrate. #[derive(Debug, Clone)] pub enum Error { + /// Non-fatal contract error occured Revert(String), + /// EVM fatal error Fatal(ExitFatal), + /// EVM normal error Error(ExitError), } @@ -38,9 +44,12 @@ } } +/// To be used in [`crate::solidity_interface`] implementation. pub type Result = core::result::Result; +/// Static information collected from [`crate::weight`]. pub struct DispatchInfo { + /// Statically predicted call weight pub weight: Weight, } @@ -55,16 +64,22 @@ } } +/// Weight information that is only available post dispatch. +/// Note: This can only be used to reduce the weight or fee, not increase it. #[derive(Default, Clone)] pub struct PostDispatchInfo { + /// Actual weight consumed by call actual_weight: Option, } impl PostDispatchInfo { + /// Calculate amount to be returned back to user pub fn calc_unspent(&self, info: &DispatchInfo) -> Weight { info.weight - self.calc_actual_weight(info) } + /// Calculate actual consumed weight, saturating to weight reported + /// pre-dispatch pub fn calc_actual_weight(&self, info: &DispatchInfo) -> Weight { if let Some(actual_weight) = self.actual_weight { actual_weight.min(info.weight) @@ -74,9 +89,12 @@ } } +/// Wrapper for PostDispatchInfo and any user-provided data #[derive(Clone)] pub struct WithPostDispatchInfo { + /// User provided data pub data: T, + /// Info known after dispatch pub post_info: PostDispatchInfo, } @@ -89,5 +107,6 @@ } } +/// Return type of items in [`crate::solidity_interface`] definition pub type ResultWithPostInfo = core::result::Result, WithPostDispatchInfo>; --- a/crates/evm-coder/src/lib.rs +++ b/crates/evm-coder/src/lib.rs @@ -14,22 +14,103 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] #[cfg(not(feature = "std"))] extern crate alloc; use abi::{AbiRead, AbiReader, AbiWriter}; -pub use evm_coder_macros::{event_topic, fn_selector, solidity_interface, solidity, weight, ToLog}; +pub use evm_coder_procedural::{event_topic, fn_selector}; pub mod abi; -pub mod events; -pub use events::ToLog; +pub use events::{ToLog, ToTopic}; use execution::DispatchInfo; pub mod execution; + +/// Derives call enum implementing [`crate::Callable`], [`crate::Weighted`] +/// and [`crate::Call`] from impl block. +/// +/// ## Macro syntax +/// +/// `#[solidity_interface(name, is, inline_is, events)]` +/// - *name* - used in generated code, and for Call enum name +/// - *is* - used to provide inheritance in Solidity +/// - *inline_is* - same as `is`, but ERC165::SupportsInterface will work differently: For `is` SupportsInterface(A) will return true +/// if A is one of the interfaces the contract is inherited from (e.g. B is created as `is(A)`). If B is created as `inline_is(A)` +/// SupportsInterface(A) will internally create a new interface that combines all methods of A and B, so SupportsInterface(A) will return +/// false. +/// +/// `#[weight(value)]` +/// Can be added to every method of impl block, used for deriving [`crate::Weighted`], which +/// is used by substrate bridge. +/// - *value*: expression, which evaluates to weight required to call this method. +/// This expression can use call arguments to calculate non-constant execution time. +/// This expression should evaluate faster than actual execution does, and may provide worse case +/// than one is called. +/// +/// `#[solidity_interface(rename_selector)]` +/// - *rename_selector* - by default, selector name will be generated by transforming method name +/// from snake_case to camelCase. Use this option, if other naming convention is required. +/// I.e: method `token_uri` will be automatically renamed to `tokenUri` in selector, but name +/// required by ERC721 standard is `tokenURI`, thus we need to specify `rename_selector = "tokenURI"` +/// explicitly. +/// +/// Both contract and contract methods may have doccomments, which will end up in a generated +/// solidity interface file, thus you should use [solidity syntax](https://docs.soliditylang.org/en/latest/natspec-format.html) for writing documentation in this macro +/// +/// ## Example +/// +/// ```ignore +/// struct SuperContract; +/// struct InlineContract; +/// struct Contract; +/// +/// #[derive(ToLog)] +/// enum ContractEvents { +/// Event(#[indexed] uint32), +/// } +/// +/// /// @dev This contract provides function to multiply two numbers +/// #[solidity_interface(name = MyContract, is(SuperContract), inline_is(InlineContract))] +/// impl Contract { +/// /// Multiply two numbers +/// /// @param a First number +/// /// @param b Second number +/// /// @return uint32 Product of two passed numbers +/// /// @dev This function returns error in case of overflow +/// #[weight(200 + a + b)] +/// #[solidity_interface(rename_selector = "mul")] +/// fn mul(&mut self, a: uint32, b: uint32) -> Result { +/// Ok(a.checked_mul(b).ok_or("overflow")?) +/// } +/// } +/// ``` +pub use evm_coder_procedural::solidity_interface; +/// See [`solidity_interface`] +pub use evm_coder_procedural::solidity; +/// See [`solidity_interface`] +pub use evm_coder_procedural::weight; + +/// Derives [`ToLog`] for enum +/// +/// Selectors will be derived from variant names, there is currently no way to have custom naming +/// for them +/// +/// `#[indexed]` +/// Marks this field as indexed, so it will appear in [`ethereum::Log`] topics instead of data +pub use evm_coder_procedural::ToLog; + +// Api of those modules shouldn't be consumed directly, it is only exported for usage in proc macros +#[doc(hidden)] +pub mod events; +#[doc(hidden)] pub mod solidity; -/// Solidity type definitions +/// Solidity type definitions (aliases from solidity name to rust type) +/// To be used in [`solidity_interface`] definitions, to make sure there is no +/// type conflict between Rust code and generated definitions pub mod types { - #![allow(non_camel_case_types)] + #![allow(non_camel_case_types, missing_docs)] #[cfg(not(feature = "std"))] use alloc::{vec::Vec}; @@ -54,6 +135,8 @@ pub type string = ::std::string::String; pub type bytes = Vec; + /// Solidity doesn't have `void` type, however we have special implementation + /// for empty tuple return type pub type void = (); //#region Special types @@ -63,36 +146,64 @@ pub type caller = address; //#endregion + /// Ethereum typed call message, similar to solidity + /// `msg` object. pub struct Msg { pub call: C, + /// Address of user, which called this contract. pub caller: H160, + /// Payment amount to contract. + /// Contract should reject payment, if target call is not payable, + /// and there is no `receiver()` function defined. pub value: U256, } } +/// Parseable EVM call, this trait should be implemented with [`solidity_interface`] macro pub trait Call: Sized { + /// Parse call buffer into typed call enum fn parse(selector: types::bytes4, input: &mut AbiReader) -> execution::Result>; } +/// Intended to be used as `#[weight]` output type +/// Should be same between evm-coder and substrate to avoid confusion +/// +/// Isn't same thing as gas, some mapping is required between those types pub type Weight = u64; +/// In substrate, we have benchmarking, which allows +/// us to not rely on gas metering, but instead predict amount of gas to execute call pub trait Weighted: Call { + /// Predict weight of this call fn weight(&self) -> DispatchInfo; } +/// Type callable with ethereum message, may be implemented by [`solidity_interface`] macro +/// on interface implementation, or for externally-owned real EVM contract pub trait Callable { + /// Call contract using specified call data fn call(&mut self, call: types::Msg) -> execution::ResultWithPostInfo; } -/// Implementation is implicitly provided for all interfaces +/// Implementation of ERC165 is implicitly generated for all interfaces in [`solidity_interface`], +/// this structure holds parsed data for ERC165Call subvariant +/// +/// Note: no [`Callable`] implementation is provided, call implementation is inlined into every +/// implementing contract /// -/// Note: no Callable implementation is provided +/// See #[derive(Debug)] pub enum ERC165Call { - SupportsInterface { interface_id: types::bytes4 }, + /// ERC165 provides single method, which returns true, if contract + /// implements specified interface + SupportsInterface { + /// Requested interface + interface_id: types::bytes4, + }, } impl ERC165Call { + /// ERC165 selector is provided by standard pub const INTERFACE_ID: types::bytes4 = u32::to_be_bytes(0x01ffc9a7); } --- a/crates/evm-coder/src/solidity.rs +++ b/crates/evm-coder/src/solidity.rs @@ -14,26 +14,31 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! Implementation detail of [`crate::solidity_interface`] macro code-generation. +//! You should not rely on any public item from this module, as it is only intended to be used +//! by procedural macro, API and output format may be changed at any time. +//! +//! Purpose of this module is to receive solidity contract definition in module-specified +//! format, and then output string, representing interface of this contract in solidity language + #[cfg(not(feature = "std"))] -use alloc::{ - string::String, - vec::Vec, - collections::{BTreeSet, BTreeMap}, - format, -}; +use alloc::{string::String, vec::Vec, collections::BTreeMap, format}; #[cfg(feature = "std")] -use std::collections::{BTreeSet, BTreeMap}; +use std::collections::BTreeMap; use core::{ fmt::{self, Write}, marker::PhantomData, cell::{Cell, RefCell}, + cmp::Reverse, }; use impl_trait_for_tuples::impl_for_tuples; use crate::types::*; #[derive(Default)] pub struct TypeCollector { - structs: RefCell>, + /// Code => id + /// id ordering is required to perform topo-sort on the resulting data + structs: RefCell>, anonymous: RefCell, usize>>, id: Cell, } @@ -42,7 +47,8 @@ Self::default() } pub fn collect(&self, item: string) { - self.structs.borrow_mut().insert(item); + let id = self.next_id(); + self.structs.borrow_mut().insert(item, id); } pub fn next_id(&self) -> usize { let v = self.id.get(); @@ -56,7 +62,7 @@ } let id = self.next_id(); let mut str = String::new(); - writeln!(str, "// Anonymous struct").unwrap(); + writeln!(str, "/// @dev anonymous struct").unwrap(); writeln!(str, "struct Tuple{} {{", id).unwrap(); for (i, name) in names.iter().enumerate() { writeln!(str, "\t{} field_{};", name, i).unwrap(); @@ -66,15 +72,19 @@ self.anonymous.borrow_mut().insert(names, id); format!("Tuple{}", id) } - pub fn finish(self) -> BTreeSet { - self.structs.into_inner() + pub fn finish(self) -> Vec { + let mut data = self.structs.into_inner().into_iter().collect::>(); + data.sort_by_key(|(_, id)| Reverse(*id)); + data.into_iter().map(|(code, _)| code).collect() } } pub trait SolidityTypeName: 'static { fn solidity_name(writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result; + /// "simple" types are stored inline, no `memory` modifier should be used in solidity fn is_simple() -> bool; fn solidity_default(writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result; + /// Specialization fn is_void() -> bool { false } @@ -125,6 +135,10 @@ } mod sealed { + /// Not every type should be directly placed in vec. + /// Vec encoding is not memory efficient, as every item will be padded + /// to 32 bytes. + /// Instead you should use specialized types (`bytes` in case of `Vec`) pub trait CanBePlacedInVec {} } @@ -180,9 +194,16 @@ fn is_simple() -> bool { false } + #[allow(unused_assignments)] fn solidity_default(writer: &mut impl fmt::Write, tc: &TypeCollector) -> fmt::Result { write!(writer, "{}(", tc.collect_tuple::())?; + let mut first = true; $( + if !first { + write!(writer, ",")?; + } else { + first = false; + } <$ident>::solidity_default(writer, tc)?; )* write!(writer, ")") @@ -395,7 +416,8 @@ } pub struct SolidityFunction { pub docs: &'static [&'static str], - pub selector: &'static str, + pub selector_str: &'static str, + pub selector: u32, pub name: &'static str, pub args: A, pub result: R, @@ -409,12 +431,14 @@ tc: &TypeCollector, ) -> fmt::Result { for doc in self.docs { - writeln!(writer, "\t//{}", doc)?; + writeln!(writer, "\t///{}", doc)?; } - if !self.docs.is_empty() { - writeln!(writer, "\t//")?; - } - writeln!(writer, "\t// Selector: {}", self.selector)?; + writeln!( + writer, + "\t/// @dev EVM selector for this function is: 0x{:0>8x},", + self.selector + )?; + writeln!(writer, "\t/// or in textual repr: {}", self.selector_str)?; write!(writer, "\tfunction {}(", self.name)?; self.args.solidity_name(writer, tc)?; write!(writer, ")")?; @@ -455,7 +479,7 @@ } } -#[impl_for_tuples(0, 24)] +#[impl_for_tuples(0, 48)] impl SolidityFunctions for Tuple { for_tuples!( where #( Tuple: SolidityFunctions ),* ); @@ -474,6 +498,7 @@ } pub struct SolidityInterface { + pub docs: &'static [&'static str], pub selector: bytes4, pub name: &'static str, pub is: &'static [&'static str], @@ -488,10 +513,13 @@ tc: &TypeCollector, ) -> fmt::Result { const ZERO_BYTES: [u8; 4] = [0; 4]; + for doc in self.docs { + writeln!(out, "///{}", doc)?; + } if self.selector != ZERO_BYTES { writeln!( out, - "// Selector: {:0>8x}", + "/// @dev the ERC-165 identifier for this interface is 0x{:0>8x}", u32::from_be_bytes(self.selector) )?; } --- a/crates/evm-coder/tests/generics.rs +++ b/crates/evm-coder/tests/generics.rs @@ -19,14 +19,14 @@ struct Generic(PhantomData); -#[solidity_interface(name = "GenericIs")] +#[solidity_interface(name = GenericIs)] impl Generic { fn test_1(&self) -> Result { unreachable!() } } -#[solidity_interface(name = "Generic", is(GenericIs))] +#[solidity_interface(name = Generic, is(GenericIs))] impl> Generic { fn test_2(&self) -> Result { unreachable!() @@ -35,7 +35,7 @@ generate_stubgen!(gen_iface, GenericCall<()>, false); -#[solidity_interface(name = "GenericWhere")] +#[solidity_interface(name = GenericWhere)] impl Generic where T: core::fmt::Debug, --- a/crates/evm-coder/tests/random.rs +++ b/crates/evm-coder/tests/random.rs @@ -16,19 +16,18 @@ #![allow(dead_code)] // This test only checks that macros is not panicking -use evm_coder::{ToLog, execution::Result, solidity_interface, types::*}; -use evm_coder_macros::{solidity, weight}; +use evm_coder::{ToLog, execution::Result, solidity_interface, types::*, solidity, weight}; struct Impls; -#[solidity_interface(name = "OurInterface")] +#[solidity_interface(name = OurInterface)] impl Impls { fn fn_a(&self, _input: uint256) -> Result { unreachable!() } } -#[solidity_interface(name = "OurInterface1")] +#[solidity_interface(name = OurInterface1)] impl Impls { fn fn_b(&self, _input: uint128) -> Result { unreachable!() @@ -48,7 +47,7 @@ } #[solidity_interface( - name = "OurInterface2", + name = OurInterface2, is(OurInterface), inline_is(OurInterface1), events(OurEvents) @@ -79,3 +78,9 @@ unreachable!() } } + +#[solidity_interface( + name = ValidSelector, + expect_selector = 0x00000000, +)] +impl Impls {} --- a/crates/evm-coder/tests/solidity_generation.rs +++ b/crates/evm-coder/tests/solidity_generation.rs @@ -18,7 +18,7 @@ struct ERC20; -#[solidity_interface(name = "ERC20")] +#[solidity_interface(name = ERC20)] impl ERC20 { fn decimals(&self) -> Result { unreachable!() --- a/node/cli/src/chain_spec.rs +++ b/node/cli/src/chain_spec.rs @@ -123,7 +123,7 @@ .public() } -/// The extensions for the [`ChainSpec`]. +/// The extensions for the [`DefaultChainSpec`]. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ChainSpecGroup, ChainSpecExtension)] #[serde(deny_unknown_fields)] pub struct Extensions { --- a/pallets/common/CHANGELOG.md +++ b/pallets/common/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to this project will be documented in this file. +## [0.1.8] - 2022-08-24 + +## Added + - Eth methods for collection + + set_collection_sponsor_substrate + + has_collection_pending_sponsor + + remove_collection_sponsor + + get_collection_sponsor +- Add convert function from `uint256` to `CrossAccountId`. + +## [0.1.7] - 2022-08-19 + +### Added + + - Add convert funtion from `CrossAccountId` to eth `uint256`. + + +## [0.1.6] - 2022-08-16 + +### Added +- New Ethereum API methods: changeOwner, changeOwner(Substrate) and verifyOwnerOrAdmin(Substrate). + ## [v0.1.5] 2022-08-16 --- a/pallets/common/Cargo.toml +++ b/pallets/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-common" -version = "0.1.5" +version = "0.1.8" license = "GPLv3" edition = "2021" --- a/pallets/common/src/erc.rs +++ b/pallets/common/src/erc.rs @@ -26,11 +26,14 @@ use sp_std::vec::Vec; use up_data_structs::{ AccessMode, CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property, - SponsoringRateLimit, + SponsoringRateLimit, SponsorshipState, }; use alloc::format; -use crate::{Pallet, CollectionHandle, Config, CollectionProperties}; +use crate::{ + Pallet, CollectionHandle, Config, CollectionProperties, + eth::{convert_cross_account_to_uint256, convert_uint256_to_cross_account}, +}; /// Events for ethereum collection helper. #[derive(ToLog)] @@ -57,10 +60,10 @@ } /// @title A contract that allows you to work with collections. -#[solidity_interface(name = "Collection")] +#[solidity_interface(name = Collection)] impl CollectionHandle where - T::AccountId: From<[u8; 32]>, + T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>, { /// Set collection property. /// @@ -125,6 +128,32 @@ save(self) } + /// Set the substrate sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Substrate address of the sponsor from whose account funds will be debited for operations with the contract. + fn set_collection_sponsor_substrate( + &mut self, + caller: caller, + sponsor: uint256, + ) -> Result { + check_is_owner_or_admin(caller, self)?; + + let sponsor = convert_uint256_to_cross_account::(sponsor); + self.set_sponsor(sponsor.as_sub().clone()) + .map_err(dispatch_to_evm::)?; + save(self) + } + + // /// Whether there is a pending sponsor. + fn has_collection_pending_sponsor(&self) -> Result { + Ok(matches!( + self.collection.sponsorship, + SponsorshipState::Unconfirmed(_) + )) + } + /// Collection sponsorship confirmation. /// /// @dev After setting the sponsor for the collection, it must be confirmed with this function. @@ -139,6 +168,32 @@ save(self) } + /// Remove collection sponsor. + fn remove_collection_sponsor(&mut self, caller: caller) -> Result { + check_is_owner_or_admin(caller, self)?; + self.remove_sponsor().map_err(dispatch_to_evm::)?; + save(self) + } + + /// Get current sponsor. + /// + /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + fn get_collection_sponsor(&self) -> Result<(address, uint256)> { + let sponsor = match self.collection.sponsorship.sponsor() { + Some(sponsor) => sponsor, + None => return Ok(Default::default()), + }; + let sponsor = T::CrossAccountId::from_sub(sponsor.clone()); + let result: (address, uint256) = if sponsor.is_canonical_substrate() { + let sponsor = convert_cross_account_to_uint256::(&sponsor); + (Default::default(), sponsor) + } else { + let sponsor = *sponsor.as_eth(); + (sponsor, Default::default()) + }; + Ok(result) + } + /// Set limits for the collection. /// @dev Throws error if limit not found. /// @param limit Name of the limit. Valid names: @@ -225,17 +280,14 @@ } /// Add collection admin by substrate address. - /// @param new_admin Substrate administrator address. + /// @param newAdmin Substrate administrator address. fn add_collection_admin_substrate( &mut self, caller: caller, new_admin: uint256, ) -> Result { let caller = T::CrossAccountId::from_eth(caller); - let mut new_admin_arr: [u8; 32] = Default::default(); - new_admin.to_big_endian(&mut new_admin_arr); - let account_id = T::AccountId::from(new_admin_arr); - let new_admin = T::CrossAccountId::from_sub(account_id); + let new_admin = convert_uint256_to_cross_account::(new_admin); >::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::)?; Ok(()) } @@ -248,16 +300,13 @@ admin: uint256, ) -> Result { let caller = T::CrossAccountId::from_eth(caller); - let mut admin_arr: [u8; 32] = Default::default(); - admin.to_big_endian(&mut admin_arr); - let account_id = T::AccountId::from(admin_arr); - let admin = T::CrossAccountId::from_sub(account_id); + let admin = convert_uint256_to_cross_account::(admin); >::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::)?; Ok(()) } /// Add collection admin. - /// @param new_admin Address of the added administrator. + /// @param newAdmin Address of the added administrator. fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result { let caller = T::CrossAccountId::from_eth(caller); let new_admin = T::CrossAccountId::from_eth(new_admin); @@ -267,7 +316,7 @@ /// Remove collection admin. /// - /// @param new_admin Address of the removed administrator. + /// @param admin Address of the removed administrator. fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result { let caller = T::CrossAccountId::from_eth(caller); let admin = T::CrossAccountId::from_eth(admin); @@ -414,12 +463,21 @@ /// /// @param user account to verify /// @return "true" if account is the owner or admin - fn verify_owner_or_admin(&self, user: address) -> Result { - Ok(check_is_owner_or_admin(user, self) - .map(|_| true) - .unwrap_or(false)) + #[solidity(rename_selector = "isOwnerOrAdmin")] + fn is_owner_or_admin_eth(&self, user: address) -> Result { + let user = T::CrossAccountId::from_eth(user); + Ok(self.is_owner_or_admin(&user)) } + /// Check that substrate account is the owner or admin of the collection + /// + /// @param user account to verify + /// @return "true" if account is the owner or admin + fn is_owner_or_admin_substrate(&self, user: uint256) -> Result { + let user = convert_uint256_to_cross_account::(user); + Ok(self.is_owner_or_admin(&user)) + } + /// Returns collection type /// /// @return `Fungible` or `NFT` or `ReFungible` @@ -431,6 +489,28 @@ }; Ok(mode.into()) } + + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner account + fn set_owner(&mut self, caller: caller, new_owner: address) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let new_owner = T::CrossAccountId::from_eth(new_owner); + self.set_owner_internal(caller, new_owner) + .map_err(dispatch_to_evm::) + } + + /// Changes collection owner to another substrate account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner substrate account + fn set_owner_substrate(&mut self, caller: caller, new_owner: uint256) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let new_owner = convert_uint256_to_cross_account::(new_owner); + self.set_owner_internal(caller, new_owner) + .map_err(dispatch_to_evm::) + } } fn check_is_owner_or_admin( @@ -440,16 +520,17 @@ let caller = T::CrossAccountId::from_eth(caller); collection .check_is_owner_or_admin(&caller) - .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; + .map_err(dispatch_to_evm::)?; Ok(caller) } fn save(collection: &CollectionHandle) -> Result { // TODO possibly delete for the lack of transaction + collection.consume_store_writes(1)?; collection .check_is_internal() .map_err(dispatch_to_evm::)?; - >::insert(collection.id, collection.collection.clone()); + collection.save().map_err(dispatch_to_evm::)?; Ok(()) } --- a/pallets/common/src/eth.rs +++ b/pallets/common/src/eth.rs @@ -16,8 +16,10 @@ //! The module contains a number of functions for converting and checking ethereum identifiers. +use evm_coder::types::uint256; +pub use pallet_evm::account::{Config, CrossAccountId}; +use sp_core::H160; use up_data_structs::CollectionId; -use sp_core::H160; // 0x17c4e6453Cc49AAAaEACA894e6D9683e00000001 - collection 1 // TODO: Unhardcode prefix @@ -47,3 +49,23 @@ pub fn is_collection(address: &H160) -> bool { address[0..16] == ETH_COLLECTION_PREFIX } + +/// Convert `CrossAccountId` to `uint256`. +pub fn convert_cross_account_to_uint256(from: &T::CrossAccountId) -> uint256 +where + T::AccountId: AsRef<[u8; 32]>, +{ + let slice = from.as_sub().as_ref(); + uint256::from_big_endian(slice) +} + +/// Convert `uint256` to `CrossAccountId`. +pub fn convert_uint256_to_cross_account(from: uint256) -> T::CrossAccountId +where + T::AccountId: From<[u8; 32]>, +{ + let mut new_admin_arr = [0_u8; 32]; + from.to_big_endian(&mut new_admin_arr); + let account_id = T::AccountId::from(new_admin_arr); + T::CrossAccountId::from_sub(account_id) +} --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -203,8 +203,8 @@ } /// Save collection to storage. - pub fn save(self) -> DispatchResult { - >::insert(self.id, self.collection); + pub fn save(&self) -> DispatchResult { + >::insert(self.id, &self.collection); Ok(()) } @@ -231,6 +231,12 @@ Ok(true) } + /// Remove collection sponsor. + pub fn remove_sponsor(&mut self) -> DispatchResult { + self.collection.sponsorship = SponsorshipState::Disabled; + Ok(()) + } + /// Checks that the collection was created with, and must be operated upon through **Unique API**. /// Now check only the `external_collection` flag and if it's **true**, then return [`Error::CollectionIsExternal`] error. pub fn check_is_internal(&self) -> DispatchResult { @@ -302,6 +308,17 @@ ); Ok(()) } + + /// Changes collection owner to another account + fn set_owner_internal( + &mut self, + caller: T::CrossAccountId, + new_owner: T::CrossAccountId, + ) -> DispatchResult { + self.check_is_owner(&caller)?; + self.collection.owner = new_owner.as_sub().clone(); + self.save() + } } #[frame_support::pallet] @@ -721,12 +738,7 @@ /// Get the effective limits for the collection. pub fn effective_collection_limits(collection: CollectionId) -> Option { - let collection = >::get(collection); - if collection.is_none() { - return None; - } - - let collection = collection.unwrap(); + let collection = >::get(collection)?; let limits = collection.limits; let effective_limits = CollectionLimits { account_token_ownership_limit: Some(limits.account_token_ownership_limit()), --- a/pallets/evm-contract-helpers/CHANGELOG.md +++ b/pallets/evm-contract-helpers/CHANGELOG.md @@ -1,4 +1,24 @@ - +# Change Log + +All notable changes to this project will be documented in this file. + +## [v0.2.0] - 2022-08-19 + +### Added + + - Set arbitrary evm address as contract sponsor. + - Ability to remove current sponsor. + +### Removed + - Remove methods + + sponsoring_enabled + + toggle_sponsoring + + ### Changed + + - Change `toggle_sponsoring` to `self_sponsored_enable`. + + ## [v0.1.2] 2022-08-16 ### Other changes --- a/pallets/evm-contract-helpers/Cargo.toml +++ b/pallets/evm-contract-helpers/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-evm-contract-helpers" -version = "0.1.2" +version = "0.2.0" license = "GPLv3" edition = "2021" --- /dev/null +++ b/pallets/evm-contract-helpers/README.md @@ -0,0 +1,13 @@ +# EVM Contract Helpers + +This pallet extends pallet-evm contracts with several new functions. + +## Overview + +Evm contract helpers pallet provides ability to + +- Tracking and getting of user, which deployed contract +- Sponsoring EVM contract calls (Make transaction calls to be free for users, instead making them being paid from contract address) +- Allowlist access mode + +As most of those functions are intented to be consumed by ethereum users, only API provided by this pallet is [ContractHelpers magic contract](./src/stubs/ContractHelpers.sol) \ No newline at end of file --- a/pallets/evm-contract-helpers/src/eth.rs +++ b/pallets/evm-contract-helpers/src/eth.rs @@ -14,22 +14,27 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +//! Implementation of magic contract + use core::marker::PhantomData; use evm_coder::{abi::AbiWriter, execution::Result, generate_stubgen, solidity_interface, types::*}; -use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder}; +use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder, dispatch_to_evm}; use pallet_evm::{ ExitRevert, OnCreate, OnMethodCall, PrecompileResult, PrecompileFailure, PrecompileHandle, account::CrossAccountId, }; use sp_core::H160; +use up_data_structs::SponsorshipState; use crate::{ AllowlistEnabled, Config, Owner, Pallet, SponsorBasket, SponsoringRateLimit, SponsoringModeT, + Sponsoring, }; use frame_support::traits::Get; use up_sponsorship::SponsorshipHandler; use sp_std::vec::Vec; -struct ContractHelpers(SubstrateRecorder); +/// See [`ContractHelpersCall`] +pub struct ContractHelpers(SubstrateRecorder); impl WithRecorder for ContractHelpers { fn recorder(&self) -> &SubstrateRecorder { &self.0 @@ -40,96 +45,238 @@ } } -#[solidity_interface(name = "ContractHelpers")] -impl ContractHelpers { +/// @title Magic contract, which allows users to reconfigure other contracts +#[solidity_interface(name = ContractHelpers)] +impl ContractHelpers +where + T::AccountId: AsRef<[u8; 32]>, +{ + /// Get user, which deployed specified contract + /// @dev May return zero address in case if contract is deployed + /// using uniquenetwork evm-migration pallet, or using other terms not + /// intended by pallet-evm + /// @dev Returns zero address if contract does not exists + /// @param contractAddress Contract to get owner of + /// @return address Owner of contract fn contract_owner(&self, contract_address: address) -> Result
{ Ok(>::get(contract_address)) } - fn sponsoring_enabled(&self, contract_address: address) -> Result { - Ok(>::sponsoring_mode(contract_address) != SponsoringModeT::Disabled) - } - - /// Deprecated - fn toggle_sponsoring( + /// Set sponsor. + /// @param contractAddress Contract for which a sponsor is being established. + /// @param sponsor User address who set as pending sponsor. + fn set_sponsor( &mut self, caller: caller, contract_address: address, - enabled: bool, + sponsor: address, ) -> Result { - >::ensure_owner(contract_address, caller)?; - >::toggle_sponsoring(contract_address, enabled); + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + Pallet::::set_sponsor( + &T::CrossAccountId::from_eth(caller), + contract_address, + &T::CrossAccountId::from_eth(sponsor), + ) + .map_err(dispatch_to_evm::)?; + + Ok(()) + } + + /// Set contract as self sponsored. + /// + /// @param contractAddress Contract for which a self sponsoring is being enabled. + fn self_sponsored_enable(&mut self, caller: caller, contract_address: address) -> Result { + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + Pallet::::self_sponsored_enable(&T::CrossAccountId::from_eth(caller), contract_address) + .map_err(dispatch_to_evm::)?; + Ok(()) } + /// Remove sponsor. + /// + /// @param contractAddress Contract for which a sponsorship is being removed. + fn remove_sponsor(&mut self, caller: caller, contract_address: address) -> Result { + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + Pallet::::remove_sponsor(&T::CrossAccountId::from_eth(caller), contract_address) + .map_err(dispatch_to_evm::)?; + + Ok(()) + } + + /// Confirm sponsorship. + /// + /// @dev Caller must be same that set via [`setSponsor`]. + /// + /// @param contractAddress Сontract for which need to confirm sponsorship. + fn confirm_sponsorship(&mut self, caller: caller, contract_address: address) -> Result { + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + Pallet::::confirm_sponsorship(&T::CrossAccountId::from_eth(caller), contract_address) + .map_err(dispatch_to_evm::)?; + + Ok(()) + } + + /// Get current sponsor. + /// + /// @param contractAddress The contract for which a sponsor is requested. + /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + fn get_sponsor(&self, contract_address: address) -> Result<(address, uint256)> { + let sponsor = + Pallet::::get_sponsor(contract_address).ok_or("Contract has no sponsor")?; + let result: (address, uint256) = if sponsor.is_canonical_substrate() { + let sponsor = pallet_common::eth::convert_cross_account_to_uint256::(&sponsor); + (Default::default(), sponsor) + } else { + let sponsor = *sponsor.as_eth(); + (sponsor, Default::default()) + }; + Ok(result) + } + + /// Check tat contract has confirmed sponsor. + /// + /// @param contractAddress The contract for which the presence of a confirmed sponsor is checked. + /// @return **true** if contract has confirmed sponsor. + fn has_sponsor(&self, contract_address: address) -> Result { + Ok(Pallet::::get_sponsor(contract_address).is_some()) + } + + /// Check tat contract has pending sponsor. + /// + /// @param contractAddress The contract for which the presence of a pending sponsor is checked. + /// @return **true** if contract has pending sponsor. + fn has_pending_sponsor(&self, contract_address: address) -> Result { + Ok(match Sponsoring::::get(contract_address) { + SponsorshipState::Disabled | SponsorshipState::Confirmed(_) => false, + SponsorshipState::Unconfirmed(_) => true, + }) + } + + fn sponsoring_enabled(&self, contract_address: address) -> Result { + Ok(>::sponsoring_mode(contract_address) != SponsoringModeT::Disabled) + } + fn set_sponsoring_mode( &mut self, caller: caller, contract_address: address, + // TODO: implement support for enums in evm-coder mode: uint8, ) -> Result { - >::ensure_owner(contract_address, caller)?; + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + >::ensure_owner(contract_address, caller).map_err(dispatch_to_evm::)?; let mode = SponsoringModeT::from_eth(mode).ok_or("unknown mode")?; >::set_sponsoring_mode(contract_address, mode); + Ok(()) } - fn sponsoring_mode(&self, contract_address: address) -> Result { - Ok(>::sponsoring_mode(contract_address).to_eth()) + /// Get current contract sponsoring rate limit + /// @param contractAddress Contract to get sponsoring mode of + /// @return uint32 Amount of blocks between two sponsored transactions + fn get_sponsoring_rate_limit(&self, contract_address: address) -> Result { + Ok(>::get(contract_address) + .try_into() + .map_err(|_| "rate limit > u32::MAX")?) } + /// Set contract sponsoring rate limit + /// @dev Sponsoring rate limit - is a minimum amount of blocks that should + /// pass between two sponsored transactions + /// @param contractAddress Contract to change sponsoring rate limit of + /// @param rateLimit Target rate limit + /// @dev Only contract owner can change this setting fn set_sponsoring_rate_limit( &mut self, caller: caller, contract_address: address, rate_limit: uint32, ) -> Result { - >::ensure_owner(contract_address, caller)?; + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + >::ensure_owner(contract_address, caller).map_err(dispatch_to_evm::)?; >::set_sponsoring_rate_limit(contract_address, rate_limit.into()); Ok(()) } - fn get_sponsoring_rate_limit(&self, contract_address: address) -> Result { - Ok(>::get(contract_address) - .try_into() - .map_err(|_| "rate limit > u32::MAX")?) - } - + /// Is specified user present in contract allow list + /// @dev Contract owner always implicitly included + /// @param contractAddress Contract to check allowlist of + /// @param user User to check + /// @return bool Is specified users exists in contract allowlist fn allowed(&self, contract_address: address, user: address) -> Result { self.0.consume_sload()?; Ok(>::allowed(contract_address, user)) } - fn allowlist_enabled(&self, contract_address: address) -> Result { - Ok(>::get(contract_address)) - } - - fn toggle_allowlist( + /// Toggle user presence in contract allowlist + /// @param contractAddress Contract to change allowlist of + /// @param user Which user presence should be toggled + /// @param isAllowed `true` if user should be allowed to be sponsored + /// or call this contract, `false` otherwise + /// @dev Only contract owner can change this setting + fn toggle_allowed( &mut self, caller: caller, contract_address: address, - enabled: bool, + user: address, + is_allowed: bool, ) -> Result { - >::ensure_owner(contract_address, caller)?; - >::toggle_allowlist(contract_address, enabled); + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + >::ensure_owner(contract_address, caller).map_err(dispatch_to_evm::)?; + >::toggle_allowed(contract_address, user, is_allowed); + Ok(()) } - fn toggle_allowed( + /// Is this contract has allowlist access enabled + /// @dev Allowlist always can have users, and it is used for two purposes: + /// in case of allowlist sponsoring mode, users will be sponsored if they exist in allowlist + /// in case of allowlist access enabled, only users from allowlist may call this contract + /// @param contractAddress Contract to get allowlist access of + /// @return bool Is specified contract has allowlist access enabled + fn allowlist_enabled(&self, contract_address: address) -> Result { + Ok(>::get(contract_address)) + } + + /// Toggle contract allowlist access + /// @param contractAddress Contract to change allowlist access of + /// @param enabled Should allowlist access to be enabled? + fn toggle_allowlist( &mut self, caller: caller, contract_address: address, - user: address, - allowed: bool, + enabled: bool, ) -> Result { - >::ensure_owner(contract_address, caller)?; - >::toggle_allowed(contract_address, user, allowed); + self.recorder().consume_sload()?; + self.recorder().consume_sstore()?; + + >::ensure_owner(contract_address, caller).map_err(dispatch_to_evm::)?; + >::toggle_allowlist(contract_address, enabled); Ok(()) } } +/// Implements [`OnMethodCall`], which delegates call to [`ContractHelpers`] pub struct HelpersOnMethodCall(PhantomData<*const T>); -impl OnMethodCall for HelpersOnMethodCall { +impl OnMethodCall for HelpersOnMethodCall +where + T::AccountId: AsRef<[u8; 32]>, +{ fn is_reserved(contract: &sp_core::H160) -> bool { contract == &T::ContractAddress::get() } @@ -167,6 +314,7 @@ } } +/// Hooks into contract creation, storing owner of newly deployed contract pub struct HelpersOnCreate(PhantomData<*const T>); impl OnCreate for HelpersOnCreate { fn on_create(owner: H160, contract: H160) { @@ -174,23 +322,32 @@ } } +/// Bridge to pallet-sponsoring pub struct HelpersContractSponsoring(PhantomData<*const T>); impl SponsorshipHandler)> for HelpersContractSponsoring { fn get_sponsor(who: &T::CrossAccountId, call: &(H160, Vec)) -> Option { - let mode = >::sponsoring_mode(call.0); + let (contract_address, _) = call; + let mode = >::sponsoring_mode(*contract_address); if mode == SponsoringModeT::Disabled { return None; } - if mode == SponsoringModeT::Allowlisted && !>::allowed(call.0, *who.as_eth()) { + let sponsor = match >::get_sponsor(*contract_address) { + Some(sponsor) => sponsor, + None => return None, + }; + + if mode == SponsoringModeT::Allowlisted + && !>::allowed(*contract_address, *who.as_eth()) + { return None; } let block_number = >::block_number() as T::BlockNumber; - if let Some(last_tx_block) = >::get(&call.0, who.as_eth()) { - let limit = >::get(&call.0); + if let Some(last_tx_block) = >::get(contract_address, who.as_eth()) { + let limit = >::get(contract_address); let timeout = last_tx_block + limit; if block_number < timeout { @@ -198,9 +355,8 @@ } } - >::insert(&call.0, who.as_eth(), block_number); + >::insert(contract_address, who.as_eth(), block_number); - let sponsor = T::CrossAccountId::from_eth(call.0); Some(sponsor) } } --- a/pallets/evm-contract-helpers/src/lib.rs +++ b/pallets/evm-contract-helpers/src/lib.rs @@ -14,7 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +#![doc = include_str!("../README.md")] #![cfg_attr(not(feature = "std"), no_std)] +#![deny(missing_docs)] use codec::{Decode, Encode, MaxEncodedLen}; pub use pallet::*; @@ -25,28 +27,40 @@ #[frame_support::pallet] pub mod pallet { pub use super::*; - use evm_coder::execution::Result; use frame_support::pallet_prelude::*; + use pallet_evm_coder_substrate::DispatchResult; use sp_core::H160; + use pallet_evm::account::CrossAccountId; + use up_data_structs::SponsorshipState; #[pallet::config] pub trait Config: frame_system::Config + pallet_evm_coder_substrate::Config + pallet_evm::account::Config { + /// Address, under which magic contract will be available type ContractAddress: Get; + /// In case of enabled sponsoring, but no sponsoring rate limit set, + /// this value will be used implicitly type DefaultSponsoringRateLimit: Get; } #[pallet::error] pub enum Error { - /// This method is only executable by owner + /// This method is only executable by contract owner NoPermission, + + /// No pending sponsor for contract. + NoPendingSponsor, } #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(_); + /// Store owner for contract. + /// + /// * **Key** - contract address. + /// * **Value** - owner for contract. #[pallet::storage] pub(super) type Owner = StorageMap; @@ -56,10 +70,33 @@ pub(super) type SelfSponsoring = StorageMap; + /// Store for contract sponsorship state. + /// + /// * **Key** - contract address. + /// * **Value** - sponsorship state. + #[pallet::storage] + pub(super) type Sponsoring = StorageMap< + Hasher = Twox64Concat, + Key = H160, + Value = SponsorshipState, + QueryKind = ValueQuery, + >; + + /// Store for sponsoring mode. + /// + /// ### Usage + /// Prefer to delete collection from storage if mode chaged to [`Disabled`](SponsoringModeT::Disabled). + /// + /// * **Key** - contract address. + /// * **Value** - [`sponsoring mode`](SponsoringModeT). #[pallet::storage] pub(super) type SponsoringMode = StorageMap; + /// Storage for sponsoring rate limit in blocks. + /// + /// * **Key** - contract address. + /// * **Value** - amount of sponsored blocks. #[pallet::storage] pub(super) type SponsoringRateLimit = StorageMap< Hasher = Twox128, @@ -69,6 +106,11 @@ OnEmpty = T::DefaultSponsoringRateLimit, >; + /// Storage for last sponsored block. + /// + /// * **Key1** - contract address. + /// * **Key2** - sponsored user address. + /// * **Value** - last sponsored block number. #[pallet::storage] pub(super) type SponsorBasket = StorageDoubleMap< Hasher1 = Twox128, @@ -79,10 +121,25 @@ QueryKind = OptionQuery, >; + /// Storege for contracts with [`Allowlisted`](SponsoringModeT::Allowlisted) sponsoring mode. + /// + /// ### Usage + /// Prefer to delete collection from storage if mode chaged to non `Allowlisted`, than set **Value** to **false**. + /// + /// * **Key** - contract address. + /// * **Value** - is contract in [`Allowlisted`](SponsoringModeT::Allowlisted) mode. #[pallet::storage] pub(super) type AllowlistEnabled = StorageMap; + /// Storage for users that allowed for sponsorship. + /// + /// ### Usage + /// Prefer to delete record from storage if user no more allowed for sponsorship. + /// + /// * **Key1** - contract address. + /// * **Key2** - user that allowed for sponsorship. + /// * **Value** - allowance for sponsorship. #[pallet::storage] pub(super) type Allowlist = StorageDoubleMap< Hasher1 = Twox128, @@ -94,6 +151,78 @@ >; impl Pallet { + /// Get contract owner. + pub fn contract_owner(contract: H160) -> H160 { + >::get(contract) + } + + /// Set `sponsor` for `contract`. + /// + /// `sender` must be owner of contract. + pub fn set_sponsor( + sender: &T::CrossAccountId, + contract: H160, + sponsor: &T::CrossAccountId, + ) -> DispatchResult { + Pallet::::ensure_owner(contract, *sender.as_eth())?; + Sponsoring::::insert( + contract, + SponsorshipState::::Unconfirmed(sponsor.clone()), + ); + Ok(()) + } + + /// Set `contract` as self sponsored. + /// + /// `sender` must be owner of contract. + pub fn self_sponsored_enable(sender: &T::CrossAccountId, contract: H160) -> DispatchResult { + Pallet::::ensure_owner(contract, *sender.as_eth())?; + Sponsoring::::insert( + contract, + SponsorshipState::::Confirmed(T::CrossAccountId::from_eth( + contract, + )), + ); + Ok(()) + } + + /// Remove sponsor for `contract`. + /// + /// `sender` must be owner of contract. + pub fn remove_sponsor(sender: &T::CrossAccountId, contract: H160) -> DispatchResult { + Pallet::::ensure_owner(contract, *sender.as_eth())?; + Sponsoring::::remove(contract); + Ok(()) + } + + /// Confirm sponsorship. + /// + /// `sender` must be same that set via [`set_sponsor`]. + pub fn confirm_sponsorship(sender: &T::CrossAccountId, contract: H160) -> DispatchResult { + match Sponsoring::::get(contract) { + SponsorshipState::Unconfirmed(sponsor) => { + ensure!(sponsor == *sender, Error::::NoPermission); + Sponsoring::::insert( + contract, + SponsorshipState::::Confirmed(sponsor), + ); + Ok(()) + } + SponsorshipState::Disabled | SponsorshipState::Confirmed(_) => { + Err(Error::::NoPendingSponsor.into()) + } + } + } + + /// Get sponsor. + pub fn get_sponsor(contract: H160) -> Option { + match Sponsoring::::get(contract) { + SponsorshipState::Disabled | SponsorshipState::Unconfirmed(_) => None, + SponsorshipState::Confirmed(sponsor) => Some(sponsor), + } + } + + /// Get current sponsoring mode, performing lazy migration from legacy storage pub fn sponsoring_mode(contract: H160) -> SponsoringModeT { >::get(contract) .or_else(|| { @@ -101,6 +230,8 @@ }) .unwrap_or_default() } + + /// Reconfigure contract sponsoring mode pub fn set_sponsoring_mode(contract: H160, mode: SponsoringModeT) { if mode == SponsoringModeT::Disabled { >::remove(contract); @@ -110,44 +241,43 @@ >::remove(contract) } - pub fn toggle_sponsoring(contract: H160, enabled: bool) { - Self::set_sponsoring_mode( - contract, - if enabled { - SponsoringModeT::Allowlisted - } else { - SponsoringModeT::Disabled - }, - ) - } - + /// Set duration between two sponsored contract calls pub fn set_sponsoring_rate_limit(contract: H160, rate_limit: T::BlockNumber) { >::insert(contract, rate_limit); } + /// Is user added to allowlist, or he is owner of specified contract pub fn allowed(contract: H160, user: H160) -> bool { >::get(&contract, &user) || >::get(&contract) == user } + /// Toggle contract allowlist access pub fn toggle_allowlist(contract: H160, enabled: bool) { >::insert(contract, enabled) } + /// Toggle user presence in contract's allowlist pub fn toggle_allowed(contract: H160, user: H160, allowed: bool) { >::insert(contract, user, allowed); } - pub fn ensure_owner(contract: H160, user: H160) -> Result<()> { - ensure!(>::get(&contract) == user, "no permission"); + /// Throw error if user is not allowed to reconfigure target contract + pub fn ensure_owner(contract: H160, user: H160) -> DispatchResult { + ensure!(>::get(&contract) == user, Error::::NoPermission); Ok(()) } } } -#[derive(Encode, Decode, PartialEq, TypeInfo, MaxEncodedLen)] +/// Available contract sponsoring modes +#[derive(Encode, Decode, PartialEq, TypeInfo, MaxEncodedLen, Default)] pub enum SponsoringModeT { + /// Sponsoring is disabled + #[default] Disabled, + /// Only users from allowlist will be sponsored Allowlisted, + /// All users will be sponsored Generous, } @@ -166,11 +296,5 @@ SponsoringModeT::Allowlisted => 1, SponsoringModeT::Generous => 2, } - } -} - -impl Default for SponsoringModeT { - fn default() -> Self { - Self::Disabled } } --- a/pallets/evm-contract-helpers/src/stubs/ContractHelpers.sol +++ b/pallets/evm-contract-helpers/src/stubs/ContractHelpers.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; -// Common stubs holder +/// @dev common stubs holder contract Dummy { uint8 dummy; string stub_error = "this contract is implemented in native"; @@ -21,9 +21,18 @@ } } -// Selector: 7b4866f9 +/// @title Magic contract, which allows users to reconfigure other contracts +/// @dev the ERC-165 identifier for this interface is 0xd77fab70 contract ContractHelpers is Dummy, ERC165 { - // Selector: contractOwner(address) 5152b14c + /// Get user, which deployed specified contract + /// @dev May return zero address in case if contract is deployed + /// using uniquenetwork evm-migration pallet, or using other terms not + /// intended by pallet-evm + /// @dev Returns zero address if contract does not exists + /// @param contractAddress Contract to get owner of + /// @return address Owner of contract + /// @dev EVM selector for this function is: 0x5152b14c, + /// or in textual repr: contractOwner(address) function contractOwner(address contractAddress) public view @@ -35,8 +44,90 @@ return 0x0000000000000000000000000000000000000000; } - // Selector: sponsoringEnabled(address) 6027dc61 - function sponsoringEnabled(address contractAddress) + /// Set sponsor. + /// @param contractAddress Contract for which a sponsor is being established. + /// @param sponsor User address who set as pending sponsor. + /// @dev EVM selector for this function is: 0xf01fba93, + /// or in textual repr: setSponsor(address,address) + function setSponsor(address contractAddress, address sponsor) public { + require(false, stub_error); + contractAddress; + sponsor; + dummy = 0; + } + + /// Set contract as self sponsored. + /// + /// @param contractAddress Contract for which a self sponsoring is being enabled. + /// @dev EVM selector for this function is: 0x89f7d9ae, + /// or in textual repr: selfSponsoredEnable(address) + function selfSponsoredEnable(address contractAddress) public { + require(false, stub_error); + contractAddress; + dummy = 0; + } + + /// Remove sponsor. + /// + /// @param contractAddress Contract for which a sponsorship is being removed. + /// @dev EVM selector for this function is: 0xef784250, + /// or in textual repr: removeSponsor(address) + function removeSponsor(address contractAddress) public { + require(false, stub_error); + contractAddress; + dummy = 0; + } + + /// Confirm sponsorship. + /// + /// @dev Caller must be same that set via [`setSponsor`]. + /// + /// @param contractAddress Сontract for which need to confirm sponsorship. + /// @dev EVM selector for this function is: 0xabc00001, + /// or in textual repr: confirmSponsorship(address) + function confirmSponsorship(address contractAddress) public { + require(false, stub_error); + contractAddress; + dummy = 0; + } + + /// Get current sponsor. + /// + /// @param contractAddress The contract for which a sponsor is requested. + /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + /// @dev EVM selector for this function is: 0x743fc745, + /// or in textual repr: getSponsor(address) + function getSponsor(address contractAddress) + public + view + returns (Tuple0 memory) + { + require(false, stub_error); + contractAddress; + dummy; + return Tuple0(0x0000000000000000000000000000000000000000, 0); + } + + /// Check tat contract has confirmed sponsor. + /// + /// @param contractAddress The contract for which the presence of a confirmed sponsor is checked. + /// @return **true** if contract has confirmed sponsor. + /// @dev EVM selector for this function is: 0x97418603, + /// or in textual repr: hasSponsor(address) + function hasSponsor(address contractAddress) public view returns (bool) { + require(false, stub_error); + contractAddress; + dummy; + return false; + } + + /// Check tat contract has pending sponsor. + /// + /// @param contractAddress The contract for which the presence of a pending sponsor is checked. + /// @return **true** if contract has pending sponsor. + /// @dev EVM selector for this function is: 0x39b9b242, + /// or in textual repr: hasPendingSponsor(address) + function hasPendingSponsor(address contractAddress) public view returns (bool) @@ -47,17 +138,21 @@ return false; } - // Deprecated - // - // Selector: toggleSponsoring(address,bool) fcac6d86 - function toggleSponsoring(address contractAddress, bool enabled) public { + /// @dev EVM selector for this function is: 0x6027dc61, + /// or in textual repr: sponsoringEnabled(address) + function sponsoringEnabled(address contractAddress) + public + view + returns (bool) + { require(false, stub_error); contractAddress; - enabled; - dummy = 0; + dummy; + return false; } - // Selector: setSponsoringMode(address,uint8) fde8a560 + /// @dev EVM selector for this function is: 0xfde8a560, + /// or in textual repr: setSponsoringMode(address,uint8) function setSponsoringMode(address contractAddress, uint8 mode) public { require(false, stub_error); contractAddress; @@ -65,11 +160,15 @@ dummy = 0; } - // Selector: sponsoringMode(address) b70c7267 - function sponsoringMode(address contractAddress) + /// Get current contract sponsoring rate limit + /// @param contractAddress Contract to get sponsoring mode of + /// @return uint32 Amount of blocks between two sponsored transactions + /// @dev EVM selector for this function is: 0x610cfabd, + /// or in textual repr: getSponsoringRateLimit(address) + function getSponsoringRateLimit(address contractAddress) public view - returns (uint8) + returns (uint32) { require(false, stub_error); contractAddress; @@ -77,7 +176,14 @@ return 0; } - // Selector: setSponsoringRateLimit(address,uint32) 77b6c908 + /// Set contract sponsoring rate limit + /// @dev Sponsoring rate limit - is a minimum amount of blocks that should + /// pass between two sponsored transactions + /// @param contractAddress Contract to change sponsoring rate limit of + /// @param rateLimit Target rate limit + /// @dev Only contract owner can change this setting + /// @dev EVM selector for this function is: 0x77b6c908, + /// or in textual repr: setSponsoringRateLimit(address,uint32) function setSponsoringRateLimit(address contractAddress, uint32 rateLimit) public { @@ -87,32 +193,53 @@ dummy = 0; } - // Selector: getSponsoringRateLimit(address) 610cfabd - function getSponsoringRateLimit(address contractAddress) + /// Is specified user present in contract allow list + /// @dev Contract owner always implicitly included + /// @param contractAddress Contract to check allowlist of + /// @param user User to check + /// @return bool Is specified users exists in contract allowlist + /// @dev EVM selector for this function is: 0x5c658165, + /// or in textual repr: allowed(address,address) + function allowed(address contractAddress, address user) public view - returns (uint32) + returns (bool) { require(false, stub_error); contractAddress; + user; dummy; - return 0; + return false; } - // Selector: allowed(address,address) 5c658165 - function allowed(address contractAddress, address user) - public - view - returns (bool) - { + /// Toggle user presence in contract allowlist + /// @param contractAddress Contract to change allowlist of + /// @param user Which user presence should be toggled + /// @param isAllowed `true` if user should be allowed to be sponsored + /// or call this contract, `false` otherwise + /// @dev Only contract owner can change this setting + /// @dev EVM selector for this function is: 0x4706cc1c, + /// or in textual repr: toggleAllowed(address,address,bool) + function toggleAllowed( + address contractAddress, + address user, + bool isAllowed + ) public { require(false, stub_error); contractAddress; user; - dummy; - return false; + isAllowed; + dummy = 0; } - // Selector: allowlistEnabled(address) c772ef6c + /// Is this contract has allowlist access enabled + /// @dev Allowlist always can have users, and it is used for two purposes: + /// in case of allowlist sponsoring mode, users will be sponsored if they exist in allowlist + /// in case of allowlist access enabled, only users from allowlist may call this contract + /// @param contractAddress Contract to get allowlist access of + /// @return bool Is specified contract has allowlist access enabled + /// @dev EVM selector for this function is: 0xc772ef6c, + /// or in textual repr: allowlistEnabled(address) function allowlistEnabled(address contractAddress) public view @@ -124,24 +251,21 @@ return false; } - // Selector: toggleAllowlist(address,bool) 36de20f5 + /// Toggle contract allowlist access + /// @param contractAddress Contract to change allowlist access of + /// @param enabled Should allowlist access to be enabled? + /// @dev EVM selector for this function is: 0x36de20f5, + /// or in textual repr: toggleAllowlist(address,bool) function toggleAllowlist(address contractAddress, bool enabled) public { require(false, stub_error); contractAddress; enabled; dummy = 0; } +} - // Selector: toggleAllowed(address,address,bool) 4706cc1c - function toggleAllowed( - address contractAddress, - address user, - bool allowed - ) public { - require(false, stub_error); - contractAddress; - user; - allowed; - dummy = 0; - } +/// @dev anonymous struct +struct Tuple0 { + address field_0; + uint256 field_1; } --- /dev/null +++ b/pallets/evm-migration/README.md @@ -0,0 +1,18 @@ +# EVM contract migration pallet + +This pallet is only callable by root, it has functionality to migrate contract +from other ethereum chain to pallet-evm + +Contract data includes contract code, and contract storage, +where contract storage is a mapping from evm word to evm word (evm word = 32 byte) + +To import contract data into pallet-evm admin should call this pallet multiple times: +1. Start migration via `begin` +2. Insert all contract data using single or + multiple (If data can't be fit into single extrinsic) calls + to `set_data` +3. Finish migration using `finish`, providing contract code + +During migration no one can insert code at address of this contract, +as [`pallet::OnMethodCall`] prevents this, and no one can call this contract, +as code is only supplied at final stage of contract deployment \ No newline at end of file --- a/pallets/evm-migration/src/benchmarking.rs +++ b/pallets/evm-migration/src/benchmarking.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +#![allow(missing_docs)] + use super::{Call, Config, Pallet}; use frame_benchmarking::benchmarks; use frame_system::RawOrigin; --- a/pallets/evm-migration/src/lib.rs +++ b/pallets/evm-migration/src/lib.rs @@ -14,7 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +#![doc = include_str!("../README.md")] #![cfg_attr(not(feature = "std"), no_std)] +#![deny(missing_docs)] pub use pallet::*; #[cfg(feature = "runtime-benchmarks")] @@ -32,6 +34,7 @@ #[pallet::config] pub trait Config: frame_system::Config + pallet_evm::Config { + /// Weights type WeightInfo: WeightInfo; } @@ -43,7 +46,9 @@ #[pallet::error] pub enum Error { + /// Can only migrate to empty address. AccountNotEmpty, + /// Migration of this account is not yet started, or already finished. AccountIsNotMigrating, } @@ -53,6 +58,8 @@ #[pallet::call] impl Pallet { + /// Start contract migration, inserts contract stub at target address, + /// and marks account as pending, allowing to insert storage #[pallet::weight(>::begin())] pub fn begin(origin: OriginFor, address: H160) -> DispatchResult { ensure_root(origin)?; @@ -65,6 +72,8 @@ Ok(()) } + /// Insert items into contract storage, this method can be called + /// multiple times #[pallet::weight(>::set_data(data.len() as u32))] pub fn set_data( origin: OriginFor, @@ -83,6 +92,9 @@ Ok(()) } + /// Finish contract migration, allows it to be called. + /// It is not possible to alter contract storage via [`Self::set_data`] + /// after this call. #[pallet::weight(>::finish(code.len() as u32))] pub fn finish(origin: OriginFor, address: H160, code: Vec) -> DispatchResult { ensure_root(origin)?; @@ -97,6 +109,7 @@ } } + /// Implements [`pallet_evm::OnMethodCall`], which reserves accounts with pending migration pub struct OnMethodCall(PhantomData); impl pallet_evm::OnMethodCall for OnMethodCall { fn is_reserved(contract: &H160) -> bool { --- a/pallets/evm-migration/src/weights.rs +++ b/pallets/evm-migration/src/weights.rs @@ -26,6 +26,7 @@ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] #![allow(unused_imports)] +#![allow(missing_docs)] #![allow(clippy::unnecessary_cast)] use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; --- /dev/null +++ b/pallets/evm-transaction-payment/README.md @@ -0,0 +1,5 @@ +# Evm transaction payment pallet + +pallet-evm-transaction-payment is a bridge between pallet-evm substrate calls and pallet-sponsoring. +It doesn't provide any sponsoring logic by itself, instead all sponsoring handlers +are loosly coupled via [`Config::EvmSponsorshipHandler`] trait. \ No newline at end of file --- a/pallets/evm-transaction-payment/src/lib.rs +++ b/pallets/evm-transaction-payment/src/lib.rs @@ -14,7 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . +#![doc = include_str!("../README.md")] #![cfg_attr(not(feature = "std"), no_std)] +#![deny(missing_docs)] use core::marker::PhantomData; use fp_evm::WithdrawReason; @@ -29,13 +31,12 @@ pub mod pallet { use super::*; - use frame_support::traits::Currency; use sp_std::vec::Vec; #[pallet::config] pub trait Config: frame_system::Config + pallet_evm::account::Config { + /// Loosly-coupled handlers for evm call sponsoring type EvmSponsorshipHandler: SponsorshipHandler)>; - type Currency: Currency; } #[pallet::pallet] @@ -43,6 +44,7 @@ pub struct Pallet(_); } +/// Implements [`fp_evm::TransactionValidityHack`], which provides sponsor address to pallet-evm pub struct TransactionValidityHack(PhantomData<*const T>); impl fp_evm::TransactionValidityHack for TransactionValidityHack { fn who_pays_fee(origin: H160, reason: &WithdrawReason) -> Option { --- a/pallets/fungible/CHANGELOG.md +++ b/pallets/fungible/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. + +## [0.1.5] - 2022-08-29 + +### Added + + - Implementation of `mint` and `mint_bulk` methods for ERC20 API. + +## [v0.1.4] - 2022-08-24 + +### Change + - Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`. + ## [v0.1.3] 2022-08-16 --- a/pallets/fungible/Cargo.toml +++ b/pallets/fungible/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-fungible" -version = "0.1.3" +version = "0.1.5" license = "GPLv3" edition = "2021" --- a/pallets/fungible/src/erc.rs +++ b/pallets/fungible/src/erc.rs @@ -50,7 +50,7 @@ }, } -#[solidity_interface(name = "ERC20", events(ERC20Events))] +#[solidity_interface(name = ERC20, events(ERC20Events))] impl FungibleHandle { fn name(&self) -> Result { Ok(decode_utf16(self.name.iter().copied()) @@ -129,8 +129,32 @@ } } -#[solidity_interface(name = "ERC20UniqueExtensions")] +#[solidity_interface(name = ERC20Mintable)] +impl FungibleHandle { + /// Mint tokens for `to` account. + /// @param to account that will receive minted tokens + /// @param amount amount of tokens to mint + #[weight(>::create_item())] + fn mint(&mut self, caller: caller, to: address, amount: uint256) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let to = T::CrossAccountId::from_eth(to); + let amount = amount.try_into().map_err(|_| "amount overflow")?; + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); + >::create_item(&self, &caller, (to, amount), &budget) + .map_err(dispatch_to_evm::)?; + Ok(true) + } +} + +#[solidity_interface(name = ERC20UniqueExtensions)] impl FungibleHandle { + /// Burn tokens from account + /// @dev Function that burns an `amount` of the tokens of a given account, + /// deducting from the sender's allowance for said account. + /// @param from The account whose tokens will be burnt. + /// @param amount The amount that will be burnt. #[weight(>::burn_from())] fn burn_from(&mut self, caller: caller, from: address, amount: uint256) -> Result { let caller = T::CrossAccountId::from_eth(caller); @@ -144,24 +168,48 @@ .map_err(dispatch_to_evm::)?; Ok(true) } + + /// Mint tokens for multiple accounts. + /// @param amounts array of pairs of account address and amount + #[weight(>::create_multiple_items_ex(amounts.len() as u32))] + fn mint_bulk(&mut self, caller: caller, amounts: Vec<(address, uint256)>) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); + let amounts = amounts + .into_iter() + .map(|(to, amount)| { + Ok(( + T::CrossAccountId::from_eth(to), + amount.try_into().map_err(|_| "amount overflow")?, + )) + }) + .collect::>()?; + + >::create_multiple_items(&self, &caller, amounts, &budget) + .map_err(dispatch_to_evm::)?; + Ok(true) + } } #[solidity_interface( - name = "UniqueFungible", + name = UniqueFungible, is( ERC20, + ERC20Mintable, ERC20UniqueExtensions, - via("CollectionHandle", common_mut, Collection) + Collection(common_mut, CollectionHandle), ) )] -impl FungibleHandle where T::AccountId: From<[u8; 32]> {} +impl FungibleHandle where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {} generate_stubgen!(gen_impl, UniqueFungibleCall<()>, true); generate_stubgen!(gen_iface, UniqueFungibleCall<()>, false); impl CommonEvmHandler for FungibleHandle where - T::AccountId: From<[u8; 32]>, + T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>, { const CODE: &'static [u8] = include_bytes!("./stubs/UniqueFungible.raw"); --- a/pallets/fungible/src/stubs/UniqueFungible.sol +++ b/pallets/fungible/src/stubs/UniqueFungible.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; -// Common stubs holder +/// @dev common stubs holder contract Dummy { uint8 dummy; string stub_error = "this contract is implemented in native"; @@ -21,24 +21,15 @@ } } -// Inline -contract ERC20Events { - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval( - address indexed owner, - address indexed spender, - uint256 value - ); -} - -// Selector: 6cf113cd +/// @title A contract that allows you to work with collections. +/// @dev the ERC-165 identifier for this interface is 0xe54be640 contract Collection is Dummy, ERC165 { - // Set collection property. - // - // @param key Property key. - // @param value Propery value. - // - // Selector: setCollectionProperty(string,bytes) 2f073f66 + /// Set collection property. + /// + /// @param key Property key. + /// @param value Propery value. + /// @dev EVM selector for this function is: 0x2f073f66, + /// or in textual repr: setCollectionProperty(string,bytes) function setCollectionProperty(string memory key, bytes memory value) public { @@ -48,25 +39,25 @@ dummy = 0; } - // Delete collection property. - // - // @param key Property key. - // - // Selector: deleteCollectionProperty(string) 7b7debce + /// Delete collection property. + /// + /// @param key Property key. + /// @dev EVM selector for this function is: 0x7b7debce, + /// or in textual repr: deleteCollectionProperty(string) function deleteCollectionProperty(string memory key) public { require(false, stub_error); key; dummy = 0; } - // Get collection property. - // - // @dev Throws error if key not found. - // - // @param key Property key. - // @return bytes The property corresponding to the key. - // - // Selector: collectionProperty(string) cf24fd6d + /// Get collection property. + /// + /// @dev Throws error if key not found. + /// + /// @param key Property key. + /// @return bytes The property corresponding to the key. + /// @dev EVM selector for this function is: 0xcf24fd6d, + /// or in textual repr: collectionProperty(string) function collectionProperty(string memory key) public view @@ -78,41 +69,81 @@ return hex""; } - // Set the sponsor of the collection. - // - // @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. - // - // @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. - // - // Selector: setCollectionSponsor(address) 7623402e + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0x7623402e, + /// or in textual repr: setCollectionSponsor(address) function setCollectionSponsor(address sponsor) public { require(false, stub_error); sponsor; dummy = 0; } - // Collection sponsorship confirmation. - // - // @dev After setting the sponsor for the collection, it must be confirmed with this function. - // - // Selector: confirmCollectionSponsorship() 3c50e97a + /// Set the substrate sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Substrate address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0xc74d6751, + /// or in textual repr: setCollectionSponsorSubstrate(uint256) + function setCollectionSponsorSubstrate(uint256 sponsor) public { + require(false, stub_error); + sponsor; + dummy = 0; + } + + /// @dev EVM selector for this function is: 0x058ac185, + /// or in textual repr: hasCollectionPendingSponsor() + function hasCollectionPendingSponsor() public view returns (bool) { + require(false, stub_error); + dummy; + return false; + } + + /// Collection sponsorship confirmation. + /// + /// @dev After setting the sponsor for the collection, it must be confirmed with this function. + /// @dev EVM selector for this function is: 0x3c50e97a, + /// or in textual repr: confirmCollectionSponsorship() function confirmCollectionSponsorship() public { require(false, stub_error); dummy = 0; } - // Set limits for the collection. - // @dev Throws error if limit not found. - // @param limit Name of the limit. Valid names: - // "accountTokenOwnershipLimit", - // "sponsoredDataSize", - // "sponsoredDataRateLimit", - // "tokenLimit", - // "sponsorTransferTimeout", - // "sponsorApproveTimeout" - // @param value Value of the limit. - // - // Selector: setCollectionLimit(string,uint32) 6a3841db + /// Remove collection sponsor. + /// @dev EVM selector for this function is: 0x6e0326a3, + /// or in textual repr: removeCollectionSponsor() + function removeCollectionSponsor() public { + require(false, stub_error); + dummy = 0; + } + + /// Get current sponsor. + /// + /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + /// @dev EVM selector for this function is: 0xb66bbc14, + /// or in textual repr: getCollectionSponsor() + function getCollectionSponsor() public view returns (Tuple6 memory) { + require(false, stub_error); + dummy; + return Tuple6(0x0000000000000000000000000000000000000000, 0); + } + + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "accountTokenOwnershipLimit", + /// "sponsoredDataSize", + /// "sponsoredDataRateLimit", + /// "tokenLimit", + /// "sponsorTransferTimeout", + /// "sponsorApproveTimeout" + /// @param value Value of the limit. + /// @dev EVM selector for this function is: 0x6a3841db, + /// or in textual repr: setCollectionLimit(string,uint32) function setCollectionLimit(string memory limit, uint32 value) public { require(false, stub_error); limit; @@ -120,15 +151,15 @@ dummy = 0; } - // Set limits for the collection. - // @dev Throws error if limit not found. - // @param limit Name of the limit. Valid names: - // "ownerCanTransfer", - // "ownerCanDestroy", - // "transfersEnabled" - // @param value Value of the limit. - // - // Selector: setCollectionLimit(string,bool) 993b7fba + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "ownerCanTransfer", + /// "ownerCanDestroy", + /// "transfersEnabled" + /// @param value Value of the limit. + /// @dev EVM selector for this function is: 0x993b7fba, + /// or in textual repr: setCollectionLimit(string,bool) function setCollectionLimit(string memory limit, bool value) public { require(false, stub_error); limit; @@ -136,73 +167,73 @@ dummy = 0; } - // Get contract address. - // - // Selector: contractAddress() f6b4dfb4 + /// Get contract address. + /// @dev EVM selector for this function is: 0xf6b4dfb4, + /// or in textual repr: contractAddress() function contractAddress() public view returns (address) { require(false, stub_error); dummy; return 0x0000000000000000000000000000000000000000; } - // Add collection admin by substrate address. - // @param new_admin Substrate administrator address. - // - // Selector: addCollectionAdminSubstrate(uint256) 5730062b + /// Add collection admin by substrate address. + /// @param newAdmin Substrate administrator address. + /// @dev EVM selector for this function is: 0x5730062b, + /// or in textual repr: addCollectionAdminSubstrate(uint256) function addCollectionAdminSubstrate(uint256 newAdmin) public { require(false, stub_error); newAdmin; dummy = 0; } - // Remove collection admin by substrate address. - // @param admin Substrate administrator address. - // - // Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9 + /// Remove collection admin by substrate address. + /// @param admin Substrate administrator address. + /// @dev EVM selector for this function is: 0x4048fcf9, + /// or in textual repr: removeCollectionAdminSubstrate(uint256) function removeCollectionAdminSubstrate(uint256 admin) public { require(false, stub_error); admin; dummy = 0; } - // Add collection admin. - // @param new_admin Address of the added administrator. - // - // Selector: addCollectionAdmin(address) 92e462c7 + /// Add collection admin. + /// @param newAdmin Address of the added administrator. + /// @dev EVM selector for this function is: 0x92e462c7, + /// or in textual repr: addCollectionAdmin(address) function addCollectionAdmin(address newAdmin) public { require(false, stub_error); newAdmin; dummy = 0; } - // Remove collection admin. - // - // @param new_admin Address of the removed administrator. - // - // Selector: removeCollectionAdmin(address) fafd7b42 + /// Remove collection admin. + /// + /// @param admin Address of the removed administrator. + /// @dev EVM selector for this function is: 0xfafd7b42, + /// or in textual repr: removeCollectionAdmin(address) function removeCollectionAdmin(address admin) public { require(false, stub_error); admin; dummy = 0; } - // Toggle accessibility of collection nesting. - // - // @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' - // - // Selector: setCollectionNesting(bool) 112d4586 + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' + /// @dev EVM selector for this function is: 0x112d4586, + /// or in textual repr: setCollectionNesting(bool) function setCollectionNesting(bool enable) public { require(false, stub_error); enable; dummy = 0; } - // Toggle accessibility of collection nesting. - // - // @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' - // @param collections Addresses of collections that will be available for nesting. - // - // Selector: setCollectionNesting(bool,address[]) 64872396 + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + /// @param collections Addresses of collections that will be available for nesting. + /// @dev EVM selector for this function is: 0x64872396, + /// or in textual repr: setCollectionNesting(bool,address[]) function setCollectionNesting(bool enable, address[] memory collections) public { @@ -212,79 +243,122 @@ dummy = 0; } - // Set the collection access method. - // @param mode Access mode - // 0 for Normal - // 1 for AllowList - // - // Selector: setCollectionAccess(uint8) 41835d4c + /// Set the collection access method. + /// @param mode Access mode + /// 0 for Normal + /// 1 for AllowList + /// @dev EVM selector for this function is: 0x41835d4c, + /// or in textual repr: setCollectionAccess(uint8) function setCollectionAccess(uint8 mode) public { require(false, stub_error); mode; dummy = 0; } - // Add the user to the allowed list. - // - // @param user Address of a trusted user. - // - // Selector: addToCollectionAllowList(address) 67844fe6 + /// Add the user to the allowed list. + /// + /// @param user Address of a trusted user. + /// @dev EVM selector for this function is: 0x67844fe6, + /// or in textual repr: addToCollectionAllowList(address) function addToCollectionAllowList(address user) public { require(false, stub_error); user; dummy = 0; } - // Remove the user from the allowed list. - // - // @param user Address of a removed user. - // - // Selector: removeFromCollectionAllowList(address) 85c51acb + /// Remove the user from the allowed list. + /// + /// @param user Address of a removed user. + /// @dev EVM selector for this function is: 0x85c51acb, + /// or in textual repr: removeFromCollectionAllowList(address) function removeFromCollectionAllowList(address user) public { require(false, stub_error); user; dummy = 0; } - // Switch permission for minting. - // - // @param mode Enable if "true". - // - // Selector: setCollectionMintMode(bool) 00018e84 + /// Switch permission for minting. + /// + /// @param mode Enable if "true". + /// @dev EVM selector for this function is: 0x00018e84, + /// or in textual repr: setCollectionMintMode(bool) function setCollectionMintMode(bool mode) public { require(false, stub_error); mode; dummy = 0; } - // Check that account is the owner or admin of the collection - // - // @param user account to verify - // @return "true" if account is the owner or admin - // - // Selector: verifyOwnerOrAdmin(address) c2282493 - function verifyOwnerOrAdmin(address user) public view returns (bool) { + /// Check that account is the owner or admin of the collection + /// + /// @param user account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x9811b0c7, + /// or in textual repr: isOwnerOrAdmin(address) + function isOwnerOrAdmin(address user) public view returns (bool) { require(false, stub_error); user; dummy; return false; } - // Returns collection type - // - // @return `Fungible` or `NFT` or `ReFungible` - // - // Selector: uniqueCollectionType() d34b55b8 + /// Check that substrate account is the owner or admin of the collection + /// + /// @param user account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x68910e00, + /// or in textual repr: isOwnerOrAdminSubstrate(uint256) + function isOwnerOrAdminSubstrate(uint256 user) public view returns (bool) { + require(false, stub_error); + user; + dummy; + return false; + } + + /// Returns collection type + /// + /// @return `Fungible` or `NFT` or `ReFungible` + /// @dev EVM selector for this function is: 0xd34b55b8, + /// or in textual repr: uniqueCollectionType() function uniqueCollectionType() public returns (string memory) { require(false, stub_error); dummy = 0; return ""; } + + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner account + /// @dev EVM selector for this function is: 0x13af4035, + /// or in textual repr: setOwner(address) + function setOwner(address newOwner) public { + require(false, stub_error); + newOwner; + dummy = 0; + } + + /// Changes collection owner to another substrate account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner substrate account + /// @dev EVM selector for this function is: 0xb212138f, + /// or in textual repr: setOwnerSubstrate(uint256) + function setOwnerSubstrate(uint256 newOwner) public { + require(false, stub_error); + newOwner; + dummy = 0; + } } -// Selector: 79cc6790 +/// @dev the ERC-165 identifier for this interface is 0x63034ac5 contract ERC20UniqueExtensions is Dummy, ERC165 { - // Selector: burnFrom(address,uint256) 79cc6790 + /// Burn tokens from account + /// @dev Function that burns an `amount` of the tokens of a given account, + /// deducting from the sender's allowance for said account. + /// @param from The account whose tokens will be burnt. + /// @param amount The amount that will be burnt. + /// @dev EVM selector for this function is: 0x79cc6790, + /// or in textual repr: burnFrom(address,uint256) function burnFrom(address from, uint256 amount) public returns (bool) { require(false, stub_error); from; @@ -292,39 +366,87 @@ dummy = 0; return false; } + + /// Mint tokens for multiple accounts. + /// @param amounts array of pairs of account address and amount + /// @dev EVM selector for this function is: 0x1acf2d55, + /// or in textual repr: mintBulk((address,uint256)[]) + function mintBulk(Tuple6[] memory amounts) public returns (bool) { + require(false, stub_error); + amounts; + dummy = 0; + return false; + } +} + +/// @dev anonymous struct +struct Tuple6 { + address field_0; + uint256 field_1; } -// Selector: 942e8b22 +/// @dev the ERC-165 identifier for this interface is 0x40c10f19 +contract ERC20Mintable is Dummy, ERC165 { + /// Mint tokens for `to` account. + /// @param to account that will receive minted tokens + /// @param amount amount of tokens to mint + /// @dev EVM selector for this function is: 0x40c10f19, + /// or in textual repr: mint(address,uint256) + function mint(address to, uint256 amount) public returns (bool) { + require(false, stub_error); + to; + amount; + dummy = 0; + return false; + } +} + +/// @dev inlined interface +contract ERC20Events { + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} + +/// @dev the ERC-165 identifier for this interface is 0x942e8b22 contract ERC20 is Dummy, ERC165, ERC20Events { - // Selector: name() 06fdde03 + /// @dev EVM selector for this function is: 0x06fdde03, + /// or in textual repr: name() function name() public view returns (string memory) { require(false, stub_error); dummy; return ""; } - // Selector: symbol() 95d89b41 + /// @dev EVM selector for this function is: 0x95d89b41, + /// or in textual repr: symbol() function symbol() public view returns (string memory) { require(false, stub_error); dummy; return ""; } - // Selector: totalSupply() 18160ddd + /// @dev EVM selector for this function is: 0x18160ddd, + /// or in textual repr: totalSupply() function totalSupply() public view returns (uint256) { require(false, stub_error); dummy; return 0; } - // Selector: decimals() 313ce567 + /// @dev EVM selector for this function is: 0x313ce567, + /// or in textual repr: decimals() function decimals() public view returns (uint8) { require(false, stub_error); dummy; return 0; } - // Selector: balanceOf(address) 70a08231 + /// @dev EVM selector for this function is: 0x70a08231, + /// or in textual repr: balanceOf(address) function balanceOf(address owner) public view returns (uint256) { require(false, stub_error); owner; @@ -332,7 +454,8 @@ return 0; } - // Selector: transfer(address,uint256) a9059cbb + /// @dev EVM selector for this function is: 0xa9059cbb, + /// or in textual repr: transfer(address,uint256) function transfer(address to, uint256 amount) public returns (bool) { require(false, stub_error); to; @@ -341,7 +464,8 @@ return false; } - // Selector: transferFrom(address,address,uint256) 23b872dd + /// @dev EVM selector for this function is: 0x23b872dd, + /// or in textual repr: transferFrom(address,address,uint256) function transferFrom( address from, address to, @@ -355,7 +479,8 @@ return false; } - // Selector: approve(address,uint256) 095ea7b3 + /// @dev EVM selector for this function is: 0x095ea7b3, + /// or in textual repr: approve(address,uint256) function approve(address spender, uint256 amount) public returns (bool) { require(false, stub_error); spender; @@ -364,7 +489,8 @@ return false; } - // Selector: allowance(address,address) dd62ed3e + /// @dev EVM selector for this function is: 0xdd62ed3e, + /// or in textual repr: allowance(address,address) function allowance(address owner, address spender) public view @@ -382,6 +508,7 @@ Dummy, ERC165, ERC20, + ERC20Mintable, ERC20UniqueExtensions, Collection {} --- a/pallets/nonfungible/CHANGELOG.md +++ b/pallets/nonfungible/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. +## [v0.1.5] - 2022-08-24 + +### Change + - Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`. + ## [v0.1.4] 2022-08-16 --- a/pallets/nonfungible/Cargo.toml +++ b/pallets/nonfungible/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-nonfungible" -version = "0.1.4" +version = "0.1.5" license = "GPLv3" edition = "2021" --- a/pallets/nonfungible/src/erc.rs +++ b/pallets/nonfungible/src/erc.rs @@ -50,14 +50,14 @@ }; /// @title A contract that allows to set and delete token properties and change token property permissions. -#[solidity_interface(name = "TokenProperties")] +#[solidity_interface(name = TokenProperties)] impl NonfungibleHandle { /// @notice Set permissions for token property. /// @dev Throws error if `msg.sender` is not admin or owner of the collection. /// @param key Property key. - /// @param is_mutable Permission to mutate property. - /// @param collection_admin Permission to mutate property by collection admin if property is mutable. - /// @param token_owner Permission to mutate property by token owner if property is mutable. + /// @param isMutable Permission to mutate property. + /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable. + /// @param tokenOwner Permission to mutate property by token owner if property is mutable. fn set_token_property_permission( &mut self, caller: caller, @@ -201,7 +201,7 @@ /// @title ERC-721 Non-Fungible Token Standard, optional metadata extension /// @dev See https://eips.ethereum.org/EIPS/eip-721 -#[solidity_interface(name = "ERC721Metadata")] +#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f)] impl NonfungibleHandle { /// @notice A descriptive name for a collection of NFTs in this contract fn name(&self) -> Result { @@ -262,7 +262,7 @@ /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension /// @dev See https://eips.ethereum.org/EIPS/eip-721 -#[solidity_interface(name = "ERC721Enumerable")] +#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63)] impl NonfungibleHandle { /// @notice Enumerate valid NFTs /// @param index A counter less than `totalSupply()` @@ -289,7 +289,7 @@ /// @title ERC-721 Non-Fungible Token Standard /// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md -#[solidity_interface(name = "ERC721", events(ERC721Events))] +#[solidity_interface(name = ERC721, events(ERC721Events), expect_selector = 0x80ac58cd)] impl NonfungibleHandle { /// @notice Count all NFTs assigned to an owner /// @dev NFTs assigned to the zero address are considered invalid, and this @@ -316,13 +316,13 @@ .as_eth()) } /// @dev Not implemented + #[solidity(rename_selector = "safeTransferFrom")] fn safe_transfer_from_with_data( &mut self, _from: address, _to: address, _token_id: uint256, _data: bytes, - _value: value, ) -> Result { // TODO: Not implemetable Err("not implemented".into()) @@ -333,7 +333,6 @@ _from: address, _to: address, _token_id: uint256, - _value: value, ) -> Result { // TODO: Not implemetable Err("not implemented".into()) @@ -348,7 +347,6 @@ /// @param from The current owner of the NFT /// @param to The new owner /// @param tokenId The NFT to transfer - /// @param _value Not used for an NFT #[weight(>::transfer_from())] fn transfer_from( &mut self, @@ -356,7 +354,6 @@ from: address, to: address, token_id: uint256, - _value: value, ) -> Result { let caller = T::CrossAccountId::from_eth(caller); let from = T::CrossAccountId::from_eth(from); @@ -378,13 +375,7 @@ /// @param approved The new approved NFT controller /// @param tokenId The NFT to approve #[weight(>::approve())] - fn approve( - &mut self, - caller: caller, - approved: address, - token_id: uint256, - _value: value, - ) -> Result { + fn approve(&mut self, caller: caller, approved: address, token_id: uint256) -> Result { let caller = T::CrossAccountId::from_eth(caller); let approved = T::CrossAccountId::from_eth(approved); let token = token_id.try_into()?; @@ -419,7 +410,7 @@ } /// @title ERC721 Token that can be irreversibly burned (destroyed). -#[solidity_interface(name = "ERC721Burnable")] +#[solidity_interface(name = ERC721Burnable)] impl NonfungibleHandle { /// @notice Burns a specific ERC721 token. /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized @@ -436,7 +427,7 @@ } /// @title ERC721 minting logic. -#[solidity_interface(name = "ERC721Mintable", events(ERC721MintableEvents))] +#[solidity_interface(name = ERC721Mintable, events(ERC721MintableEvents))] impl NonfungibleHandle { fn minting_finished(&self) -> Result { Ok(false) @@ -597,22 +588,15 @@ } /// @title Unique extensions for ERC721. -#[solidity_interface(name = "ERC721UniqueExtensions")] +#[solidity_interface(name = ERC721UniqueExtensions)] impl NonfungibleHandle { /// @notice Transfer ownership of an NFT /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` /// is the zero address. Throws if `tokenId` is not a valid NFT. /// @param to The new owner /// @param tokenId The NFT to transfer - /// @param _value Not used for an NFT #[weight(>::transfer())] - fn transfer( - &mut self, - caller: caller, - to: address, - token_id: uint256, - _value: value, - ) -> Result { + fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result { let caller = T::CrossAccountId::from_eth(caller); let to = T::CrossAccountId::from_eth(to); let token = token_id.try_into()?; @@ -630,15 +614,8 @@ /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT. /// @param from The current owner of the NFT /// @param tokenId The NFT to transfer - /// @param _value Not used for an NFT #[weight(>::burn_from())] - fn burn_from( - &mut self, - caller: caller, - from: address, - token_id: uint256, - _value: value, - ) -> Result { + fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result { let caller = T::CrossAccountId::from_eth(caller); let from = T::CrossAccountId::from_eth(from); let token = token_id.try_into()?; @@ -751,7 +728,7 @@ } #[solidity_interface( - name = "UniqueNFT", + name = UniqueNFT, is( ERC721, ERC721Metadata, @@ -759,11 +736,11 @@ ERC721UniqueExtensions, ERC721Mintable, ERC721Burnable, - via("CollectionHandle", common_mut, Collection), + Collection(common_mut, CollectionHandle), TokenProperties, ) )] -impl NonfungibleHandle where T::AccountId: From<[u8; 32]> {} +impl NonfungibleHandle where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {} // Not a tests, but code generators generate_stubgen!(gen_impl, UniqueNFTCall<()>, true); @@ -771,7 +748,7 @@ impl CommonEvmHandler for NonfungibleHandle where - T::AccountId: From<[u8; 32]>, + T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>, { const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw"); --- a/pallets/nonfungible/src/stubs/UniqueNFT.sol +++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol @@ -3,13 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; -// Anonymous struct -struct Tuple0 { - uint256 field_0; - string field_1; -} - -// Common stubs holder +/// @dev common stubs holder contract Dummy { uint8 dummy; string stub_error = "this contract is implemented in native"; @@ -27,40 +21,17 @@ } } -// Inline -contract ERC721Events { - event Transfer( - address indexed from, - address indexed to, - uint256 indexed tokenId - ); - event Approval( - address indexed owner, - address indexed approved, - uint256 indexed tokenId - ); - event ApprovalForAll( - address indexed owner, - address indexed operator, - bool approved - ); -} - -// Inline -contract ERC721MintableEvents { - event MintingFinished(); -} - -// Selector: 41369377 +/// @title A contract that allows to set and delete token properties and change token property permissions. +/// @dev the ERC-165 identifier for this interface is 0x41369377 contract TokenProperties is Dummy, ERC165 { - // @notice Set permissions for token property. - // @dev Throws error if `msg.sender` is not admin or owner of the collection. - // @param key Property key. - // @param is_mutable Permission to mutate property. - // @param collection_admin Permission to mutate property by collection admin if property is mutable. - // @param token_owner Permission to mutate property by token owner if property is mutable. - // - // Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa + /// @notice Set permissions for token property. + /// @dev Throws error if `msg.sender` is not admin or owner of the collection. + /// @param key Property key. + /// @param isMutable Permission to mutate property. + /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable. + /// @param tokenOwner Permission to mutate property by token owner if property is mutable. + /// @dev EVM selector for this function is: 0x222d97fa, + /// or in textual repr: setTokenPropertyPermission(string,bool,bool,bool) function setTokenPropertyPermission( string memory key, bool isMutable, @@ -75,13 +46,13 @@ dummy = 0; } - // @notice Set token property value. - // @dev Throws error if `msg.sender` has no permission to edit the property. - // @param tokenId ID of the token. - // @param key Property key. - // @param value Property value. - // - // Selector: setProperty(uint256,string,bytes) 1752d67b + /// @notice Set token property value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param key Property key. + /// @param value Property value. + /// @dev EVM selector for this function is: 0x1752d67b, + /// or in textual repr: setProperty(uint256,string,bytes) function setProperty( uint256 tokenId, string memory key, @@ -94,12 +65,12 @@ dummy = 0; } - // @notice Delete token property value. - // @dev Throws error if `msg.sender` has no permission to edit the property. - // @param tokenId ID of the token. - // @param key Property key. - // - // Selector: deleteProperty(uint256,string) 066111d1 + /// @notice Delete token property value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param key Property key. + /// @dev EVM selector for this function is: 0x066111d1, + /// or in textual repr: deleteProperty(uint256,string) function deleteProperty(uint256 tokenId, string memory key) public { require(false, stub_error); tokenId; @@ -107,13 +78,13 @@ dummy = 0; } - // @notice Get token property value. - // @dev Throws error if key not found - // @param tokenId ID of the token. - // @param key Property key. - // @return Property value bytes - // - // Selector: property(uint256,string) 7228c327 + /// @notice Get token property value. + /// @dev Throws error if key not found + /// @param tokenId ID of the token. + /// @param key Property key. + /// @return Property value bytes + /// @dev EVM selector for this function is: 0x7228c327, + /// or in textual repr: property(uint256,string) function property(uint256 tokenId, string memory key) public view @@ -127,511 +98,513 @@ } } -// Selector: 42966c68 -contract ERC721Burnable is Dummy, ERC165 { - // @notice Burns a specific ERC721 token. - // @dev Throws unless `msg.sender` is the current NFT owner, or an authorized - // operator of the current owner. - // @param tokenId The NFT to approve - // - // Selector: burn(uint256) 42966c68 - function burn(uint256 tokenId) public { +/// @title A contract that allows you to work with collections. +/// @dev the ERC-165 identifier for this interface is 0xe54be640 +contract Collection is Dummy, ERC165 { + /// Set collection property. + /// + /// @param key Property key. + /// @param value Propery value. + /// @dev EVM selector for this function is: 0x2f073f66, + /// or in textual repr: setCollectionProperty(string,bytes) + function setCollectionProperty(string memory key, bytes memory value) + public + { require(false, stub_error); - tokenId; + key; + value; dummy = 0; } -} -// Selector: 58800161 -contract ERC721 is Dummy, ERC165, ERC721Events { - // @notice Count all NFTs assigned to an owner - // @dev NFTs assigned to the zero address are considered invalid, and this - // function throws for queries about the zero address. - // @param owner An address for whom to query the balance - // @return The number of NFTs owned by `owner`, possibly zero - // - // Selector: balanceOf(address) 70a08231 - function balanceOf(address owner) public view returns (uint256) { + /// Delete collection property. + /// + /// @param key Property key. + /// @dev EVM selector for this function is: 0x7b7debce, + /// or in textual repr: deleteCollectionProperty(string) + function deleteCollectionProperty(string memory key) public { require(false, stub_error); - owner; - dummy; - return 0; + key; + dummy = 0; } - // @notice Find the owner of an NFT - // @dev NFTs assigned to zero address are considered invalid, and queries - // about them do throw. - // @param tokenId The identifier for an NFT - // @return The address of the owner of the NFT - // - // Selector: ownerOf(uint256) 6352211e - function ownerOf(uint256 tokenId) public view returns (address) { + /// Get collection property. + /// + /// @dev Throws error if key not found. + /// + /// @param key Property key. + /// @return bytes The property corresponding to the key. + /// @dev EVM selector for this function is: 0xcf24fd6d, + /// or in textual repr: collectionProperty(string) + function collectionProperty(string memory key) + public + view + returns (bytes memory) + { require(false, stub_error); - tokenId; + key; dummy; - return 0x0000000000000000000000000000000000000000; + return hex""; } - // @dev Not implemented - // - // Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672 - function safeTransferFromWithData( - address from, - address to, - uint256 tokenId, - bytes memory data - ) public { + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0x7623402e, + /// or in textual repr: setCollectionSponsor(address) + function setCollectionSponsor(address sponsor) public { require(false, stub_error); - from; - to; - tokenId; - data; + sponsor; dummy = 0; } - // @dev Not implemented - // - // Selector: safeTransferFrom(address,address,uint256) 42842e0e - function safeTransferFrom( - address from, - address to, - uint256 tokenId - ) public { + /// Set the substrate sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Substrate address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0xc74d6751, + /// or in textual repr: setCollectionSponsorSubstrate(uint256) + function setCollectionSponsorSubstrate(uint256 sponsor) public { require(false, stub_error); - from; - to; - tokenId; + sponsor; dummy = 0; } - // @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE - // TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE - // THEY MAY BE PERMANENTLY LOST - // @dev Throws unless `msg.sender` is the current owner or an authorized - // operator for this NFT. Throws if `from` is not the current owner. Throws - // if `to` is the zero address. Throws if `tokenId` is not a valid NFT. - // @param from The current owner of the NFT - // @param to The new owner - // @param tokenId The NFT to transfer - // @param _value Not used for an NFT - // - // Selector: transferFrom(address,address,uint256) 23b872dd - function transferFrom( - address from, - address to, - uint256 tokenId - ) public { + /// @dev EVM selector for this function is: 0x058ac185, + /// or in textual repr: hasCollectionPendingSponsor() + function hasCollectionPendingSponsor() public view returns (bool) { require(false, stub_error); - from; - to; - tokenId; - dummy = 0; + dummy; + return false; } - // @notice Set or reaffirm the approved address for an NFT - // @dev The zero address indicates there is no approved address. - // @dev Throws unless `msg.sender` is the current NFT owner, or an authorized - // operator of the current owner. - // @param approved The new approved NFT controller - // @param tokenId The NFT to approve - // - // Selector: approve(address,uint256) 095ea7b3 - function approve(address approved, uint256 tokenId) public { + /// Collection sponsorship confirmation. + /// + /// @dev After setting the sponsor for the collection, it must be confirmed with this function. + /// @dev EVM selector for this function is: 0x3c50e97a, + /// or in textual repr: confirmCollectionSponsorship() + function confirmCollectionSponsorship() public { require(false, stub_error); - approved; - tokenId; dummy = 0; } - // @dev Not implemented - // - // Selector: setApprovalForAll(address,bool) a22cb465 - function setApprovalForAll(address operator, bool approved) public { + /// Remove collection sponsor. + /// @dev EVM selector for this function is: 0x6e0326a3, + /// or in textual repr: removeCollectionSponsor() + function removeCollectionSponsor() public { require(false, stub_error); - operator; - approved; dummy = 0; } - // @dev Not implemented - // - // Selector: getApproved(uint256) 081812fc - function getApproved(uint256 tokenId) public view returns (address) { + /// Get current sponsor. + /// + /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + /// @dev EVM selector for this function is: 0xb66bbc14, + /// or in textual repr: getCollectionSponsor() + function getCollectionSponsor() public view returns (Tuple17 memory) { require(false, stub_error); - tokenId; dummy; - return 0x0000000000000000000000000000000000000000; + return Tuple17(0x0000000000000000000000000000000000000000, 0); } - // @dev Not implemented - // - // Selector: isApprovedForAll(address,address) e985e9c5 - function isApprovedForAll(address owner, address operator) - public - view - returns (address) - { + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "accountTokenOwnershipLimit", + /// "sponsoredDataSize", + /// "sponsoredDataRateLimit", + /// "tokenLimit", + /// "sponsorTransferTimeout", + /// "sponsorApproveTimeout" + /// @param value Value of the limit. + /// @dev EVM selector for this function is: 0x6a3841db, + /// or in textual repr: setCollectionLimit(string,uint32) + function setCollectionLimit(string memory limit, uint32 value) public { require(false, stub_error); - owner; - operator; - dummy; - return 0x0000000000000000000000000000000000000000; + limit; + value; + dummy = 0; } -} -// Selector: 5b5e139f -contract ERC721Metadata is Dummy, ERC165 { - // @notice A descriptive name for a collection of NFTs in this contract - // - // Selector: name() 06fdde03 - function name() public view returns (string memory) { + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "ownerCanTransfer", + /// "ownerCanDestroy", + /// "transfersEnabled" + /// @param value Value of the limit. + /// @dev EVM selector for this function is: 0x993b7fba, + /// or in textual repr: setCollectionLimit(string,bool) + function setCollectionLimit(string memory limit, bool value) public { require(false, stub_error); - dummy; - return ""; + limit; + value; + dummy = 0; } - // @notice An abbreviated name for NFTs in this contract - // - // Selector: symbol() 95d89b41 - function symbol() public view returns (string memory) { + /// Get contract address. + /// @dev EVM selector for this function is: 0xf6b4dfb4, + /// or in textual repr: contractAddress() + function contractAddress() public view returns (address) { require(false, stub_error); dummy; - return ""; + return 0x0000000000000000000000000000000000000000; } - // @notice A distinct Uniform Resource Identifier (URI) for a given asset. - // - // @dev If the token has a `url` property and it is not empty, it is returned. - // Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`. - // If the collection property `baseURI` is empty or absent, return "" (empty string) - // otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix - // otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings). - // - // @return token's const_metadata - // - // Selector: tokenURI(uint256) c87b56dd - function tokenURI(uint256 tokenId) public view returns (string memory) { + /// Add collection admin by substrate address. + /// @param newAdmin Substrate administrator address. + /// @dev EVM selector for this function is: 0x5730062b, + /// or in textual repr: addCollectionAdminSubstrate(uint256) + function addCollectionAdminSubstrate(uint256 newAdmin) public { require(false, stub_error); - tokenId; - dummy; - return ""; + newAdmin; + dummy = 0; } -} -// Selector: 68ccfe89 -contract ERC721Mintable is Dummy, ERC165, ERC721MintableEvents { - // Selector: mintingFinished() 05d2035b - function mintingFinished() public view returns (bool) { + /// Remove collection admin by substrate address. + /// @param admin Substrate administrator address. + /// @dev EVM selector for this function is: 0x4048fcf9, + /// or in textual repr: removeCollectionAdminSubstrate(uint256) + function removeCollectionAdminSubstrate(uint256 admin) public { require(false, stub_error); - dummy; - return false; + admin; + dummy = 0; } - // @notice Function to mint token. - // @dev `tokenId` should be obtained with `nextTokenId` method, - // unlike standard, you can't specify it manually - // @param to The new owner - // @param tokenId ID of the minted NFT - // - // Selector: mint(address,uint256) 40c10f19 - function mint(address to, uint256 tokenId) public returns (bool) { + /// Add collection admin. + /// @param newAdmin Address of the added administrator. + /// @dev EVM selector for this function is: 0x92e462c7, + /// or in textual repr: addCollectionAdmin(address) + function addCollectionAdmin(address newAdmin) public { require(false, stub_error); - to; - tokenId; + newAdmin; dummy = 0; - return false; } - // @notice Function to mint token with the given tokenUri. - // @dev `tokenId` should be obtained with `nextTokenId` method, - // unlike standard, you can't specify it manually - // @param to The new owner - // @param tokenId ID of the minted NFT - // @param tokenUri Token URI that would be stored in the NFT properties - // - // Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f - function mintWithTokenURI( - address to, - uint256 tokenId, - string memory tokenUri - ) public returns (bool) { + /// Remove collection admin. + /// + /// @param admin Address of the removed administrator. + /// @dev EVM selector for this function is: 0xfafd7b42, + /// or in textual repr: removeCollectionAdmin(address) + function removeCollectionAdmin(address admin) public { require(false, stub_error); - to; - tokenId; - tokenUri; + admin; dummy = 0; - return false; } - // @dev Not implemented - // - // Selector: finishMinting() 7d64bcb4 - function finishMinting() public returns (bool) { + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' + /// @dev EVM selector for this function is: 0x112d4586, + /// or in textual repr: setCollectionNesting(bool) + function setCollectionNesting(bool enable) public { require(false, stub_error); + enable; dummy = 0; - return false; } -} -// Selector: 6cf113cd -contract Collection is Dummy, ERC165 { - // Set collection property. - // - // @param key Property key. - // @param value Propery value. - // - // Selector: setCollectionProperty(string,bytes) 2f073f66 - function setCollectionProperty(string memory key, bytes memory value) + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + /// @param collections Addresses of collections that will be available for nesting. + /// @dev EVM selector for this function is: 0x64872396, + /// or in textual repr: setCollectionNesting(bool,address[]) + function setCollectionNesting(bool enable, address[] memory collections) public { require(false, stub_error); - key; - value; + enable; + collections; dummy = 0; } - // Delete collection property. - // - // @param key Property key. - // - // Selector: deleteCollectionProperty(string) 7b7debce - function deleteCollectionProperty(string memory key) public { + /// Set the collection access method. + /// @param mode Access mode + /// 0 for Normal + /// 1 for AllowList + /// @dev EVM selector for this function is: 0x41835d4c, + /// or in textual repr: setCollectionAccess(uint8) + function setCollectionAccess(uint8 mode) public { require(false, stub_error); - key; + mode; dummy = 0; } - // Get collection property. - // - // @dev Throws error if key not found. - // - // @param key Property key. - // @return bytes The property corresponding to the key. - // - // Selector: collectionProperty(string) cf24fd6d - function collectionProperty(string memory key) - public - view - returns (bytes memory) - { - require(false, stub_error); - key; - dummy; - return hex""; - } - - // Set the sponsor of the collection. - // - // @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. - // - // @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. - // - // Selector: setCollectionSponsor(address) 7623402e - function setCollectionSponsor(address sponsor) public { + /// Add the user to the allowed list. + /// + /// @param user Address of a trusted user. + /// @dev EVM selector for this function is: 0x67844fe6, + /// or in textual repr: addToCollectionAllowList(address) + function addToCollectionAllowList(address user) public { require(false, stub_error); - sponsor; + user; dummy = 0; } - // Collection sponsorship confirmation. - // - // @dev After setting the sponsor for the collection, it must be confirmed with this function. - // - // Selector: confirmCollectionSponsorship() 3c50e97a - function confirmCollectionSponsorship() public { + /// Remove the user from the allowed list. + /// + /// @param user Address of a removed user. + /// @dev EVM selector for this function is: 0x85c51acb, + /// or in textual repr: removeFromCollectionAllowList(address) + function removeFromCollectionAllowList(address user) public { require(false, stub_error); + user; dummy = 0; } - // Set limits for the collection. - // @dev Throws error if limit not found. - // @param limit Name of the limit. Valid names: - // "accountTokenOwnershipLimit", - // "sponsoredDataSize", - // "sponsoredDataRateLimit", - // "tokenLimit", - // "sponsorTransferTimeout", - // "sponsorApproveTimeout" - // @param value Value of the limit. - // - // Selector: setCollectionLimit(string,uint32) 6a3841db - function setCollectionLimit(string memory limit, uint32 value) public { + /// Switch permission for minting. + /// + /// @param mode Enable if "true". + /// @dev EVM selector for this function is: 0x00018e84, + /// or in textual repr: setCollectionMintMode(bool) + function setCollectionMintMode(bool mode) public { require(false, stub_error); - limit; - value; + mode; dummy = 0; } - // Set limits for the collection. - // @dev Throws error if limit not found. - // @param limit Name of the limit. Valid names: - // "ownerCanTransfer", - // "ownerCanDestroy", - // "transfersEnabled" - // @param value Value of the limit. - // - // Selector: setCollectionLimit(string,bool) 993b7fba - function setCollectionLimit(string memory limit, bool value) public { + /// Check that account is the owner or admin of the collection + /// + /// @param user account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x9811b0c7, + /// or in textual repr: isOwnerOrAdmin(address) + function isOwnerOrAdmin(address user) public view returns (bool) { require(false, stub_error); - limit; - value; - dummy = 0; + user; + dummy; + return false; } - // Get contract address. - // - // Selector: contractAddress() f6b4dfb4 - function contractAddress() public view returns (address) { + /// Check that substrate account is the owner or admin of the collection + /// + /// @param user account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x68910e00, + /// or in textual repr: isOwnerOrAdminSubstrate(uint256) + function isOwnerOrAdminSubstrate(uint256 user) public view returns (bool) { require(false, stub_error); + user; dummy; - return 0x0000000000000000000000000000000000000000; + return false; } - // Add collection admin by substrate address. - // @param new_admin Substrate administrator address. - // - // Selector: addCollectionAdminSubstrate(uint256) 5730062b - function addCollectionAdminSubstrate(uint256 newAdmin) public { + /// Returns collection type + /// + /// @return `Fungible` or `NFT` or `ReFungible` + /// @dev EVM selector for this function is: 0xd34b55b8, + /// or in textual repr: uniqueCollectionType() + function uniqueCollectionType() public returns (string memory) { require(false, stub_error); - newAdmin; dummy = 0; + return ""; } - // Remove collection admin by substrate address. - // @param admin Substrate administrator address. - // - // Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9 - function removeCollectionAdminSubstrate(uint256 admin) public { + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner account + /// @dev EVM selector for this function is: 0x13af4035, + /// or in textual repr: setOwner(address) + function setOwner(address newOwner) public { require(false, stub_error); - admin; + newOwner; dummy = 0; } - // Add collection admin. - // @param new_admin Address of the added administrator. - // - // Selector: addCollectionAdmin(address) 92e462c7 - function addCollectionAdmin(address newAdmin) public { + /// Changes collection owner to another substrate account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner substrate account + /// @dev EVM selector for this function is: 0xb212138f, + /// or in textual repr: setOwnerSubstrate(uint256) + function setOwnerSubstrate(uint256 newOwner) public { require(false, stub_error); - newAdmin; + newOwner; dummy = 0; } +} - // Remove collection admin. - // - // @param new_admin Address of the removed administrator. - // - // Selector: removeCollectionAdmin(address) fafd7b42 - function removeCollectionAdmin(address admin) public { +/// @dev anonymous struct +struct Tuple17 { + address field_0; + uint256 field_1; +} + +/// @title ERC721 Token that can be irreversibly burned (destroyed). +/// @dev the ERC-165 identifier for this interface is 0x42966c68 +contract ERC721Burnable is Dummy, ERC165 { + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param tokenId The NFT to approve + /// @dev EVM selector for this function is: 0x42966c68, + /// or in textual repr: burn(uint256) + function burn(uint256 tokenId) public { require(false, stub_error); - admin; + tokenId; dummy = 0; } +} + +/// @dev inlined interface +contract ERC721MintableEvents { + event MintingFinished(); +} - // Toggle accessibility of collection nesting. - // - // @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' - // - // Selector: setCollectionNesting(bool) 112d4586 - function setCollectionNesting(bool enable) public { +/// @title ERC721 minting logic. +/// @dev the ERC-165 identifier for this interface is 0x68ccfe89 +contract ERC721Mintable is Dummy, ERC165, ERC721MintableEvents { + /// @dev EVM selector for this function is: 0x05d2035b, + /// or in textual repr: mintingFinished() + function mintingFinished() public view returns (bool) { require(false, stub_error); - enable; - dummy = 0; + dummy; + return false; } - // Toggle accessibility of collection nesting. - // - // @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' - // @param collections Addresses of collections that will be available for nesting. - // - // Selector: setCollectionNesting(bool,address[]) 64872396 - function setCollectionNesting(bool enable, address[] memory collections) - public - { + /// @notice Function to mint token. + /// @dev `tokenId` should be obtained with `nextTokenId` method, + /// unlike standard, you can't specify it manually + /// @param to The new owner + /// @param tokenId ID of the minted NFT + /// @dev EVM selector for this function is: 0x40c10f19, + /// or in textual repr: mint(address,uint256) + function mint(address to, uint256 tokenId) public returns (bool) { require(false, stub_error); - enable; - collections; + to; + tokenId; dummy = 0; + return false; } - // Set the collection access method. - // @param mode Access mode - // 0 for Normal - // 1 for AllowList - // - // Selector: setCollectionAccess(uint8) 41835d4c - function setCollectionAccess(uint8 mode) public { + /// @notice Function to mint token with the given tokenUri. + /// @dev `tokenId` should be obtained with `nextTokenId` method, + /// unlike standard, you can't specify it manually + /// @param to The new owner + /// @param tokenId ID of the minted NFT + /// @param tokenUri Token URI that would be stored in the NFT properties + /// @dev EVM selector for this function is: 0x50bb4e7f, + /// or in textual repr: mintWithTokenURI(address,uint256,string) + function mintWithTokenURI( + address to, + uint256 tokenId, + string memory tokenUri + ) public returns (bool) { require(false, stub_error); - mode; + to; + tokenId; + tokenUri; dummy = 0; + return false; } - // Add the user to the allowed list. - // - // @param user Address of a trusted user. - // - // Selector: addToCollectionAllowList(address) 67844fe6 - function addToCollectionAllowList(address user) public { + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x7d64bcb4, + /// or in textual repr: finishMinting() + function finishMinting() public returns (bool) { require(false, stub_error); - user; dummy = 0; + return false; } +} - // Remove the user from the allowed list. - // - // @param user Address of a removed user. - // - // Selector: removeFromCollectionAllowList(address) 85c51acb - function removeFromCollectionAllowList(address user) public { +/// @title Unique extensions for ERC721. +/// @dev the ERC-165 identifier for this interface is 0xd74d154f +contract ERC721UniqueExtensions is Dummy, ERC165 { + /// @notice Transfer ownership of an NFT + /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` + /// is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param to The new owner + /// @param tokenId The NFT to transfer + /// @dev EVM selector for this function is: 0xa9059cbb, + /// or in textual repr: transfer(address,uint256) + function transfer(address to, uint256 tokenId) public { require(false, stub_error); - user; + to; + tokenId; dummy = 0; } - // Switch permission for minting. - // - // @param mode Enable if "true". - // - // Selector: setCollectionMintMode(bool) 00018e84 - function setCollectionMintMode(bool mode) public { + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this NFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param from The current owner of the NFT + /// @param tokenId The NFT to transfer + /// @dev EVM selector for this function is: 0x79cc6790, + /// or in textual repr: burnFrom(address,uint256) + function burnFrom(address from, uint256 tokenId) public { require(false, stub_error); - mode; + from; + tokenId; dummy = 0; } - // Check that account is the owner or admin of the collection - // - // @param user account to verify - // @return "true" if account is the owner or admin - // - // Selector: verifyOwnerOrAdmin(address) c2282493 - function verifyOwnerOrAdmin(address user) public view returns (bool) { + /// @notice Returns next free NFT ID. + /// @dev EVM selector for this function is: 0x75794a3c, + /// or in textual repr: nextTokenId() + function nextTokenId() public view returns (uint256) { require(false, stub_error); - user; dummy; + return 0; + } + + /// @notice Function to mint multiple tokens. + /// @dev `tokenIds` should be an array of consecutive numbers and first number + /// should be obtained with `nextTokenId` method + /// @param to The new owner + /// @param tokenIds IDs of the minted NFTs + /// @dev EVM selector for this function is: 0x44a9945e, + /// or in textual repr: mintBulk(address,uint256[]) + function mintBulk(address to, uint256[] memory tokenIds) + public + returns (bool) + { + require(false, stub_error); + to; + tokenIds; + dummy = 0; return false; } - // Returns collection type - // - // @return `Fungible` or `NFT` or `ReFungible` - // - // Selector: uniqueCollectionType() d34b55b8 - function uniqueCollectionType() public returns (string memory) { + /// @notice Function to mint multiple tokens with the given tokenUris. + /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive + /// numbers and first number should be obtained with `nextTokenId` method + /// @param to The new owner + /// @param tokens array of pairs of token ID and token URI for minted tokens + /// @dev EVM selector for this function is: 0x36543006, + /// or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[]) + function mintBulkWithTokenURI(address to, Tuple8[] memory tokens) + public + returns (bool) + { require(false, stub_error); + to; + tokens; dummy = 0; - return ""; + return false; } } -// Selector: 780e9d63 +/// @dev anonymous struct +struct Tuple8 { + uint256 field_0; + string field_1; +} + +/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// @dev the ERC-165 identifier for this interface is 0x780e9d63 contract ERC721Enumerable is Dummy, ERC165 { - // @notice Enumerate valid NFTs - // @param index A counter less than `totalSupply()` - // @return The token identifier for the `index`th NFT, - // (sort order not specified) - // - // Selector: tokenByIndex(uint256) 4f6ccce7 + /// @notice Enumerate valid NFTs + /// @param index A counter less than `totalSupply()` + /// @return The token identifier for the `index`th NFT, + /// (sort order not specified) + /// @dev EVM selector for this function is: 0x4f6ccce7, + /// or in textual repr: tokenByIndex(uint256) function tokenByIndex(uint256 index) public view returns (uint256) { require(false, stub_error); index; @@ -639,9 +612,9 @@ return 0; } - // @dev Not implemented - // - // Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59 + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x2f745c59, + /// or in textual repr: tokenOfOwnerByIndex(address,uint256) function tokenOfOwnerByIndex(address owner, uint256 index) public view @@ -654,11 +627,11 @@ return 0; } - // @notice Count NFTs tracked by this contract - // @return A count of valid NFTs tracked by this contract, where each one of - // them has an assigned and queryable owner not equal to the zero address - // - // Selector: totalSupply() 18160ddd + /// @notice Count NFTs tracked by this contract + /// @return A count of valid NFTs tracked by this contract, where each one of + /// them has an assigned and queryable owner not equal to the zero address + /// @dev EVM selector for this function is: 0x18160ddd, + /// or in textual repr: totalSupply() function totalSupply() public view returns (uint256) { require(false, stub_error); dummy; @@ -666,82 +639,201 @@ } } -// Selector: d74d154f -contract ERC721UniqueExtensions is Dummy, ERC165 { - // @notice Transfer ownership of an NFT - // @dev Throws unless `msg.sender` is the current owner. Throws if `to` - // is the zero address. Throws if `tokenId` is not a valid NFT. - // @param to The new owner - // @param tokenId The NFT to transfer - // @param _value Not used for an NFT - // - // Selector: transfer(address,uint256) a9059cbb - function transfer(address to, uint256 tokenId) public { +/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// @dev the ERC-165 identifier for this interface is 0x5b5e139f +contract ERC721Metadata is Dummy, ERC165 { + /// @notice A descriptive name for a collection of NFTs in this contract + /// @dev EVM selector for this function is: 0x06fdde03, + /// or in textual repr: name() + function name() public view returns (string memory) { + require(false, stub_error); + dummy; + return ""; + } + + /// @notice An abbreviated name for NFTs in this contract + /// @dev EVM selector for this function is: 0x95d89b41, + /// or in textual repr: symbol() + function symbol() public view returns (string memory) { + require(false, stub_error); + dummy; + return ""; + } + + /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. + /// + /// @dev If the token has a `url` property and it is not empty, it is returned. + /// Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`. + /// If the collection property `baseURI` is empty or absent, return "" (empty string) + /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix + /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings). + /// + /// @return token's const_metadata + /// @dev EVM selector for this function is: 0xc87b56dd, + /// or in textual repr: tokenURI(uint256) + function tokenURI(uint256 tokenId) public view returns (string memory) { + require(false, stub_error); + tokenId; + dummy; + return ""; + } +} + +/// @dev inlined interface +contract ERC721Events { + event Transfer( + address indexed from, + address indexed to, + uint256 indexed tokenId + ); + event Approval( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); +} + +/// @title ERC-721 Non-Fungible Token Standard +/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md +/// @dev the ERC-165 identifier for this interface is 0x80ac58cd +contract ERC721 is Dummy, ERC165, ERC721Events { + /// @notice Count all NFTs assigned to an owner + /// @dev NFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param owner An address for whom to query the balance + /// @return The number of NFTs owned by `owner`, possibly zero + /// @dev EVM selector for this function is: 0x70a08231, + /// or in textual repr: balanceOf(address) + function balanceOf(address owner) public view returns (uint256) { + require(false, stub_error); + owner; + dummy; + return 0; + } + + /// @notice Find the owner of an NFT + /// @dev NFTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// @param tokenId The identifier for an NFT + /// @return The address of the owner of the NFT + /// @dev EVM selector for this function is: 0x6352211e, + /// or in textual repr: ownerOf(uint256) + function ownerOf(uint256 tokenId) public view returns (address) { + require(false, stub_error); + tokenId; + dummy; + return 0x0000000000000000000000000000000000000000; + } + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0xb88d4fde, + /// or in textual repr: safeTransferFrom(address,address,uint256,bytes) + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public { + require(false, stub_error); + from; + to; + tokenId; + data; + dummy = 0; + } + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x42842e0e, + /// or in textual repr: safeTransferFrom(address,address,uint256) + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public { require(false, stub_error); + from; to; tokenId; dummy = 0; } - // @notice Burns a specific ERC721 token. - // @dev Throws unless `msg.sender` is the current owner or an authorized - // operator for this NFT. Throws if `from` is not the current owner. Throws - // if `to` is the zero address. Throws if `tokenId` is not a valid NFT. - // @param from The current owner of the NFT - // @param tokenId The NFT to transfer - // @param _value Not used for an NFT - // - // Selector: burnFrom(address,uint256) 79cc6790 - function burnFrom(address from, uint256 tokenId) public { + /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE + /// THEY MAY BE PERMANENTLY LOST + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this NFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param from The current owner of the NFT + /// @param to The new owner + /// @param tokenId The NFT to transfer + /// @dev EVM selector for this function is: 0x23b872dd, + /// or in textual repr: transferFrom(address,address,uint256) + function transferFrom( + address from, + address to, + uint256 tokenId + ) public { require(false, stub_error); from; + to; tokenId; dummy = 0; } - // @notice Returns next free NFT ID. - // - // Selector: nextTokenId() 75794a3c - function nextTokenId() public view returns (uint256) { + /// @notice Set or reaffirm the approved address for an NFT + /// @dev The zero address indicates there is no approved address. + /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param approved The new approved NFT controller + /// @param tokenId The NFT to approve + /// @dev EVM selector for this function is: 0x095ea7b3, + /// or in textual repr: approve(address,uint256) + function approve(address approved, uint256 tokenId) public { require(false, stub_error); - dummy; - return 0; + approved; + tokenId; + dummy = 0; } - // @notice Function to mint multiple tokens. - // @dev `tokenIds` should be an array of consecutive numbers and first number - // should be obtained with `nextTokenId` method - // @param to The new owner - // @param tokenIds IDs of the minted NFTs - // - // Selector: mintBulk(address,uint256[]) 44a9945e - function mintBulk(address to, uint256[] memory tokenIds) - public - returns (bool) - { + /// @dev Not implemented + /// @dev EVM selector for this function is: 0xa22cb465, + /// or in textual repr: setApprovalForAll(address,bool) + function setApprovalForAll(address operator, bool approved) public { require(false, stub_error); - to; - tokenIds; + operator; + approved; dummy = 0; - return false; } - // @notice Function to mint multiple tokens with the given tokenUris. - // @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive - // numbers and first number should be obtained with `nextTokenId` method - // @param to The new owner - // @param tokens array of pairs of token ID and token URI for minted tokens - // - // Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006 - function mintBulkWithTokenURI(address to, Tuple0[] memory tokens) + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x081812fc, + /// or in textual repr: getApproved(uint256) + function getApproved(uint256 tokenId) public view returns (address) { + require(false, stub_error); + tokenId; + dummy; + return 0x0000000000000000000000000000000000000000; + } + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0xe985e9c5, + /// or in textual repr: isApprovedForAll(address,address) + function isApprovedForAll(address owner, address operator) public - returns (bool) + view + returns (address) { require(false, stub_error); - to; - tokens; - dummy = 0; - return false; + owner; + operator; + dummy; + return 0x0000000000000000000000000000000000000000; } } --- a/pallets/refungible/CHANGELOG.md +++ b/pallets/refungible/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. +## [v0.2.4] - 2022-08-24 + +### Change + - Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`. + ## [v0.2.3] 2022-08-16 --- a/pallets/refungible/Cargo.toml +++ b/pallets/refungible/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-refungible" -version = "0.2.3" +version = "0.2.4" license = "GPLv3" edition = "2021" --- a/pallets/refungible/src/benchmarking.rs +++ b/pallets/refungible/src/benchmarking.rs @@ -281,15 +281,6 @@ let item = create_max_item(&collection, &sender, [(owner.clone(), 100)])?; }: {>::repartition(&collection, &owner, item, 200)?} - set_parent_nft_unchecked { - bench_init!{ - owner: sub; collection: collection(owner); - sender: cross_from_sub(owner); owner: cross_sub; - }; - let item = create_max_item(&collection, &sender, [(owner.clone(), 100)])?; - - }: {>::set_parent_nft_unchecked(&collection, item, owner, T::CrossAccountId::from_eth(H160::default()))?} - token_owner { bench_init!{ owner: sub; collection: collection(owner); --- a/pallets/refungible/src/erc.rs +++ b/pallets/refungible/src/erc.rs @@ -53,14 +53,14 @@ pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff); /// @title A contract that allows to set and delete token properties and change token property permissions. -#[solidity_interface(name = "TokenProperties")] +#[solidity_interface(name = TokenProperties)] impl RefungibleHandle { /// @notice Set permissions for token property. /// @dev Throws error if `msg.sender` is not admin or owner of the collection. /// @param key Property key. - /// @param is_mutable Permission to mutate property. - /// @param collection_admin Permission to mutate property by collection admin if property is mutable. - /// @param token_owner Permission to mutate property by token owner if property is mutable. + /// @param isMutable Permission to mutate property. + /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable. + /// @param tokenOwner Permission to mutate property by token owner if property is mutable. fn set_token_property_permission( &mut self, caller: caller, @@ -197,7 +197,7 @@ MintingFinished {}, } -#[solidity_interface(name = "ERC721Metadata")] +#[solidity_interface(name = ERC721Metadata)] impl RefungibleHandle { /// @notice A descriptive name for a collection of RFTs in this contract fn name(&self) -> Result { @@ -258,7 +258,7 @@ /// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension /// @dev See https://eips.ethereum.org/EIPS/eip-721 -#[solidity_interface(name = "ERC721Enumerable")] +#[solidity_interface(name = ERC721Enumerable)] impl RefungibleHandle { /// @notice Enumerate valid RFTs /// @param index A counter less than `totalSupply()` @@ -285,7 +285,7 @@ /// @title ERC-721 Non-Fungible Token Standard /// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md -#[solidity_interface(name = "ERC721", events(ERC721Events))] +#[solidity_interface(name = ERC721, events(ERC721Events))] impl RefungibleHandle { /// @notice Count all RFTs assigned to an owner /// @dev RFTs assigned to the zero address are considered invalid, and this @@ -322,7 +322,6 @@ _to: address, _token_id: uint256, _data: bytes, - _value: value, ) -> Result { // TODO: Not implemetable Err("not implemented".into()) @@ -334,7 +333,6 @@ _from: address, _to: address, _token_id: uint256, - _value: value, ) -> Result { // TODO: Not implemetable Err("not implemented".into()) @@ -350,7 +348,6 @@ /// @param from The current owner of the NFT /// @param to The new owner /// @param tokenId The NFT to transfer - /// @param _value Not used for an NFT #[weight(>::transfer_from_creating_removing())] fn transfer_from( &mut self, @@ -358,7 +355,6 @@ from: address, to: address, token_id: uint256, - _value: value, ) -> Result { let caller = T::CrossAccountId::from_eth(caller); let from = T::CrossAccountId::from_eth(from); @@ -378,13 +374,7 @@ } /// @dev Not implemented - fn approve( - &mut self, - _caller: caller, - _approved: address, - _token_id: uint256, - _value: value, - ) -> Result { + fn approve(&mut self, _caller: caller, _approved: address, _token_id: uint256) -> Result { Err("not implemented".into()) } @@ -438,7 +428,7 @@ } /// @title ERC721 Token that can be irreversibly burned (destroyed). -#[solidity_interface(name = "ERC721Burnable")] +#[solidity_interface(name = ERC721Burnable)] impl RefungibleHandle { /// @notice Burns a specific ERC721 token. /// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized @@ -458,7 +448,7 @@ } /// @title ERC721 minting logic. -#[solidity_interface(name = "ERC721Mintable", events(ERC721MintableEvents))] +#[solidity_interface(name = ERC721Mintable, events(ERC721MintableEvents))] impl RefungibleHandle { fn minting_finished(&self) -> Result { Ok(false) @@ -616,7 +606,7 @@ } /// @title Unique extensions for ERC721. -#[solidity_interface(name = "ERC721UniqueExtensions")] +#[solidity_interface(name = ERC721UniqueExtensions)] impl RefungibleHandle { /// @notice Transfer ownership of an RFT /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` @@ -624,15 +614,8 @@ /// Throws if RFT pieces have multiple owners. /// @param to The new owner /// @param tokenId The RFT to transfer - /// @param _value Not used for an RFT #[weight(>::transfer_creating_removing())] - fn transfer( - &mut self, - caller: caller, - to: address, - token_id: uint256, - _value: value, - ) -> Result { + fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result { let caller = T::CrossAccountId::from_eth(caller); let to = T::CrossAccountId::from_eth(to); let token = token_id.try_into()?; @@ -655,15 +638,8 @@ /// Throws if RFT pieces have multiple owners. /// @param from The current owner of the RFT /// @param tokenId The RFT to transfer - /// @param _value Not used for an RFT #[weight(>::burn_from())] - fn burn_from( - &mut self, - caller: caller, - from: address, - token_id: uint256, - _value: value, - ) -> Result { + fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result { let caller = T::CrossAccountId::from_eth(caller); let from = T::CrossAccountId::from_eth(from); let token = token_id.try_into()?; @@ -801,7 +777,7 @@ } #[solidity_interface( - name = "UniqueRefungible", + name = UniqueRefungible, is( ERC721, ERC721Metadata, @@ -809,11 +785,11 @@ ERC721UniqueExtensions, ERC721Mintable, ERC721Burnable, - via("CollectionHandle", common_mut, Collection), + Collection(common_mut, CollectionHandle), TokenProperties, ) )] -impl RefungibleHandle where T::AccountId: From<[u8; 32]> {} +impl RefungibleHandle where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {} // Not a tests, but code generators generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true); @@ -821,7 +797,7 @@ impl CommonEvmHandler for RefungibleHandle where - T::AccountId: From<[u8; 32]>, + T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>, { const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw"); fn call( --- a/pallets/refungible/src/erc_token.rs +++ b/pallets/refungible/src/erc_token.rs @@ -29,86 +29,36 @@ convert::TryInto, ops::Deref, }; -use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight}; +use evm_coder::{ToLog, execution::*, generate_stubgen, solidity_interface, types::*, weight}; use pallet_common::{ CommonWeightInfo, - erc::{CommonEvmHandler, PrecompileResult, static_property::key}, - eth::map_eth_to_id, + erc::{CommonEvmHandler, PrecompileResult}, + eth::collection_id_to_address, }; use pallet_evm::{account::CrossAccountId, PrecompileHandle}; use pallet_evm_coder_substrate::{call, dispatch_to_evm, WithRecorder}; use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _}; -use sp_core::H160; use sp_std::vec::Vec; -use up_data_structs::{mapping::TokenAddressMapping, PropertyScope, TokenId}; +use up_data_structs::TokenId; use crate::{ Allowance, Balance, common::CommonWeights, Config, Pallet, RefungibleHandle, SelfWeightOf, - TokenProperties, TotalSupply, weights::WeightInfo, + TotalSupply, weights::WeightInfo, }; pub struct RefungibleTokenHandle(pub RefungibleHandle, pub TokenId); -#[solidity_interface(name = "ERC1633")] +#[solidity_interface(name = ERC1633)] impl RefungibleTokenHandle { fn parent_token(&self) -> Result
{ - self.consume_store_reads(2)?; - let props = >::get((self.id, self.1)); - let key = key::parent_nft(); - - let key_scoped = PropertyScope::Eth - .apply(key) - .expect("property key shouldn't exceed length limit"); - if let Some(value) = props.get(&key_scoped) { - Ok(H160::from_slice(value.as_slice())) - } else { - Ok(*T::CrossTokenAddressMapping::token_to_address(self.id, self.1).as_eth()) - } + Ok(collection_id_to_address(self.id)) } fn parent_token_id(&self) -> Result { - self.consume_store_reads(2)?; - let props = >::get((self.id, self.1)); - let key = key::parent_nft(); - - let key_scoped = PropertyScope::Eth - .apply(key) - .expect("property key shouldn't exceed length limit"); - if let Some(value) = props.get(&key_scoped) { - let nft_token_address = H160::from_slice(value.as_slice()); - let nft_token_account = T::CrossAccountId::from_eth(nft_token_address); - let (_, token_id) = T::CrossTokenAddressMapping::address_to_token(&nft_token_account) - .ok_or("parent NFT should contain NFT token address")?; - - Ok(token_id.into()) - } else { - Ok(self.1.into()) - } + Ok(self.1.into()) } } -#[solidity_interface(name = "ERC1633UniqueExtensions")] -impl RefungibleTokenHandle { - #[solidity(rename_selector = "setParentNFT")] - #[weight(>::token_owner() + >::set_parent_nft_unchecked())] - fn set_parent_nft( - &mut self, - caller: caller, - collection: address, - nft_id: uint256, - ) -> Result { - self.consume_store_reads(1)?; - let caller = T::CrossAccountId::from_eth(caller); - let nft_collection = map_eth_to_id(&collection).ok_or("collection not found")?; - let nft_token = nft_id.try_into()?; - - >::set_parent_nft(&self.0, self.1, caller, nft_collection, nft_token) - .map_err(dispatch_to_evm::)?; - - Ok(true) - } -} - #[derive(ToLog)] pub enum ERC20Events { /// @dev This event is emitted when the amount of tokens (value) is sent @@ -137,7 +87,7 @@ /// /// @dev Implementation of the basic standard token. /// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md -#[solidity_interface(name = "ERC20", events(ERC20Events))] +#[solidity_interface(name = ERC20, events(ERC20Events))] impl RefungibleTokenHandle { /// @return the name of the token. fn name(&self) -> Result { @@ -246,7 +196,7 @@ } } -#[solidity_interface(name = "ERC20UniqueExtensions")] +#[solidity_interface(name = ERC20UniqueExtensions)] impl RefungibleTokenHandle { /// @dev Function that burns an amount of the token of a given account, /// deducting from the sender's allowance for said account. @@ -306,8 +256,8 @@ } #[solidity_interface( - name = "UniqueRefungibleToken", - is(ERC20, ERC20UniqueExtensions, ERC1633, ERC1633UniqueExtensions) + name = UniqueRefungibleToken, + is(ERC20, ERC20UniqueExtensions, ERC1633) )] impl RefungibleTokenHandle where T::AccountId: From<[u8; 32]> {} --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -1379,68 +1379,4 @@ Some(res) } } - - /// Sets the NFT token as a parent for the RFT token - /// - /// Throws if `sender` is not the owner of the NFT token. - /// Throws if `sender` is not the owner of all of the RFT token pieces. - pub fn set_parent_nft( - collection: &RefungibleHandle, - rft_token_id: TokenId, - sender: T::CrossAccountId, - nft_collection: CollectionId, - nft_token: TokenId, - ) -> DispatchResult { - let handle = >::try_get(nft_collection)?; - if handle.mode != CollectionMode::NFT { - return Err("Only NFT token could be parent to RFT".into()); - } - let dispatch = T::CollectionDispatch::dispatch(handle); - let dispatch = dispatch.as_dyn(); - - let owner = dispatch.token_owner(nft_token).ok_or("owner not found")?; - if owner != sender { - return Err("Only owned token could be set as parent".into()); - } - - let nft_token_address = - T::CrossTokenAddressMapping::token_to_address(nft_collection, nft_token); - - Self::set_parent_nft_unchecked(collection, rft_token_id, sender, nft_token_address) - } - - /// Sets the NFT token as a parent for the RFT token - /// - /// `sender` should be the owner of the NFT token. - /// Throws if `sender` is not the owner of all of the RFT token pieces. - pub fn set_parent_nft_unchecked( - collection: &RefungibleHandle, - rft_token_id: TokenId, - sender: T::CrossAccountId, - nft_token_address: T::CrossAccountId, - ) -> DispatchResult { - let owner_balance = >::get((collection.id, rft_token_id, &sender)); - let total_supply = >::get((collection.id, rft_token_id)); - if total_supply != owner_balance { - return Err("token has multiple owners".into()); - } - - let parent_nft_property_key = key::parent_nft(); - - let parent_nft_property_value = - property_value_from_bytes(&nft_token_address.as_eth().to_fixed_bytes()) - .expect("address should fit in value length limit"); - - >::set_scoped_token_property( - collection.id, - rft_token_id, - PropertyScope::Eth, - Property { - key: parent_nft_property_key, - value: parent_nft_property_value, - }, - )?; - - Ok(()) - } } --- a/pallets/refungible/src/stubs/UniqueRefungible.sol +++ b/pallets/refungible/src/stubs/UniqueRefungible.sol @@ -3,13 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; -// Anonymous struct -struct Tuple0 { - uint256 field_0; - string field_1; -} - -// Common stubs holder +/// @dev common stubs holder contract Dummy { uint8 dummy; string stub_error = "this contract is implemented in native"; @@ -27,40 +21,17 @@ } } -// Inline -contract ERC721Events { - event Transfer( - address indexed from, - address indexed to, - uint256 indexed tokenId - ); - event Approval( - address indexed owner, - address indexed approved, - uint256 indexed tokenId - ); - event ApprovalForAll( - address indexed owner, - address indexed operator, - bool approved - ); -} - -// Inline -contract ERC721MintableEvents { - event MintingFinished(); -} - -// Selector: 41369377 +/// @title A contract that allows to set and delete token properties and change token property permissions. +/// @dev the ERC-165 identifier for this interface is 0x41369377 contract TokenProperties is Dummy, ERC165 { - // @notice Set permissions for token property. - // @dev Throws error if `msg.sender` is not admin or owner of the collection. - // @param key Property key. - // @param is_mutable Permission to mutate property. - // @param collection_admin Permission to mutate property by collection admin if property is mutable. - // @param token_owner Permission to mutate property by token owner if property is mutable. - // - // Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa + /// @notice Set permissions for token property. + /// @dev Throws error if `msg.sender` is not admin or owner of the collection. + /// @param key Property key. + /// @param isMutable Permission to mutate property. + /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable. + /// @param tokenOwner Permission to mutate property by token owner if property is mutable. + /// @dev EVM selector for this function is: 0x222d97fa, + /// or in textual repr: setTokenPropertyPermission(string,bool,bool,bool) function setTokenPropertyPermission( string memory key, bool isMutable, @@ -75,13 +46,13 @@ dummy = 0; } - // @notice Set token property value. - // @dev Throws error if `msg.sender` has no permission to edit the property. - // @param tokenId ID of the token. - // @param key Property key. - // @param value Property value. - // - // Selector: setProperty(uint256,string,bytes) 1752d67b + /// @notice Set token property value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param key Property key. + /// @param value Property value. + /// @dev EVM selector for this function is: 0x1752d67b, + /// or in textual repr: setProperty(uint256,string,bytes) function setProperty( uint256 tokenId, string memory key, @@ -94,12 +65,12 @@ dummy = 0; } - // @notice Delete token property value. - // @dev Throws error if `msg.sender` has no permission to edit the property. - // @param tokenId ID of the token. - // @param key Property key. - // - // Selector: deleteProperty(uint256,string) 066111d1 + /// @notice Delete token property value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param key Property key. + /// @dev EVM selector for this function is: 0x066111d1, + /// or in textual repr: deleteProperty(uint256,string) function deleteProperty(uint256 tokenId, string memory key) public { require(false, stub_error); tokenId; @@ -107,13 +78,13 @@ dummy = 0; } - // @notice Get token property value. - // @dev Throws error if key not found - // @param tokenId ID of the token. - // @param key Property key. - // @return Property value bytes - // - // Selector: property(uint256,string) 7228c327 + /// @notice Get token property value. + /// @dev Throws error if key not found + /// @param tokenId ID of the token. + /// @param key Property key. + /// @return Property value bytes + /// @dev EVM selector for this function is: 0x7228c327, + /// or in textual repr: property(uint256,string) function property(uint256 tokenId, string memory key) public view @@ -127,509 +98,527 @@ } } -// Selector: 42966c68 -contract ERC721Burnable is Dummy, ERC165 { - // @notice Burns a specific ERC721 token. - // @dev Throws unless `msg.sender` is the current RFT owner, or an authorized - // operator of the current owner. - // @param tokenId The RFT to approve - // - // Selector: burn(uint256) 42966c68 - function burn(uint256 tokenId) public { +/// @title A contract that allows you to work with collections. +/// @dev the ERC-165 identifier for this interface is 0xe54be640 +contract Collection is Dummy, ERC165 { + /// Set collection property. + /// + /// @param key Property key. + /// @param value Propery value. + /// @dev EVM selector for this function is: 0x2f073f66, + /// or in textual repr: setCollectionProperty(string,bytes) + function setCollectionProperty(string memory key, bytes memory value) + public + { require(false, stub_error); - tokenId; + key; + value; dummy = 0; } -} -// Selector: 58800161 -contract ERC721 is Dummy, ERC165, ERC721Events { - // @notice Count all RFTs assigned to an owner - // @dev RFTs assigned to the zero address are considered invalid, and this - // function throws for queries about the zero address. - // @param owner An address for whom to query the balance - // @return The number of RFTs owned by `owner`, possibly zero - // - // Selector: balanceOf(address) 70a08231 - function balanceOf(address owner) public view returns (uint256) { + /// Delete collection property. + /// + /// @param key Property key. + /// @dev EVM selector for this function is: 0x7b7debce, + /// or in textual repr: deleteCollectionProperty(string) + function deleteCollectionProperty(string memory key) public { require(false, stub_error); - owner; - dummy; - return 0; + key; + dummy = 0; } - // @notice Find the owner of an RFT - // @dev RFTs assigned to zero address are considered invalid, and queries - // about them do throw. - // Returns special 0xffffffffffffffffffffffffffffffffffffffff address for - // the tokens that are partially owned. - // @param tokenId The identifier for an RFT - // @return The address of the owner of the RFT - // - // Selector: ownerOf(uint256) 6352211e - function ownerOf(uint256 tokenId) public view returns (address) { + /// Get collection property. + /// + /// @dev Throws error if key not found. + /// + /// @param key Property key. + /// @return bytes The property corresponding to the key. + /// @dev EVM selector for this function is: 0xcf24fd6d, + /// or in textual repr: collectionProperty(string) + function collectionProperty(string memory key) + public + view + returns (bytes memory) + { require(false, stub_error); - tokenId; + key; dummy; - return 0x0000000000000000000000000000000000000000; + return hex""; } - // @dev Not implemented - // - // Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672 - function safeTransferFromWithData( - address from, - address to, - uint256 tokenId, - bytes memory data - ) public { + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0x7623402e, + /// or in textual repr: setCollectionSponsor(address) + function setCollectionSponsor(address sponsor) public { require(false, stub_error); - from; - to; - tokenId; - data; + sponsor; dummy = 0; } - // @dev Not implemented - // - // Selector: safeTransferFrom(address,address,uint256) 42842e0e - function safeTransferFrom( - address from, - address to, - uint256 tokenId - ) public { + /// Set the substrate sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Substrate address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0xc74d6751, + /// or in textual repr: setCollectionSponsorSubstrate(uint256) + function setCollectionSponsorSubstrate(uint256 sponsor) public { require(false, stub_error); - from; - to; - tokenId; + sponsor; dummy = 0; } - // @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE - // TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE - // THEY MAY BE PERMANENTLY LOST - // @dev Throws unless `msg.sender` is the current owner or an authorized - // operator for this RFT. Throws if `from` is not the current owner. Throws - // if `to` is the zero address. Throws if `tokenId` is not a valid RFT. - // Throws if RFT pieces have multiple owners. - // @param from The current owner of the NFT - // @param to The new owner - // @param tokenId The NFT to transfer - // @param _value Not used for an NFT - // - // Selector: transferFrom(address,address,uint256) 23b872dd - function transferFrom( - address from, - address to, - uint256 tokenId - ) public { + /// @dev EVM selector for this function is: 0x058ac185, + /// or in textual repr: hasCollectionPendingSponsor() + function hasCollectionPendingSponsor() public view returns (bool) { require(false, stub_error); - from; - to; - tokenId; - dummy = 0; + dummy; + return false; } - // @dev Not implemented - // - // Selector: approve(address,uint256) 095ea7b3 - function approve(address approved, uint256 tokenId) public { + /// Collection sponsorship confirmation. + /// + /// @dev After setting the sponsor for the collection, it must be confirmed with this function. + /// @dev EVM selector for this function is: 0x3c50e97a, + /// or in textual repr: confirmCollectionSponsorship() + function confirmCollectionSponsorship() public { require(false, stub_error); - approved; - tokenId; dummy = 0; } - // @dev Not implemented - // - // Selector: setApprovalForAll(address,bool) a22cb465 - function setApprovalForAll(address operator, bool approved) public { + /// Remove collection sponsor. + /// @dev EVM selector for this function is: 0x6e0326a3, + /// or in textual repr: removeCollectionSponsor() + function removeCollectionSponsor() public { require(false, stub_error); - operator; - approved; dummy = 0; } - // @dev Not implemented - // - // Selector: getApproved(uint256) 081812fc - function getApproved(uint256 tokenId) public view returns (address) { + /// Get current sponsor. + /// + /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + /// @dev EVM selector for this function is: 0xb66bbc14, + /// or in textual repr: getCollectionSponsor() + function getCollectionSponsor() public view returns (Tuple17 memory) { require(false, stub_error); - tokenId; dummy; - return 0x0000000000000000000000000000000000000000; + return Tuple17(0x0000000000000000000000000000000000000000, 0); } - // @dev Not implemented - // - // Selector: isApprovedForAll(address,address) e985e9c5 - function isApprovedForAll(address owner, address operator) - public - view - returns (address) - { + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "accountTokenOwnershipLimit", + /// "sponsoredDataSize", + /// "sponsoredDataRateLimit", + /// "tokenLimit", + /// "sponsorTransferTimeout", + /// "sponsorApproveTimeout" + /// @param value Value of the limit. + /// @dev EVM selector for this function is: 0x6a3841db, + /// or in textual repr: setCollectionLimit(string,uint32) + function setCollectionLimit(string memory limit, uint32 value) public { require(false, stub_error); - owner; - operator; - dummy; - return 0x0000000000000000000000000000000000000000; + limit; + value; + dummy = 0; } -} -// Selector: 5b5e139f -contract ERC721Metadata is Dummy, ERC165 { - // @notice A descriptive name for a collection of RFTs in this contract - // - // Selector: name() 06fdde03 - function name() public view returns (string memory) { + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "ownerCanTransfer", + /// "ownerCanDestroy", + /// "transfersEnabled" + /// @param value Value of the limit. + /// @dev EVM selector for this function is: 0x993b7fba, + /// or in textual repr: setCollectionLimit(string,bool) + function setCollectionLimit(string memory limit, bool value) public { require(false, stub_error); - dummy; - return ""; + limit; + value; + dummy = 0; } - // @notice An abbreviated name for RFTs in this contract - // - // Selector: symbol() 95d89b41 - function symbol() public view returns (string memory) { + /// Get contract address. + /// @dev EVM selector for this function is: 0xf6b4dfb4, + /// or in textual repr: contractAddress() + function contractAddress() public view returns (address) { require(false, stub_error); dummy; - return ""; + return 0x0000000000000000000000000000000000000000; } - // @notice A distinct Uniform Resource Identifier (URI) for a given asset. - // - // @dev If the token has a `url` property and it is not empty, it is returned. - // Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`. - // If the collection property `baseURI` is empty or absent, return "" (empty string) - // otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix - // otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings). - // - // @return token's const_metadata - // - // Selector: tokenURI(uint256) c87b56dd - function tokenURI(uint256 tokenId) public view returns (string memory) { + /// Add collection admin by substrate address. + /// @param newAdmin Substrate administrator address. + /// @dev EVM selector for this function is: 0x5730062b, + /// or in textual repr: addCollectionAdminSubstrate(uint256) + function addCollectionAdminSubstrate(uint256 newAdmin) public { require(false, stub_error); - tokenId; - dummy; - return ""; + newAdmin; + dummy = 0; } -} -// Selector: 68ccfe89 -contract ERC721Mintable is Dummy, ERC165, ERC721MintableEvents { - // Selector: mintingFinished() 05d2035b - function mintingFinished() public view returns (bool) { + /// Remove collection admin by substrate address. + /// @param admin Substrate administrator address. + /// @dev EVM selector for this function is: 0x4048fcf9, + /// or in textual repr: removeCollectionAdminSubstrate(uint256) + function removeCollectionAdminSubstrate(uint256 admin) public { require(false, stub_error); - dummy; - return false; + admin; + dummy = 0; } - // @notice Function to mint token. - // @dev `tokenId` should be obtained with `nextTokenId` method, - // unlike standard, you can't specify it manually - // @param to The new owner - // @param tokenId ID of the minted RFT - // - // Selector: mint(address,uint256) 40c10f19 - function mint(address to, uint256 tokenId) public returns (bool) { + /// Add collection admin. + /// @param newAdmin Address of the added administrator. + /// @dev EVM selector for this function is: 0x92e462c7, + /// or in textual repr: addCollectionAdmin(address) + function addCollectionAdmin(address newAdmin) public { require(false, stub_error); - to; - tokenId; + newAdmin; dummy = 0; - return false; } - // @notice Function to mint token with the given tokenUri. - // @dev `tokenId` should be obtained with `nextTokenId` method, - // unlike standard, you can't specify it manually - // @param to The new owner - // @param tokenId ID of the minted RFT - // @param tokenUri Token URI that would be stored in the RFT properties - // - // Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f - function mintWithTokenURI( - address to, - uint256 tokenId, - string memory tokenUri - ) public returns (bool) { + /// Remove collection admin. + /// + /// @param admin Address of the removed administrator. + /// @dev EVM selector for this function is: 0xfafd7b42, + /// or in textual repr: removeCollectionAdmin(address) + function removeCollectionAdmin(address admin) public { require(false, stub_error); - to; - tokenId; - tokenUri; + admin; dummy = 0; - return false; } - // @dev Not implemented - // - // Selector: finishMinting() 7d64bcb4 - function finishMinting() public returns (bool) { + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' + /// @dev EVM selector for this function is: 0x112d4586, + /// or in textual repr: setCollectionNesting(bool) + function setCollectionNesting(bool enable) public { require(false, stub_error); + enable; dummy = 0; - return false; } -} -// Selector: 6cf113cd -contract Collection is Dummy, ERC165 { - // Set collection property. - // - // @param key Property key. - // @param value Propery value. - // - // Selector: setCollectionProperty(string,bytes) 2f073f66 - function setCollectionProperty(string memory key, bytes memory value) + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + /// @param collections Addresses of collections that will be available for nesting. + /// @dev EVM selector for this function is: 0x64872396, + /// or in textual repr: setCollectionNesting(bool,address[]) + function setCollectionNesting(bool enable, address[] memory collections) public { require(false, stub_error); - key; - value; + enable; + collections; dummy = 0; } - // Delete collection property. - // - // @param key Property key. - // - // Selector: deleteCollectionProperty(string) 7b7debce - function deleteCollectionProperty(string memory key) public { + /// Set the collection access method. + /// @param mode Access mode + /// 0 for Normal + /// 1 for AllowList + /// @dev EVM selector for this function is: 0x41835d4c, + /// or in textual repr: setCollectionAccess(uint8) + function setCollectionAccess(uint8 mode) public { require(false, stub_error); - key; + mode; dummy = 0; } - // Get collection property. - // - // @dev Throws error if key not found. - // - // @param key Property key. - // @return bytes The property corresponding to the key. - // - // Selector: collectionProperty(string) cf24fd6d - function collectionProperty(string memory key) - public - view - returns (bytes memory) - { + /// Add the user to the allowed list. + /// + /// @param user Address of a trusted user. + /// @dev EVM selector for this function is: 0x67844fe6, + /// or in textual repr: addToCollectionAllowList(address) + function addToCollectionAllowList(address user) public { require(false, stub_error); - key; - dummy; - return hex""; - } - - // Set the sponsor of the collection. - // - // @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. - // - // @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. - // - // Selector: setCollectionSponsor(address) 7623402e - function setCollectionSponsor(address sponsor) public { - require(false, stub_error); - sponsor; + user; dummy = 0; } - // Collection sponsorship confirmation. - // - // @dev After setting the sponsor for the collection, it must be confirmed with this function. - // - // Selector: confirmCollectionSponsorship() 3c50e97a - function confirmCollectionSponsorship() public { + /// Remove the user from the allowed list. + /// + /// @param user Address of a removed user. + /// @dev EVM selector for this function is: 0x85c51acb, + /// or in textual repr: removeFromCollectionAllowList(address) + function removeFromCollectionAllowList(address user) public { require(false, stub_error); + user; dummy = 0; } - // Set limits for the collection. - // @dev Throws error if limit not found. - // @param limit Name of the limit. Valid names: - // "accountTokenOwnershipLimit", - // "sponsoredDataSize", - // "sponsoredDataRateLimit", - // "tokenLimit", - // "sponsorTransferTimeout", - // "sponsorApproveTimeout" - // @param value Value of the limit. - // - // Selector: setCollectionLimit(string,uint32) 6a3841db - function setCollectionLimit(string memory limit, uint32 value) public { + /// Switch permission for minting. + /// + /// @param mode Enable if "true". + /// @dev EVM selector for this function is: 0x00018e84, + /// or in textual repr: setCollectionMintMode(bool) + function setCollectionMintMode(bool mode) public { require(false, stub_error); - limit; - value; + mode; dummy = 0; } - // Set limits for the collection. - // @dev Throws error if limit not found. - // @param limit Name of the limit. Valid names: - // "ownerCanTransfer", - // "ownerCanDestroy", - // "transfersEnabled" - // @param value Value of the limit. - // - // Selector: setCollectionLimit(string,bool) 993b7fba - function setCollectionLimit(string memory limit, bool value) public { + /// Check that account is the owner or admin of the collection + /// + /// @param user account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x9811b0c7, + /// or in textual repr: isOwnerOrAdmin(address) + function isOwnerOrAdmin(address user) public view returns (bool) { require(false, stub_error); - limit; - value; - dummy = 0; + user; + dummy; + return false; } - // Get contract address. - // - // Selector: contractAddress() f6b4dfb4 - function contractAddress() public view returns (address) { + /// Check that substrate account is the owner or admin of the collection + /// + /// @param user account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x68910e00, + /// or in textual repr: isOwnerOrAdminSubstrate(uint256) + function isOwnerOrAdminSubstrate(uint256 user) public view returns (bool) { require(false, stub_error); + user; dummy; - return 0x0000000000000000000000000000000000000000; + return false; } - // Add collection admin by substrate address. - // @param new_admin Substrate administrator address. - // - // Selector: addCollectionAdminSubstrate(uint256) 5730062b - function addCollectionAdminSubstrate(uint256 newAdmin) public { + /// Returns collection type + /// + /// @return `Fungible` or `NFT` or `ReFungible` + /// @dev EVM selector for this function is: 0xd34b55b8, + /// or in textual repr: uniqueCollectionType() + function uniqueCollectionType() public returns (string memory) { require(false, stub_error); - newAdmin; dummy = 0; + return ""; } - // Remove collection admin by substrate address. - // @param admin Substrate administrator address. - // - // Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9 - function removeCollectionAdminSubstrate(uint256 admin) public { + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner account + /// @dev EVM selector for this function is: 0x13af4035, + /// or in textual repr: setOwner(address) + function setOwner(address newOwner) public { require(false, stub_error); - admin; + newOwner; dummy = 0; } - // Add collection admin. - // @param new_admin Address of the added administrator. - // - // Selector: addCollectionAdmin(address) 92e462c7 - function addCollectionAdmin(address newAdmin) public { + /// Changes collection owner to another substrate account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner substrate account + /// @dev EVM selector for this function is: 0xb212138f, + /// or in textual repr: setOwnerSubstrate(uint256) + function setOwnerSubstrate(uint256 newOwner) public { require(false, stub_error); - newAdmin; + newOwner; dummy = 0; } +} - // Remove collection admin. - // - // @param new_admin Address of the removed administrator. - // - // Selector: removeCollectionAdmin(address) fafd7b42 - function removeCollectionAdmin(address admin) public { +/// @dev anonymous struct +struct Tuple17 { + address field_0; + uint256 field_1; +} + +/// @title ERC721 Token that can be irreversibly burned (destroyed). +/// @dev the ERC-165 identifier for this interface is 0x42966c68 +contract ERC721Burnable is Dummy, ERC165 { + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized + /// operator of the current owner. + /// @param tokenId The RFT to approve + /// @dev EVM selector for this function is: 0x42966c68, + /// or in textual repr: burn(uint256) + function burn(uint256 tokenId) public { require(false, stub_error); - admin; + tokenId; dummy = 0; } +} - // Toggle accessibility of collection nesting. - // - // @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' - // - // Selector: setCollectionNesting(bool) 112d4586 - function setCollectionNesting(bool enable) public { +/// @dev inlined interface +contract ERC721MintableEvents { + event MintingFinished(); +} + +/// @title ERC721 minting logic. +/// @dev the ERC-165 identifier for this interface is 0x68ccfe89 +contract ERC721Mintable is Dummy, ERC165, ERC721MintableEvents { + /// @dev EVM selector for this function is: 0x05d2035b, + /// or in textual repr: mintingFinished() + function mintingFinished() public view returns (bool) { require(false, stub_error); - enable; - dummy = 0; + dummy; + return false; } - // Toggle accessibility of collection nesting. - // - // @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' - // @param collections Addresses of collections that will be available for nesting. - // - // Selector: setCollectionNesting(bool,address[]) 64872396 - function setCollectionNesting(bool enable, address[] memory collections) - public - { + /// @notice Function to mint token. + /// @dev `tokenId` should be obtained with `nextTokenId` method, + /// unlike standard, you can't specify it manually + /// @param to The new owner + /// @param tokenId ID of the minted RFT + /// @dev EVM selector for this function is: 0x40c10f19, + /// or in textual repr: mint(address,uint256) + function mint(address to, uint256 tokenId) public returns (bool) { require(false, stub_error); - enable; - collections; + to; + tokenId; dummy = 0; + return false; } - // Set the collection access method. - // @param mode Access mode - // 0 for Normal - // 1 for AllowList - // - // Selector: setCollectionAccess(uint8) 41835d4c - function setCollectionAccess(uint8 mode) public { + /// @notice Function to mint token with the given tokenUri. + /// @dev `tokenId` should be obtained with `nextTokenId` method, + /// unlike standard, you can't specify it manually + /// @param to The new owner + /// @param tokenId ID of the minted RFT + /// @param tokenUri Token URI that would be stored in the RFT properties + /// @dev EVM selector for this function is: 0x50bb4e7f, + /// or in textual repr: mintWithTokenURI(address,uint256,string) + function mintWithTokenURI( + address to, + uint256 tokenId, + string memory tokenUri + ) public returns (bool) { require(false, stub_error); - mode; + to; + tokenId; + tokenUri; dummy = 0; + return false; } - // Add the user to the allowed list. - // - // @param user Address of a trusted user. - // - // Selector: addToCollectionAllowList(address) 67844fe6 - function addToCollectionAllowList(address user) public { + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x7d64bcb4, + /// or in textual repr: finishMinting() + function finishMinting() public returns (bool) { require(false, stub_error); - user; dummy = 0; + return false; } +} - // Remove the user from the allowed list. - // - // @param user Address of a removed user. - // - // Selector: removeFromCollectionAllowList(address) 85c51acb - function removeFromCollectionAllowList(address user) public { +/// @title Unique extensions for ERC721. +/// @dev the ERC-165 identifier for this interface is 0x7c3bef89 +contract ERC721UniqueExtensions is Dummy, ERC165 { + /// @notice Transfer ownership of an RFT + /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` + /// is the zero address. Throws if `tokenId` is not a valid RFT. + /// Throws if RFT pieces have multiple owners. + /// @param to The new owner + /// @param tokenId The RFT to transfer + /// @dev EVM selector for this function is: 0xa9059cbb, + /// or in textual repr: transfer(address,uint256) + function transfer(address to, uint256 tokenId) public { require(false, stub_error); - user; + to; + tokenId; dummy = 0; } - // Switch permission for minting. - // - // @param mode Enable if "true". - // - // Selector: setCollectionMintMode(bool) 00018e84 - function setCollectionMintMode(bool mode) public { + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this RFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT. + /// Throws if RFT pieces have multiple owners. + /// @param from The current owner of the RFT + /// @param tokenId The RFT to transfer + /// @dev EVM selector for this function is: 0x79cc6790, + /// or in textual repr: burnFrom(address,uint256) + function burnFrom(address from, uint256 tokenId) public { require(false, stub_error); - mode; + from; + tokenId; dummy = 0; } - // Check that account is the owner or admin of the collection - // - // @param user account to verify - // @return "true" if account is the owner or admin - // - // Selector: verifyOwnerOrAdmin(address) c2282493 - function verifyOwnerOrAdmin(address user) public view returns (bool) { + /// @notice Returns next free RFT ID. + /// @dev EVM selector for this function is: 0x75794a3c, + /// or in textual repr: nextTokenId() + function nextTokenId() public view returns (uint256) { require(false, stub_error); - user; dummy; + return 0; + } + + /// @notice Function to mint multiple tokens. + /// @dev `tokenIds` should be an array of consecutive numbers and first number + /// should be obtained with `nextTokenId` method + /// @param to The new owner + /// @param tokenIds IDs of the minted RFTs + /// @dev EVM selector for this function is: 0x44a9945e, + /// or in textual repr: mintBulk(address,uint256[]) + function mintBulk(address to, uint256[] memory tokenIds) + public + returns (bool) + { + require(false, stub_error); + to; + tokenIds; + dummy = 0; return false; } - // Returns collection type - // - // @return `Fungible` or `NFT` or `ReFungible` - // - // Selector: uniqueCollectionType() d34b55b8 - function uniqueCollectionType() public returns (string memory) { + /// @notice Function to mint multiple tokens with the given tokenUris. + /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive + /// numbers and first number should be obtained with `nextTokenId` method + /// @param to The new owner + /// @param tokens array of pairs of token ID and token URI for minted tokens + /// @dev EVM selector for this function is: 0x36543006, + /// or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[]) + function mintBulkWithTokenURI(address to, Tuple8[] memory tokens) + public + returns (bool) + { require(false, stub_error); + to; + tokens; dummy = 0; - return ""; + return false; } + + /// Returns EVM address for refungible token + /// + /// @param token ID of the token + /// @dev EVM selector for this function is: 0xab76fac6, + /// or in textual repr: tokenContractAddress(uint256) + function tokenContractAddress(uint256 token) public view returns (address) { + require(false, stub_error); + token; + dummy; + return 0x0000000000000000000000000000000000000000; + } } -// Selector: 780e9d63 +/// @dev anonymous struct +struct Tuple8 { + uint256 field_0; + string field_1; +} + +/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// @dev the ERC-165 identifier for this interface is 0x780e9d63 contract ERC721Enumerable is Dummy, ERC165 { - // @notice Enumerate valid RFTs - // @param index A counter less than `totalSupply()` - // @return The token identifier for the `index`th NFT, - // (sort order not specified) - // - // Selector: tokenByIndex(uint256) 4f6ccce7 + /// @notice Enumerate valid RFTs + /// @param index A counter less than `totalSupply()` + /// @return The token identifier for the `index`th NFT, + /// (sort order not specified) + /// @dev EVM selector for this function is: 0x4f6ccce7, + /// or in textual repr: tokenByIndex(uint256) function tokenByIndex(uint256 index) public view returns (uint256) { require(false, stub_error); index; @@ -637,9 +626,9 @@ return 0; } - // Not implemented - // - // Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59 + /// Not implemented + /// @dev EVM selector for this function is: 0x2f745c59, + /// or in textual repr: tokenOfOwnerByIndex(address,uint256) function tokenOfOwnerByIndex(address owner, uint256 index) public view @@ -652,11 +641,11 @@ return 0; } - // @notice Count RFTs tracked by this contract - // @return A count of valid RFTs tracked by this contract, where each one of - // them has an assigned and queryable owner not equal to the zero address - // - // Selector: totalSupply() 18160ddd + /// @notice Count RFTs tracked by this contract + /// @return A count of valid RFTs tracked by this contract, where each one of + /// them has an assigned and queryable owner not equal to the zero address + /// @dev EVM selector for this function is: 0x18160ddd, + /// or in textual repr: totalSupply() function totalSupply() public view returns (uint256) { require(false, stub_error); dummy; @@ -664,94 +653,195 @@ } } -// Selector: 7c3bef89 -contract ERC721UniqueExtensions is Dummy, ERC165 { - // @notice Transfer ownership of an RFT - // @dev Throws unless `msg.sender` is the current owner. Throws if `to` - // is the zero address. Throws if `tokenId` is not a valid RFT. - // Throws if RFT pieces have multiple owners. - // @param to The new owner - // @param tokenId The RFT to transfer - // @param _value Not used for an RFT - // - // Selector: transfer(address,uint256) a9059cbb - function transfer(address to, uint256 tokenId) public { +/// @dev the ERC-165 identifier for this interface is 0x5b5e139f +contract ERC721Metadata is Dummy, ERC165 { + /// @notice A descriptive name for a collection of RFTs in this contract + /// @dev EVM selector for this function is: 0x06fdde03, + /// or in textual repr: name() + function name() public view returns (string memory) { + require(false, stub_error); + dummy; + return ""; + } + + /// @notice An abbreviated name for RFTs in this contract + /// @dev EVM selector for this function is: 0x95d89b41, + /// or in textual repr: symbol() + function symbol() public view returns (string memory) { require(false, stub_error); + dummy; + return ""; + } + + /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. + /// + /// @dev If the token has a `url` property and it is not empty, it is returned. + /// Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`. + /// If the collection property `baseURI` is empty or absent, return "" (empty string) + /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix + /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings). + /// + /// @return token's const_metadata + /// @dev EVM selector for this function is: 0xc87b56dd, + /// or in textual repr: tokenURI(uint256) + function tokenURI(uint256 tokenId) public view returns (string memory) { + require(false, stub_error); + tokenId; + dummy; + return ""; + } +} + +/// @dev inlined interface +contract ERC721Events { + event Transfer( + address indexed from, + address indexed to, + uint256 indexed tokenId + ); + event Approval( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); +} + +/// @title ERC-721 Non-Fungible Token Standard +/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md +/// @dev the ERC-165 identifier for this interface is 0x58800161 +contract ERC721 is Dummy, ERC165, ERC721Events { + /// @notice Count all RFTs assigned to an owner + /// @dev RFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param owner An address for whom to query the balance + /// @return The number of RFTs owned by `owner`, possibly zero + /// @dev EVM selector for this function is: 0x70a08231, + /// or in textual repr: balanceOf(address) + function balanceOf(address owner) public view returns (uint256) { + require(false, stub_error); + owner; + dummy; + return 0; + } + + /// @notice Find the owner of an RFT + /// @dev RFTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// Returns special 0xffffffffffffffffffffffffffffffffffffffff address for + /// the tokens that are partially owned. + /// @param tokenId The identifier for an RFT + /// @return The address of the owner of the RFT + /// @dev EVM selector for this function is: 0x6352211e, + /// or in textual repr: ownerOf(uint256) + function ownerOf(uint256 tokenId) public view returns (address) { + require(false, stub_error); + tokenId; + dummy; + return 0x0000000000000000000000000000000000000000; + } + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x60a11672, + /// or in textual repr: safeTransferFromWithData(address,address,uint256,bytes) + function safeTransferFromWithData( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public { + require(false, stub_error); + from; to; tokenId; + data; dummy = 0; } - // @notice Burns a specific ERC721 token. - // @dev Throws unless `msg.sender` is the current owner or an authorized - // operator for this RFT. Throws if `from` is not the current owner. Throws - // if `to` is the zero address. Throws if `tokenId` is not a valid RFT. - // Throws if RFT pieces have multiple owners. - // @param from The current owner of the RFT - // @param tokenId The RFT to transfer - // @param _value Not used for an RFT - // - // Selector: burnFrom(address,uint256) 79cc6790 - function burnFrom(address from, uint256 tokenId) public { + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x42842e0e, + /// or in textual repr: safeTransferFrom(address,address,uint256) + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public { require(false, stub_error); from; + to; tokenId; dummy = 0; } - // @notice Returns next free RFT ID. - // - // Selector: nextTokenId() 75794a3c - function nextTokenId() public view returns (uint256) { + /// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE + /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE + /// THEY MAY BE PERMANENTLY LOST + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this RFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT. + /// Throws if RFT pieces have multiple owners. + /// @param from The current owner of the NFT + /// @param to The new owner + /// @param tokenId The NFT to transfer + /// @dev EVM selector for this function is: 0x23b872dd, + /// or in textual repr: transferFrom(address,address,uint256) + function transferFrom( + address from, + address to, + uint256 tokenId + ) public { require(false, stub_error); - dummy; - return 0; + from; + to; + tokenId; + dummy = 0; } - // @notice Function to mint multiple tokens. - // @dev `tokenIds` should be an array of consecutive numbers and first number - // should be obtained with `nextTokenId` method - // @param to The new owner - // @param tokenIds IDs of the minted RFTs - // - // Selector: mintBulk(address,uint256[]) 44a9945e - function mintBulk(address to, uint256[] memory tokenIds) - public - returns (bool) - { + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x095ea7b3, + /// or in textual repr: approve(address,uint256) + function approve(address approved, uint256 tokenId) public { require(false, stub_error); - to; - tokenIds; + approved; + tokenId; dummy = 0; - return false; } - // @notice Function to mint multiple tokens with the given tokenUris. - // @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive - // numbers and first number should be obtained with `nextTokenId` method - // @param to The new owner - // @param tokens array of pairs of token ID and token URI for minted tokens - // - // Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006 - function mintBulkWithTokenURI(address to, Tuple0[] memory tokens) - public - returns (bool) - { + /// @dev Not implemented + /// @dev EVM selector for this function is: 0xa22cb465, + /// or in textual repr: setApprovalForAll(address,bool) + function setApprovalForAll(address operator, bool approved) public { require(false, stub_error); - to; - tokens; + operator; + approved; dummy = 0; - return false; } - // Returns EVM address for refungible token - // - // @param token ID of the token - // - // Selector: tokenContractAddress(uint256) ab76fac6 - function tokenContractAddress(uint256 token) public view returns (address) { + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x081812fc, + /// or in textual repr: getApproved(uint256) + function getApproved(uint256 tokenId) public view returns (address) { require(false, stub_error); - token; + tokenId; + dummy; + return 0x0000000000000000000000000000000000000000; + } + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0xe985e9c5, + /// or in textual repr: isApprovedForAll(address,address) + function isApprovedForAll(address owner, address operator) + public + view + returns (address) + { + require(false, stub_error); + owner; + operator; dummy; return 0x0000000000000000000000000000000000000000; } --- a/pallets/refungible/src/stubs/UniqueRefungibleToken.sol +++ b/pallets/refungible/src/stubs/UniqueRefungibleToken.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; -// Common stubs holder +/// @dev common stubs holder contract Dummy { uint8 dummy; string stub_error = "this contract is implemented in native"; @@ -21,41 +21,18 @@ } } -// Inline -contract ERC20Events { - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval( - address indexed owner, - address indexed spender, - uint256 value - ); -} - -// Selector: 042f1106 -contract ERC1633UniqueExtensions is Dummy, ERC165 { - // Selector: setParentNFT(address,uint256) 042f1106 - function setParentNFT(address collection, uint256 nftId) - public - returns (bool) - { - require(false, stub_error); - collection; - nftId; - dummy = 0; - return false; - } -} - -// Selector: 5755c3f2 +/// @dev the ERC-165 identifier for this interface is 0x5755c3f2 contract ERC1633 is Dummy, ERC165 { - // Selector: parentToken() 80a54001 + /// @dev EVM selector for this function is: 0x80a54001, + /// or in textual repr: parentToken() function parentToken() public view returns (address) { require(false, stub_error); dummy; return 0x0000000000000000000000000000000000000000; } - // Selector: parentTokenId() d7f083f3 + /// @dev EVM selector for this function is: 0xd7f083f3, + /// or in textual repr: parentTokenId() function parentTokenId() public view returns (uint256) { require(false, stub_error); dummy; @@ -63,49 +40,92 @@ } } -// Selector: 942e8b22 +/// @dev the ERC-165 identifier for this interface is 0xab8deb37 +contract ERC20UniqueExtensions is Dummy, ERC165 { + /// @dev Function that burns an amount of the token of a given account, + /// deducting from the sender's allowance for said account. + /// @param from The account whose tokens will be burnt. + /// @param amount The amount that will be burnt. + /// @dev EVM selector for this function is: 0x79cc6790, + /// or in textual repr: burnFrom(address,uint256) + function burnFrom(address from, uint256 amount) public returns (bool) { + require(false, stub_error); + from; + amount; + dummy = 0; + return false; + } + + /// @dev Function that changes total amount of the tokens. + /// Throws if `msg.sender` doesn't owns all of the tokens. + /// @param amount New total amount of the tokens. + /// @dev EVM selector for this function is: 0xd2418ca7, + /// or in textual repr: repartition(uint256) + function repartition(uint256 amount) public returns (bool) { + require(false, stub_error); + amount; + dummy = 0; + return false; + } +} + +/// @dev inlined interface +contract ERC20Events { + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} + +/// @title Standard ERC20 token +/// +/// @dev Implementation of the basic standard token. +/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md +/// @dev the ERC-165 identifier for this interface is 0x942e8b22 contract ERC20 is Dummy, ERC165, ERC20Events { - // @return the name of the token. - // - // Selector: name() 06fdde03 + /// @return the name of the token. + /// @dev EVM selector for this function is: 0x06fdde03, + /// or in textual repr: name() function name() public view returns (string memory) { require(false, stub_error); dummy; return ""; } - // @return the symbol of the token. - // - // Selector: symbol() 95d89b41 + /// @return the symbol of the token. + /// @dev EVM selector for this function is: 0x95d89b41, + /// or in textual repr: symbol() function symbol() public view returns (string memory) { require(false, stub_error); dummy; return ""; } - // @dev Total number of tokens in existence - // - // Selector: totalSupply() 18160ddd + /// @dev Total number of tokens in existence + /// @dev EVM selector for this function is: 0x18160ddd, + /// or in textual repr: totalSupply() function totalSupply() public view returns (uint256) { require(false, stub_error); dummy; return 0; } - // @dev Not supported - // - // Selector: decimals() 313ce567 + /// @dev Not supported + /// @dev EVM selector for this function is: 0x313ce567, + /// or in textual repr: decimals() function decimals() public view returns (uint8) { require(false, stub_error); dummy; return 0; } - // @dev Gets the balance of the specified address. - // @param owner The address to query the balance of. - // @return An uint256 representing the amount owned by the passed address. - // - // Selector: balanceOf(address) 70a08231 + /// @dev Gets the balance of the specified address. + /// @param owner The address to query the balance of. + /// @return An uint256 representing the amount owned by the passed address. + /// @dev EVM selector for this function is: 0x70a08231, + /// or in textual repr: balanceOf(address) function balanceOf(address owner) public view returns (uint256) { require(false, stub_error); owner; @@ -113,11 +133,11 @@ return 0; } - // @dev Transfer token for a specified address - // @param to The address to transfer to. - // @param amount The amount to be transferred. - // - // Selector: transfer(address,uint256) a9059cbb + /// @dev Transfer token for a specified address + /// @param to The address to transfer to. + /// @param amount The amount to be transferred. + /// @dev EVM selector for this function is: 0xa9059cbb, + /// or in textual repr: transfer(address,uint256) function transfer(address to, uint256 amount) public returns (bool) { require(false, stub_error); to; @@ -126,12 +146,12 @@ return false; } - // @dev Transfer tokens from one address to another - // @param from address The address which you want to send tokens from - // @param to address The address which you want to transfer to - // @param amount uint256 the amount of tokens to be transferred - // - // Selector: transferFrom(address,address,uint256) 23b872dd + /// @dev Transfer tokens from one address to another + /// @param from address The address which you want to send tokens from + /// @param to address The address which you want to transfer to + /// @param amount uint256 the amount of tokens to be transferred + /// @dev EVM selector for this function is: 0x23b872dd, + /// or in textual repr: transferFrom(address,address,uint256) function transferFrom( address from, address to, @@ -145,15 +165,15 @@ return false; } - // @dev Approve the passed address to spend the specified amount of tokens on behalf of `msg.sender`. - // Beware that changing an allowance with this method brings the risk that someone may use both the old - // and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this - // race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: - // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - // @param spender The address which will spend the funds. - // @param amount The amount of tokens to be spent. - // - // Selector: approve(address,uint256) 095ea7b3 + /// @dev Approve the passed address to spend the specified amount of tokens on behalf of `msg.sender`. + /// Beware that changing an allowance with this method brings the risk that someone may use both the old + /// and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this + /// race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: + /// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + /// @param spender The address which will spend the funds. + /// @param amount The amount of tokens to be spent. + /// @dev EVM selector for this function is: 0x095ea7b3, + /// or in textual repr: approve(address,uint256) function approve(address spender, uint256 amount) public returns (bool) { require(false, stub_error); spender; @@ -162,12 +182,12 @@ return false; } - // @dev Function to check the amount of tokens that an owner allowed to a spender. - // @param owner address The address which owns the funds. - // @param spender address The address which will spend the funds. - // @return A uint256 specifying the amount of tokens still available for the spender. - // - // Selector: allowance(address,address) dd62ed3e + /// @dev Function to check the amount of tokens that an owner allowed to a spender. + /// @param owner address The address which owns the funds. + /// @param spender address The address which will spend the funds. + /// @return A uint256 specifying the amount of tokens still available for the spender. + /// @dev EVM selector for this function is: 0xdd62ed3e, + /// or in textual repr: allowance(address,address) function allowance(address owner, address spender) public view @@ -181,40 +201,10 @@ } } -// Selector: ab8deb37 -contract ERC20UniqueExtensions is Dummy, ERC165 { - // @dev Function that burns an amount of the token of a given account, - // deducting from the sender's allowance for said account. - // @param from The account whose tokens will be burnt. - // @param amount The amount that will be burnt. - // - // Selector: burnFrom(address,uint256) 79cc6790 - function burnFrom(address from, uint256 amount) public returns (bool) { - require(false, stub_error); - from; - amount; - dummy = 0; - return false; - } - - // @dev Function that changes total amount of the tokens. - // Throws if `msg.sender` doesn't owns all of the tokens. - // @param amount New total amount of the tokens. - // - // Selector: repartition(uint256) d2418ca7 - function repartition(uint256 amount) public returns (bool) { - require(false, stub_error); - amount; - dummy = 0; - return false; - } -} - contract UniqueRefungibleToken is Dummy, ERC165, ERC20, ERC20UniqueExtensions, - ERC1633, - ERC1633UniqueExtensions + ERC1633 {} --- a/pallets/refungible/src/weights.rs +++ b/pallets/refungible/src/weights.rs @@ -53,7 +53,6 @@ fn set_token_properties(b: u32, ) -> Weight; fn delete_token_properties(b: u32, ) -> Weight; fn repartition_item() -> Weight; - fn set_parent_nft_unchecked() -> Weight; fn token_owner() -> Weight; } @@ -254,14 +253,6 @@ (22_356_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) - } - // Storage: Refungible Balance (r:1 w:0) - // Storage: Refungible TotalSupply (r:1 w:0) - // Storage: Refungible TokenProperties (r:1 w:1) - fn set_parent_nft_unchecked() -> Weight { - (12_015_000 as Weight) - .saturating_add(T::DbWeight::get().reads(3 as Weight)) - .saturating_add(T::DbWeight::get().writes(1 as Weight)) } // Storage: Refungible Balance (r:2 w:0) fn token_owner() -> Weight { @@ -466,14 +457,6 @@ (22_356_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) - } - // Storage: Refungible Balance (r:1 w:0) - // Storage: Refungible TotalSupply (r:1 w:0) - // Storage: Refungible TokenProperties (r:1 w:1) - fn set_parent_nft_unchecked() -> Weight { - (12_015_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(3 as Weight)) - .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } // Storage: Refungible Balance (r:2 w:0) fn token_owner() -> Weight { --- a/pallets/unique/src/eth/mod.rs +++ b/pallets/unique/src/eth/mod.rs @@ -154,17 +154,6 @@ Ok(data) } -fn parent_nft_property_permissions() -> PropertyKeyPermission { - PropertyKeyPermission { - key: key::parent_nft(), - permission: PropertyPermission { - mutable: false, - collection_admin: false, - token_owner: true, - }, - } -} - fn create_refungible_collection_internal< T: Config + pallet_nonfungible::Config + pallet_refungible::Config, >( @@ -188,22 +177,12 @@ let collection_id = T::CollectionDispatch::create(caller.clone(), data) .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; - - let handle = >::try_get(collection_id).map_err(dispatch_to_evm::)?; - >::set_scoped_token_property_permissions( - &handle, - &caller, - PropertyScope::Eth, - vec![parent_nft_property_permissions()], - ) - .map_err(dispatch_to_evm::)?; - let address = pallet_common::eth::collection_id_to_address(collection_id); Ok(address) } /// @title Contract, which allows users to operate with collections -#[solidity_interface(name = "CollectionHelpers", events(CollectionHelpersEvents))] +#[solidity_interface(name = CollectionHelpers, events(CollectionHelpersEvents))] impl EvmCollectionHelpers where T: Config + pallet_nonfungible::Config + pallet_refungible::Config, @@ -211,7 +190,7 @@ /// Create an NFT collection /// @param name Name of the collection /// @param description Informative description of the collection - /// @param token_prefix Token prefix to represent the collection tokens in UI and user applications + /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications /// @return address Address of the newly created collection #[weight(>::create_collection())] fn create_nonfungible_collection( @@ -304,7 +283,7 @@ } /// Check if a collection exists - /// @param collection_address Address of the collection in question + /// @param collectionAddress Address of the collection in question /// @return bool Does the collection exist? fn is_collection_exist(&self, _caller: caller, collection_address: address) -> Result { if let Some(id) = pallet_common::eth::map_eth_to_id(&collection_address) { --- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol +++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; -// Common stubs holder +/// @dev common stubs holder contract Dummy { uint8 dummy; string stub_error = "this contract is implemented in native"; @@ -21,7 +21,7 @@ } } -// Inline +/// @dev inlined interface contract CollectionHelpersEvents { event CollectionCreated( address indexed owner, @@ -29,15 +29,16 @@ ); } -// Selector: 675f3074 +/// @title Contract, which allows users to operate with collections +/// @dev the ERC-165 identifier for this interface is 0x675f3074 contract CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents { - // Create an NFT collection - // @param name Name of the collection - // @param description Informative description of the collection - // @param token_prefix Token prefix to represent the collection tokens in UI and user applications - // @return address Address of the newly created collection - // - // Selector: createNonfungibleCollection(string,string,string) e34a6844 + /// Create an NFT collection + /// @param name Name of the collection + /// @param description Informative description of the collection + /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications + /// @return address Address of the newly created collection + /// @dev EVM selector for this function is: 0xe34a6844, + /// or in textual repr: createNonfungibleCollection(string,string,string) function createNonfungibleCollection( string memory name, string memory description, @@ -51,7 +52,8 @@ return 0x0000000000000000000000000000000000000000; } - // Selector: createERC721MetadataCompatibleCollection(string,string,string,string) a634a5f9 + /// @dev EVM selector for this function is: 0xa634a5f9, + /// or in textual repr: createERC721MetadataCompatibleCollection(string,string,string,string) function createERC721MetadataCompatibleCollection( string memory name, string memory description, @@ -67,7 +69,8 @@ return 0x0000000000000000000000000000000000000000; } - // Selector: createRefungibleCollection(string,string,string) 44a68ad5 + /// @dev EVM selector for this function is: 0x44a68ad5, + /// or in textual repr: createRefungibleCollection(string,string,string) function createRefungibleCollection( string memory name, string memory description, @@ -81,7 +84,8 @@ return 0x0000000000000000000000000000000000000000; } - // Selector: createERC721MetadataCompatibleRFTCollection(string,string,string,string) a5596388 + /// @dev EVM selector for this function is: 0xa5596388, + /// or in textual repr: createERC721MetadataCompatibleRFTCollection(string,string,string,string) function createERC721MetadataCompatibleRFTCollection( string memory name, string memory description, @@ -97,11 +101,11 @@ return 0x0000000000000000000000000000000000000000; } - // Check if a collection exists - // @param collection_address Address of the collection in question - // @return bool Does the collection exist? - // - // Selector: isCollectionExist(address) c3de1494 + /// Check if a collection exists + /// @param collectionAddress Address of the collection in question + /// @return bool Does the collection exist? + /// @dev EVM selector for this function is: 0xc3de1494, + /// or in textual repr: isCollectionExist(address) function isCollectionExist(address collectionAddress) public view --- a/primitives/data-structs/src/lib.rs +++ b/primitives/data-structs/src/lib.rs @@ -1050,7 +1050,6 @@ pub enum PropertyScope { None, Rmrk, - Eth, } impl PropertyScope { @@ -1059,7 +1058,6 @@ let scope_str: &[u8] = match self { Self::None => return Ok(key), Self::Rmrk => b"rmrk", - Self::Eth => b"eth", }; [scope_str, b":", key.as_slice()] --- a/runtime/common/config/ethereum.rs +++ b/runtime/common/config/ethereum.rs @@ -120,5 +120,4 @@ impl pallet_evm_transaction_payment::Config for Runtime { type EvmSponsorshipHandler = EvmSponsorshipHandler; - type Currency = Balances; } --- a/runtime/common/dispatch.rs +++ b/runtime/common/dispatch.rs @@ -124,7 +124,7 @@ + pallet_fungible::Config + pallet_nonfungible::Config + pallet_refungible::Config, - T::AccountId: From<[u8; 32]>, + T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>, { fn is_reserved(target: &H160) -> bool { map_eth_to_id(target).is_some() --- a/runtime/tests/src/lib.rs +++ b/runtime/tests/src/lib.rs @@ -167,7 +167,7 @@ } #[derive(Encode, Decode, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, TypeInfo, MaxEncodedLen)] -pub struct TestCrossAccountId(u64, sp_core::H160); +pub struct TestCrossAccountId(u64, sp_core::H160, bool); impl CrossAccountId for TestCrossAccountId { fn as_sub(&self) -> &u64 { &self.0 @@ -178,17 +178,20 @@ fn from_sub(sub: u64) -> Self { let mut eth = [0; 20]; eth[12..20].copy_from_slice(&sub.to_be_bytes()); - Self(sub, sp_core::H160(eth)) + Self(sub, sp_core::H160(eth), true) } fn from_eth(eth: sp_core::H160) -> Self { let mut sub_raw = [0; 8]; sub_raw.copy_from_slice(ð.0[0..8]); let sub = u64::from_be_bytes(sub_raw); - Self(sub, eth) + Self(sub, eth, false) } fn conv_eq(&self, other: &Self) -> bool { self.as_sub() == other.as_sub() } + fn is_canonical_substrate(&self) -> bool { + self.2 + } } impl Default for TestCrossAccountId { --- a/tests/src/addCollectionAdmin.test.ts +++ b/tests/src/addCollectionAdmin.test.ts @@ -14,136 +14,122 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {ApiPromise} from '@polkadot/api'; +import {IKeyringPair} from '@polkadot/types/types'; import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import {default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync} from './substrate/substrate-api'; -import {addCollectionAdminExpectSuccess, createCollectionExpectSuccess, destroyCollectionExpectSuccess, getAdminList, normalizeAccountId, queryCollectionExpectSuccess} from './util/helpers'; +import {usingPlaygrounds} from './util/playgrounds'; chai.use(chaiAsPromised); const expect = chai.expect; +let donor: IKeyringPair; + +before(async () => { + await usingPlaygrounds(async (_, privateKeyWrapper) => { + donor = privateKeyWrapper('//Alice'); + }); +}); + describe('Integration Test addCollectionAdmin(collection_id, new_admin_id):', () => { it('Add collection admin.', async () => { - await usingApi(async (api, privateKeyWrapper) => { - const collectionId = await createCollectionExpectSuccess(); - const alice = privateKeyWrapper('//Alice'); - const bob = privateKeyWrapper('//Bob'); + await usingPlaygrounds(async (helper) => { + const [alice, bob] = await helper.arrange.creteAccounts([10n, 10n], donor); + const {collectionId} = await helper.nft.mintCollection(alice, {name: 'Collection Name', description: 'Collection Description', tokenPrefix: 'COL'}); - const collection = await queryCollectionExpectSuccess(api, collectionId); - expect(collection.owner.toString()).to.be.equal(alice.address); + const collection = await helper.collection.getData(collectionId); + expect(collection!.normalizedOwner!).to.be.equal(alice.address); - const changeAdminTx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(bob.address)); - await submitTransactionAsync(alice, changeAdminTx); + await helper.nft.addAdmin(alice, collectionId, {Substrate: bob.address}); - const adminListAfterAddAdmin = await getAdminList(api, collectionId); - expect(adminListAfterAddAdmin).to.be.deep.contains(normalizeAccountId(bob.address)); + const adminListAfterAddAdmin = await helper.collection.getAdmins(collectionId); + expect(adminListAfterAddAdmin).to.be.deep.contains({Substrate: bob.address}); }); }); }); describe('Negative Integration Test addCollectionAdmin(collection_id, new_admin_id):', () => { it("Not owner can't add collection admin.", async () => { - await usingApi(async (api, privateKeyWrapper) => { - const collectionId = await createCollectionExpectSuccess(); - const alice = privateKeyWrapper('//Alice'); - const bob = privateKeyWrapper('//Bob'); - const charlie = privateKeyWrapper('//CHARLIE'); + await usingPlaygrounds(async (helper) => { + const [alice, bob, charlie] = await helper.arrange.creteAccounts([10n, 10n, 10n], donor); + const {collectionId} = await helper.nft.mintCollection(alice, {name: 'Collection Name', description: 'Collection Description', tokenPrefix: 'COL'}); - const collection = await queryCollectionExpectSuccess(api, collectionId); - expect(collection.owner.toString()).to.be.equal(alice.address); + const collection = await helper.collection.getData(collectionId); + expect(collection?.normalizedOwner).to.be.equal(alice.address); - const adminListAfterAddAdmin = await getAdminList(api, collectionId); - expect(adminListAfterAddAdmin).to.be.not.deep.contains(normalizeAccountId(bob.address)); + const changeAdminTxBob = async () => helper.collection.addAdmin(bob, collectionId, {Substrate: bob.address}); + const changeAdminTxCharlie = async () => helper.collection.addAdmin(bob, collectionId, {Substrate: charlie.address}); + await expect(changeAdminTxCharlie()).to.be.rejected; + await expect(changeAdminTxBob()).to.be.rejected; - const changeAdminTxCharlie = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(charlie.address)); - await expect(submitTransactionAsync(bob, changeAdminTxCharlie)).to.be.rejected; - - const adminListAfterAddNewAdmin = await getAdminList(api, collectionId); - expect(adminListAfterAddNewAdmin).to.be.not.deep.contains(normalizeAccountId(bob.address)); - expect(adminListAfterAddNewAdmin).to.be.not.deep.contains(normalizeAccountId(charlie.address)); + const adminListAfterAddAdmin = await helper.collection.getAdmins(collectionId); + expect(adminListAfterAddAdmin).to.be.not.deep.contains({Substrate: charlie.address}); + expect(adminListAfterAddAdmin).to.be.not.deep.contains({Substrate: bob.address}); }); }); it("Admin can't add collection admin.", async () => { - await usingApi(async (api, privateKeyWrapper) => { - const collectionId = await createCollectionExpectSuccess(); - const alice = privateKeyWrapper('//Alice'); - const bob = privateKeyWrapper('//Bob'); - const charlie = privateKeyWrapper('//CHARLIE'); + await usingPlaygrounds(async (helper) => { + const [alice, bob, charlie] = await helper.arrange.creteAccounts([10n, 10n, 10n], donor); + const collection = await helper.nft.mintCollection(alice, {name: 'Collection Name', description: 'Collection Description', tokenPrefix: 'COL'}); - const collection = await queryCollectionExpectSuccess(api, collectionId); - expect(collection.owner.toString()).to.be.equal(alice.address); + await collection.addAdmin(alice, {Substrate: bob.address}); - const changeAdminTx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(bob.address)); - await submitTransactionAsync(alice, changeAdminTx); + const adminListAfterAddAdmin = await collection.getAdmins(); + expect(adminListAfterAddAdmin).to.be.deep.contains({Substrate: bob.address}); - const adminListAfterAddAdmin = await getAdminList(api, collectionId); - expect(adminListAfterAddAdmin).to.be.deep.contains(normalizeAccountId(bob.address)); + const changeAdminTxCharlie = async () => collection.addAdmin(bob, {Substrate: charlie.address}); + await expect(changeAdminTxCharlie()).to.be.rejected; - const changeAdminTxCharlie = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(charlie.address)); - await expect(submitTransactionAsync(bob, changeAdminTxCharlie)).to.be.rejected; - - const adminListAfterAddNewAdmin = await getAdminList(api, collectionId); - expect(adminListAfterAddNewAdmin).to.be.deep.contains(normalizeAccountId(bob.address)); - expect(adminListAfterAddNewAdmin).to.be.not.deep.contains(normalizeAccountId(charlie.address)); + const adminListAfterAddNewAdmin = await collection.getAdmins(); + expect(adminListAfterAddNewAdmin).to.be.deep.contains({Substrate: bob.address}); + expect(adminListAfterAddNewAdmin).to.be.not.deep.contains({Substrate: charlie.address}); }); }); it("Can't add collection admin of not existing collection.", async () => { - await usingApi(async (api, privateKeyWrapper) => { + await usingPlaygrounds(async (helper) => { + const [alice, bob] = await helper.arrange.creteAccounts([10n, 10n, 10n], donor); // tslint:disable-next-line: no-bitwise const collectionId = (1 << 32) - 1; - const alice = privateKeyWrapper('//Alice'); - const bob = privateKeyWrapper('//Bob'); - const changeOwnerTx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(bob.address)); - await expect(submitTransactionExpectFailAsync(alice, changeOwnerTx)).to.be.rejected; + const addAdminTx = async () => helper.collection.addAdmin(alice, collectionId, {Substrate: bob.address}); + await expect(addAdminTx()).to.be.rejected; // Verifying that nothing bad happened (network is live, new collections can be created, etc.) - await createCollectionExpectSuccess(); + await helper.nft.mintCollection(alice, {name: 'Collection Name', description: 'Collection Description', tokenPrefix: 'COL'}); }); }); it("Can't add an admin to a destroyed collection.", async () => { - await usingApi(async (api, privateKeyWrapper) => { - const collectionId = await createCollectionExpectSuccess(); - const alice = privateKeyWrapper('//Alice'); - const bob = privateKeyWrapper('//Bob'); - await destroyCollectionExpectSuccess(collectionId); - const changeOwnerTx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(bob.address)); - await expect(submitTransactionExpectFailAsync(alice, changeOwnerTx)).to.be.rejected; + await usingPlaygrounds(async (helper) => { + const [alice, bob] = await helper.arrange.creteAccounts([10n, 10n, 10n], donor); + const collection = await helper.nft.mintCollection(alice, {name: 'Collection Name', description: 'Collection Description', tokenPrefix: 'COL'}); + + await collection.burn(alice); + const addAdminTx = async () => collection.addAdmin(alice, {Substrate: bob.address}); + await expect(addAdminTx()).to.be.rejected; // Verifying that nothing bad happened (network is live, new collections can be created, etc.) - await createCollectionExpectSuccess(); + await helper.nft.mintCollection(alice, {name: 'Collection Name', description: 'Collection Description', tokenPrefix: 'COL'}); }); }); it('Add an admin to a collection that has reached the maximum number of admins limit', async () => { - await usingApi(async (api: ApiPromise, privateKeyWrapper) => { - const alice = privateKeyWrapper('//Alice'); - const accounts = [ - privateKeyWrapper('//AdminTest/1').address, - privateKeyWrapper('//AdminTest/2').address, - privateKeyWrapper('//AdminTest/3').address, - privateKeyWrapper('//AdminTest/4').address, - privateKeyWrapper('//AdminTest/5').address, - privateKeyWrapper('//AdminTest/6').address, - privateKeyWrapper('//AdminTest/7').address, - ]; - const collectionId = await createCollectionExpectSuccess(); + await usingPlaygrounds(async (helper) => { + const [alice, ...accounts] = await helper.arrange.creteAccounts([10n, 0n, 0n, 0n, 0n, 0n, 0n, 0n], donor); + const collection = await helper.nft.mintCollection(alice, {name: 'Collection Name', description: 'Collection Description', tokenPrefix: 'COL'}); - const chainAdminLimit = (api.consts.common.collectionAdminsLimit as any).toNumber(); + const chainAdminLimit = (helper.api!.consts.common.collectionAdminsLimit as any).toNumber(); expect(chainAdminLimit).to.be.equal(5); for (let i = 0; i < chainAdminLimit; i++) { - await addCollectionAdminExpectSuccess(alice, collectionId, accounts[i]); - const adminListAfterAddAdmin = await getAdminList(api, collectionId); - expect(adminListAfterAddAdmin).to.be.deep.contains(normalizeAccountId(accounts[i])); + await collection.addAdmin(alice, {Substrate: accounts[i].address}); + const adminListAfterAddAdmin = await collection.getAdmins(); + expect(adminListAfterAddAdmin).to.be.deep.contains({Substrate: accounts[i].address}); } - const tx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(accounts[chainAdminLimit])); - await expect(submitTransactionExpectFailAsync(alice, tx)).to.be.rejected; + const addExtraAdminTx = async () => collection.addAdmin(alice, {Substrate: accounts[chainAdminLimit].address}); + await expect(addExtraAdminTx()).to.be.rejected; }); }); }); --- a/tests/src/eth/api/CollectionHelpers.sol +++ b/tests/src/eth/api/CollectionHelpers.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; -// Common stubs holder +/// @dev common stubs holder interface Dummy { } @@ -12,7 +12,7 @@ function supportsInterface(bytes4 interfaceID) external view returns (bool); } -// Inline +/// @dev inlined interface interface CollectionHelpersEvents { event CollectionCreated( address indexed owner, @@ -20,22 +20,24 @@ ); } -// Selector: 675f3074 +/// @title Contract, which allows users to operate with collections +/// @dev the ERC-165 identifier for this interface is 0x675f3074 interface CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents { - // Create an NFT collection - // @param name Name of the collection - // @param description Informative description of the collection - // @param token_prefix Token prefix to represent the collection tokens in UI and user applications - // @return address Address of the newly created collection - // - // Selector: createNonfungibleCollection(string,string,string) e34a6844 + /// Create an NFT collection + /// @param name Name of the collection + /// @param description Informative description of the collection + /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications + /// @return address Address of the newly created collection + /// @dev EVM selector for this function is: 0xe34a6844, + /// or in textual repr: createNonfungibleCollection(string,string,string) function createNonfungibleCollection( string memory name, string memory description, string memory tokenPrefix ) external returns (address); - // Selector: createERC721MetadataCompatibleCollection(string,string,string,string) a634a5f9 + /// @dev EVM selector for this function is: 0xa634a5f9, + /// or in textual repr: createERC721MetadataCompatibleCollection(string,string,string,string) function createERC721MetadataCompatibleCollection( string memory name, string memory description, @@ -43,14 +45,16 @@ string memory baseUri ) external returns (address); - // Selector: createRefungibleCollection(string,string,string) 44a68ad5 + /// @dev EVM selector for this function is: 0x44a68ad5, + /// or in textual repr: createRefungibleCollection(string,string,string) function createRefungibleCollection( string memory name, string memory description, string memory tokenPrefix ) external returns (address); - // Selector: createERC721MetadataCompatibleRFTCollection(string,string,string,string) a5596388 + /// @dev EVM selector for this function is: 0xa5596388, + /// or in textual repr: createERC721MetadataCompatibleRFTCollection(string,string,string,string) function createERC721MetadataCompatibleRFTCollection( string memory name, string memory description, @@ -58,11 +62,11 @@ string memory baseUri ) external returns (address); - // Check if a collection exists - // @param collection_address Address of the collection in question - // @return bool Does the collection exist? - // - // Selector: isCollectionExist(address) c3de1494 + /// Check if a collection exists + /// @param collectionAddress Address of the collection in question + /// @return bool Does the collection exist? + /// @dev EVM selector for this function is: 0xc3de1494, + /// or in textual repr: isCollectionExist(address) function isCollectionExist(address collectionAddress) external view --- a/tests/src/eth/api/ContractHelpers.sol +++ b/tests/src/eth/api/ContractHelpers.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; -// Common stubs holder +/// @dev common stubs holder interface Dummy { } @@ -12,63 +12,164 @@ function supportsInterface(bytes4 interfaceID) external view returns (bool); } -// Selector: 7b4866f9 +/// @title Magic contract, which allows users to reconfigure other contracts +/// @dev the ERC-165 identifier for this interface is 0xd77fab70 interface ContractHelpers is Dummy, ERC165 { - // Selector: contractOwner(address) 5152b14c + /// Get user, which deployed specified contract + /// @dev May return zero address in case if contract is deployed + /// using uniquenetwork evm-migration pallet, or using other terms not + /// intended by pallet-evm + /// @dev Returns zero address if contract does not exists + /// @param contractAddress Contract to get owner of + /// @return address Owner of contract + /// @dev EVM selector for this function is: 0x5152b14c, + /// or in textual repr: contractOwner(address) function contractOwner(address contractAddress) external view returns (address); - // Selector: sponsoringEnabled(address) 6027dc61 - function sponsoringEnabled(address contractAddress) + /// Set sponsor. + /// @param contractAddress Contract for which a sponsor is being established. + /// @param sponsor User address who set as pending sponsor. + /// @dev EVM selector for this function is: 0xf01fba93, + /// or in textual repr: setSponsor(address,address) + function setSponsor(address contractAddress, address sponsor) external; + + /// Set contract as self sponsored. + /// + /// @param contractAddress Contract for which a self sponsoring is being enabled. + /// @dev EVM selector for this function is: 0x89f7d9ae, + /// or in textual repr: selfSponsoredEnable(address) + function selfSponsoredEnable(address contractAddress) external; + + /// Remove sponsor. + /// + /// @param contractAddress Contract for which a sponsorship is being removed. + /// @dev EVM selector for this function is: 0xef784250, + /// or in textual repr: removeSponsor(address) + function removeSponsor(address contractAddress) external; + + /// Confirm sponsorship. + /// + /// @dev Caller must be same that set via [`setSponsor`]. + /// + /// @param contractAddress Сontract for which need to confirm sponsorship. + /// @dev EVM selector for this function is: 0xabc00001, + /// or in textual repr: confirmSponsorship(address) + function confirmSponsorship(address contractAddress) external; + + /// Get current sponsor. + /// + /// @param contractAddress The contract for which a sponsor is requested. + /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + /// @dev EVM selector for this function is: 0x743fc745, + /// or in textual repr: getSponsor(address) + function getSponsor(address contractAddress) external view - returns (bool); + returns (Tuple0 memory); - // Deprecated - // - // Selector: toggleSponsoring(address,bool) fcac6d86 - function toggleSponsoring(address contractAddress, bool enabled) external; + /// Check tat contract has confirmed sponsor. + /// + /// @param contractAddress The contract for which the presence of a confirmed sponsor is checked. + /// @return **true** if contract has confirmed sponsor. + /// @dev EVM selector for this function is: 0x97418603, + /// or in textual repr: hasSponsor(address) + function hasSponsor(address contractAddress) external view returns (bool); - // Selector: setSponsoringMode(address,uint8) fde8a560 - function setSponsoringMode(address contractAddress, uint8 mode) external; + /// Check tat contract has pending sponsor. + /// + /// @param contractAddress The contract for which the presence of a pending sponsor is checked. + /// @return **true** if contract has pending sponsor. + /// @dev EVM selector for this function is: 0x39b9b242, + /// or in textual repr: hasPendingSponsor(address) + function hasPendingSponsor(address contractAddress) + external + view + returns (bool); - // Selector: sponsoringMode(address) b70c7267 - function sponsoringMode(address contractAddress) + /// @dev EVM selector for this function is: 0x6027dc61, + /// or in textual repr: sponsoringEnabled(address) + function sponsoringEnabled(address contractAddress) external view - returns (uint8); + returns (bool); - // Selector: setSponsoringRateLimit(address,uint32) 77b6c908 - function setSponsoringRateLimit(address contractAddress, uint32 rateLimit) - external; + /// @dev EVM selector for this function is: 0xfde8a560, + /// or in textual repr: setSponsoringMode(address,uint8) + function setSponsoringMode(address contractAddress, uint8 mode) external; - // Selector: getSponsoringRateLimit(address) 610cfabd + /// Get current contract sponsoring rate limit + /// @param contractAddress Contract to get sponsoring mode of + /// @return uint32 Amount of blocks between two sponsored transactions + /// @dev EVM selector for this function is: 0x610cfabd, + /// or in textual repr: getSponsoringRateLimit(address) function getSponsoringRateLimit(address contractAddress) external view returns (uint32); - // Selector: allowed(address,address) 5c658165 + /// Set contract sponsoring rate limit + /// @dev Sponsoring rate limit - is a minimum amount of blocks that should + /// pass between two sponsored transactions + /// @param contractAddress Contract to change sponsoring rate limit of + /// @param rateLimit Target rate limit + /// @dev Only contract owner can change this setting + /// @dev EVM selector for this function is: 0x77b6c908, + /// or in textual repr: setSponsoringRateLimit(address,uint32) + function setSponsoringRateLimit(address contractAddress, uint32 rateLimit) + external; + + /// Is specified user present in contract allow list + /// @dev Contract owner always implicitly included + /// @param contractAddress Contract to check allowlist of + /// @param user User to check + /// @return bool Is specified users exists in contract allowlist + /// @dev EVM selector for this function is: 0x5c658165, + /// or in textual repr: allowed(address,address) function allowed(address contractAddress, address user) external view returns (bool); - // Selector: allowlistEnabled(address) c772ef6c + /// Toggle user presence in contract allowlist + /// @param contractAddress Contract to change allowlist of + /// @param user Which user presence should be toggled + /// @param isAllowed `true` if user should be allowed to be sponsored + /// or call this contract, `false` otherwise + /// @dev Only contract owner can change this setting + /// @dev EVM selector for this function is: 0x4706cc1c, + /// or in textual repr: toggleAllowed(address,address,bool) + function toggleAllowed( + address contractAddress, + address user, + bool isAllowed + ) external; + + /// Is this contract has allowlist access enabled + /// @dev Allowlist always can have users, and it is used for two purposes: + /// in case of allowlist sponsoring mode, users will be sponsored if they exist in allowlist + /// in case of allowlist access enabled, only users from allowlist may call this contract + /// @param contractAddress Contract to get allowlist access of + /// @return bool Is specified contract has allowlist access enabled + /// @dev EVM selector for this function is: 0xc772ef6c, + /// or in textual repr: allowlistEnabled(address) function allowlistEnabled(address contractAddress) external view returns (bool); - // Selector: toggleAllowlist(address,bool) 36de20f5 + /// Toggle contract allowlist access + /// @param contractAddress Contract to change allowlist access of + /// @param enabled Should allowlist access to be enabled? + /// @dev EVM selector for this function is: 0x36de20f5, + /// or in textual repr: toggleAllowlist(address,bool) function toggleAllowlist(address contractAddress, bool enabled) external; +} - // Selector: toggleAllowed(address,address,bool) 4706cc1c - function toggleAllowed( - address contractAddress, - address user, - bool allowed - ) external; +/// @dev anonymous struct +struct Tuple0 { + address field_0; + uint256 field_1; } --- a/tests/src/eth/api/UniqueFungible.sol +++ b/tests/src/eth/api/UniqueFungible.sol @@ -3,225 +3,312 @@ pragma solidity >=0.8.0 <0.9.0; -// Common stubs holder +/// @dev common stubs holder interface Dummy { } interface ERC165 is Dummy { function supportsInterface(bytes4 interfaceID) external view returns (bool); -} - -// Inline -interface ERC20Events { - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval( - address indexed owner, - address indexed spender, - uint256 value - ); } -// Selector: 6cf113cd +/// @title A contract that allows you to work with collections. +/// @dev the ERC-165 identifier for this interface is 0xe54be640 interface Collection is Dummy, ERC165 { - // Set collection property. - // - // @param key Property key. - // @param value Propery value. - // - // Selector: setCollectionProperty(string,bytes) 2f073f66 + /// Set collection property. + /// + /// @param key Property key. + /// @param value Propery value. + /// @dev EVM selector for this function is: 0x2f073f66, + /// or in textual repr: setCollectionProperty(string,bytes) function setCollectionProperty(string memory key, bytes memory value) external; - // Delete collection property. - // - // @param key Property key. - // - // Selector: deleteCollectionProperty(string) 7b7debce + /// Delete collection property. + /// + /// @param key Property key. + /// @dev EVM selector for this function is: 0x7b7debce, + /// or in textual repr: deleteCollectionProperty(string) function deleteCollectionProperty(string memory key) external; - // Get collection property. - // - // @dev Throws error if key not found. - // - // @param key Property key. - // @return bytes The property corresponding to the key. - // - // Selector: collectionProperty(string) cf24fd6d + /// Get collection property. + /// + /// @dev Throws error if key not found. + /// + /// @param key Property key. + /// @return bytes The property corresponding to the key. + /// @dev EVM selector for this function is: 0xcf24fd6d, + /// or in textual repr: collectionProperty(string) function collectionProperty(string memory key) external view returns (bytes memory); - // Set the sponsor of the collection. - // - // @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. - // - // @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. - // - // Selector: setCollectionSponsor(address) 7623402e + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0x7623402e, + /// or in textual repr: setCollectionSponsor(address) function setCollectionSponsor(address sponsor) external; - // Collection sponsorship confirmation. - // - // @dev After setting the sponsor for the collection, it must be confirmed with this function. - // - // Selector: confirmCollectionSponsorship() 3c50e97a + /// Set the substrate sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Substrate address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0xc74d6751, + /// or in textual repr: setCollectionSponsorSubstrate(uint256) + function setCollectionSponsorSubstrate(uint256 sponsor) external; + + /// @dev EVM selector for this function is: 0x058ac185, + /// or in textual repr: hasCollectionPendingSponsor() + function hasCollectionPendingSponsor() external view returns (bool); + + /// Collection sponsorship confirmation. + /// + /// @dev After setting the sponsor for the collection, it must be confirmed with this function. + /// @dev EVM selector for this function is: 0x3c50e97a, + /// or in textual repr: confirmCollectionSponsorship() function confirmCollectionSponsorship() external; - // Set limits for the collection. - // @dev Throws error if limit not found. - // @param limit Name of the limit. Valid names: - // "accountTokenOwnershipLimit", - // "sponsoredDataSize", - // "sponsoredDataRateLimit", - // "tokenLimit", - // "sponsorTransferTimeout", - // "sponsorApproveTimeout" - // @param value Value of the limit. - // - // Selector: setCollectionLimit(string,uint32) 6a3841db + /// Remove collection sponsor. + /// @dev EVM selector for this function is: 0x6e0326a3, + /// or in textual repr: removeCollectionSponsor() + function removeCollectionSponsor() external; + + /// Get current sponsor. + /// + /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + /// @dev EVM selector for this function is: 0xb66bbc14, + /// or in textual repr: getCollectionSponsor() + function getCollectionSponsor() external view returns (Tuple6 memory); + + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "accountTokenOwnershipLimit", + /// "sponsoredDataSize", + /// "sponsoredDataRateLimit", + /// "tokenLimit", + /// "sponsorTransferTimeout", + /// "sponsorApproveTimeout" + /// @param value Value of the limit. + /// @dev EVM selector for this function is: 0x6a3841db, + /// or in textual repr: setCollectionLimit(string,uint32) function setCollectionLimit(string memory limit, uint32 value) external; - // Set limits for the collection. - // @dev Throws error if limit not found. - // @param limit Name of the limit. Valid names: - // "ownerCanTransfer", - // "ownerCanDestroy", - // "transfersEnabled" - // @param value Value of the limit. - // - // Selector: setCollectionLimit(string,bool) 993b7fba + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "ownerCanTransfer", + /// "ownerCanDestroy", + /// "transfersEnabled" + /// @param value Value of the limit. + /// @dev EVM selector for this function is: 0x993b7fba, + /// or in textual repr: setCollectionLimit(string,bool) function setCollectionLimit(string memory limit, bool value) external; - // Get contract address. - // - // Selector: contractAddress() f6b4dfb4 + /// Get contract address. + /// @dev EVM selector for this function is: 0xf6b4dfb4, + /// or in textual repr: contractAddress() function contractAddress() external view returns (address); - // Add collection admin by substrate address. - // @param new_admin Substrate administrator address. - // - // Selector: addCollectionAdminSubstrate(uint256) 5730062b + /// Add collection admin by substrate address. + /// @param newAdmin Substrate administrator address. + /// @dev EVM selector for this function is: 0x5730062b, + /// or in textual repr: addCollectionAdminSubstrate(uint256) function addCollectionAdminSubstrate(uint256 newAdmin) external; - // Remove collection admin by substrate address. - // @param admin Substrate administrator address. - // - // Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9 + /// Remove collection admin by substrate address. + /// @param admin Substrate administrator address. + /// @dev EVM selector for this function is: 0x4048fcf9, + /// or in textual repr: removeCollectionAdminSubstrate(uint256) function removeCollectionAdminSubstrate(uint256 admin) external; - // Add collection admin. - // @param new_admin Address of the added administrator. - // - // Selector: addCollectionAdmin(address) 92e462c7 + /// Add collection admin. + /// @param newAdmin Address of the added administrator. + /// @dev EVM selector for this function is: 0x92e462c7, + /// or in textual repr: addCollectionAdmin(address) function addCollectionAdmin(address newAdmin) external; - // Remove collection admin. - // - // @param new_admin Address of the removed administrator. - // - // Selector: removeCollectionAdmin(address) fafd7b42 + /// Remove collection admin. + /// + /// @param admin Address of the removed administrator. + /// @dev EVM selector for this function is: 0xfafd7b42, + /// or in textual repr: removeCollectionAdmin(address) function removeCollectionAdmin(address admin) external; - // Toggle accessibility of collection nesting. - // - // @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' - // - // Selector: setCollectionNesting(bool) 112d4586 + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' + /// @dev EVM selector for this function is: 0x112d4586, + /// or in textual repr: setCollectionNesting(bool) function setCollectionNesting(bool enable) external; - // Toggle accessibility of collection nesting. - // - // @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' - // @param collections Addresses of collections that will be available for nesting. - // - // Selector: setCollectionNesting(bool,address[]) 64872396 + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + /// @param collections Addresses of collections that will be available for nesting. + /// @dev EVM selector for this function is: 0x64872396, + /// or in textual repr: setCollectionNesting(bool,address[]) function setCollectionNesting(bool enable, address[] memory collections) external; - // Set the collection access method. - // @param mode Access mode - // 0 for Normal - // 1 for AllowList - // - // Selector: setCollectionAccess(uint8) 41835d4c + /// Set the collection access method. + /// @param mode Access mode + /// 0 for Normal + /// 1 for AllowList + /// @dev EVM selector for this function is: 0x41835d4c, + /// or in textual repr: setCollectionAccess(uint8) function setCollectionAccess(uint8 mode) external; - // Add the user to the allowed list. - // - // @param user Address of a trusted user. - // - // Selector: addToCollectionAllowList(address) 67844fe6 + /// Add the user to the allowed list. + /// + /// @param user Address of a trusted user. + /// @dev EVM selector for this function is: 0x67844fe6, + /// or in textual repr: addToCollectionAllowList(address) function addToCollectionAllowList(address user) external; - // Remove the user from the allowed list. - // - // @param user Address of a removed user. - // - // Selector: removeFromCollectionAllowList(address) 85c51acb + /// Remove the user from the allowed list. + /// + /// @param user Address of a removed user. + /// @dev EVM selector for this function is: 0x85c51acb, + /// or in textual repr: removeFromCollectionAllowList(address) function removeFromCollectionAllowList(address user) external; - // Switch permission for minting. - // - // @param mode Enable if "true". - // - // Selector: setCollectionMintMode(bool) 00018e84 + /// Switch permission for minting. + /// + /// @param mode Enable if "true". + /// @dev EVM selector for this function is: 0x00018e84, + /// or in textual repr: setCollectionMintMode(bool) function setCollectionMintMode(bool mode) external; - // Check that account is the owner or admin of the collection - // - // @param user account to verify - // @return "true" if account is the owner or admin - // - // Selector: verifyOwnerOrAdmin(address) c2282493 - function verifyOwnerOrAdmin(address user) external view returns (bool); + /// Check that account is the owner or admin of the collection + /// + /// @param user account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x9811b0c7, + /// or in textual repr: isOwnerOrAdmin(address) + function isOwnerOrAdmin(address user) external view returns (bool); + + /// Check that substrate account is the owner or admin of the collection + /// + /// @param user account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x68910e00, + /// or in textual repr: isOwnerOrAdminSubstrate(uint256) + function isOwnerOrAdminSubstrate(uint256 user) external view returns (bool); - // Returns collection type - // - // @return `Fungible` or `NFT` or `ReFungible` - // - // Selector: uniqueCollectionType() d34b55b8 + /// Returns collection type + /// + /// @return `Fungible` or `NFT` or `ReFungible` + /// @dev EVM selector for this function is: 0xd34b55b8, + /// or in textual repr: uniqueCollectionType() function uniqueCollectionType() external returns (string memory); + + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner account + /// @dev EVM selector for this function is: 0x13af4035, + /// or in textual repr: setOwner(address) + function setOwner(address newOwner) external; + + /// Changes collection owner to another substrate account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner substrate account + /// @dev EVM selector for this function is: 0xb212138f, + /// or in textual repr: setOwnerSubstrate(uint256) + function setOwnerSubstrate(uint256 newOwner) external; } -// Selector: 79cc6790 +/// @dev the ERC-165 identifier for this interface is 0x63034ac5 interface ERC20UniqueExtensions is Dummy, ERC165 { - // Selector: burnFrom(address,uint256) 79cc6790 + /// Burn tokens from account + /// @dev Function that burns an `amount` of the tokens of a given account, + /// deducting from the sender's allowance for said account. + /// @param from The account whose tokens will be burnt. + /// @param amount The amount that will be burnt. + /// @dev EVM selector for this function is: 0x79cc6790, + /// or in textual repr: burnFrom(address,uint256) function burnFrom(address from, uint256 amount) external returns (bool); + + /// Mint tokens for multiple accounts. + /// @param amounts array of pairs of account address and amount + /// @dev EVM selector for this function is: 0x1acf2d55, + /// or in textual repr: mintBulk((address,uint256)[]) + function mintBulk(Tuple6[] memory amounts) external returns (bool); +} + +/// @dev anonymous struct +struct Tuple6 { + address field_0; + uint256 field_1; +} + +/// @dev the ERC-165 identifier for this interface is 0x40c10f19 +interface ERC20Mintable is Dummy, ERC165 { + /// Mint tokens for `to` account. + /// @param to account that will receive minted tokens + /// @param amount amount of tokens to mint + /// @dev EVM selector for this function is: 0x40c10f19, + /// or in textual repr: mint(address,uint256) + function mint(address to, uint256 amount) external returns (bool); } -// Selector: 942e8b22 +/// @dev inlined interface +interface ERC20Events { + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} + +/// @dev the ERC-165 identifier for this interface is 0x942e8b22 interface ERC20 is Dummy, ERC165, ERC20Events { - // Selector: name() 06fdde03 + /// @dev EVM selector for this function is: 0x06fdde03, + /// or in textual repr: name() function name() external view returns (string memory); - // Selector: symbol() 95d89b41 + /// @dev EVM selector for this function is: 0x95d89b41, + /// or in textual repr: symbol() function symbol() external view returns (string memory); - // Selector: totalSupply() 18160ddd + /// @dev EVM selector for this function is: 0x18160ddd, + /// or in textual repr: totalSupply() function totalSupply() external view returns (uint256); - // Selector: decimals() 313ce567 + /// @dev EVM selector for this function is: 0x313ce567, + /// or in textual repr: decimals() function decimals() external view returns (uint8); - // Selector: balanceOf(address) 70a08231 + /// @dev EVM selector for this function is: 0x70a08231, + /// or in textual repr: balanceOf(address) function balanceOf(address owner) external view returns (uint256); - // Selector: transfer(address,uint256) a9059cbb + /// @dev EVM selector for this function is: 0xa9059cbb, + /// or in textual repr: transfer(address,uint256) function transfer(address to, uint256 amount) external returns (bool); - // Selector: transferFrom(address,address,uint256) 23b872dd + /// @dev EVM selector for this function is: 0x23b872dd, + /// or in textual repr: transferFrom(address,address,uint256) function transferFrom( address from, address to, uint256 amount ) external returns (bool); - // Selector: approve(address,uint256) 095ea7b3 + /// @dev EVM selector for this function is: 0x095ea7b3, + /// or in textual repr: approve(address,uint256) function approve(address spender, uint256 amount) external returns (bool); - // Selector: allowance(address,address) dd62ed3e + /// @dev EVM selector for this function is: 0xdd62ed3e, + /// or in textual repr: allowance(address,address) function allowance(address owner, address spender) external view @@ -232,6 +319,7 @@ Dummy, ERC165, ERC20, + ERC20Mintable, ERC20UniqueExtensions, Collection {} --- a/tests/src/eth/api/UniqueNFT.sol +++ b/tests/src/eth/api/UniqueNFT.sol @@ -3,55 +3,26 @@ pragma solidity >=0.8.0 <0.9.0; -// Anonymous struct -struct Tuple0 { - uint256 field_0; - string field_1; -} - -// Common stubs holder +/// @dev common stubs holder interface Dummy { } interface ERC165 is Dummy { function supportsInterface(bytes4 interfaceID) external view returns (bool); -} - -// Inline -interface ERC721Events { - event Transfer( - address indexed from, - address indexed to, - uint256 indexed tokenId - ); - event Approval( - address indexed owner, - address indexed approved, - uint256 indexed tokenId - ); - event ApprovalForAll( - address indexed owner, - address indexed operator, - bool approved - ); } -// Inline -interface ERC721MintableEvents { - event MintingFinished(); -} - -// Selector: 41369377 +/// @title A contract that allows to set and delete token properties and change token property permissions. +/// @dev the ERC-165 identifier for this interface is 0x41369377 interface TokenProperties is Dummy, ERC165 { - // @notice Set permissions for token property. - // @dev Throws error if `msg.sender` is not admin or owner of the collection. - // @param key Property key. - // @param is_mutable Permission to mutate property. - // @param collection_admin Permission to mutate property by collection admin if property is mutable. - // @param token_owner Permission to mutate property by token owner if property is mutable. - // - // Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa + /// @notice Set permissions for token property. + /// @dev Throws error if `msg.sender` is not admin or owner of the collection. + /// @param key Property key. + /// @param isMutable Permission to mutate property. + /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable. + /// @param tokenOwner Permission to mutate property by token owner if property is mutable. + /// @dev EVM selector for this function is: 0x222d97fa, + /// or in textual repr: setTokenPropertyPermission(string,bool,bool,bool) function setTokenPropertyPermission( string memory key, bool isMutable, @@ -59,437 +30,530 @@ bool tokenOwner ) external; - // @notice Set token property value. - // @dev Throws error if `msg.sender` has no permission to edit the property. - // @param tokenId ID of the token. - // @param key Property key. - // @param value Property value. - // - // Selector: setProperty(uint256,string,bytes) 1752d67b + /// @notice Set token property value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param key Property key. + /// @param value Property value. + /// @dev EVM selector for this function is: 0x1752d67b, + /// or in textual repr: setProperty(uint256,string,bytes) function setProperty( uint256 tokenId, string memory key, bytes memory value ) external; - // @notice Delete token property value. - // @dev Throws error if `msg.sender` has no permission to edit the property. - // @param tokenId ID of the token. - // @param key Property key. - // - // Selector: deleteProperty(uint256,string) 066111d1 + /// @notice Delete token property value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param key Property key. + /// @dev EVM selector for this function is: 0x066111d1, + /// or in textual repr: deleteProperty(uint256,string) function deleteProperty(uint256 tokenId, string memory key) external; - // @notice Get token property value. - // @dev Throws error if key not found - // @param tokenId ID of the token. - // @param key Property key. - // @return Property value bytes - // - // Selector: property(uint256,string) 7228c327 + /// @notice Get token property value. + /// @dev Throws error if key not found + /// @param tokenId ID of the token. + /// @param key Property key. + /// @return Property value bytes + /// @dev EVM selector for this function is: 0x7228c327, + /// or in textual repr: property(uint256,string) function property(uint256 tokenId, string memory key) external view returns (bytes memory); } -// Selector: 42966c68 -interface ERC721Burnable is Dummy, ERC165 { - // @notice Burns a specific ERC721 token. - // @dev Throws unless `msg.sender` is the current NFT owner, or an authorized - // operator of the current owner. - // @param tokenId The NFT to approve - // - // Selector: burn(uint256) 42966c68 - function burn(uint256 tokenId) external; -} - -// Selector: 58800161 -interface ERC721 is Dummy, ERC165, ERC721Events { - // @notice Count all NFTs assigned to an owner - // @dev NFTs assigned to the zero address are considered invalid, and this - // function throws for queries about the zero address. - // @param owner An address for whom to query the balance - // @return The number of NFTs owned by `owner`, possibly zero - // - // Selector: balanceOf(address) 70a08231 - function balanceOf(address owner) external view returns (uint256); - - // @notice Find the owner of an NFT - // @dev NFTs assigned to zero address are considered invalid, and queries - // about them do throw. - // @param tokenId The identifier for an NFT - // @return The address of the owner of the NFT - // - // Selector: ownerOf(uint256) 6352211e - function ownerOf(uint256 tokenId) external view returns (address); - - // @dev Not implemented - // - // Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672 - function safeTransferFromWithData( - address from, - address to, - uint256 tokenId, - bytes memory data - ) external; - - // @dev Not implemented - // - // Selector: safeTransferFrom(address,address,uint256) 42842e0e - function safeTransferFrom( - address from, - address to, - uint256 tokenId - ) external; - - // @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE - // TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE - // THEY MAY BE PERMANENTLY LOST - // @dev Throws unless `msg.sender` is the current owner or an authorized - // operator for this NFT. Throws if `from` is not the current owner. Throws - // if `to` is the zero address. Throws if `tokenId` is not a valid NFT. - // @param from The current owner of the NFT - // @param to The new owner - // @param tokenId The NFT to transfer - // @param _value Not used for an NFT - // - // Selector: transferFrom(address,address,uint256) 23b872dd - function transferFrom( - address from, - address to, - uint256 tokenId - ) external; - - // @notice Set or reaffirm the approved address for an NFT - // @dev The zero address indicates there is no approved address. - // @dev Throws unless `msg.sender` is the current NFT owner, or an authorized - // operator of the current owner. - // @param approved The new approved NFT controller - // @param tokenId The NFT to approve - // - // Selector: approve(address,uint256) 095ea7b3 - function approve(address approved, uint256 tokenId) external; - - // @dev Not implemented - // - // Selector: setApprovalForAll(address,bool) a22cb465 - function setApprovalForAll(address operator, bool approved) external; - - // @dev Not implemented - // - // Selector: getApproved(uint256) 081812fc - function getApproved(uint256 tokenId) external view returns (address); - - // @dev Not implemented - // - // Selector: isApprovedForAll(address,address) e985e9c5 - function isApprovedForAll(address owner, address operator) - external - view - returns (address); -} - -// Selector: 5b5e139f -interface ERC721Metadata is Dummy, ERC165 { - // @notice A descriptive name for a collection of NFTs in this contract - // - // Selector: name() 06fdde03 - function name() external view returns (string memory); - - // @notice An abbreviated name for NFTs in this contract - // - // Selector: symbol() 95d89b41 - function symbol() external view returns (string memory); - - // @notice A distinct Uniform Resource Identifier (URI) for a given asset. - // - // @dev If the token has a `url` property and it is not empty, it is returned. - // Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`. - // If the collection property `baseURI` is empty or absent, return "" (empty string) - // otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix - // otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings). - // - // @return token's const_metadata - // - // Selector: tokenURI(uint256) c87b56dd - function tokenURI(uint256 tokenId) external view returns (string memory); -} - -// Selector: 68ccfe89 -interface ERC721Mintable is Dummy, ERC165, ERC721MintableEvents { - // Selector: mintingFinished() 05d2035b - function mintingFinished() external view returns (bool); - - // @notice Function to mint token. - // @dev `tokenId` should be obtained with `nextTokenId` method, - // unlike standard, you can't specify it manually - // @param to The new owner - // @param tokenId ID of the minted NFT - // - // Selector: mint(address,uint256) 40c10f19 - function mint(address to, uint256 tokenId) external returns (bool); - - // @notice Function to mint token with the given tokenUri. - // @dev `tokenId` should be obtained with `nextTokenId` method, - // unlike standard, you can't specify it manually - // @param to The new owner - // @param tokenId ID of the minted NFT - // @param tokenUri Token URI that would be stored in the NFT properties - // - // Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f - function mintWithTokenURI( - address to, - uint256 tokenId, - string memory tokenUri - ) external returns (bool); - - // @dev Not implemented - // - // Selector: finishMinting() 7d64bcb4 - function finishMinting() external returns (bool); -} - -// Selector: 6cf113cd +/// @title A contract that allows you to work with collections. +/// @dev the ERC-165 identifier for this interface is 0xe54be640 interface Collection is Dummy, ERC165 { - // Set collection property. - // - // @param key Property key. - // @param value Propery value. - // - // Selector: setCollectionProperty(string,bytes) 2f073f66 + /// Set collection property. + /// + /// @param key Property key. + /// @param value Propery value. + /// @dev EVM selector for this function is: 0x2f073f66, + /// or in textual repr: setCollectionProperty(string,bytes) function setCollectionProperty(string memory key, bytes memory value) external; - // Delete collection property. - // - // @param key Property key. - // - // Selector: deleteCollectionProperty(string) 7b7debce + /// Delete collection property. + /// + /// @param key Property key. + /// @dev EVM selector for this function is: 0x7b7debce, + /// or in textual repr: deleteCollectionProperty(string) function deleteCollectionProperty(string memory key) external; - // Get collection property. - // - // @dev Throws error if key not found. - // - // @param key Property key. - // @return bytes The property corresponding to the key. - // - // Selector: collectionProperty(string) cf24fd6d + /// Get collection property. + /// + /// @dev Throws error if key not found. + /// + /// @param key Property key. + /// @return bytes The property corresponding to the key. + /// @dev EVM selector for this function is: 0xcf24fd6d, + /// or in textual repr: collectionProperty(string) function collectionProperty(string memory key) external view returns (bytes memory); - // Set the sponsor of the collection. - // - // @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. - // - // @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. - // - // Selector: setCollectionSponsor(address) 7623402e + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0x7623402e, + /// or in textual repr: setCollectionSponsor(address) function setCollectionSponsor(address sponsor) external; - // Collection sponsorship confirmation. - // - // @dev After setting the sponsor for the collection, it must be confirmed with this function. - // - // Selector: confirmCollectionSponsorship() 3c50e97a + /// Set the substrate sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Substrate address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0xc74d6751, + /// or in textual repr: setCollectionSponsorSubstrate(uint256) + function setCollectionSponsorSubstrate(uint256 sponsor) external; + + /// @dev EVM selector for this function is: 0x058ac185, + /// or in textual repr: hasCollectionPendingSponsor() + function hasCollectionPendingSponsor() external view returns (bool); + + /// Collection sponsorship confirmation. + /// + /// @dev After setting the sponsor for the collection, it must be confirmed with this function. + /// @dev EVM selector for this function is: 0x3c50e97a, + /// or in textual repr: confirmCollectionSponsorship() function confirmCollectionSponsorship() external; - // Set limits for the collection. - // @dev Throws error if limit not found. - // @param limit Name of the limit. Valid names: - // "accountTokenOwnershipLimit", - // "sponsoredDataSize", - // "sponsoredDataRateLimit", - // "tokenLimit", - // "sponsorTransferTimeout", - // "sponsorApproveTimeout" - // @param value Value of the limit. - // - // Selector: setCollectionLimit(string,uint32) 6a3841db + /// Remove collection sponsor. + /// @dev EVM selector for this function is: 0x6e0326a3, + /// or in textual repr: removeCollectionSponsor() + function removeCollectionSponsor() external; + + /// Get current sponsor. + /// + /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + /// @dev EVM selector for this function is: 0xb66bbc14, + /// or in textual repr: getCollectionSponsor() + function getCollectionSponsor() external view returns (Tuple17 memory); + + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "accountTokenOwnershipLimit", + /// "sponsoredDataSize", + /// "sponsoredDataRateLimit", + /// "tokenLimit", + /// "sponsorTransferTimeout", + /// "sponsorApproveTimeout" + /// @param value Value of the limit. + /// @dev EVM selector for this function is: 0x6a3841db, + /// or in textual repr: setCollectionLimit(string,uint32) function setCollectionLimit(string memory limit, uint32 value) external; - // Set limits for the collection. - // @dev Throws error if limit not found. - // @param limit Name of the limit. Valid names: - // "ownerCanTransfer", - // "ownerCanDestroy", - // "transfersEnabled" - // @param value Value of the limit. - // - // Selector: setCollectionLimit(string,bool) 993b7fba + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "ownerCanTransfer", + /// "ownerCanDestroy", + /// "transfersEnabled" + /// @param value Value of the limit. + /// @dev EVM selector for this function is: 0x993b7fba, + /// or in textual repr: setCollectionLimit(string,bool) function setCollectionLimit(string memory limit, bool value) external; - // Get contract address. - // - // Selector: contractAddress() f6b4dfb4 + /// Get contract address. + /// @dev EVM selector for this function is: 0xf6b4dfb4, + /// or in textual repr: contractAddress() function contractAddress() external view returns (address); - // Add collection admin by substrate address. - // @param new_admin Substrate administrator address. - // - // Selector: addCollectionAdminSubstrate(uint256) 5730062b + /// Add collection admin by substrate address. + /// @param newAdmin Substrate administrator address. + /// @dev EVM selector for this function is: 0x5730062b, + /// or in textual repr: addCollectionAdminSubstrate(uint256) function addCollectionAdminSubstrate(uint256 newAdmin) external; - // Remove collection admin by substrate address. - // @param admin Substrate administrator address. - // - // Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9 + /// Remove collection admin by substrate address. + /// @param admin Substrate administrator address. + /// @dev EVM selector for this function is: 0x4048fcf9, + /// or in textual repr: removeCollectionAdminSubstrate(uint256) function removeCollectionAdminSubstrate(uint256 admin) external; - // Add collection admin. - // @param new_admin Address of the added administrator. - // - // Selector: addCollectionAdmin(address) 92e462c7 + /// Add collection admin. + /// @param newAdmin Address of the added administrator. + /// @dev EVM selector for this function is: 0x92e462c7, + /// or in textual repr: addCollectionAdmin(address) function addCollectionAdmin(address newAdmin) external; - // Remove collection admin. - // - // @param new_admin Address of the removed administrator. - // - // Selector: removeCollectionAdmin(address) fafd7b42 + /// Remove collection admin. + /// + /// @param admin Address of the removed administrator. + /// @dev EVM selector for this function is: 0xfafd7b42, + /// or in textual repr: removeCollectionAdmin(address) function removeCollectionAdmin(address admin) external; - // Toggle accessibility of collection nesting. - // - // @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' - // - // Selector: setCollectionNesting(bool) 112d4586 + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' + /// @dev EVM selector for this function is: 0x112d4586, + /// or in textual repr: setCollectionNesting(bool) function setCollectionNesting(bool enable) external; - // Toggle accessibility of collection nesting. - // - // @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' - // @param collections Addresses of collections that will be available for nesting. - // - // Selector: setCollectionNesting(bool,address[]) 64872396 + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + /// @param collections Addresses of collections that will be available for nesting. + /// @dev EVM selector for this function is: 0x64872396, + /// or in textual repr: setCollectionNesting(bool,address[]) function setCollectionNesting(bool enable, address[] memory collections) external; - // Set the collection access method. - // @param mode Access mode - // 0 for Normal - // 1 for AllowList - // - // Selector: setCollectionAccess(uint8) 41835d4c + /// Set the collection access method. + /// @param mode Access mode + /// 0 for Normal + /// 1 for AllowList + /// @dev EVM selector for this function is: 0x41835d4c, + /// or in textual repr: setCollectionAccess(uint8) function setCollectionAccess(uint8 mode) external; - // Add the user to the allowed list. - // - // @param user Address of a trusted user. - // - // Selector: addToCollectionAllowList(address) 67844fe6 + /// Add the user to the allowed list. + /// + /// @param user Address of a trusted user. + /// @dev EVM selector for this function is: 0x67844fe6, + /// or in textual repr: addToCollectionAllowList(address) function addToCollectionAllowList(address user) external; - // Remove the user from the allowed list. - // - // @param user Address of a removed user. - // - // Selector: removeFromCollectionAllowList(address) 85c51acb + /// Remove the user from the allowed list. + /// + /// @param user Address of a removed user. + /// @dev EVM selector for this function is: 0x85c51acb, + /// or in textual repr: removeFromCollectionAllowList(address) function removeFromCollectionAllowList(address user) external; - // Switch permission for minting. - // - // @param mode Enable if "true". - // - // Selector: setCollectionMintMode(bool) 00018e84 + /// Switch permission for minting. + /// + /// @param mode Enable if "true". + /// @dev EVM selector for this function is: 0x00018e84, + /// or in textual repr: setCollectionMintMode(bool) function setCollectionMintMode(bool mode) external; - // Check that account is the owner or admin of the collection - // - // @param user account to verify - // @return "true" if account is the owner or admin - // - // Selector: verifyOwnerOrAdmin(address) c2282493 - function verifyOwnerOrAdmin(address user) external view returns (bool); + /// Check that account is the owner or admin of the collection + /// + /// @param user account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x9811b0c7, + /// or in textual repr: isOwnerOrAdmin(address) + function isOwnerOrAdmin(address user) external view returns (bool); - // Returns collection type - // - // @return `Fungible` or `NFT` or `ReFungible` - // - // Selector: uniqueCollectionType() d34b55b8 + /// Check that substrate account is the owner or admin of the collection + /// + /// @param user account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x68910e00, + /// or in textual repr: isOwnerOrAdminSubstrate(uint256) + function isOwnerOrAdminSubstrate(uint256 user) external view returns (bool); + + /// Returns collection type + /// + /// @return `Fungible` or `NFT` or `ReFungible` + /// @dev EVM selector for this function is: 0xd34b55b8, + /// or in textual repr: uniqueCollectionType() function uniqueCollectionType() external returns (string memory); + + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner account + /// @dev EVM selector for this function is: 0x13af4035, + /// or in textual repr: setOwner(address) + function setOwner(address newOwner) external; + + /// Changes collection owner to another substrate account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner substrate account + /// @dev EVM selector for this function is: 0xb212138f, + /// or in textual repr: setOwnerSubstrate(uint256) + function setOwnerSubstrate(uint256 newOwner) external; } -// Selector: 780e9d63 -interface ERC721Enumerable is Dummy, ERC165 { - // @notice Enumerate valid NFTs - // @param index A counter less than `totalSupply()` - // @return The token identifier for the `index`th NFT, - // (sort order not specified) - // - // Selector: tokenByIndex(uint256) 4f6ccce7 - function tokenByIndex(uint256 index) external view returns (uint256); +/// @dev anonymous struct +struct Tuple17 { + address field_0; + uint256 field_1; +} - // @dev Not implemented - // - // Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59 - function tokenOfOwnerByIndex(address owner, uint256 index) - external - view - returns (uint256); +/// @title ERC721 Token that can be irreversibly burned (destroyed). +/// @dev the ERC-165 identifier for this interface is 0x42966c68 +interface ERC721Burnable is Dummy, ERC165 { + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param tokenId The NFT to approve + /// @dev EVM selector for this function is: 0x42966c68, + /// or in textual repr: burn(uint256) + function burn(uint256 tokenId) external; +} + +/// @dev inlined interface +interface ERC721MintableEvents { + event MintingFinished(); +} + +/// @title ERC721 minting logic. +/// @dev the ERC-165 identifier for this interface is 0x68ccfe89 +interface ERC721Mintable is Dummy, ERC165, ERC721MintableEvents { + /// @dev EVM selector for this function is: 0x05d2035b, + /// or in textual repr: mintingFinished() + function mintingFinished() external view returns (bool); + + /// @notice Function to mint token. + /// @dev `tokenId` should be obtained with `nextTokenId` method, + /// unlike standard, you can't specify it manually + /// @param to The new owner + /// @param tokenId ID of the minted NFT + /// @dev EVM selector for this function is: 0x40c10f19, + /// or in textual repr: mint(address,uint256) + function mint(address to, uint256 tokenId) external returns (bool); + + /// @notice Function to mint token with the given tokenUri. + /// @dev `tokenId` should be obtained with `nextTokenId` method, + /// unlike standard, you can't specify it manually + /// @param to The new owner + /// @param tokenId ID of the minted NFT + /// @param tokenUri Token URI that would be stored in the NFT properties + /// @dev EVM selector for this function is: 0x50bb4e7f, + /// or in textual repr: mintWithTokenURI(address,uint256,string) + function mintWithTokenURI( + address to, + uint256 tokenId, + string memory tokenUri + ) external returns (bool); - // @notice Count NFTs tracked by this contract - // @return A count of valid NFTs tracked by this contract, where each one of - // them has an assigned and queryable owner not equal to the zero address - // - // Selector: totalSupply() 18160ddd - function totalSupply() external view returns (uint256); + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x7d64bcb4, + /// or in textual repr: finishMinting() + function finishMinting() external returns (bool); } -// Selector: d74d154f +/// @title Unique extensions for ERC721. +/// @dev the ERC-165 identifier for this interface is 0xd74d154f interface ERC721UniqueExtensions is Dummy, ERC165 { - // @notice Transfer ownership of an NFT - // @dev Throws unless `msg.sender` is the current owner. Throws if `to` - // is the zero address. Throws if `tokenId` is not a valid NFT. - // @param to The new owner - // @param tokenId The NFT to transfer - // @param _value Not used for an NFT - // - // Selector: transfer(address,uint256) a9059cbb + /// @notice Transfer ownership of an NFT + /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` + /// is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param to The new owner + /// @param tokenId The NFT to transfer + /// @dev EVM selector for this function is: 0xa9059cbb, + /// or in textual repr: transfer(address,uint256) function transfer(address to, uint256 tokenId) external; - // @notice Burns a specific ERC721 token. - // @dev Throws unless `msg.sender` is the current owner or an authorized - // operator for this NFT. Throws if `from` is not the current owner. Throws - // if `to` is the zero address. Throws if `tokenId` is not a valid NFT. - // @param from The current owner of the NFT - // @param tokenId The NFT to transfer - // @param _value Not used for an NFT - // - // Selector: burnFrom(address,uint256) 79cc6790 + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this NFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param from The current owner of the NFT + /// @param tokenId The NFT to transfer + /// @dev EVM selector for this function is: 0x79cc6790, + /// or in textual repr: burnFrom(address,uint256) function burnFrom(address from, uint256 tokenId) external; - // @notice Returns next free NFT ID. - // - // Selector: nextTokenId() 75794a3c + /// @notice Returns next free NFT ID. + /// @dev EVM selector for this function is: 0x75794a3c, + /// or in textual repr: nextTokenId() function nextTokenId() external view returns (uint256); - // @notice Function to mint multiple tokens. - // @dev `tokenIds` should be an array of consecutive numbers and first number - // should be obtained with `nextTokenId` method - // @param to The new owner - // @param tokenIds IDs of the minted NFTs - // - // Selector: mintBulk(address,uint256[]) 44a9945e + /// @notice Function to mint multiple tokens. + /// @dev `tokenIds` should be an array of consecutive numbers and first number + /// should be obtained with `nextTokenId` method + /// @param to The new owner + /// @param tokenIds IDs of the minted NFTs + /// @dev EVM selector for this function is: 0x44a9945e, + /// or in textual repr: mintBulk(address,uint256[]) function mintBulk(address to, uint256[] memory tokenIds) external returns (bool); - // @notice Function to mint multiple tokens with the given tokenUris. - // @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive - // numbers and first number should be obtained with `nextTokenId` method - // @param to The new owner - // @param tokens array of pairs of token ID and token URI for minted tokens - // - // Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006 - function mintBulkWithTokenURI(address to, Tuple0[] memory tokens) + /// @notice Function to mint multiple tokens with the given tokenUris. + /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive + /// numbers and first number should be obtained with `nextTokenId` method + /// @param to The new owner + /// @param tokens array of pairs of token ID and token URI for minted tokens + /// @dev EVM selector for this function is: 0x36543006, + /// or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[]) + function mintBulkWithTokenURI(address to, Tuple8[] memory tokens) external returns (bool); } +/// @dev anonymous struct +struct Tuple8 { + uint256 field_0; + string field_1; +} + +/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// @dev the ERC-165 identifier for this interface is 0x780e9d63 +interface ERC721Enumerable is Dummy, ERC165 { + /// @notice Enumerate valid NFTs + /// @param index A counter less than `totalSupply()` + /// @return The token identifier for the `index`th NFT, + /// (sort order not specified) + /// @dev EVM selector for this function is: 0x4f6ccce7, + /// or in textual repr: tokenByIndex(uint256) + function tokenByIndex(uint256 index) external view returns (uint256); + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x2f745c59, + /// or in textual repr: tokenOfOwnerByIndex(address,uint256) + function tokenOfOwnerByIndex(address owner, uint256 index) + external + view + returns (uint256); + + /// @notice Count NFTs tracked by this contract + /// @return A count of valid NFTs tracked by this contract, where each one of + /// them has an assigned and queryable owner not equal to the zero address + /// @dev EVM selector for this function is: 0x18160ddd, + /// or in textual repr: totalSupply() + function totalSupply() external view returns (uint256); +} + +/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// @dev the ERC-165 identifier for this interface is 0x5b5e139f +interface ERC721Metadata is Dummy, ERC165 { + /// @notice A descriptive name for a collection of NFTs in this contract + /// @dev EVM selector for this function is: 0x06fdde03, + /// or in textual repr: name() + function name() external view returns (string memory); + + /// @notice An abbreviated name for NFTs in this contract + /// @dev EVM selector for this function is: 0x95d89b41, + /// or in textual repr: symbol() + function symbol() external view returns (string memory); + + /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. + /// + /// @dev If the token has a `url` property and it is not empty, it is returned. + /// Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`. + /// If the collection property `baseURI` is empty or absent, return "" (empty string) + /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix + /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings). + /// + /// @return token's const_metadata + /// @dev EVM selector for this function is: 0xc87b56dd, + /// or in textual repr: tokenURI(uint256) + function tokenURI(uint256 tokenId) external view returns (string memory); +} + +/// @dev inlined interface +interface ERC721Events { + event Transfer( + address indexed from, + address indexed to, + uint256 indexed tokenId + ); + event Approval( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); +} + +/// @title ERC-721 Non-Fungible Token Standard +/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md +/// @dev the ERC-165 identifier for this interface is 0x80ac58cd +interface ERC721 is Dummy, ERC165, ERC721Events { + /// @notice Count all NFTs assigned to an owner + /// @dev NFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param owner An address for whom to query the balance + /// @return The number of NFTs owned by `owner`, possibly zero + /// @dev EVM selector for this function is: 0x70a08231, + /// or in textual repr: balanceOf(address) + function balanceOf(address owner) external view returns (uint256); + + /// @notice Find the owner of an NFT + /// @dev NFTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// @param tokenId The identifier for an NFT + /// @return The address of the owner of the NFT + /// @dev EVM selector for this function is: 0x6352211e, + /// or in textual repr: ownerOf(uint256) + function ownerOf(uint256 tokenId) external view returns (address); + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0xb88d4fde, + /// or in textual repr: safeTransferFrom(address,address,uint256,bytes) + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) external; + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x42842e0e, + /// or in textual repr: safeTransferFrom(address,address,uint256) + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE + /// THEY MAY BE PERMANENTLY LOST + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this NFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT. + /// @param from The current owner of the NFT + /// @param to The new owner + /// @param tokenId The NFT to transfer + /// @dev EVM selector for this function is: 0x23b872dd, + /// or in textual repr: transferFrom(address,address,uint256) + function transferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /// @notice Set or reaffirm the approved address for an NFT + /// @dev The zero address indicates there is no approved address. + /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + /// operator of the current owner. + /// @param approved The new approved NFT controller + /// @param tokenId The NFT to approve + /// @dev EVM selector for this function is: 0x095ea7b3, + /// or in textual repr: approve(address,uint256) + function approve(address approved, uint256 tokenId) external; + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0xa22cb465, + /// or in textual repr: setApprovalForAll(address,bool) + function setApprovalForAll(address operator, bool approved) external; + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x081812fc, + /// or in textual repr: getApproved(uint256) + function getApproved(uint256 tokenId) external view returns (address); + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0xe985e9c5, + /// or in textual repr: isApprovedForAll(address,address) + function isApprovedForAll(address owner, address operator) + external + view + returns (address); +} + interface UniqueNFT is Dummy, ERC165, --- a/tests/src/eth/api/UniqueRefungible.sol +++ b/tests/src/eth/api/UniqueRefungible.sol @@ -3,13 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; -// Anonymous struct -struct Tuple0 { - uint256 field_0; - string field_1; -} - -// Common stubs holder +/// @dev common stubs holder interface Dummy { } @@ -18,40 +12,17 @@ function supportsInterface(bytes4 interfaceID) external view returns (bool); } -// Inline -interface ERC721Events { - event Transfer( - address indexed from, - address indexed to, - uint256 indexed tokenId - ); - event Approval( - address indexed owner, - address indexed approved, - uint256 indexed tokenId - ); - event ApprovalForAll( - address indexed owner, - address indexed operator, - bool approved - ); -} - -// Inline -interface ERC721MintableEvents { - event MintingFinished(); -} - -// Selector: 41369377 +/// @title A contract that allows to set and delete token properties and change token property permissions. +/// @dev the ERC-165 identifier for this interface is 0x41369377 interface TokenProperties is Dummy, ERC165 { - // @notice Set permissions for token property. - // @dev Throws error if `msg.sender` is not admin or owner of the collection. - // @param key Property key. - // @param is_mutable Permission to mutate property. - // @param collection_admin Permission to mutate property by collection admin if property is mutable. - // @param token_owner Permission to mutate property by token owner if property is mutable. - // - // Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa + /// @notice Set permissions for token property. + /// @dev Throws error if `msg.sender` is not admin or owner of the collection. + /// @param key Property key. + /// @param isMutable Permission to mutate property. + /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable. + /// @param tokenOwner Permission to mutate property by token owner if property is mutable. + /// @dev EVM selector for this function is: 0x222d97fa, + /// or in textual repr: setTokenPropertyPermission(string,bool,bool,bool) function setTokenPropertyPermission( string memory key, bool isMutable, @@ -59,447 +30,538 @@ bool tokenOwner ) external; - // @notice Set token property value. - // @dev Throws error if `msg.sender` has no permission to edit the property. - // @param tokenId ID of the token. - // @param key Property key. - // @param value Property value. - // - // Selector: setProperty(uint256,string,bytes) 1752d67b + /// @notice Set token property value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param key Property key. + /// @param value Property value. + /// @dev EVM selector for this function is: 0x1752d67b, + /// or in textual repr: setProperty(uint256,string,bytes) function setProperty( uint256 tokenId, string memory key, bytes memory value ) external; - // @notice Delete token property value. - // @dev Throws error if `msg.sender` has no permission to edit the property. - // @param tokenId ID of the token. - // @param key Property key. - // - // Selector: deleteProperty(uint256,string) 066111d1 + /// @notice Delete token property value. + /// @dev Throws error if `msg.sender` has no permission to edit the property. + /// @param tokenId ID of the token. + /// @param key Property key. + /// @dev EVM selector for this function is: 0x066111d1, + /// or in textual repr: deleteProperty(uint256,string) function deleteProperty(uint256 tokenId, string memory key) external; - // @notice Get token property value. - // @dev Throws error if key not found - // @param tokenId ID of the token. - // @param key Property key. - // @return Property value bytes - // - // Selector: property(uint256,string) 7228c327 + /// @notice Get token property value. + /// @dev Throws error if key not found + /// @param tokenId ID of the token. + /// @param key Property key. + /// @return Property value bytes + /// @dev EVM selector for this function is: 0x7228c327, + /// or in textual repr: property(uint256,string) function property(uint256 tokenId, string memory key) external view returns (bytes memory); } -// Selector: 42966c68 -interface ERC721Burnable is Dummy, ERC165 { - // @notice Burns a specific ERC721 token. - // @dev Throws unless `msg.sender` is the current RFT owner, or an authorized - // operator of the current owner. - // @param tokenId The RFT to approve - // - // Selector: burn(uint256) 42966c68 - function burn(uint256 tokenId) external; -} - -// Selector: 58800161 -interface ERC721 is Dummy, ERC165, ERC721Events { - // @notice Count all RFTs assigned to an owner - // @dev RFTs assigned to the zero address are considered invalid, and this - // function throws for queries about the zero address. - // @param owner An address for whom to query the balance - // @return The number of RFTs owned by `owner`, possibly zero - // - // Selector: balanceOf(address) 70a08231 - function balanceOf(address owner) external view returns (uint256); - - // @notice Find the owner of an RFT - // @dev RFTs assigned to zero address are considered invalid, and queries - // about them do throw. - // Returns special 0xffffffffffffffffffffffffffffffffffffffff address for - // the tokens that are partially owned. - // @param tokenId The identifier for an RFT - // @return The address of the owner of the RFT - // - // Selector: ownerOf(uint256) 6352211e - function ownerOf(uint256 tokenId) external view returns (address); - - // @dev Not implemented - // - // Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672 - function safeTransferFromWithData( - address from, - address to, - uint256 tokenId, - bytes memory data - ) external; - - // @dev Not implemented - // - // Selector: safeTransferFrom(address,address,uint256) 42842e0e - function safeTransferFrom( - address from, - address to, - uint256 tokenId - ) external; - - // @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE - // TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE - // THEY MAY BE PERMANENTLY LOST - // @dev Throws unless `msg.sender` is the current owner or an authorized - // operator for this RFT. Throws if `from` is not the current owner. Throws - // if `to` is the zero address. Throws if `tokenId` is not a valid RFT. - // Throws if RFT pieces have multiple owners. - // @param from The current owner of the NFT - // @param to The new owner - // @param tokenId The NFT to transfer - // @param _value Not used for an NFT - // - // Selector: transferFrom(address,address,uint256) 23b872dd - function transferFrom( - address from, - address to, - uint256 tokenId - ) external; - - // @dev Not implemented - // - // Selector: approve(address,uint256) 095ea7b3 - function approve(address approved, uint256 tokenId) external; - - // @dev Not implemented - // - // Selector: setApprovalForAll(address,bool) a22cb465 - function setApprovalForAll(address operator, bool approved) external; - - // @dev Not implemented - // - // Selector: getApproved(uint256) 081812fc - function getApproved(uint256 tokenId) external view returns (address); - - // @dev Not implemented - // - // Selector: isApprovedForAll(address,address) e985e9c5 - function isApprovedForAll(address owner, address operator) - external - view - returns (address); -} - -// Selector: 5b5e139f -interface ERC721Metadata is Dummy, ERC165 { - // @notice A descriptive name for a collection of RFTs in this contract - // - // Selector: name() 06fdde03 - function name() external view returns (string memory); - - // @notice An abbreviated name for RFTs in this contract - // - // Selector: symbol() 95d89b41 - function symbol() external view returns (string memory); - - // @notice A distinct Uniform Resource Identifier (URI) for a given asset. - // - // @dev If the token has a `url` property and it is not empty, it is returned. - // Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`. - // If the collection property `baseURI` is empty or absent, return "" (empty string) - // otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix - // otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings). - // - // @return token's const_metadata - // - // Selector: tokenURI(uint256) c87b56dd - function tokenURI(uint256 tokenId) external view returns (string memory); -} - -// Selector: 68ccfe89 -interface ERC721Mintable is Dummy, ERC165, ERC721MintableEvents { - // Selector: mintingFinished() 05d2035b - function mintingFinished() external view returns (bool); - - // @notice Function to mint token. - // @dev `tokenId` should be obtained with `nextTokenId` method, - // unlike standard, you can't specify it manually - // @param to The new owner - // @param tokenId ID of the minted RFT - // - // Selector: mint(address,uint256) 40c10f19 - function mint(address to, uint256 tokenId) external returns (bool); - - // @notice Function to mint token with the given tokenUri. - // @dev `tokenId` should be obtained with `nextTokenId` method, - // unlike standard, you can't specify it manually - // @param to The new owner - // @param tokenId ID of the minted RFT - // @param tokenUri Token URI that would be stored in the RFT properties - // - // Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f - function mintWithTokenURI( - address to, - uint256 tokenId, - string memory tokenUri - ) external returns (bool); - - // @dev Not implemented - // - // Selector: finishMinting() 7d64bcb4 - function finishMinting() external returns (bool); -} - -// Selector: 6cf113cd +/// @title A contract that allows you to work with collections. +/// @dev the ERC-165 identifier for this interface is 0xe54be640 interface Collection is Dummy, ERC165 { - // Set collection property. - // - // @param key Property key. - // @param value Propery value. - // - // Selector: setCollectionProperty(string,bytes) 2f073f66 + /// Set collection property. + /// + /// @param key Property key. + /// @param value Propery value. + /// @dev EVM selector for this function is: 0x2f073f66, + /// or in textual repr: setCollectionProperty(string,bytes) function setCollectionProperty(string memory key, bytes memory value) external; - // Delete collection property. - // - // @param key Property key. - // - // Selector: deleteCollectionProperty(string) 7b7debce + /// Delete collection property. + /// + /// @param key Property key. + /// @dev EVM selector for this function is: 0x7b7debce, + /// or in textual repr: deleteCollectionProperty(string) function deleteCollectionProperty(string memory key) external; - // Get collection property. - // - // @dev Throws error if key not found. - // - // @param key Property key. - // @return bytes The property corresponding to the key. - // - // Selector: collectionProperty(string) cf24fd6d + /// Get collection property. + /// + /// @dev Throws error if key not found. + /// + /// @param key Property key. + /// @return bytes The property corresponding to the key. + /// @dev EVM selector for this function is: 0xcf24fd6d, + /// or in textual repr: collectionProperty(string) function collectionProperty(string memory key) external view returns (bytes memory); - // Set the sponsor of the collection. - // - // @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. - // - // @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. - // - // Selector: setCollectionSponsor(address) 7623402e + /// Set the sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0x7623402e, + /// or in textual repr: setCollectionSponsor(address) function setCollectionSponsor(address sponsor) external; - // Collection sponsorship confirmation. - // - // @dev After setting the sponsor for the collection, it must be confirmed with this function. - // - // Selector: confirmCollectionSponsorship() 3c50e97a + /// Set the substrate sponsor of the collection. + /// + /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor. + /// + /// @param sponsor Substrate address of the sponsor from whose account funds will be debited for operations with the contract. + /// @dev EVM selector for this function is: 0xc74d6751, + /// or in textual repr: setCollectionSponsorSubstrate(uint256) + function setCollectionSponsorSubstrate(uint256 sponsor) external; + + /// @dev EVM selector for this function is: 0x058ac185, + /// or in textual repr: hasCollectionPendingSponsor() + function hasCollectionPendingSponsor() external view returns (bool); + + /// Collection sponsorship confirmation. + /// + /// @dev After setting the sponsor for the collection, it must be confirmed with this function. + /// @dev EVM selector for this function is: 0x3c50e97a, + /// or in textual repr: confirmCollectionSponsorship() function confirmCollectionSponsorship() external; - // Set limits for the collection. - // @dev Throws error if limit not found. - // @param limit Name of the limit. Valid names: - // "accountTokenOwnershipLimit", - // "sponsoredDataSize", - // "sponsoredDataRateLimit", - // "tokenLimit", - // "sponsorTransferTimeout", - // "sponsorApproveTimeout" - // @param value Value of the limit. - // - // Selector: setCollectionLimit(string,uint32) 6a3841db + /// Remove collection sponsor. + /// @dev EVM selector for this function is: 0x6e0326a3, + /// or in textual repr: removeCollectionSponsor() + function removeCollectionSponsor() external; + + /// Get current sponsor. + /// + /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw. + /// @dev EVM selector for this function is: 0xb66bbc14, + /// or in textual repr: getCollectionSponsor() + function getCollectionSponsor() external view returns (Tuple17 memory); + + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "accountTokenOwnershipLimit", + /// "sponsoredDataSize", + /// "sponsoredDataRateLimit", + /// "tokenLimit", + /// "sponsorTransferTimeout", + /// "sponsorApproveTimeout" + /// @param value Value of the limit. + /// @dev EVM selector for this function is: 0x6a3841db, + /// or in textual repr: setCollectionLimit(string,uint32) function setCollectionLimit(string memory limit, uint32 value) external; - // Set limits for the collection. - // @dev Throws error if limit not found. - // @param limit Name of the limit. Valid names: - // "ownerCanTransfer", - // "ownerCanDestroy", - // "transfersEnabled" - // @param value Value of the limit. - // - // Selector: setCollectionLimit(string,bool) 993b7fba + /// Set limits for the collection. + /// @dev Throws error if limit not found. + /// @param limit Name of the limit. Valid names: + /// "ownerCanTransfer", + /// "ownerCanDestroy", + /// "transfersEnabled" + /// @param value Value of the limit. + /// @dev EVM selector for this function is: 0x993b7fba, + /// or in textual repr: setCollectionLimit(string,bool) function setCollectionLimit(string memory limit, bool value) external; - // Get contract address. - // - // Selector: contractAddress() f6b4dfb4 + /// Get contract address. + /// @dev EVM selector for this function is: 0xf6b4dfb4, + /// or in textual repr: contractAddress() function contractAddress() external view returns (address); - // Add collection admin by substrate address. - // @param new_admin Substrate administrator address. - // - // Selector: addCollectionAdminSubstrate(uint256) 5730062b + /// Add collection admin by substrate address. + /// @param newAdmin Substrate administrator address. + /// @dev EVM selector for this function is: 0x5730062b, + /// or in textual repr: addCollectionAdminSubstrate(uint256) function addCollectionAdminSubstrate(uint256 newAdmin) external; - // Remove collection admin by substrate address. - // @param admin Substrate administrator address. - // - // Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9 + /// Remove collection admin by substrate address. + /// @param admin Substrate administrator address. + /// @dev EVM selector for this function is: 0x4048fcf9, + /// or in textual repr: removeCollectionAdminSubstrate(uint256) function removeCollectionAdminSubstrate(uint256 admin) external; - // Add collection admin. - // @param new_admin Address of the added administrator. - // - // Selector: addCollectionAdmin(address) 92e462c7 + /// Add collection admin. + /// @param newAdmin Address of the added administrator. + /// @dev EVM selector for this function is: 0x92e462c7, + /// or in textual repr: addCollectionAdmin(address) function addCollectionAdmin(address newAdmin) external; - // Remove collection admin. - // - // @param new_admin Address of the removed administrator. - // - // Selector: removeCollectionAdmin(address) fafd7b42 + /// Remove collection admin. + /// + /// @param admin Address of the removed administrator. + /// @dev EVM selector for this function is: 0xfafd7b42, + /// or in textual repr: removeCollectionAdmin(address) function removeCollectionAdmin(address admin) external; - // Toggle accessibility of collection nesting. - // - // @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' - // - // Selector: setCollectionNesting(bool) 112d4586 + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled' + /// @dev EVM selector for this function is: 0x112d4586, + /// or in textual repr: setCollectionNesting(bool) function setCollectionNesting(bool enable) external; - // Toggle accessibility of collection nesting. - // - // @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' - // @param collections Addresses of collections that will be available for nesting. - // - // Selector: setCollectionNesting(bool,address[]) 64872396 + /// Toggle accessibility of collection nesting. + /// + /// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled' + /// @param collections Addresses of collections that will be available for nesting. + /// @dev EVM selector for this function is: 0x64872396, + /// or in textual repr: setCollectionNesting(bool,address[]) function setCollectionNesting(bool enable, address[] memory collections) external; - // Set the collection access method. - // @param mode Access mode - // 0 for Normal - // 1 for AllowList - // - // Selector: setCollectionAccess(uint8) 41835d4c + /// Set the collection access method. + /// @param mode Access mode + /// 0 for Normal + /// 1 for AllowList + /// @dev EVM selector for this function is: 0x41835d4c, + /// or in textual repr: setCollectionAccess(uint8) function setCollectionAccess(uint8 mode) external; - // Add the user to the allowed list. - // - // @param user Address of a trusted user. - // - // Selector: addToCollectionAllowList(address) 67844fe6 + /// Add the user to the allowed list. + /// + /// @param user Address of a trusted user. + /// @dev EVM selector for this function is: 0x67844fe6, + /// or in textual repr: addToCollectionAllowList(address) function addToCollectionAllowList(address user) external; - // Remove the user from the allowed list. - // - // @param user Address of a removed user. - // - // Selector: removeFromCollectionAllowList(address) 85c51acb + /// Remove the user from the allowed list. + /// + /// @param user Address of a removed user. + /// @dev EVM selector for this function is: 0x85c51acb, + /// or in textual repr: removeFromCollectionAllowList(address) function removeFromCollectionAllowList(address user) external; - // Switch permission for minting. - // - // @param mode Enable if "true". - // - // Selector: setCollectionMintMode(bool) 00018e84 + /// Switch permission for minting. + /// + /// @param mode Enable if "true". + /// @dev EVM selector for this function is: 0x00018e84, + /// or in textual repr: setCollectionMintMode(bool) function setCollectionMintMode(bool mode) external; - // Check that account is the owner or admin of the collection - // - // @param user account to verify - // @return "true" if account is the owner or admin - // - // Selector: verifyOwnerOrAdmin(address) c2282493 - function verifyOwnerOrAdmin(address user) external view returns (bool); + /// Check that account is the owner or admin of the collection + /// + /// @param user account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x9811b0c7, + /// or in textual repr: isOwnerOrAdmin(address) + function isOwnerOrAdmin(address user) external view returns (bool); - // Returns collection type - // - // @return `Fungible` or `NFT` or `ReFungible` - // - // Selector: uniqueCollectionType() d34b55b8 + /// Check that substrate account is the owner or admin of the collection + /// + /// @param user account to verify + /// @return "true" if account is the owner or admin + /// @dev EVM selector for this function is: 0x68910e00, + /// or in textual repr: isOwnerOrAdminSubstrate(uint256) + function isOwnerOrAdminSubstrate(uint256 user) external view returns (bool); + + /// Returns collection type + /// + /// @return `Fungible` or `NFT` or `ReFungible` + /// @dev EVM selector for this function is: 0xd34b55b8, + /// or in textual repr: uniqueCollectionType() function uniqueCollectionType() external returns (string memory); + + /// Changes collection owner to another account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner account + /// @dev EVM selector for this function is: 0x13af4035, + /// or in textual repr: setOwner(address) + function setOwner(address newOwner) external; + + /// Changes collection owner to another substrate account + /// + /// @dev Owner can be changed only by current owner + /// @param newOwner new owner substrate account + /// @dev EVM selector for this function is: 0xb212138f, + /// or in textual repr: setOwnerSubstrate(uint256) + function setOwnerSubstrate(uint256 newOwner) external; +} + +/// @dev anonymous struct +struct Tuple17 { + address field_0; + uint256 field_1; } -// Selector: 780e9d63 -interface ERC721Enumerable is Dummy, ERC165 { - // @notice Enumerate valid RFTs - // @param index A counter less than `totalSupply()` - // @return The token identifier for the `index`th NFT, - // (sort order not specified) - // - // Selector: tokenByIndex(uint256) 4f6ccce7 - function tokenByIndex(uint256 index) external view returns (uint256); +/// @title ERC721 Token that can be irreversibly burned (destroyed). +/// @dev the ERC-165 identifier for this interface is 0x42966c68 +interface ERC721Burnable is Dummy, ERC165 { + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized + /// operator of the current owner. + /// @param tokenId The RFT to approve + /// @dev EVM selector for this function is: 0x42966c68, + /// or in textual repr: burn(uint256) + function burn(uint256 tokenId) external; +} + +/// @dev inlined interface +interface ERC721MintableEvents { + event MintingFinished(); +} + +/// @title ERC721 minting logic. +/// @dev the ERC-165 identifier for this interface is 0x68ccfe89 +interface ERC721Mintable is Dummy, ERC165, ERC721MintableEvents { + /// @dev EVM selector for this function is: 0x05d2035b, + /// or in textual repr: mintingFinished() + function mintingFinished() external view returns (bool); - // Not implemented - // - // Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59 - function tokenOfOwnerByIndex(address owner, uint256 index) - external - view - returns (uint256); + /// @notice Function to mint token. + /// @dev `tokenId` should be obtained with `nextTokenId` method, + /// unlike standard, you can't specify it manually + /// @param to The new owner + /// @param tokenId ID of the minted RFT + /// @dev EVM selector for this function is: 0x40c10f19, + /// or in textual repr: mint(address,uint256) + function mint(address to, uint256 tokenId) external returns (bool); + + /// @notice Function to mint token with the given tokenUri. + /// @dev `tokenId` should be obtained with `nextTokenId` method, + /// unlike standard, you can't specify it manually + /// @param to The new owner + /// @param tokenId ID of the minted RFT + /// @param tokenUri Token URI that would be stored in the RFT properties + /// @dev EVM selector for this function is: 0x50bb4e7f, + /// or in textual repr: mintWithTokenURI(address,uint256,string) + function mintWithTokenURI( + address to, + uint256 tokenId, + string memory tokenUri + ) external returns (bool); - // @notice Count RFTs tracked by this contract - // @return A count of valid RFTs tracked by this contract, where each one of - // them has an assigned and queryable owner not equal to the zero address - // - // Selector: totalSupply() 18160ddd - function totalSupply() external view returns (uint256); + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x7d64bcb4, + /// or in textual repr: finishMinting() + function finishMinting() external returns (bool); } -// Selector: 7c3bef89 +/// @title Unique extensions for ERC721. +/// @dev the ERC-165 identifier for this interface is 0x7c3bef89 interface ERC721UniqueExtensions is Dummy, ERC165 { - // @notice Transfer ownership of an RFT - // @dev Throws unless `msg.sender` is the current owner. Throws if `to` - // is the zero address. Throws if `tokenId` is not a valid RFT. - // Throws if RFT pieces have multiple owners. - // @param to The new owner - // @param tokenId The RFT to transfer - // @param _value Not used for an RFT - // - // Selector: transfer(address,uint256) a9059cbb + /// @notice Transfer ownership of an RFT + /// @dev Throws unless `msg.sender` is the current owner. Throws if `to` + /// is the zero address. Throws if `tokenId` is not a valid RFT. + /// Throws if RFT pieces have multiple owners. + /// @param to The new owner + /// @param tokenId The RFT to transfer + /// @dev EVM selector for this function is: 0xa9059cbb, + /// or in textual repr: transfer(address,uint256) function transfer(address to, uint256 tokenId) external; - // @notice Burns a specific ERC721 token. - // @dev Throws unless `msg.sender` is the current owner or an authorized - // operator for this RFT. Throws if `from` is not the current owner. Throws - // if `to` is the zero address. Throws if `tokenId` is not a valid RFT. - // Throws if RFT pieces have multiple owners. - // @param from The current owner of the RFT - // @param tokenId The RFT to transfer - // @param _value Not used for an RFT - // - // Selector: burnFrom(address,uint256) 79cc6790 + /// @notice Burns a specific ERC721 token. + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this RFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT. + /// Throws if RFT pieces have multiple owners. + /// @param from The current owner of the RFT + /// @param tokenId The RFT to transfer + /// @dev EVM selector for this function is: 0x79cc6790, + /// or in textual repr: burnFrom(address,uint256) function burnFrom(address from, uint256 tokenId) external; - // @notice Returns next free RFT ID. - // - // Selector: nextTokenId() 75794a3c + /// @notice Returns next free RFT ID. + /// @dev EVM selector for this function is: 0x75794a3c, + /// or in textual repr: nextTokenId() function nextTokenId() external view returns (uint256); - // @notice Function to mint multiple tokens. - // @dev `tokenIds` should be an array of consecutive numbers and first number - // should be obtained with `nextTokenId` method - // @param to The new owner - // @param tokenIds IDs of the minted RFTs - // - // Selector: mintBulk(address,uint256[]) 44a9945e + /// @notice Function to mint multiple tokens. + /// @dev `tokenIds` should be an array of consecutive numbers and first number + /// should be obtained with `nextTokenId` method + /// @param to The new owner + /// @param tokenIds IDs of the minted RFTs + /// @dev EVM selector for this function is: 0x44a9945e, + /// or in textual repr: mintBulk(address,uint256[]) function mintBulk(address to, uint256[] memory tokenIds) external returns (bool); - // @notice Function to mint multiple tokens with the given tokenUris. - // @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive - // numbers and first number should be obtained with `nextTokenId` method - // @param to The new owner - // @param tokens array of pairs of token ID and token URI for minted tokens - // - // Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006 - function mintBulkWithTokenURI(address to, Tuple0[] memory tokens) + /// @notice Function to mint multiple tokens with the given tokenUris. + /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive + /// numbers and first number should be obtained with `nextTokenId` method + /// @param to The new owner + /// @param tokens array of pairs of token ID and token URI for minted tokens + /// @dev EVM selector for this function is: 0x36543006, + /// or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[]) + function mintBulkWithTokenURI(address to, Tuple8[] memory tokens) external returns (bool); - // Returns EVM address for refungible token - // - // @param token ID of the token - // - // Selector: tokenContractAddress(uint256) ab76fac6 + /// Returns EVM address for refungible token + /// + /// @param token ID of the token + /// @dev EVM selector for this function is: 0xab76fac6, + /// or in textual repr: tokenContractAddress(uint256) function tokenContractAddress(uint256 token) external view returns (address); } +/// @dev anonymous struct +struct Tuple8 { + uint256 field_0; + string field_1; +} + +/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// @dev the ERC-165 identifier for this interface is 0x780e9d63 +interface ERC721Enumerable is Dummy, ERC165 { + /// @notice Enumerate valid RFTs + /// @param index A counter less than `totalSupply()` + /// @return The token identifier for the `index`th NFT, + /// (sort order not specified) + /// @dev EVM selector for this function is: 0x4f6ccce7, + /// or in textual repr: tokenByIndex(uint256) + function tokenByIndex(uint256 index) external view returns (uint256); + + /// Not implemented + /// @dev EVM selector for this function is: 0x2f745c59, + /// or in textual repr: tokenOfOwnerByIndex(address,uint256) + function tokenOfOwnerByIndex(address owner, uint256 index) + external + view + returns (uint256); + + /// @notice Count RFTs tracked by this contract + /// @return A count of valid RFTs tracked by this contract, where each one of + /// them has an assigned and queryable owner not equal to the zero address + /// @dev EVM selector for this function is: 0x18160ddd, + /// or in textual repr: totalSupply() + function totalSupply() external view returns (uint256); +} + +/// @dev the ERC-165 identifier for this interface is 0x5b5e139f +interface ERC721Metadata is Dummy, ERC165 { + /// @notice A descriptive name for a collection of RFTs in this contract + /// @dev EVM selector for this function is: 0x06fdde03, + /// or in textual repr: name() + function name() external view returns (string memory); + + /// @notice An abbreviated name for RFTs in this contract + /// @dev EVM selector for this function is: 0x95d89b41, + /// or in textual repr: symbol() + function symbol() external view returns (string memory); + + /// @notice A distinct Uniform Resource Identifier (URI) for a given asset. + /// + /// @dev If the token has a `url` property and it is not empty, it is returned. + /// Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`. + /// If the collection property `baseURI` is empty or absent, return "" (empty string) + /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix + /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings). + /// + /// @return token's const_metadata + /// @dev EVM selector for this function is: 0xc87b56dd, + /// or in textual repr: tokenURI(uint256) + function tokenURI(uint256 tokenId) external view returns (string memory); +} + +/// @dev inlined interface +interface ERC721Events { + event Transfer( + address indexed from, + address indexed to, + uint256 indexed tokenId + ); + event Approval( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + event ApprovalForAll( + address indexed owner, + address indexed operator, + bool approved + ); +} + +/// @title ERC-721 Non-Fungible Token Standard +/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md +/// @dev the ERC-165 identifier for this interface is 0x58800161 +interface ERC721 is Dummy, ERC165, ERC721Events { + /// @notice Count all RFTs assigned to an owner + /// @dev RFTs assigned to the zero address are considered invalid, and this + /// function throws for queries about the zero address. + /// @param owner An address for whom to query the balance + /// @return The number of RFTs owned by `owner`, possibly zero + /// @dev EVM selector for this function is: 0x70a08231, + /// or in textual repr: balanceOf(address) + function balanceOf(address owner) external view returns (uint256); + + /// @notice Find the owner of an RFT + /// @dev RFTs assigned to zero address are considered invalid, and queries + /// about them do throw. + /// Returns special 0xffffffffffffffffffffffffffffffffffffffff address for + /// the tokens that are partially owned. + /// @param tokenId The identifier for an RFT + /// @return The address of the owner of the RFT + /// @dev EVM selector for this function is: 0x6352211e, + /// or in textual repr: ownerOf(uint256) + function ownerOf(uint256 tokenId) external view returns (address); + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x60a11672, + /// or in textual repr: safeTransferFromWithData(address,address,uint256,bytes) + function safeTransferFromWithData( + address from, + address to, + uint256 tokenId, + bytes memory data + ) external; + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x42842e0e, + /// or in textual repr: safeTransferFrom(address,address,uint256) + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE + /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE + /// THEY MAY BE PERMANENTLY LOST + /// @dev Throws unless `msg.sender` is the current owner or an authorized + /// operator for this RFT. Throws if `from` is not the current owner. Throws + /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT. + /// Throws if RFT pieces have multiple owners. + /// @param from The current owner of the NFT + /// @param to The new owner + /// @param tokenId The NFT to transfer + /// @dev EVM selector for this function is: 0x23b872dd, + /// or in textual repr: transferFrom(address,address,uint256) + function transferFrom( + address from, + address to, + uint256 tokenId + ) external; + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x095ea7b3, + /// or in textual repr: approve(address,uint256) + function approve(address approved, uint256 tokenId) external; + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0xa22cb465, + /// or in textual repr: setApprovalForAll(address,bool) + function setApprovalForAll(address operator, bool approved) external; + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0x081812fc, + /// or in textual repr: getApproved(uint256) + function getApproved(uint256 tokenId) external view returns (address); + + /// @dev Not implemented + /// @dev EVM selector for this function is: 0xe985e9c5, + /// or in textual repr: isApprovedForAll(address,address) + function isApprovedForAll(address owner, address operator) + external + view + returns (address); +} + interface UniqueRefungible is Dummy, ERC165, --- a/tests/src/eth/api/UniqueRefungibleToken.sol +++ b/tests/src/eth/api/UniqueRefungibleToken.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; -// Common stubs holder +/// @dev common stubs holder interface Dummy { } @@ -12,7 +12,36 @@ function supportsInterface(bytes4 interfaceID) external view returns (bool); } -// Inline +/// @dev the ERC-165 identifier for this interface is 0x5755c3f2 +interface ERC1633 is Dummy, ERC165 { + /// @dev EVM selector for this function is: 0x80a54001, + /// or in textual repr: parentToken() + function parentToken() external view returns (address); + + /// @dev EVM selector for this function is: 0xd7f083f3, + /// or in textual repr: parentTokenId() + function parentTokenId() external view returns (uint256); +} + +/// @dev the ERC-165 identifier for this interface is 0xab8deb37 +interface ERC20UniqueExtensions is Dummy, ERC165 { + /// @dev Function that burns an amount of the token of a given account, + /// deducting from the sender's allowance for said account. + /// @param from The account whose tokens will be burnt. + /// @param amount The amount that will be burnt. + /// @dev EVM selector for this function is: 0x79cc6790, + /// or in textual repr: burnFrom(address,uint256) + function burnFrom(address from, uint256 amount) external returns (bool); + + /// @dev Function that changes total amount of the tokens. + /// Throws if `msg.sender` doesn't owns all of the tokens. + /// @param amount New total amount of the tokens. + /// @dev EVM selector for this function is: 0xd2418ca7, + /// or in textual repr: repartition(uint256) + function repartition(uint256 amount) external returns (bool); +} + +/// @dev inlined interface interface ERC20Events { event Transfer(address indexed from, address indexed to, uint256 value); event Approval( @@ -22,110 +51,79 @@ ); } -// Selector: 042f1106 -interface ERC1633UniqueExtensions is Dummy, ERC165 { - // Selector: setParentNFT(address,uint256) 042f1106 - function setParentNFT(address collection, uint256 nftId) - external - returns (bool); -} - -// Selector: 5755c3f2 -interface ERC1633 is Dummy, ERC165 { - // Selector: parentToken() 80a54001 - function parentToken() external view returns (address); - - // Selector: parentTokenId() d7f083f3 - function parentTokenId() external view returns (uint256); -} - -// Selector: 942e8b22 +/// @title Standard ERC20 token +/// +/// @dev Implementation of the basic standard token. +/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md +/// @dev the ERC-165 identifier for this interface is 0x942e8b22 interface ERC20 is Dummy, ERC165, ERC20Events { - // @return the name of the token. - // - // Selector: name() 06fdde03 + /// @return the name of the token. + /// @dev EVM selector for this function is: 0x06fdde03, + /// or in textual repr: name() function name() external view returns (string memory); - // @return the symbol of the token. - // - // Selector: symbol() 95d89b41 + /// @return the symbol of the token. + /// @dev EVM selector for this function is: 0x95d89b41, + /// or in textual repr: symbol() function symbol() external view returns (string memory); - // @dev Total number of tokens in existence - // - // Selector: totalSupply() 18160ddd + /// @dev Total number of tokens in existence + /// @dev EVM selector for this function is: 0x18160ddd, + /// or in textual repr: totalSupply() function totalSupply() external view returns (uint256); - // @dev Not supported - // - // Selector: decimals() 313ce567 + /// @dev Not supported + /// @dev EVM selector for this function is: 0x313ce567, + /// or in textual repr: decimals() function decimals() external view returns (uint8); - // @dev Gets the balance of the specified address. - // @param owner The address to query the balance of. - // @return An uint256 representing the amount owned by the passed address. - // - // Selector: balanceOf(address) 70a08231 + /// @dev Gets the balance of the specified address. + /// @param owner The address to query the balance of. + /// @return An uint256 representing the amount owned by the passed address. + /// @dev EVM selector for this function is: 0x70a08231, + /// or in textual repr: balanceOf(address) function balanceOf(address owner) external view returns (uint256); - // @dev Transfer token for a specified address - // @param to The address to transfer to. - // @param amount The amount to be transferred. - // - // Selector: transfer(address,uint256) a9059cbb + /// @dev Transfer token for a specified address + /// @param to The address to transfer to. + /// @param amount The amount to be transferred. + /// @dev EVM selector for this function is: 0xa9059cbb, + /// or in textual repr: transfer(address,uint256) function transfer(address to, uint256 amount) external returns (bool); - // @dev Transfer tokens from one address to another - // @param from address The address which you want to send tokens from - // @param to address The address which you want to transfer to - // @param amount uint256 the amount of tokens to be transferred - // - // Selector: transferFrom(address,address,uint256) 23b872dd + /// @dev Transfer tokens from one address to another + /// @param from address The address which you want to send tokens from + /// @param to address The address which you want to transfer to + /// @param amount uint256 the amount of tokens to be transferred + /// @dev EVM selector for this function is: 0x23b872dd, + /// or in textual repr: transferFrom(address,address,uint256) function transferFrom( address from, address to, uint256 amount ) external returns (bool); - // @dev Approve the passed address to spend the specified amount of tokens on behalf of `msg.sender`. - // Beware that changing an allowance with this method brings the risk that someone may use both the old - // and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this - // race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: - // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - // @param spender The address which will spend the funds. - // @param amount The amount of tokens to be spent. - // - // Selector: approve(address,uint256) 095ea7b3 + /// @dev Approve the passed address to spend the specified amount of tokens on behalf of `msg.sender`. + /// Beware that changing an allowance with this method brings the risk that someone may use both the old + /// and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this + /// race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: + /// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + /// @param spender The address which will spend the funds. + /// @param amount The amount of tokens to be spent. + /// @dev EVM selector for this function is: 0x095ea7b3, + /// or in textual repr: approve(address,uint256) function approve(address spender, uint256 amount) external returns (bool); - // @dev Function to check the amount of tokens that an owner allowed to a spender. - // @param owner address The address which owns the funds. - // @param spender address The address which will spend the funds. - // @return A uint256 specifying the amount of tokens still available for the spender. - // - // Selector: allowance(address,address) dd62ed3e + /// @dev Function to check the amount of tokens that an owner allowed to a spender. + /// @param owner address The address which owns the funds. + /// @param spender address The address which will spend the funds. + /// @return A uint256 specifying the amount of tokens still available for the spender. + /// @dev EVM selector for this function is: 0xdd62ed3e, + /// or in textual repr: allowance(address,address) function allowance(address owner, address spender) external view returns (uint256); -} - -// Selector: ab8deb37 -interface ERC20UniqueExtensions is Dummy, ERC165 { - // @dev Function that burns an amount of the token of a given account, - // deducting from the sender's allowance for said account. - // @param from The account whose tokens will be burnt. - // @param amount The amount that will be burnt. - // - // Selector: burnFrom(address,uint256) 79cc6790 - function burnFrom(address from, uint256 amount) external returns (bool); - - // @dev Function that changes total amount of the tokens. - // Throws if `msg.sender` doesn't owns all of the tokens. - // @param amount New total amount of the tokens. - // - // Selector: repartition(uint256) d2418ca7 - function repartition(uint256 amount) external returns (bool); } interface UniqueRefungibleToken is @@ -133,6 +131,5 @@ ERC165, ERC20, ERC20UniqueExtensions, - ERC1633, - ERC1633UniqueExtensions + ERC1633 {} --- a/tests/src/eth/base.test.ts +++ b/tests/src/eth/base.test.ts @@ -94,7 +94,7 @@ }); itWeb3('ERC721 support', async ({web3}) => { - expect(await contract(web3).methods.supportsInterface('0x58800161').call()).to.be.true; + expect(await contract(web3).methods.supportsInterface('0x780e9d63').call()).to.be.true; }); itWeb3('ERC721Metadata support', async ({web3}) => { --- a/tests/src/eth/collectionAdmin.test.ts +++ b/tests/src/eth/collectionAdmin.test.ts @@ -15,6 +15,7 @@ import {expect} from 'chai'; import privateKey from '../substrate/privateKey'; +import { UNIQUE } from '../util/helpers'; import { createEthAccount, createEthAccountWithBalance, @@ -22,6 +23,8 @@ evmCollectionHelpers, getCollectionAddressFromResult, itWeb3, + recordEthFee, + subToEth, } from './util/helpers'; describe('Add collection admins', () => { @@ -71,9 +74,9 @@ const newAdmin = createEthAccount(web3); const collectionEvm = evmCollection(web3, owner, collectionIdAddress); - expect(await collectionEvm.methods.verifyOwnerOrAdmin(newAdmin).call()).to.be.false; + expect(await collectionEvm.methods.isOwnerOrAdmin(newAdmin).call()).to.be.false; await collectionEvm.methods.addCollectionAdmin(newAdmin).send(); - expect(await collectionEvm.methods.verifyOwnerOrAdmin(newAdmin).call()).to.be.true; + expect(await collectionEvm.methods.isOwnerOrAdmin(newAdmin).call()).to.be.true; }); itWeb3('(!negative tests!) Add admin by ADMIN is not allowed', async ({api, web3, privateKeyWrapper}) => { @@ -309,4 +312,102 @@ expect(adminList[0].asSubstrate.toString().toLocaleLowerCase()) .to.be.eq(adminSub.address.toLocaleLowerCase()); }); +}); + +describe('Change owner tests', () => { + itWeb3('Change owner', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const newOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelper = evmCollectionHelpers(web3, owner); + const result = await collectionHelper.methods + .createNonfungibleCollection('A', 'B', 'C') + .send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + + await collectionEvm.methods.setOwner(newOwner).send(); + + expect(await collectionEvm.methods.isOwnerOrAdmin(owner).call()).to.be.false; + expect(await collectionEvm.methods.isOwnerOrAdmin(newOwner).call()).to.be.true; + }); + + itWeb3('change owner call fee', async ({web3, api, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const newOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelper = evmCollectionHelpers(web3, owner); + const result = await collectionHelper.methods + .createNonfungibleCollection('A', 'B', 'C') + .send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + + const cost = await recordEthFee(api, owner, () => collectionEvm.methods.setOwner(newOwner).send()); + expect(cost < BigInt(0.2 * Number(UNIQUE))); + expect(cost > 0); + }); + + itWeb3('(!negative tests!) call setOwner by non owner', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const newOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelper = evmCollectionHelpers(web3, owner); + const result = await collectionHelper.methods + .createNonfungibleCollection('A', 'B', 'C') + .send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + + await expect(collectionEvm.methods.setOwner(newOwner).send({from: newOwner})).to.be.rejected; + expect(await collectionEvm.methods.isOwnerOrAdmin(newOwner).call()).to.be.false; + }); +}); + +describe('Change substrate owner tests', () => { + itWeb3('Change owner', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const newOwner = privateKeyWrapper('//Alice'); + const collectionHelper = evmCollectionHelpers(web3, owner); + const result = await collectionHelper.methods + .createNonfungibleCollection('A', 'B', 'C') + .send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + + expect(await collectionEvm.methods.isOwnerOrAdmin(owner).call()).to.be.true; + expect(await collectionEvm.methods.isOwnerOrAdminSubstrate(newOwner.addressRaw).call()).to.be.false; + + await collectionEvm.methods.setOwnerSubstrate(newOwner.addressRaw).send(); + + expect(await collectionEvm.methods.isOwnerOrAdmin(owner).call()).to.be.false; + expect(await collectionEvm.methods.isOwnerOrAdminSubstrate(newOwner.addressRaw).call()).to.be.true; + }); + + itWeb3('change owner call fee', async ({web3, api, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const newOwner = privateKeyWrapper('//Alice'); + const collectionHelper = evmCollectionHelpers(web3, owner); + const result = await collectionHelper.methods + .createNonfungibleCollection('A', 'B', 'C') + .send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + + const cost = await recordEthFee(api, owner, () => collectionEvm.methods.setOwnerSubstrate(newOwner.addressRaw).send()); + expect(cost < BigInt(0.2 * Number(UNIQUE))); + expect(cost > 0); + }); + + itWeb3('(!negative tests!) call setOwner by non owner', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const otherReceiver = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const newOwner = privateKeyWrapper('//Alice'); + const collectionHelper = evmCollectionHelpers(web3, owner); + const result = await collectionHelper.methods + .createNonfungibleCollection('A', 'B', 'C') + .send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + + await expect(collectionEvm.methods.setOwnerSubstrate(newOwner.addressRaw).send({from: otherReceiver})).to.be.rejected; + expect(await collectionEvm.methods.isOwnerOrAdminSubstrate(newOwner.addressRaw).call()).to.be.false; + }); }); \ No newline at end of file --- a/tests/src/eth/collectionSponsoring.test.ts +++ b/tests/src/eth/collectionSponsoring.test.ts @@ -1,7 +1,10 @@ -import {addToAllowListExpectSuccess, confirmSponsorshipExpectSuccess, createCollectionExpectSuccess, enablePublicMintingExpectSuccess, setCollectionSponsorExpectSuccess} from '../util/helpers'; -import {itWeb3, createEthAccount, collectionIdToAddress, GAS_ARGS, normalizeEvents} from './util/helpers'; +import {addToAllowListExpectSuccess, bigIntToSub, confirmSponsorshipExpectSuccess, createCollectionExpectSuccess, enablePublicMintingExpectSuccess, getDetailedCollectionInfo, setCollectionSponsorExpectSuccess} from '../util/helpers'; +import {itWeb3, createEthAccount, collectionIdToAddress, GAS_ARGS, normalizeEvents, createEthAccountWithBalance, evmCollectionHelpers, getCollectionAddressFromResult, evmCollection, ethBalanceViaSub, subToEth} from './util/helpers'; import nonFungibleAbi from './nonFungibleAbi.json'; import {expect} from 'chai'; +import {evmToAddress} from '@polkadot/util-crypto'; +import {submitTransactionAsync} from '../substrate/substrate-api'; +import getBalance from '../substrate/get-balance'; describe('evm collection sponsoring', () => { itWeb3('sponsors mint transactions', async ({web3, privateKeyWrapper}) => { @@ -36,4 +39,226 @@ }, ]); }); + + itWeb3('Set substrate sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const sponsor = privateKeyWrapper('//Alice'); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + + expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false; + result = await collectionEvm.methods.setCollectionSponsorSubstrate(sponsor.addressRaw).send({from: owner}); + expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.true; + + const confirmTx = await api.tx.unique.confirmSponsorship(collectionId); + await submitTransactionAsync(sponsor, confirmTx); + expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false; + + const sponsorTuple = await collectionEvm.methods.getCollectionSponsor().call({from: owner}); + expect(bigIntToSub(api, BigInt(sponsorTuple[1]))).to.be.eq(sponsor.address); + }); + + itWeb3('Remove sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); + const {collectionIdAddress} = await getCollectionAddressFromResult(api, result); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + + expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false; + result = await collectionEvm.methods.setCollectionSponsor(sponsor).send({from: owner}); + expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.true; + + await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor}); + expect(await collectionEvm.methods.hasCollectionPendingSponsor().call({from: owner})).to.be.false; + + await collectionEvm.methods.removeCollectionSponsor().send({from: owner}); + + const sponsorTuple = await collectionEvm.methods.getCollectionSponsor().call({from: owner}); + expect(sponsorTuple.field_0).to.be.eq('0x0000000000000000000000000000000000000000'); + }); + + itWeb3('Sponsoring collection from evm address via access list', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + result = await collectionEvm.methods.setCollectionSponsor(sponsor).send({from: owner}); + let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + const ss58Format = (api.registry.getChainProperties())!.toJSON().ss58Format; + expect(collectionSub.sponsorship.isUnconfirmed).to.be.true; + expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); + await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); + + await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor}); + collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collectionSub.sponsorship.isConfirmed).to.be.true; + expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); + + const user = createEthAccount(web3); + const nextTokenId = await collectionEvm.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + + const oldPermissions = (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); + expect(oldPermissions.mintMode).to.be.false; + expect(oldPermissions.access).to.be.equal('Normal'); + + await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner}); + await collectionEvm.methods.addToCollectionAllowList(user).send({from: owner}); + await collectionEvm.methods.setCollectionMintMode(true).send({from: owner}); + + const newPermissions = (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); + expect(newPermissions.mintMode).to.be.true; + expect(newPermissions.access).to.be.equal('AllowList'); + + const ownerBalanceBefore = await ethBalanceViaSub(api, owner); + const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); + + { + const nextTokenId = await collectionEvm.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + const result = await collectionEvm.methods.mintWithTokenURI( + user, + nextTokenId, + 'Test URI', + ).send({from: user}); + const events = normalizeEvents(result.events); + + expect(events).to.be.deep.equal([ + { + address: collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: user, + tokenId: nextTokenId, + }, + }, + ]); + + const ownerBalanceAfter = await ethBalanceViaSub(api, owner); + const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); + + expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); + expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter); + expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true; + } + }); + + itWeb3('Sponsoring collection from substrate address via access list', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + const result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const sponsor = privateKeyWrapper('//Alice'); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + + await collectionEvm.methods.setCollectionSponsorSubstrate(sponsor.addressRaw).send({from: owner}); + + const confirmTx = await api.tx.unique.confirmSponsorship(collectionId); + await submitTransactionAsync(sponsor, confirmTx); + + const user = createEthAccount(web3); + const nextTokenId = await collectionEvm.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + + await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner}); + await collectionEvm.methods.addToCollectionAllowList(user).send({from: owner}); + await collectionEvm.methods.setCollectionMintMode(true).send({from: owner}); + + const ownerBalanceBefore = await ethBalanceViaSub(api, owner); + const sponsorBalanceBefore = (await getBalance(api, [sponsor.address]))[0]; + + { + const nextTokenId = await collectionEvm.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + const result = await collectionEvm.methods.mintWithTokenURI( + user, + nextTokenId, + 'Test URI', + ).send({from: user}); + const events = normalizeEvents(result.events); + + expect(events).to.be.deep.equal([ + { + address: collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: user, + tokenId: nextTokenId, + }, + }, + ]); + + const ownerBalanceAfter = await ethBalanceViaSub(api, owner); + const sponsorBalanceAfter = (await getBalance(api, [sponsor.address]))[0]; + + expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); + expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter); + expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true; + } + }); + + itWeb3('Check that transaction via EVM spend money from sponsor address', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionHelpers = evmCollectionHelpers(web3, owner); + let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); + const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const collectionEvm = evmCollection(web3, owner, collectionIdAddress); + result = await collectionEvm.methods.setCollectionSponsor(sponsor).send(); + let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + const ss58Format = (api.registry.getChainProperties())!.toJSON().ss58Format; + expect(collectionSub.sponsorship.isUnconfirmed).to.be.true; + expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); + await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); + const sponsorCollection = evmCollection(web3, sponsor, collectionIdAddress); + await sponsorCollection.methods.confirmCollectionSponsorship().send(); + collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; + expect(collectionSub.sponsorship.isConfirmed).to.be.true; + expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); + + const user = createEthAccount(web3); + await collectionEvm.methods.addCollectionAdmin(user).send(); + + const ownerBalanceBefore = await ethBalanceViaSub(api, owner); + const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); + + + const userCollectionEvm = evmCollection(web3, user, collectionIdAddress); + const nextTokenId = await userCollectionEvm.methods.nextTokenId().call(); + expect(nextTokenId).to.be.equal('1'); + result = await userCollectionEvm.methods.mintWithTokenURI( + user, + nextTokenId, + 'Test URI', + ).send(); + + const events = normalizeEvents(result.events); + const address = collectionIdToAddress(collectionId); + + expect(events).to.be.deep.equal([ + { + address, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: user, + tokenId: nextTokenId, + }, + }, + ]); + expect(await userCollectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); + + const ownerBalanceAfter = await ethBalanceViaSub(api, owner); + expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore); + const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; + }); }); --- a/tests/src/eth/contractSponsoring.test.ts +++ b/tests/src/eth/contractSponsoring.test.ts @@ -23,33 +23,35 @@ itWeb3, SponsoringMode, createEthAccount, - collectionIdToAddress, - GAS_ARGS, - normalizeEvents, - subToEth, - executeEthTxOnSub, - evmCollectionHelpers, - getCollectionAddressFromResult, - evmCollection, ethBalanceViaSub, } from './util/helpers'; -import { - addCollectionAdminExpectSuccess, - createCollectionExpectSuccess, - getDetailedCollectionInfo, - transferBalanceTo, -} from '../util/helpers'; -import nonFungibleAbi from './nonFungibleAbi.json'; -import getBalance from '../substrate/get-balance'; -import {evmToAddress} from '@polkadot/util-crypto'; describe('Sponsoring EVM contracts', () => { + itWeb3('Self sponsored can be set by the address that deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected; + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + }); + + itWeb3('Self sponsored can not be set by the address that did not deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.selfSponsoredEnable(flipper.options.address).call({from: notOwner})).to.be.rejectedWith('NoPermission'); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + }); + itWeb3('Sponsoring can be set by the address that has deployed the contract', async ({api, web3, privateKeyWrapper}) => { const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const flipper = await deployFlipper(web3, owner); const helpers = contractHelpers(web3, owner); expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; - await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner}); + await expect(helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner})).to.be.not.rejected; expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; }); @@ -59,11 +61,150 @@ const flipper = await deployFlipper(web3, owner); const helpers = contractHelpers(web3, owner); expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; - await expect(helpers.methods.setSponsoringMode(notOwner, SponsoringMode.Allowlisted).send({from: notOwner})).to.rejected; + await expect(helpers.methods.setSponsoringMode(notOwner, SponsoringMode.Allowlisted).call({from: notOwner})).to.be.rejectedWith('NoPermission'); expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; }); + + itWeb3('Sponsor can be set by the address that deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasPendingSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.setSponsor(flipper.options.address, sponsor).send()).to.be.not.rejected; + expect(await helpers.methods.hasPendingSponsor(flipper.options.address).call()).to.be.true; + }); + + itWeb3('Sponsor can not be set by the address that did not deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasPendingSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.setSponsor(flipper.options.address, sponsor).call({from: notOwner})).to.be.rejectedWith('NoPermission'); + expect(await helpers.methods.hasPendingSponsor(flipper.options.address).call()).to.be.false; + }); + + itWeb3('Sponsorship can be confirmed by the address that pending as sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.setSponsor(flipper.options.address, sponsor).send()).to.be.not.rejected; + await expect(helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor})).to.be.not.rejected; + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + }); + + itWeb3('Sponsorship can not be confirmed by the address that not pending as sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notSponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.setSponsor(flipper.options.address, sponsor).send()).to.be.not.rejected; + await expect(helpers.methods.confirmSponsorship(flipper.options.address).call({from: notSponsor})).to.be.rejectedWith('NoPermission'); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + }); + + itWeb3('Sponsorship can not be confirmed by the address that not set as sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notSponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await expect(helpers.methods.confirmSponsorship(flipper.options.address).call({from: notSponsor})).to.be.rejectedWith('NoPendingSponsor'); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + }); + + itWeb3('Get self sponsored sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + await helpers.methods.selfSponsoredEnable(flipper.options.address).send(); + + const result = await helpers.methods.getSponsor(flipper.options.address).call(); + + expect(result[0]).to.be.eq(flipper.options.address); + expect(result[1]).to.be.eq('0'); + }); + + itWeb3('Get confirmed sponsor', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); + + const result = await helpers.methods.getSponsor(flipper.options.address).call(); + expect(result[0]).to.be.eq(sponsor); + expect(result[1]).to.be.eq('0'); + }); + + itWeb3('Sponsor can be removed by the address that deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + + await helpers.methods.removeSponsor(flipper.options.address).send(); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + }); + + itWeb3('Sponsor can not be removed by the address that did not deployed the contract', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const notOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const flipper = await deployFlipper(web3, owner); + const helpers = contractHelpers(web3, owner); + + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false; + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + + await expect(helpers.methods.removeSponsor(flipper.options.address).call({from: notOwner})).to.be.rejectedWith('NoPermission'); + expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true; + }); + itWeb3('In generous mode, non-allowlisted user transaction will be sponsored', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + + const flipper = await deployFlipper(web3, owner); + + const helpers = contractHelpers(web3, owner); + + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); + + await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: owner}); + await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); + + const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); + const callerBalanceBefore = await ethBalanceViaSub(api, caller); + + await flipper.methods.flip().send({from: caller}); + expect(await flipper.methods.getValue().call()).to.be.true; + + // Balance should be taken from sponsor instead of caller + const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); + const callerBalanceAfter = await ethBalanceViaSub(api, caller); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; + expect(callerBalanceAfter).to.be.eq(callerBalanceBefore); + }); + + itWeb3('In generous mode, non-allowlisted user transaction will be self sponsored', async ({api, web3, privateKeyWrapper}) => { const alice = privateKeyWrapper('//Alice'); const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); @@ -73,28 +214,29 @@ const helpers = contractHelpers(web3, owner); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; + await helpers.methods.selfSponsoredEnable(flipper.options.address).send(); + await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: owner}); await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; await transferBalanceToEth(api, alice, flipper.options.address); - const originalFlipperBalance = await web3.eth.getBalance(flipper.options.address); - expect(originalFlipperBalance).to.be.not.equal('0'); + const contractBalanceBefore = await ethBalanceViaSub(api, flipper.options.address); + const callerBalanceBefore = await ethBalanceViaSub(api, caller); await flipper.methods.flip().send({from: caller}); expect(await flipper.methods.getValue().call()).to.be.true; - // Balance should be taken from flipper instead of caller - const balanceAfter = await web3.eth.getBalance(flipper.options.address); - expect(+balanceAfter).to.be.lessThan(+originalFlipperBalance); + // Balance should be taken from sponsor instead of caller + const contractBalanceAfter = await ethBalanceViaSub(api, flipper.options.address); + const callerBalanceAfter = await ethBalanceViaSub(api, caller); + expect(contractBalanceAfter < contractBalanceBefore).to.be.true; + expect(callerBalanceAfter).to.be.eq(callerBalanceBefore); }); itWeb3('Sponsoring is set, an address that has no UNQ can send a transaction and it works. Sponsor balance should decrease (allowlisted)', async ({api, web3, privateKeyWrapper}) => { - const alice = privateKeyWrapper('//Alice'); - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const caller = createEthAccount(web3); const flipper = await deployFlipper(web3, owner); @@ -103,22 +245,21 @@ await helpers.methods.toggleAllowlist(flipper.options.address, true).send({from: owner}); await helpers.methods.toggleAllowed(flipper.options.address, caller, true).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner}); await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; - await transferBalanceToEth(api, alice, flipper.options.address); + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); - const originalFlipperBalance = await web3.eth.getBalance(flipper.options.address); - expect(originalFlipperBalance).to.be.not.equal('0'); + const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); + expect(sponsorBalanceBefore).to.be.not.equal('0'); await flipper.methods.flip().send({from: caller}); expect(await flipper.methods.getValue().call()).to.be.true; // Balance should be taken from flipper instead of caller - const balanceAfter = await web3.eth.getBalance(flipper.options.address); - expect(+balanceAfter).to.be.lessThan(+originalFlipperBalance); + const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; }); itWeb3('Sponsoring is set, an address that has no UNQ can send a transaction and it works. Sponsor balance should not decrease (non-allowlisted)', async ({api, web3, privateKeyWrapper}) => { @@ -131,10 +272,8 @@ const helpers = contractHelpers(web3, owner); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner}); await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; await transferBalanceToEth(api, alice, flipper.options.address); @@ -150,11 +289,9 @@ }); itWeb3('Sponsoring is set, an address that has UNQ can send a transaction and it works. User balance should not change', async ({api, web3, privateKeyWrapper}) => { - const alice = privateKeyWrapper('//Alice'); - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const originalCallerBalance = await web3.eth.getBalance(caller); const flipper = await deployFlipper(web3, owner); @@ -162,26 +299,27 @@ await helpers.methods.toggleAllowlist(flipper.options.address, true).send({from: owner}); await helpers.methods.toggleAllowed(flipper.options.address, caller, true).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner}); await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; - await transferBalanceToEth(api, alice, flipper.options.address); + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); - const originalFlipperBalance = await web3.eth.getBalance(flipper.options.address); - expect(originalFlipperBalance).to.be.not.equal('0'); + const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); + const callerBalanceBefore = await ethBalanceViaSub(api, caller); await flipper.methods.flip().send({from: caller}); expect(await flipper.methods.getValue().call()).to.be.true; - expect(await web3.eth.getBalance(caller)).to.be.equals(originalCallerBalance); + const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); + const callerBalanceAfter = await ethBalanceViaSub(api, caller); + expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; + expect(callerBalanceAfter).to.be.equals(callerBalanceBefore); }); itWeb3('Sponsoring is limited, with setContractRateLimit. The limitation is working if transactions are sent more often, the sender pays the commission.', async ({api, web3, privateKeyWrapper}) => { - const alice = privateKeyWrapper('//Alice'); - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const originalCallerBalance = await web3.eth.getBalance(caller); @@ -191,25 +329,24 @@ await helpers.methods.toggleAllowlist(flipper.options.address, true).send({from: owner}); await helpers.methods.toggleAllowed(flipper.options.address, caller, true).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner}); await helpers.methods.setSponsoringRateLimit(flipper.options.address, 10).send({from: owner}); - expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; - await transferBalanceToEth(api, alice, flipper.options.address); + await helpers.methods.setSponsor(flipper.options.address, sponsor).send(); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); - const originalFlipperBalance = await web3.eth.getBalance(flipper.options.address); + const originalFlipperBalance = await web3.eth.getBalance(sponsor); expect(originalFlipperBalance).to.be.not.equal('0'); await flipper.methods.flip().send({from: caller}); expect(await flipper.methods.getValue().call()).to.be.true; expect(await web3.eth.getBalance(caller)).to.be.equals(originalCallerBalance); - const newFlipperBalance = await web3.eth.getBalance(flipper.options.address); + const newFlipperBalance = await web3.eth.getBalance(sponsor); expect(newFlipperBalance).to.be.not.equals(originalFlipperBalance); await flipper.methods.flip().send({from: caller}); - expect(await web3.eth.getBalance(flipper.options.address)).to.be.equal(newFlipperBalance); + expect(await web3.eth.getBalance(sponsor)).to.be.equal(newFlipperBalance); expect(await web3.eth.getBalance(caller)).to.be.not.equals(originalCallerBalance); }); @@ -219,131 +356,5 @@ const flipper = await deployFlipper(web3, owner); const helpers = contractHelpers(web3, owner); expect(await helpers.methods.getSponsoringRateLimit(flipper.options.address).call()).to.be.equals('7200'); - }); - - itWeb3('Sponsoring collection from evm address via access list', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const collectionHelpers = evmCollectionHelpers(web3, owner); - let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); - const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); - const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const collectionEvm = evmCollection(web3, owner, collectionIdAddress); - result = await collectionEvm.methods.setCollectionSponsor(sponsor).send({from: owner}); - let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; - const ss58Format = (api.registry.getChainProperties())!.toJSON().ss58Format; - expect(collectionSub.sponsorship.isUnconfirmed).to.be.true; - expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); - await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); - - await collectionEvm.methods.confirmCollectionSponsorship().send({from: sponsor}); - collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; - expect(collectionSub.sponsorship.isConfirmed).to.be.true; - expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); - - const user = createEthAccount(web3); - const nextTokenId = await collectionEvm.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - - const oldPermissions = (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); - expect(oldPermissions.mintMode).to.be.false; - expect(oldPermissions.access).to.be.equal('Normal'); - - await collectionEvm.methods.setCollectionAccess(1 /*'AllowList'*/).send({from: owner}); - await collectionEvm.methods.addToCollectionAllowList(user).send({from: owner}); - await collectionEvm.methods.setCollectionMintMode(true).send({from: owner}); - - const newPermissions = (await getDetailedCollectionInfo(api, collectionId))!.permissions.toHuman(); - expect(newPermissions.mintMode).to.be.true; - expect(newPermissions.access).to.be.equal('AllowList'); - - const ownerBalanceBefore = await ethBalanceViaSub(api, owner); - const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); - - { - const nextTokenId = await collectionEvm.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - const result = await collectionEvm.methods.mintWithTokenURI( - user, - nextTokenId, - 'Test URI', - ).send({from: user}); - const events = normalizeEvents(result.events); - - expect(events).to.be.deep.equal([ - { - address: collectionIdAddress, - event: 'Transfer', - args: { - from: '0x0000000000000000000000000000000000000000', - to: user, - tokenId: nextTokenId, - }, - }, - ]); - - const ownerBalanceAfter = await ethBalanceViaSub(api, owner); - const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); - - expect(await collectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); - expect(ownerBalanceBefore).to.be.eq(ownerBalanceAfter); - expect(sponsorBalanceBefore > sponsorBalanceAfter).to.be.true; - } - }); - - itWeb3('Check that transaction via EVM spend money from sponsor address', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const collectionHelpers = evmCollectionHelpers(web3, owner); - let result = await collectionHelpers.methods.createNonfungibleCollection('Sponsor collection', '1', '1').send(); - const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result); - const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - const collectionEvm = evmCollection(web3, owner, collectionIdAddress); - result = await collectionEvm.methods.setCollectionSponsor(sponsor).send(); - let collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; - const ss58Format = (api.registry.getChainProperties())!.toJSON().ss58Format; - expect(collectionSub.sponsorship.isUnconfirmed).to.be.true; - expect(collectionSub.sponsorship.asUnconfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); - await expect(collectionEvm.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); - const sponsorCollection = evmCollection(web3, sponsor, collectionIdAddress); - await sponsorCollection.methods.confirmCollectionSponsorship().send(); - collectionSub = (await getDetailedCollectionInfo(api, collectionId))!; - expect(collectionSub.sponsorship.isConfirmed).to.be.true; - expect(collectionSub.sponsorship.asConfirmed.toHuman()).to.be.eq(evmToAddress(sponsor, Number(ss58Format))); - - const user = createEthAccount(web3); - await collectionEvm.methods.addCollectionAdmin(user).send(); - - const ownerBalanceBefore = await ethBalanceViaSub(api, owner); - const sponsorBalanceBefore = await ethBalanceViaSub(api, sponsor); - - - const userCollectionEvm = evmCollection(web3, user, collectionIdAddress); - const nextTokenId = await userCollectionEvm.methods.nextTokenId().call(); - expect(nextTokenId).to.be.equal('1'); - result = await userCollectionEvm.methods.mintWithTokenURI( - user, - nextTokenId, - 'Test URI', - ).send(); - - const events = normalizeEvents(result.events); - const address = collectionIdToAddress(collectionId); - - expect(events).to.be.deep.equal([ - { - address, - event: 'Transfer', - args: { - from: '0x0000000000000000000000000000000000000000', - to: user, - tokenId: nextTokenId, - }, - }, - ]); - expect(await userCollectionEvm.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI'); - - const ownerBalanceAfter = await ethBalanceViaSub(api, owner); - expect(ownerBalanceAfter).to.be.eq(ownerBalanceBefore); - const sponsorBalanceAfter = await ethBalanceViaSub(api, sponsor); - expect(sponsorBalanceAfter < sponsorBalanceBefore).to.be.true; }); }); --- a/tests/src/eth/fractionalizer/Fractionalizer.bin +++ /dev/null @@ -1 +0,0 @@ -60c0604052600a60805269526546756e6769626c6560b01b60a0527fcdb72fd4e1d0d6d4eebd7ab142113ec2b4b06ddb24324db5c287ef01ab484d6b60055534801561004a57600080fd5b50600480546001600160a01b0319163317905561137a8061006c6000396000f3fe608060405234801561001057600080fd5b50600436106100575760003560e01c8063115091401461005c5780631b191ea214610071578063d470e60f14610084578063dbc38ad214610097578063eb292412146100aa575b600080fd5b61006f61006a366004610f85565b6100bd565b005b61006f61007f366004610ff2565b61035b565b61006f61009236600461108c565b6104c0565b61006f6100a53660046110b8565b61089d565b61006f6100b8366004611114565b610ee0565b6004546001600160a01b031633146100f05760405162461bcd60e51b81526004016100e79061114d565b60405180910390fd5b6000546001600160a01b0316156101495760405162461bcd60e51b815260206004820152601d60248201527f52465420636f6c6c656374696f6e20697320616c72656164792073657400000060448201526064016100e7565b60008190506000816001600160a01b031663d34b55b86040518163ffffffff1660e01b81526004016000604051808303816000875af1158015610190573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526101b8919081019061118b565b905060055481805190602001201461022f5760405162461bcd60e51b815260206004820152603460248201527f57726f6e6720636f6c6c656374696f6e20747970652e20436f6c6c656374696f604482015273371034b9903737ba103932b33ab733b4b136329760611b60648201526084016100e7565b816001600160a01b03166304a460536040518163ffffffff1660e01b81526004016020604051808303816000875af115801561026f573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610293919061125b565b6103055760405162461bcd60e51b815260206004820152603c60248201527f4672616374696f6e616c697a657220636f6e74726163742073686f756c64206260448201527f6520616e2061646d696e206f662074686520636f6c6c656374696f6e0000000060648201526084016100e7565b600080546001600160a01b0319166001600160a01b0385169081179091556040519081527f7186a599bf2297b1f4c8957d30b0965291eee0021a5f9a0aeb54dcbd1ffdceef9060200160405180910390a1505050565b6004546001600160a01b031633146103855760405162461bcd60e51b81526004016100e79061114d565b6000546001600160a01b0316156103de5760405162461bcd60e51b815260206004820152601d60248201527f52465420636f6c6c656374696f6e20697320616c72656164792073657400000060448201526064016100e7565b6040516344a68ad560e01b8152736c4e9fe1ae37a41e93cee429e8e1881abdcbb54f9081906344a68ad590610421908a908a908a908a908a908a906004016112a1565b6020604051808303816000875af1158015610440573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061046491906112ea565b600080546001600160a01b0319166001600160a01b039290921691821790556040519081527f7186a599bf2297b1f4c8957d30b0965291eee0021a5f9a0aeb54dcbd1ffdceef906020015b60405180910390a150505050505050565b6000546001600160a01b03166105145760405162461bcd60e51b81526020600482015260196024820152781491950818dbdb1b1958dd1a5bdb881a5cc81b9bdd081cd95d603a1b60448201526064016100e7565b6000546001600160a01b038381169116146105685760405162461bcd60e51b81526020600482015260146024820152732bb937b7339029232a1031b7b63632b1ba34b7b760611b60448201526064016100e7565b600080546040516355bb7d6360e11b8152600481018490526001600160a01b039091169190829063ab76fac690602401602060405180830381865afa1580156105b5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105d991906112ea565b6001600160a01b03808216600090815260036020908152604091829020825180840190935280549093168083526001909301549082015291925061065f5760405162461bcd60e51b815260206004820181905260248201527f4e6f20636f72726573706f6e64696e67204e465420746f6b656e20666f756e6460448201526064016100e7565b6000829050806001600160a01b03166318160ddd6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156106a2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106c69190611307565b6040516370a0823160e01b81523360048201526001600160a01b038316906370a0823190602401602060405180830381865afa15801561070a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061072e9190611307565b1461078a5760405162461bcd60e51b815260206004820152602660248201527f4e6f7420616c6c2070696563657320617265206f776e6564206279207468652060448201526531b0b63632b960d11b60648201526084016100e7565b6040516323b872dd60e01b81526001600160a01b038516906323b872dd906107ba90339030908a90600401611320565b600060405180830381600087803b1580156107d457600080fd5b505af11580156107e8573d6000803e3d6000fd5b5050835160208501516040516323b872dd60e01b81526001600160a01b0390921693506323b872dd92506108229130913391600401611320565b600060405180830381600087803b15801561083c57600080fd5b505af1158015610850573d6000803e3d6000fd5b5050835160208501516040517fe9e9808d24ff79ccc3b1ecf48be7b2d11591adccc452150d0d7947cb48eb0d53945061088d935087929190611320565b60405180910390a1505050505050565b6000546001600160a01b03166108f15760405162461bcd60e51b81526020600482015260196024820152781491950818dbdb1b1958dd1a5bdb881a5cc81b9bdd081cd95d603a1b60448201526064016100e7565b600080546001600160a01b0385811683526001602081905260409093205491169160ff90911615151461098c5760405162461bcd60e51b815260206004820152603c60248201527f4672616374696f6e616c697a6174696f6e206f66207468697320636f6c6c656360448201527f74696f6e206973206e6f7420616c6c6f7765642062792061646d696e0000000060648201526084016100e7565b6040516331a9108f60e11b81526004810184905233906001600160a01b03861690636352211e90602401602060405180830381865afa1580156109d3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109f791906112ea565b6001600160a01b031614610a5d5760405162461bcd60e51b815260206004820152602760248201527f4f6e6c7920746f6b656e206f776e657220636f756c64206672616374696f6e616044820152661b1a5e99481a5d60ca1b60648201526084016100e7565b6040516323b872dd60e01b81526001600160a01b038516906323b872dd90610a8d90339030908890600401611320565b600060405180830381600087803b158015610aa757600080fd5b505af1158015610abb573d6000803e3d6000fd5b505050506001600160a01b0384166000908152600260209081526040808320868452909152812054819081908103610d0757836001600160a01b03166375794a3c6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610b2b573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b4f9190611307565b6040516340c10f1960e01b8152306004820152602481018290529093506001600160a01b038516906340c10f19906044016020604051808303816000875af1158015610b9f573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610bc3919061125b565b506040516355bb7d6360e11b8152600481018490526001600160a01b0385169063ab76fac690602401602060405180830381865afa158015610c09573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610c2d91906112ea565b6001600160a01b0388811660008181526002602090815260408083208c84528252808320899055805180820182528481528083018d8152878716808652600390945293829020905181546001600160a01b0319169616959095178555915160019094019390935551630217888360e11b81526004810191909152602481018990529193508392509063042f1106906044016020604051808303816000875af1158015610cdd573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d01919061125b565b50610d98565b6001600160a01b0387811660009081526002602090815260408083208a8452909152908190205490516355bb7d6360e11b8152600481018290529094509085169063ab76fac690602401602060405180830381865afa158015610d6e573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610d9291906112ea565b91508190505b60405163d2418ca760e01b81526001600160801b03861660048201526001600160a01b0382169063d2418ca7906024016020604051808303816000875af1158015610de7573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e0b919061125b565b5060405163a9059cbb60e01b81523360048201526001600160801b03861660248201526001600160a01b0382169063a9059cbb906044016020604051808303816000875af1158015610e61573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e85919061125b565b50604080516001600160a01b03808a168252602082018990528416918101919091526001600160801b03861660608201527f29f372538523984f33874da1b50e596ce0f180a6eb04d7ff22bb2ce80e1576b6906080016104af565b6004546001600160a01b03163314610f0a5760405162461bcd60e51b81526004016100e79061114d565b6001600160a01b038216600081815260016020908152604091829020805460ff19168515159081179091558251938452908301527f6dad0aed33f4b7f07095619b668698e17943fd9f4c83e7cfcc7f6dd880a11588910160405180910390a15050565b6001600160a01b0381168114610f8257600080fd5b50565b600060208284031215610f9757600080fd5b8135610fa281610f6d565b9392505050565b60008083601f840112610fbb57600080fd5b50813567ffffffffffffffff811115610fd357600080fd5b602083019150836020828501011115610feb57600080fd5b9250929050565b6000806000806000806060878903121561100b57600080fd5b863567ffffffffffffffff8082111561102357600080fd5b61102f8a838b01610fa9565b9098509650602089013591508082111561104857600080fd5b6110548a838b01610fa9565b9096509450604089013591508082111561106d57600080fd5b5061107a89828a01610fa9565b979a9699509497509295939492505050565b6000806040838503121561109f57600080fd5b82356110aa81610f6d565b946020939093013593505050565b6000806000606084860312156110cd57600080fd5b83356110d881610f6d565b92506020840135915060408401356001600160801b03811681146110fb57600080fd5b809150509250925092565b8015158114610f8257600080fd5b6000806040838503121561112757600080fd5b823561113281610f6d565b9150602083013561114281611106565b809150509250929050565b6020808252600e908201526d27b7363c9037bbb732b91031b0b760911b604082015260600190565b634e487b7160e01b600052604160045260246000fd5b6000602080838503121561119e57600080fd5b825167ffffffffffffffff808211156111b657600080fd5b818501915085601f8301126111ca57600080fd5b8151818111156111dc576111dc611175565b604051601f8201601f19908116603f0116810190838211818310171561120457611204611175565b81604052828152888684870101111561121c57600080fd5b600093505b8284101561123e5784840186015181850187015292850192611221565b8284111561124f5760008684830101525b98975050505050505050565b60006020828403121561126d57600080fd5b8151610fa281611106565b81835281816020850137506000828201602090810191909152601f909101601f19169091010190565b6060815260006112b560608301888a611278565b82810360208401526112c8818789611278565b905082810360408401526112dd818587611278565b9998505050505050505050565b6000602082840312156112fc57600080fd5b8151610fa281610f6d565b60006020828403121561131957600080fd5b5051919050565b6001600160a01b03938416815291909216602082015260408101919091526060019056fea2646970667358221220a118fe8c3b83b3933baa7bfe084ed165a014ec509367252e5c99e1a3332d000364736f6c634300080f0033 \ No newline at end of file --- a/tests/src/eth/fractionalizer/Fractionalizer.sol +++ b/tests/src/eth/fractionalizer/Fractionalizer.sol @@ -16,8 +16,8 @@ } address rftCollection; mapping(address => bool) nftCollectionAllowList; - mapping(address => mapping(uint256 => uint256)) nft2rftMapping; - mapping(address => Token) rft2nftMapping; + mapping(address => mapping(uint256 => uint256)) public nft2rftMapping; + mapping(address => Token) public rft2nftMapping; bytes32 refungibleCollectionType = keccak256(bytes("ReFungible")); receive() external payable onlyOwner {} @@ -64,7 +64,7 @@ "Wrong collection type. Collection is not refungible." ); require( - refungibleContract.verifyOwnerOrAdmin(address(this)), + refungibleContract.isOwnerOrAdmin(address(this)), "Fractionalizer contract should be an admin of the collection" ); rftCollection = _collection; @@ -137,7 +137,6 @@ rft2nftMapping[rftTokenAddress] = Token(_collection, _token); rftTokenContract = UniqueRefungibleToken(rftTokenAddress); - rftTokenContract.setParentNFT(_collection, _token); } else { rftTokenId = nft2rftMapping[_collection][_token]; rftTokenAddress = rftCollectionContract.tokenContractAddress(rftTokenId); --- a/tests/src/eth/fractionalizer/FractionalizerAbi.json +++ /dev/null @@ -1,142 +0,0 @@ -[ - { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "_collection", - "type": "address" - }, - { - "indexed": false, - "internalType": "bool", - "name": "_status", - "type": "bool" - } - ], - "name": "AllowListSet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "_rftToken", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "_nftCollection", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_nftTokenId", - "type": "uint256" - } - ], - "name": "Defractionalized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "_collection", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_tokenId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "_rftToken", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint128", - "name": "_amount", - "type": "uint128" - } - ], - "name": "Fractionalized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "_collection", - "type": "address" - } - ], - "name": "RFTCollectionSet", - "type": "event" - }, - { - "inputs": [ - { "internalType": "string", "name": "_name", "type": "string" }, - { "internalType": "string", "name": "_description", "type": "string" }, - { "internalType": "string", "name": "_tokenPrefix", "type": "string" } - ], - "name": "createAndSetRFTCollection", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "_collection", "type": "address" }, - { "internalType": "uint256", "name": "_token", "type": "uint256" }, - { "internalType": "uint128", "name": "_pieces", "type": "uint128" } - ], - "name": "nft2rft", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "_collection", "type": "address" }, - { "internalType": "uint256", "name": "_token", "type": "uint256" } - ], - "name": "rft2nft", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "collection", "type": "address" }, - { "internalType": "bool", "name": "status", "type": "bool" } - ], - "name": "setNftCollectionIsAllowed", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "_collection", "type": "address" } - ], - "name": "setRFTCollection", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - } -] --- a/tests/src/eth/fractionalizer/fractionalizer.test.ts +++ b/tests/src/eth/fractionalizer/fractionalizer.test.ts @@ -223,6 +223,28 @@ }, }); }); + + itWeb3('Test fractionalizer NFT <-> RFT mapping ', async ({api, web3, privateKeyWrapper}) => { + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + + const {fractionalizer, rftCollectionAddress} = await initFractionalizer(api, web3, privateKeyWrapper, owner); + const {rftTokenAddress, nftCollectionAddress, nftTokenId} = await createRFTToken(api, web3, owner, fractionalizer, 100n); + + const {collectionId, tokenId} = tokenIdFromAddress(rftTokenAddress); + const refungibleAddress = collectionIdToAddress(collectionId); + expect(rftCollectionAddress).to.be.equal(refungibleAddress); + const refungibleTokenContract = uniqueRefungibleToken(web3, rftTokenAddress, owner); + await refungibleTokenContract.methods.approve(fractionalizer.options.address, 100).send(); + + const rft2nft = await fractionalizer.methods.rft2nftMapping(rftTokenAddress).call(); + expect(rft2nft).to.be.like({ + _collection: nftCollectionAddress, + _tokenId: nftTokenId, + }); + + const nft2rft = await fractionalizer.methods.nft2rftMapping(nftCollectionAddress, nftTokenId).call(); + expect(nft2rft).to.be.eq(tokenId.toString()); + }); }); --- a/tests/src/eth/fungible.test.ts +++ b/tests/src/eth/fungible.test.ts @@ -14,10 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {approveExpectSuccess, createCollectionExpectSuccess, createFungibleItemExpectSuccess, transferExpectSuccess, transferFromExpectSuccess, UNIQUE} from '../util/helpers'; -import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, GAS_ARGS, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, transferBalanceToEth} from './util/helpers'; +import {approveExpectSuccess, createCollection, createCollectionExpectSuccess, createFungibleItemExpectSuccess, transferExpectSuccess, transferFromExpectSuccess, UNIQUE} from '../util/helpers'; +import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, evmCollection, GAS_ARGS, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, transferBalanceToEth} from './util/helpers'; import fungibleAbi from './fungibleAbi.json'; import {expect} from 'chai'; +import {submitTransactionAsync} from '../substrate/substrate-api'; describe('Fungible: Information getting', () => { itWeb3('totalSupply', async ({api, web3, privateKeyWrapper}) => { @@ -58,6 +59,128 @@ }); describe('Fungible: Plain calls', () => { + itWeb3('Can perform mint()', async ({web3, api, privateKeyWrapper}) => { + const alice = privateKeyWrapper('//Alice'); + const collection = await createCollection(api, alice, { + name: 'token name', + mode: {type: 'Fungible', decimalPoints: 0}, + }); + + const receiver = createEthAccount(web3); + + const collectionIdAddress = collectionIdToAddress(collection.collectionId); + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const changeAdminTx = api.tx.unique.addCollectionAdmin(collection.collectionId, {Ethereum: owner}); + await submitTransactionAsync(alice, changeAdminTx); + + const collectionContract = evmCollection(web3, owner, collectionIdAddress, {type: 'Fungible', decimalPoints: 0}); + const result = await collectionContract.methods.mint(receiver, 100).send(); + const events = normalizeEvents(result.events); + + expect(events).to.be.deep.equal([ + { + address: collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver, + value: '100', + }, + }, + ]); + }); + + itWeb3('Can perform mintBulk()', async ({web3, api, privateKeyWrapper}) => { + const alice = privateKeyWrapper('//Alice'); + const collection = await createCollection(api, alice, { + name: 'token name', + mode: {type: 'Fungible', decimalPoints: 0}, + }); + + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const receiver1 = createEthAccount(web3); + const receiver2 = createEthAccount(web3); + const receiver3 = createEthAccount(web3); + + const collectionIdAddress = collectionIdToAddress(collection.collectionId); + const changeAdminTx = api.tx.unique.addCollectionAdmin(collection.collectionId, {Ethereum: owner}); + await submitTransactionAsync(alice, changeAdminTx); + + const collectionContract = evmCollection(web3, owner, collectionIdAddress, {type: 'Fungible', decimalPoints: 0}); + const result = await collectionContract.methods.mintBulk([ + [receiver1, 10], + [receiver2, 20], + [receiver3, 30], + ]).send(); + const events = normalizeEvents(result.events); + + expect(events).to.be.deep.contain({ + address:collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver1, + value: '10', + }, + }); + + expect(events).to.be.deep.contain({ + address:collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver2, + value: '20', + }, + }); + + expect(events).to.be.deep.contain({ + address:collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver3, + value: '30', + }, + }); + }); + + itWeb3('Can perform burn()', async ({web3, api, privateKeyWrapper}) => { + const alice = privateKeyWrapper('//Alice'); + const collection = await createCollection(api, alice, { + name: 'token name', + mode: {type: 'Fungible', decimalPoints: 0}, + }); + + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const changeAdminTx = api.tx.unique.addCollectionAdmin(collection.collectionId, {Ethereum: owner}); + await submitTransactionAsync(alice, changeAdminTx); + const receiver = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + + const collectionIdAddress = collectionIdToAddress(collection.collectionId); + const collectionContract = evmCollection(web3, owner, collectionIdAddress, {type: 'Fungible', decimalPoints: 0}); + await collectionContract.methods.mint(receiver, 100).send(); + + const result = await collectionContract.methods.burnFrom(receiver, 49).send({from: receiver}); + + const events = normalizeEvents(result.events); + + expect(events).to.be.deep.equal([ + { + address: collectionIdAddress, + event: 'Transfer', + args: { + from: receiver, + to: '0x0000000000000000000000000000000000000000', + value: '49', + }, + }, + ]); + + const balance = await collectionContract.methods.balanceOf(receiver).call(); + expect(balance).to.equal('51'); + }); + itWeb3('Can perform approve()', async ({web3, api, privateKeyWrapper}) => { const collection = await createCollectionExpectSuccess({ name: 'token name', --- a/tests/src/eth/fungibleAbi.json +++ b/tests/src/eth/fungibleAbi.json @@ -152,6 +152,75 @@ }, { "inputs": [], + "name": "getCollectionSponsor", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "hasCollectionPendingSponsor", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "isOwnerOrAdmin", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "user", "type": "uint256" } + ], + "name": "isOwnerOrAdminSubstrate", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "mint", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6[]", + "name": "amounts", + "type": "tuple[]" + } + ], + "name": "mintBulk", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], "name": "name", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", @@ -176,6 +245,13 @@ "type": "function" }, { + "inputs": [], + "name": "removeCollectionSponsor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [ { "internalType": "address", "name": "user", "type": "address" } ], @@ -260,6 +336,33 @@ }, { "inputs": [ + { "internalType": "uint256", "name": "sponsor", "type": "uint256" } + ], + "name": "setCollectionSponsorSubstrate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "setOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "newOwner", "type": "uint256" } + ], + "name": "setOwnerSubstrate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" } ], "name": "supportsInterface", @@ -307,15 +410,6 @@ "name": "uniqueCollectionType", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "user", "type": "address" } - ], - "name": "verifyOwnerOrAdmin", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", "type": "function" } ] --- a/tests/src/eth/marketplace/marketplace.test.ts +++ b/tests/src/eth/marketplace/marketplace.test.ts @@ -39,6 +39,7 @@ describe('Matcher contract usage', () => { itWeb3('With UNQ', async ({api, web3, privateKeyWrapper}) => { const alice = privateKeyWrapper('//Alice'); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const matcherOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const matcherContract = new web3.eth.Contract(JSON.parse((await readFile(`${__dirname}/MarketPlace.abi`)).toString()), undefined, { from: matcherOwner, @@ -48,7 +49,9 @@ const helpers = contractHelpers(web3, matcherOwner); await helpers.methods.setSponsoringMode(matcher.options.address, SponsoringMode.Allowlisted).send({from: matcherOwner}); await helpers.methods.setSponsoringRateLimit(matcher.options.address, 1).send({from: matcherOwner}); - await transferBalanceToEth(api, alice, matcher.options.address); + + await helpers.methods.setSponsor(matcher.options.address, sponsor).send({from: matcherOwner}); + await helpers.methods.confirmSponsorship(matcher.options.address).send({from: sponsor}); const collectionId = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); await setCollectionLimitsExpectSuccess(alice, collectionId, {sponsorApproveTimeout: 1}); @@ -100,6 +103,7 @@ itWeb3('With escrow', async ({api, web3, privateKeyWrapper}) => { const alice = privateKeyWrapper('//Alice'); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const matcherOwner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const escrow = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const matcherContract = new web3.eth.Contract(JSON.parse((await readFile(`${__dirname}/MarketPlace.abi`)).toString()), undefined, { @@ -111,7 +115,9 @@ const helpers = contractHelpers(web3, matcherOwner); await helpers.methods.setSponsoringMode(matcher.options.address, SponsoringMode.Allowlisted).send({from: matcherOwner}); await helpers.methods.setSponsoringRateLimit(matcher.options.address, 1).send({from: matcherOwner}); - await transferBalanceToEth(api, alice, matcher.options.address); + + await helpers.methods.setSponsor(matcher.options.address, sponsor).send({from: matcherOwner}); + await helpers.methods.confirmSponsorship(matcher.options.address).send({from: sponsor}); const collectionId = await createCollectionExpectSuccess({mode: {type: 'NFT'}}); await setCollectionLimitsExpectSuccess(alice, collectionId, {sponsorApproveTimeout: 1}); --- a/tests/src/eth/nonFungibleAbi.json +++ b/tests/src/eth/nonFungibleAbi.json @@ -200,6 +200,30 @@ "type": "function" }, { + "inputs": [], + "name": "getCollectionSponsor", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple17", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "hasCollectionPendingSponsor", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [ { "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "operator", "type": "address" } @@ -211,6 +235,24 @@ }, { "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "isOwnerOrAdmin", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "user", "type": "uint256" } + ], + "name": "isOwnerOrAdminSubstrate", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], @@ -237,7 +279,7 @@ { "internalType": "uint256", "name": "field_0", "type": "uint256" }, { "internalType": "string", "name": "field_1", "type": "string" } ], - "internalType": "struct Tuple0[]", + "internalType": "struct Tuple8[]", "name": "tokens", "type": "tuple[]" } @@ -317,6 +359,13 @@ "type": "function" }, { + "inputs": [], + "name": "removeCollectionSponsor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [ { "internalType": "address", "name": "user", "type": "address" } ], @@ -343,7 +392,7 @@ { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, { "internalType": "bytes", "name": "data", "type": "bytes" } ], - "name": "safeTransferFromWithData", + "name": "safeTransferFrom", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -434,6 +483,33 @@ }, { "inputs": [ + { "internalType": "uint256", "name": "sponsor", "type": "uint256" } + ], + "name": "setCollectionSponsorSubstrate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "setOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "newOwner", "type": "uint256" } + ], + "name": "setOwnerSubstrate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, { "internalType": "string", "name": "key", "type": "string" }, { "internalType": "bytes", "name": "value", "type": "bytes" } @@ -532,15 +608,6 @@ "name": "uniqueCollectionType", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "user", "type": "address" } - ], - "name": "verifyOwnerOrAdmin", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", "type": "function" } ] --- a/tests/src/eth/reFungibleAbi.json +++ b/tests/src/eth/reFungibleAbi.json @@ -200,6 +200,30 @@ "type": "function" }, { + "inputs": [], + "name": "getCollectionSponsor", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple17", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "hasCollectionPendingSponsor", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { "inputs": [ { "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "operator", "type": "address" } @@ -211,6 +235,24 @@ }, { "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "isOwnerOrAdmin", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "user", "type": "uint256" } + ], + "name": "isOwnerOrAdminSubstrate", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "to", "type": "address" }, { "internalType": "uint256", "name": "tokenId", "type": "uint256" } ], @@ -237,7 +279,7 @@ { "internalType": "uint256", "name": "field_0", "type": "uint256" }, { "internalType": "string", "name": "field_1", "type": "string" } ], - "internalType": "struct Tuple0[]", + "internalType": "struct Tuple8[]", "name": "tokens", "type": "tuple[]" } @@ -317,6 +359,13 @@ "type": "function" }, { + "inputs": [], + "name": "removeCollectionSponsor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [ { "internalType": "address", "name": "user", "type": "address" } ], @@ -434,6 +483,33 @@ }, { "inputs": [ + { "internalType": "uint256", "name": "sponsor", "type": "uint256" } + ], + "name": "setCollectionSponsorSubstrate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newOwner", "type": "address" } + ], + "name": "setOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "newOwner", "type": "uint256" } + ], + "name": "setOwnerSubstrate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ { "internalType": "uint256", "name": "tokenId", "type": "uint256" }, { "internalType": "string", "name": "key", "type": "string" }, { "internalType": "bytes", "name": "value", "type": "bytes" } @@ -541,15 +617,6 @@ "name": "uniqueCollectionType", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "user", "type": "address" } - ], - "name": "verifyOwnerOrAdmin", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", "type": "function" } ] --- a/tests/src/eth/reFungibleToken.test.ts +++ b/tests/src/eth/reFungibleToken.test.ts @@ -655,31 +655,6 @@ await requirePallets(this, [Pallets.ReFungible]); }); - itWeb3('Parent NFT token address and id', async ({api, web3, privateKeyWrapper}) => { - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); - - const {collectionIdAddress: nftCollectionAddress} = await createNonfungibleCollection(api, web3, owner); - const nftContract = uniqueNFT(web3, nftCollectionAddress, owner); - const nftTokenId = await nftContract.methods.nextTokenId().call(); - await nftContract.methods.mint(owner, nftTokenId).send(); - const nftCollectionId = collectionIdFromAddress(nftCollectionAddress); - - const {collectionIdAddress, collectionId} = await createRefungibleCollection(api, web3, owner); - const refungibleContract = uniqueRefungible(web3, collectionIdAddress, owner); - const refungibleTokenId = await refungibleContract.methods.nextTokenId().call(); - await refungibleContract.methods.mint(owner, refungibleTokenId).send(); - - const rftTokenAddress = tokenIdToAddress(collectionId, refungibleTokenId); - const refungibleTokenContract = uniqueRefungibleToken(web3, rftTokenAddress, owner); - await refungibleTokenContract.methods.setParentNFT(nftCollectionAddress, nftTokenId).send(); - - const tokenAddress = await refungibleTokenContract.methods.parentToken().call(); - const tokenId = await refungibleTokenContract.methods.parentTokenId().call(); - const nftTokenAddress = tokenIdToAddress(nftCollectionId, nftTokenId); - expect(tokenAddress).to.be.equal(nftTokenAddress); - expect(tokenId).to.be.equal(nftTokenId); - }); - itWeb3('Default parent token address and id', async ({api, web3, privateKeyWrapper}) => { const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); @@ -693,7 +668,7 @@ const tokenAddress = await refungibleTokenContract.methods.parentToken().call(); const tokenId = await refungibleTokenContract.methods.parentTokenId().call(); - expect(tokenAddress).to.be.equal(rftTokenAddress); + expect(tokenAddress).to.be.equal(collectionIdAddress); expect(tokenId).to.be.equal(refungibleTokenId); }); }); --- a/tests/src/eth/reFungibleTokenAbi.json +++ b/tests/src/eth/reFungibleTokenAbi.json @@ -127,16 +127,6 @@ }, { "inputs": [ - { "internalType": "address", "name": "collection", "type": "address" }, - { "internalType": "uint256", "name": "nftId", "type": "uint256" } - ], - "name": "setParentNFT", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" } ], "name": "supportsInterface", --- a/tests/src/eth/refungibleAbi.json +++ /dev/null @@ -1,167 +0,0 @@ -[ - { - "inputs": [ - { "internalType": "address", "name": "newAdmin", "type": "address" } - ], - "name": "addCollectionAdmin", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "uint256", "name": "newAdmin", "type": "uint256" } - ], - "name": "addCollectionAdminSubstrate", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "user", "type": "address" } - ], - "name": "addToCollectionAllowList", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "string", "name": "key", "type": "string" }], - "name": "collectionProperty", - "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "confirmCollectionSponsorship", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "contractAddress", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "string", "name": "key", "type": "string" }], - "name": "deleteCollectionProperty", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "admin", "type": "address" } - ], - "name": "removeCollectionAdmin", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "uint256", "name": "admin", "type": "uint256" } - ], - "name": "removeCollectionAdminSubstrate", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "user", "type": "address" } - ], - "name": "removeFromCollectionAllowList", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "uint8", "name": "mode", "type": "uint8" }], - "name": "setCollectionAccess", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "string", "name": "limit", "type": "string" }, - { "internalType": "uint32", "name": "value", "type": "uint32" } - ], - "name": "setCollectionLimit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "string", "name": "limit", "type": "string" }, - { "internalType": "bool", "name": "value", "type": "bool" } - ], - "name": "setCollectionLimit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "bool", "name": "mode", "type": "bool" }], - "name": "setCollectionMintMode", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "bool", "name": "enable", "type": "bool" }], - "name": "setCollectionNesting", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bool", "name": "enable", "type": "bool" }, - { - "internalType": "address[]", - "name": "collections", - "type": "address[]" - } - ], - "name": "setCollectionNesting", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "string", "name": "key", "type": "string" }, - { "internalType": "bytes", "name": "value", "type": "bytes" } - ], - "name": "setCollectionProperty", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "sponsor", "type": "address" } - ], - "name": "setCollectionSponsor", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" } - ], - "name": "supportsInterface", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - } -] --- a/tests/src/eth/sponsoring.test.ts +++ b/tests/src/eth/sponsoring.test.ts @@ -15,13 +15,12 @@ // along with Unique Network. If not, see . import {expect} from 'chai'; -import {contractHelpers, createEthAccount, createEthAccountWithBalance, deployCollector, deployFlipper, itWeb3, SponsoringMode, transferBalanceToEth} from './util/helpers'; +import {contractHelpers, createEthAccount, createEthAccountWithBalance, deployCollector, deployFlipper, itWeb3, SponsoringMode} from './util/helpers'; describe('EVM sponsoring', () => { itWeb3('Fee is deducted from contract if sponsoring is enabled', async ({api, web3, privateKeyWrapper}) => { - const alice = privateKeyWrapper('//Alice'); - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const caller = createEthAccount(web3); const originalCallerBalance = await web3.eth.getBalance(caller); expect(originalCallerBalance).to.be.equal('0'); @@ -31,28 +30,29 @@ const helpers = contractHelpers(web3, owner); await helpers.methods.toggleAllowlist(flipper.options.address, true).send({from: owner}); await helpers.methods.toggleAllowed(flipper.options.address, caller, true).send({from: owner}); + + await helpers.methods.setSponsor(flipper.options.address, sponsor).send({from: owner}); + await helpers.methods.confirmSponsorship(flipper.options.address).send({from: sponsor}); expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.false; await helpers.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Allowlisted).send({from: owner}); await helpers.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: owner}); expect(await helpers.methods.sponsoringEnabled(flipper.options.address).call()).to.be.true; - await transferBalanceToEth(api, alice, flipper.options.address); - - const originalFlipperBalance = await web3.eth.getBalance(flipper.options.address); - expect(originalFlipperBalance).to.be.not.equal('0'); + const originalSponsorBalance = await web3.eth.getBalance(sponsor); + expect(originalSponsorBalance).to.be.not.equal('0'); await flipper.methods.flip().send({from: caller}); expect(await flipper.methods.getValue().call()).to.be.true; // Balance should be taken from flipper instead of caller expect(await web3.eth.getBalance(caller)).to.be.equals(originalCallerBalance); - expect(await web3.eth.getBalance(flipper.options.address)).to.be.not.equals(originalFlipperBalance); + expect(await web3.eth.getBalance(sponsor)).to.be.not.equals(originalSponsorBalance); }); + itWeb3('...but this doesn\'t applies to payable value', async ({api, web3, privateKeyWrapper}) => { - const alice = privateKeyWrapper('//Alice'); - const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const sponsor = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper); const originalCallerBalance = await web3.eth.getBalance(caller); expect(originalCallerBalance).to.be.not.equal('0'); @@ -68,16 +68,17 @@ await helpers.methods.setSponsoringRateLimit(collector.options.address, 0).send({from: owner}); expect(await helpers.methods.sponsoringEnabled(collector.options.address).call()).to.be.true; - await transferBalanceToEth(api, alice, collector.options.address); + await helpers.methods.setSponsor(collector.options.address, sponsor).send({from: owner}); + await helpers.methods.confirmSponsorship(collector.options.address).send({from: sponsor}); - const originalCollectorBalance = await web3.eth.getBalance(collector.options.address); - expect(originalCollectorBalance).to.be.not.equal('0'); + const originalSponsorBalance = await web3.eth.getBalance(sponsor); + expect(originalSponsorBalance).to.be.not.equal('0'); await collector.methods.giveMoney().send({from: caller, value: '10000'}); // Balance will be taken from both caller (value) and from collector (fee) expect(await web3.eth.getBalance(caller)).to.be.equals((BigInt(originalCallerBalance) - 10000n).toString()); - expect(await web3.eth.getBalance(collector.options.address)).to.be.not.equals(originalCollectorBalance); + expect(await web3.eth.getBalance(sponsor)).to.be.not.equals(originalSponsorBalance); expect(await collector.methods.getCollected().call()).to.be.equal('10000'); }); }); --- a/tests/src/eth/util/contractHelpersAbi.json +++ b/tests/src/eth/util/contractHelpersAbi.json @@ -34,6 +34,19 @@ "type": "address" } ], + "name": "confirmSponsorship", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], "name": "contractOwner", "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "view", @@ -47,6 +60,29 @@ "type": "address" } ], + "name": "getSponsor", + "outputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple0", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], "name": "getSponsoringRateLimit", "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], "stateMutability": "view", @@ -58,12 +94,11 @@ "internalType": "address", "name": "contractAddress", "type": "address" - }, - { "internalType": "uint8", "name": "mode", "type": "uint8" } + } ], - "name": "setSponsoringMode", - "outputs": [], - "stateMutability": "nonpayable", + "name": "hasPendingSponsor", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", "type": "function" }, { @@ -72,10 +107,22 @@ "internalType": "address", "name": "contractAddress", "type": "address" - }, - { "internalType": "uint32", "name": "rateLimit", "type": "uint32" } + } ], - "name": "setSponsoringRateLimit", + "name": "hasSponsor", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "removeSponsor", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -88,9 +135,9 @@ "type": "address" } ], - "name": "sponsoringEnabled", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", + "name": "selfSponsoredEnable", + "outputs": [], + "stateMutability": "nonpayable", "type": "function" }, { @@ -99,20 +146,26 @@ "internalType": "address", "name": "contractAddress", "type": "address" - } + }, + { "internalType": "address", "name": "sponsor", "type": "address" } ], - "name": "sponsoringMode", - "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], - "stateMutability": "view", + "name": "setSponsor", + "outputs": [], + "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ - { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" } + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { "internalType": "uint8", "name": "mode", "type": "uint8" } ], - "name": "supportsInterface", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", + "name": "setSponsoringMode", + "outputs": [], + "stateMutability": "nonpayable", "type": "function" }, { @@ -122,10 +175,9 @@ "name": "contractAddress", "type": "address" }, - { "internalType": "address", "name": "user", "type": "address" }, - { "internalType": "bool", "name": "allowed", "type": "bool" } + { "internalType": "uint32", "name": "rateLimit", "type": "uint32" } ], - "name": "toggleAllowed", + "name": "setSponsoringRateLimit", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -136,10 +188,33 @@ "internalType": "address", "name": "contractAddress", "type": "address" + } + ], + "name": "sponsoringEnabled", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" } + ], + "name": "supportsInterface", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" }, - { "internalType": "bool", "name": "enabled", "type": "bool" } + { "internalType": "address", "name": "user", "type": "address" }, + { "internalType": "bool", "name": "isAllowed", "type": "bool" } ], - "name": "toggleAllowlist", + "name": "toggleAllowed", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -153,7 +228,7 @@ }, { "internalType": "bool", "name": "enabled", "type": "bool" } ], - "name": "toggleSponsoring", + "name": "toggleAllowlist", "outputs": [], "stateMutability": "nonpayable", "type": "function" --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -99,6 +99,10 @@ } } +export function bigIntToSub(api: ApiPromise, number: bigint) { + return api.registry.createType('AccountId', '0x' + number.toString(16).padStart(64, '0')).toJSON(); +} + export function normalizeAccountId(input: string | AccountId | CrossAccountId | IKeyringPair): CrossAccountId { if (typeof input === 'string') { if (input.length >= 47) { --- a/tests/src/util/playgrounds/index.ts +++ b/tests/src/util/playgrounds/index.ts @@ -2,15 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import {IKeyringPair} from '@polkadot/types/types'; -import {UniqueHelper} from './unique'; import config from '../../config'; import '../../interfaces/augment-api-events'; -import * as defs from '../../interfaces/definitions'; -import {ApiPromise, WsProvider} from '@polkadot/api'; - +import {DevUniqueHelper} from './unique.dev'; class SilentLogger { - log(msg: any, level: any): void {} + log(msg: any, level: any): void { } level = { ERROR: 'ERROR' as const, WARNING: 'WARNING' as const, @@ -18,45 +15,7 @@ }; } - -class DevUniqueHelper extends UniqueHelper { - async connect(wsEndpoint: string, listeners?: any): Promise { - const wsProvider = new WsProvider(wsEndpoint); - this.api = new ApiPromise({ - provider: wsProvider, - signedExtensions: { - ContractHelpers: { - extrinsic: {}, - payload: {}, - }, - FakeTransactionFinalizer: { - extrinsic: {}, - payload: {}, - }, - }, - rpc: { - unique: defs.unique.rpc, - rmrk: defs.rmrk.rpc, - eth: { - feeHistory: { - description: 'Dummy', - params: [], - type: 'u8', - }, - maxPriorityFeePerGas: { - description: 'Dummy', - params: [], - type: 'u8', - }, - }, - }, - }); - await this.api.isReadyOrError; - this.network = await UniqueHelper.detectNetwork(this.api); - } -} - -export const usingPlaygrounds = async (code: (helper: UniqueHelper, privateKey: (seed: string) => IKeyringPair) => Promise) => { +export const usingPlaygrounds = async (code: (helper: DevUniqueHelper, privateKey: (seed: string) => IKeyringPair) => Promise) => { // TODO: Remove, this is temporary: Filter unneeded API output // (Jaco promised it will be removed in the next version) const consoleErr = console.error; @@ -83,7 +42,7 @@ const ss58Format = helper.chain.getChainProperties().ss58Format; const privateKey = (seed: string) => helper.util.fromSeed(seed, ss58Format); await code(helper, privateKey); - } + } finally { await helper.disconnect(); console.error = consoleErr; --- /dev/null +++ b/tests/src/util/playgrounds/types.ts @@ -0,0 +1,130 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +import {IKeyringPair} from '@polkadot/types/types'; + +export interface IChainEvent { + data: any; + method: string; + section: string; +} + +export interface ITransactionResult { + status: 'Fail' | 'Success'; + result: { + events: { + event: IChainEvent + }[]; + }, + moduleError?: string; +} + +export interface ILogger { + log: (msg: any, level?: string) => void; + level: { + ERROR: 'ERROR'; + WARNING: 'WARNING'; + INFO: 'INFO'; + [key: string]: string; + } +} + +export interface IUniqueHelperLog { + executedAt: number; + executionTime: number; + type: 'extrinsic' | 'rpc'; + status: 'Fail' | 'Success'; + call: string; + params: any[]; + moduleError?: string; + events?: any; +} + +export interface IApiListeners { + connected?: (...args: any[]) => any; + disconnected?: (...args: any[]) => any; + error?: (...args: any[]) => any; + ready?: (...args: any[]) => any; + decorated?: (...args: any[]) => any; +} + +export interface ICrossAccountId { + Substrate?: TSubstrateAccount; + Ethereum?: TEthereumAccount; +} + +export interface ICrossAccountIdLower { + substrate?: TSubstrateAccount; + ethereum?: TEthereumAccount; +} + +export interface ICollectionLimits { + accountTokenOwnershipLimit?: number | null; + sponsoredDataSize?: number | null; + sponsoredDataRateLimit?: {blocks: number} | {sponsoringDisabled: null} | null; + tokenLimit?: number | null; + sponsorTransferTimeout?: number | null; + sponsorApproveTimeout?: number | null; + ownerCanTransfer?: boolean | null; + ownerCanDestroy?: boolean | null; + transfersEnabled?: boolean | null; +} + +export interface INestingPermissions { + tokenOwner?: boolean; + collectionAdmin?: boolean; + restricted?: number[] | null; +} + +export interface ICollectionPermissions { + access?: 'Normal' | 'AllowList'; + mintMode?: boolean; + nesting?: INestingPermissions; +} + +export interface IProperty { + key: string; + value: string; +} + +export interface ITokenPropertyPermission { + key: string; + permission: { + mutable: boolean; + tokenOwner: boolean; + collectionAdmin: boolean; + } +} + +export interface IToken { + collectionId: number; + tokenId: number; +} + +export interface ICollectionCreationOptions { + name: string | number[]; + description: string | number[]; + tokenPrefix: string | number[]; + mode?: { + nft?: null; + refungible?: null; + fungible?: number; + } + permissions?: ICollectionPermissions; + properties?: IProperty[]; + tokenPropertyPermissions?: ITokenPropertyPermission[]; + limits?: ICollectionLimits; + pendingSponsor?: TSubstrateAccount; +} + +export interface IChainProperties { + ss58Format: number; + tokenDecimals: number[]; + tokenSymbol: string[] +} + +export type TSubstrateAccount = string; +export type TEthereumAccount = string; +export type TApiAllowedListeners = 'connected' | 'disconnected' | 'error' | 'ready' | 'decorated'; +export type TUniqueNetworks = 'opal' | 'quartz' | 'unique'; +export type TSigner = IKeyringPair; // | 'string' \ No newline at end of file --- /dev/null +++ b/tests/src/util/playgrounds/unique.dev.ts @@ -0,0 +1,136 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +import {mnemonicGenerate} from '@polkadot/util-crypto'; +import {UniqueHelper} from './unique'; +import {ApiPromise, WsProvider} from '@polkadot/api'; +import * as defs from '../../interfaces/definitions'; +import {TSigner} from './types'; +import {IKeyringPair} from '@polkadot/types/types'; + + +export class DevUniqueHelper extends UniqueHelper { + /** + * Arrange methods for tests + */ + arrange: ArrangeGroup; + + constructor(logger: { log: (msg: any, level: any) => void, level: any }) { + super(logger); + this.arrange = new ArrangeGroup(this); + } + + async connect(wsEndpoint: string, listeners?: any): Promise { + const wsProvider = new WsProvider(wsEndpoint); + this.api = new ApiPromise({ + provider: wsProvider, + signedExtensions: { + ContractHelpers: { + extrinsic: {}, + payload: {}, + }, + FakeTransactionFinalizer: { + extrinsic: {}, + payload: {}, + }, + }, + rpc: { + unique: defs.unique.rpc, + rmrk: defs.rmrk.rpc, + eth: { + feeHistory: { + description: 'Dummy', + params: [], + type: 'u8', + }, + maxPriorityFeePerGas: { + description: 'Dummy', + params: [], + type: 'u8', + }, + }, + }, + }); + await this.api.isReadyOrError; + this.network = await UniqueHelper.detectNetwork(this.api); + } +} + +class ArrangeGroup { + helper: UniqueHelper; + + constructor(helper: UniqueHelper) { + this.helper = helper; + } + + /** + * Generates accounts with the specified UNQ token balance + * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal. + * @param donor donor account for balances + * @returns array of newly created accounts + * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor); + */ + creteAccounts = async (balances: bigint[], donor: IKeyringPair): Promise => { + let nonce = await this.helper.chain.getNonce(donor.address); + const tokenNominal = this.helper.balance.getOneTokenNominal(); + const transactions = []; + const accounts: IKeyringPair[] = []; + for (const balance of balances) { + const recepient = this.helper.util.fromSeed(mnemonicGenerate()); + accounts.push(recepient); + if (balance !== 0n) { + const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, balance * tokenNominal]); + transactions.push(this.helper.signTransaction(donor, tx, 'account generation', {nonce})); + nonce++; + } + } + + await Promise.all(transactions).catch(e => {}); + + //#region TODO remove this region, when nonce problem will be solved + const checkBalances = async () => { + let isSuccess = true; + for (let i = 0; i < balances.length; i++) { + const balance = await this.helper.balance.getSubstrate(accounts[i].address); + if (balance !== balances[i] * tokenNominal) { + isSuccess = false; + break; + } + } + return isSuccess; + }; + + let accountsCreated = false; + // checkBalances retry up to 5 blocks + for (let index = 0; index < 5; index++) { + console.log(await this.helper.chain.getLatestBlockNumber()); + accountsCreated = await checkBalances(); + if(accountsCreated) break; + await this.waitNewBlocks(1); + } + + if (!accountsCreated) throw Error('Accounts generation failed'); + //#endregion + + return accounts; + }; + + /** + * Wait for specified bnumber of blocks + * @param blocksCount number of blocks to wait + * @returns + */ + async waitNewBlocks(blocksCount = 1): Promise { + const promise = new Promise(async (resolve) => { + const unsubscribe = await this.helper.api!.rpc.chain.subscribeNewHeads(() => { + if (blocksCount > 0) { + blocksCount--; + } else { + unsubscribe(); + resolve(); + } + }); + }); + return promise; + } +} \ No newline at end of file --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -7,10 +7,10 @@ import {ApiPromise, WsProvider, Keyring} from '@polkadot/api'; import {ApiInterfaceEvents} from '@polkadot/api/types'; +import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto'; import {IKeyringPair} from '@polkadot/types/types'; -import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto'; +import {IApiListeners, IChainEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, TUniqueNetworks} from './types'; - const crossAccountIdFromLower = (lowerAddress: ICrossAccountIdLower): ICrossAccountId => { const address = {} as ICrossAccountId; if(lowerAddress.substrate) address.Substrate = lowerAddress.substrate; @@ -43,134 +43,7 @@ return this.toChecksumAddress(`0xf8238ccfff8ed887463fd5e0${collectionId.toString(16).padStart(8, '0')}${tokenId.toString(16).padStart(8, '0')}`); }, }; - - -interface IChainEvent { - data: any; - method: string; - section: string; -} - -interface ITransactionResult { - status: 'Fail' | 'Success'; - result: { - events: { - event: IChainEvent - }[]; - }, - moduleError?: string; -} - -interface ILogger { - log: (msg: any, level?: string) => void; - level: { - ERROR: 'ERROR'; - WARNING: 'WARNING'; - INFO: 'INFO'; - [key: string]: string; - } -} - -interface IUniqueHelperLog { - executedAt: number; - executionTime: number; - type: 'extrinsic' | 'rpc'; - status: 'Fail' | 'Success'; - call: string; - params: any[]; - moduleError?: string; - events?: any; -} - -interface IApiListeners { - connected?: (...args: any[]) => any; - disconnected?: (...args: any[]) => any; - error?: (...args: any[]) => any; - ready?: (...args: any[]) => any; - decorated?: (...args: any[]) => any; -} - -interface ICrossAccountId { - Substrate?: TSubstrateAccount; - Ethereum?: TEthereumAccount; -} - -interface ICrossAccountIdLower { - substrate?: TSubstrateAccount; - ethereum?: TEthereumAccount; -} - -interface ICollectionLimits { - accountTokenOwnershipLimit?: number | null; - sponsoredDataSize?: number | null; - sponsoredDataRateLimit?: {blocks: number} | {sponsoringDisabled: null} | null; - tokenLimit?: number | null; - sponsorTransferTimeout?: number | null; - sponsorApproveTimeout?: number | null; - ownerCanTransfer?: boolean | null; - ownerCanDestroy?: boolean | null; - transfersEnabled?: boolean | null; -} - -interface INestingPermissions { - tokenOwner?: boolean; - collectionAdmin?: boolean; - restricted?: number[] | null; -} - -interface ICollectionPermissions { - access?: 'Normal' | 'AllowList'; - mintMode?: boolean; - nesting?: INestingPermissions; -} - -interface IProperty { - key: string; - value: string; -} - -interface ITokenPropertyPermission { - key: string; - permission: { - mutable: boolean; - tokenOwner: boolean; - collectionAdmin: boolean; - } -} - -interface IToken { - collectionId: number; - tokenId: number; -} - -interface ICollectionCreationOptions { - name: string | number[]; - description: string | number[]; - tokenPrefix: string | number[]; - mode?: { - nft?: null; - refungible?: null; - fungible?: number; - } - permissions?: ICollectionPermissions; - properties?: IProperty[]; - tokenPropertyPermissions?: ITokenPropertyPermission[]; - limits?: ICollectionLimits; - pendingSponsor?: TSubstrateAccount; -} - -interface IChainProperties { - ss58Format: number; - tokenDecimals: number[]; - tokenSymbol: string[] -} -type TSubstrateAccount = string; -type TEthereumAccount = string; -type TApiAllowedListeners = 'connected' | 'disconnected' | 'error' | 'ready' | 'decorated'; -type TUniqueNetworks = 'opal' | 'quartz' | 'unique'; -type TSigner = IKeyringPair; // | 'string' - class UniqueUtil { static transactionStatus = { NOT_READY: 'NotReady', @@ -439,7 +312,7 @@ return this.transactionStatus.FAIL; } - signTransaction(sender: TSigner, transaction: any, label = 'transaction', options = null) { + signTransaction(sender: TSigner, transaction: any, label = 'transaction', options: any = null) { const sign = (callback: any) => { if(options !== null) return transaction.signAndSend(sender, options, callback); return transaction.signAndSend(sender, callback); @@ -652,6 +525,22 @@ } /** + * Get the normalized addresses added to the collection allow-list. + * @param collectionId ID of collection + * @example await getAllowList(1) + * @returns array of allow-listed addresses + */ + async getAllowList(collectionId: number): Promise { + const normalized = []; + const allowListed = (await this.helper.callRpc('api.rpc.unique.allowlist', [collectionId])).toHuman(); + for (const address of allowListed) { + if (address.Substrate) normalized.push({Substrate: this.helper.address.normalizeSubstrate(address.Substrate)}); + else normalized.push(address); + } + return normalized; + } + + /** * Get the effective limits of the collection instead of null for default values * * @param collectionId ID of collection @@ -795,6 +684,25 @@ } /** + * Adds an address to allow list + * @param signer keyring of signer + * @param collectionId ID of collection + * @param addressObj address to add to the allow list + * @param label extra label for log + * @returns ```true``` if extrinsic success, otherwise ```false``` + */ + async addToAllowList(signer: TSigner, collectionId: number, addressObj: ICrossAccountId, label?: string): Promise { + if(typeof label === 'undefined') label = `collection #${collectionId}`; + const result = await this.helper.executeExtrinsic( + signer, + 'api.tx.unique.addToAllowList', [collectionId, addressObj], + true, `Unable to add address to allow list for ${label}`, + ); + + return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'AllowListAddressAdded'); + } + + /** * Removes a collection administrator. * * @param signer keyring of signer @@ -2119,6 +2027,10 @@ return await this.helper.collection.getAdmins(this.collectionId); } + async getAllowList() { + return await this.helper.collection.getAllowList(this.collectionId); + } + async getEffectiveLimits() { return await this.helper.collection.getEffectiveLimits(this.collectionId); } @@ -2143,6 +2055,10 @@ return await this.helper.collection.addAdmin(signer, this.collectionId, adminAddressObj, label); } + async addToAllowList(signer: TSigner, addressObj: ICrossAccountId, label?: string) { + return await this.helper.collection.addToAllowList(signer, this.collectionId, addressObj, label); + } + async removeAdmin(signer: TSigner, adminAddressObj: ICrossAccountId, label?: string) { return await this.helper.collection.removeAdmin(signer, this.collectionId, adminAddressObj, label); } -- gitstuff