diff --git a/.cargo/audit.toml b/.cargo/audit.toml index fd7d0f076a..c1c2811ff0 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -3,4 +3,5 @@ ignore = [ "RUSTSEC-2021-0127", # serde_cbor is unmaintained https://github.com/iotaledger/identity.rs/issues/518 "RUSTSEC-2023-0052", # temporary ignore until fix is provided "RUSTSEC-2023-0065", # temporary ignore until fix is provided + "RUSTSEC-2023-0071", # temporary ignore until fix is provided ] diff --git a/.dockerignore b/.dockerignore index 115fe4a561..327b28b236 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,3 @@ target/ -bindings/wasm/ +bindings/wasm/identity_wasm bindings/grpc/target/ diff --git a/.github/actions/iota-rebase-sandbox/setup/action.yml b/.github/actions/iota-rebase-sandbox/setup/action.yml new file mode 100644 index 0000000000..8137127c25 --- /dev/null +++ b/.github/actions/iota-rebase-sandbox/setup/action.yml @@ -0,0 +1,62 @@ +name: "iota-private-network setup" +description: "Setup IOTA Sandbox" + +inputs: + platform: + description: "Platform to download binary for (linux or macos)" + required: true + default: "linux" + logfile: + description: "Optional log file to store server log as workflow artifact" + required: false + default: "" + +runs: + using: composite + steps: + - name: Set up IOTA Node + shell: bash + run: | + set -e + mkdir -p iota + cd iota + + # Set platform + PLATFORM="${{ inputs.platform }}" + echo "Looking for platform: $PLATFORM" + + # pinned releases from: + # url = https://api.github.com/repos/iotaledger/iota/releases/latest + # releases might be visible before all binaries are available, so refer to fixed binaries here + if [ "$PLATFORM" = "linux" ]; then + DOWNLOAD_URL="https://github.com/iotaledger/iota/releases/download/v0.9.2-rc/iota-v0.9.2-rc-linux-x86_64.tgz" + elif [ "$PLATFORM" = "macos" ]; then + DOWNLOAD_URL="https://github.com/iotaledger/iota/releases/download/v0.9.2-rc/iota-v0.9.2-rc-macos-arm64.tgz" + else + echo "not binaries for platform: $PLATFORM" + exit 1 + fi + + # Download and extract + echo "Downloading from: $DOWNLOAD_URL" + curl -L -o iota.tar.gz $DOWNLOAD_URL + tar -xzf iota.tar.gz + + echo "$PWD" >> $GITHUB_PATH + export PATH="$PWD:$PATH" + + which iota || echo "iota not found in PATH" + ls -la "$PWD" + - name: Start the Network + shell: bash + working-directory: iota + run: | + # Clear previous configuration + rm -rf ~/.iota || true + + # Check log file arg + LOGFILE="${{ inputs.logfile }}" + echo "Starting server with log file: $LOGFILE" + + # Start the network + iota start --with-faucet ${{ inputs.logfile && format('> {0} 2>&1', inputs.logfile) || '' }} & diff --git a/.github/actions/iota-sandbox/setup/action.yml b/.github/actions/iota-sandbox/setup/action.yml deleted file mode 100644 index 8b32b8608d..0000000000 --- a/.github/actions/iota-sandbox/setup/action.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: 'iota-sandbox-setup' -description: 'Setup IOTA Sandbox' -runs: - using: "composite" - steps: - - name: Setup iota sandbox - shell: bash - run: | - # Use next lines for using the GitHub release - mkdir iota-sandbox - cd iota-sandbox - mkdir sandbox - cd sandbox - # Use the output of https://api.github.com/repos/iotaledger/iota-sandbox/releases/latest - DOWNLOAD_URL=$(curl "https://api.github.com/repos/iotaledger/iota-sandbox/releases" | jq -r '.[0].assets[] | select(.name | contains("iota_sandbox")) | .browser_download_url') - echo "Downloading sandbox from $DOWNLOAD_URL" - curl -L -o iota_sandbox.tar.gz $DOWNLOAD_URL - tar -xf iota_sandbox.tar.gz - - # Use the next lines to use the main branch - # git clone https://github.com/iotaledger/iota-sandbox - # cd iota-sandbox/sandbox - - # Start Tangle - sudo ./bootstrap.sh - docker compose --profile inx-faucet up -d - - name: Wait for tangle to start - shell: bash - run: wget -qO- https://raw.githubusercontent.com/eficode/wait-for/$WAIT_FOR_VERSION/wait-for | sh -s -- -t 60 http://localhost/health -- echo "Tangle is up" - env: - WAIT_FOR_VERSION: 4df3f9262d84cab0039c07bf861045fbb3c20ab7 # v2.2.3 - - name: Wait for faucet to start - shell: bash - run: wget -qO- https://raw.githubusercontent.com/eficode/wait-for/$WAIT_FOR_VERSION/wait-for | sh -s -- -t 60 http://localhost/faucet/api/info -- echo "Faucet is up" - env: - WAIT_FOR_VERSION: 4df3f9262d84cab0039c07bf861045fbb3c20ab7 # v2.2.3 diff --git a/.github/actions/iota-sandbox/tear-down/action.yml b/.github/actions/iota-sandbox/tear-down/action.yml deleted file mode 100644 index c8e6225d0b..0000000000 --- a/.github/actions/iota-sandbox/tear-down/action.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: 'iota-sandbox-tear-down' -description: 'tear-down a iota sandbox' -runs: - using: "composite" - steps: - - name: Tear down iota sandbox - shell: bash - run: | - cd iota-sandbox/sandbox - docker compose down - cd ../.. - sudo rm -rf iota-sandbox diff --git a/.github/actions/publish/publish-wasm/action.yml b/.github/actions/publish/publish-wasm/action.yml index 35217d70ef..ea3e5056ba 100644 --- a/.github/actions/publish/publish-wasm/action.yml +++ b/.github/actions/publish/publish-wasm/action.yml @@ -23,7 +23,7 @@ runs: - name: Set up Node.js uses: actions/setup-node@v2 with: - node-version: '16.x' + node-version: '20.x' registry-url: 'https://registry.npmjs.org' - name: Download bindings/wasm artifacts diff --git a/.github/actions/release/bump-versions/action.yml b/.github/actions/release/bump-versions/action.yml index cf1cf65781..e5155a18e6 100644 --- a/.github/actions/release/bump-versions/action.yml +++ b/.github/actions/release/bump-versions/action.yml @@ -51,7 +51,7 @@ runs: uses: actions/setup-node@v2 if: ${{inputs.release-target == 'wasm'}} with: - node-version: '16.x' + node-version: '20.x' registry-url: 'https://registry.npmjs.org' - name: Bump Wasm npm package version diff --git a/.github/actions/release/changelog-generator/action.yml b/.github/actions/release/changelog-generator/action.yml index 751407c73b..79a1ad4be8 100644 --- a/.github/actions/release/changelog-generator/action.yml +++ b/.github/actions/release/changelog-generator/action.yml @@ -37,12 +37,12 @@ runs: fi echo SINCE_ARG=$SINCE_ARG echo SINCE_ARG=$SINCE_ARG >> $GITHUB_ENV - + - name: Prepare Repository For Changelog Generator shell: bash run: | GITHUB_REPOSITORY_USER=$( echo $GITHUB_REPOSITORY | awk -F'/' '{print $1}') - GITHUB_REPOSITORY_PROJECT=$( echo $GITHUB_REPOSITORY | awk -F'/' '{print $2}') + GITHUB_REPOSITORY_PROJECT=$( echo $GITHUB_REPOSITORY | awk -F'/' '{print $2}') echo GITHUB_REPOSITORY_USER=$GITHUB_REPOSITORY_USER echo GITHUB_REPOSITORY_PROJECT=$GITHUB_REPOSITORY_PROJECT diff --git a/.github/actions/rust/rust-setup/action.yml b/.github/actions/rust/rust-setup/action.yml index 5f783a98cc..b8e968addf 100644 --- a/.github/actions/rust/rust-setup/action.yml +++ b/.github/actions/rust/rust-setup/action.yml @@ -90,7 +90,7 @@ runs: rustup show - name: Cache cargo - uses: actions/cache@v2.1.7 + uses: actions/cache@v4 if: inputs.cargo-cache-enabled == 'true' with: # https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci @@ -115,7 +115,7 @@ runs: shell: bash - name: Cache build target - uses: actions/cache@v2.1.7 + uses: actions/cache@v4 if: inputs.target-cache-enabled == 'true' with: path: ${{ inputs.target-cache-path }} @@ -127,7 +127,7 @@ runs: ${{ inputs.os }}-target- - name: Cache sccache - uses: actions/cache@v2.1.7 + uses: actions/cache@v4 if: inputs.sccache-enabled == 'true' with: path: ${{ inputs.sccache-path }} diff --git a/.github/actions/rust/sccache/setup-sccache/action.yml b/.github/actions/rust/sccache/setup-sccache/action.yml index 2cded190e1..f6392ded7e 100644 --- a/.github/actions/rust/sccache/setup-sccache/action.yml +++ b/.github/actions/rust/sccache/setup-sccache/action.yml @@ -15,8 +15,8 @@ runs: brew update --auto-update brew install sccache - - name: Install sccache (ubuntu-latest) - if: inputs.os == 'ubuntu-latest' + - name: Install sccache (ubuntu) + if: ${{ startsWith(inputs.os, 'ubuntu-') }} shell: bash run: | SCCACHE_DOWNLOAD_LINK=https://github.com/mozilla/sccache/releases/download @@ -42,5 +42,6 @@ runs: - name: Start sccache shell: bash run: | + echo "starting sccache on ${{ inputs.os }}" sccache --start-server sccache -s diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 3ad214f1bf..1f70c7f68c 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -14,6 +14,8 @@ on: pull_request: branches: - main + - 'feat/**' + - 'support/**' paths: - "**/Cargo.lock" - "**/Cargo.toml" diff --git a/.github/workflows/build-and-test-grpc.yml b/.github/workflows/build-and-test-grpc.yml index 2a561bd952..db8d391756 100644 --- a/.github/workflows/build-and-test-grpc.yml +++ b/.github/workflows/build-and-test-grpc.yml @@ -8,7 +8,7 @@ on: types: [ opened, synchronize, reopened, ready_for_review ] branches: - main - - 'epic/**' + - 'feat/**' - 'support/**' paths: - '.github/workflows/build-and-test.yml' diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c92432e36f..7833236061 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -8,16 +8,17 @@ on: types: [ opened, synchronize, reopened, ready_for_review ] branches: - main - - 'epic/**' + - 'feat/**' - 'support/**' paths: - '.github/workflows/build-and-test.yml' + - '.github/workflows/shared-build-wasm.yml' - '.github/actions/**' - '**.rs' - '**.toml' - 'bindings/**' - '!bindings/**.md' - - 'bindings/wasm/README.md' # the Readme contain txm tests + - 'bindings/wasm/identity_wasm/README.md' # the Readme contain txm tests env: RUST_BACKTRACE: full @@ -28,7 +29,7 @@ env: jobs: check-for-run-condition: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: should-run: ${{ !github.event.pull_request || github.event.pull_request.draft == false }} steps: @@ -38,7 +39,7 @@ jobs: check-for-modification: needs: check-for-run-condition if: ${{ needs.check-for-run-condition.outputs.should-run == 'true' }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: core-modified: ${{ steps.change-detection.outputs.core-modified }} # map step output to job output steps: @@ -55,7 +56,7 @@ jobs: CORE_MODIFIED=true else # unmodified - CORE_MODIFIED=false + CORE_MODIFIED=false fi echo CORE_MODIFIED=$CORE_MODIFIED echo "core-modified=$CORE_MODIFIED" >> $GITHUB_OUTPUT @@ -67,9 +68,9 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] + os: [ ubuntu-24.04, macos-latest, windows-latest ] include: - - os: ubuntu-latest + - os: ubuntu-24.04 sccache-path: /home/runner/.cache/sccache - os: macos-latest sccache-path: /Users/runner/Library/Caches/Mozilla.sccache @@ -78,6 +79,18 @@ jobs: env: SCCACHE_DIR: ${{ matrix.sccache-path }} RUSTC_WRAPPER: sccache + IOTA_SERVER_LOGFILE: >- + ${{ + matrix.os != 'windows-latest' && + format( + 'iota-server-logs-build-and-test-{0}-{1}-{2}-{3}.log', + matrix.os == 'ubuntu-24.04' && 'linux' || 'macos', + github.run_id, + github.run_number, + github.run_attempt + ) || + '' + }} steps: - uses: actions/checkout@v3 @@ -104,7 +117,7 @@ jobs: os: ${{matrix.os}} - name: Check --no-default-features - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-24.04' run: | cargo metadata --format-version 1 | \ jq -r '.workspace_members[]' | \ @@ -112,7 +125,7 @@ jobs: xargs -I {} cargo check -p {} --no-default-features - name: Check default features - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-24.04' run: | cargo metadata --format-version 1 | \ jq -r '.workspace_members[]' | \ @@ -122,7 +135,7 @@ jobs: # Clean debug target to avoid bloating the GitHub Actions cache. # The previous builds cannot be re-used at all for the full --all-features --release build anyway. - name: Clean target - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-24.04' run: cargo clean # Build the library, tests, and examples without running them to avoid recompilation in the run tests step @@ -130,18 +143,35 @@ jobs: run: cargo build --workspace --tests --examples --release - name: Start iota sandbox - if: matrix.os == 'ubuntu-latest' - uses: './.github/actions/iota-sandbox/setup' + if: matrix.os != 'windows-latest' + uses: './.github/actions/iota-rebase-sandbox/setup' + with: + platform: ${{ matrix.os == 'ubuntu-24.04' && 'linux' || 'macos' }} + logfile: ${{ env.IOTA_SERVER_LOGFILE }} + + - name: test IotaIdentity package + if: matrix.os != 'windows-latest' + # publish the package and set the IOTA_IDENTITY_PKG_ID env variable + run: | + iota move test + working-directory: identity_iota_core/packages/iota_identity + + - name: publish IotaIdentity package + if: matrix.os != 'windows-latest' + # publish the package and set the IOTA_IDENTITY_PKG_ID env variable + run: echo "IOTA_IDENTITY_PKG_ID=$(./publish_identity_package.sh)" >> "$GITHUB_ENV" + working-directory: identity_iota_core/scripts/ - name: Run tests excluding `custom_time` feature - run: cargo test --workspace --release + if: matrix.os != 'windows-latest' + run: cargo test --workspace --release -- --test-threads=1 - name: Run tests with `custom_time` feature - run: cargo test --test custom_time --features="custom_time" + run: cargo test --test custom_time --features="custom_time" -- --test-threads=1 - name: Run Rust examples # run examples only on ubuntu for now - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-24.04' run: | cargo metadata --format-version 1 --manifest-path ./examples/Cargo.toml | \ jq -r '.packages[] | select(.name == "examples") | .targets[].name' | \ @@ -151,15 +181,20 @@ jobs: - name: Run Rust Readme examples # run examples only on ubuntu for now - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-24.04' run: | - cd bindings/wasm + cd bindings/wasm/identity_wasm npm ci npm run test:readme:rust - - name: Tear down iota sandbox - if: matrix.os == 'ubuntu-latest' && always() - uses: './.github/actions/iota-sandbox/tear-down' + - name: Archive server logs + if: ${{ env.IOTA_SERVER_LOGFILE }} + uses: actions/upload-artifact@v4 + with: + name: ${{ env.IOTA_SERVER_LOGFILE }} + path: iota/${{ env.IOTA_SERVER_LOGFILE }} + if-no-files-found: error + retention-days: 1 - name: Stop sccache uses: './.github/actions/rust/sccache/stop-sccache' @@ -169,21 +204,21 @@ jobs: build-wasm: needs: check-for-run-condition if: ${{ needs.check-for-run-condition.outputs.should-run == 'true' }} - # owner/repository of workflow has to be static, see https://github.community/t/env-variables-in-uses/17466 - uses: iotaledger/identity.rs/.github/workflows/shared-build-wasm.yml@main + uses: './.github/workflows/shared-build-wasm.yml' with: + run-unit-tests: false output-artifact-name: identity-wasm-bindings-build test-wasm: needs: build-wasm if: ${{ needs.check-for-run-condition.outputs.should-run == 'true' }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - os: [ ubuntu-latest ] + os: [ ubuntu-24.04 ] include: - - os: ubuntu-latest + - os: ubuntu-24.04 steps: - uses: actions/checkout@v3 @@ -191,39 +226,54 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v1 with: - node-version: 16.x + node-version: 20.x - name: Install JS dependencies run: npm ci - working-directory: bindings/wasm + working-directory: bindings/wasm/identity_wasm - - name: Download bindings/wasm artifacts + - name: Download bindings/wasm/identity_wasm artifacts uses: actions/download-artifact@v4 with: name: identity-wasm-bindings-build - path: bindings/wasm + path: bindings/wasm/ - name: Start iota sandbox - uses: './.github/actions/iota-sandbox/setup' + uses: './.github/actions/iota-rebase-sandbox/setup' + + - name: publish IotaIdentity package + if: matrix.os != 'windows-latest' + # publish the package and set the IOTA_IDENTITY_PKG_ID env variable + run: echo "IOTA_IDENTITY_PKG_ID=$(./publish_identity_package.sh)" >> "$GITHUB_ENV" + working-directory: identity_iota_core/scripts/ + + - name: Install JS dependencies + run: npm ci + working-directory: bindings/wasm/identity_wasm + + - name: Install JS dependencies # This is problematic: @iota/iota-sdk seems to not get used from the identity_wasm package, that is why reinstall deps here + run: npm ci + working-directory: bindings/wasm/iota_interaction_ts + + - name: Setup link + run: npm link ../iota_interaction_ts + working-directory: bindings/wasm/identity_wasm - name: Run Wasm examples run: npm run test:readme && npm run test:node - working-directory: bindings/wasm - - - name: Tear down iota sandbox - if: always() - uses: './.github/actions/iota-sandbox/tear-down' + working-directory: bindings/wasm/identity_wasm test-wasm-firefox: needs: build-wasm - if: ${{ needs.check-for-run-condition.outputs.should-run == 'true' }} - runs-on: ubuntu-latest + # if: ${{ needs.check-for-run-condition.outputs.should-run == 'true' }} + if: ${{ false }} # disable for now + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - os: [ ubuntu-latest ] + os: [ ubuntu-24.04 ] include: - - os: ubuntu-latest + - os: ubuntu-24.04 steps: - uses: actions/checkout@v3 @@ -231,26 +281,26 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v1 with: - node-version: 16.x - + node-version: 20.x + - name: Install JS dependencies run: npm ci - working-directory: bindings/wasm + working-directory: bindings/wasm/identity_wasm - - name: Download bindings/wasm artifacts + - name: Download bindings/wasm/identity_wasm artifacts uses: actions/download-artifact@v4 with: name: identity-wasm-bindings-build - path: bindings/wasm + path: bindings/wasm/ - name: Start iota sandbox - uses: './.github/actions/iota-sandbox/setup' + uses: './.github/actions/iota-rebase-sandbox/setup' - name: Build Docker image uses: docker/build-push-action@v6.2.0 with: - context: bindings/wasm/ - file: bindings/wasm/cypress/Dockerfile + context: bindings/wasm/identity_wasm/ + file: bindings/wasm/identity_wasm/cypress/Dockerfile push: false tags: cypress-test:latest load: true @@ -258,20 +308,17 @@ jobs: - name: Run cypress run: docker run --network host cypress-test test:browser:parallel:firefox - - name: Tear down iota sandbox - if: always() - uses: './.github/actions/iota-sandbox/tear-down' - test-wasm-chrome: needs: build-wasm - if: ${{ needs.check-for-run-condition.outputs.should-run == 'true' }} - runs-on: ubuntu-latest + # if: ${{ needs.check-for-run-condition.outputs.should-run == 'true' }} + if: ${{ false }} # disable for now + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - os: [ ubuntu-latest ] + os: [ ubuntu-24.04 ] include: - - os: ubuntu-latest + - os: ubuntu-24.04 steps: - uses: actions/checkout@v3 @@ -279,33 +326,29 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v1 with: - node-version: 16.x + node-version: 20.x - name: Install JS dependencies run: npm ci - working-directory: bindings/wasm + working-directory: bindings/wasm/identity_wasm - - name: Download bindings/wasm artifacts + - name: Download bindings/wasm/identity_wasm artifacts uses: actions/download-artifact@v4 with: name: identity-wasm-bindings-build - path: bindings/wasm + path: bindings/wasm/ - name: Start iota sandbox - uses: './.github/actions/iota-sandbox/setup' + uses: './.github/actions/iota-rebase-sandbox/setup' - name: Build Docker image uses: docker/build-push-action@v6.2.0 with: - context: bindings/wasm/ - file: bindings/wasm/cypress/Dockerfile + context: bindings/wasm/identity_wasm/ + file: bindings/wasm/identity_wasm/cypress/Dockerfile push: false tags: cypress-test:latest load: true - name: Run cypress run: docker run --network host cypress-test test:browser:parallel:chrome - - - name: Tear down iota sandbox - if: always() - uses: './.github/actions/iota-sandbox/tear-down' diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 8861bbc19f..018b46147e 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -7,7 +7,7 @@ on: pull_request: branches: - main - - 'epic/**' + - 'feat/**' - 'support/**' paths: - '.github/workflows/clippy.yml' @@ -34,13 +34,16 @@ jobs: # Download a pre-compiled wasm-bindgen binary. - name: Install wasm-bindgen-cli uses: jetli/wasm-bindgen-action@24ba6f9fff570246106ac3f80f35185600c3f6c9 + with: + version: '0.2.100' - name: core clippy check uses: actions-rs-plus/clippy-check@b09a9c37c9df7db8b1a5d52e8fe8e0b6e3d574c4 with: args: --all-targets --all-features -- -D warnings - - name: Wasm clippy check + - name: Wasm clippy check identity_wasm uses: actions-rs-plus/clippy-check@b09a9c37c9df7db8b1a5d52e8fe8e0b6e3d574c4 + if: ${{ false }} with: - args: --manifest-path ./bindings/wasm/Cargo.toml --target wasm32-unknown-unknown --all-targets --all-features -- -D warnings \ No newline at end of file + args: --manifest-path ./bindings/wasm/identity_wasm/Cargo.toml --target wasm32-unknown-unknown --all-targets --all-features -- -D warnings \ No newline at end of file diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index e943362458..ec09e1bde8 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -7,7 +7,7 @@ on: pull_request: branches: - main - - 'epic/**' + - 'feat/**' - 'support/**' paths: - '.github/workflows/format.yml' @@ -45,8 +45,8 @@ jobs: - name: core fmt check run: cargo +nightly fmt --all -- --check - - name: wasm fmt check - run: cargo +nightly fmt --manifest-path ./bindings/wasm/Cargo.toml --all -- --check + - name: wasm fmt check identity_wasm + run: cargo +nightly fmt --manifest-path ./bindings/wasm/identity_wasm/Cargo.toml --all -- --check - name: Cargo.toml fmt check run: diff --git a/.github/workflows/interactions-publish-to-npm.yml b/.github/workflows/interactions-publish-to-npm.yml index 3646517fc3..37a3ed347a 100644 --- a/.github/workflows/interactions-publish-to-npm.yml +++ b/.github/workflows/interactions-publish-to-npm.yml @@ -38,5 +38,5 @@ jobs: dry-run: ${{ github.event.inputs.dry-run }} input-artifact-name: identity-wasm-bindings-build npm-token: ${{ secrets.NPM_TOKEN }} - working-directory: iota_interaction_ts + working-directory: ./bindings/wasm/iota_interaction_ts tag: ${{ github.event.inputs.tag }} diff --git a/.github/workflows/rust-automatic-release-and-publish.yml b/.github/workflows/rust-automatic-release-and-publish.yml index 12825d9149..a7e9a22953 100644 --- a/.github/workflows/rust-automatic-release-and-publish.yml +++ b/.github/workflows/rust-automatic-release-and-publish.yml @@ -10,8 +10,7 @@ on: jobs: call-create-release-workflow: if: github.event.pull_request.merged == true - # owner/repository of workflow has to be static, see https://github.community/t/env-variables-in-uses/17466 - uses: iotaledger/identity.rs/.github/workflows/shared-release.yml@main + uses: './.github/workflows/shared-release.yml' with: changelog-config-path: ./.github/.github_changelog_generator pre-release-tag-regex: ^v[0-9]+\.[0-9]+\.[0-9]+-(?\w+)\.\d+$ diff --git a/.github/workflows/rust-create-hotfix-pr.yml b/.github/workflows/rust-create-hotfix-pr.yml index 13ed90557c..c2f9cdd2c9 100644 --- a/.github/workflows/rust-create-hotfix-pr.yml +++ b/.github/workflows/rust-create-hotfix-pr.yml @@ -9,8 +9,7 @@ on: jobs: create-hotfix-pr: - # owner/repository of workflow has to be static, see https://github.community/t/env-variables-in-uses/17466 - uses: iotaledger/identity.rs/.github/workflows/shared-create-hotfix-pr.yml@main + uses: './.github/workflows/shared-create-hotfix-pr.yml' with: branch: ${{ github.event.inputs.branch }} branch-regex: ^support\/v[0-9]+\.[0-9]+$ diff --git a/.github/workflows/rust-create-release-pr.yml b/.github/workflows/rust-create-release-pr.yml index 2db35ee710..644ea64e28 100644 --- a/.github/workflows/rust-create-release-pr.yml +++ b/.github/workflows/rust-create-release-pr.yml @@ -20,8 +20,7 @@ on: jobs: create-dev-release-pr: if: github.event.inputs.release-type != 'main' - # owner/repository of workflow has to be static, see https://github.community/t/env-variables-in-uses/17466 - uses: iotaledger/identity.rs/.github/workflows/shared-create-dev-release-pr.yml@main + uses: './.github/workflows/shared-create-dev-release-pr.yml' with: tag-prefix: v tag-postfix: -${{ github.event.inputs.release-type }}. @@ -34,8 +33,7 @@ jobs: GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} create-main-release-pr: if: github.event.inputs.release-type == 'main' - # owner/repository of workflow has to be static, see https://github.community/t/env-variables-in-uses/17466 - uses: iotaledger/identity.rs/.github/workflows/shared-create-main-release-pr.yml@main + uses: './.github/workflows/shared-create-main-release-pr.yml' with: tag-prefix: v tag-base: ${{ github.event.inputs.version }} diff --git a/.github/workflows/rust-deploy-docs.yml b/.github/workflows/rust-deploy-docs.yml index f7901ab521..58976dfeda 100644 --- a/.github/workflows/rust-deploy-docs.yml +++ b/.github/workflows/rust-deploy-docs.yml @@ -45,4 +45,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/shared-build-wasm.yml b/.github/workflows/shared-build-wasm.yml index 263e1da66c..a489b43feb 100644 --- a/.github/workflows/shared-build-wasm.yml +++ b/.github/workflows/shared-build-wasm.yml @@ -23,7 +23,7 @@ jobs: build-wasm: defaults: run: - working-directory: bindings/wasm + working-directory: bindings/wasm/ shell: bash runs-on: ubuntu-latest strategy: @@ -46,6 +46,7 @@ jobs: with: os: ${{ runner.os }} job: ${{ github.job }} + target: wasm32-unknown-unknown cargo-cache-enabled: true target-cache-enabled: true sccache-enabled: true @@ -55,6 +56,8 @@ jobs: # Download a pre-compiled wasm-bindgen binary. - name: Install wasm-bindgen-cli uses: jetli/wasm-bindgen-action@24ba6f9fff570246106ac3f80f35185600c3f6c9 + with: + version: '0.2.100' - name: Setup sccache uses: './.github/actions/rust/sccache/setup-sccache' @@ -64,17 +67,28 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v1 with: - node-version: 16.x + node-version: 20.x + + - name: Install JS dependencies + run: npm ci + working-directory: bindings/wasm/iota_interaction_ts + + - name: Build IOTA interaction bindings + run: npm run build + working-directory: bindings/wasm/iota_interaction_ts - name: Install JS dependencies run: npm ci + working-directory: bindings/wasm/identity_wasm - name: Build WASM bindings run: npm run build + working-directory: bindings/wasm/identity_wasm - name: Run Node unit tests if: ${{ inputs.run-unit-tests }} run: npm run test:unit:node + working-directory: bindings/wasm/identity_wasm - name: Stop sccache uses: './.github/actions/rust/sccache/stop-sccache' @@ -86,9 +100,11 @@ jobs: with: name: ${{ inputs.output-artifact-name }} path: | - bindings/wasm/node - bindings/wasm/web - bindings/wasm/examples/dist - bindings/wasm/docs + bindings/wasm/identity_wasm/node + bindings/wasm/identity_wasm/web + bindings/wasm/identity_wasm/examples/dist + bindings/wasm/identity_wasm/docs + bindings/wasm/iota_interaction_ts/node + bindings/wasm/iota_interaction_ts/web if-no-files-found: error retention-days: 1 diff --git a/.github/workflows/upload-docs.yml b/.github/workflows/upload-docs.yml index 7c98b7cb7e..c6d42ed3ba 100644 --- a/.github/workflows/upload-docs.yml +++ b/.github/workflows/upload-docs.yml @@ -17,8 +17,7 @@ permissions: jobs: build-wasm: - # owner/repository of workflow has to be static, see https://github.community/t/env-variables-in-uses/17466 - uses: iotaledger/identity.rs/.github/workflows/shared-build-wasm.yml@main + uses: './.github/workflows/shared-build-wasm.yml' with: run-unit-tests: false ref: ${{ inputs.ref }} @@ -43,7 +42,7 @@ jobs: echo VERSION=$VERSION >> $GITHUB_OUTPUT - name: Compress generated docs run: | - tar czvf wasm.tar.gz docs/* + tar czvf wasm.tar.gz identity_wasm/docs/* - name: Upload docs to AWS S3 env: diff --git a/.github/workflows/wasm-automatic-release-and-publish.yml b/.github/workflows/wasm-automatic-release-and-publish.yml index 1c7b4f141a..d2172d1ee7 100644 --- a/.github/workflows/wasm-automatic-release-and-publish.yml +++ b/.github/workflows/wasm-automatic-release-and-publish.yml @@ -11,10 +11,9 @@ on: jobs: call-create-release-workflow: if: github.event.pull_request.merged == true - # owner/repository of workflow has to be static, see https://github.community/t/env-variables-in-uses/17466 - uses: iotaledger/identity.rs/.github/workflows/shared-release.yml@main + uses: './.github/workflows/shared-release.yml' with: - changelog-config-path: ./bindings/wasm/.github_changelog_generator + changelog-config-path: ./bindings/wasm/identity_wasm/.github_changelog_generator pre-release-tag-regex: ^wasm-v[0-9]+\.[0-9]+\.[0-9]+-(?\w+)\.\d+$ main-release-tag-regex: ^wasm-v[0-9]+\.[0-9]+\.[0-9]+$ create-github-release: false @@ -25,9 +24,9 @@ jobs: build-wasm: needs: call-create-release-workflow if: ${{ needs.call-create-release-workflow.outputs.is-release }} - # owner/repository of workflow has to be static, see https://github.community/t/env-variables-in-uses/17466 - uses: iotaledger/identity.rs/.github/workflows/shared-build-wasm.yml@main + uses: './.github/workflows/shared-build-wasm.yml' with: + run-unit-tests: false output-artifact-name: identity-wasm-bindings-build release-wasm: diff --git a/.github/workflows/wasm-create-hotfix-pr.yml b/.github/workflows/wasm-create-hotfix-pr.yml index 12f12eb59a..f9d7b3897c 100644 --- a/.github/workflows/wasm-create-hotfix-pr.yml +++ b/.github/workflows/wasm-create-hotfix-pr.yml @@ -9,15 +9,14 @@ on: jobs: create-hotfix-pr: - # owner/repository of workflow has to be static, see https://github.community/t/env-variables-in-uses/17466 - uses: iotaledger/identity.rs/.github/workflows/shared-create-hotfix-pr.yml@main + uses: './.github/workflows/shared-create-hotfix-pr.yml' with: branch: ${{ github.event.inputs.branch }} branch-regex: ^support\/wasm-v[0-9]+\.[0-9]+$ tag-prefix: wasm-v main-tag-regex: ^wasm-v[0-9]+\.[0-9]+\.[0-9]+$ - changelog-config-path: ./bindings/wasm/.github_changelog_generator - changelog-path: ./bindings/wasm/CHANGELOG.md + changelog-config-path: ./bindings/wasm/identity_wasm/.github_changelog_generator + changelog-path: ./bindings/wasm/identity_wasm/CHANGELOG.md release-target: wasm secrets: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} diff --git a/.github/workflows/wasm-create-release-pr.yml b/.github/workflows/wasm-create-release-pr.yml index d5e7df3444..d91c8b31bc 100644 --- a/.github/workflows/wasm-create-release-pr.yml +++ b/.github/workflows/wasm-create-release-pr.yml @@ -20,15 +20,14 @@ on: jobs: create-dev-release-pr: if: github.event.inputs.release-type != 'main' - # owner/repository of workflow has to be static, see https://github.community/t/env-variables-in-uses/17466 - uses: iotaledger/identity.rs/.github/workflows/shared-create-dev-release-pr.yml@main + uses: './.github/workflows/shared-create-dev-release-pr.yml' with: tag-prefix: wasm-v tag-postfix: -${{ github.event.inputs.release-type }}. tag-base: ${{ github.event.inputs.version }} main-tag-regex: ^wasm-v[0-9]+\.[0-9]+\.[0-9]+$ - changelog-config-path: ./bindings/wasm/.github_changelog_generator - changelog-path: ./bindings/wasm/CHANGELOG.md + changelog-config-path: ./bindings/wasm/identity_wasm/.github_changelog_generator + changelog-path: ./bindings/wasm/identity_wasm/CHANGELOG.md pr-body-text: On merge a pre-release will be published to npm. release-target: wasm secrets: @@ -37,14 +36,13 @@ jobs: create-main-release-pr: if: github.event.inputs.release-type == 'main' - # owner/repository of workflow has to be static, see https://github.community/t/env-variables-in-uses/17466 - uses: iotaledger/identity.rs/.github/workflows/shared-create-main-release-pr.yml@main + uses: './.github/workflows/shared-create-main-release-pr.yml' with: tag-prefix: wasm-v tag-base: ${{ github.event.inputs.version }} main-tag-regex: ^wasm-v[0-9]+\.[0-9]+\.[0-9]+$ - changelog-config-path: ./bindings/wasm/.github_changelog_generator - changelog-path: ./bindings/wasm/CHANGELOG.md + changelog-config-path: ./bindings/wasm/identity_wasm/.github_changelog_generator + changelog-path: ./bindings/wasm/identity_wasm/CHANGELOG.md release-target: wasm secrets: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} diff --git a/.github/workflows/wasm-publish-to-npm.yml b/.github/workflows/wasm-publish-to-npm.yml index 266433e9eb..c8b188d7fe 100644 --- a/.github/workflows/wasm-publish-to-npm.yml +++ b/.github/workflows/wasm-publish-to-npm.yml @@ -18,9 +18,9 @@ on: jobs: build-wasm: - # owner/repository of workflow has to be static, see https://github.community/t/env-variables-in-uses/17466 - uses: iotaledger/identity.rs/.github/workflows/shared-build-wasm.yml@main + uses: './.github/workflows/shared-build-wasm.yml' with: + run-unit-tests: false ref: ${{ github.event.inputs.branch }} output-artifact-name: identity-wasm-bindings-build @@ -38,5 +38,5 @@ jobs: dry-run: ${{ github.event.inputs.dry-run }} input-artifact-name: identity-wasm-bindings-build npm-token: ${{ secrets.NPM_TOKEN }} - working-directory: identity_wasm + working-directory: ./bindings/wasm/identity_wasm tag: ${{ github.event.inputs.tag }} diff --git a/.gitignore b/.gitignore index 52f081d501..0bcf313dbe 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,10 @@ index.html *.hodl *.hodl.* -!/bindings/wasm/static/index.html +!/bindings/wasm/identity_wasm/static/index.html docs +# ignore IOTA build artifacts & package locks +build +identity_iota_core/packages/*/Move.lock + diff --git a/.license_template b/.license_template index a437281e00..21571572cb 100644 --- a/.license_template +++ b/.license_template @@ -1,2 +1,2 @@ -// Copyright {20\d{2}(-20\d{2})?} IOTA Stiftung{(?:, .+)?} +{(\/\/ Copyright.*\n)*?}// {(Modifications )?}Copyright {(\(c\) )?}{20\d{2}(-20\d{2})?} IOTA Stiftung{(?:, .+)?} // SPDX-License-Identifier: Apache-2.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index f51e2936c3..490f37d86c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [v1.6.0-alpha.1](https://github.com/iotaledger/identity.rs/tree/v1.6.0-alpha.1) (2024-14-04) + +[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v1.4.0...v1.6.0-alpha.1) + +This release is targeting IOTA Rebased networks and is meant for early testing. We still expect minor changes in the API and potentially in the on-chain objects. + +Identities created on IOTA Stardust networks can be migrated via the [Stardust package](https://docs.iota.org/developer/stardust/stardust-migration) + ## [v1.5.0](https://github.com/iotaledger/identity.rs/tree/v1.5.0) (2025-01-20) [Full Changelog](https://github.com/iotaledger/identity.rs/compare/v1.4.0...v1.5.0) diff --git a/Cargo.toml b/Cargo.toml index 6dc4d7dae2..e9870fa073 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,11 @@ +[workspace.package] +authors = ["IOTA Stiftung"] +edition = "2021" +homepage = "https://www.iota.org" +license = "Apache-2.0" +repository = "https://github.com/iotaledger/identity.rs" +rust-version = "1.65" + [workspace] resolver = "2" members = [ @@ -15,25 +23,28 @@ members = [ "identity_ecdsa_verifier", "identity_eddsa_verifier", "examples", + "identity_iota_interaction", + "bindings/wasm/iota_interaction_ts", ] -exclude = ["bindings/wasm", "bindings/grpc"] +exclude = ["bindings/wasm/identity_wasm", "bindings/grpc"] [workspace.dependencies] bls12_381_plus = { version = "0.8.17" } serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } -thiserror = { version = "1.0", default-features = false } -strum = { version = "0.25", default-features = false, features = ["std", "derive"] } serde_json = { version = "1.0", default-features = false } +strum = { version = "0.25", default-features = false, features = ["std", "derive"] } +thiserror = { version = "1.0", default-features = false } json-proof-token = { version = "0.3.5" } zkryptium = { version = "0.2.2", default-features = false, features = ["bbsplus"] } -[workspace.package] -authors = ["IOTA Stiftung"] -edition = "2021" -homepage = "https://www.iota.org" -license = "Apache-2.0" -repository = "https://github.com/iotaledger/identity.rs" - [workspace.lints.clippy] result_large_err = "allow" + +[profile.release.package.iota_interaction_ts] +opt-level = 's' +# Enabling debug for profile.release may lead to more helpful logged call stacks. +# TODO: Clarify if 'debug = true' facilitates error analysis via console logs. +# If not, remove the next line +# If yes, describe the helping effect in the comment above +# debug = true diff --git a/README.md b/README.md index 407314d782..14256c1249 100644 --- a/README.md +++ b/README.md @@ -23,33 +23,51 @@ --- > [!NOTE] -> This version of the library is compatible with IOTA Stardust networks, for a version of the library compatible with IOTA Rebased networks check [here](https://github.com/iotaledger/identity.rs/tree/feat/identity-rebased-alpha/) +> This version of the library is compatible with IOTA Rebased networks and in active development, for a version of the library compatible with IOTA Stardust networks check [here](https://github.com/iotaledger/identity.rs/) ## Introduction -IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentralized digital identity, also known as Self-Sovereign Identity (SSI). It implements the W3C [Decentralized Identifiers (DID)](https://www.w3.org/TR/did-core/) and [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) specifications. This library can be used to create, resolve and authenticate digital identities and to create verifiable credentials and presentations in order to share information in a verifiable manner and establish trust in the digital world. It does so while supporting secure storage of cryptographic keys, which can be implemented for your preferred key management system. Many of the individual libraries (Rust crates) are agnostic over the concrete DID method, with the exception of some libraries dedicated to implement the [IOTA DID method](https://wiki.iota.org/identity.rs/specs/did/iota_did_method_spec/), which is an implementation of decentralized digital identity on the IOTA and Shimmer networks. Written in stable Rust, IOTA Identity has strong guarantees of memory safety and process integrity while maintaining exceptional performance. +IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentralized digital identity, also known as Self-Sovereign Identity (SSI). It implements the W3C [Decentralized Identifiers (DID)](https://www.w3.org/TR/did-core/) and [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) specifications. This library can be used to create, resolve and authenticate digital identities and to create verifiable credentials and presentations in order to share information in a verifiable manner and establish trust in the digital world. It does so while supporting secure storage of cryptographic keys, which can be implemented for your preferred key management system. Many of the individual libraries (Rust crates) are agnostic over the concrete DID method, with the exception of some libraries dedicated to implement the [IOTA DID method](https://docs.iota.org/references/iota-identity/iota-did-method-spec/), which is an implementation of decentralized digital identity on IOTA Rebased networks. Written in stable Rust, IOTA Identity has strong guarantees of memory safety and process integrity while maintaining exceptional performance. -## Bindings + ## gRPC -We provide a collection of experimental [gRPC services](https://github.com/iotaledger/identity.rs/blob/HEAD/bindings/grpc/) +We provide a collection of experimental [gRPC services](https://github.com/iotaledger/identity.rs/blob/feat/identity-rebased-alpha/bindings/grpc/) ## Documentation and Resources - API References: - - [Rust API Reference](https://docs.rs/identity_iota/latest/identity_iota/): Package documentation (cargo docs). - - [Wasm API Reference](https://wiki.iota.org/identity.rs/libraries/wasm/api_reference/): Wasm Package documentation. -- [Identity Documentation Pages](https://wiki.iota.org/identity.rs/introduction): Supplementing documentation with context around identity and simple examples on library usage. -- [Examples](https://github.com/iotaledger/identity.rs/blob/HEAD/examples): Practical code snippets to get you started with the library. + - [Rust API Reference](https://iotaledger.github.io/identity.rs/identity_iota/index.html): Package documentation (cargo docs). + +- [Identity Documentation Pages](https://docs.iota.org/iota-identity): Supplementing documentation with context around identity and simple examples on library usage. +- [Examples](https://github.com/iotaledger/identity.rs/blob/feat/identity-rebased-alpha/examples): Practical code snippets to get you started with the library. + +## Universal Resolver + +IOTA Identity includes a [Universal Resolver](https://github.com/decentralized-identity/universal-resolver/) driver implementation for the `did:iota` method. The Universal Resolver is a crucial component that enables the resolution of DIDs across different DID methods. + +Our implementation allows for resolving IOTA DIDs through the standardized Universal Resolver interface, supporting multiple networks including testnet, devnet, and custom networks. The resolver is available as a Docker container for easy deployment and integration. + +For more information and implementation details, visit our [Universal Resolver Driver Repository](https://github.com/iotaledger/uni-resolver-driver-iota). + +### Quick Start with Docker + +```bash +# Pull and run the Universal Resolver driver +docker run -p 8080:8080 iotaledger/uni-resolver-driver-iota + +# Resolve a DID +curl -X GET http://localhost:8080/1.0/identifiers/did:iota:0xf4d6f08f5a1b80dd578da7dc1b49c886d580acd4cf7d48119dfeb82b538ad88a +``` ## Prerequisites -- [Rust](https://www.rust-lang.org/) (>= 1.65) -- [Cargo](https://doc.rust-lang.org/cargo/) (>= 1.65) +- [Rust](https://www.rust-lang.org/) (>= 1.83) +- [Cargo](https://doc.rust-lang.org/cargo/) (>= 1.83) ## Getting Started @@ -57,19 +75,23 @@ If you want to include IOTA Identity in your project, simply add it as a depende ```toml [dependencies] -identity_iota = { version = "1.5.0" } +identity_iota = { git = "https://github.com/iotaledger/identity.rs.git", tag = "v1.6.0-alpha" } ``` -To try out the [examples](https://github.com/iotaledger/identity.rs/blob/HEAD/examples), you can also do this: +To try out the [examples](https://github.com/iotaledger/identity.rs/blob/feat/identity-rebased-alpha/examples), you can also do this: 1. Clone the repository, e.g. through `git clone https://github.com/iotaledger/identity.rs` -2. Start IOTA Sandbox as described in the [next section](#example-creating-an-identity) -3. Run the example to create a DID using `cargo run --release --example 0_create_did` +2. Get the [IOTA binaries](https://github.com/iotaledger/iota/releases). +3. Start a local network for testing with `iota start --force-regenesis --with-faucet`. +4. Request funds with `iota client faucet`. +5. Publish a test identity package to your local network: `./identity_iota_core/scripts/publish_identity_package.sh`. +6. Get the `packageId` value from the output (the entry with `"type": "published"`) and pass this as `IOTA_IDENTITY_PKG_ID` env value. +7. Run the example to create a DID using `IOTA_IDENTITY_PKG_ID=(the value from previous step) run --release --example 0_create_did` ## Example: Creating an Identity The following code creates and publishes a new IOTA DID Document to a locally running private network. -See the [instructions](https://github.com/iotaledger/iota-sandbox) on running your own private network for development. +See the [instructions](https://github.com/iotaledger/iota/docker/iota-private-network) on running your own private network for development. _Cargo.toml_ @@ -77,9 +99,9 @@ _Cargo.toml_ Test this example using https://github.com/anko/txm: `txm README.md` !test program -cd ../.. +cd ../../.. mkdir tmp -cat | sed -e 's#identity_iota = { version = "[^"]*"#identity_iota = { path = "../identity_iota"#' > tmp/Cargo.toml +cat | sed -e 's#identity_iota = { git = "[^"]*", tag = "[^"]*"#identity_iota = { path = "../identity_iota"#' > tmp/Cargo.toml echo '[workspace]' >>tmp/Cargo.toml --> @@ -91,11 +113,11 @@ version = "1.0.0" edition = "2021" [dependencies] -identity_iota = { version = "1.5.0", features = ["memstore"] } -iota-sdk = { version = "1.0.2", default-features = true, features = ["tls", "client", "stronghold"] } -tokio = { version = "1", features = ["full"] } anyhow = "1.0.62" +identity_iota = { git = "https://github.com/iotaledger/identity.rs.git", tag = "v1.6.0-alpha", features = ["memstore"] } +iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v0.9.2-rc" } rand = "0.8.5" +tokio = { version = "1", features = ["full"] } ``` _main.__rs_ @@ -112,78 +134,62 @@ timeout 360 cargo build || (echo "Process timed out after 360 seconds" && exit 1 --> - ```rust,no_run -use identity_iota::core::ToJson; -use identity_iota::iota::IotaClientExt; +use anyhow::Context; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; +use identity_iota::iota::rebased::client::convert_to_address; +use identity_iota::iota::rebased::client::get_sender_public_key; +use identity_iota::iota::rebased::client::IdentityClient; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; +use identity_iota::iota::rebased::transaction::Transaction; use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkMemStore; +use identity_iota::storage::JwkStorage; use identity_iota::storage::KeyIdMemstore; +use identity_iota::storage::KeyType; use identity_iota::storage::Storage; +use identity_iota::storage::StorageSigner; use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodScope; -use iota_sdk::client::api::GetAddressesOptions; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::crypto::keys::bip39; -use iota_sdk::types::block::address::Bech32Address; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::dto::AliasOutputDto; +use iota_sdk::IotaClientBuilder; use tokio::io::AsyncReadExt; -// The endpoint of the IOTA node to use. -static API_ENDPOINT: &str = "http://localhost"; - -/// Demonstrates how to create a DID Document and publish it in a new Alias Output. +/// Demonstrates how to create a DID Document and publish it in a new identity. #[tokio::main] async fn main() -> anyhow::Result<()> { // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() + let iota_client = IotaClientBuilder::default() + .build_localnet() + .await + .map_err(|err| anyhow::anyhow!(format!("failed to connect to network; {}", err)))?; + + // Create new storage and generate new key. + let storage = Storage::new(JwkMemStore::new(), KeyIdMemstore::new()); + let generate = storage + .key_storage() + .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) .await?; - - // Create a new Stronghold. - let stronghold = StrongholdSecretManager::builder() - .password("secure_password".to_owned()) - .build("./example-strong.hodl")?; - - // Generate a mnemonic and store it in the Stronghold. - let random: [u8; 32] = rand::random(); - let mnemonic = - bip39::wordlist::encode(random.as_ref(), &bip39::wordlist::ENGLISH).map_err(|err| anyhow::anyhow!("{err:?}"))?; - stronghold.store_mnemonic(mnemonic).await?; - - // Create a new secret manager backed by the Stronghold. - let secret_manager: SecretManager = SecretManager::Stronghold(stronghold); - - // Get the Bech32 human-readable part (HRP) of the network. - let network_name: NetworkName = client.network_name().await?; - - // Get an address from the secret manager. - let address: Bech32Address = secret_manager - .generate_ed25519_addresses( - GetAddressesOptions::default() - .with_range(0..1) - .with_bech32_hrp((&network_name).try_into()?), - ) - .await?[0]; - - println!("Your wallet address is: {}", address); - println!("Please request funds from http://localhost/faucet/, wait for a couple of seconds and then press Enter."); + let public_key_jwk = generate.jwk.to_public().expect("public components should be derivable"); + let public_key_bytes = get_sender_public_key(&public_key_jwk)?; + let sender_address = convert_to_address(&public_key_bytes)?; + let package_id = std::env::var("IOTA_IDENTITY_PKG_ID") + .map_err(|e| { + anyhow::anyhow!("env variable IOTA_IDENTITY_PKG_ID must be set in order to run the examples").context(e) + }) + .and_then(|pkg_str| pkg_str.parse().context("invalid package id"))?; + + // Create identity client with signing capabilities. + let read_only_client = IdentityClientReadOnly::new_with_pkg_id(iota_client, package_id).await?; + let signer = StorageSigner::new(&storage, generate.key_id, public_key_jwk); + let identity_client = IdentityClient::new(read_only_client, signer).await?; + + println!("Your wallet address is: {}", sender_address); + println!("Please request funds from http://127.0.0.1:9123/gas, wait for a couple of seconds and then press Enter."); tokio::io::stdin().read_u8().await?; // Create a new DID document with a placeholder DID. - // The DID will be derived from the Alias Id of the Alias Output after publishing. - let mut document: IotaDocument = IotaDocument::new(&network_name); - - // Insert a new Ed25519 verification method in the DID document. - let storage: Storage = Storage::new(JwkMemStore::new(), KeyIdMemstore::new()); - document + let mut unpublished: IotaDocument = IotaDocument::new(identity_client.network()); + unpublished .generate_method( &storage, JwkMemStore::ED25519_KEY_TYPE, @@ -193,13 +199,13 @@ async fn main() -> anyhow::Result<()> { ) .await?; - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - let alias_output: AliasOutput = client.new_did_output(address.into(), document, None).await?; - println!("Alias Output: {}", AliasOutputDto::from(&alias_output).to_json_pretty()?); + // Publish new DID document. + let document = identity_client + .publish_did_document(unpublished) + .execute(&identity_client) + .await? + .output; - // Publish the Alias Output and get the published DID document. - let document: IotaDocument = client.publish_did_output(&secret_manager, alias_output).await?; println!("Published DID document: {:#}", document); Ok(()) @@ -230,8 +236,6 @@ _Example output_ "meta": { "created": "2023-08-29T14:47:26Z", "updated": "2023-08-29T14:47:26Z", - "governorAddress": "tst1qqd7kyu8xadzx9vutznu72336npqpj92jtp27uyu2tj2sa5hx6n3k0vrzwv", - "stateControllerAddress": "tst1qqd7kyu8xadzx9vutznu72336npqpj92jtp27uyu2tj2sa5hx6n3k0vrzwv" } } ``` diff --git a/bindings/grpc/Cargo.toml b/bindings/grpc/Cargo.toml index 2b542712db..bd66481494 100644 --- a/bindings/grpc/Cargo.toml +++ b/bindings/grpc/Cargo.toml @@ -17,27 +17,34 @@ name = "identity-grpc" path = "src/main.rs" [dependencies] -anyhow = "1.0.75" +anyhow = "1.0" futures = { version = "0.3" } identity_eddsa_verifier = { path = "../../identity_eddsa_verifier" } -identity_iota = { path = "../../identity_iota", features = ["resolver", "sd-jwt", "domain-linkage", "domain-linkage-fetch", "status-list-2021"] } +identity_iota = { path = "../../identity_iota", features = ["resolver", "sd-jwt", "domain-linkage", "domain-linkage-fetch", "status-list-2021", "iota-client", "send-sync-storage"] } +identity_jose = { path = "../../identity_jose" } +identity_storage = { path = "../../identity_storage", features = ["memstore"] } identity_stronghold = { path = "../../identity_stronghold", features = ["send-sync-storage"] } -iota-sdk = { version = "1.1.5", features = ["stronghold"] } -openssl = { version = "0.10", features = ["vendored"] } -prost = "0.12" +iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v0.9.2-rc" } +iota-sdk-legacy = { package = "iota-sdk", version = "1.1.2", features = ["stronghold"] } +prost = "0.13" rand = "0.8.5" -serde = { version = "1.0.193", features = ["derive", "alloc"] } -serde_json = { version = "1.0.108", features = ["alloc"] } -thiserror = "1.0.50" +serde = { version = "1.0", features = ["derive", "alloc"] } +serde_json = { version = "1.0", features = ["alloc"] } +thiserror = "1.0" tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } tokio-stream = { version = "0.1.14", features = ["net"] } -tonic = "0.10" +tonic = "0.12" tracing = { version = "0.1.40", features = ["async-await"] } tracing-subscriber = "0.3.18" url = { version = "2.5", default-features = false } +[target.x86_64-unknown-linux-musl.dependencies] +openssl = { version = "0.10", features = ["vendored"] } # this is not an unused dependency but required for the docker build + [dev-dependencies] +fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "5f2c63266a065996d53f98156f0412782b468597", package = "fastcrypto" } identity_storage = { path = "../../identity_storage", features = ["memstore"] } +jsonpath-rust = "0.7" [build-dependencies] -tonic-build = "0.10" +tonic-build = "0.12" diff --git a/bindings/grpc/proto/document.proto b/bindings/grpc/proto/document.proto index d25558c243..49f86af94d 100644 --- a/bindings/grpc/proto/document.proto +++ b/bindings/grpc/proto/document.proto @@ -5,8 +5,8 @@ syntax = "proto3"; package document; message CreateDIDRequest { - // An IOTA's bech32 encoded address. - string bech32_address = 1; + // KeyID for getting the public key from stronghold. + string key_id = 1; } message CreateDIDResponse { diff --git a/bindings/grpc/src/main.rs b/bindings/grpc/src/main.rs index 04927b1c9c..cc9ee844df 100644 --- a/bindings/grpc/src/main.rs +++ b/bindings/grpc/src/main.rs @@ -1,11 +1,15 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::str::FromStr; + use anyhow::Context; use identity_grpc::server::GRpcServer; use identity_stronghold::StrongholdStorage; -use iota_sdk::client::stronghold::StrongholdAdapter; -use iota_sdk::client::Client; +use iota_sdk_legacy::client::stronghold::StrongholdAdapter; + +use identity_iota::iota::rebased::client::IdentityClientReadOnly; +use iota_sdk::types::base_types::ObjectID; #[tokio::main] #[tracing::instrument(err)] @@ -15,15 +19,19 @@ async fn main() -> anyhow::Result<()> { let api_endpoint = std::env::var("API_ENDPOINT")?; - let client: Client = Client::builder() - .with_primary_node(&api_endpoint, None)? - .finish() - .await?; + let identity_iota_pkg_id = std::env::var("IDENTITY_IOTA_PKG_ID")?; + + let identity_pkg_id = ObjectID::from_str(&identity_iota_pkg_id)?; + + let iota_client = iota_sdk::IotaClientBuilder::default().build(api_endpoint).await?; + + let read_only_client = IdentityClientReadOnly::new_with_pkg_id(iota_client, identity_pkg_id).await?; + let stronghold = init_stronghold()?; let addr = "0.0.0.0:50051".parse()?; tracing::info!("gRPC server listening on {}", addr); - GRpcServer::new(client, stronghold).serve(addr).await?; + GRpcServer::new(read_only_client, stronghold).serve(addr).await?; Ok(()) } diff --git a/bindings/grpc/src/server.rs b/bindings/grpc/src/server.rs index c7fa5b527c..02274d6a71 100644 --- a/bindings/grpc/src/server.rs +++ b/bindings/grpc/src/server.rs @@ -4,7 +4,7 @@ use std::net::SocketAddr; use identity_stronghold::StrongholdStorage; -use iota_sdk::client::Client; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; use tonic::transport::server::Router; use tonic::transport::server::Server; @@ -17,7 +17,7 @@ pub struct GRpcServer { } impl GRpcServer { - pub fn new(client: Client, stronghold: StrongholdStorage) -> Self { + pub fn new(client: IdentityClientReadOnly, stronghold: StrongholdStorage) -> Self { let router = Server::builder().add_routes(services::routes(&client, &stronghold)); Self { router, stronghold } } diff --git a/bindings/grpc/src/services/credential/jwt.rs b/bindings/grpc/src/services/credential/jwt.rs index 6cfb3368e6..af2705ff70 100644 --- a/bindings/grpc/src/services/credential/jwt.rs +++ b/bindings/grpc/src/services/credential/jwt.rs @@ -12,7 +12,7 @@ use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwsSignatureOptions; use identity_iota::storage::Storage; use identity_stronghold::StrongholdStorage; -use iota_sdk::client::Client; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; use tonic::Request; use tonic::Response; use tonic::Status; @@ -31,7 +31,7 @@ pub struct JwtService { } impl JwtService { - pub fn new(client: &Client, stronghold: &StrongholdStorage) -> Self { + pub fn new(client: &IdentityClientReadOnly, stronghold: &StrongholdStorage) -> Self { let mut resolver = Resolver::new(); resolver.attach_iota_handler(client.clone()); Self { @@ -80,6 +80,6 @@ impl JwtSvc for JwtService { } } -pub fn service(client: &Client, stronghold: &StrongholdStorage) -> JwtServer { +pub fn service(client: &IdentityClientReadOnly, stronghold: &StrongholdStorage) -> JwtServer { JwtServer::new(JwtService::new(client, stronghold)) } diff --git a/bindings/grpc/src/services/credential/mod.rs b/bindings/grpc/src/services/credential/mod.rs index 8d71ccacee..7322899f6f 100644 --- a/bindings/grpc/src/services/credential/mod.rs +++ b/bindings/grpc/src/services/credential/mod.rs @@ -6,10 +6,10 @@ pub mod revocation; pub mod validation; use identity_stronghold::StrongholdStorage; -use iota_sdk::client::Client; -use tonic::transport::server::RoutesBuilder; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; +use tonic::service::RoutesBuilder; -pub fn init_services(routes: &mut RoutesBuilder, client: &Client, stronghold: &StrongholdStorage) { +pub fn init_services(routes: &mut RoutesBuilder, client: &IdentityClientReadOnly, stronghold: &StrongholdStorage) { routes.add_service(revocation::service(client)); routes.add_service(jwt::service(client, stronghold)); routes.add_service(validation::service(client)); diff --git a/bindings/grpc/src/services/credential/revocation.rs b/bindings/grpc/src/services/credential/revocation.rs index d637bce22e..7c2f118ee3 100644 --- a/bindings/grpc/src/services/credential/revocation.rs +++ b/bindings/grpc/src/services/credential/revocation.rs @@ -12,7 +12,7 @@ use identity_iota::credential::RevocationBitmapStatus; use identity_iota::credential::{self}; use identity_iota::prelude::IotaDocument; use identity_iota::prelude::Resolver; -use iota_sdk::client::Client; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; use prost::bytes::Bytes; use serde::Deserialize; use serde::Serialize; @@ -107,7 +107,7 @@ pub struct CredentialVerifier { } impl CredentialVerifier { - pub fn new(client: &Client) -> Self { + pub fn new(client: &IdentityClientReadOnly) -> Self { let mut resolver = Resolver::new(); resolver.attach_iota_handler(client.clone()); Self { resolver } @@ -156,6 +156,6 @@ impl CredentialRevocation for CredentialVerifier { } } -pub fn service(client: &Client) -> CredentialRevocationServer { +pub fn service(client: &IdentityClientReadOnly) -> CredentialRevocationServer { CredentialRevocationServer::new(CredentialVerifier::new(client)) } diff --git a/bindings/grpc/src/services/credential/validation.rs b/bindings/grpc/src/services/credential/validation.rs index fb218b727b..ed033a5913 100644 --- a/bindings/grpc/src/services/credential/validation.rs +++ b/bindings/grpc/src/services/credential/validation.rs @@ -16,7 +16,7 @@ use identity_iota::credential::StatusCheck; use identity_iota::iota::IotaDID; use identity_iota::resolver; use identity_iota::resolver::Resolver; -use iota_sdk::client::Client; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; use _credentials::vc_validation_server::VcValidation; use _credentials::vc_validation_server::VcValidationServer; @@ -63,7 +63,7 @@ pub struct VcValidator { } impl VcValidator { - pub fn new(client: &Client) -> Self { + pub fn new(client: &IdentityClientReadOnly) -> Self { let mut resolver = Resolver::new(); resolver.attach_iota_handler(client.clone()); Self { resolver } @@ -130,6 +130,6 @@ impl VcValidation for VcValidator { } } -pub fn service(client: &Client) -> VcValidationServer { +pub fn service(client: &IdentityClientReadOnly) -> VcValidationServer { VcValidationServer::new(VcValidator::new(client)) } diff --git a/bindings/grpc/src/services/document.rs b/bindings/grpc/src/services/document.rs index 0ed1298637..7104e1e96e 100644 --- a/bindings/grpc/src/services/document.rs +++ b/bindings/grpc/src/services/document.rs @@ -6,19 +6,22 @@ use _document::document_service_server::DocumentServiceServer; use _document::CreateDidRequest; use _document::CreateDidResponse; use identity_iota::core::ToJson; -use identity_iota::iota::IotaClientExt; +use identity_iota::iota::rebased::client::IdentityClient; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; +use identity_iota::iota::rebased::transaction::Transaction; +use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkStorageDocumentError; use identity_iota::storage::Storage; use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodScope; +use identity_storage::KeyId; +use identity_storage::KeyStorageErrorKind; +use identity_storage::StorageSigner; +use identity_stronghold::StrongholdKeyType; use identity_stronghold::StrongholdStorage; use identity_stronghold::ED25519_KEY_TYPE; -use iota_sdk::client::Client; -use iota_sdk::types::block::address::Address; -use std::error::Error as _; use tonic::Code; use tonic::Request; use tonic::Response; @@ -30,31 +33,31 @@ mod _document { #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("The provided address is not a valid bech32 encoded address")] - InvalidAddress, #[error(transparent)] IotaClientError(identity_iota::iota::Error), #[error(transparent)] StorageError(JwkStorageDocumentError), + #[error(transparent)] + StrongholdError(identity_iota::core::SingleStructError), + #[error(transparent)] + IdentityClientError(identity_iota::iota::rebased::Error), + #[error("did error : {0}")] + DIDError(String), } impl From for Status { fn from(value: Error) -> Self { - let code = match &value { - Error::InvalidAddress => Code::InvalidArgument, - _ => Code::Internal, - }; - Status::new(code, value.to_string()) + Status::new(Code::Internal, value.to_string()) } } pub struct DocumentSvc { storage: Storage, - client: Client, + client: IdentityClientReadOnly, } impl DocumentSvc { - pub fn new(client: &Client, stronghold: &StrongholdStorage) -> Self { + pub fn new(client: &IdentityClientReadOnly, stronghold: &StrongholdStorage) -> Self { Self { storage: Storage::new(stronghold.clone(), stronghold.clone()), client: client.clone(), @@ -72,35 +75,55 @@ impl DocumentService for DocumentSvc { err, )] async fn create(&self, req: Request) -> Result, Status> { - let CreateDidRequest { bech32_address } = req.into_inner(); - let address = Address::try_from_bech32(&bech32_address).map_err(|_| Error::InvalidAddress)?; - let network_name = self.client.network_name().await.map_err(Error::IotaClientError)?; + let CreateDidRequest { key_id } = req.into_inner(); + + let key_id = KeyId::new(key_id); + let pub_key = self + .storage + .key_id_storage() + .get_public_key_with_type(&key_id, StrongholdKeyType::Ed25519) + .await + .map_err(Error::StrongholdError)?; + + let network_name = self.client.network(); + + let storage = StorageSigner::new(&self.storage, key_id, pub_key); - let mut document = IotaDocument::new(&network_name); + let identity_client = IdentityClient::new(self.client.clone(), storage) + .await + .map_err(Error::IdentityClientError)?; + + let mut created_identity = identity_client + .create_identity(IotaDocument::new(network_name)) + .finish() + .execute(&identity_client) + .await + .map_err(Error::IdentityClientError)? + .output; + + let did = + IotaDID::parse(format!("did:iota:{}", created_identity.id())).map_err(|e| Error::DIDError(e.to_string()))?; + + let mut document = IotaDocument::new_with_id(did.clone()); let fragment = document .generate_method( &self.storage, ED25519_KEY_TYPE.clone(), JwsAlgorithm::EdDSA, - None, + Some(identity_client.signer().key_id().as_str()), MethodScope::VerificationMethod, ) .await .map_err(Error::StorageError)?; - let alias_output = self - .client - .new_did_output(address, document, None) + created_identity + .update_did_document(document.clone()) + .finish(&identity_client) .await - .map_err(Error::IotaClientError)?; - - let document = self - .client - .publish_did_output(self.storage.key_storage().as_secret_manager(), alias_output) + .map_err(Error::IdentityClientError)? + .execute(&identity_client) .await - .map_err(Error::IotaClientError) - .inspect_err(|e| tracing::error!("{:?}", e.source()))?; - let did = document.id(); + .map_err(Error::IdentityClientError)?; Ok(Response::new(CreateDidResponse { document_json: document.to_json().unwrap(), @@ -110,6 +133,6 @@ impl DocumentService for DocumentSvc { } } -pub fn service(client: &Client, stronghold: &StrongholdStorage) -> DocumentServiceServer { +pub fn service(client: &IdentityClientReadOnly, stronghold: &StrongholdStorage) -> DocumentServiceServer { DocumentServiceServer::new(DocumentSvc::new(client, stronghold)) } diff --git a/bindings/grpc/src/services/domain_linkage.rs b/bindings/grpc/src/services/domain_linkage.rs index bb8b214982..ebdb4c1369 100644 --- a/bindings/grpc/src/services/domain_linkage.rs +++ b/bindings/grpc/src/services/domain_linkage.rs @@ -29,7 +29,7 @@ use identity_iota::did::CoreDID; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; use identity_iota::resolver::Resolver; -use iota_sdk::client::Client; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; use serde::Deserialize; use serde::Serialize; use thiserror::Error; @@ -109,7 +109,7 @@ pub struct DomainLinkageService { } impl DomainLinkageService { - pub fn new(client: &Client) -> Self { + pub fn new(client: &IdentityClientReadOnly) -> Self { let mut resolver = Resolver::new(); resolver.attach_iota_handler(client.clone()); Self { resolver } @@ -421,6 +421,6 @@ impl DomainLinkage for DomainLinkageService { } } -pub fn service(client: &Client) -> DomainLinkageServer { +pub fn service(client: &IdentityClientReadOnly) -> DomainLinkageServer { DomainLinkageServer::new(DomainLinkageService::new(client)) } diff --git a/bindings/grpc/src/services/mod.rs b/bindings/grpc/src/services/mod.rs index 00abe17ce1..7dd588b733 100644 --- a/bindings/grpc/src/services/mod.rs +++ b/bindings/grpc/src/services/mod.rs @@ -10,11 +10,11 @@ pub mod status_list_2021; pub mod utils; use identity_stronghold::StrongholdStorage; -use iota_sdk::client::Client; -use tonic::transport::server::Routes; -use tonic::transport::server::RoutesBuilder; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; +use tonic::service::Routes; +use tonic::service::RoutesBuilder; -pub fn routes(client: &Client, stronghold: &StrongholdStorage) -> Routes { +pub fn routes(client: &IdentityClientReadOnly, stronghold: &StrongholdStorage) -> Routes { let mut routes = RoutesBuilder::default(); routes.add_service(health_check::service()); credential::init_services(&mut routes, client, stronghold); diff --git a/bindings/grpc/src/services/sd_jwt.rs b/bindings/grpc/src/services/sd_jwt.rs index af792e51f6..0194501501 100644 --- a/bindings/grpc/src/services/sd_jwt.rs +++ b/bindings/grpc/src/services/sd_jwt.rs @@ -20,7 +20,7 @@ use identity_iota::iota::IotaDocument; use identity_iota::resolver::Resolver; use identity_iota::sd_jwt_payload::SdJwt; use identity_iota::sd_jwt_payload::SdObjectDecoder; -use iota_sdk::client::Client; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; use serde::Deserialize; use serde::Serialize; use thiserror::Error; @@ -91,7 +91,7 @@ pub struct SdJwtService { } impl SdJwtService { - pub fn new(client: &Client) -> Self { + pub fn new(client: &IdentityClientReadOnly) -> Self { let mut resolver = Resolver::new(); resolver.attach_iota_handler(client.clone()); Self { resolver } @@ -159,6 +159,6 @@ impl Verification for SdJwtService { } } -pub fn service(client: &Client) -> VerificationServer { +pub fn service(client: &IdentityClientReadOnly) -> VerificationServer { VerificationServer::new(SdJwtService::new(client)) } diff --git a/bindings/grpc/src/services/utils.rs b/bindings/grpc/src/services/utils.rs index 0e7d2fc570..d1bf4c588d 100644 --- a/bindings/grpc/src/services/utils.rs +++ b/bindings/grpc/src/services/utils.rs @@ -8,6 +8,7 @@ use _utils::DataSigningResponse; use identity_iota::storage::JwkStorage; use identity_iota::storage::KeyId; use identity_iota::storage::KeyStorageError; +use identity_stronghold::StrongholdKeyType; use identity_stronghold::StrongholdStorage; use tonic::Request; use tonic::Response; @@ -51,7 +52,11 @@ impl SigningSvc for SigningService { async fn sign(&self, req: Request) -> Result, Status> { let DataSigningRequest { data, key_id } = req.into_inner(); let key_id = KeyId::new(key_id); - let public_key_jwk = self.storage.get_public_key(&key_id).await.map_err(Error)?; + let public_key_jwk = self + .storage + .get_public_key_with_type(&key_id, StrongholdKeyType::Ed25519) + .await + .map_err(Error)?; let signature = self .storage .sign(&key_id, &data, &public_key_jwk) diff --git a/bindings/grpc/tests/api/credential_revocation_check.rs b/bindings/grpc/tests/api/credential_revocation_check.rs index 9e92197c72..a2dc1722d7 100644 --- a/bindings/grpc/tests/api/credential_revocation_check.rs +++ b/bindings/grpc/tests/api/credential_revocation_check.rs @@ -7,9 +7,11 @@ use identity_iota::credential::RevocationBitmap; use identity_iota::credential::RevocationBitmapStatus; use identity_iota::credential::{self}; use identity_iota::did::DID; +use identity_stronghold::StrongholdStorage; use serde_json::json; use crate::credential_revocation_check::credentials::RevocationCheckRequest; +use crate::helpers::make_stronghold; use crate::helpers::Entity; use crate::helpers::TestServer; @@ -21,7 +23,10 @@ mod credentials { async fn checking_status_of_credential_works() -> anyhow::Result<()> { let server = TestServer::new().await; let client = server.client(); - let mut issuer = Entity::new(); + + let stronghold = StrongholdStorage::new(make_stronghold()); + + let mut issuer = Entity::new_with_stronghold(stronghold); issuer.create_did(client).await?; let mut subject = Entity::new(); @@ -61,7 +66,7 @@ async fn checking_status_of_credential_works() -> anyhow::Result<()> { // Revoke credential issuer - .update_document(&client, |mut doc| { + .update_document(client, |mut doc| { doc.revoke_credentials("my-revocation-service", &[3]).ok().map(|_| doc) }) .await?; diff --git a/bindings/grpc/tests/api/credential_validation.rs b/bindings/grpc/tests/api/credential_validation.rs index f1bfedf100..dbb8bb1700 100644 --- a/bindings/grpc/tests/api/credential_validation.rs +++ b/bindings/grpc/tests/api/credential_validation.rs @@ -62,8 +62,8 @@ async fn credential_validation() -> anyhow::Result<()> { .unwrap() .create_credential_jwt( &credential, - &issuer.storage(), - &issuer.fragment().unwrap(), + issuer.storage(), + issuer.fragment().unwrap(), &JwsSignatureOptions::default(), None, ) @@ -128,8 +128,8 @@ async fn revoked_credential_validation() -> anyhow::Result<()> { .unwrap() .create_credential_jwt( &credential, - &issuer.storage(), - &issuer.fragment().unwrap(), + issuer.storage(), + issuer.fragment().unwrap(), &JwsSignatureOptions::default(), None, ) diff --git a/bindings/grpc/tests/api/did_document_creation.rs b/bindings/grpc/tests/api/did_document_creation.rs index 394217e7a3..0c480c9deb 100644 --- a/bindings/grpc/tests/api/did_document_creation.rs +++ b/bindings/grpc/tests/api/did_document_creation.rs @@ -2,14 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use identity_stronghold::StrongholdStorage; -use iota_sdk::types::block::address::ToBech32Ext; +use identity_iota::iota::rebased::utils::request_funds; + use tonic::Request; -use crate::helpers::get_address_with_funds; +use crate::helpers::get_address; use crate::helpers::make_stronghold; use crate::helpers::Entity; use crate::helpers::TestServer; -use crate::helpers::FAUCET_ENDPOINT; + use _document::document_service_client::DocumentServiceClient; use _document::CreateDidRequest; @@ -21,21 +22,16 @@ mod _document { async fn did_document_creation() -> anyhow::Result<()> { let stronghold = StrongholdStorage::new(make_stronghold()); let server = TestServer::new_with_stronghold(stronghold.clone()).await; - let api_client = server.client(); - let hrp = api_client.get_bech32_hrp().await?; let user = Entity::new_with_stronghold(stronghold); - let user_address = get_address_with_funds( - api_client, - user.storage().key_storage().as_secret_manager(), - FAUCET_ENDPOINT, - ) - .await?; + let (user_address, key_id, _) = get_address(user.storage()).await?; + + request_funds(&user_address).await?; let mut grpc_client = DocumentServiceClient::connect(server.endpoint()).await?; grpc_client .create(Request::new(CreateDidRequest { - bech32_address: user_address.to_bech32(hrp).to_string(), + key_id: key_id.as_str().to_string(), })) .await?; diff --git a/bindings/grpc/tests/api/domain_linkage.rs b/bindings/grpc/tests/api/domain_linkage.rs index 4870c74a8d..96b7b933a8 100644 --- a/bindings/grpc/tests/api/domain_linkage.rs +++ b/bindings/grpc/tests/api/domain_linkage.rs @@ -69,7 +69,7 @@ async fn prepare_test() -> anyhow::Result<(TestServer, Url, Url, String, Jwt)> { let service_url: DIDUrl = did.clone().join("#domain-linkage")?; let linked_domain_service: LinkedDomainService = LinkedDomainService::new(service_url, domains, Object::new())?; issuer - .update_document(&api_client, |mut doc| { + .update_document(api_client, |mut doc| { doc.insert_service(linked_domain_service.into()).ok().map(|_| doc) }) .await?; @@ -101,8 +101,8 @@ async fn prepare_test() -> anyhow::Result<(TestServer, Url, Url, String, Jwt)> { let jwt: Jwt = updated_did_document .create_credential_jwt( &domain_linkage_credential, - &issuer.storage(), - &issuer + issuer.storage(), + issuer .fragment() .ok_or_else(|| anyhow::anyhow!("no fragment for issuer"))?, &JwsSignatureOptions::default(), @@ -125,16 +125,25 @@ async fn can_validate_domain() -> anyhow::Result<()> { did_configuration: configuration_resource.to_string(), }) .await?; - let did_id = IotaDocument::from_json(&did)?.id().to_string(); + + // created and updated are retrieved directly from the contract and not from the given DID document, + // so we'll replace them with values from the result for the check here + let validated_did_response = response.into_inner(); + let mut did_document: IotaDocument = IotaDocument::from_json(&did)?; + let did_id = did_document.id().to_string(); + let response_did_document: IotaDocument = + serde_json::from_str(&validated_did_response.linked_dids.as_ref().unwrap().valid[0].did).unwrap(); + did_document.metadata.created = response_did_document.metadata.created; + did_document.metadata.updated = response_did_document.metadata.updated; assert_eq!( - response.into_inner(), + validated_did_response, ValidateDomainResponse { linked_dids: Some(LinkedDids { invalid: vec![], valid: vec![ValidDid { service_id: did_id, - did: did.to_string().clone(), + did: did_document.to_string().clone(), credential: jwt.as_str().to_string(), }] }), diff --git a/bindings/grpc/tests/api/health_check.rs b/bindings/grpc/tests/api/health_check.rs index d8ea486269..ef3a703c55 100644 --- a/bindings/grpc/tests/api/health_check.rs +++ b/bindings/grpc/tests/api/health_check.rs @@ -1,13 +1,13 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use health_check::health_check_client::HealthCheckClient; -use health_check::HealthCheckRequest; -use health_check::HealthCheckResponse; +use _health_check::health_check_client::HealthCheckClient; +use _health_check::HealthCheckRequest; +use _health_check::HealthCheckResponse; use crate::helpers::TestServer; -mod health_check { +mod _health_check { tonic::include_proto!("health_check"); } diff --git a/bindings/grpc/tests/api/helpers.rs b/bindings/grpc/tests/api/helpers.rs index c307213db7..dae3f5d08c 100644 --- a/bindings/grpc/tests/api/helpers.rs +++ b/bindings/grpc/tests/api/helpers.rs @@ -1,49 +1,68 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use anyhow::anyhow; use anyhow::Context; -use identity_iota::iota::IotaClientExt; +use fastcrypto::ed25519::Ed25519PublicKey; +use fastcrypto::traits::ToFromBytes; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::iota::NetworkName; use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodScope; +use identity_jose::jwk::Jwk; use identity_storage::key_id_storage::KeyIdMemstore; use identity_storage::key_storage::JwkMemStore; use identity_storage::JwkDocumentExt; use identity_storage::JwkStorage; +use identity_storage::KeyId; use identity_storage::KeyIdStorage; +use identity_storage::KeyType; use identity_storage::Storage; +use identity_storage::StorageSigner; use identity_stronghold::StrongholdStorage; -use iota_sdk::client::api::GetAddressesOptions; -use iota_sdk::client::node_api::indexer::query_parameters::QueryParameter; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::stronghold::StrongholdAdapter; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::crypto::keys::bip39; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::address::Bech32Address; -use iota_sdk::types::block::address::Hrp; -use iota_sdk::types::block::output::AliasOutputBuilder; +use identity_iota::iota::rebased::client::IdentityClient; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; +use identity_iota::iota::rebased::transaction::Transaction; +use iota_sdk_legacy::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk_legacy::client::stronghold::StrongholdAdapter; +use iota_sdk_legacy::client::Password; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::IotaClient; +use iota_sdk::IotaClientBuilder; +use jsonpath_rust::JsonPathQuery; use rand::distributions::Alphanumeric; use rand::distributions::DistString; use rand::thread_rng; +use serde_json::Value; +use std::io::Write; use std::net::SocketAddr; use std::path::PathBuf; +use std::str::FromStr; + use tokio::net::TcpListener; +use tokio::process::Command; +use tokio::sync::OnceCell; use tokio::task::JoinHandle; use tonic::transport::Uri; +const TEST_GAS_BUDGET: u64 = 50_000_000; + +type MemStorage = Storage; -pub type MemStorage = Storage; +const FAUCET_ENDPOINT: &str = "http://localhost:9123/gas"; -pub const API_ENDPOINT: &str = "http://localhost"; -pub const FAUCET_ENDPOINT: &str = "http://localhost/faucet/api/enqueue"; +static PACKAGE_ID: OnceCell = OnceCell::const_new(); + +const SCRIPT_DIR: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../", + "identity_iota_core", + "/scripts" +); +const CACHED_PKG_ID: &str = "/tmp/identity_iota_pkg_id.txt"; -#[derive(Debug)] pub struct TestServer { - client: Client, + client: IdentityClientReadOnly, addr: SocketAddr, _handle: JoinHandle>, } @@ -67,20 +86,28 @@ impl TestServer { .expect("Failed to bind to random OS's port"); let addr = listener.local_addr().unwrap(); - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None) - .unwrap() - .finish() + let iota_client = IotaClientBuilder::default() + .build_localnet() .await .expect("Failed to connect to API's endpoint"); - let server = identity_grpc::server::GRpcServer::new(client.clone(), stronghold) + let identity_pkg_id = PACKAGE_ID + .get_or_try_init(|| init(&iota_client)) + .await + .copied() + .expect("failed to publish package ID"); + + let identity_client = IdentityClientReadOnly::new(iota_client, identity_pkg_id) + .await + .expect("Failed to build Identity client"); + + let server = identity_grpc::server::GRpcServer::new(identity_client.clone(), stronghold) .into_router() .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener)); TestServer { _handle: tokio::spawn(server), addr, - client, + client: identity_client, } } @@ -90,31 +117,38 @@ impl TestServer { .expect("Failed to parse server's URI") } - pub fn client(&self) -> &Client { + pub fn client(&self) -> &IdentityClientReadOnly { &self.client } } pub async fn create_did( - client: &Client, - secret_manager: &mut SecretManager, + client: &IdentityClientReadOnly, storage: &Storage, -) -> anyhow::Result<(Address, IotaDocument, String)> +) -> anyhow::Result<(IotaAddress, IotaDocument, KeyId, String)> where K: JwkStorage, I: KeyIdStorage, { - let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT) - .await - .context("failed to get address with funds")?; + let (address, key_id, pub_key_jwk) = get_address(storage).await.context("failed to get address with funds")?; - let network_name = client.network_name().await?; - let (document, fragment): (IotaDocument, String) = create_did_document(&network_name, storage).await?; - let alias_output = client.new_did_output(address, document, None).await?; + // Fund the account + request_faucet_funds(address, FAUCET_ENDPOINT).await?; - let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?; + let signer = StorageSigner::new(storage, key_id.clone(), pub_key_jwk); - Ok((address, document, fragment)) + let identity_client = IdentityClient::new(client.clone(), signer).await?; + + let network_name = client.network(); + let (document, fragment): (IotaDocument, String) = create_did_document(network_name, storage).await?; + + let document = identity_client + .publish_did_document(document) + .execute(&identity_client) + .await? + .output; + + Ok((address, document, key_id, fragment)) } /// Creates an example DID document with the given `network_name`. @@ -144,77 +178,57 @@ where Ok((document, fragment)) } -/// Generates an address from the given [`SecretManager`] and adds funds from the faucet. -pub async fn get_address_with_funds( - client: &Client, - stronghold: &SecretManager, - faucet_endpoint: &str, -) -> anyhow::Result
{ - let address = get_address(client, stronghold).await?; - - request_faucet_funds(client, address, faucet_endpoint) - .await - .context("failed to request faucet funds")?; +/// Generates a new Ed25519 key pair +pub async fn get_address(storage: &Storage) -> anyhow::Result<(IotaAddress, KeyId, Jwk)> +where + K: JwkStorage, + I: KeyIdStorage, +{ + let generated_key = storage + .key_storage() + .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) + .await?; - Ok(*address) -} + let key_id = generated_key.key_id; -/// Initializes the [`SecretManager`] with a new mnemonic, if necessary, -/// and generates an address from the given [`SecretManager`]. -pub async fn get_address(client: &Client, secret_manager: &SecretManager) -> anyhow::Result { - let random: [u8; 32] = rand::random(); - let mnemonic = bip39::wordlist::encode(random.as_ref(), &bip39::wordlist::ENGLISH) - .map_err(|err| anyhow::anyhow!(format!("{err:?}")))?; - - if let SecretManager::Stronghold(ref stronghold) = secret_manager { - match stronghold.store_mnemonic(mnemonic).await { - Ok(()) => (), - Err(iota_sdk::client::stronghold::Error::MnemonicAlreadyStored) => (), - Err(err) => anyhow::bail!(err), - } - } else { - anyhow::bail!("expected a `StrongholdSecretManager`"); - } + let pub_key_jwt = generated_key.jwk.to_public().expect("should not fail"); + let pub_key_bytes = pub_key_jwt + .try_okp_params() + .map(|key| identity_jose::jwu::decode_b64(key.x.clone()).expect("should be decodeable"))?; - let bech32_hrp: Hrp = client.get_bech32_hrp().await?; - let address: Bech32Address = secret_manager - .generate_ed25519_addresses( - GetAddressesOptions::default() - .with_range(0..1) - .with_bech32_hrp(bech32_hrp), - ) - .await?[0]; + let address = Ed25519PublicKey::from_bytes(&pub_key_bytes)?; - Ok(address) + Ok((IotaAddress::from(&address), key_id, pub_key_jwt)) } /// Requests funds from the faucet for the given `address`. -async fn request_faucet_funds(client: &Client, address: Bech32Address, faucet_endpoint: &str) -> anyhow::Result<()> { - iota_sdk::client::request_funds_from_faucet(faucet_endpoint, &address).await?; - - tokio::time::timeout(std::time::Duration::from_secs(45), async { - loop { - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - - let balance = get_address_balance(client, &address) - .await - .context("failed to get address balance")?; - if balance > 0 { - break; - } - } - Ok::<(), anyhow::Error>(()) - }) - .await - .context("maximum timeout exceeded")??; +async fn request_faucet_funds(address: IotaAddress, faucet_endpoint: &str) -> anyhow::Result<()> { + let output = Command::new("iota") + .arg("client") + .arg("faucet") + .arg("--address") + .arg(address.to_string()) + .arg("--url") + .arg(faucet_endpoint) + .arg("--json") + .output() + .await + .context("Failed to execute command")?; + + // Check if the output is success + if !output.status.success() { + anyhow::bail!( + "Failed to request funds from faucet: {}", + std::str::from_utf8(&output.stderr).unwrap() + ); + } Ok(()) } pub struct Entity { - secret_manager: SecretManager, storage: Storage, - did: Option<(Address, IotaDocument, String)>, + did: Option<(IotaAddress, IotaDocument, KeyId, String)>, } pub fn random_password(len: usize) -> Password { @@ -232,14 +246,9 @@ pub fn random_stronghold_path() -> PathBuf { impl Default for Entity { fn default() -> Self { - let secret_manager = SecretManager::Stronghold(make_stronghold()); let storage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - Self { - secret_manager, - storage, - did: None, - } + Self { storage, did: None } } } @@ -251,25 +260,17 @@ impl Entity { impl Entity { pub fn new_with_stronghold(s: StrongholdStorage) -> Self { - let secret_manager = SecretManager::Stronghold(make_stronghold()); let storage = Storage::new(s.clone(), s); - Self { - secret_manager, - storage, - did: None, - } + Self { storage, did: None } } } impl Entity { - pub async fn create_did(&mut self, client: &Client) -> anyhow::Result<()> { - let Entity { - secret_manager, - storage, - did, - } = self; - *did = Some(create_did(client, secret_manager, storage).await?); + pub async fn create_did(&mut self, client: &IdentityClientReadOnly) -> anyhow::Result<()> { + let Entity { storage, did, .. } = self; + + *did = Some(create_did(client, storage).await?); Ok(()) } @@ -279,53 +280,40 @@ impl Entity { } pub fn document(&self) -> Option<&IotaDocument> { - self.did.as_ref().map(|(_, doc, _)| doc) + self.did.as_ref().map(|(_, doc, _, _)| doc) } pub fn fragment(&self) -> Option<&str> { - self.did.as_ref().map(|(_, _, frag)| frag.as_ref()) + self.did.as_ref().map(|(_, _, _, frag)| frag.as_ref()) } +} - pub async fn update_document(&mut self, client: &Client, f: F) -> anyhow::Result<()> +impl Entity { + pub async fn update_document(&mut self, client: &IdentityClientReadOnly, f: F) -> anyhow::Result<()> where F: FnOnce(IotaDocument) -> Option, { - let (address, doc, fragment) = self.did.take().context("Missing doc")?; + let (address, doc, key_id, fragment) = self.did.take().context("Missing doc")?; let mut new_doc = f(doc.clone()); if let Some(doc) = new_doc.take() { - let alias_output = client.update_did_output(doc.clone()).await?; - let rent_structure = client.get_rent_structure().await?; - let alias_output = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; + let Entity { storage, .. } = self; - new_doc = Some(client.publish_did_output(&self.secret_manager, alias_output).await?); - } + let public_key = storage.key_id_storage().get_public_key(&key_id).await?; + let signer = StorageSigner::new(storage, key_id.clone(), public_key); - self.did = Some((address, new_doc.unwrap_or(doc), fragment)); + let identity_client = IdentityClient::new(client.clone(), signer).await?; - Ok(()) - } -} -/// Returns the balance of the given Bech32-encoded `address`. -async fn get_address_balance(client: &Client, address: &Bech32Address) -> anyhow::Result { - let output_ids = client - .basic_output_ids(vec![ - QueryParameter::Address(address.to_owned()), - QueryParameter::HasExpiration(false), - QueryParameter::HasTimelock(false), - QueryParameter::HasStorageDepositReturn(false), - ]) - .await?; + new_doc = Some( + identity_client + .publish_did_document_update(doc, TEST_GAS_BUDGET) + .await?, + ); + } - let outputs = client.get_outputs(&output_ids).await?; + self.did = Some((address, new_doc.unwrap_or(doc), key_id, fragment)); - let mut total_amount = 0; - for output_response in outputs { - total_amount += output_response.output().amount(); + Ok(()) } - - Ok(total_amount) } pub fn make_stronghold() -> StrongholdAdapter { @@ -334,3 +322,71 @@ pub fn make_stronghold() -> StrongholdAdapter { .build(random_stronghold_path()) .expect("Failed to create temporary stronghold") } + +async fn get_active_address() -> anyhow::Result { + Command::new("iota") + .arg("client") + .arg("active-address") + .arg("--json") + .output() + .await + .context("Failed to execute command") + .and_then(|output| Ok(serde_json::from_slice::(&output.stdout)?)) +} + +async fn init(iota_client: &IotaClient) -> anyhow::Result { + let network_id = iota_client.read_api().get_chain_identifier().await?; + let address = get_active_address().await?; + + // Request Funds + + request_faucet_funds(address, FAUCET_ENDPOINT).await.unwrap(); + + if let Ok(id) = std::env::var("IDENTITY_IOTA_PKG_ID").or(get_cached_id(&network_id).await) { + std::env::set_var("IDENTITY_IOTA_PKG_ID", id.clone()); + id.parse().context("failed to parse object id from str") + } else { + publish_package(address).await + } +} + +async fn get_cached_id(network_id: &str) -> anyhow::Result { + let cache = tokio::fs::read_to_string(CACHED_PKG_ID).await?; + let (cached_id, cached_network_id) = cache.split_once(';').ok_or(anyhow!("Invalid or empty cached data"))?; + + if cached_network_id == network_id { + Ok(cached_id.to_owned()) + } else { + Err(anyhow!("A network change has invalidated the cached data")) + } +} + +async fn publish_package(active_address: IotaAddress) -> anyhow::Result { + let output = Command::new("sh") + .current_dir(SCRIPT_DIR) + .arg("publish_identity_package.sh") + .output() + .await?; + let stdout = std::str::from_utf8(&output.stdout).unwrap(); + + if !output.status.success() { + let stderr = std::str::from_utf8(&output.stderr).unwrap(); + anyhow::bail!("Failed to publish move package: \n\n{stdout}\n\n{stderr}"); + } + + let package_id: ObjectID = { + let stdout_trimmed = stdout.trim(); + ObjectID::from_str(stdout_trimmed).with_context(|| { + let stderr = std::str::from_utf8(&output.stderr).unwrap(); + format!("failed to find IDENTITY_IOTA_PKG_ID in response from: '{stdout_trimmed}'; {stderr}") + })? + }; + + // Persist package ID in order to avoid publishing the package for every test. + let package_id_str = package_id.to_string(); + std::env::set_var("IDENTITY_IOTA_PKG_ID", package_id_str.as_str()); + let mut file = std::fs::File::create(CACHED_PKG_ID)?; + write!(&mut file, "{};{}", package_id_str, active_address)?; + + Ok(package_id) +} diff --git a/bindings/grpc/tests/api/jwt.rs b/bindings/grpc/tests/api/jwt.rs index 927027b300..d996329a00 100644 --- a/bindings/grpc/tests/api/jwt.rs +++ b/bindings/grpc/tests/api/jwt.rs @@ -9,7 +9,7 @@ use identity_iota::core::ToJson; use identity_iota::credential::CredentialBuilder; use identity_iota::did::DID; use identity_stronghold::StrongholdStorage; -use iota_sdk::Url; +use iota_sdk_legacy::Url; use serde_json::json; use crate::helpers::make_stronghold; diff --git a/bindings/grpc/tests/api/main.rs b/bindings/grpc/tests/api/main.rs index af4929bfae..d070ba4c04 100644 --- a/bindings/grpc/tests/api/main.rs +++ b/bindings/grpc/tests/api/main.rs @@ -1,13 +1,22 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +#[cfg(test)] mod credential_revocation_check; +#[cfg(test)] mod credential_validation; +#[cfg(test)] mod did_document_creation; +#[cfg(test)] mod domain_linkage; +#[cfg(test)] mod health_check; +#[cfg(test)] mod helpers; +#[cfg(test)] mod jwt; +#[cfg(test)] mod sd_jwt_validation; +#[cfg(test)] mod status_list_2021; mod utils; diff --git a/bindings/grpc/tests/api/status_list_2021.rs b/bindings/grpc/tests/api/status_list_2021.rs index 67ad31b34d..726cd2fd9c 100644 --- a/bindings/grpc/tests/api/status_list_2021.rs +++ b/bindings/grpc/tests/api/status_list_2021.rs @@ -82,9 +82,7 @@ async fn status_list_2021_credential_update() -> anyhow::Result<()> { status_list_credential.update(|status_list| { for idx in entries_to_set { - if let Err(e) = status_list.set_entry(idx as usize, true) { - return Err(e); - } + status_list.set_entry(idx as usize, true)? } Ok(()) })?; diff --git a/bindings/wasm/README.md b/bindings/wasm/README.md index b7cee7a287..35cefa4264 100644 --- a/bindings/wasm/README.md +++ b/bindings/wasm/README.md @@ -1,310 +1,105 @@ -# IOTA Identity WASM +# WASM build projects using wasm-bindgen -> This is the 1.0 version of the official WASM bindings for [IOTA Identity](https://github.com/iotaledger/identity.rs). +This folder contains several crates using wasm-bindgen to import or export TS types from & to JS runtimes. These crates +are named _artifact_ in the following to indicate that the NodeJS based JS build system is used instead of cargo. -## [API Reference](https://wiki.iota.org/identity.rs/libraries/wasm/api_reference) +The `build` folder provides build scripts needed to build the artifacts. -## [Examples](https://github.com/iotaledger/identity.rs/blob/main/bindings/wasm/examples/README.md) +Here is an overview of the existing artifacts: -## Install the library: +* `identity_wasm`
+ Exports the IdentityClient to Typescript using wasm-bindgen generated wasm bindings -Latest Release: this version matches the `main` branch of this repository. +* `iota_interaction_ts`
+ Imports Typescript IOTA Client SDK types using wasm-bindgen generated wasm bindings + and implements identity_iota_interaction traits (among others, IotaClient and MoveCall traits) for wasm32 platforms. -```bash -npm install @iota/identity-wasm -``` +## Building an Artifact -## Build +For build instructions please have a look into the artifact README file. -Alternatively, you can build the bindings yourself if you have Rust installed. If not, refer to [rustup.rs](https://rustup.rs) for the installation. +## Build process in general -Install [`wasm-bindgen-cli`](https://github.com/rustwasm/wasm-bindgen). A manual installation is required because we use the [Weak References](https://rustwasm.github.io/wasm-bindgen/reference/weak-references.html) feature, which [`wasm-pack` does not expose](https://github.com/rustwasm/wasm-pack/issues/930). +Each artifact is located in its own artifact folder (see above) containing the following important files and subfolders: -```bash -cargo install --force wasm-bindgen-cli -``` +* `tsconfig` files for the `nodejs` and `web` runtimes +* The `package.json` file +* `lib` folder
+ Contains TS files used for wasm-bindings + * Contains `tsconfig` files for the `nodejs` and `web` runtimes with additional TS compiler configurations +* `node` folder
+ Distribution folder for the `nodejs` runtime +* `web` folder
+ Distribution folder for the `web` runtime +* `src` folder
+ Rust code of the crate/artifact +* `tests` folder
+ Test code +* `examples` folder
+ Example code -Then, install the necessary dependencies using: +The build process is defined by run scripts contained in the artifacts `package.json` file . +The build process for the `nodejs` and `web` runtimes, consists of the following steps: -```bash -npm install -``` +* cargo build of the crate with target wasm32-unknown-unknown +* wasm-bindgen CLI call, generating `___.js` and `___.d.ts` files in the distribution folder of the artifact (`node` or + `web`) +* execute the `build/node` or `build/web` build script (see below) +* typescript transpiler call (tsc)
+ Converts the TS files in the `lib` folder into JS files. + JS files are written into the distribution folder of the artifact. + The distribution folder is configured + in the applied tsconfig file (located in the `lib` folder of the artifact). +* execute the `build/replace_paths` build script (see below) -and build the bindings for `node.js` with +## Build scripts contained in the `build` folder -```bash -npm run build:nodejs -``` +### node.js -or for the `web` with +Used by the `bundle:nodejs` run task in the package.json file of the artifact. + +Process steps: + +* Add a [node-fetch polyfill](https://github.com/seanmonstar/reqwest/issues/910) + at the top of the main js file of the artifact +* Generate a `package.json` file derived from the original package.json of the artifact + (done by `utils/generatePackage.js`) + +### web.js + +Used by the `bundle:web` run task in the package.json file of the artifact. + +Process steps: + +* In the main js file of the artifact: + * Comment out a webpack workaround by commenting out all occurrences of
+ `input = new URL(, import.meta.url);` + * Create an init function which imports the artifact wasm file. +* In the typescript source map file `.d.ts`: + * Adds the declaration of the above created init function to the typescript source map file +* Generate a `package.json` file derived from the original package.json file of the artifact + (done by `utils/generatePackage.js`) + +### replace_paths.js + +Processes all JS and TS files contained in the artifact distribution folder that have previously been created +by wasm-bindgen and the TS compiler (tsc) call. + +For each file, it replaces aliases defined in the +[compilerOptions.paths](https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths) +configuration of a specific +tsconfig file by the last entry of the aliases path list (only 1 or 2 paths supported). + +It is used by the following run tasks for the following tsconfig files and distribution folders: + +| run task | tsconfig file | distribution folder | +|----------------------|--------------------------------|---------------------| +| `bundle:nodejs` | `./lib/tsconfig.json` | `node` | +| `bundle:web` | `./lib/tsconfig.web.json` | `web` | +| `build:examples:web` | `./examples/tsconfig.web.json` | `./examples/dist` | -```bash -npm run build:web -``` -## Minimum Requirements -The minimum supported version for node is: `v16` - -## NodeJS Usage - -The following code creates a new IOTA DID Document suitable for publishing to a locally running private network. -See the [instructions](https://github.com/iotaledger/hornet/tree/develop/private_tangle) on running your own private network. - - - - -```typescript -const { - Jwk, - JwkType, - EdCurve, - MethodScope, - IotaDocument, - VerificationMethod, - Service, - MethodRelationship, - IotaIdentityClient, -} = require('@iota/identity-wasm/node'); -const { Client } = require('@iota/sdk-wasm/node'); -const EXAMPLE_JWK = new Jwk({ - kty: JwkType.Okp, - crv: EdCurve.Ed25519, - x: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", -}); -// The endpoint of the IOTA node to use. -const API_ENDPOINT = "http://localhost"; - -/** Demonstrate how to create a DID Document. */ -async function main() { - // Create a new client with the given network endpoint. - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - - const didClient = new IotaIdentityClient(client); - - // Get the Bech32 human-readable part (HRP) of the network. - const networkHrp = await didClient.getNetworkHrp(); - - // Create a new DID document with a placeholder DID. - // The DID will be derived from the Alias Id of the Alias Output after publishing. - const document = new IotaDocument(networkHrp); - - // Insert a new Ed25519 verification method in the DID document. - const method = VerificationMethod.newFromJwk( - document.id(), - EXAMPLE_JWK, - "#key-1" - ); - document.insertMethod(method, MethodScope.VerificationMethod()); - - // Attach a new method relationship to the existing method. - document.attachMethodRelationship( - document.id().join("#key-1"), - MethodRelationship.Authentication - ); - - // Add a new Service. - const service = new Service({ - id: document.id().join("#linked-domain"), - type: "LinkedDomains", - serviceEndpoint: "https://iota.org/", - }); - document.insertService(service); - - console.log(`Created document `, JSON.stringify(document.toJSON(), null, 2)); -} - -main(); -``` - -which prints - -``` -Created document { - "id": "did:iota:tst:0x0000000000000000000000000000000000000000000000000000000000000000", - "verificationMethod": [ - { - "id": "did:iota:tst:0x0000000000000000000000000000000000000000000000000000000000000000#key-1", - "controller": "did:iota:tst:0x0000000000000000000000000000000000000000000000000000000000000000", - "type": "JsonWebKey", - "publicKeyJwk": { - "kty": "OKP", - "crv": "Ed25519", - "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" - } - } - ], - "authentication": [ - "did:iota:tst:0x0000000000000000000000000000000000000000000000000000000000000000#key-1" - ], - "service": [ - { - "id": "did:iota:tst:0x0000000000000000000000000000000000000000000000000000000000000000#linked-domain", - "type": "LinkedDomains", - "serviceEndpoint": "https://iota.org/" - } - ] -} -``` - -**NOTE: see the [examples](https://github.com/iotaledger/identity.rs/blob/main/bindings/wasm/examples/README.md) for how to publish an IOTA DID Document.** - -## Web Setup - -The library loads the WASM file with an HTTP GET request, so the .wasm file must be copied to the root of the dist folder. - -### Rollup - -- Install `rollup-plugin-copy`: - -```bash -$ npm install rollup-plugin-copy --save-dev -``` - -- Add the copy plugin usage to the `plugins` array under `rollup.config.js`: - -```js -// Include the copy plugin -import copy from "rollup-plugin-copy"; - -// Add the copy plugin to the `plugins` array of your rollup config: -copy({ - targets: [ - { - src: "node_modules/@iota/sdk-wasm/web/wasm/iota_sdk_wasm_bg.wasm", - dest: "public", - rename: "iota_sdk_wasm_bg.wasm", - }, - { - src: "node_modules/@iota/identity-wasm/web/identity_wasm_bg.wasm", - dest: "public", - rename: "identity_wasm_bg.wasm", - }, - ], -}); -``` - -### Webpack - -- Install `copy-webpack-plugin`: - -```bash -$ npm install copy-webpack-plugin --save-dev -``` - -```js -// Include the copy plugin -const CopyWebPlugin= require('copy-webpack-plugin'); - -// Add the copy plugin to the `plugins` array of your webpack config: - -new CopyWebPlugin({ - patterns: [ - { - from: 'node_modules/@iota/sdk-wasm/web/wasm/iota_sdk_wasm_bg.wasm', - to: 'iota_sdk_wasm_bg.wasm' - }, - { - from: 'node_modules/@iota/identity-wasm/web/identity_wasm_bg.wasm', - to: 'identity_wasm_bg.wasm' - } - ] -}), -``` - -### Web Usage - -```typescript -import init, { Client } from "@iota/sdk-wasm/web"; -import * as identity from "@iota/identity-wasm/web"; - -// The endpoint of the IOTA node to use. -const API_ENDPOINT = "http://localhost"; - -const EXAMPLE_JWK = new identity.Jwk({ - kty: identity.JwkType.Okp, - crv: identity.EdCurve.Ed25519, - x: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", -}); - -/** Demonstrate how to create a DID Document. */ -async function createDocument() { - // Create a new client with the given network endpoint. - const iotaClient = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - - const didClient = new identity.IotaIdentityClient(iotaClient); - - // Get the Bech32 human-readable part (HRP) of the network. - const networkHrp = await didClient.getNetworkHrp(); - - // Create a new DID document with a placeholder DID. - // The DID will be derived from the Alias Id of the Alias Output after publishing. - const document = new identity.IotaDocument(networkHrp); - - // Insert a new Ed25519 verification method in the DID document. - let method = identity.VerificationMethod.newFromJwk( - document.id(), - EXAMPLE_JWK, - "#key-1" - ); - document.insertMethod(method, identity.MethodScope.VerificationMethod()); - - // Attach a new method relationship to the existing method. - document.attachMethodRelationship( - document.id().join("#key-1"), - identity.MethodRelationship.Authentication - ); - - // Add a new Service. - const service = new identity.Service({ - id: document.id().join("#linked-domain"), - type: "LinkedDomains", - serviceEndpoint: "https://iota.org/", - }); - document.insertService(service); - - console.log(`Created document `, JSON.stringify(document.toJSON(), null, 2)); -} - -init() - .then(() => identity.init()) - .then(() => { - await createDocument(); - }); - -// or - -(async () => { - await init(); - await identity.init(); - - await createDocument(); -})(); -// Default path is "identity_wasm_bg.wasm", but you can override it like this -await identity.init("./static/identity_wasm_bg.wasm"); -``` - -Calling `identity.init().then()` or `await identity.init()` is required to load the Wasm file from the server if not available, because of that it will only be slow for the first time. - -**NOTE: see the [examples](https://github.com/iotaledger/identity.rs/blob/main/bindings/wasm/examples/README.md) for how to publish an IOTA DID Document.** - -## Examples in the Wild - -You may find it useful to see how the WASM bindings are being used in existing applications: - -- [Zebra IOTA Edge SDK](https://github.com/ZebraDevs/Zebra-Iota-Edge-SDK) (mobile apps using Capacitor.js + Svelte) diff --git a/bindings/wasm/build/node.js b/bindings/wasm/build/node.js index 376b3f5be1..95c58bd3ce 100644 --- a/bindings/wasm/build/node.js +++ b/bindings/wasm/build/node.js @@ -3,9 +3,12 @@ const fs = require("fs"); const { lintAll } = require("./lints"); const generatePackage = require("./utils/generatePackage"); -const RELEASE_FOLDER = path.join(__dirname, "../node/"); -const entryFilePathNode = path.join(RELEASE_FOLDER, "identity_wasm.js"); +const artifact = process.argv[2]; + +const RELEASE_FOLDER = path.join(__dirname, "..", artifact, "node"); +const entryFilePathNode = path.join(RELEASE_FOLDER, `${artifact}.js`); const entryFileNode = fs.readFileSync(entryFilePathNode).toString(); +console.log(`[build/node.js] Processing entryFile '${entryFilePathNode}' for artifact '${artifact}'`); lintAll(entryFileNode); @@ -26,10 +29,15 @@ fs.writeFileSync( entryFilePathNode, changedFileNode, ); +console.log( + `[build/node.js] Added node-fetch polyfill to entryFile '${entryFilePathNode}'. Starting generatePackage().`, +); // Generate `package.json`. const newPackage = generatePackage({ main: "index.js", types: "index.d.ts", + artifact, }); fs.writeFileSync(path.join(RELEASE_FOLDER, "package.json"), JSON.stringify(newPackage, null, 2)); +console.log(`[build/node.js] Finished processing entryFile '${entryFilePathNode}' for artifact '${artifact}'`); diff --git a/bindings/wasm/build/replace_paths.js b/bindings/wasm/build/replace_paths.js index 4b4199ce5a..0ac2c0e403 100644 --- a/bindings/wasm/build/replace_paths.js +++ b/bindings/wasm/build/replace_paths.js @@ -6,19 +6,20 @@ const path = require("path"); * If more than one path is defined. The second path is used. Otherwise the first path. * @param {string} tsconfig - Path to tsconfig that should be processed * @param {string} dist - Folder of files that should be processed + * @param {string} artifact - Name of the artifact folder. Example: "indentity_wasm" * @param {'resolve'=} mode - In "resolve" mode relative paths will be replaced paths relative to the processed file. Note: `basePath` in the tsconfig will not be considered. */ -function replace(tsconfig, dist, mode) { +function replace(tsconfig, dist, artifact, mode) { // Read tsconfig file. - const tsconfigPath = path.join(__dirname, "..", tsconfig); + const tsconfigPath = path.join(__dirname, "..", artifact, tsconfig); console.log(`\n using ${tsconfigPath}`); - let data = JSON.parse(fs.readFileSync(path.join(__dirname, "..", tsconfig), "utf8")); + let data = JSON.parse(fs.readFileSync(tsconfigPath, "utf8")); let a = data.compilerOptions.paths; let keys = Object.keys(a); // Get `.js` and `.ts` file names from directory. - const distPath = path.join(__dirname, `../${dist}`); + const distPath = path.join(__dirname, "..", artifact, dist); console.log(`\n working in ${distPath}`); let files = readdirSync(distPath); files = files.filter((fileName) => fileName.endsWith(".ts") || fileName.endsWith(".js")); @@ -52,4 +53,4 @@ const readdirSync = (p, a = []) => { return a; }; -replace(process.argv[2], process.argv[3], process.argv[4]); +replace(process.argv[2], process.argv[3], process.argv[4], process.argv[5]); diff --git a/bindings/wasm/build/utils/generatePackage.js b/bindings/wasm/build/utils/generatePackage.js index 25109c2b06..261058869d 100644 --- a/bindings/wasm/build/utils/generatePackage.js +++ b/bindings/wasm/build/utils/generatePackage.js @@ -1,6 +1,5 @@ -const rootPackage = require("../../package.json"); - module.exports = (options) => { + const rootPackage = require(`../../${options.artifact}/package.json`); const newPackage = { name: rootPackage.name, description: rootPackage.description, diff --git a/bindings/wasm/build/web.js b/bindings/wasm/build/web.js index 722072d21e..e4830dda54 100644 --- a/bindings/wasm/build/web.js +++ b/bindings/wasm/build/web.js @@ -1,12 +1,14 @@ const path = require("path"); const fs = require("fs"); -const fse = require("fs-extra"); const { lintAll } = require("./lints"); const generatePackage = require("./utils/generatePackage"); -const RELEASE_FOLDER = path.join(__dirname, "../web/"); -const entryFilePath = path.join(RELEASE_FOLDER, "identity_wasm.js"); +const artifact = process.argv[2]; + +const RELEASE_FOLDER = path.join(__dirname, "..", artifact, "web"); +const entryFilePath = path.join(RELEASE_FOLDER, `${artifact}.js`); const entryFile = fs.readFileSync(entryFilePath).toString(); +console.log(`[build/web.js] Processing entryFile '${entryFilePath}' for artifact '${artifact}'`); lintAll(entryFile); @@ -19,15 +21,16 @@ let changedFile = entryFile ) // Create an init function which imports the wasm file. .concat( - "let __initializedIotaWasm = false\r\n\r\nexport function init(path) {\r\n if (__initializedIotaWasm) {\r\n return Promise.resolve(wasm)\r\n }\r\n return __wbg_init(path || 'identity_wasm_bg.wasm').then(() => {\r\n __initializedIotaWasm = true\r\n return wasm\r\n })\r\n}\r\n", + `let __initializedIotaWasm = false\r\n\r\nexport function init(path) {\r\n if (__initializedIotaWasm) {\r\n return Promise.resolve(wasm)\r\n }\r\n return __wbg_init(path || '${artifact}_bg.wasm').then(() => {\r\n __initializedIotaWasm = true\r\n return wasm\r\n })\r\n}\r\n`, ); fs.writeFileSync( entryFilePath, changedFile, ); +console.log(`[build/web.js] Commented out webpack workaround for '${entryFilePath}'.`); -const entryFilePathTs = path.join(RELEASE_FOLDER, "identity_wasm.d.ts"); +const entryFilePathTs = path.join(RELEASE_FOLDER, `${artifact}.d.ts`); const entryFileTs = fs.readFileSync(entryFilePathTs).toString(); let changedFileTs = entryFileTs.concat( @@ -43,9 +46,13 @@ fs.writeFileSync( entryFilePathTs, changedFileTs, ); +console.log(`[build/web.js] Created init function for '${entryFilePathTs}'. Starting generatePackage().`); + // Generate `package.json`. const newPackage = generatePackage({ module: "index.js", types: "index.d.ts", + artifact, }); fs.writeFileSync(path.join(RELEASE_FOLDER, "package.json"), JSON.stringify(newPackage, null, 2)); +console.log(`[build/web.js] Finished processing entryFile '${entryFilePathTs}' for artifact '${artifact}'`); diff --git a/bindings/wasm/cypress/support/setup.js b/bindings/wasm/cypress/support/setup.js deleted file mode 100644 index 6f66f43751..0000000000 --- a/bindings/wasm/cypress/support/setup.js +++ /dev/null @@ -1,8 +0,0 @@ -import init from "@iota/sdk-wasm/web"; -import * as identity from "../../web"; - -export const setup = async (func) => { - await init("../../../node_modules/@iota/sdk-wasm/web/wasm/iota_sdk_wasm_bg.wasm") - .then(async () => await identity.init("../../../web/identity_wasm_bg.wasm")) - .then(func); -}; diff --git a/bindings/wasm/examples/README.md b/bindings/wasm/examples/README.md deleted file mode 100644 index 74914481b9..0000000000 --- a/bindings/wasm/examples/README.md +++ /dev/null @@ -1,67 +0,0 @@ -![banner](./../../../documentation/static/img/Banner/banner_identity.svg) - -## IOTA Identity Examples - -The following code examples demonstrate how to use the IOTA Identity Wasm bindings in JavaScript/TypeScript. - -The examples are written in TypeScript and can be run with Node.js. - -### Node.js - -Install the dependencies: - -```bash -npm install -``` - -Build the bindings: - -```bash -npm run build -``` - -Then, run an example using: - -```bash -npm run example:node -- -``` - -For instance, to run the `0_create_did` example execute: - -```bash -npm run example:node -- 0_create_did -``` - -## Basic Examples - -The following basic CRUD (Create, Read, Update, Delete) examples are available: - -| Name | Information | -| :-------------------------------------------------- | :----------------------------------------------------------------------------------- | -| [0_create_did](src/0_basic/0_create_did.ts) | Demonstrates how to create a DID Document and publish it in a new Alias Output. | -| [1_update_did](src/0_basic/1_update_did.ts) | Demonstrates how to update a DID document in an existing Alias Output. | -| [2_resolve_did](src/0_basic/2_resolve_did.ts) | Demonstrates how to resolve an existing DID in an Alias Output. | -| [3_deactivate_did](src/0_basic/3_deactivate_did.ts) | Demonstrates how to deactivate a DID in an Alias Output. | -| [4_delete_did](src/0_basic/4_delete_did.ts) | Demonstrates how to delete a DID in an Alias Output, reclaiming the storage deposit. | -| [5_create_vc](src/0_basic/5_create_vc.ts) | Demonstrates how to create and verify verifiable credentials. | -| [6_create_vp](src/0_basic/6_create_vp.ts) | Demonstrates how to create and verify verifiable presentations. | -| [7_revoke_vc](src/0_basic/7_revoke_vc.ts) | Demonstrates how to revoke a verifiable credential. | - -## Advanced Examples - -The following advanced examples are available: - -| Name | Information | -| :----------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- | -| [0_did_controls_did](src/1_advanced/0_did_controls_did.ts) | Demonstrates how an identity can control another identity. | -| [1_did_issues_nft](src/1_advanced/1_did_issues_nft.ts) | Demonstrates how an identity can issue and own NFTs, and how observers can verify the issuer of the NFT. | -| [2_nft_owns_did](src/1_advanced/2_nft_owns_did.ts) | Demonstrates how an identity can be owned by NFTs, and how observers can verify that relationship. | -| [3_did_issues_tokens](src/1_advanced/3_did_issues_tokens.ts) | Demonstrates how an identity can issue and control a Token Foundry and its tokens. | -| [4_custom_resolution](src/1_advanced/4_custom_resolution.ts) | Demonstrates how to set up a resolver using custom handlers. | -| [5_domain_linkage](src/1_advanced/5_domain_linkage.ts) | Demonstrates how to link a domain and a DID and verify the linkage. | -| [6_sd_jwt](src/1_advanced/6_sd_jwt.ts) | Demonstrates how to create a selective disclosure verifiable credential | -| [7_domain_linkage](src/1_advanced/7_status_list_2021.ts) | Demonstrates how to revoke a credential using `StatusList2021`. | - -## Browser - -While the examples should work in a browser environment, we do not provide browser examples yet. diff --git a/bindings/wasm/examples/src/0_basic/0_create_did.ts b/bindings/wasm/examples/src/0_basic/0_create_did.ts deleted file mode 100644 index aacc6d5338..0000000000 --- a/bindings/wasm/examples/src/0_basic/0_create_did.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { - IotaDID, - IotaDocument, - IotaIdentityClient, - JwkMemStore, - JwsAlgorithm, - KeyIdMemStore, - MethodScope, - Storage, -} from "@iota/identity-wasm/node"; -import { AliasOutput, Client, MnemonicSecretManager, SecretManager, Utils } from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, ensureAddressHasFunds } from "../util"; - -/** Demonstrate how to create a DID Document and publish it in a new Alias Output. */ -export async function createIdentity(): Promise<{ - didClient: IotaIdentityClient; - secretManager: SecretManager; - walletAddressBech32: string; - did: IotaDID; -}> { - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - const didClient = new IotaIdentityClient(client); - - // Get the Bech32 human-readable part (HRP) of the network. - const networkHrp: string = await didClient.getNetworkHrp(); - - const mnemonicSecretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Generate a random mnemonic for our wallet. - const secretManager: SecretManager = new SecretManager(mnemonicSecretManager); - - const walletAddressBech32 = (await secretManager.generateEd25519Addresses({ - accountIndex: 0, - range: { - start: 0, - end: 1, - }, - bech32Hrp: networkHrp, - }))[0]; - console.log("Wallet address Bech32:", walletAddressBech32); - - // Request funds for the wallet, if needed - only works on development networks. - await ensureAddressHasFunds(client, walletAddressBech32); - - // Create a new DID document with a placeholder DID. - // The DID will be derived from the Alias Id of the Alias Output after publishing. - const document = new IotaDocument(networkHrp); - const storage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore()); - - // Insert a new Ed25519 verification method in the DID document. - await document.generateMethod( - storage, - JwkMemStore.ed25519KeyType(), - JwsAlgorithm.EdDSA, - "#key-1", - MethodScope.VerificationMethod(), - ); - - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - const address = Utils.parseBech32Address(walletAddressBech32); - const aliasOutput: AliasOutput = await didClient.newDidOutput(address, document); - console.log("Alias Output:", JSON.stringify(aliasOutput, null, 2)); - - // Publish the Alias Output and get the published DID document. - const published = await didClient.publishDidOutput(mnemonicSecretManager, aliasOutput); - console.log("Published DID document:", JSON.stringify(published, null, 2)); - - return { - didClient, - secretManager, - walletAddressBech32, - did: published.id(), - }; -} diff --git a/bindings/wasm/examples/src/0_basic/1_update_did.ts b/bindings/wasm/examples/src/0_basic/1_update_did.ts deleted file mode 100644 index be635b0dbd..0000000000 --- a/bindings/wasm/examples/src/0_basic/1_update_did.ts +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { - IotaDocument, - IotaIdentityClient, - JwkMemStore, - JwsAlgorithm, - KeyIdMemStore, - MethodRelationship, - MethodScope, - Service, - Storage, - Timestamp, - VerificationMethod, -} from "@iota/identity-wasm/node"; -import { AliasOutput, Client, IRent, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; - -/** Demonstrates how to update a DID document in an existing Alias Output. */ -export async function updateIdentity() { - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - const didClient = new IotaIdentityClient(client); - - // Generate a random mnemonic for our wallet. - const secretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Creates a new wallet and identity (see "0_create_did" example). - const storage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore()); - let { document, fragment } = await createDid( - client, - secretManager, - storage, - ); - const did = document.id(); - - // Resolve the latest state of the document. - // Technically this is equivalent to the document above. - document = await didClient.resolveDid(did); - - // Insert a new Ed25519 verification method in the DID document. - await document.generateMethod( - storage, - JwkMemStore.ed25519KeyType(), - JwsAlgorithm.EdDSA, - "#key-2", - MethodScope.VerificationMethod(), - ); - - // Attach a new method relationship to the inserted method. - document.attachMethodRelationship(did.join("#key-2"), MethodRelationship.Authentication); - - // Add a new Service. - const service: Service = new Service({ - id: did.join("#linked-domain"), - type: "LinkedDomains", - serviceEndpoint: "https://iota.org/", - }); - document.insertService(service); - document.setMetadataUpdated(Timestamp.nowUTC()); - - // Remove a verification method. - let originalMethod = document.resolveMethod(fragment) as VerificationMethod; - await document.purgeMethod(storage, originalMethod?.id()); - - // Resolve the latest output and update it with the given document. - let aliasOutput: AliasOutput = await didClient.updateDidOutput(document); - - // Because the size of the DID document increased, we have to increase the allocated storage deposit. - // This increases the deposit amount to the new minimum. - const rentStructure: IRent = await didClient.getRentStructure(); - - aliasOutput = await client.buildAliasOutput({ - ...aliasOutput, - amount: Utils.computeStorageDeposit(aliasOutput, rentStructure), - aliasId: aliasOutput.getAliasId(), - unlockConditions: aliasOutput.getUnlockConditions(), - }); - - // Publish the output. - const updated: IotaDocument = await didClient.publishDidOutput(secretManager, aliasOutput); - console.log("Updated DID document:", JSON.stringify(updated, null, 2)); -} diff --git a/bindings/wasm/examples/src/0_basic/2_resolve_did.ts b/bindings/wasm/examples/src/0_basic/2_resolve_did.ts deleted file mode 100644 index ce8ea7c3e1..0000000000 --- a/bindings/wasm/examples/src/0_basic/2_resolve_did.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { - CoreDocument, - DIDJwk, - IotaDocument, - IotaIdentityClient, - IToCoreDocument, - JwkMemStore, - KeyIdMemStore, - Resolver, - Storage, -} from "@iota/identity-wasm/node"; -import { AliasOutput, Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; - -const DID_JWK: string = - "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9"; - -/** Demonstrates how to resolve an existing DID in an Alias Output. */ -export async function resolveIdentity() { - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - const didClient = new IotaIdentityClient(client); - - // Generate a random mnemonic for our wallet. - const secretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Creates a new wallet and identity (see "0_create_did" example). - const storage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore()); - let { document } = await createDid( - client, - secretManager, - storage, - ); - const did = document.id(); - - // Resolve the associated Alias Output and extract the DID document from it. - const resolved: IotaDocument = await didClient.resolveDid(did); - console.log("Resolved DID document:", JSON.stringify(resolved, null, 2)); - - // We can also resolve the Alias Output directly. - const aliasOutput: AliasOutput = await didClient.resolveDidOutput(did); - console.log("The Alias Output holds " + aliasOutput.getAmount() + " tokens"); - - // did:jwk can be resolved as well. - const handlers = new Map Promise>(); - handlers.set("jwk", didJwkHandler); - const resolver = new Resolver({ handlers }); - const did_jwk_resolved_doc = await resolver.resolve(DID_JWK); - console.log(`DID ${DID_JWK} resolves to:\n ${JSON.stringify(did_jwk_resolved_doc, null, 2)}`); -} - -const didJwkHandler = async (did: string) => { - let did_jwk = DIDJwk.parse(did); - return CoreDocument.expandDIDJwk(did_jwk); -}; diff --git a/bindings/wasm/examples/src/0_basic/3_deactivate_did.ts b/bindings/wasm/examples/src/0_basic/3_deactivate_did.ts deleted file mode 100644 index 93fef4926a..0000000000 --- a/bindings/wasm/examples/src/0_basic/3_deactivate_did.ts +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { IotaDocument, IotaIdentityClient, JwkMemStore, KeyIdMemStore, Storage } from "@iota/identity-wasm/node"; -import { AliasOutput, Client, IRent, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; - -/** Demonstrates how to deactivate a DID in an Alias Output. */ -export async function deactivateIdentity() { - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - const didClient = new IotaIdentityClient(client); - - // Generate a random mnemonic for our wallet. - const secretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Creates a new wallet and identity (see "0_create_did" example). - const storage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore()); - let { document } = await createDid( - client, - secretManager, - storage, - ); - const did = document.id(); - - // Resolve the latest state of the DID document, so we can reactivate it later. - // Technically this is equivalent to the document above. - document = await didClient.resolveDid(did); - - // Deactivate the DID by publishing an empty document. - // This process can be reversed since the Alias Output is not destroyed. - // Deactivation may only be performed by the state controller of the Alias Output. - let deactivatedOutput: AliasOutput = await didClient.deactivateDidOutput(did); - - // Optional: reduce and reclaim the storage deposit, sending the tokens to the state controller. - const rentStructure: IRent = await didClient.getRentStructure(); - - deactivatedOutput = await client.buildAliasOutput({ - ...deactivatedOutput, - amount: Utils.computeStorageDeposit(deactivatedOutput, rentStructure), - aliasId: deactivatedOutput.getAliasId(), - unlockConditions: deactivatedOutput.getUnlockConditions(), - }); - - // Publish the deactivated DID document. - await didClient.publishDidOutput(secretManager, deactivatedOutput); - - // Resolving a deactivated DID returns an empty DID document - // with its `deactivated` metadata field set to `true`. - let deactivated: IotaDocument = await didClient.resolveDid(did); - console.log("Deactivated DID document:", JSON.stringify(deactivated, null, 2)); - if (deactivated.metadataDeactivated() !== true) { - throw new Error("Failed to deactivate DID document"); - } - - // Re-activate the DID by publishing a valid DID document. - let reactivatedOutput: AliasOutput = await didClient.updateDidOutput(document); - - // Increase the storage deposit to the minimum again, if it was reclaimed during deactivation. - reactivatedOutput = await client.buildAliasOutput({ - ...reactivatedOutput, - amount: Utils.computeStorageDeposit(reactivatedOutput, rentStructure), - aliasId: reactivatedOutput.getAliasId(), - unlockConditions: reactivatedOutput.getUnlockConditions(), - }); - - await didClient.publishDidOutput(secretManager, reactivatedOutput); - - // Resolve the reactivated DID document. - let reactivated: IotaDocument = await didClient.resolveDid(did); - if (reactivated.metadataDeactivated() === true) { - throw new Error("Failed to reactivate DID document"); - } -} diff --git a/bindings/wasm/examples/src/0_basic/4_delete_did.ts b/bindings/wasm/examples/src/0_basic/4_delete_did.ts deleted file mode 100644 index c85bdb8e96..0000000000 --- a/bindings/wasm/examples/src/0_basic/4_delete_did.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { IotaIdentityClient, JwkMemStore, KeyIdMemStore, Storage } from "@iota/identity-wasm/node"; -import { Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; - -/** Demonstrates how to delete a DID in an Alias Output, reclaiming the storage deposit. */ -export async function deleteIdentity() { - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - const didClient = new IotaIdentityClient(client); - - // Generate a random mnemonic for our wallet. - const secretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Creates a new wallet and identity (see "0_create_did" example). - // const { address, document } = await createDid(client, secretManager); - const storage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore()); - let { address, document } = await createDid( - client, - secretManager, - storage, - ); - const did = document.id(); - - // Deletes the Alias Output and its contained DID Document, rendering the DID permanently destroyed. - // This operation is *not* reversible. - // Deletion can only be done by the governor of the Alias Output. - const destinationAddress = address; - await didClient.deleteDidOutput(secretManager, destinationAddress, did); - - // Attempting to resolve a deleted DID results in a `NotFound` error. - let deleted = false; - try { - await didClient.resolveDid(did); - } catch (err) { - deleted = true; - } - if (!deleted) { - throw new Error("failed to delete DID"); - } -} diff --git a/bindings/wasm/examples/src/1_advanced/0_did_controls_did.ts b/bindings/wasm/examples/src/1_advanced/0_did_controls_did.ts deleted file mode 100644 index 0eeb9f214e..0000000000 --- a/bindings/wasm/examples/src/1_advanced/0_did_controls_did.ts +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { - IotaDID, - IotaDocument, - IotaIdentityClient, - JwkMemStore, - JwsAlgorithm, - KeyIdMemStore, - MethodScope, - Storage, -} from "@iota/identity-wasm/node"; -import { - Address, - AliasAddress, - AliasOutput, - Client, - IRent, - IssuerFeature, - MnemonicSecretManager, - StateControllerAddressUnlockCondition, - UnlockConditionType, - Utils, -} from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; - -/** Demonstrates how an identity can control another identity. - -For this example, we consider the case where a parent company's DID controls the DID of a subsidiary. */ -export async function didControlsDid() { - // ======================================================== - // Create the company's and subsidiary's Alias Output DIDs. - // ======================================================== - - // Create a new Client to interact with the IOTA ledger. - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - const didClient = new IotaIdentityClient(client); - - // Generate a random mnemonic for our wallet. - const secretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Creates a new wallet and identity (see "0_create_did" example). - const storage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore()); - let { document } = await createDid( - client, - secretManager, - storage, - ); - let companyDid = document.id(); - - // Get the current byte costs. - const rentStructure: IRent = await didClient.getRentStructure(); - - // Get the Bech32 human-readable part (HRP) of the network. - const networkName: string = await didClient.getNetworkHrp(); - - // Construct a new DID document for the subsidiary. - var subsidiaryDocument: IotaDocument = new IotaDocument(networkName); - - // Create the Alias Address of the company. - const companyAliasAddress: Address = new AliasAddress(companyDid.toAliasId()); - - // Create a DID for the subsidiary that is controlled by the parent company's DID. - // This means the subsidiary's Alias Output can only be updated or destroyed by - // the state controller or governor of the company's Alias Output respectively. - var subsidiaryAlias: AliasOutput = await didClient.newDidOutput( - companyAliasAddress, - subsidiaryDocument, - rentStructure, - ); - - // Optionally, we can mark the company as the issuer of the subsidiary DID. - // This allows to verify trust relationships between DIDs, as a resolver can - // verify that the subsidiary DID was created by the parent company. - subsidiaryAlias = await client.buildAliasOutput({ - ...subsidiaryAlias, - immutableFeatures: [new IssuerFeature(companyAliasAddress)], - aliasId: subsidiaryAlias.getAliasId(), - unlockConditions: subsidiaryAlias.getUnlockConditions(), - }); - - // Adding the issuer feature means we have to recalculate the required storage deposit. - subsidiaryAlias = await client.buildAliasOutput({ - ...subsidiaryAlias, - amount: Utils.computeStorageDeposit(subsidiaryAlias, rentStructure), - aliasId: subsidiaryAlias.getAliasId(), - unlockConditions: subsidiaryAlias.getUnlockConditions(), - }); - - // Publish the subsidiary's DID. - subsidiaryDocument = await didClient.publishDidOutput(secretManager, subsidiaryAlias); - - // ===================================== - // Update the subsidiary's Alias Output. - // ===================================== - - // Add a verification method to the subsidiary. - // This only serves as an example for updating the subsidiary DID. - await subsidiaryDocument.generateMethod( - storage, - JwkMemStore.ed25519KeyType(), - JwsAlgorithm.EdDSA, - "#key-2", - MethodScope.VerificationMethod(), - ); - - // Update the subsidiary's Alias Output with the updated document - // and increase the storage deposit. - let subsidiaryAliasUpdate: AliasOutput = await didClient.updateDidOutput(subsidiaryDocument); - subsidiaryAliasUpdate = await client.buildAliasOutput({ - ...subsidiaryAliasUpdate, - amount: Utils.computeStorageDeposit(subsidiaryAliasUpdate, rentStructure), - aliasId: subsidiaryAliasUpdate.getAliasId(), - unlockConditions: subsidiaryAliasUpdate.getUnlockConditions(), - }); - - // Publish the updated subsidiary's DID. - // - // This works because `secret_manager` can unlock the company's Alias Output, - // which is required in order to update the subsidiary's Alias Output. - subsidiaryDocument = await didClient.publishDidOutput(secretManager, subsidiaryAliasUpdate); - - // =================================================================== - // Determine the controlling company's DID given the subsidiary's DID. - // =================================================================== - - // Resolve the subsidiary's Alias Output. - const subsidiaryOutput: AliasOutput = await didClient.resolveDidOutput(subsidiaryDocument.id()); - - // Extract the company's Alias Id from the state controller unlock condition. - // - // If instead we wanted to determine the original creator of the DID, - // we could inspect the issuer feature. This feature needs to be set when creating the DID. - // - // Non-null assertion is safe to use since every Alias Output has a state controller unlock condition. - // Cast to StateControllerAddressUnlockCondition is safe as we check the type in find. - const stateControllerUnlockCondition: StateControllerAddressUnlockCondition = subsidiaryOutput.getUnlockConditions() - .find( - unlockCondition => unlockCondition.getType() == UnlockConditionType.StateControllerAddress, - )! as StateControllerAddressUnlockCondition; - - // Cast to IAliasAddress is safe because we set an Alias Address earlier. - const companyAliasId: string = (stateControllerUnlockCondition.getAddress() as AliasAddress).getAliasId(); - - // Reconstruct the company's DID from the Alias Id and the network. - companyDid = IotaDID.fromAliasId(companyAliasId, networkName); - - // Resolve the company's DID document. - const companyDocument: IotaDocument = await didClient.resolveDid(companyDid); - - console.log("Company ", JSON.stringify(companyDocument, null, 2)); - console.log("Subsidiary ", JSON.stringify(subsidiaryDocument, null, 2)); -} diff --git a/bindings/wasm/examples/src/1_advanced/1_did_issues_nft.ts b/bindings/wasm/examples/src/1_advanced/1_did_issues_nft.ts deleted file mode 100644 index bf70963e5d..0000000000 --- a/bindings/wasm/examples/src/1_advanced/1_did_issues_nft.ts +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { - IotaDID, - IotaDocument, - IotaIdentityClient, - JwkMemStore, - KeyIdMemStore, - Storage, -} from "@iota/identity-wasm/node"; -import { - Address, - AddressType, - AddressUnlockCondition, - AliasAddress, - Client, - FeatureType, - IRent, - IssuerFeature, - MetadataFeature, - MnemonicSecretManager, - NftOutput, - Output, - OutputResponse, - OutputType, - Payload, - PayloadType, - RegularTransactionEssence, - TransactionEssenceType, - TransactionPayload, - utf8ToHex, - Utils, -} from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; - -/** Demonstrates how an identity can issue and own NFTs, -and how observers can verify the issuer of the NFT. - -For this example, we consider the case where a manufacturer issues -a digital product passport (DPP) as an NFT. */ -export async function didIssuesNft() { - // ============================================== - // Create the manufacturer's DID and the DPP NFT. - // ============================================== - - // Create a new Client to interact with the IOTA ledger. - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - const didClient = new IotaIdentityClient(client); - - // Generate a random mnemonic for our wallet. - const secretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Create a new DID for the manufacturer. (see "0_create_did" example). - const storage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore()); - let { document } = await createDid( - client, - secretManager, - storage, - ); - let manufacturerDid = document.id(); - - // Get the current byte costs. - const rentStructure: IRent = await didClient.getRentStructure(); - - // Get the Bech32 human-readable part (HRP) of the network. - const networkName: string = await didClient.getNetworkHrp(); - - // Create the Alias Address of the manufacturer. - const manufacturerAliasAddress: Address = new AliasAddress( - manufacturerDid.toAliasId(), - ); - - // Create a Digital Product Passport NFT issued by the manufacturer. - let productPassportNft: NftOutput = await client.buildNftOutput({ - nftId: "0x0000000000000000000000000000000000000000000000000000000000000000", - immutableFeatures: [ - // Set the manufacturer as the immutable issuer. - new IssuerFeature(manufacturerAliasAddress), - // A proper DPP would hold its metadata here. - new MetadataFeature(utf8ToHex("Digital Product Passport Metadata")), - ], - unlockConditions: [ - // The NFT will initially be owned by the manufacturer. - new AddressUnlockCondition(manufacturerAliasAddress), - ], - }); - - // Set the appropriate storage deposit. - productPassportNft = await client.buildNftOutput({ - ...productPassportNft, - amount: Utils.computeStorageDeposit(productPassportNft, rentStructure), - nftId: productPassportNft.getNftId(), - unlockConditions: productPassportNft.getUnlockConditions(), - }); - - // Publish the NFT. - const [blockId, block] = await client.buildAndPostBlock(secretManager, { outputs: [productPassportNft] }); - await client.retryUntilIncluded(blockId); - - // ======================================================== - // Resolve the Digital Product Passport NFT and its issuer. - // ======================================================== - - // Extract the identifier of the NFT from the published block. - // Non-null assertion is safe because we published a block with a payload. - let nftId: string = computeNftOutputId(block.payload!); - - // Fetch the NFT Output. - const nftOutputId: string = await client.nftOutputId(nftId); - const outputResponse: OutputResponse = await client.getOutput(nftOutputId); - const output: Output = outputResponse.output; - - // Extract the issuer of the NFT. - let manufacturerAliasId: string; - if (output.getType() === OutputType.Nft && (output as NftOutput).getImmutableFeatures()) { - // Cast is fine as we checked the type. - const nftOutput: NftOutput = output as NftOutput; - // Non-null assertion is fine as we checked the immutable features are present. - const issuerFeature: IssuerFeature = nftOutput.getImmutableFeatures()!.find(feature => - feature.getType() === FeatureType.Issuer - ) as IssuerFeature; - if (issuerFeature && issuerFeature.getIssuer().getType() === AddressType.Alias) { - manufacturerAliasId = (issuerFeature.getIssuer() as AliasAddress).getAliasId(); - } else { - throw new Error("expected to find issuer feature with an alias address"); - } - } else { - throw new Error("expected NFT output with immutable features"); - } - - // Reconstruct the manufacturer's DID from the Alias Id. - manufacturerDid = IotaDID.fromAliasId(manufacturerAliasId, networkName); - - // Resolve the issuer of the NFT. - const manufacturerDocument: IotaDocument = await didClient.resolveDid(manufacturerDid); - - console.log("The issuer of the Digital Product Passport NFT is:", JSON.stringify(manufacturerDocument, null, 2)); -} - -function computeNftOutputId(payload: Payload): string { - if (payload.getType() === PayloadType.Transaction) { - const transactionPayload: TransactionPayload = payload as TransactionPayload; - const transactionId = Utils.transactionId(transactionPayload); - - if (transactionPayload.essence.getType() === TransactionEssenceType.Regular) { - const regularTxPayload = transactionPayload.essence as RegularTransactionEssence; - const outputs = regularTxPayload.outputs; - for (const index in outputs) { - if (outputs[index].getType() === OutputType.Nft) { - const outputId: string = Utils.computeOutputId(transactionId, parseInt(index)); - return Utils.computeNftId(outputId); - } - } - throw new Error("no NFT output in transaction essence"); - } else { - throw new Error("expected transaction essence"); - } - } else { - throw new Error("expected transaction payload"); - } -} diff --git a/bindings/wasm/examples/src/1_advanced/2_nft_owns_did.ts b/bindings/wasm/examples/src/1_advanced/2_nft_owns_did.ts deleted file mode 100644 index 1c4aee6abc..0000000000 --- a/bindings/wasm/examples/src/1_advanced/2_nft_owns_did.ts +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { IotaDocument, IotaIdentityClient } from "@iota/identity-wasm/node"; -import { - Address, - AddressType, - AddressUnlockCondition, - AliasOutput, - Client, - IRent, - MnemonicSecretManager, - NftAddress, - NftOutput, - Output, - OutputResponse, - OutputType, - Payload, - PayloadType, - RegularTransactionEssence, - SecretManager, - StateControllerAddressUnlockCondition, - TransactionEssenceType, - TransactionPayload, - UnlockConditionType, - Utils, -} from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, ensureAddressHasFunds } from "../util"; - -/** Demonstrates how an identity can be owned by NFTs, -and how observers can verify that relationship. - -For this example, we consider the case where a car's NFT owns -the DID of the car, so that transferring the NFT also transfers DID ownership. */ -export async function nftOwnsDid() { - // ============================= - // Create the car's NFT and DID. - // ============================= - - // Create a new Client to interact with the IOTA ledger. - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - const didClient = new IotaIdentityClient(client); - - // Generate a random mnemonic for our wallet. - const secretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Get the current byte costs. - const rentStructure: IRent = await didClient.getRentStructure(); - - // Get the Bech32 human-readable part (HRP) of the network. - const networkName: string = await didClient.getNetworkHrp(); - - // Create a new address that will own the NFT. - const addressBech32 = (await new SecretManager(secretManager).generateEd25519Addresses({ - accountIndex: 0, - range: { - start: 0, - end: 1, - }, - bech32Hrp: networkName, - }))[0]; - const address = Utils.parseBech32Address(addressBech32); - - // Get funds for testing from the faucet. - await ensureAddressHasFunds(client, addressBech32); - - // Create the car NFT with an Ed25519 address as the unlock condition. - let carNft: NftOutput = await client.buildNftOutput({ - nftId: "0x0000000000000000000000000000000000000000000000000000000000000000", - unlockConditions: [ - // The NFT will initially be owned by the Ed25519 address. - new AddressUnlockCondition(address), - ], - }); - - // Set the appropriate storage deposit. - carNft = await client.buildNftOutput({ - ...carNft, - amount: Utils.computeStorageDeposit(carNft, rentStructure), - nftId: carNft.getNftId(), - unlockConditions: carNft.getUnlockConditions(), - }); - - // Publish the NFT. - const [blockId, block] = await client.buildAndPostBlock(secretManager, { outputs: [carNft] }); - await client.retryUntilIncluded(blockId); - - // Extract the identifier of the NFT from the published block. - // Non-null assertion is safe because we published a block with a payload. - var carNftId: string = computeNftOutputId(block.payload!); - - // Create the address of the NFT. - const nftAddress: Address = new NftAddress(carNftId); - - // Construct a DID document for the car. - var carDocument: IotaDocument = new IotaDocument(networkName); - - // Create a new DID for the car that is owned by the car NFT. - var carDidAliasOutput: AliasOutput = await didClient.newDidOutput(nftAddress, carDocument, rentStructure); - - // Publish the car DID. - carDocument = await didClient.publishDidOutput(secretManager, carDidAliasOutput); - - // ============================================ - // Determine the car's NFT given the car's DID. - // ============================================ - - // Resolve the Alias Output of the DID. - carDidAliasOutput = await didClient.resolveDidOutput(carDocument.id()); - - // Extract the NFT Id from the state controller unlock condition. - const stateControllerUnlockCondition: StateControllerAddressUnlockCondition = carDidAliasOutput - .getUnlockConditions() - .find(feature => - feature.getType() === UnlockConditionType.StateControllerAddress - ) as StateControllerAddressUnlockCondition; - if (stateControllerUnlockCondition.getAddress().getType() === AddressType.Nft) { - carNftId = (stateControllerUnlockCondition.getAddress() as NftAddress).getNftId(); - } else { - throw new Error("expected nft address unlock condition"); - } - - // Fetch the NFT Output of the car. - const nftOutputId: string = await client.nftOutputId(carNftId); - const outputResponse: OutputResponse = await client.getOutput(nftOutputId); - const output: Output = outputResponse.output; - - if (output.getType() === OutputType.Nft) { - carNft = output as NftOutput; - } else { - throw new Error("expected nft output type"); - } - - console.log("The car's DID is:", JSON.stringify(carDocument, null, 2)); - console.log("The car's NFT is:", JSON.stringify(carNft, null, 2)); -} - -function computeNftOutputId(payload: Payload): string { - if (payload.getType() === PayloadType.Transaction) { - const transactionPayload: TransactionPayload = payload as TransactionPayload; - const transactionId = Utils.transactionId(transactionPayload); - - if (transactionPayload.essence.getType() === TransactionEssenceType.Regular) { - const regularTxPayload = transactionPayload.essence as RegularTransactionEssence; - const outputs = regularTxPayload.outputs; - for (const index in outputs) { - if (outputs[index].getType() === OutputType.Nft) { - const outputId: string = Utils.computeOutputId(transactionId, parseInt(index)); - return Utils.computeNftId(outputId); - } - } - throw new Error("no NFT output in transaction essence"); - } else { - throw new Error("expected transaction essence"); - } - } else { - throw new Error("expected transaction payload"); - } -} diff --git a/bindings/wasm/examples/src/1_advanced/3_did_issues_tokens.ts b/bindings/wasm/examples/src/1_advanced/3_did_issues_tokens.ts deleted file mode 100644 index ae55925790..0000000000 --- a/bindings/wasm/examples/src/1_advanced/3_did_issues_tokens.ts +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { - IotaDID, - IotaDocument, - IotaIdentityClient, - JwkMemStore, - KeyIdMemStore, - Storage, -} from "@iota/identity-wasm/node"; -import { - AddressUnlockCondition, - AliasAddress, - AliasOutput, - BasicOutput, - Client, - ExpirationUnlockCondition, - FoundryOutput, - ImmutableAliasAddressUnlockCondition, - IRent, - MnemonicSecretManager, - Output, - OutputResponse, - OutputType, - SecretManager, - SimpleTokenScheme, - UnlockConditionType, - Utils, -} from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; - -/** Demonstrates how an identity can issue and control a Token Foundry and its tokens. - -For this example, we consider the case where an authority issues carbon credits -that can be used to pay for carbon emissions or traded on a marketplace. */ -export async function didIssuesTokens() { - // =========================================== - // Create the authority's DID and the foundry. - // =========================================== - - // Create a new Client to interact with the IOTA ledger. - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - const didClient = new IotaIdentityClient(client); - - // Generate a random mnemonic for our wallet. - const secretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Create a new DID for the authority. (see "0_create_did" example). - const storage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore()); - let { document } = await createDid( - client, - secretManager, - storage, - ); - let authorityDid = document.id(); - - // Get the current byte costs. - const rentStructure: IRent = await didClient.getRentStructure(); - - // Get the Bech32 human-readable part (HRP) of the network. - const networkName: string = await didClient.getNetworkHrp(); - - // We want to update the foundry counter of the authority's Alias Output, so we create an - // updated version of the output. We pass in the previous document, - // because we don't want to modify it in this update. - var authorityDocument: IotaDocument = await didClient.resolveDid(authorityDid); - var authorityAliasOutput: AliasOutput = await didClient.updateDidOutput(authorityDocument); - - // We will add one foundry to this Alias Output. - authorityAliasOutput = await client.buildAliasOutput({ - ...authorityAliasOutput, - foundryCounter: authorityAliasOutput.getFoundryCounter() + 1, - aliasId: authorityAliasOutput.getAliasId(), - unlockConditions: authorityAliasOutput.getUnlockConditions(), - }); - - // Create a token foundry that represents carbon credits. - const tokenScheme: SimpleTokenScheme = new SimpleTokenScheme( - BigInt(500_000), - BigInt(0), - BigInt(1_000_000), - ); - - // Create the identifier of the token, which is partially derived from the Alias Address. - const tokenId: string = Utils.computeTokenId(authorityDid.toAliasId(), 1, tokenScheme.getType()); - - // Create a token foundry that represents carbon credits. - var carbonCreditsFoundry: FoundryOutput = await client.buildFoundryOutput({ - tokenScheme, - serialNumber: 1, - // Initially, all carbon credits are owned by the foundry. - nativeTokens: [ - { - id: tokenId, - amount: BigInt(500_000), - }, - ], - // The authority is set as the immutable owner. - unlockConditions: [ - new ImmutableAliasAddressUnlockCondition( - new AliasAddress(authorityDid.toAliasId()), - ), - ], - }); - - // Set the appropriate storage deposit. - carbonCreditsFoundry = await client.buildFoundryOutput({ - ...carbonCreditsFoundry, - amount: Utils.computeStorageDeposit(carbonCreditsFoundry, rentStructure), - tokenScheme, - serialNumber: carbonCreditsFoundry.getSerialNumber(), - unlockConditions: carbonCreditsFoundry.getUnlockConditions(), - }); - - // Publish the foundry. - const [blockId, block] = await client.buildAndPostBlock(secretManager, { - outputs: [authorityAliasOutput, carbonCreditsFoundry], - }); - await client.retryUntilIncluded(blockId); - - // =================================== - // Resolve foundry and its issuer DID. - // =================================== - - // Get the latest output that contains the foundry. - const carbonCreditsFoundryId: string = tokenId; - const outputId: string = await client.foundryOutputId(carbonCreditsFoundryId); - const outputResponse: OutputResponse = await client.getOutput(outputId); - const output: Output = outputResponse.output; - - if (output.getType() === OutputType.Foundry) { - carbonCreditsFoundry = output as FoundryOutput; - } else { - throw new Error("expected foundry output"); - } - - // Get the Alias Id of the authority that issued the carbon credits foundry. - // Non-null assertion is safe as each founry output needs to have an immutable alias unlock condition. - const aliasUnlockCondition: ImmutableAliasAddressUnlockCondition = carbonCreditsFoundry.getUnlockConditions().find( - unlockCondition => unlockCondition.getType() === UnlockConditionType.ImmutableAliasAddress, - )! as ImmutableAliasAddressUnlockCondition; - - // We know the immutable alias unlock condition contains an alias address. - const authorityAliasId: string = (aliasUnlockCondition.getAddress() as AliasAddress).getAliasId(); - - // Reconstruct the DID of the authority. - authorityDid = IotaDID.fromAliasId(authorityAliasId, networkName); - - // Resolve the authority's DID document. - authorityDocument = await didClient.resolveDid(authorityDid); - - console.log("The authority's DID is:", JSON.stringify(authorityDocument, null, 2)); - - // ========================================================= - // Transfer 1000 carbon credits to the address of a company. - // ========================================================= - - // Create a new address that represents the company. - const companyAddressBech32: string = (await new SecretManager(secretManager).generateEd25519Addresses({ - accountIndex: 0, - range: { - start: 1, - end: 2, - }, - }))[0]; - const companyAddress = Utils.parseBech32Address(companyAddressBech32); - - // Create a timestamp 24 hours from now. - const tomorrow: number = Math.floor(Date.now() / 1000) + (60 * 60 * 24); - - // Create a basic output containing our carbon credits that we'll send to the company's address. - const basicOutput: BasicOutput = await client.buildBasicOutput({ - nativeTokens: [ - { - amount: BigInt(1000), - id: tokenId, - }, - ], - // Allow the company to claim the credits within 24 hours by using an expiration unlock condition. - unlockConditions: [ - new AddressUnlockCondition(companyAddress), - new ExpirationUnlockCondition( - new AliasAddress(authorityAliasId), - tomorrow, - ), - ], - }); - - // Reduce the carbon credits in the foundry by the amount that is sent to the company. - carbonCreditsFoundry = await client.buildFoundryOutput({ - ...carbonCreditsFoundry, - nativeTokens: [ - { - amount: BigInt(499_000), - id: tokenId, - }, - ], - tokenScheme, - serialNumber: carbonCreditsFoundry.getSerialNumber(), - unlockConditions: carbonCreditsFoundry.getUnlockConditions(), - }); - - // Publish the Basic Output and the updated foundry. - const [blockId2, block2] = await client.buildAndPostBlock(secretManager, { - outputs: [basicOutput, carbonCreditsFoundry], - }); - await client.retryUntilIncluded(blockId2); - - console.log("Sent carbon credits to", companyAddressBech32); -} diff --git a/bindings/wasm/examples/src/1_advanced/4_custom_resolution.ts b/bindings/wasm/examples/src/1_advanced/4_custom_resolution.ts deleted file mode 100644 index 4a89b51331..0000000000 --- a/bindings/wasm/examples/src/1_advanced/4_custom_resolution.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - CoreDocument, - IotaDocument, - IotaIdentityClient, - JwkMemStore, - KeyIdMemStore, - Resolver, - Storage, -} from "@iota/identity-wasm/node"; -import { Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; - -// Use this external package to avoid implementing the entire did:key method in this example. -// @ts-ignore -import { DidKeyDriver } from "@digitalcredentials/did-method-key"; -const didKeyDriver = new DidKeyDriver(); - -/** Demonstrates how to set up a resolver using custom handlers. - */ -export async function customResolution() { - // Set up a handler for resolving Ed25519 did:key - const keyHandler = async function(didKey: string): Promise { - let document = await didKeyDriver.get({ did: didKey }); - return CoreDocument.fromJSON(document); - }; - - // Create a new Client to interact with the IOTA ledger. - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - const didClient = new IotaIdentityClient(client); - - // Construct a Resolver capable of resolving the did:key and iota methods. - let handlerMap: Map Promise> = new Map(); - handlerMap.set("key", keyHandler); - - const resolver = new Resolver( - { - client: didClient, - handlers: handlerMap, - }, - ); - - // A valid Ed25519 did:key value taken from https://w3c-ccg.github.io/did-method-key/#example-1-a-simple-ed25519-did-key-value. - const didKey = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"; - - // Generate a random mnemonic for our wallet. - const secretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Creates a new wallet and identity for us to resolve (see "0_create_did" example). - const storage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore()); - let { document } = await createDid( - client, - secretManager, - storage, - ); - const did = document.id(); - - // Resolve didKey into a DID document. - const didKeyDoc = await resolver.resolve(didKey); - - // Resolve the DID we created on the IOTA ledger. - const didIotaDoc = await resolver.resolve(did.toString()); - - // Check that the types of the resolved documents match our expectations: - - if (didKeyDoc instanceof CoreDocument) { - console.log("Resolved DID Key document:", JSON.stringify(didKeyDoc, null, 2)); - } else { - throw new Error( - "the resolved document type should match the output type of keyHandler", - ); - } - - if (didIotaDoc instanceof IotaDocument) { - console.log("Resolved IOTA DID document:", JSON.stringify(didIotaDoc, null, 2)); - } else { - throw new Error( - "the resolved document type should match IotaDocument", - ); - } -} diff --git a/bindings/wasm/examples/src/tests/0_did_controls_did.ts b/bindings/wasm/examples/src/tests/0_did_controls_did.ts deleted file mode 100644 index 1ec2bb4012..0000000000 --- a/bindings/wasm/examples/src/tests/0_did_controls_did.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { didControlsDid } from "../1_advanced/0_did_controls_did"; - -// Only verifies that no uncaught exceptions are thrown, including syntax errors etc. -describe("Test node examples", function() { - it("Did controls Did", async () => { - await didControlsDid(); - }); -}); diff --git a/bindings/wasm/examples/src/tests/1_did_issues_nft.ts b/bindings/wasm/examples/src/tests/1_did_issues_nft.ts deleted file mode 100644 index 90660df3ae..0000000000 --- a/bindings/wasm/examples/src/tests/1_did_issues_nft.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { didIssuesNft } from "../1_advanced/1_did_issues_nft"; - -// Only verifies that no uncaught exceptions are thrown, including syntax errors etc. -describe("Test node examples", function() { - it("Did issues Nft", async () => { - await didIssuesNft(); - }); -}); diff --git a/bindings/wasm/examples/src/tests/2_nft_owns_did.ts b/bindings/wasm/examples/src/tests/2_nft_owns_did.ts deleted file mode 100644 index f148462eb3..0000000000 --- a/bindings/wasm/examples/src/tests/2_nft_owns_did.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { nftOwnsDid } from "../1_advanced/2_nft_owns_did"; - -// Only verifies that no uncaught exceptions are thrown, including syntax errors etc. -describe("Test node examples", function() { - it("Nft owns Did", async () => { - await nftOwnsDid(); - }); -}); diff --git a/bindings/wasm/examples/src/tests/3_did_issues_tokens.ts b/bindings/wasm/examples/src/tests/3_did_issues_tokens.ts deleted file mode 100644 index a3dc41e1d9..0000000000 --- a/bindings/wasm/examples/src/tests/3_did_issues_tokens.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { didIssuesTokens } from "../1_advanced/3_did_issues_tokens"; - -// Only verifies that no uncaught exceptions are thrown, including syntax errors etc. -describe("Test node examples", function() { - it("Did issues tokens", async () => { - await didIssuesTokens(); - }); -}); diff --git a/bindings/wasm/examples/src/tests/4_delete_did.ts b/bindings/wasm/examples/src/tests/4_delete_did.ts deleted file mode 100644 index e22e6e27e2..0000000000 --- a/bindings/wasm/examples/src/tests/4_delete_did.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { deleteIdentity } from "../0_basic/4_delete_did"; - -// Only verifies that no uncaught exceptions are thrown, including syntax errors etc. -describe("Test node examples", function() { - it("Delete Identity", async () => { - await deleteIdentity(); - }); -}); diff --git a/bindings/wasm/examples/src/util.ts b/bindings/wasm/examples/src/util.ts deleted file mode 100644 index 3fc2be116e..0000000000 --- a/bindings/wasm/examples/src/util.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { - IotaDocument, - IotaIdentityClient, - JwkMemStore, - JwsAlgorithm, - MethodScope, - Storage, -} from "@iota/identity-wasm/node"; -import { - type Address, - AliasOutput, - type Client, - IOutputsResponse, - SecretManager, - SecretManagerType, - Utils, -} from "@iota/sdk-wasm/node"; - -export const API_ENDPOINT = "http://localhost"; -export const FAUCET_ENDPOINT = "http://localhost/faucet/api/enqueue"; - -/** Creates a DID Document and publishes it in a new Alias Output. - -Its functionality is equivalent to the "create DID" example -and exists for convenient calling from the other examples. */ -export async function createDid(client: Client, secretManager: SecretManagerType, storage: Storage): Promise<{ - address: Address; - document: IotaDocument; - fragment: string; -}> { - const didClient = new IotaIdentityClient(client); - const networkHrp: string = await didClient.getNetworkHrp(); - - const secretManagerInstance = new SecretManager(secretManager); - const walletAddressBech32 = (await secretManagerInstance.generateEd25519Addresses({ - accountIndex: 0, - range: { - start: 0, - end: 1, - }, - bech32Hrp: networkHrp, - }))[0]; - - console.log("Wallet address Bech32:", walletAddressBech32); - - await ensureAddressHasFunds(client, walletAddressBech32); - - const address: Address = Utils.parseBech32Address(walletAddressBech32); - - // Create a new DID document with a placeholder DID. - // The DID will be derived from the Alias Id of the Alias Output after publishing. - const document = new IotaDocument(networkHrp); - - const fragment = await document.generateMethod( - storage, - JwkMemStore.ed25519KeyType(), - JwsAlgorithm.EdDSA, - "#jwk", - MethodScope.AssertionMethod(), - ); - - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - const aliasOutput: AliasOutput = await didClient.newDidOutput(address, document); - - // Publish the Alias Output and get the published DID document. - const published = await didClient.publishDidOutput(secretManager, aliasOutput); - - return { address, document: published, fragment }; -} - -/** Request funds from the faucet API, if needed, and wait for them to show in the wallet. */ -export async function ensureAddressHasFunds(client: Client, addressBech32: string) { - let balance = await getAddressBalance(client, addressBech32); - if (balance > BigInt(0)) { - return; - } - - await requestFundsFromFaucet(addressBech32); - - for (let i = 0; i < 9; i++) { - // Wait for the funds to reflect. - await new Promise(f => setTimeout(f, 5000)); - - let balance = await getAddressBalance(client, addressBech32); - if (balance > BigInt(0)) { - break; - } - } -} - -/** Returns the balance of the given Bech32-encoded address. */ -async function getAddressBalance(client: Client, addressBech32: string): Promise { - const outputIds: IOutputsResponse = await client.basicOutputIds([ - { address: addressBech32 }, - { hasExpiration: false }, - { hasTimelock: false }, - { hasStorageDepositReturn: false }, - ]); - const outputs = await client.getOutputs(outputIds.items); - - let totalAmount = BigInt(0); - for (const output of outputs) { - totalAmount += output.output.getAmount(); - } - - return totalAmount; -} - -/** Request tokens from the faucet API. */ -async function requestFundsFromFaucet(addressBech32: string) { - const requestObj = JSON.stringify({ address: addressBech32 }); - let errorMessage, data; - try { - const response = await fetch(FAUCET_ENDPOINT, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: requestObj, - }); - if (response.status === 202) { - errorMessage = "OK"; - } else if (response.status === 429) { - errorMessage = "too many requests, please try again later."; - } else { - data = await response.json(); - // @ts-ignore - errorMessage = data.error.message; - } - } catch (error) { - errorMessage = error; - } - - if (errorMessage != "OK") { - throw new Error(`failed to get funds from faucet: ${errorMessage}`); - } -} diff --git a/bindings/wasm/examples/tsconfig.web.json b/bindings/wasm/examples/tsconfig.web.json deleted file mode 100644 index 7cc4be3d7d..0000000000 --- a/bindings/wasm/examples/tsconfig.web.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES6", - "outDir": "./dist/web", - "baseUrl": "./", - "lib": ["ES6", "dom"], - "esModuleInterop": true, - "moduleResolution": "node", - "paths": { - "@iota/identity-wasm/node": ["../web"], - "@iota/sdk-wasm/node": ["@iota/sdk-wasm/web"] - } - }, - "exclude": ["tests"] -} diff --git a/bindings/wasm/.dockerignore b/bindings/wasm/identity_wasm/.dockerignore similarity index 100% rename from bindings/wasm/.dockerignore rename to bindings/wasm/identity_wasm/.dockerignore diff --git a/bindings/wasm/.github_changelog_generator b/bindings/wasm/identity_wasm/.github_changelog_generator similarity index 100% rename from bindings/wasm/.github_changelog_generator rename to bindings/wasm/identity_wasm/.github_changelog_generator diff --git a/bindings/wasm/CHANGELOG.md b/bindings/wasm/identity_wasm/CHANGELOG.md similarity index 100% rename from bindings/wasm/CHANGELOG.md rename to bindings/wasm/identity_wasm/CHANGELOG.md diff --git a/bindings/wasm/identity_wasm/Cargo.toml b/bindings/wasm/identity_wasm/Cargo.toml new file mode 100644 index 0000000000..606254fa2f --- /dev/null +++ b/bindings/wasm/identity_wasm/Cargo.toml @@ -0,0 +1,83 @@ +[package] +name = "identity_wasm" +version = "1.6.0-alpha.2" +authors = ["IOTA Stiftung"] +edition = "2021" +homepage = "https://www.iota.org" +keywords = ["iota", "tangle", "identity", "wasm"] +license = "Apache-2.0" +publish = false +readme = "README.md" +repository = "https://github.com/iotaledger/identity.rs" +resolver = "2" +description = "Web Assembly bindings for the identity-rs crate." + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +anyhow = "1.0.95" +async-trait = { version = "0.1", default-features = false } +console_error_panic_hook = { version = "0.1" } +fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "5f2c63266a065996d53f98156f0412782b468597", package = "fastcrypto" } +identity_ecdsa_verifier = { path = "../../../identity_ecdsa_verifier", default-features = false, features = ["es256", "es256k"] } +identity_eddsa_verifier = { path = "../../../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } +# Remove iota-sdk dependency while working on issue #1445 +iota-sdk = { version = "1.1.5", default-features = false, features = ["serde", "std"] } +js-sys = { version = "0.3.61" } +json-proof-token = "0.3.4" +proc_typescript = { version = "0.1.0", path = "./proc_typescript" } +secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", default-features = false, tag = "v0.2.0" } +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.6.5" +serde_json = { version = "1.0", default-features = false } +serde_repr = { version = "0.1", default-features = false } +# Want to use the nice API of tokio::sync::RwLock for now even though we can't use threads. +tokio = { version = "=1.39.2", default-features = false, features = ["sync"] } +tsify = "0.4.5" +wasm-bindgen = { version = "0.2.100", features = ["serde-serialize"] } +wasm-bindgen-futures = { version = "0.4", default-features = false } + +[dependencies.identity_iota] +path = "../../../identity_iota" +default-features = false +features = [ + "iota-client", + "revocation-bitmap", + "resolver", + "domain-linkage", + "sd-jwt", + "sd-jwt-vc", + "status-list-2021", + "jpt-bbs-plus", +] + +# dummy-client dependencies +[dependencies.iota_interaction_ts] +path = "../iota_interaction_ts" +optional = true + +[target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi")))'.dependencies] +getrandom = { version = "0.2", default-features = false, features = ["js"] } + +[profile.release] +opt-level = 's' +lto = true +# Enabling debug for profile.release may lead to more helpfull loged call stacks. +# TODO: Clarify if 'debug = true' facilitates error analysis via console logs. +# If not, remove the next line +# If yes, describe the helping effect in the comment above +# debug = true + +[lints.clippy] +# can be removed as soon as fix has been added to clippy +# see https://github.com/rust-lang/rust-clippy/issues/12377 +empty_docs = "allow" + +[lints.rust] +# required for current wasm_bindgen version +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(wasm_bindgen_unstable_test_coverage)'] } + +[features] +default = ["dummy-client"] +dummy-client = ["dep:iota_interaction_ts"] diff --git a/bindings/wasm/LICENSE b/bindings/wasm/identity_wasm/LICENSE similarity index 100% rename from bindings/wasm/LICENSE rename to bindings/wasm/identity_wasm/LICENSE diff --git a/bindings/wasm/identity_wasm/README.md b/bindings/wasm/identity_wasm/README.md new file mode 100644 index 0000000000..eb3cdac003 --- /dev/null +++ b/bindings/wasm/identity_wasm/README.md @@ -0,0 +1,311 @@ +# IOTA Identity WASM + +> This is the 1.0 version of the official WASM bindings for [IOTA Identity](https://github.com/iotaledger/identity.rs). + +## [API Reference](https://wiki.iota.org/identity.rs/libraries/wasm/api_reference) + +## [Examples](https://github.com/iotaledger/identity.rs/blob/main/bindings/wasm/identity_wasm/examples/README.md) + +## Install the library: + +Latest Release: this version matches the `main` branch of this repository. + +```bash +npm install @iota/identity-wasm +``` + +## Build + +Alternatively, you can build the bindings yourself if you have Rust installed. If not, refer +to [rustup.rs](https://rustup.rs) for the installation. + +Install [`wasm-bindgen-cli`](https://github.com/rustwasm/wasm-bindgen). A manual installation is required because we use +the [Weak References](https://rustwasm.github.io/wasm-bindgen/reference/weak-references.html) feature, which [ +`wasm-pack` does not expose](https://github.com/rustwasm/wasm-pack/issues/930). + +```bash +cargo install --force wasm-bindgen-cli +``` + +Then, install the necessary dependencies using: + +```bash +npm install +``` + +and build the bindings for `node.js` with + +```bash +npm run build:nodejs +``` + +or for the `web` with + +```bash +npm run build:web +``` + +## Minimum Requirements + +The minimum supported version for node is: `v16` + +## NodeJS Usage + +The following code creates a new IOTA DID Document suitable for publishing to a locally running private network. +See the [instructions](https://github.com/iotaledger/hornet/tree/develop/private_tangle) on running your own private +network. + + + + +```typescript +import { + EdCurve, + IdentityClientReadOnly, + IotaDocument, + Jwk, + JwkType, + MethodRelationship, + MethodScope, + Service, + VerificationMethod, +} from '@iota/identity-wasm/node'; +import { IotaClient } from "@iota/iota-sdk/client"; + +const EXAMPLE_JWK = new Jwk({ + kty: JwkType.Okp, + crv: EdCurve.Ed25519, + x: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", +}); + +// The endpoint of the IOTA node to use. +const NETWORK_URL = "https://api.testnet.iota.cafe"; + +/** Demonstrate how to create a DID Document. */ +export async function main() { + // Create a new client with the given network endpoint. + const iotaClient = new IotaClient({ url: NETWORK_URL }); + + const identityClient = await IdentityClientReadOnly.create(iotaClient); + + // Get the Bech32 human-readable part (HRP) of the network. + const networkHrp = identityClient.network(); + + // Create a new DID document with a placeholder DID. + // The DID will be derived from the ObjectId of the identity after publishing. + const document = new IotaDocument(networkHrp); + + // Insert a new Ed25519 verification method in the DID document. + const method = VerificationMethod.newFromJwk( + document.id(), + EXAMPLE_JWK, + "#key-1" + ); + document.insertMethod(method, MethodScope.VerificationMethod()); + + // Attach a new method relationship to the existing method. + document.attachMethodRelationship( + document.id().join("#key-1"), + MethodRelationship.Authentication + ); + + // Add a new Service. + const service = new Service({ + id: document.id().join("#linked-domain"), + type: "LinkedDomains", + serviceEndpoint: "https://iota.org/", + }); + document.insertService(service); + + console.log(`Created document `, JSON.stringify(document.toJSON(), null, 2)); +} + +main(); +``` + +which prints + +``` +Created document { + "doc": { + "id": "did:iota:testnet:0x0000000000000000000000000000000000000000000000000000000000000000", + "verificationMethod": [ + { + "id": "did:iota:testnet:0x0000000000000000000000000000000000000000000000000000000000000000#key-1", + "controller": "did:iota:testnet:0x0000000000000000000000000000000000000000000000000000000000000000", + "type": "JsonWebKey2020", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + } + } + ], + "authentication": [ + "did:iota:testnet:0x0000000000000000000000000000000000000000000000000000000000000000#key-1" + ], + "service": [ + { + "id": "did:iota:testnet:0x0000000000000000000000000000000000000000000000000000000000000000#linked-domain", + "type": "LinkedDomains", + "serviceEndpoint": "https://iota.org/" + } + ] + }, + "meta": { + "created": "2025-02-19T12:47:28Z", + "updated": "2025-02-19T12:47:28Z" + } +} +``` + +**NOTE: see +the [examples](https://github.com/iotaledger/identity.rs/blob/main/bindings/wasm/identity_wasm/examples/README.md) for +how to publish an IOTA DID Document.** + +## Web Setup + +The library loads the WASM file with an HTTP GET request, so the .wasm file must be copied to the root of the dist +folder. + +### Rollup + +- Install `rollup-plugin-copy`: + +```bash +$ npm install rollup-plugin-copy --save-dev +``` + +- Add the copy plugin usage to the `plugins` array under `rollup.config.js`: + +```js +// Include the copy plugin +import copy from "rollup-plugin-copy"; + +// Add the copy plugin to the `plugins` array of your rollup config: +copy({ + targets: [ + { + src: "node_modules/@iota/identity-wasm/web/identity_wasm_bg.wasm", + dest: "public", + rename: "identity_wasm_bg.wasm", + }, + ], +}); +``` + +### Webpack + +- Install `copy-webpack-plugin`: + +```bash +$ npm install copy-webpack-plugin --save-dev +``` + +```js +// Include the copy plugin +const CopyWebPlugin= require('copy-webpack-plugin'); + +// Add the copy plugin to the `plugins` array of your webpack config: + +new CopyWebPlugin({ + patterns: [ + { + from: 'node_modules/@iota/identity-wasm/web/identity_wasm_bg.wasm', + to: 'identity_wasm_bg.wasm' + } + ] +}), +``` + +### Web Usage + +```typescript +import * as identity from "@iota/identity-wasm/web"; + +// The endpoint of the IOTA node to use. +const API_ENDPOINT = "http://localhost"; + +const EXAMPLE_JWK = new identity.Jwk({ + kty: identity.JwkType.Okp, + crv: identity.EdCurve.Ed25519, + x: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", +}); + +/** Demonstrate how to create a DID Document. */ +async function createDocument() { + // Create a new client with the given network endpoint. + const iotaClient = new Client({ + primaryNode: API_ENDPOINT, + localPow: true, + }); + + const didClient = new identity.IotaIdentityClient(iotaClient); + + // Get the Bech32 human-readable part (HRP) of the network. + const networkHrp = await didClient.getNetworkHrp(); + + // Create a new DID document with a placeholder DID. + // The DID will be derived from the ObjectId of the identity after publishing. + const document = new identity.IotaDocument(networkHrp); + + // Insert a new Ed25519 verification method in the DID document. + let method = identity.VerificationMethod.newFromJwk( + document.id(), + EXAMPLE_JWK, + "#key-1" + ); + document.insertMethod(method, identity.MethodScope.VerificationMethod()); + + // Attach a new method relationship to the existing method. + document.attachMethodRelationship( + document.id().join("#key-1"), + identity.MethodRelationship.Authentication + ); + + // Add a new Service. + const service = new identity.Service({ + id: document.id().join("#linked-domain"), + type: "LinkedDomains", + serviceEndpoint: "https://iota.org/", + }); + document.insertService(service); + + console.log(`Created document `, JSON.stringify(document.toJSON(), null, 2)); +} + +identity.init() + .then(() => { + await createDocument(); + }); + +// or + +(async () => { + await identity.init(); + + await createDocument(); +})(); + +// Default path is "identity_wasm_bg.wasm", but you can override it like this +await identity.init("./static/identity_wasm_bg.wasm"); +``` + +Calling `identity.init().then()` or `await identity.init()` is required to load the Wasm file from the server +if not available, because of that it will only be slow for the first time. + +**NOTE: see +the [examples](https://github.com/iotaledger/identity.rs/blob/main/bindings/wasm/identity_wasm/examples/README.md) for +how to publish an IOTA DID Document.** + +## Examples in the Wild + +You may find it useful to see how the WASM bindings are being used in existing applications: + +- [Zebra IOTA Edge SDK](https://github.com/ZebraDevs/Zebra-Iota-Edge-SDK) (mobile apps using Capacitor.js + Svelte) diff --git a/bindings/wasm/cypress.config.ts b/bindings/wasm/identity_wasm/cypress.config.ts similarity index 100% rename from bindings/wasm/cypress.config.ts rename to bindings/wasm/identity_wasm/cypress.config.ts diff --git a/bindings/wasm/cypress/Dockerfile b/bindings/wasm/identity_wasm/cypress/Dockerfile similarity index 100% rename from bindings/wasm/cypress/Dockerfile rename to bindings/wasm/identity_wasm/cypress/Dockerfile diff --git a/bindings/wasm/cypress/e2e/0_basic/0_create_did.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/0_basic/0_create_did.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/0_basic/0_create_did.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/0_basic/0_create_did.cy.js diff --git a/bindings/wasm/cypress/e2e/0_basic/1_update_did.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/0_basic/1_update_did.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/0_basic/1_update_did.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/0_basic/1_update_did.cy.js diff --git a/bindings/wasm/cypress/e2e/0_basic/2_resolve_did.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/0_basic/2_resolve_did.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/0_basic/2_resolve_did.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/0_basic/2_resolve_did.cy.js diff --git a/bindings/wasm/cypress/e2e/0_basic/3_deactivate_did.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/0_basic/3_deactivate_did.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/0_basic/3_deactivate_did.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/0_basic/3_deactivate_did.cy.js diff --git a/bindings/wasm/cypress/e2e/0_basic/4_delete_did.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/0_basic/4_delete_did.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/0_basic/4_delete_did.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/0_basic/4_delete_did.cy.js diff --git a/bindings/wasm/cypress/e2e/0_basic/5_create_vc.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/0_basic/5_create_vc.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/0_basic/5_create_vc.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/0_basic/5_create_vc.cy.js diff --git a/bindings/wasm/cypress/e2e/0_basic/6_create_vp.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/0_basic/6_create_vp.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/0_basic/6_create_vp.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/0_basic/6_create_vp.cy.js diff --git a/bindings/wasm/cypress/e2e/0_basic/7_revoke_vc.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/0_basic/7_revoke_vc.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/0_basic/7_revoke_vc.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/0_basic/7_revoke_vc.cy.js diff --git a/bindings/wasm/cypress/e2e/1_advanced/0_did_controls_did.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/1_advanced/0_did_controls_did.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/1_advanced/0_did_controls_did.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/1_advanced/0_did_controls_did.cy.js diff --git a/bindings/wasm/cypress/e2e/1_advanced/1_did_issues_nft.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/1_advanced/1_did_issues_nft.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/1_advanced/1_did_issues_nft.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/1_advanced/1_did_issues_nft.cy.js diff --git a/bindings/wasm/cypress/e2e/1_advanced/2_nft_owns_did.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/1_advanced/2_nft_owns_did.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/1_advanced/2_nft_owns_did.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/1_advanced/2_nft_owns_did.cy.js diff --git a/bindings/wasm/cypress/e2e/1_advanced/3_did_issues_tokens.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/1_advanced/3_did_issues_tokens.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/1_advanced/3_did_issues_tokens.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/1_advanced/3_did_issues_tokens.cy.js diff --git a/bindings/wasm/cypress/e2e/1_advanced/4_custom_resolution.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/1_advanced/4_custom_resolution.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/1_advanced/4_custom_resolution.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/1_advanced/4_custom_resolution.cy.js diff --git a/bindings/wasm/cypress/e2e/1_advanced/5_domain_linkage.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/1_advanced/5_domain_linkage.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/1_advanced/5_domain_linkage.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/1_advanced/5_domain_linkage.cy.js diff --git a/bindings/wasm/cypress/e2e/1_advanced/6_sd_jwt.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/1_advanced/6_sd_jwt.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/1_advanced/6_sd_jwt.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/1_advanced/6_sd_jwt.cy.js diff --git a/bindings/wasm/cypress/e2e/1_advanced/7_status_list_2021.cy.js b/bindings/wasm/identity_wasm/cypress/e2e/1_advanced/7_status_list_2021.cy.js similarity index 100% rename from bindings/wasm/cypress/e2e/1_advanced/7_status_list_2021.cy.js rename to bindings/wasm/identity_wasm/cypress/e2e/1_advanced/7_status_list_2021.cy.js diff --git a/bindings/wasm/cypress/fixtures/.gitkeep b/bindings/wasm/identity_wasm/cypress/fixtures/.gitkeep similarity index 100% rename from bindings/wasm/cypress/fixtures/.gitkeep rename to bindings/wasm/identity_wasm/cypress/fixtures/.gitkeep diff --git a/bindings/wasm/cypress/plugins/.gitkeep b/bindings/wasm/identity_wasm/cypress/plugins/.gitkeep similarity index 100% rename from bindings/wasm/cypress/plugins/.gitkeep rename to bindings/wasm/identity_wasm/cypress/plugins/.gitkeep diff --git a/bindings/wasm/cypress/support/.gitkeep b/bindings/wasm/identity_wasm/cypress/support/.gitkeep similarity index 100% rename from bindings/wasm/cypress/support/.gitkeep rename to bindings/wasm/identity_wasm/cypress/support/.gitkeep diff --git a/bindings/wasm/identity_wasm/cypress/support/setup.js b/bindings/wasm/identity_wasm/cypress/support/setup.js new file mode 100644 index 0000000000..6a3c5722b1 --- /dev/null +++ b/bindings/wasm/identity_wasm/cypress/support/setup.js @@ -0,0 +1,6 @@ +import * as identity from "../../web"; + +export const setup = async (func) => { + await identity.init("../../../web/identity_wasm_bg.wasm") + .then(func); +}; diff --git a/bindings/wasm/identity_wasm/examples/README.md b/bindings/wasm/identity_wasm/examples/README.md new file mode 100644 index 0000000000..bacca08e28 --- /dev/null +++ b/bindings/wasm/identity_wasm/examples/README.md @@ -0,0 +1,89 @@ +![banner](./../../../documentation/static/img/Banner/banner_identity.svg) + +## IOTA Identity Examples + +The following code examples demonstrate how to use the IOTA Identity Wasm bindings in JavaScript/TypeScript. + +The examples are written in TypeScript and can be run with Node.js. + +### Prerequisites + +Examples can be run against +- a local IOTA node +- or an existing network, e.g. the IOTA testnet + +When setting up the local node, you'll also need to publish an identity package as described in +[Getting Started](../../../../README.md#getting-started). +You'll also need to provide an environment variable `IOTA_IDENTITY_PKG_ID` set to the package-id of your locally deployed +identity package, to be able to run the examples against the local node. + +In case of running the examples against an existing network, this network needs to have a faucet to fund your accounts (the IOTA testnet (`https://api.testnet.iota.cafe`) supports this), and you need to specify this via `NETWORK_URL`. + +The examples require you to have the node you want to use in the iota clients "envs" (`iota client env`) configuration. If this node is configured as `localnet`, you don't have to provide it when running the examples, if not, provide its name as `NETWORK_NAME_FAUCET`. The table below assumes - in case you're running a local node - you have it configured as `localnet` in your IOTA clients "env" setting. + +### Environment variables + +Summarizing the last point, you'll need one or more of the following environment variables: + +| Name | Required for local node | Required for testnet | Required for other node | Comment | +| -------------------- | :---------------------: | :------------------: | :---------------------: | :------------------: | +| IOTA_IDENTITY_PKG_ID | x | | x | | +| NETWORK_URL | | x | x | | +| NETWORK_NAME_FAUCET | | x | x | see assumption above | + +### Node.js + +Install the dependencies: + +```bash +npm install +``` + +Build the bindings: + +```bash +npm run build +``` + +Then, run an example using the following command, environment variables depend on your setup, see [Environment variables](#environment-variables). + +```bash +IOTA_IDENTITY_PKG_ID=0x7a67dd504eb1291958495c71a07d20985951648dd5ebf01ac921a50257346818 npm run example:node -- +``` + +For instance, to run the `0_create_did` example with the following (environment variables depend on you setup, see [Environment variables](#environment-variables)): + +```bash +IOTA_IDENTITY_PKG_ID=0x7a67dd504eb1291958495c71a07d20985951648dd5ebf01ac921a50257346818 npm run example:node -- 0_create_did +``` + +## Basic Examples + +The following basic CRUD (Create, Read, Update, Delete) examples are available: + +| Name | Information | +| :-------------------------------------------------- | :-------------------------------------------------------------------------- | +| [0_create_did](src/0_basic/0_create_did.ts) | Demonstrates how to create a DID Document and publish it in a new identity. | +| [1_update_did](src/0_basic/1_update_did.ts) | Demonstrates how to update a DID document in an existing identity. | +| [2_resolve_did](src/0_basic/2_resolve_did.ts) | Demonstrates how to resolve an existing DID in an identity. | +| [3_deactivate_did](src/0_basic/3_deactivate_did.ts) | Demonstrates how to deactivate a DID in an identity. | +| [5_create_vc](src/0_basic/5_create_vc.ts) | Demonstrates how to create and verify verifiable credentials. | +| [6_create_vp](src/0_basic/6_create_vp.ts) | Demonstrates how to create and verify verifiable presentations. | +| [7_revoke_vc](src/0_basic/7_revoke_vc.ts) | Demonstrates how to revoke a verifiable credential. | + +## Advanced Examples + +The following advanced examples are available: + +| Name | Information | +|:-------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------| +| [4_custom_resolution](src/1_advanced/4_custom_resolution.ts) | Demonstrates how to set up a resolver using custom handlers. | +| [5_domain_linkage](src/1_advanced/5_domain_linkage.ts) | Demonstrates how to link a domain and a DID and verify the linkage. | +| [6_sd_jwt](src/1_advanced/6_sd_jwt.ts) | Demonstrates how to create a selective disclosure verifiable credential | +| [7_status_list_2021](src/1_advanced/7_status_list_2021.ts) | Demonstrates how to revoke a credential using `StatusList2021`. | +| [8_zkp](./1_advanced/8_zkp.ts) | Demonstrates how to create an Anonymous Credential with BBS+. | +| [9_zkp_revocation](./1_advanced/9_zkp_revocation.ts) | Demonstrates how to revoke a credential. | + +## Browser + +While the examples should work in a browser environment, we do not provide browser examples yet. diff --git a/bindings/wasm/identity_wasm/examples/src/0_basic/0_create_did.ts b/bindings/wasm/identity_wasm/examples/src/0_basic/0_create_did.ts new file mode 100644 index 0000000000..3072ed8447 --- /dev/null +++ b/bindings/wasm/identity_wasm/examples/src/0_basic/0_create_did.ts @@ -0,0 +1,33 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { IotaDID } from "@iota/identity-wasm/node"; +import { IotaClient } from "@iota/iota-sdk/client"; +import { createDocumentForNetwork, getFundedClient, getMemstorage, NETWORK_URL } from "../util"; + +/** Demonstrate how to create a DID Document and publish it. */ +export async function createIdentity(): Promise { + // create new client to connect to IOTA network + const iotaClient = new IotaClient({ url: NETWORK_URL }); + const network = await iotaClient.getChainIdentifier(); + + // create new client that offers identity related functions + const storage = getMemstorage(); + const identityClient = await getFundedClient(storage); + + // create new unpublished document + const [unpublished] = await createDocumentForNetwork(storage, network); + console.log(`Unpublished DID document: ${JSON.stringify(unpublished, null, 2)}`); + let did: IotaDID; + + console.log("Creating new identity"); + const { output: identity } = await identityClient + .createIdentity(unpublished) + .finish() + .execute(identityClient); + did = identity.didDocument().id(); + + // check if we can resolve it via client + const resolved = await identityClient.resolveDid(did); + console.log(`Resolved DID document: ${JSON.stringify(resolved, null, 2)}`); +} diff --git a/bindings/wasm/identity_wasm/examples/src/0_basic/1_update_did.ts b/bindings/wasm/identity_wasm/examples/src/0_basic/1_update_did.ts new file mode 100644 index 0000000000..1780befbd0 --- /dev/null +++ b/bindings/wasm/identity_wasm/examples/src/0_basic/1_update_did.ts @@ -0,0 +1,70 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { + JwkMemStore, + JwsAlgorithm, + MethodRelationship, + MethodScope, + Service, + VerificationMethod, +} from "@iota/identity-wasm/node"; +import { IotaClient } from "@iota/iota-sdk/client"; +import { createDocumentForNetwork, getFundedClient, getMemstorage, NETWORK_URL, TEST_GAS_BUDGET } from "../util"; + +/** Demonstrates how to update a DID document in an existing identity. */ +export async function updateIdentity() { + // create new clients and create new account + const iotaClient = new IotaClient({ url: NETWORK_URL }); + const network = await iotaClient.getChainIdentifier(); + const storage = getMemstorage(); + const identityClient = await getFundedClient(storage); + const [unpublished, vmFragment1] = await createDocumentForNetwork(storage, network); + + // create new identity for this account and publish document for it + const { output: identity } = await identityClient + .createIdentity(unpublished) + .finish() + .execute(identityClient); + const did = identity.didDocument().id(); + + // Resolve the latest state of the document. + // Technically this is equivalent to the document above. + const resolved = await identityClient.resolveDid(did); + + // Insert a new Ed25519 verification method in the DID document. + await resolved.generateMethod( + storage, + JwkMemStore.ed25519KeyType(), + JwsAlgorithm.EdDSA, + "#key-2", + MethodScope.VerificationMethod(), + ); + + // Attach a new method relationship to the inserted method. + resolved.attachMethodRelationship(did.join("#key-2"), MethodRelationship.Authentication); + + // Remove a verification method. + let originalMethod = resolved.resolveMethod(vmFragment1) as VerificationMethod; + await resolved.purgeMethod(storage, originalMethod?.id()); + + // Add a new Service. + const service: Service = new Service({ + id: did.join("#linked-domain"), + type: "LinkedDomains", + serviceEndpoint: "https://iota.org/", + }); + resolved.insertService(service); + + let maybePendingProposal = await identity + .updateDidDocument(resolved.clone()) + .withGasBudget(TEST_GAS_BUDGET) + .execute(identityClient) + .then(result => result.output); + + console.assert(maybePendingProposal === undefined, "the proposal should have been executed right away!"); + + // and resolve again to make sure we're looking at the onchain information + const resolvedAgain = await identityClient.resolveDid(did); + console.log(`Updated DID document result: ${JSON.stringify(resolvedAgain, null, 2)}`); +} diff --git a/bindings/wasm/identity_wasm/examples/src/0_basic/2_resolve_did.ts b/bindings/wasm/identity_wasm/examples/src/0_basic/2_resolve_did.ts new file mode 100644 index 0000000000..fbffebbb5f --- /dev/null +++ b/bindings/wasm/identity_wasm/examples/src/0_basic/2_resolve_did.ts @@ -0,0 +1,79 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { + CoreDocument, + DIDJwk, + IdentityClientReadOnly, + IotaDocument, + IToCoreDocument, + Resolver, +} from "@iota/identity-wasm/node"; +import { IotaClient } from "@iota/iota-sdk/client"; +import { createDocumentForNetwork, getFundedClient, getMemstorage, IOTA_IDENTITY_PKG_ID, NETWORK_URL } from "../util"; + +const DID_JWK: string = + "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9"; + +/** Demonstrates how to resolve an existing DID in an identity. */ +export async function resolveIdentity() { + // create new clients and create new account + const iotaClient = new IotaClient({ url: NETWORK_URL }); + const network = await iotaClient.getChainIdentifier(); + const storage = getMemstorage(); + const identityClient = await getFundedClient(storage); + const [unpublished] = await createDocumentForNetwork(storage, network); + + // create new identity for this account and publish document for it + const { output: identity } = await identityClient + .createIdentity(unpublished) + .finish() + .execute(identityClient); + const did = identity.didDocument().id(); + + // Resolve the associated identity and extract the DID document from it. + const resolved = await identityClient.resolveDid(did); + console.log("Resolved DID document:", JSON.stringify(resolved, null, 2)); + + // We can resolve the Object ID directly + const resolvedIdentity = await identityClient.getIdentity(identity.id()); + console.dir(resolvedIdentity); + console.log(`Identity client resolved identity has object ID ${resolvedIdentity.toFullFledged()?.id()}`); + + // Or we can resolve it via the `Resolver` api: + + // While at it, define a custom resolver for jwk DIDs as well. + const handlers = new Map Promise>(); + handlers.set("jwk", didJwkHandler); + + // Create new `Resolver` instance with the client with write capabilities we already have at hand + const resolver = new Resolver({ client: identityClient, handlers }); + + // and resolve identity DID with it. + const resolverResolved = await resolver.resolve(did.toString()); + console.log(`resolverResolved ${did.toString()} resolves to:\n ${JSON.stringify(resolverResolved, null, 2)}`); + + // We can also resolve via the custom resolver defined before: + const did_jwk_resolved_doc = await resolver.resolve(DID_JWK); + console.log(`DID ${DID_JWK} resolves to:\n ${JSON.stringify(did_jwk_resolved_doc, null, 2)}`); + + // We can also create a resolver with a read-only client + const identityClientReadOnly = await IdentityClientReadOnly.createWithPkgId(iotaClient, IOTA_IDENTITY_PKG_ID); + // In this case we will only be resolving `IotaDocument` instances, as we don't pass a `handler` configuration. + // Therefore we can limit the type of the resolved documents to `IotaDocument` when creating the new resolver as well. + const resolverWithReadOnlyClient = new Resolver({ client: identityClientReadOnly }); + + // And resolve as before. + const resolvedViaReadOnly = await resolverWithReadOnlyClient.resolve(did.toString()); + console.log( + `resolverWithReadOnlyClient ${did.toString()} resolves to:\n ${JSON.stringify(resolvedViaReadOnly, null, 2)}`, + ); + + // As our `Resolver` instance will only return `IotaDocument` instances, we can directly work with them, e.g. + console.log(`${did.toString()}'s metadata is ${resolvedViaReadOnly.metadata()}`); +} + +const didJwkHandler = async (did: string) => { + let did_jwk = DIDJwk.parse(did); + return CoreDocument.expandDIDJwk(did_jwk); +}; diff --git a/bindings/wasm/identity_wasm/examples/src/0_basic/3_deactivate_did.ts b/bindings/wasm/identity_wasm/examples/src/0_basic/3_deactivate_did.ts new file mode 100644 index 0000000000..f31fb460f3 --- /dev/null +++ b/bindings/wasm/identity_wasm/examples/src/0_basic/3_deactivate_did.ts @@ -0,0 +1,55 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { IotaClient } from "@iota/iota-sdk/client"; +import { createDocumentForNetwork, getFundedClient, getMemstorage, NETWORK_URL, TEST_GAS_BUDGET } from "../util"; + +/** Demonstrates how to deactivate a DID of an identity. */ +export async function deactivateIdentity() { + // create new clients and create new account + const iotaClient = new IotaClient({ url: NETWORK_URL }); + const network = await iotaClient.getChainIdentifier(); + const storage = getMemstorage(); + const identityClient = await getFundedClient(storage); + const [unpublished] = await createDocumentForNetwork(storage, network); + + // create new identity for this account and publish document for it + const { output: identity } = await identityClient + .createIdentity(unpublished) + .finish() + .execute(identityClient); + const did = identity.didDocument().id(); + + // Resolve the latest state of the document. + // Technically this is equivalent to the document above. + const resolved = await identityClient.resolveDid(did); + console.log("Resolved DID document:", JSON.stringify(resolved, null, 2)); + + // Deactivate the DID. + await identity + .deactivateDid() + .withGasBudget(TEST_GAS_BUDGET) + .execute(identityClient); + + // Resolving a deactivated DID returns an empty DID document + // with its `deactivated` metadata field set to `true`. + let deactivated = await identityClient.resolveDid(did); + console.log("Deactivated DID document:", JSON.stringify(deactivated, null, 2)); + if (deactivated.metadataDeactivated() !== true) { + throw new Error("Failed to deactivate DID document"); + } + + // Re-activate the DID by publishing a valid DID document. + console.log("Publishing this:", JSON.stringify(resolved, null, 2)); + await identity + .updateDidDocument(resolved) + .withGasBudget(TEST_GAS_BUDGET) + .execute(identityClient); + + // Resolve the reactivated DID document. + let resolvedReactivated = await identityClient.resolveDid(did); + console.log("Reactivated DID document:", JSON.stringify(resolvedReactivated, null, 2)); + if (resolvedReactivated.metadataDeactivated() === true) { + throw new Error("Failed to reactivate DID document"); + } +} diff --git a/bindings/wasm/examples/src/0_basic/5_create_vc.ts b/bindings/wasm/identity_wasm/examples/src/0_basic/5_create_vc.ts similarity index 67% rename from bindings/wasm/examples/src/0_basic/5_create_vc.ts rename to bindings/wasm/identity_wasm/examples/src/0_basic/5_create_vc.ts index 1b711e51ad..681def72e3 100644 --- a/bindings/wasm/examples/src/0_basic/5_create_vc.ts +++ b/bindings/wasm/identity_wasm/examples/src/0_basic/5_create_vc.ts @@ -5,15 +5,12 @@ import { Credential, EdDSAJwsVerifier, FailFast, - JwkMemStore, JwsSignatureOptions, JwtCredentialValidationOptions, JwtCredentialValidator, - KeyIdMemStore, - Storage, } from "@iota/identity-wasm/node"; -import { Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; +import { IotaClient } from "@iota/iota-sdk/client"; +import { createDocumentForNetwork, getFundedClient, getMemstorage, NETWORK_URL } from "../util"; /** * This example shows how to create a Verifiable Credential and validate it. @@ -22,31 +19,29 @@ import { API_ENDPOINT, createDid } from "../util"; * This Verifiable Credential can be verified by anyone, allowing Alice to take control of it and share it with whomever they please. */ export async function createVC() { - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - - // Generate a random mnemonic for our wallet. - const secretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; + // create new client to connect to IOTA network + const iotaClient = new IotaClient({ url: NETWORK_URL }); + const network = await iotaClient.getChainIdentifier(); - // Create an identity for the issuer with one verification method `key-1`. - const issuerStorage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore()); - let { document: issuerDocument, fragment: issuerFragment } = await createDid( - client, - secretManager, - issuerStorage, - ); + // Create an identity for the issuer with one verification method `key-1`, and publish DID document for it. + const issuerStorage = getMemstorage(); + const issuerClient = await getFundedClient(issuerStorage); + const [unpublishedIssuerDocument, issuerFragment] = await createDocumentForNetwork(issuerStorage, network); + const { output: issuerIdentity } = await issuerClient + .createIdentity(unpublishedIssuerDocument) + .finish() + .execute(issuerClient); + const issuerDocument = issuerIdentity.didDocument(); - // Create an identity for the holder, in this case also the subject. - const aliceStorage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore()); - let { document: aliceDocument } = await createDid( - client, - secretManager, - aliceStorage, - ); + // Create an identity for the holder, and publish DID document for it, in this case also the subject. + const aliceStorage = getMemstorage(); + const aliceClient = await getFundedClient(aliceStorage); + const [unpublishedAliceDocument] = await createDocumentForNetwork(aliceStorage, network); + const { output: aliceIdentity } = await aliceClient + .createIdentity(unpublishedAliceDocument) + .finish() + .execute(aliceClient); + const aliceDocument = aliceIdentity.didDocument(); // Create a credential subject indicating the degree earned by Alice, linked to their DID. const subject = { diff --git a/bindings/wasm/examples/src/0_basic/6_create_vp.ts b/bindings/wasm/identity_wasm/examples/src/0_basic/6_create_vp.ts similarity index 82% rename from bindings/wasm/examples/src/0_basic/6_create_vp.ts rename to bindings/wasm/identity_wasm/examples/src/0_basic/6_create_vp.ts index f78f19dd1b..66fdcd73a2 100644 --- a/bindings/wasm/examples/src/0_basic/6_create_vp.ts +++ b/bindings/wasm/identity_wasm/examples/src/0_basic/6_create_vp.ts @@ -7,8 +7,8 @@ import { Duration, EdDSAJwsVerifier, FailFast, - IotaIdentityClient, - JwkMemStore, + IdentityClientReadOnly, + IotaDocument, JwsSignatureOptions, JwsVerificationOptions, Jwt, @@ -17,15 +17,13 @@ import { JwtPresentationOptions, JwtPresentationValidationOptions, JwtPresentationValidator, - KeyIdMemStore, Presentation, Resolver, - Storage, SubjectHolderRelationship, Timestamp, } from "@iota/identity-wasm/node"; -import { Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; +import { IotaClient } from "@iota/iota-sdk/client"; +import { createDocumentForNetwork, getFundedClient, getMemstorage, IOTA_IDENTITY_PKG_ID, NETWORK_URL } from "../util"; /** * This example shows how to create a Verifiable Presentation and validate it. @@ -37,39 +35,29 @@ export async function createVP() { // Step 1: Create identities for the issuer and the holder. // =========================================================================== - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - const didClient = new IotaIdentityClient(client); - - // Creates a new wallet and identity (see "0_create_did" example). - const issuerSecretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - const issuerStorage: Storage = new Storage( - new JwkMemStore(), - new KeyIdMemStore(), - ); - let { document: issuerDocument, fragment: issuerFragment } = await createDid( - client, - issuerSecretManager, - issuerStorage, - ); - - // Create an identity for the holder, in this case also the subject. - const aliceSecretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - const aliceStorage: Storage = new Storage( - new JwkMemStore(), - new KeyIdMemStore(), - ); - let { document: aliceDocument, fragment: aliceFragment } = await createDid( - client, - aliceSecretManager, - aliceStorage, - ); + // create new client to connect to IOTA network + const iotaClient = new IotaClient({ url: NETWORK_URL }); + const network = await iotaClient.getChainIdentifier(); + + // create issuer account, create identity, and publish DID document for it + const issuerStorage = getMemstorage(); + const issuerClient = await getFundedClient(issuerStorage); + const [unpublishedIssuerDocument, issuerFragment] = await createDocumentForNetwork(issuerStorage, network); + const { output: issuerIdentity } = await issuerClient + .createIdentity(unpublishedIssuerDocument) + .finish() + .execute(issuerClient); + const issuerDocument = issuerIdentity.didDocument(); + + // create holder account, create identity, and publish DID document for it + const aliceStorage = getMemstorage(); + const aliceClient = await getFundedClient(aliceStorage); + const [unpublishedAliceDocument, aliceFragment] = await createDocumentForNetwork(aliceStorage, network); + const { output: aliceIdentity } = await aliceClient + .createIdentity(unpublishedAliceDocument) + .finish() + .execute(aliceClient); + const aliceDocument = aliceIdentity.didDocument(); // =========================================================================== // Step 2: Issuer creates and signs a Verifiable Credential. @@ -169,8 +157,8 @@ export async function createVP() { }, ); - const resolver = new Resolver({ - client: didClient, + const resolver = new Resolver({ + client: await IdentityClientReadOnly.createWithPkgId(iotaClient, IOTA_IDENTITY_PKG_ID), }); // Resolve the presentation holder. const presentationHolderDID: CoreDID = JwtPresentationValidator.extractHolder(presentationJwt); diff --git a/bindings/wasm/examples/src/0_basic/7_revoke_vc.ts b/bindings/wasm/identity_wasm/examples/src/0_basic/7_revoke_vc.ts similarity index 65% rename from bindings/wasm/examples/src/0_basic/7_revoke_vc.ts rename to bindings/wasm/identity_wasm/examples/src/0_basic/7_revoke_vc.ts index 63b00ee4d7..c650c078f4 100644 --- a/bindings/wasm/examples/src/0_basic/7_revoke_vc.ts +++ b/bindings/wasm/identity_wasm/examples/src/0_basic/7_revoke_vc.ts @@ -3,27 +3,30 @@ import { Credential, - EdCurve, FailFast, + IdentityClientReadOnly, IJwsVerifier, IotaDocument, - IotaIdentityClient, Jwk, - JwkMemStore, JwsAlgorithm, JwsSignatureOptions, JwtCredentialValidationOptions, JwtCredentialValidator, - KeyIdMemStore, Resolver, RevocationBitmap, Service, - Storage, VerificationMethod, verifyEd25519, } from "@iota/identity-wasm/node"; -import { AliasOutput, Client, IRent, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; +import { IotaClient } from "@iota/iota-sdk/client"; +import { + createDocumentForNetwork, + getFundedClient, + getMemstorage, + IOTA_IDENTITY_PKG_ID, + NETWORK_URL, + TEST_GAS_BUDGET, +} from "../util"; /** * This example shows how to revoke a verifiable credential. @@ -38,43 +41,29 @@ export async function revokeVC() { // Create a Verifiable Credential. // =========================================================================== - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - const didClient = new IotaIdentityClient(client); - - // Generate a random mnemonic for the issuer. - const issuerSecretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Create an identity for the issuer with one verification method `key-1`. - const issuerStorage: Storage = new Storage( - new JwkMemStore(), - new KeyIdMemStore(), - ); - let { document: issuerDocument, fragment: issuerFragment } = await createDid( - client, - issuerSecretManager, - issuerStorage, - ); - - // Generate a random mnemonic for Alice. - const aliceSecretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Create an identity for the holder, in this case also the subject. - const aliceStorage: Storage = new Storage( - new JwkMemStore(), - new KeyIdMemStore(), - ); - let { document: aliceDocument } = await createDid( - client, - aliceSecretManager, - aliceStorage, - ); + // Create new client to connect to IOTA network. + const iotaClient = new IotaClient({ url: NETWORK_URL }); + const network = await iotaClient.getChainIdentifier(); + + // Create an identity for the issuer with one verification method `key-1`, and publish DID document for it. + const issuerStorage = getMemstorage(); + const issuerClient = await getFundedClient(issuerStorage); + const [unpublishedIssuerDocument, issuerFragment] = await createDocumentForNetwork(issuerStorage, network); + const { output: issuerIdentity } = await issuerClient + .createIdentity(unpublishedIssuerDocument) + .finish() + .execute(issuerClient); + let issuerDocument = issuerIdentity.didDocument(); + + // create holder account, create identity, and publish DID document for it. + const aliceStorage = getMemstorage(); + const aliceClient = await getFundedClient(aliceStorage); + const [unpublishedAliceDocument, aliceFragment] = await createDocumentForNetwork(aliceStorage, network); + const { output: aliceIdentity } = await aliceClient + .createIdentity(unpublishedAliceDocument) + .finish() + .execute(aliceClient); + const aliceDocument = aliceIdentity.didDocument(); // Create a new empty revocation bitmap. No credential is revoked yet. const revocationBitmap = new RevocationBitmap(); @@ -84,26 +73,11 @@ export async function revokeVC() { const service: Service = revocationBitmap.toService(serviceId); issuerDocument.insertService(service); - // Resolve the latest output and update it with the given document. - let aliasOutput: AliasOutput = await didClient.updateDidOutput( - issuerDocument, - ); - - // Because the size of the DID document increased, we have to increase the allocated storage deposit. - // This increases the deposit amount to the new minimum. - let rentStructure: IRent = await didClient.getRentStructure(); - aliasOutput = await client.buildAliasOutput({ - ...aliasOutput, - amount: Utils.computeStorageDeposit(aliasOutput, rentStructure), - aliasId: aliasOutput.getAliasId(), - unlockConditions: aliasOutput.getUnlockConditions(), - }); - - // Publish the document. - issuerDocument = await didClient.publishDidOutput( - issuerSecretManager, - aliasOutput, - ); + // Publish the updated document. + await issuerIdentity + .updateDidDocument(issuerDocument) + .withGasBudget(TEST_GAS_BUDGET) + .execute(issuerClient); // Create a credential subject indicating the degree earned by Alice, linked to their DID. const subject = { @@ -156,19 +130,10 @@ export async function revokeVC() { issuerDocument.revokeCredentials("my-revocation-service", CREDENTIAL_INDEX); // Publish the changes. - aliasOutput = await didClient.updateDidOutput(issuerDocument); - rentStructure = await didClient.getRentStructure(); - aliasOutput = await client.buildAliasOutput({ - ...aliasOutput, - amount: Utils.computeStorageDeposit(aliasOutput, rentStructure), - aliasId: aliasOutput.getAliasId(), - unlockConditions: aliasOutput.getUnlockConditions(), - }); - - const update2: IotaDocument = await didClient.publishDidOutput( - issuerSecretManager, - aliasOutput, - ); + await issuerIdentity + .updateDidDocument(issuerDocument) + .withGasBudget(TEST_GAS_BUDGET) + .execute(issuerClient); // Credential verification now fails. try { @@ -195,22 +160,17 @@ export async function revokeVC() { await issuerDocument.purgeMethod(issuerStorage, originalMethod.id()); // Publish the changes. - aliasOutput = await didClient.updateDidOutput(issuerDocument); - rentStructure = await didClient.getRentStructure(); - aliasOutput = await client.buildAliasOutput({ - ...aliasOutput, - amount: Utils.computeStorageDeposit(aliasOutput, rentStructure), - aliasId: aliasOutput.getAliasId(), - unlockConditions: aliasOutput.getUnlockConditions(), - }); + await issuerIdentity + .updateDidDocument(issuerDocument) + .withGasBudget(TEST_GAS_BUDGET) + .execute(issuerClient); - issuerDocument = await didClient.publishDidOutput( - issuerSecretManager, - aliasOutput, - ); + issuerDocument = issuerIdentity.didDocument(); // We expect the verifiable credential to be revoked. - const resolver = new Resolver({ client: didClient }); + const resolver = new Resolver({ + client: await IdentityClientReadOnly.createWithPkgId(iotaClient, IOTA_IDENTITY_PKG_ID), + }); try { // Resolve the issuer's updated DID Document to ensure the key was revoked successfully. const resolvedIssuerDoc = await resolver.resolve( diff --git a/bindings/wasm/examples/src/1_advanced/10_sd_jwt_vc.ts b/bindings/wasm/identity_wasm/examples/src/1_advanced/10_sd_jwt_vc.ts similarity index 100% rename from bindings/wasm/examples/src/1_advanced/10_sd_jwt_vc.ts rename to bindings/wasm/identity_wasm/examples/src/1_advanced/10_sd_jwt_vc.ts diff --git a/bindings/wasm/identity_wasm/examples/src/1_advanced/4_custom_resolution.ts b/bindings/wasm/identity_wasm/examples/src/1_advanced/4_custom_resolution.ts new file mode 100644 index 0000000000..cbe6048cbf --- /dev/null +++ b/bindings/wasm/identity_wasm/examples/src/1_advanced/4_custom_resolution.ts @@ -0,0 +1,82 @@ +import { CoreDocument, IotaDocument, Resolver } from "@iota/identity-wasm/node"; +import { IotaClient } from "@iota/iota-sdk/client"; +import { createDocumentForNetwork, getFundedClient, getMemstorage, NETWORK_URL } from "../util"; + +// Use this external package to avoid implementing the entire did:key method in this example. +// @ts-ignore +import { DidKeyDriver } from "@digitalcredentials/did-method-key"; +const didKeyDriver = new DidKeyDriver(); + +type KeyDocument = { customProperty: String } & CoreDocument; + +function isKeyDocument(doc: object): doc is KeyDocument { + return "customProperty" in doc; +} + +/** Demonstrates how to set up a resolver using custom handlers. + */ +export async function customResolution() { + // Set up a handler for resolving Ed25519 did:key + const keyHandler = async function(didKey: string): Promise { + let document = await didKeyDriver.get({ did: didKey }); + + // for demo purposes we'll just inject the custom property into a core document + // to create a new KeyDocument instance + let coreDocument = CoreDocument.fromJSON(document); + (coreDocument as unknown as KeyDocument).customProperty = "foobar"; + return coreDocument as unknown as KeyDocument; + }; + + // create new clients and create new account + const iotaClient = new IotaClient({ url: NETWORK_URL }); + const network = await iotaClient.getChainIdentifier(); + const storage = getMemstorage(); + const identityClient = await getFundedClient(storage); + const [unpublished] = await createDocumentForNetwork(storage, network); + + // create new identity for this account and publish document for it, DID of it will be resolved later on + const { output: identity } = await identityClient + .createIdentity(unpublished) + .finish() + .execute(identityClient); + const did = identity.didDocument().id(); + + // Construct a Resolver capable of resolving the did:key and iota methods. + let handlerMap: Map Promise> = new Map(); + handlerMap.set("key", keyHandler); + + const resolver = new Resolver( + { + client: identityClient, + handlers: handlerMap, + }, + ); + + // A valid Ed25519 did:key value taken from https://w3c-ccg.github.io/did-method-key/#example-1-a-simple-ed25519-did-key-value. + const didKey = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"; + + // Resolve didKey into a DID document. + const didKeyDoc = await resolver.resolve(didKey); + + // Resolve the DID we created on the IOTA network. + const didIotaDoc = await resolver.resolve(did.toString()); + + // Check that the types of the resolved documents match our expectations: + + if (isKeyDocument(didKeyDoc)) { + console.log("Resolved DID Key document:", JSON.stringify(didKeyDoc, null, 2)); + console.log(`Resolved DID Key document has a custom property with the value '${didKeyDoc.customProperty}'`); + } else { + throw new Error( + "the resolved document type should match the output type of keyHandler", + ); + } + + if (didIotaDoc instanceof IotaDocument) { + console.log("Resolved IOTA DID document:", JSON.stringify(didIotaDoc, null, 2)); + } else { + throw new Error( + "the resolved document type should match IotaDocument", + ); + } +} diff --git a/bindings/wasm/examples/src/1_advanced/5_domain_linkage.ts b/bindings/wasm/identity_wasm/examples/src/1_advanced/5_domain_linkage.ts similarity index 75% rename from bindings/wasm/examples/src/1_advanced/5_domain_linkage.ts rename to bindings/wasm/identity_wasm/examples/src/1_advanced/5_domain_linkage.ts index a2af8f83fb..6b928690eb 100644 --- a/bindings/wasm/examples/src/1_advanced/5_domain_linkage.ts +++ b/bindings/wasm/identity_wasm/examples/src/1_advanced/5_domain_linkage.ts @@ -10,39 +10,33 @@ import { EdDSAJwsVerifier, IotaDID, IotaDocument, - IotaIdentityClient, - JwkMemStore, JwsSignatureOptions, JwtCredentialValidationOptions, JwtDomainLinkageValidator, - KeyIdMemStore, LinkedDomainService, - Storage, Timestamp, } from "@iota/identity-wasm/node"; -import { AliasOutput, Client, IRent, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; +import { IotaClient } from "@iota/iota-sdk/client"; +import { createDocumentForNetwork, getFundedClient, getMemstorage, NETWORK_URL, TEST_GAS_BUDGET } from "../util"; /** * Demonstrates how to link a domain and a DID and verify the linkage. */ export async function domainLinkage() { - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - const didClient = new IotaIdentityClient(client); - - // Generate a random mnemonic for our wallet. - const secretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - const storage: Storage = new Storage(new JwkMemStore(), new KeyIdMemStore()); - - // Creates a new wallet and identity (see "0_create_did" example). - let { document, fragment } = await createDid(client, secretManager, storage); - const did: IotaDID = document.id(); + // create new clients and create new account + const iotaClient = new IotaClient({ url: NETWORK_URL }); + const network = await iotaClient.getChainIdentifier(); + const storage = getMemstorage(); + const identityClient = await getFundedClient(storage); + const [unpublished, vmFragment1] = await createDocumentForNetwork(storage, network); + + // create new identity for this account and publish document for it + const { output: identity } = await identityClient + .createIdentity(unpublished) + .finish() + .execute(identityClient); + const document = identity.didDocument(); + const did = document.id(); // ===================================================== // Create Linked Domain service @@ -59,7 +53,9 @@ export async function domainLinkage() { domains: [domainFoo, domainBar], }); document.insertService(linkedDomainService.toService()); - let updatedDidDocument = await publishDocument(didClient, secretManager, document); + await identity.updateDidDocument(document).execute(identityClient); + + let updatedDidDocument = identity.didDocument(); console.log("Updated DID document:", JSON.stringify(updatedDidDocument, null, 2)); // ===================================================== @@ -81,7 +77,7 @@ export async function domainLinkage() { // Sign the credential. const credentialJwt = await document.createCredentialJwt( storage, - fragment, + vmFragment1, domainLinkageCredential, new JwsSignatureOptions(), ); @@ -120,7 +116,7 @@ export async function domainLinkage() { // Retrieve the issuers of the Domain Linkage Credentials which correspond to the possibly linked DIDs. // Note that in this example only the first entry in the credential is validated. let issuers: Array = fetchedConfigurationResource.issuers(); - const issuerDocument: IotaDocument = await didClient.resolveDid(IotaDID.parse(issuers[0].toString())); + const issuerDocument: IotaDocument = await identityClient.resolveDid(IotaDID.parse(issuers[0].toString())); // Validate the linkage between the Domain Linkage Credential in the configuration and the provided issuer DID. // Validation succeeds when no error is thrown. @@ -135,7 +131,7 @@ export async function domainLinkage() { // → Case 2: starting from a DID // ===================================================== - const didDocument: IotaDocument = await didClient.resolveDid(did); + const didDocument: IotaDocument = await identityClient.resolveDid(did); // Get the Linked Domain Services from the DID Document. let linkedDomainServices: LinkedDomainService[] = didDocument @@ -167,26 +163,3 @@ export async function domainLinkage() { console.log("Successfully validated Domain Linkage!"); } - -async function publishDocument( - client: IotaIdentityClient, - secretManager: MnemonicSecretManager, - document: IotaDocument, -): Promise { - // Resolve the latest output and update it with the given document. - let aliasOutput: AliasOutput = await client.updateDidOutput(document); - - // Because the size of the DID document increased, we have to increase the allocated storage deposit. - // This increases the deposit amount to the new minimum. - const rentStructure: IRent = await client.getRentStructure(); - aliasOutput = await client.client.buildAliasOutput({ - ...aliasOutput, - amount: Utils.computeStorageDeposit(aliasOutput, rentStructure), - aliasId: aliasOutput.getAliasId(), - unlockConditions: aliasOutput.getUnlockConditions(), - }); - - // Publish the output. - const updated: IotaDocument = await client.publishDidOutput(secretManager, aliasOutput); - return updated; -} diff --git a/bindings/wasm/examples/src/1_advanced/6_sd_jwt.ts b/bindings/wasm/identity_wasm/examples/src/1_advanced/6_sd_jwt.ts similarity index 81% rename from bindings/wasm/examples/src/1_advanced/6_sd_jwt.ts rename to bindings/wasm/identity_wasm/examples/src/1_advanced/6_sd_jwt.ts index e389118f8c..84d5bf4e52 100644 --- a/bindings/wasm/examples/src/1_advanced/6_sd_jwt.ts +++ b/bindings/wasm/identity_wasm/examples/src/1_advanced/6_sd_jwt.ts @@ -6,63 +6,51 @@ import { DecodedJwtCredential, EdDSAJwsVerifier, FailFast, - JwkMemStore, JwsSignatureOptions, JwsVerificationOptions, JwtCredentialValidationOptions, KeyBindingJwtClaims, KeyBindingJWTValidationOptions, - KeyIdMemStore, SdJwt, SdJwtCredentialValidator, SdObjectEncoder, - Storage, Timestamp, } from "@iota/identity-wasm/node"; -import { Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; +import { IotaClient } from "@iota/iota-sdk/client"; +import { createDocumentForNetwork, getFundedClient, getMemstorage, NETWORK_URL } from "../util"; /** - * Demonstrates how to create a selective disclosure verifiable credential and validate it - * using the [Selective Disclosure for JWTs (SD-JWT)](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html) specification. + * Demonstrates how to create a selective disclosure verifiable credential and validate it * using the [Selective Disclosure for JWTs (SD-JWT)](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html) specification. */ export async function sdJwt() { // =========================================================================== // Step 1: Create identities for the issuer and the holder. // =========================================================================== - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); + // create new client to connect to IOTA network + const iotaClient = new IotaClient({ url: NETWORK_URL }); + const network = await iotaClient.getChainIdentifier(); // Creates a new wallet and identity (see "0_create_did" example). - const issuerSecretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - const issuerStorage: Storage = new Storage( - new JwkMemStore(), - new KeyIdMemStore(), - ); - let { document: issuerDocument, fragment: issuerFragment } = await createDid( - client, - issuerSecretManager, - issuerStorage, - ); - - // Create an identity for the holder, in this case also the subject. - const aliceSecretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - const aliceStorage: Storage = new Storage( - new JwkMemStore(), - new KeyIdMemStore(), - ); - let { document: aliceDocument, fragment: aliceFragment } = await createDid( - client, - aliceSecretManager, - aliceStorage, - ); + // Create an identity for the issuer with one verification method `key-1`, and publish DID document for it. + const issuerStorage = getMemstorage(); + const issuerClient = await getFundedClient(issuerStorage); + const [unpublishedIssuerDocument, issuerFragment] = await createDocumentForNetwork(issuerStorage, network); + const { output: issuerIdentity } = await issuerClient + .createIdentity(unpublishedIssuerDocument) + .finish() + .execute(issuerClient); + const issuerDocument = issuerIdentity.didDocument(); + + // Create an identity for the holder, and publish DID document for it, in this case also the subject. + const aliceStorage = getMemstorage(); + const aliceClient = await getFundedClient(aliceStorage); + const [unpublishedAliceDocument, aliceFragment] = await createDocumentForNetwork(aliceStorage, network); + const { output: aliceIdentity } = await aliceClient + .createIdentity(unpublishedAliceDocument) + .finish() + .execute(aliceClient); + const aliceDocument = aliceIdentity.didDocument(); // =========================================================================== // Step 2: Issuer creates and signs a selectively disclosable JWT verifiable credential. diff --git a/bindings/wasm/examples/src/1_advanced/7_status_list_2021.ts b/bindings/wasm/identity_wasm/examples/src/1_advanced/7_status_list_2021.ts similarity index 79% rename from bindings/wasm/examples/src/1_advanced/7_status_list_2021.ts rename to bindings/wasm/identity_wasm/examples/src/1_advanced/7_status_list_2021.ts index 4e70d8fa19..a36bea5443 100644 --- a/bindings/wasm/examples/src/1_advanced/7_status_list_2021.ts +++ b/bindings/wasm/identity_wasm/examples/src/1_advanced/7_status_list_2021.ts @@ -5,63 +5,47 @@ import { Credential, EdDSAJwsVerifier, FailFast, - JwkMemStore, JwsSignatureOptions, JwtCredentialValidationOptions, JwtCredentialValidator, - KeyIdMemStore, StatusCheck, StatusList2021, StatusList2021Credential, StatusList2021CredentialBuilder, StatusList2021Entry, StatusPurpose, - Storage, } from "@iota/identity-wasm/node"; -import { Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, createDid } from "../util"; +import { IotaClient } from "@iota/iota-sdk/client"; +import { createDocumentForNetwork, getFundedClient, getMemstorage, NETWORK_URL } from "../util"; export async function statusList2021() { // =========================================================================== // Create a Verifiable Credential. // =========================================================================== - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - - // Generate a random mnemonic for the issuer. - const issuerSecretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Create an identity for the issuer with one verification method `key-1`. - const issuerStorage: Storage = new Storage( - new JwkMemStore(), - new KeyIdMemStore(), - ); - let { document: issuerDocument, fragment: issuerFragment } = await createDid( - client, - issuerSecretManager, - issuerStorage, - ); - - // Generate a random mnemonic for Alice. - const aliceSecretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - - // Create an identity for the holder, in this case also the subject. - const aliceStorage: Storage = new Storage( - new JwkMemStore(), - new KeyIdMemStore(), - ); - let { document: aliceDocument } = await createDid( - client, - aliceSecretManager, - aliceStorage, - ); + // create new client to connect to IOTA network + const iotaClient = new IotaClient({ url: NETWORK_URL }); + const network = await iotaClient.getChainIdentifier(); + + // Create an identity for the issuer with one verification method `key-1`, and publish DID document for it. + const issuerStorage = getMemstorage(); + const issuerClient = await getFundedClient(issuerStorage); + const [unpublishedIssuerDocument, issuerFragment] = await createDocumentForNetwork(issuerStorage, network); + const { output: issuerIdentity } = await issuerClient + .createIdentity(unpublishedIssuerDocument) + .finish() + .execute(issuerClient); + const issuerDocument = issuerIdentity.didDocument(); + + // Create an identity for the holder, and publish DID document for it, in this case also the subject. + const aliceStorage = getMemstorage(); + const aliceClient = await getFundedClient(aliceStorage); + const [unpublishedAliceDocument] = await createDocumentForNetwork(aliceStorage, network); + const { output: aliceIdentity } = await aliceClient + .createIdentity(unpublishedAliceDocument) + .finish() + .execute(aliceClient); + const aliceDocument = aliceIdentity.didDocument(); // Create a new empty status list. No credentials have been revoked yet. const statusList = new StatusList2021(); diff --git a/bindings/wasm/examples/src/1_advanced/8_zkp.ts b/bindings/wasm/identity_wasm/examples/src/1_advanced/8_zkp.ts similarity index 71% rename from bindings/wasm/examples/src/1_advanced/8_zkp.ts rename to bindings/wasm/identity_wasm/examples/src/1_advanced/8_zkp.ts index 55d0c82fca..b216ebce0f 100644 --- a/bindings/wasm/examples/src/1_advanced/8_zkp.ts +++ b/bindings/wasm/identity_wasm/examples/src/1_advanced/8_zkp.ts @@ -1,106 +1,48 @@ import { Credential, FailFast, + IdentityClientReadOnly, IotaDID, IotaDocument, - IotaIdentityClient, JptCredentialValidationOptions, JptCredentialValidator, JptCredentialValidatorUtils, JptPresentationValidationOptions, JptPresentationValidator, JptPresentationValidatorUtils, - JwkMemStore, JwpCredentialOptions, JwpPresentationOptions, - KeyIdMemStore, MethodScope, ProofAlgorithm, SelectiveDisclosurePresentation, - Storage, } from "@iota/identity-wasm/node"; -import { - type Address, - AliasOutput, - Client, - MnemonicSecretManager, - SecretManager, - SecretManagerType, - Utils, -} from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, ensureAddressHasFunds } from "../util"; - -/** Creates a DID Document and publishes it in a new Alias Output. - -Its functionality is equivalent to the "create DID" example -and exists for convenient calling from the other examples. */ -export async function createDid(client: Client, secretManager: SecretManagerType, storage: Storage): Promise<{ - address: Address; - document: IotaDocument; - fragment: string; -}> { - const didClient = new IotaIdentityClient(client); - const networkHrp: string = await didClient.getNetworkHrp(); - - const secretManagerInstance = new SecretManager(secretManager); - const walletAddressBech32 = (await secretManagerInstance.generateEd25519Addresses({ - accountIndex: 0, - range: { - start: 0, - end: 1, - }, - bech32Hrp: networkHrp, - }))[0]; - - console.log("Wallet address Bech32:", walletAddressBech32); - - await ensureAddressHasFunds(client, walletAddressBech32); - - const address: Address = Utils.parseBech32Address(walletAddressBech32); - - // Create a new DID document with a placeholder DID. - // The DID will be derived from the Alias Id of the Alias Output after publishing. - const document = new IotaDocument(networkHrp); - - const fragment = await document.generateMethodJwp( - storage, - ProofAlgorithm.BLS12381_SHA256, - undefined, - MethodScope.VerificationMethod(), - ); - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - const aliasOutput: AliasOutput = await didClient.newDidOutput(address, document); - - // Publish the Alias Output and get the published DID document. - const published = await didClient.publishDidOutput(secretManager, aliasOutput); +import { IotaClient } from "@iota/iota-sdk/client"; +import { getFundedClient, getMemstorage, IOTA_IDENTITY_PKG_ID, NETWORK_URL } from "../util"; - return { address, document: published, fragment }; -} export async function zkp() { // =========================================================================== // Step 1: Create identity for the issuer. // =========================================================================== - // Create a new client to interact with the IOTA ledger. - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); + // create new client to connect to IOTA network + const iotaClient = new IotaClient({ url: NETWORK_URL }); + const network = await iotaClient.getChainIdentifier(); - // Creates a new wallet and identity (see "0_create_did" example). - const issuerSecretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - const issuerStorage: Storage = new Storage( - new JwkMemStore(), - new KeyIdMemStore(), - ); - let { document: issuerDocument, fragment: issuerFragment } = await createDid( - client, - issuerSecretManager, + // Create an identity for the issuer with one verification method `key-1`, and publish DID document for it. + const issuerStorage = getMemstorage(); + const issuerClient = await getFundedClient(issuerStorage); + const unpublishedIssuerDocument = new IotaDocument(network); + const issuerFragment = await unpublishedIssuerDocument.generateMethodJwp( issuerStorage, + ProofAlgorithm.BLS12381_SHA256, + undefined, + MethodScope.VerificationMethod(), ); + const { output: issuerIdentity } = await issuerClient + .createIdentity(unpublishedIssuerDocument) + .finish() + .execute(issuerClient); + const issuerDocument = issuerIdentity.didDocument(); // =========================================================================== // Step 2: Issuer creates and signs a Verifiable Credential with BBS algorithm. @@ -148,11 +90,14 @@ export async function zkp() { // ============================================================================================ // Step 4: Holder resolve Issuer's DID, retrieve Issuer's document and validate the Credential // ============================================================================================ - const identityClient = new IotaIdentityClient(client); + const identityClientReadOnly = await IdentityClientReadOnly.createWithPkgId( + iotaClient, + IOTA_IDENTITY_PKG_ID, + ); // Holder resolves issuer's DID. let issuerDid = IotaDID.parse(JptCredentialValidatorUtils.extractIssuerFromIssuedJpt(credentialJpt).toString()); - let issuerDoc = await identityClient.resolveDid(issuerDid); + let issuerDoc = await identityClientReadOnly.resolveDid(issuerDid); // Holder validates the credential and retrieve the JwpIssued, needed to construct the JwpPresented let decodedCredential = JptCredentialValidator.validate( @@ -212,7 +157,7 @@ export async function zkp() { const issuerDidV = IotaDID.parse( JptPresentationValidatorUtils.extractIssuerFromPresentedJpt(presentationJpt).toString(), ); - const issuerDocV = await identityClient.resolveDid(issuerDidV); + const issuerDocV = await identityClientReadOnly.resolveDid(issuerDidV); const presentationValidationOptions = new JptPresentationValidationOptions({ nonce: challenge }); const decodedPresentedCredential = JptPresentationValidator.validate( diff --git a/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts b/bindings/wasm/identity_wasm/examples/src/1_advanced/9_zkp_revocation.ts similarity index 64% rename from bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts rename to bindings/wasm/identity_wasm/examples/src/1_advanced/9_zkp_revocation.ts index e8c3d586a1..b9bb2a768c 100644 --- a/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts +++ b/bindings/wasm/identity_wasm/examples/src/1_advanced/9_zkp_revocation.ts @@ -2,19 +2,15 @@ import { Credential, Duration, FailFast, - IotaDID, IotaDocument, - IotaIdentityClient, JptCredentialValidationOptions, JptCredentialValidator, JptCredentialValidatorUtils, JptPresentationValidationOptions, JptPresentationValidator, JptPresentationValidatorUtils, - JwkMemStore, JwpCredentialOptions, JwpPresentationOptions, - KeyIdMemStore, MethodScope, ProofAlgorithm, RevocationBitmap, @@ -22,104 +18,47 @@ import { SelectiveDisclosurePresentation, Status, StatusCheck, - Storage, Timestamp, } from "@iota/identity-wasm/node"; -import { - type Address, - AliasOutput, - Client, - MnemonicSecretManager, - SecretManager, - SecretManagerType, - Utils, -} from "@iota/sdk-wasm/node"; -import { API_ENDPOINT, ensureAddressHasFunds } from "../util"; - -/** Creates a DID Document and publishes it in a new Alias Output. - -Its functionality is equivalent to the "create DID" example -and exists for convenient calling from the other examples. */ -export async function createDid(client: Client, secretManager: SecretManagerType, storage: Storage): Promise<{ - address: Address; - document: IotaDocument; - fragment: string; -}> { - const didClient = new IotaIdentityClient(client); - const networkHrp: string = await didClient.getNetworkHrp(); - - const secretManagerInstance = new SecretManager(secretManager); - const walletAddressBech32 = (await secretManagerInstance.generateEd25519Addresses({ - accountIndex: 0, - range: { - start: 0, - end: 1, - }, - bech32Hrp: networkHrp, - }))[0]; - - console.log("Wallet address Bech32:", walletAddressBech32); - - await ensureAddressHasFunds(client, walletAddressBech32); +import { IotaClient } from "@iota/iota-sdk/client"; +import { createDocumentForNetwork, getFundedClient, getMemstorage, NETWORK_URL, TEST_GAS_BUDGET } from "../util"; - const address: Address = Utils.parseBech32Address(walletAddressBech32); - - // Create a new DID document with a placeholder DID. - // The DID will be derived from the Alias Id of the Alias Output after publishing. - const document = new IotaDocument(networkHrp); - - const fragment = await document.generateMethodJwp( - storage, +export async function zkp_revocation() { + // create new client to connect to IOTA network + const iotaClient = new IotaClient({ url: NETWORK_URL }); + const network = await iotaClient.getChainIdentifier(); + + // Create an identity for the issuer with one verification method `key-1`, and publish DID document for it. + const issuerStorage = getMemstorage(); + const issuerClient = await getFundedClient(issuerStorage); + const unpublishedIssuerDocument = new IotaDocument(network); + const issuerFragment = await unpublishedIssuerDocument.generateMethodJwp( + issuerStorage, ProofAlgorithm.BLS12381_SHA256, undefined, MethodScope.VerificationMethod(), ); const revocationBitmap = new RevocationBitmap(); - const serviceId = document.id().toUrl().join("#my-revocation-service"); + // add service for revocation + const serviceId = unpublishedIssuerDocument.id().toUrl().join("#my-revocation-service"); const service = revocationBitmap.toService(serviceId); + unpublishedIssuerDocument.insertService(service); + const { output: issuerIdentity } = await issuerClient + .createIdentity(unpublishedIssuerDocument) + .finish() + .execute(issuerClient); + let issuerDocument = issuerIdentity.didDocument(); + + // Create an identity for the holder, and publish DID document for it, in this case also the subject. + const holderStorage = getMemstorage(); + const holderClient = await getFundedClient(holderStorage); + const [unpublishedholderDocument] = await createDocumentForNetwork(holderStorage, network); + const { output: holderIdentity } = await holderClient + .createIdentity(unpublishedholderDocument) + .finish() + .execute(holderClient); + const holderDocument = holderIdentity.didDocument(); - document.insertService(service); - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - const aliasOutput: AliasOutput = await didClient.newDidOutput(address, document); - - // Publish the Alias Output and get the published DID document. - const published = await didClient.publishDidOutput(secretManager, aliasOutput); - - return { address, document: published, fragment }; -} -export async function zkp_revocation() { - // Create a new client to interact with the IOTA ledger. - const client = new Client({ - primaryNode: API_ENDPOINT, - localPow: true, - }); - - // Creates a new wallet and identity (see "0_create_did" example). - const issuerSecretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - const issuerStorage: Storage = new Storage( - new JwkMemStore(), - new KeyIdMemStore(), - ); - let { document: issuerDocument, fragment: issuerFragment } = await createDid( - client, - issuerSecretManager, - issuerStorage, - ); - const holderSecretManager: MnemonicSecretManager = { - mnemonic: Utils.generateMnemonic(), - }; - const holderStorage: Storage = new Storage( - new JwkMemStore(), - new KeyIdMemStore(), - ); - let { document: holderDocument, fragment: holderFragment } = await createDid( - client, - holderSecretManager, - holderStorage, - ); // ========================================================================================= // Step 1: Create a new RevocationTimeframeStatus containing the current validityTimeframe // ======================================================================================= @@ -132,9 +71,9 @@ export async function zkp_revocation() { Timestamp.nowUTC(), ); - // Create a credential subject indicating the degree earned by Alice. + // Create a credential subject indicating the degree earned by holder. const subject = { - name: "Alice", + name: "holder", mainCourses: ["Object-oriented Programming", "Mathematics"], degree: { type: "BachelorDegree", @@ -244,7 +183,7 @@ export async function zkp_revocation() { StatusCheck.Strict, ); } catch (_) { - console.log("successfully expired!"); + console.log("Successfully expired!"); } // =========================================================================== @@ -253,20 +192,11 @@ export async function zkp_revocation() { console.log("Issuer decides to revoke the Credential"); - const identityClient = new IotaIdentityClient(client); - // Update the RevocationBitmap service in the issuer's DID Document. // This revokes the credential's unique index. issuerDocument.revokeCredentials("my-revocation-service", 5); - let aliasOutput = await identityClient.updateDidOutput(issuerDocument); - const rent = await identityClient.getRentStructure(); - aliasOutput = await client.buildAliasOutput({ - ...aliasOutput, - amount: Utils.computeStorageDeposit(aliasOutput, rent), - aliasId: aliasOutput.getAliasId(), - unlockConditions: aliasOutput.getUnlockConditions(), - }); - issuerDocument = await identityClient.publishDidOutput(issuerSecretManager, aliasOutput); + await issuerIdentity.updateDidDocument(issuerDocument).execute(issuerClient); + issuerDocument = issuerIdentity.didDocument(); // Holder checks if his credential has been revoked by the Issuer try { diff --git a/bindings/wasm/examples/src/main.ts b/bindings/wasm/identity_wasm/examples/src/main.ts similarity index 76% rename from bindings/wasm/examples/src/main.ts rename to bindings/wasm/identity_wasm/examples/src/main.ts index c88ee419c0..c08bbce8c3 100644 --- a/bindings/wasm/examples/src/main.ts +++ b/bindings/wasm/identity_wasm/examples/src/main.ts @@ -5,15 +5,10 @@ import { createIdentity } from "./0_basic/0_create_did"; import { updateIdentity } from "./0_basic/1_update_did"; import { resolveIdentity } from "./0_basic/2_resolve_did"; import { deactivateIdentity } from "./0_basic/3_deactivate_did"; -import { deleteIdentity } from "./0_basic/4_delete_did"; import { createVC } from "./0_basic/5_create_vc"; import { createVP } from "./0_basic/6_create_vp"; import { revokeVC } from "./0_basic/7_revoke_vc"; -import { didControlsDid } from "./1_advanced/0_did_controls_did"; import { sdJwtVc } from "./1_advanced/10_sd_jwt_vc"; -import { didIssuesNft } from "./1_advanced/1_did_issues_nft"; -import { nftOwnsDid } from "./1_advanced/2_nft_owns_did"; -import { didIssuesTokens } from "./1_advanced/3_did_issues_tokens"; import { customResolution } from "./1_advanced/4_custom_resolution"; import { domainLinkage } from "./1_advanced/5_domain_linkage"; import { sdJwt } from "./1_advanced/6_sd_jwt"; @@ -37,22 +32,12 @@ async function main() { return await resolveIdentity(); case "3_deactivate_did": return await deactivateIdentity(); - case "4_delete_did": - return await deleteIdentity(); case "5_create_vc": return await createVC(); case "6_create_vp": return await createVP(); case "7_revoke_vc": return await revokeVC(); - case "0_did_controls_did": - return await didControlsDid(); - case "1_did_issues_nft": - return await didIssuesNft(); - case "2_nft_owns_did": - return await nftOwnsDid(); - case "3_did_issues_tokens": - return await didIssuesTokens(); case "4_custom_resolution": return await customResolution(); case "5_domain_linkage": diff --git a/bindings/wasm/examples/src/tests/0_create_did.ts b/bindings/wasm/identity_wasm/examples/src/tests/0_create_did.ts similarity index 100% rename from bindings/wasm/examples/src/tests/0_create_did.ts rename to bindings/wasm/identity_wasm/examples/src/tests/0_create_did.ts diff --git a/bindings/wasm/examples/src/tests/1_update_did.ts b/bindings/wasm/identity_wasm/examples/src/tests/1_update_did.ts similarity index 100% rename from bindings/wasm/examples/src/tests/1_update_did.ts rename to bindings/wasm/identity_wasm/examples/src/tests/1_update_did.ts diff --git a/bindings/wasm/examples/src/tests/2_resolve_did.ts b/bindings/wasm/identity_wasm/examples/src/tests/2_resolve_did.ts similarity index 100% rename from bindings/wasm/examples/src/tests/2_resolve_did.ts rename to bindings/wasm/identity_wasm/examples/src/tests/2_resolve_did.ts diff --git a/bindings/wasm/examples/src/tests/3_deactivate_did.ts b/bindings/wasm/identity_wasm/examples/src/tests/3_deactivate_did.ts similarity index 100% rename from bindings/wasm/examples/src/tests/3_deactivate_did.ts rename to bindings/wasm/identity_wasm/examples/src/tests/3_deactivate_did.ts diff --git a/bindings/wasm/examples/src/tests/4_custom_resolution.ts b/bindings/wasm/identity_wasm/examples/src/tests/4_custom_resolution.ts similarity index 100% rename from bindings/wasm/examples/src/tests/4_custom_resolution.ts rename to bindings/wasm/identity_wasm/examples/src/tests/4_custom_resolution.ts diff --git a/bindings/wasm/examples/src/tests/5_create_vc.ts b/bindings/wasm/identity_wasm/examples/src/tests/5_create_vc.ts similarity index 100% rename from bindings/wasm/examples/src/tests/5_create_vc.ts rename to bindings/wasm/identity_wasm/examples/src/tests/5_create_vc.ts diff --git a/bindings/wasm/examples/src/tests/5_domain_linkage.ts b/bindings/wasm/identity_wasm/examples/src/tests/5_domain_linkage.ts similarity index 100% rename from bindings/wasm/examples/src/tests/5_domain_linkage.ts rename to bindings/wasm/identity_wasm/examples/src/tests/5_domain_linkage.ts diff --git a/bindings/wasm/examples/src/tests/6_create_vp.ts b/bindings/wasm/identity_wasm/examples/src/tests/6_create_vp.ts similarity index 100% rename from bindings/wasm/examples/src/tests/6_create_vp.ts rename to bindings/wasm/identity_wasm/examples/src/tests/6_create_vp.ts diff --git a/bindings/wasm/examples/src/tests/6_sd_jwt.ts b/bindings/wasm/identity_wasm/examples/src/tests/6_sd_jwt.ts similarity index 100% rename from bindings/wasm/examples/src/tests/6_sd_jwt.ts rename to bindings/wasm/identity_wasm/examples/src/tests/6_sd_jwt.ts diff --git a/bindings/wasm/examples/src/tests/7_revoke_vc.ts b/bindings/wasm/identity_wasm/examples/src/tests/7_revoke_vc.ts similarity index 100% rename from bindings/wasm/examples/src/tests/7_revoke_vc.ts rename to bindings/wasm/identity_wasm/examples/src/tests/7_revoke_vc.ts diff --git a/bindings/wasm/examples/src/tests/7_status_list_2021.ts b/bindings/wasm/identity_wasm/examples/src/tests/7_status_list_2021.ts similarity index 100% rename from bindings/wasm/examples/src/tests/7_status_list_2021.ts rename to bindings/wasm/identity_wasm/examples/src/tests/7_status_list_2021.ts diff --git a/bindings/wasm/examples/src/tests/8_zkp.ts b/bindings/wasm/identity_wasm/examples/src/tests/8_zkp.ts similarity index 100% rename from bindings/wasm/examples/src/tests/8_zkp.ts rename to bindings/wasm/identity_wasm/examples/src/tests/8_zkp.ts diff --git a/bindings/wasm/examples/src/tests/9_zkp_revocation.ts b/bindings/wasm/identity_wasm/examples/src/tests/9_zkp_revocation.ts similarity index 100% rename from bindings/wasm/examples/src/tests/9_zkp_revocation.ts rename to bindings/wasm/identity_wasm/examples/src/tests/9_zkp_revocation.ts diff --git a/bindings/wasm/identity_wasm/examples/src/util.ts b/bindings/wasm/identity_wasm/examples/src/util.ts new file mode 100644 index 0000000000..45fb2e2e97 --- /dev/null +++ b/bindings/wasm/identity_wasm/examples/src/util.ts @@ -0,0 +1,92 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { + IdentityClient, + IdentityClientReadOnly, + IotaDocument, + JwkMemStore, + JwsAlgorithm, + KeyIdMemStore, + MethodScope, + Storage, + StorageSigner, +} from "@iota/identity-wasm/node"; +import { IotaClient } from "@iota/iota-sdk/client"; +import { getFaucetHost, requestIotaFromFaucetV0 } from "@iota/iota-sdk/faucet"; + +export const IOTA_IDENTITY_PKG_ID = process.env.IOTA_IDENTITY_PKG_ID + || "0x7a67dd504eb1291958495c71a07d20985951648dd5ebf01ac921a50257346818"; +export const NETWORK_NAME_FAUCET = process.env.NETWORK_NAME_FAUCET || "localnet"; +export const NETWORK_URL = process.env.NETWORK_URL || "http://127.0.0.1:9000"; +export const TEST_GAS_BUDGET = BigInt(50_000_000); + +export function getMemstorage(): Storage { + return new Storage(new JwkMemStore(), new KeyIdMemStore()); +} + +export async function createDocumentForNetwork(storage: Storage, network: string): Promise<[IotaDocument, string]> { + // Create a new DID document with a placeholder DID. + const unpublished = new IotaDocument(network); + + const verificationMethodFragment = await unpublished.generateMethod( + storage, + JwkMemStore.ed25519KeyType(), + JwsAlgorithm.EdDSA, + "#key-1", + MethodScope.VerificationMethod(), + ); + + return [unpublished, verificationMethodFragment]; +} + +export async function getFundedClient(storage: Storage): Promise { + if (!IOTA_IDENTITY_PKG_ID) { + throw new Error(`IOTA_IDENTITY_PKG_ID env variable must be provided to run the examples`); + } + + const iotaClient = new IotaClient({ url: NETWORK_URL }); + + const identityClientReadOnly = await IdentityClientReadOnly.createWithPkgId( + iotaClient, + IOTA_IDENTITY_PKG_ID, + ); + + // generate new key + let generate = await storage.keyStorage().generate("Ed25519", JwsAlgorithm.EdDSA); + + let publicKeyJwk = generate.jwk().toPublic(); + if (typeof publicKeyJwk === "undefined") { + throw new Error("failed to derive public JWK from generated JWK"); + } + let keyId = generate.keyId(); + + // create signer from storage + let signer = new StorageSigner(storage, keyId, publicKeyJwk); + const identityClient = await IdentityClient.create(identityClientReadOnly, signer); + + await requestIotaFromFaucetV0({ + host: getFaucetHost(NETWORK_NAME_FAUCET), + recipient: identityClient.senderAddress(), + }); + + const balance = await iotaClient.getBalance({ owner: identityClient.senderAddress() }); + if (balance.totalBalance === "0") { + throw new Error("Balance is still 0"); + } else { + console.log(`Received gas from faucet: ${balance.totalBalance} for owner ${identityClient.senderAddress()}`); + } + + return identityClient; +} + +export async function createDidDocument( + identityClient: IdentityClient, + unpublished: IotaDocument, +): Promise { + let txOutput = await identityClient + .publishDidDocument(unpublished) + .execute(identityClient); + + return txOutput.output; +} diff --git a/bindings/wasm/identity_wasm/examples/tsconfig.web.json b/bindings/wasm/identity_wasm/examples/tsconfig.web.json new file mode 100644 index 0000000000..4d96c67e9d --- /dev/null +++ b/bindings/wasm/identity_wasm/examples/tsconfig.web.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es2020", + "outDir": "./dist/web", + "baseUrl": "./", + "lib": [ + "ES6", + "dom" + ], + "esModuleInterop": true, + "moduleResolution": "node", + "paths": { + "@iota/identity-wasm/node": [ + "../web" + ], + } + }, + "exclude": [ + "tests" + ] +} diff --git a/bindings/wasm/lib/append_functions.ts b/bindings/wasm/identity_wasm/lib/append_functions.ts similarity index 99% rename from bindings/wasm/lib/append_functions.ts rename to bindings/wasm/identity_wasm/lib/append_functions.ts index 7674a247a6..74fc9977e9 100644 --- a/bindings/wasm/lib/append_functions.ts +++ b/bindings/wasm/identity_wasm/lib/append_functions.ts @@ -1,4 +1,5 @@ import { CoreDID, CoreDocument, IotaDID, IotaDocument, IToCoreDID, IToCoreDocument } from "~identity_wasm"; + type GetCoreDocument = (arg: IToCoreDocument) => CoreDocument; type MaybeGetIotaDocument = (arg: IToCoreDocument) => IotaDocument | void; type GetCoreDidClone = (arg: IToCoreDID) => CoreDID; @@ -8,6 +9,7 @@ declare global { var _maybeGetIotaDocumentInternal: MaybeGetIotaDocument; var _getCoreDidCloneInternal: GetCoreDidClone; } + function _getCoreDocumentInternal(arg: IToCoreDocument): CoreDocument { if (arg instanceof CoreDocument) { return arg._shallowCloneInternal(); diff --git a/bindings/wasm/lib/index.ts b/bindings/wasm/identity_wasm/lib/index.ts similarity index 62% rename from bindings/wasm/lib/index.ts rename to bindings/wasm/identity_wasm/lib/index.ts index dd46503528..2deb0c3aca 100644 --- a/bindings/wasm/lib/index.ts +++ b/bindings/wasm/identity_wasm/lib/index.ts @@ -2,9 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import "./append_functions.js"; -export * from "./iota_identity_client.js"; + export * from "./jose"; export * from "./jwk_storage"; export * from "./key_id_storage"; export * from "~identity_wasm"; + +// keep this export last to override the original `Resolver` from `identity_wasm` in the exports +export { Resolver } from "./resolver"; diff --git a/bindings/wasm/lib/jose/ec_curve.ts b/bindings/wasm/identity_wasm/lib/jose/ec_curve.ts similarity index 100% rename from bindings/wasm/lib/jose/ec_curve.ts rename to bindings/wasm/identity_wasm/lib/jose/ec_curve.ts diff --git a/bindings/wasm/lib/jose/ed_curve.ts b/bindings/wasm/identity_wasm/lib/jose/ed_curve.ts similarity index 100% rename from bindings/wasm/lib/jose/ed_curve.ts rename to bindings/wasm/identity_wasm/lib/jose/ed_curve.ts diff --git a/bindings/wasm/lib/jose/index.ts b/bindings/wasm/identity_wasm/lib/jose/index.ts similarity index 100% rename from bindings/wasm/lib/jose/index.ts rename to bindings/wasm/identity_wasm/lib/jose/index.ts diff --git a/bindings/wasm/lib/jose/jwk_operation.ts b/bindings/wasm/identity_wasm/lib/jose/jwk_operation.ts similarity index 100% rename from bindings/wasm/lib/jose/jwk_operation.ts rename to bindings/wasm/identity_wasm/lib/jose/jwk_operation.ts diff --git a/bindings/wasm/lib/jose/jwk_type.ts b/bindings/wasm/identity_wasm/lib/jose/jwk_type.ts similarity index 100% rename from bindings/wasm/lib/jose/jwk_type.ts rename to bindings/wasm/identity_wasm/lib/jose/jwk_type.ts diff --git a/bindings/wasm/lib/jose/jwk_use.ts b/bindings/wasm/identity_wasm/lib/jose/jwk_use.ts similarity index 100% rename from bindings/wasm/lib/jose/jwk_use.ts rename to bindings/wasm/identity_wasm/lib/jose/jwk_use.ts diff --git a/bindings/wasm/lib/jose/jws_algorithm.ts b/bindings/wasm/identity_wasm/lib/jose/jws_algorithm.ts similarity index 100% rename from bindings/wasm/lib/jose/jws_algorithm.ts rename to bindings/wasm/identity_wasm/lib/jose/jws_algorithm.ts diff --git a/bindings/wasm/lib/jwk_storage.ts b/bindings/wasm/identity_wasm/lib/jwk_storage.ts similarity index 100% rename from bindings/wasm/lib/jwk_storage.ts rename to bindings/wasm/identity_wasm/lib/jwk_storage.ts index 235abcc8ce..97a27b2123 100644 --- a/bindings/wasm/lib/jwk_storage.ts +++ b/bindings/wasm/identity_wasm/lib/jwk_storage.ts @@ -18,10 +18,6 @@ export class JwkMemStore implements JwkStorage { return "Ed25519"; } - private _get_key(keyId: string): Jwk | undefined { - return this._keys.get(keyId); - } - public async generate(keyType: string, algorithm: JwsAlgorithm): Promise { if (keyType !== JwkMemStore.ed25519KeyType()) { throw new Error(`unsupported key type ${keyType}`); @@ -91,6 +87,10 @@ export class JwkMemStore implements JwkStorage { public count(): number { return this._keys.size; } + + private _get_key(keyId: string): Jwk | undefined { + return this._keys.get(keyId); + } } // Encodes a Ed25519 keypair into a Jwk. diff --git a/bindings/wasm/lib/key_id_storage.ts b/bindings/wasm/identity_wasm/lib/key_id_storage.ts similarity index 100% rename from bindings/wasm/lib/key_id_storage.ts rename to bindings/wasm/identity_wasm/lib/key_id_storage.ts diff --git a/bindings/wasm/identity_wasm/lib/resolver.ts b/bindings/wasm/identity_wasm/lib/resolver.ts new file mode 100644 index 0000000000..a0e207b559 --- /dev/null +++ b/bindings/wasm/identity_wasm/lib/resolver.ts @@ -0,0 +1,38 @@ +// Copyright 2021-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { CoreDocument, IToCoreDocument, Resolver as ResolverInner } from "~identity_wasm"; + +// `Resolver` type below acts the same as the "normal" resolver from `~identity_wasm` +// with the difference being that the `Resolver` here allows to pass generic type params to +// the constructor to specify the types expected to be returned by the `resolve` function. + +/** + * Convenience type for resolving DID documents from different DID methods. + * + * DID documents resolved with `resolve` will have the type specified as generic type parameter T. + * With the default being `CoreDocument | IToCoreDocument`. + * + * Also provides methods for resolving DID Documents associated with + * verifiable {@link identity_wasm/node/identity_wasm.Credential | Credential}s + * and {@link identity_wasm/node/identity_wasm.Presentation | Presentation}s. + * + * # Configuration + * + * The resolver will only be able to resolve DID documents for methods it has been configured for in the constructor. + */ +export class Resolver extends ResolverInner { + /** + * Fetches the DID Document of the given DID. + * + * ### Errors + * + * Errors if the resolver has not been configured to handle the method + * corresponding to the given DID or the resolution process itself fails. + * @param {string} did + * @returns {Promise} + */ + async resolve(did: string): Promise { + return super.resolve(did) as unknown as T; + } +} diff --git a/bindings/wasm/identity_wasm/lib/tsconfig.json b/bindings/wasm/identity_wasm/lib/tsconfig.json new file mode 100644 index 0000000000..49e2616ec2 --- /dev/null +++ b/bindings/wasm/identity_wasm/lib/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../tsconfig.node.json", + "compilerOptions": { + "baseUrl": "./", + "paths": { + "~identity_wasm": [ + "../node/identity_wasm", + "./identity_wasm.js" + ], + "@iota/iota-interaction-ts/": [ + "@iota/iota-interaction-ts/node/" + ], + "../lib": [ + "." + ] + }, + "outDir": "../node", + "declarationDir": "../node" + } +} diff --git a/bindings/wasm/identity_wasm/lib/tsconfig.web.json b/bindings/wasm/identity_wasm/lib/tsconfig.web.json new file mode 100644 index 0000000000..eefe43ba39 --- /dev/null +++ b/bindings/wasm/identity_wasm/lib/tsconfig.web.json @@ -0,0 +1,21 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": "./", + "paths": { + "~identity_wasm": [ + "../web/identity_wasm", + "./identity_wasm.js" + ], + "@iota/iota-interaction-ts/": [ + "@iota/iota-interaction-ts/web/" + ], + "../lib": [ + "." + ] + }, + "outDir": "../web", + "declarationDir": "../web", + "module": "ES2020" + } +} diff --git a/bindings/wasm/package-lock.json b/bindings/wasm/identity_wasm/package-lock.json similarity index 83% rename from bindings/wasm/package-lock.json rename to bindings/wasm/identity_wasm/package-lock.json index c187513181..261d4954c6 100644 --- a/bindings/wasm/package-lock.json +++ b/bindings/wasm/identity_wasm/package-lock.json @@ -1,15 +1,17 @@ { "name": "@iota/identity-wasm", - "version": "1.5.0", + "version": "1.6.0-alpha.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@iota/identity-wasm", - "version": "1.5.0", + "version": "1.6.0-alpha.2", "license": "Apache-2.0", "dependencies": { + "@iota/iota-interaction-ts": "^0.3.0", "@noble/ed25519": "^1.7.3", + "@noble/hashes": "^1.4.0", "@types/node-fetch": "^2.6.2", "base64-arraybuffer": "^1.0.2", "jose": "^5.9.6", @@ -19,6 +21,7 @@ "@digitalcredentials/did-method-key": "^2.0.3", "@types/jsonwebtoken": "^9.0.7", "@types/mocha": "^9.1.0", + "@types/node": "^22.0.0", "big-integer": "^1.6.51", "copy-webpack-plugin": "^7.0.0", "cypress": "^13.12.0", @@ -27,20 +30,74 @@ "fs-extra": "^10.1.0", "jsdoc-to-markdown": "^7.1.1", "mocha": "^9.2.0", + "rimraf": "^6.0.1", "ts-mocha": "^9.0.2", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "tsconfig-paths": "^4.1.0", "txm": "^8.1.0", - "typedoc": "^0.24.6", - "typedoc-plugin-markdown": "^3.14.0", - "typescript": "^4.7.2", + "typedoc": "^0.27.6", + "typedoc-plugin-markdown": "^4.4.1", + "typescript": "^5.7.3", "wasm-opt": "^1.3.0" }, "engines": { - "node": ">=16" + "node": ">=20" + }, + "peerDependencies": { + "@iota/iota-sdk": "^0.5.0" + } + }, + "../iota_interaction_ts": { + "name": "@iota/iota-interaction-ts", + "version": "0.1.0", + "extraneous": true, + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^22.0.0", + "dprint": "^0.33.0", + "tsconfig-paths": "^4.1.0", + "typescript": "^5.7.3", + "wasm-opt": "^1.4.0" + }, + "engines": { + "node": ">=20" }, "peerDependencies": { - "@iota/sdk-wasm": "^1.0.4" + "@iota/iota-sdk": "^0.5.0" + } + }, + "../iota_interaction_ts/node": { + "name": "@iota/iota-interaction-ts", + "version": "1.4.0", + "extraneous": true, + "license": "Apache-2.0" + }, + "node_modules/@0no-co/graphql.web": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.1.1.tgz", + "integrity": "sha512-F2i3xdycesw78QCOBHmpTn7eaD2iNXGwB2gkfwxcOfBbeauYpr8RBSyJOkDrFtKtVRMclg8Sg3n1ip0ACyUuag==", + "peer": true, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, + "node_modules/@0no-co/graphqlsp": { + "version": "1.12.16", + "resolved": "https://registry.npmjs.org/@0no-co/graphqlsp/-/graphqlsp-1.12.16.tgz", + "integrity": "sha512-B5pyYVH93Etv7xjT6IfB7QtMBdaaC07yjbhN6v8H7KgFStMkPvi+oWYBTibMFRMY89qwc9H8YixXg8SXDVgYWw==", + "peer": true, + "dependencies": { + "@gql.tada/internal": "^1.0.0", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0" } }, "node_modules/@babel/parser": { @@ -228,56 +285,239 @@ "dev": true, "license": "Unlicense" }, - "node_modules/@iota/sdk-wasm": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@iota/sdk-wasm/-/sdk-wasm-1.0.4.tgz", - "integrity": "sha512-sS+9avq4GFgFbDYNX2+sn8H3+rFYRhJiB7aa22T+xIixt4/VuMaHkWCzZSTnTUXeY92M7D0ABYKCF+OlHSLzxg==", + "node_modules/@gerrit0/mini-shiki": { + "version": "1.27.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-1.27.2.tgz", + "integrity": "sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^1.27.2", + "@shikijs/types": "^1.27.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "node_modules/@gql.tada/cli-utils": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@gql.tada/cli-utils/-/cli-utils-1.6.3.tgz", + "integrity": "sha512-jFFSY8OxYeBxdKi58UzeMXG1tdm4FVjXa8WHIi66Gzu9JWtCE6mqom3a8xkmSw+mVaybFW5EN2WXf1WztJVNyQ==", "peer": true, "dependencies": { - "class-transformer": "^0.5.1", - "node-fetch": "^2.6.7", - "qs": "^6.9.7", - "reflect-metadata": "^0.1.13", - "semver": "^7.5.2", - "text-encoding": "^0.7.0" + "@0no-co/graphqlsp": "^1.12.13", + "@gql.tada/internal": "1.0.8", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "@0no-co/graphqlsp": "^1.12.13", + "@gql.tada/svelte-support": "1.0.1", + "@gql.tada/vue-support": "1.0.1", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0" }, + "peerDependenciesMeta": { + "@gql.tada/svelte-support": { + "optional": true + }, + "@gql.tada/vue-support": { + "optional": true + } + } + }, + "node_modules/@gql.tada/internal": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@gql.tada/internal/-/internal-1.0.8.tgz", + "integrity": "sha512-XYdxJhtHC5WtZfdDqtKjcQ4d7R1s0d1rnlSs3OcBEUbYiPoJJfZU7tWsVXuv047Z6msvmr4ompJ7eLSK5Km57g==", + "peer": true, + "dependencies": { + "@0no-co/graphql.web": "^1.0.5" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "peer": true, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@iota/bcs": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@iota/bcs/-/bcs-0.2.1.tgz", + "integrity": "sha512-T+iv5gZhUZP7BiDY7+Ir4MA2rYmyGNZA2b+nxjv219Fp8klFt+l38OWA+1RgJXrCmzuZ+M4hbMAeHhHziURX6Q==", + "peer": true, + "dependencies": { + "bs58": "^6.0.0" + } + }, + "node_modules/@iota/iota-interaction-ts": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@iota/iota-interaction-ts/-/iota-interaction-ts-0.3.0.tgz", + "integrity": "sha512-TAaAvURvHdA19ZTsXhPWb1vMCTtM+OuU44QWaWjjY9qhwV+4e5t5JKEUkdz/3SocI4/Twod4qu0iCk5Yc6NhRA==", "engines": { - "node": ">=16" + "node": ">=20" }, - "optionalDependencies": { - "fsevents": "^2.3.2" + "peerDependencies": { + "@iota/iota-sdk": "^0.5.0" } }, - "node_modules/@iota/sdk-wasm/node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "node_modules/@iota/iota-sdk": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@iota/iota-sdk/-/iota-sdk-0.5.0.tgz", + "integrity": "sha512-ZFg4C5EuHV55fHITKOO6Mg1dLqgojZqJsDsR3SRt8W9Ofzbjt8shlM2uLNRwDpiM7GzTb4UUFcKXpOt//5gEmQ==", "peer": true, "dependencies": { - "side-channel": "^1.0.4" + "@graphql-typed-document-node/core": "^3.2.0", + "@iota/bcs": "0.2.1", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@scure/bip32": "^1.4.0", + "@scure/bip39": "^1.3.0", + "@suchipi/femver": "^1.0.0", + "bech32": "^2.0.0", + "gql.tada": "^1.8.2", + "graphql": "^16.9.0", + "tweetnacl": "^1.0.3", + "valibot": "^0.36.0" }, "engines": { - "node": ">=0.6" + "node": ">=20" + } + }, + "node_modules/@iota/iota-sdk/node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "peer": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", @@ -288,24 +528,38 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -324,6 +578,21 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@noble/curves": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "peer": true, + "dependencies": { + "@noble/hashes": "1.7.1" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/ed25519": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", @@ -335,6 +604,18 @@ } ] }, + "node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -370,6 +651,71 @@ "node": ">= 8" } }, + "node_modules/@scure/base": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.4.tgz", + "integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==", + "peer": true, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", + "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", + "peer": true, + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", + "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", + "peer": true, + "dependencies": { + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.4" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.27.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.27.2.tgz", + "integrity": "sha512-FZYKD1KN7srvpkz4lbGLOYWlyDU4Rd+2RtuKfABTkafAPOFr+J6umfIwY/TzOQqfNtWjL7SAwPAO0dcOraRLaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.27.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "node_modules/@shikijs/types": { + "version": "1.27.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.27.2.tgz", + "integrity": "sha512-DM9OWUyjmdYdnKDpaGB/GEn9XkToyK1tqxuqbmc5PV+5K8WjjwfygL3+cIvbkSw2v1ySwHDgqATq/+98pJ4Kyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz", + "integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==", + "dev": true, + "license": "MIT" + }, "node_modules/@stablelib/binary": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz", @@ -429,6 +775,12 @@ "integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==", "dev": true }, + "node_modules/@suchipi/femver": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@suchipi/femver/-/femver-1.0.0.tgz", + "integrity": "sha512-bprE8+K5V+DPX7q2e2K57ImqNBdfGHDIWaGI5xHxZoxbKOuQZn4wzPiUxOAHnsUr3w3xHrWXwN7gnG/iIuEMIg==", + "peer": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -463,10 +815,11 @@ } }, "node_modules/@types/eslint": { - "version": "8.4.6", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz", - "integrity": "sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@types/estree": "*", @@ -474,10 +827,11 @@ } }, "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@types/eslint": "*", @@ -485,12 +839,23 @@ } }, "node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, + "license": "MIT", "peer": true }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -558,9 +923,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.7.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.18.tgz", - "integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==" + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } }, "node_modules/@types/node-fetch": { "version": "2.6.2", @@ -606,163 +975,178 @@ "dev": true }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, + "license": "Apache-2.0", "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -771,6 +1155,7 @@ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, + "license": "BSD-3-Clause", "peer": true }, "node_modules/@xtuc/long": { @@ -778,13 +1163,15 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, + "license": "Apache-2.0", "peer": true }, "node_modules/acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -792,16 +1179,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "peer": true, - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-walk": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", @@ -840,6 +1217,51 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -903,13 +1325,6 @@ "node": ">=8" } }, - "node_modules/ansi-sequence-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", - "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", - "dev": true, - "license": "MIT" - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1075,6 +1490,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base-x": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz", + "integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==", + "peer": true + }, "node_modules/base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", @@ -1135,6 +1556,12 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "peer": true + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -1203,9 +1630,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "funding": [ { @@ -1215,14 +1642,19 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -1231,6 +1663,15 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "peer": true, + "dependencies": { + "base-x": "^5.0.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1306,6 +1747,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -1327,9 +1769,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001457", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001457.tgz", - "integrity": "sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA==", + "version": "1.0.30001692", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", + "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", "dev": true, "funding": [ { @@ -1339,8 +1781,13 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], + "license": "CC-BY-4.0", "peer": true }, "node_modules/caseless": { @@ -1445,10 +1892,11 @@ } }, "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6.0" @@ -1469,12 +1917,6 @@ "node": ">=8" } }, - "node_modules/class-transformer": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "peer": true - }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -1764,9 +2206,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -1849,6 +2291,7 @@ "resolved": "https://registry.npmjs.org/cypress-multi-reporters/-/cypress-multi-reporters-1.6.4.tgz", "integrity": "sha512-3xU2t6pZjZy/ORHaCvci5OT1DAboS4UuMMM8NBAizeb2C9qmHt+cgAjXgurazkwkPRdO7ccK39M5ZaPCju0r6A==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "debug": "^4.3.4", @@ -2184,6 +2627,12 @@ "dprint": "bin.js" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -2212,10 +2661,11 @@ "license": "Unlicense" }, "node_modules/electron-to-chromium": { - "version": "1.4.304", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.304.tgz", - "integrity": "sha512-6c8M+ojPgDIXN2NyfGn8oHASXYnayj+gSEnGeLMKb9zjsySeVB/j7KkNAAG9yDcv8gNlhvFg5REa1N/kQU6pgA==", + "version": "1.5.80", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.80.tgz", + "integrity": "sha512-LTrKpW0AqIuHwmlVNV+cjFYTnXtM9K37OGhpe0ZI10ScPSxqVSryZHIY3WnCS5NSYbBODRTZyhRMS2h5FAEqAw==", "dev": true, + "license": "ISC", "peer": true }, "node_modules/emoji-regex": { @@ -2243,10 +2693,11 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz", - "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -2279,17 +2730,19 @@ } }, "node_modules/es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2308,6 +2761,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "license": "BSD-2-Clause", "peer": true, "dependencies": { "esrecurse": "^4.3.0", @@ -2322,6 +2776,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "peer": true, "dependencies": { "estraverse": "^5.2.0" @@ -2335,6 +2790,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "peer": true, "engines": { "node": ">=4.0" @@ -2345,6 +2801,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, + "license": "BSD-2-Clause", "peer": true, "engines": { "node": ">=4.0" @@ -2361,6 +2818,7 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=0.8.x" @@ -2464,6 +2922,24 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", + "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -2577,6 +3053,34 @@ "flat": "cli.js" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -2644,6 +3148,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -2656,7 +3161,8 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "node_modules/get-caller-file": { "version": "2.0.5", @@ -2671,6 +3177,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -2760,6 +3267,7 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, + "license": "BSD-2-Clause", "peer": true }, "node_modules/global-dirs": { @@ -2797,11 +3305,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gql.tada": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/gql.tada/-/gql.tada-1.8.10.tgz", + "integrity": "sha512-FrvSxgz838FYVPgZHGOSgbpOjhR+yq44rCzww3oOPJYi0OvBJjAgCiP6LEokZIYND2fUTXzQAyLgcvgw1yNP5A==", + "peer": true, + "dependencies": { + "@0no-co/graphql.web": "^1.0.5", + "@0no-co/graphqlsp": "^1.12.13", + "@gql.tada/cli-utils": "1.6.3", + "@gql.tada/internal": "1.0.8" + }, + "bin": { + "gql-tada": "bin/cli.js", + "gql.tada": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } + }, "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphql": { + "version": "16.10.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz", + "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } }, "node_modules/growl": { "version": "1.10.5", @@ -2837,6 +3374,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -2857,6 +3395,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -2868,6 +3407,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -3147,11 +3687,27 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, + "node_modules/jackspeak": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@types/node": "*", @@ -3299,6 +3855,7 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/json-schema": { @@ -3331,13 +3888,6 @@ "node": ">=6" } }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -3433,6 +3983,7 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6.11.5" @@ -3572,6 +4123,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -4451,10 +5003,11 @@ } }, "node_modules/node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/normalize-path": { @@ -4488,6 +5041,7 @@ "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4585,6 +5139,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4612,6 +5172,40 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -4634,10 +5228,11 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, + "license": "ISC", "peer": true }, "node_modules/picomatch": { @@ -4713,6 +5308,16 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.10.4", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", @@ -4867,12 +5472,6 @@ "node": ">=0.10.0" } }, - "node_modules/reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", - "peer": true - }, "node_modules/remark-parse": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz", @@ -4906,6 +5505,17 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -4956,6 +5566,81 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5027,10 +5712,11 @@ "dev": true }, "node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -5048,6 +5734,7 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -5094,23 +5781,11 @@ "node": ">=8" } }, - "node_modules/shiki": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", - "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-sequence-parser": "^1.1.0", - "jsonc-parser": "^3.2.0", - "vscode-oniguruma": "^1.7.0", - "vscode-textmate": "^8.0.0" - } - }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -5254,10 +5929,25 @@ "integrity": "sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/string-width": { + "node_modules/string-width-cjs": { + "name": "string-width", "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", @@ -5283,6 +5973,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -5367,6 +6070,7 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6" @@ -5396,14 +6100,15 @@ "dev": true }, "node_modules/terser": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", - "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "dev": true, + "license": "BSD-2-Clause", "peer": true, "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -5415,17 +6120,18 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", - "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "terser": "^5.14.1" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -5450,21 +6156,84 @@ } }, "node_modules/terser-webpack-plugin/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", - "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "peer": true, "dependencies": { "randombytes": "^2.1.0" @@ -5475,6 +6244,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/test-value": { @@ -5502,13 +6272,6 @@ "node": ">=4" } }, - "node_modules/text-encoding": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", - "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", - "deprecated": "no longer maintained", - "peer": true - }, "node_modules/throttleit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", @@ -5684,10 +6447,11 @@ } }, "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -5818,38 +6582,39 @@ } }, "node_modules/typedoc": { - "version": "0.24.8", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.8.tgz", - "integrity": "sha512-ahJ6Cpcvxwaxfu4KtjA8qZNqS43wYt6JL27wYiIgl1vd38WW/KWX11YuAeZhuz9v+ttrutSsgK+XO1CjL1kA3w==", + "version": "0.27.6", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.6.tgz", + "integrity": "sha512-oBFRoh2Px6jFx366db0lLlihcalq/JzyCVp7Vaq1yphL/tbgx2e+bkpkCgJPunaPvPwoTOXSwasfklWHm7GfAw==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@gerrit0/mini-shiki": "^1.24.0", "lunr": "^2.3.9", - "marked": "^4.3.0", - "minimatch": "^9.0.0", - "shiki": "^0.14.1" + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.6.1" }, "bin": { "typedoc": "bin/typedoc" }, "engines": { - "node": ">= 14.14" + "node": ">= 18" }, "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x" + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x" } }, "node_modules/typedoc-plugin-markdown": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.17.1.tgz", - "integrity": "sha512-QzdU3fj0Kzw2XSdoL15ExLASt2WPqD7FbLeaqwT70+XjKyTshBnUlQA5nNREO1C2P8Uen0CDjsBLMsCQ+zd0lw==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.4.1.tgz", + "integrity": "sha512-fx23nSCvewI9IR8lzIYtzDphETcgTDuxKcmHKGD4lo36oexC+B1k4NaCOY58Snqb4OlE8OXDAGVcQXYYuLRCNw==", "dev": true, "license": "MIT", - "dependencies": { - "handlebars": "^4.7.7" + "engines": { + "node": ">= 18" }, "peerDependencies": { - "typedoc": ">=0.24.0" + "typedoc": "0.27.x" } }, "node_modules/typedoc/node_modules/brace-expansion": { @@ -5862,6 +6627,54 @@ "balanced-match": "^1.0.0" } }, + "node_modules/typedoc/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/typedoc/node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/typedoc/node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/typedoc/node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, "node_modules/typedoc/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -5878,17 +6691,23 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/typescript": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", - "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", + "node_modules/typedoc/node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/typical": { @@ -5922,6 +6741,12 @@ "integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==", "dev": true }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, "node_modules/unified": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", @@ -5973,9 +6798,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "dev": true, "funding": [ { @@ -5985,15 +6810,20 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -6051,6 +6881,12 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/valibot": { + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.36.0.tgz", + "integrity": "sha512-CjF1XN4sUce8sBK9TixrDqFM7RwNkuXdJu174/AwmQUB62QbCQADg5lLe8ldBalFgtj1uKj+pKwDJiNo4Mn+eQ==", + "peer": true + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -6095,20 +6931,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/vscode-oniguruma": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", - "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", - "dev": true, - "license": "MIT" - }, - "node_modules/vscode-textmate": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", - "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", - "dev": true, - "license": "MIT" - }, "node_modules/walk-back": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-5.1.0.tgz", @@ -6133,10 +6955,11 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -6152,35 +6975,35 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.76.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", - "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -6204,6 +7027,7 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=10.13.0" @@ -6290,6 +7114,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6314,7 +7156,21 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } }, "node_modules/yargs-parser": { "version": "20.2.4", @@ -6382,6 +7238,23 @@ } }, "dependencies": { + "@0no-co/graphql.web": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.1.1.tgz", + "integrity": "sha512-F2i3xdycesw78QCOBHmpTn7eaD2iNXGwB2gkfwxcOfBbeauYpr8RBSyJOkDrFtKtVRMclg8Sg3n1ip0ACyUuag==", + "peer": true, + "requires": {} + }, + "@0no-co/graphqlsp": { + "version": "1.12.16", + "resolved": "https://registry.npmjs.org/@0no-co/graphqlsp/-/graphqlsp-1.12.16.tgz", + "integrity": "sha512-B5pyYVH93Etv7xjT6IfB7QtMBdaaC07yjbhN6v8H7KgFStMkPvi+oWYBTibMFRMY89qwc9H8YixXg8SXDVgYWw==", + "peer": true, + "requires": { + "@gql.tada/internal": "^1.0.0", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + } + }, "@babel/parser": { "version": "7.21.1", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.1.tgz", @@ -6530,42 +7403,175 @@ } } }, - "@iota/sdk-wasm": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@iota/sdk-wasm/-/sdk-wasm-1.0.4.tgz", - "integrity": "sha512-sS+9avq4GFgFbDYNX2+sn8H3+rFYRhJiB7aa22T+xIixt4/VuMaHkWCzZSTnTUXeY92M7D0ABYKCF+OlHSLzxg==", + "@gerrit0/mini-shiki": { + "version": "1.27.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-1.27.2.tgz", + "integrity": "sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==", + "dev": true, + "requires": { + "@shikijs/engine-oniguruma": "^1.27.2", + "@shikijs/types": "^1.27.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "@gql.tada/cli-utils": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@gql.tada/cli-utils/-/cli-utils-1.6.3.tgz", + "integrity": "sha512-jFFSY8OxYeBxdKi58UzeMXG1tdm4FVjXa8WHIi66Gzu9JWtCE6mqom3a8xkmSw+mVaybFW5EN2WXf1WztJVNyQ==", "peer": true, "requires": { - "class-transformer": "^0.5.1", - "fsevents": "^2.3.2", - "node-fetch": "^2.6.7", - "qs": "^6.9.7", - "reflect-metadata": "^0.1.13", - "semver": "^7.5.2", - "text-encoding": "^0.7.0" + "@0no-co/graphqlsp": "^1.12.13", + "@gql.tada/internal": "1.0.8", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + } + }, + "@gql.tada/internal": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@gql.tada/internal/-/internal-1.0.8.tgz", + "integrity": "sha512-XYdxJhtHC5WtZfdDqtKjcQ4d7R1s0d1rnlSs3OcBEUbYiPoJJfZU7tWsVXuv047Z6msvmr4ompJ7eLSK5Km57g==", + "peer": true, + "requires": { + "@0no-co/graphql.web": "^1.0.5" + } + }, + "@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "peer": true, + "requires": {} + }, + "@iota/bcs": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@iota/bcs/-/bcs-0.2.1.tgz", + "integrity": "sha512-T+iv5gZhUZP7BiDY7+Ir4MA2rYmyGNZA2b+nxjv219Fp8klFt+l38OWA+1RgJXrCmzuZ+M4hbMAeHhHziURX6Q==", + "peer": true, + "requires": { + "bs58": "^6.0.0" + } + }, + "@iota/iota-interaction-ts": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@iota/iota-interaction-ts/-/iota-interaction-ts-0.3.0.tgz", + "integrity": "sha512-TAaAvURvHdA19ZTsXhPWb1vMCTtM+OuU44QWaWjjY9qhwV+4e5t5JKEUkdz/3SocI4/Twod4qu0iCk5Yc6NhRA==", + "requires": {} + }, + "@iota/iota-sdk": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@iota/iota-sdk/-/iota-sdk-0.5.0.tgz", + "integrity": "sha512-ZFg4C5EuHV55fHITKOO6Mg1dLqgojZqJsDsR3SRt8W9Ofzbjt8shlM2uLNRwDpiM7GzTb4UUFcKXpOt//5gEmQ==", + "peer": true, + "requires": { + "@graphql-typed-document-node/core": "^3.2.0", + "@iota/bcs": "0.2.1", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@scure/bip32": "^1.4.0", + "@scure/bip39": "^1.3.0", + "@suchipi/femver": "^1.0.0", + "bech32": "^2.0.0", + "gql.tada": "^1.8.2", + "graphql": "^16.9.0", + "tweetnacl": "^1.0.3", + "valibot": "^0.36.0" }, "dependencies": { - "qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", - "peer": true, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "peer": true + } + } + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "requires": { - "side-channel": "^1.0.4" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" } } } }, "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, "peer": true, "requires": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + } } }, "@jridgewell/resolve-uri": { @@ -6575,21 +7581,34 @@ "dev": true }, "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "peer": true }, "@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "peer": true, "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + } } }, "@jridgewell/sourcemap-codec": { @@ -6608,11 +7627,25 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@noble/curves": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "peer": true, + "requires": { + "@noble/hashes": "1.7.1" + } + }, "@noble/ed25519": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==" }, + "@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6635,10 +7668,63 @@ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@scure/base": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.4.tgz", + "integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==", + "peer": true + }, + "@scure/bip32": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", + "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", + "peer": true, + "requires": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.2" + } + }, + "@scure/bip39": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", + "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", + "peer": true, + "requires": { + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.4" + } + }, + "@shikijs/engine-oniguruma": { + "version": "1.27.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.27.2.tgz", + "integrity": "sha512-FZYKD1KN7srvpkz4lbGLOYWlyDU4Rd+2RtuKfABTkafAPOFr+J6umfIwY/TzOQqfNtWjL7SAwPAO0dcOraRLaQ==", + "dev": true, + "requires": { + "@shikijs/types": "1.27.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "@shikijs/types": { + "version": "1.27.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.27.2.tgz", + "integrity": "sha512-DM9OWUyjmdYdnKDpaGB/GEn9XkToyK1tqxuqbmc5PV+5K8WjjwfygL3+cIvbkSw2v1ySwHDgqATq/+98pJ4Kyg==", + "dev": true, + "requires": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" } }, + "@shikijs/vscode-textmate": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz", + "integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==", + "dev": true + }, "@stablelib/binary": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz", @@ -6698,6 +7784,12 @@ "integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==", "dev": true }, + "@suchipi/femver": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@suchipi/femver/-/femver-1.0.0.tgz", + "integrity": "sha512-bprE8+K5V+DPX7q2e2K57ImqNBdfGHDIWaGI5xHxZoxbKOuQZn4wzPiUxOAHnsUr3w3xHrWXwN7gnG/iIuEMIg==", + "peer": true + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -6732,9 +7824,9 @@ } }, "@types/eslint": { - "version": "8.4.6", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz", - "integrity": "sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "peer": true, "requires": { @@ -6743,9 +7835,9 @@ } }, "@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "peer": true, "requires": { @@ -6754,12 +7846,21 @@ } }, "@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "peer": true }, + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "requires": { + "@types/unist": "*" + } + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -6826,9 +7927,12 @@ "dev": true }, "@types/node": { - "version": "18.7.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.18.tgz", - "integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==" + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "requires": { + "undici-types": "~6.20.0" + } }, "@types/node-fetch": { "version": "2.6.2", @@ -6874,73 +7978,73 @@ "dev": true }, "@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "peer": true, "requires": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, "peer": true }, "@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, "peer": true }, "@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, "peer": true }, "@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "peer": true, "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, "peer": true }, "@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "peer": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "peer": true, "requires": { @@ -6948,9 +8052,9 @@ } }, "@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "peer": true, "requires": { @@ -6958,79 +8062,79 @@ } }, "@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, "peer": true }, "@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "peer": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "peer": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "peer": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "peer": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "peer": true, "requires": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -7049,19 +8153,11 @@ "peer": true }, "acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true }, - "acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "peer": true, - "requires": {} - }, "acorn-walk": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", @@ -7090,6 +8186,38 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "peer": true, + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "peer": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "peer": true + } + } + }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -7135,12 +8263,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, - "ansi-sequence-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", - "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", - "dev": true - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -7258,6 +8380,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base-x": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz", + "integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==", + "peer": true + }, "base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", @@ -7293,6 +8421,12 @@ "tweetnacl": "^0.14.3" } }, + "bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "peer": true + }, "big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -7349,16 +8483,25 @@ "dev": true }, "browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "peer": true, "requires": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + } + }, + "bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "peer": true, + "requires": { + "base-x": "^5.0.0" } }, "buffer": { @@ -7412,6 +8555,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -7424,9 +8568,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001457", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001457.tgz", - "integrity": "sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA==", + "version": "1.0.30001692", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", + "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", "dev": true, "peer": true }, @@ -7501,9 +8645,9 @@ "dev": true }, "chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "peer": true }, @@ -7513,12 +8657,6 @@ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true }, - "class-transformer": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "peer": true - }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -7747,9 +8885,9 @@ "dev": true }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -8069,6 +9207,12 @@ "yauzl": "=2.10.0" } }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -8097,9 +9241,9 @@ } }, "electron-to-chromium": { - "version": "1.4.304", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.304.tgz", - "integrity": "sha512-6c8M+ojPgDIXN2NyfGn8oHASXYnayj+gSEnGeLMKb9zjsySeVB/j7KkNAAG9yDcv8gNlhvFg5REa1N/kQU6pgA==", + "version": "1.5.80", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.80.tgz", + "integrity": "sha512-LTrKpW0AqIuHwmlVNV+cjFYTnXtM9K37OGhpe0ZI10ScPSxqVSryZHIY3WnCS5NSYbBODRTZyhRMS2h5FAEqAw==", "dev": true, "peer": true }, @@ -8125,9 +9269,9 @@ } }, "enhanced-resolve": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz", - "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", "dev": true, "peer": true, "requires": { @@ -8152,16 +9296,16 @@ "dev": true }, "es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "dev": true, "peer": true }, "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, "escape-string-regexp": { @@ -8295,6 +9439,13 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "fast-uri": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", + "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", + "dev": true, + "peer": true + }, "fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -8382,6 +9533,24 @@ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -8434,12 +9603,14 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "optional": true }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "get-caller-file": { "version": "2.0.5", @@ -8451,6 +9622,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -8544,12 +9716,30 @@ "slash": "^3.0.0" } }, + "gql.tada": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/gql.tada/-/gql.tada-1.8.10.tgz", + "integrity": "sha512-FrvSxgz838FYVPgZHGOSgbpOjhR+yq44rCzww3oOPJYi0OvBJjAgCiP6LEokZIYND2fUTXzQAyLgcvgw1yNP5A==", + "peer": true, + "requires": { + "@0no-co/graphql.web": "^1.0.5", + "@0no-co/graphqlsp": "^1.12.13", + "@gql.tada/cli-utils": "1.6.3", + "@gql.tada/internal": "1.0.8" + } + }, "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "graphql": { + "version": "16.10.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz", + "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==", + "peer": true + }, "growl": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", @@ -8573,6 +9763,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -8586,12 +9777,14 @@ "has-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true }, "he": { "version": "1.2.0", @@ -8765,6 +9958,15 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, + "jackspeak": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2" + } + }, "jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", @@ -8914,12 +10116,6 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, - "jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true - }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -9101,6 +10297,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "requires": { "yallist": "^4.0.0" } @@ -9655,9 +10852,9 @@ } }, "node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, "peer": true }, @@ -9685,7 +10882,8 @@ "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true }, "object-to-spawn-args": { "version": "2.0.1", @@ -9750,6 +10948,12 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9768,6 +10972,30 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "requires": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "dependencies": { + "lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + } + } + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -9787,9 +11015,9 @@ "dev": true }, "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "peer": true }, @@ -9845,6 +11073,12 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true + }, "qs": { "version": "6.10.4", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", @@ -9956,12 +11190,6 @@ } } }, - "reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", - "peer": true - }, "remark-parse": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz", @@ -9988,6 +11216,13 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "peer": true + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -10031,6 +11266,56 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, + "rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "requires": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + } + }, + "minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + } + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10071,9 +11356,9 @@ "dev": true }, "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "requires": { "@types/json-schema": "^7.0.8", @@ -10085,6 +11370,7 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, "requires": { "lru-cache": "^6.0.0" } @@ -10119,22 +11405,11 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, - "shiki": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", - "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", - "dev": true, - "requires": { - "ansi-sequence-parser": "^1.1.0", - "jsonc-parser": "^3.2.0", - "vscode-oniguruma": "^1.7.0", - "vscode-textmate": "^8.0.0" - } - }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -10258,6 +11533,17 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -10267,6 +11553,15 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -10352,14 +11647,14 @@ "dev": true }, "terser": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", - "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "dev": true, "peer": true, "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -10374,34 +11669,77 @@ } }, "terser-webpack-plugin": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", - "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", "dev": true, "peer": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "terser": "^5.14.1" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "dependencies": { "@jridgewell/trace-mapping": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", - "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "peer": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "peer": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "peer": true + }, + "schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dev": true, "peer": true, "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" } }, "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "peer": true, "requires": { @@ -10431,12 +11769,6 @@ } } }, - "text-encoding": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", - "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", - "peer": true - }, "throttleit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", @@ -10568,9 +11900,9 @@ } }, "ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "requires": { "@cspotcode/source-map-support": "^0.8.0", @@ -10657,15 +11989,16 @@ "dev": true }, "typedoc": { - "version": "0.24.8", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.8.tgz", - "integrity": "sha512-ahJ6Cpcvxwaxfu4KtjA8qZNqS43wYt6JL27wYiIgl1vd38WW/KWX11YuAeZhuz9v+ttrutSsgK+XO1CjL1kA3w==", + "version": "0.27.6", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.6.tgz", + "integrity": "sha512-oBFRoh2Px6jFx366db0lLlihcalq/JzyCVp7Vaq1yphL/tbgx2e+bkpkCgJPunaPvPwoTOXSwasfklWHm7GfAw==", "dev": true, "requires": { + "@gerrit0/mini-shiki": "^1.24.0", "lunr": "^2.3.9", - "marked": "^4.3.0", - "minimatch": "^9.0.0", - "shiki": "^0.14.1" + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.6.1" }, "dependencies": { "brace-expansion": { @@ -10677,6 +12010,41 @@ "balanced-match": "^1.0.0" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, + "linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "requires": { + "uc.micro": "^2.0.0" + } + }, + "markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + } + }, + "mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, "minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -10685,23 +12053,26 @@ "requires": { "brace-expansion": "^2.0.1" } + }, + "uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true } } }, "typedoc-plugin-markdown": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.17.1.tgz", - "integrity": "sha512-QzdU3fj0Kzw2XSdoL15ExLASt2WPqD7FbLeaqwT70+XjKyTshBnUlQA5nNREO1C2P8Uen0CDjsBLMsCQ+zd0lw==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.4.1.tgz", + "integrity": "sha512-fx23nSCvewI9IR8lzIYtzDphETcgTDuxKcmHKGD4lo36oexC+B1k4NaCOY58Snqb4OlE8OXDAGVcQXYYuLRCNw==", "dev": true, - "requires": { - "handlebars": "^4.7.7" - } + "requires": {} }, "typescript": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", - "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", - "dev": true + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==" }, "typical": { "version": "2.6.1", @@ -10728,6 +12099,11 @@ "integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==", "dev": true }, + "undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, "unified": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", @@ -10765,14 +12141,14 @@ "dev": true }, "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "dev": true, "peer": true, "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" } }, "uri-js": { @@ -10818,6 +12194,12 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "valibot": { + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.36.0.tgz", + "integrity": "sha512-CjF1XN4sUce8sBK9TixrDqFM7RwNkuXdJu174/AwmQUB62QbCQADg5lLe8ldBalFgtj1uKj+pKwDJiNo4Mn+eQ==", + "peer": true + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -10851,18 +12233,6 @@ "unist-util-stringify-position": "^3.0.0" } }, - "vscode-oniguruma": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", - "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", - "dev": true - }, - "vscode-textmate": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", - "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", - "dev": true - }, "walk-back": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-5.1.0.tgz", @@ -10880,9 +12250,9 @@ } }, "watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "peer": true, "requires": { @@ -10896,35 +12266,34 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "webpack": { - "version": "5.76.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", - "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, "peer": true, "requires": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" } }, @@ -11000,6 +12369,17 @@ "strip-ansi": "^6.0.0" } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -11021,7 +12401,14 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true }, "yargs-parser": { "version": "20.2.4", diff --git a/bindings/wasm/package.json b/bindings/wasm/identity_wasm/package.json similarity index 66% rename from bindings/wasm/package.json rename to bindings/wasm/identity_wasm/package.json index a63db388a7..78bdb27387 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/identity_wasm/package.json @@ -1,7 +1,10 @@ { "name": "@iota/identity-wasm", - "version": "1.5.0", + "version": "1.6.0-alpha.2", + "author": "IOTA Foundation ", "description": "WASM bindings for IOTA Identity - A Self Sovereign Identity Framework implementing the DID and VC standards from W3C. To be used in Javascript/Typescript", + "homepage": "https://www.iota.org", + "license": "Apache-2.0", "repository": { "type": "git", "url": "git+https://github.com/iotaledger/identity.rs.git" @@ -10,13 +13,15 @@ "example": "examples" }, "scripts": { - "build:src": "cargo build --lib --release --target wasm32-unknown-unknown", - "bundle:nodejs": "wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target nodejs --out-dir node && node ./build/node && tsc --project ./lib/tsconfig.json && node ./build/replace_paths ./lib/tsconfig.json node", - "bundle:web": "wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target web --out-dir web && node ./build/web && tsc --project ./lib/tsconfig.web.json && node ./build/replace_paths ./lib/tsconfig.web.json web", - "build:nodejs": "npm run build:src && npm run bundle:nodejs && wasm-opt -O node/identity_wasm_bg.wasm -o node/identity_wasm_bg.wasm", - "build:web": "npm run build:src && npm run bundle:web && wasm-opt -O web/identity_wasm_bg.wasm -o web/identity_wasm_bg.wasm", + "build:src": "cargo build --lib --release --target wasm32-unknown-unknown --target-dir ../target", + "prebundle:nodejs": "rimraf node", + "bundle:nodejs": "wasm-bindgen ../target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target nodejs --out-dir node && node ../build/node identity_wasm && tsc --project ./lib/tsconfig.json && node ../build/replace_paths ./lib/tsconfig.json node identity_wasm", + "prebundle:web": "rimraf web", + "bundle:web": "wasm-bindgen ../target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --target web --out-dir web && node ../build/web identity_wasm && tsc --project ./lib/tsconfig.web.json && node ../build/replace_paths ./lib/tsconfig.web.json web identity_wasm", + "build:nodejs": "npm run build:src && npm run bundle:nodejs", + "build:web": "npm run build:src && npm run bundle:web", "build:docs": "typedoc && npm run fix_docs", - "build:examples:web": "tsc --project ./examples/tsconfig.web.json && node ./build/replace_paths ./examples/tsconfig.web.json ./examples/dist resolve", + "build:examples:web": "tsc --project ./examples/tsconfig.web.json && node ../build/replace_paths ./examples/tsconfig.web.json ./examples/dist identity_wasm resolve", "build": "npm run build:web && npm run build:nodejs && npm run build:docs", "example:node": "ts-node --project tsconfig.node.json -r tsconfig-paths/register ./examples/src/main.ts", "test": "npm run test:unit:node && npm run test:readme && npm run test:node && npm run test:browser:parallel", @@ -32,23 +37,14 @@ "test:unit:node": "ts-mocha -p tsconfig.node.json ./tests/*.ts --parallel --exit", "cypress": "cypress open", "fmt": "dprint fmt", - "fix_docs": "sed -Ei 's/(\\.md?#([^#]*)?)#/\\1/' ./docs/wasm/**/*.md" + "fix_docs": "find ./docs/wasm/ -type f -name '*.md' -exec sed -E -i.bak -e 's/(\\.md?#([^#]*)?)#/\\1/' {} ';' -exec rm {}.bak ';'" }, "config": { "CYPRESS_VERIFY_TIMEOUT": 100000 }, - "contributors": [ - "Jelle Millenaar ", - "Devin Turner ", - "Tensor ", - "Thoralf Müller ", - "Sebastian Heusser " - ], - "license": "Apache-2.0", "bugs": { "url": "https://github.com/iotaledger/identity.rs/issues" }, - "homepage": "https://www.iota.org", "publishConfig": { "access": "public" }, @@ -60,6 +56,7 @@ "@digitalcredentials/did-method-key": "^2.0.3", "@types/jsonwebtoken": "^9.0.7", "@types/mocha": "^9.1.0", + "@types/node": "^22.0.0", "big-integer": "^1.6.51", "copy-webpack-plugin": "^7.0.0", "cypress": "^13.12.0", @@ -68,26 +65,29 @@ "fs-extra": "^10.1.0", "jsdoc-to-markdown": "^7.1.1", "mocha": "^9.2.0", + "rimraf": "^6.0.1", "ts-mocha": "^9.0.2", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "tsconfig-paths": "^4.1.0", "txm": "^8.1.0", - "typedoc": "^0.24.6", - "typedoc-plugin-markdown": "^3.14.0", - "typescript": "^4.7.2", + "typedoc": "^0.27.6", + "typedoc-plugin-markdown": "^4.4.1", + "typescript": "^5.7.3", "wasm-opt": "^1.3.0" }, "dependencies": { + "@iota/iota-interaction-ts": "^0.3.0", "@noble/ed25519": "^1.7.3", + "@noble/hashes": "^1.4.0", "@types/node-fetch": "^2.6.2", "base64-arraybuffer": "^1.0.2", "jose": "^5.9.6", "node-fetch": "^2.6.7" }, "peerDependencies": { - "@iota/sdk-wasm": "^1.0.4" + "@iota/iota-sdk": "^0.5.0" }, "engines": { - "node": ">=16" + "node": ">=20" } } diff --git a/bindings/wasm/proc_typescript/Cargo.toml b/bindings/wasm/identity_wasm/proc_typescript/Cargo.toml similarity index 100% rename from bindings/wasm/proc_typescript/Cargo.toml rename to bindings/wasm/identity_wasm/proc_typescript/Cargo.toml diff --git a/bindings/wasm/proc_typescript/src/lib.rs b/bindings/wasm/identity_wasm/proc_typescript/src/lib.rs similarity index 100% rename from bindings/wasm/proc_typescript/src/lib.rs rename to bindings/wasm/identity_wasm/proc_typescript/src/lib.rs diff --git a/bindings/wasm/rust-toolchain.toml b/bindings/wasm/identity_wasm/rust-toolchain.toml similarity index 50% rename from bindings/wasm/rust-toolchain.toml rename to bindings/wasm/identity_wasm/rust-toolchain.toml index eb46cc977d..825d39b571 100644 --- a/bindings/wasm/rust-toolchain.toml +++ b/bindings/wasm/identity_wasm/rust-toolchain.toml @@ -1,6 +1,5 @@ [toolchain] -# @itsyaasir - Update to latest stable version when wasm-bindgen is updated -channel = "1.81" +channel = "stable" components = ["rustfmt"] targets = ["wasm32-unknown-unknown"] profile = "minimal" diff --git a/bindings/wasm/src/common/imported_document_lock.rs b/bindings/wasm/identity_wasm/src/common/imported_document_lock.rs similarity index 100% rename from bindings/wasm/src/common/imported_document_lock.rs rename to bindings/wasm/identity_wasm/src/common/imported_document_lock.rs diff --git a/bindings/wasm/src/common/mod.rs b/bindings/wasm/identity_wasm/src/common/mod.rs similarity index 100% rename from bindings/wasm/src/common/mod.rs rename to bindings/wasm/identity_wasm/src/common/mod.rs diff --git a/bindings/wasm/src/common/timestamp.rs b/bindings/wasm/identity_wasm/src/common/timestamp.rs similarity index 99% rename from bindings/wasm/src/common/timestamp.rs rename to bindings/wasm/identity_wasm/src/common/timestamp.rs index a6337d91b7..6f9ad99e96 100644 --- a/bindings/wasm/src/common/timestamp.rs +++ b/bindings/wasm/identity_wasm/src/common/timestamp.rs @@ -14,6 +14,7 @@ extern "C" { pub type OptionTimestamp; } +/// A parsed Timestamp. #[wasm_bindgen(js_name = Timestamp, inspectable)] pub struct WasmTimestamp(pub(crate) Timestamp); diff --git a/bindings/wasm/src/common/types.rs b/bindings/wasm/identity_wasm/src/common/types.rs similarity index 99% rename from bindings/wasm/src/common/types.rs rename to bindings/wasm/identity_wasm/src/common/types.rs index 8264e923ce..04d9793519 100644 --- a/bindings/wasm/src/common/types.rs +++ b/bindings/wasm/identity_wasm/src/common/types.rs @@ -50,7 +50,6 @@ extern "C" { #[wasm_bindgen(typescript_type = "Service[]")] pub type ArrayService; - } impl TryFrom for MapStringAny { diff --git a/bindings/wasm/src/common/utils.rs b/bindings/wasm/identity_wasm/src/common/utils.rs similarity index 100% rename from bindings/wasm/src/common/utils.rs rename to bindings/wasm/identity_wasm/src/common/utils.rs diff --git a/bindings/wasm/src/credential/credential.rs b/bindings/wasm/identity_wasm/src/credential/credential.rs similarity index 99% rename from bindings/wasm/src/credential/credential.rs rename to bindings/wasm/identity_wasm/src/credential/credential.rs index 69ef827834..75ff8bdea0 100644 --- a/bindings/wasm/src/credential/credential.rs +++ b/bindings/wasm/identity_wasm/src/credential/credential.rs @@ -29,6 +29,7 @@ use crate::credential::WasmProof; use crate::error::Result; use crate::error::WasmResult; +/// Represents a set of claims describing an entity. #[wasm_bindgen(js_name = Credential, inspectable)] #[derive(Clone, Debug, Eq, PartialEq)] pub struct WasmCredential(pub(crate) Credential); diff --git a/bindings/wasm/src/credential/credential_builder.rs b/bindings/wasm/identity_wasm/src/credential/credential_builder.rs similarity index 100% rename from bindings/wasm/src/credential/credential_builder.rs rename to bindings/wasm/identity_wasm/src/credential/credential_builder.rs diff --git a/bindings/wasm/src/credential/domain_linkage_configuration.rs b/bindings/wasm/identity_wasm/src/credential/domain_linkage_configuration.rs similarity index 100% rename from bindings/wasm/src/credential/domain_linkage_configuration.rs rename to bindings/wasm/identity_wasm/src/credential/domain_linkage_configuration.rs diff --git a/bindings/wasm/src/credential/domain_linkage_credential_builder.rs b/bindings/wasm/identity_wasm/src/credential/domain_linkage_credential_builder.rs similarity index 100% rename from bindings/wasm/src/credential/domain_linkage_credential_builder.rs rename to bindings/wasm/identity_wasm/src/credential/domain_linkage_credential_builder.rs diff --git a/bindings/wasm/src/credential/domain_linkage_validator.rs b/bindings/wasm/identity_wasm/src/credential/domain_linkage_validator.rs similarity index 100% rename from bindings/wasm/src/credential/domain_linkage_validator.rs rename to bindings/wasm/identity_wasm/src/credential/domain_linkage_validator.rs diff --git a/bindings/wasm/src/credential/jpt.rs b/bindings/wasm/identity_wasm/src/credential/jpt.rs similarity index 100% rename from bindings/wasm/src/credential/jpt.rs rename to bindings/wasm/identity_wasm/src/credential/jpt.rs diff --git a/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs b/bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs similarity index 100% rename from bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs rename to bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs b/bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs similarity index 100% rename from bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs rename to bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs b/bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs similarity index 100% rename from bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs rename to bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs b/bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs similarity index 100% rename from bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs rename to bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs b/bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs similarity index 100% rename from bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs rename to bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs b/bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs similarity index 97% rename from bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs rename to bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs index d7ef8b5b89..9f666b79ef 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs +++ b/bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs @@ -6,6 +6,7 @@ use crate::error::WasmResult; use identity_iota::document::verifiable::JwpVerificationOptions; use wasm_bindgen::prelude::*; +/// Holds additional options for verifying a JWP #[wasm_bindgen(js_name = JwpVerificationOptions, inspectable)] #[derive(Clone, Debug, Default)] pub struct WasmJwpVerificationOptions(pub(crate) JwpVerificationOptions); diff --git a/bindings/wasm/src/credential/jpt_credential_validator/mod.rs b/bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/mod.rs similarity index 100% rename from bindings/wasm/src/credential/jpt_credential_validator/mod.rs rename to bindings/wasm/identity_wasm/src/credential/jpt_credential_validator/mod.rs diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs b/bindings/wasm/identity_wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs similarity index 100% rename from bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs rename to bindings/wasm/identity_wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs b/bindings/wasm/identity_wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs similarity index 100% rename from bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs rename to bindings/wasm/identity_wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs b/bindings/wasm/identity_wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs similarity index 100% rename from bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs rename to bindings/wasm/identity_wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs b/bindings/wasm/identity_wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs similarity index 100% rename from bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs rename to bindings/wasm/identity_wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs b/bindings/wasm/identity_wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs similarity index 100% rename from bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs rename to bindings/wasm/identity_wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs b/bindings/wasm/identity_wasm/src/credential/jpt_presentiation_validation/mod.rs similarity index 100% rename from bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs rename to bindings/wasm/identity_wasm/src/credential/jpt_presentiation_validation/mod.rs diff --git a/bindings/wasm/src/credential/jws.rs b/bindings/wasm/identity_wasm/src/credential/jws.rs similarity index 100% rename from bindings/wasm/src/credential/jws.rs rename to bindings/wasm/identity_wasm/src/credential/jws.rs diff --git a/bindings/wasm/src/credential/jwt.rs b/bindings/wasm/identity_wasm/src/credential/jwt.rs similarity index 100% rename from bindings/wasm/src/credential/jwt.rs rename to bindings/wasm/identity_wasm/src/credential/jwt.rs diff --git a/bindings/wasm/src/credential/jwt_credential_validation/decoded_jwt_credential.rs b/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/decoded_jwt_credential.rs similarity index 100% rename from bindings/wasm/src/credential/jwt_credential_validation/decoded_jwt_credential.rs rename to bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/decoded_jwt_credential.rs diff --git a/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs b/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs similarity index 100% rename from bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs rename to bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs diff --git a/bindings/wasm/src/credential/jwt_credential_validation/kb_validation_options.rs b/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/kb_validation_options.rs similarity index 100% rename from bindings/wasm/src/credential/jwt_credential_validation/kb_validation_options.rs rename to bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/kb_validation_options.rs diff --git a/bindings/wasm/src/credential/jwt_credential_validation/mod.rs b/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/mod.rs similarity index 100% rename from bindings/wasm/src/credential/jwt_credential_validation/mod.rs rename to bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/mod.rs diff --git a/bindings/wasm/src/credential/jwt_credential_validation/options.rs b/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/options.rs similarity index 100% rename from bindings/wasm/src/credential/jwt_credential_validation/options.rs rename to bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/options.rs diff --git a/bindings/wasm/src/credential/jwt_credential_validation/sd_jwt_validator.rs b/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/sd_jwt_validator.rs similarity index 100% rename from bindings/wasm/src/credential/jwt_credential_validation/sd_jwt_validator.rs rename to bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/sd_jwt_validator.rs diff --git a/bindings/wasm/src/credential/jwt_credential_validation/unknown_credential.rs b/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/unknown_credential.rs similarity index 100% rename from bindings/wasm/src/credential/jwt_credential_validation/unknown_credential.rs rename to bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/unknown_credential.rs diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs b/bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs similarity index 100% rename from bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs rename to bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs b/bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs similarity index 100% rename from bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs rename to bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs b/bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/mod.rs similarity index 100% rename from bindings/wasm/src/credential/jwt_presentation_validation/mod.rs rename to bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/mod.rs diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/options.rs b/bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/options.rs similarity index 100% rename from bindings/wasm/src/credential/jwt_presentation_validation/options.rs rename to bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/options.rs diff --git a/bindings/wasm/src/credential/linked_domain_service.rs b/bindings/wasm/identity_wasm/src/credential/linked_domain_service.rs similarity index 100% rename from bindings/wasm/src/credential/linked_domain_service.rs rename to bindings/wasm/identity_wasm/src/credential/linked_domain_service.rs index 8bdf5bf0c4..e51a4f6620 100644 --- a/bindings/wasm/src/credential/linked_domain_service.rs +++ b/bindings/wasm/identity_wasm/src/credential/linked_domain_service.rs @@ -16,11 +16,11 @@ use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; +/// A service wrapper for a +/// [Linked Domain Service Endpoint](https://identity.foundation/.well-known/resources/did-configuration/#linked-domain-service-endpoint). #[wasm_bindgen(js_name = LinkedDomainService, inspectable)] pub struct WasmLinkedDomainService(LinkedDomainService); -/// A service wrapper for a -/// [Linked Domain Service Endpoint](https://identity.foundation/.well-known/resources/did-configuration/#linked-domain-service-endpoint). #[wasm_bindgen(js_class = LinkedDomainService)] impl WasmLinkedDomainService { /// Constructs a new {@link LinkedDomainService} that wraps a spec compliant [Linked Domain Service Endpoint](https://identity.foundation/.well-known/resources/did-configuration/#linked-domain-service-endpoint). diff --git a/bindings/wasm/src/credential/linked_verifiable_presentation_service.rs b/bindings/wasm/identity_wasm/src/credential/linked_verifiable_presentation_service.rs similarity index 96% rename from bindings/wasm/src/credential/linked_verifiable_presentation_service.rs rename to bindings/wasm/identity_wasm/src/credential/linked_verifiable_presentation_service.rs index 1033316cc7..ec7d8f3a3d 100644 --- a/bindings/wasm/src/credential/linked_verifiable_presentation_service.rs +++ b/bindings/wasm/identity_wasm/src/credential/linked_verifiable_presentation_service.rs @@ -16,6 +16,7 @@ use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; +/// A service wrapper for a [Linked Verifiable Presentation Service Endpoint](https://identity.foundation/linked-vp/#linked-verifiable-presentation-service-endpoint). #[wasm_bindgen(js_name = LinkedVerifiablePresentationService, inspectable)] pub struct WasmLinkedVerifiablePresentationService(LinkedVerifiablePresentationService); diff --git a/bindings/wasm/src/credential/mod.rs b/bindings/wasm/identity_wasm/src/credential/mod.rs similarity index 100% rename from bindings/wasm/src/credential/mod.rs rename to bindings/wasm/identity_wasm/src/credential/mod.rs diff --git a/bindings/wasm/src/credential/options.rs b/bindings/wasm/identity_wasm/src/credential/options.rs similarity index 100% rename from bindings/wasm/src/credential/options.rs rename to bindings/wasm/identity_wasm/src/credential/options.rs diff --git a/bindings/wasm/src/credential/presentation/mod.rs b/bindings/wasm/identity_wasm/src/credential/presentation/mod.rs similarity index 100% rename from bindings/wasm/src/credential/presentation/mod.rs rename to bindings/wasm/identity_wasm/src/credential/presentation/mod.rs diff --git a/bindings/wasm/src/credential/presentation/presentation.rs b/bindings/wasm/identity_wasm/src/credential/presentation/presentation.rs similarity index 98% rename from bindings/wasm/src/credential/presentation/presentation.rs rename to bindings/wasm/identity_wasm/src/credential/presentation/presentation.rs index d2a3ce2fc3..5c2b5ca9a8 100644 --- a/bindings/wasm/src/credential/presentation/presentation.rs +++ b/bindings/wasm/identity_wasm/src/credential/presentation/presentation.rs @@ -21,6 +21,7 @@ use crate::credential::WasmUnknownCredentialContainer; use crate::error::Result; use crate::error::WasmResult; +/// Represents a bundle of one or more {@link Credential}s. #[wasm_bindgen(js_name = Presentation, inspectable)] pub struct WasmPresentation(pub(crate) Presentation); diff --git a/bindings/wasm/src/credential/presentation/presentation_builder.rs b/bindings/wasm/identity_wasm/src/credential/presentation/presentation_builder.rs similarity index 100% rename from bindings/wasm/src/credential/presentation/presentation_builder.rs rename to bindings/wasm/identity_wasm/src/credential/presentation/presentation_builder.rs diff --git a/bindings/wasm/src/credential/proof.rs b/bindings/wasm/identity_wasm/src/credential/proof.rs similarity index 100% rename from bindings/wasm/src/credential/proof.rs rename to bindings/wasm/identity_wasm/src/credential/proof.rs diff --git a/bindings/wasm/src/credential/revocation/mod.rs b/bindings/wasm/identity_wasm/src/credential/revocation/mod.rs similarity index 100% rename from bindings/wasm/src/credential/revocation/mod.rs rename to bindings/wasm/identity_wasm/src/credential/revocation/mod.rs diff --git a/bindings/wasm/src/credential/revocation/status_list_2021/credential.rs b/bindings/wasm/identity_wasm/src/credential/revocation/status_list_2021/credential.rs similarity index 99% rename from bindings/wasm/src/credential/revocation/status_list_2021/credential.rs rename to bindings/wasm/identity_wasm/src/credential/revocation/status_list_2021/credential.rs index d440dc8814..6bedf8516e 100644 --- a/bindings/wasm/src/credential/revocation/status_list_2021/credential.rs +++ b/bindings/wasm/identity_wasm/src/credential/revocation/status_list_2021/credential.rs @@ -53,7 +53,9 @@ impl From for CredentialStatus { #[wasm_bindgen(js_name = StatusPurpose)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub enum WasmStatusPurpose { + /// Used for revocation. Revocation = 0, + /// Used for suspension. Suspension = 1, } diff --git a/bindings/wasm/src/credential/revocation/status_list_2021/entry.rs b/bindings/wasm/identity_wasm/src/credential/revocation/status_list_2021/entry.rs similarity index 97% rename from bindings/wasm/src/credential/revocation/status_list_2021/entry.rs rename to bindings/wasm/identity_wasm/src/credential/revocation/status_list_2021/entry.rs index 85ecb2eafe..03df803fb5 100644 --- a/bindings/wasm/src/credential/revocation/status_list_2021/entry.rs +++ b/bindings/wasm/identity_wasm/src/credential/revocation/status_list_2021/entry.rs @@ -57,7 +57,7 @@ impl WasmStatusList2021Entry { self.0.status_list_credential().to_string() } - /// Downcasts {@link this} to {@link Status} + /// Downcasts {@link StatusList2021Entry} to {@link Status} #[wasm_bindgen(js_name = "toStatus")] pub fn to_status(self) -> Result { Ok( diff --git a/bindings/wasm/src/credential/revocation/status_list_2021/mod.rs b/bindings/wasm/identity_wasm/src/credential/revocation/status_list_2021/mod.rs similarity index 100% rename from bindings/wasm/src/credential/revocation/status_list_2021/mod.rs rename to bindings/wasm/identity_wasm/src/credential/revocation/status_list_2021/mod.rs diff --git a/bindings/wasm/src/credential/revocation/status_list_2021/status_list.rs b/bindings/wasm/identity_wasm/src/credential/revocation/status_list_2021/status_list.rs similarity index 100% rename from bindings/wasm/src/credential/revocation/status_list_2021/status_list.rs rename to bindings/wasm/identity_wasm/src/credential/revocation/status_list_2021/status_list.rs diff --git a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs b/bindings/wasm/identity_wasm/src/credential/revocation/validity_timeframe_2024/mod.rs similarity index 100% rename from bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs rename to bindings/wasm/identity_wasm/src/credential/revocation/validity_timeframe_2024/mod.rs diff --git a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs b/bindings/wasm/identity_wasm/src/credential/revocation/validity_timeframe_2024/status.rs similarity index 100% rename from bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs rename to bindings/wasm/identity_wasm/src/credential/revocation/validity_timeframe_2024/status.rs diff --git a/bindings/wasm/src/credential/types.rs b/bindings/wasm/identity_wasm/src/credential/types.rs similarity index 100% rename from bindings/wasm/src/credential/types.rs rename to bindings/wasm/identity_wasm/src/credential/types.rs diff --git a/bindings/wasm/src/did/did_jwk.rs b/bindings/wasm/identity_wasm/src/did/did_jwk.rs similarity index 100% rename from bindings/wasm/src/did/did_jwk.rs rename to bindings/wasm/identity_wasm/src/did/did_jwk.rs diff --git a/bindings/wasm/src/did/jws_verification_options.rs b/bindings/wasm/identity_wasm/src/did/jws_verification_options.rs similarity index 96% rename from bindings/wasm/src/did/jws_verification_options.rs rename to bindings/wasm/identity_wasm/src/did/jws_verification_options.rs index 5ac1aed252..ffb36e1eef 100644 --- a/bindings/wasm/src/did/jws_verification_options.rs +++ b/bindings/wasm/identity_wasm/src/did/jws_verification_options.rs @@ -9,6 +9,7 @@ use wasm_bindgen::prelude::*; use super::WasmDIDUrl; +/// Holds additional options for verifying a JWS with {@link CoreDocument.verifyJws}. #[wasm_bindgen(js_name = JwsVerificationOptions, inspectable)] pub struct WasmJwsVerificationOptions(pub(crate) JwsVerificationOptions); diff --git a/bindings/wasm/src/did/mod.rs b/bindings/wasm/identity_wasm/src/did/mod.rs similarity index 100% rename from bindings/wasm/src/did/mod.rs rename to bindings/wasm/identity_wasm/src/did/mod.rs diff --git a/bindings/wasm/src/did/service.rs b/bindings/wasm/identity_wasm/src/did/service.rs similarity index 100% rename from bindings/wasm/src/did/service.rs rename to bindings/wasm/identity_wasm/src/did/service.rs diff --git a/bindings/wasm/src/did/wasm_core_did.rs b/bindings/wasm/identity_wasm/src/did/wasm_core_did.rs similarity index 99% rename from bindings/wasm/src/did/wasm_core_did.rs rename to bindings/wasm/identity_wasm/src/did/wasm_core_did.rs index 292cb0bb93..1703c00c0d 100644 --- a/bindings/wasm/src/did/wasm_core_did.rs +++ b/bindings/wasm/identity_wasm/src/did/wasm_core_did.rs @@ -151,7 +151,6 @@ extern "C" { // or {@link IotaDID}. #[wasm_bindgen(js_name = _getCoreDidCloneInternal, skip_typescript)] pub fn get_core_did_clone(input: &IToCoreDID) -> WasmCoreDID; - } #[wasm_bindgen(typescript_custom_section)] diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/identity_wasm/src/did/wasm_core_document.rs similarity index 100% rename from bindings/wasm/src/did/wasm_core_document.rs rename to bindings/wasm/identity_wasm/src/did/wasm_core_document.rs diff --git a/bindings/wasm/src/did/wasm_did_url.rs b/bindings/wasm/identity_wasm/src/did/wasm_did_url.rs similarity index 100% rename from bindings/wasm/src/did/wasm_did_url.rs rename to bindings/wasm/identity_wasm/src/did/wasm_did_url.rs diff --git a/bindings/wasm/src/error.rs b/bindings/wasm/identity_wasm/src/error.rs similarity index 91% rename from bindings/wasm/src/error.rs rename to bindings/wasm/identity_wasm/src/error.rs index 8c0effc4c7..e9c8e4f277 100644 --- a/bindings/wasm/src/error.rs +++ b/bindings/wasm/identity_wasm/src/error.rs @@ -109,6 +109,7 @@ impl_wasm_error_from!( identity_iota::credential::KeyBindingJwtError, identity_iota::credential::status_list_2021::StatusListError, identity_iota::credential::status_list_2021::StatusList2021CredentialError, + identity_iota::iota::rebased::Error, identity_iota::sd_jwt_rework::Error ); @@ -185,8 +186,8 @@ impl From for WasmError<'_> { } } -impl From for WasmError<'_> { - fn from(error: identity_iota::iota::block::Error) -> Self { +impl From for WasmError<'_> { + fn from(error: iota_sdk::types::block::Error) -> Self { Self { name: Cow::Borrowed("iota_sdk::types::block::Error"), message: Cow::Owned(error.to_string()), @@ -194,15 +195,6 @@ impl From for WasmError<'_> { } } -impl From for WasmError<'_> { - fn from(value: serde_wasm_bindgen::Error) -> Self { - Self { - name: Cow::Borrowed("JSConversionError"), - message: Cow::Owned(value.to_string()), - } - } -} - impl From for WasmError<'_> { fn from(error: identity_iota::credential::CompoundCredentialValidationError) -> Self { Self { @@ -284,6 +276,24 @@ impl From for WasmError<'_> { } } +impl From for WasmError<'_> { + fn from(error: serde_wasm_bindgen::Error) -> Self { + Self { + name: Cow::Borrowed("serde_wasm_bindgen::Error"), + message: Cow::Owned(ErrorMessage(&error).to_string()), + } + } +} + +impl From for WasmError<'_> { + fn from(error: secret_storage::Error) -> Self { + Self { + name: Cow::Borrowed("secret_storage::Error"), + message: Cow::Owned(ErrorMessage(&error).to_string()), + } + } +} + impl From for WasmError<'_> { fn from(error: identity_iota::credential::sd_jwt_vc::Error) -> Self { Self { @@ -319,6 +329,12 @@ impl JsValueResult { pub fn to_iota_core_error(self) -> StdResult { self.stringify_error().map_err(identity_iota::iota::Error::JsError) } + + pub fn to_iota_client_error(self) -> StdResult { + self + .stringify_error() + .map_err(|e| identity_iota::iota::rebased::Error::FfiError(e.to_string())) + } } /// Consumes the struct and returns a Result<_, String>, leaving an `Ok` value untouched. @@ -359,3 +375,13 @@ impl serde::Deserialize<'a>> From for KeyIdStorageResu }) } } + +impl serde::Deserialize<'a>> From for StdResult { + fn from(result: JsValueResult) -> Self { + result.to_iota_client_error().and_then(|js_value| { + js_value + .into_serde() + .map_err(|e| identity_iota::iota::rebased::Error::FfiError(e.to_string())) + }) + } +} diff --git a/bindings/wasm/src/iota/iota_did.rs b/bindings/wasm/identity_wasm/src/iota/iota_did.rs similarity index 95% rename from bindings/wasm/src/iota/iota_did.rs rename to bindings/wasm/identity_wasm/src/iota/iota_did.rs index cbf126b80d..2c11537241 100644 --- a/bindings/wasm/src/iota/iota_did.rs +++ b/bindings/wasm/identity_wasm/src/iota/iota_did.rs @@ -3,7 +3,6 @@ use identity_iota::did::Error as DIDError; use identity_iota::did::DID; -use identity_iota::iota::block::output::AliasId; use identity_iota::iota::IotaDID; use identity_iota::iota::NetworkName; use wasm_bindgen::prelude::*; @@ -55,9 +54,9 @@ impl WasmIotaDID { /// network name. #[wasm_bindgen(js_name = fromAliasId)] #[allow(non_snake_case)] - pub fn from_alias_id(aliasId: String, network: String) -> Result { + pub fn from_object_id(objectId: String, network: String) -> Result { let network_name: NetworkName = NetworkName::try_from(network).wasm_result()?; - Ok(Self::from(IotaDID::from_alias_id(aliasId.as_ref(), &network_name))) + Ok(Self::from(IotaDID::from_object_id(objectId.as_ref(), &network_name))) } /// Creates a new placeholder {@link IotaDID} with the given network name. @@ -155,8 +154,8 @@ impl WasmIotaDID { /// Returns the hex-encoded AliasId with a '0x' prefix, from the DID tag. #[wasm_bindgen(js_name = toAliasId)] - pub fn to_alias_id(&self) -> String { - AliasId::from(&self.0).to_string() + pub fn to_object_id(&self) -> String { + self.0.to_string() } /// Converts the `DID` into a {@link DIDUrl}, consuming it. diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/identity_wasm/src/iota/iota_document.rs similarity index 91% rename from bindings/wasm/src/iota/iota_document.rs rename to bindings/wasm/identity_wasm/src/iota/iota_document.rs index 1747f82e6e..5a51019042 100644 --- a/bindings/wasm/src/iota/iota_document.rs +++ b/bindings/wasm/identity_wasm/src/iota/iota_document.rs @@ -14,9 +14,6 @@ use identity_iota::credential::JwtPresentationOptions; use identity_iota::credential::Presentation; use identity_iota::did::DIDUrl; -use identity_iota::iota::block::output::dto::AliasOutputDto; -use identity_iota::iota::block::output::AliasOutput; -use identity_iota::iota::block::TryFromDto; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; use identity_iota::iota::NetworkName; @@ -62,7 +59,6 @@ use crate::did::WasmJwsVerificationOptions; use crate::did::WasmService; use crate::error::Result; use crate::error::WasmResult; -use crate::iota::identity_client_ext::WasmAliasOutput; use crate::iota::WasmIotaDID; use crate::iota::WasmIotaDocumentMetadata; use crate::iota::WasmStateMetadataEncoding; @@ -114,6 +110,7 @@ impl IotaDocumentLock { /// Note: All methods that involve reading from this class may potentially raise an error /// if the object is being concurrently modified. #[wasm_bindgen(js_name = IotaDocument, inspectable)] +#[derive(Clone)] pub struct WasmIotaDocument(pub(crate) Rc); #[wasm_bindgen(js_class = IotaDocument)] @@ -419,14 +416,14 @@ impl WasmIotaDocument { // Publishing // =========================================================================== - /// Serializes the document for inclusion in an Alias Output's state metadata + /// Serializes the document for inclusion in an identity's metadata /// with the default {@link StateMetadataEncoding}. #[wasm_bindgen] pub fn pack(&self) -> Result> { self.0.try_read()?.clone().pack().wasm_result() } - /// Serializes the document for inclusion in an Alias Output's state metadata. + /// Serializes the document for inclusion in an identity's metadata. #[wasm_bindgen(js_name = packWithEncoding)] pub fn pack_with_encoding(&self, encoding: WasmStateMetadataEncoding) -> Result> { self @@ -437,64 +434,6 @@ impl WasmIotaDocument { .wasm_result() } - /// Deserializes the document from an Alias Output. - /// - /// If `allowEmpty` is true, this will return an empty DID document marked as `deactivated` - /// if `stateMetadata` is empty. - /// - /// The `tokenSupply` must be equal to the token supply of the network the DID is associated with. - /// - /// NOTE: `did` is required since it is omitted from the serialized DID Document and - /// cannot be inferred from the state metadata. It also indicates the network, which is not - /// encoded in the `AliasId` alone. - #[allow(non_snake_case)] - #[wasm_bindgen(js_name = unpackFromOutput)] - pub fn unpack_from_output( - did: &WasmIotaDID, - aliasOutput: WasmAliasOutput, - allowEmpty: bool, - ) -> Result { - let alias_dto: AliasOutputDto = aliasOutput.into_serde().wasm_result()?; - let alias_output: AliasOutput = AliasOutput::try_from_dto(alias_dto) - .map_err(|err| { - identity_iota::iota::Error::JsError(format!("get_alias_output failed to convert AliasOutputDto: {err}")) - }) - .wasm_result()?; - IotaDocument::unpack_from_output(&did.0, &alias_output, allowEmpty) - .map(WasmIotaDocument::from) - .wasm_result() - } - - /// Returns all DID documents of the Alias Outputs contained in the block's transaction payload - /// outputs, if any. - /// - /// Errors if any Alias Output does not contain a valid or empty DID Document. - #[allow(non_snake_case)] - #[wasm_bindgen(js_name = unpackFromBlock)] - pub fn unpack_from_block(network: String, block: &WasmBlock) -> Result { - let network_name: NetworkName = NetworkName::try_from(network).wasm_result()?; - let block_dto: identity_iota::iota::block::BlockDto = block - .into_serde() - .map_err(|err| { - identity_iota::iota::Error::JsError(format!("unpackFromBlock failed to deserialize BlockDto: {err}")) - }) - .wasm_result()?; - - let block: identity_iota::iota::block::Block = identity_iota::iota::block::Block::try_from_dto(block_dto) - .map_err(|err| identity_iota::iota::Error::JsError(format!("unpackFromBlock failed to convert BlockDto: {err}"))) - .wasm_result()?; - - Ok( - IotaDocument::unpack_from_block(&network_name, &block) - .wasm_result()? - .into_iter() - .map(WasmIotaDocument::from) - .map(JsValue::from) - .collect::() - .unchecked_into::(), - ) - } - // =========================================================================== // Metadata // =========================================================================== @@ -644,7 +583,9 @@ impl WasmIotaDocument { /// Serializes to a plain JS representation. #[wasm_bindgen(js_name = toJSON)] pub fn to_json(&self) -> Result { - JsValue::from_serde(&self.0.try_read()?.as_ref()).wasm_result() + let read_guard = self.0.try_read()?; + let iota_document: &IotaDocument = &read_guard; + JsValue::from_serde(&iota_document).wasm_result() } /// Deserializes an instance from a plain JS representation. @@ -1010,17 +951,9 @@ extern "C" { #[wasm_bindgen(typescript_type = "IotaDID[]")] pub type ArrayIotaDID; + #[wasm_bindgen(typescript_type = "Promise")] + pub type PromiseIotaDocument; + #[wasm_bindgen(typescript_type = "IotaDocument[]")] pub type ArrayIotaDocument; - - // External interface from `@iota/sdk-wasm`, must be deserialized via BlockDto. - #[wasm_bindgen(typescript_type = "Block")] - pub type WasmBlock; - - // External interface from `@iota/sdk-wasm`, must be deserialized via ProtocolParameters. - #[wasm_bindgen(typescript_type = "INodeInfoProtocol")] - pub type INodeInfoProtocol; } - -#[wasm_bindgen(typescript_custom_section)] -const TYPESCRIPT_IMPORTS: &'static str = r#"import type { Block, INodeInfoProtocol } from '~sdk-wasm';"#; diff --git a/bindings/wasm/src/iota/iota_document_metadata.rs b/bindings/wasm/identity_wasm/src/iota/iota_document_metadata.rs similarity index 100% rename from bindings/wasm/src/iota/iota_document_metadata.rs rename to bindings/wasm/identity_wasm/src/iota/iota_document_metadata.rs diff --git a/bindings/wasm/src/iota/iota_metadata_encoding.rs b/bindings/wasm/identity_wasm/src/iota/iota_metadata_encoding.rs similarity index 85% rename from bindings/wasm/src/iota/iota_metadata_encoding.rs rename to bindings/wasm/identity_wasm/src/iota/iota_metadata_encoding.rs index f25d54de78..e0b4b090ec 100644 --- a/bindings/wasm/src/iota/iota_metadata_encoding.rs +++ b/bindings/wasm/identity_wasm/src/iota/iota_metadata_encoding.rs @@ -6,10 +6,12 @@ use serde_repr::Deserialize_repr; use serde_repr::Serialize_repr; use wasm_bindgen::prelude::*; +/// Indicates the encoding of a DID document in state metadata. #[wasm_bindgen(js_name = StateMetadataEncoding)] #[derive(Serialize_repr, Deserialize_repr)] #[repr(u8)] pub enum WasmStateMetadataEncoding { + /// State Metadata encoded as JSON. Json = 0, } diff --git a/bindings/wasm/src/iota/mod.rs b/bindings/wasm/identity_wasm/src/iota/mod.rs similarity index 72% rename from bindings/wasm/src/iota/mod.rs rename to bindings/wasm/identity_wasm/src/iota/mod.rs index fa68380d80..a8f882a79c 100644 --- a/bindings/wasm/src/iota/mod.rs +++ b/bindings/wasm/identity_wasm/src/iota/mod.rs @@ -1,16 +1,13 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -pub(crate) use identity_client::WasmIotaIdentityClient; -pub use identity_client_ext::PromiseIotaDocument; pub use iota_did::WasmIotaDID; pub(crate) use iota_document::IotaDocumentLock; +pub use iota_document::PromiseIotaDocument; pub use iota_document::WasmIotaDocument; pub use iota_document_metadata::WasmIotaDocumentMetadata; pub use iota_metadata_encoding::WasmStateMetadataEncoding; -mod identity_client; -mod identity_client_ext; mod iota_did; mod iota_document; mod iota_document_metadata; diff --git a/bindings/wasm/src/jose/decoded_jws.rs b/bindings/wasm/identity_wasm/src/jose/decoded_jws.rs similarity index 100% rename from bindings/wasm/src/jose/decoded_jws.rs rename to bindings/wasm/identity_wasm/src/jose/decoded_jws.rs diff --git a/bindings/wasm/src/jose/jwk.rs b/bindings/wasm/identity_wasm/src/jose/jwk.rs similarity index 99% rename from bindings/wasm/src/jose/jwk.rs rename to bindings/wasm/identity_wasm/src/jose/jwk.rs index cf20245302..569f33dd74 100644 --- a/bindings/wasm/src/jose/jwk.rs +++ b/bindings/wasm/identity_wasm/src/jose/jwk.rs @@ -20,6 +20,9 @@ use crate::jose::WasmJwkUse; use crate::jose::WasmJwsAlgorithm; use core::ops::Deref; +/// JSON Web Key. +/// +/// [More Info](https://tools.ietf.org/html/rfc7517#section-4) #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(transparent)] #[wasm_bindgen(js_name = Jwk)] diff --git a/bindings/wasm/src/jose/jws_header.rs b/bindings/wasm/identity_wasm/src/jose/jws_header.rs similarity index 100% rename from bindings/wasm/src/jose/jws_header.rs rename to bindings/wasm/identity_wasm/src/jose/jws_header.rs diff --git a/bindings/wasm/src/jose/jwu.rs b/bindings/wasm/identity_wasm/src/jose/jwu.rs similarity index 100% rename from bindings/wasm/src/jose/jwu.rs rename to bindings/wasm/identity_wasm/src/jose/jwu.rs diff --git a/bindings/wasm/src/jose/mod.rs b/bindings/wasm/identity_wasm/src/jose/mod.rs similarity index 100% rename from bindings/wasm/src/jose/mod.rs rename to bindings/wasm/identity_wasm/src/jose/mod.rs diff --git a/bindings/wasm/src/jose/types.rs b/bindings/wasm/identity_wasm/src/jose/types.rs similarity index 100% rename from bindings/wasm/src/jose/types.rs rename to bindings/wasm/identity_wasm/src/jose/types.rs diff --git a/bindings/wasm/src/jpt/encoding.rs b/bindings/wasm/identity_wasm/src/jpt/encoding.rs similarity index 100% rename from bindings/wasm/src/jpt/encoding.rs rename to bindings/wasm/identity_wasm/src/jpt/encoding.rs diff --git a/bindings/wasm/src/jpt/issuer_protected_header.rs b/bindings/wasm/identity_wasm/src/jpt/issuer_protected_header.rs similarity index 100% rename from bindings/wasm/src/jpt/issuer_protected_header.rs rename to bindings/wasm/identity_wasm/src/jpt/issuer_protected_header.rs diff --git a/bindings/wasm/src/jpt/jpt_claims.rs b/bindings/wasm/identity_wasm/src/jpt/jpt_claims.rs similarity index 100% rename from bindings/wasm/src/jpt/jpt_claims.rs rename to bindings/wasm/identity_wasm/src/jpt/jpt_claims.rs diff --git a/bindings/wasm/src/jpt/jwp_issued.rs b/bindings/wasm/identity_wasm/src/jpt/jwp_issued.rs similarity index 100% rename from bindings/wasm/src/jpt/jwp_issued.rs rename to bindings/wasm/identity_wasm/src/jpt/jwp_issued.rs diff --git a/bindings/wasm/src/jpt/jwp_presentation_builder.rs b/bindings/wasm/identity_wasm/src/jpt/jwp_presentation_builder.rs similarity index 100% rename from bindings/wasm/src/jpt/jwp_presentation_builder.rs rename to bindings/wasm/identity_wasm/src/jpt/jwp_presentation_builder.rs diff --git a/bindings/wasm/src/jpt/mod.rs b/bindings/wasm/identity_wasm/src/jpt/mod.rs similarity index 100% rename from bindings/wasm/src/jpt/mod.rs rename to bindings/wasm/identity_wasm/src/jpt/mod.rs diff --git a/bindings/wasm/src/jpt/payload.rs b/bindings/wasm/identity_wasm/src/jpt/payload.rs similarity index 100% rename from bindings/wasm/src/jpt/payload.rs rename to bindings/wasm/identity_wasm/src/jpt/payload.rs diff --git a/bindings/wasm/src/jpt/presentation_protected_header.rs b/bindings/wasm/identity_wasm/src/jpt/presentation_protected_header.rs similarity index 100% rename from bindings/wasm/src/jpt/presentation_protected_header.rs rename to bindings/wasm/identity_wasm/src/jpt/presentation_protected_header.rs diff --git a/bindings/wasm/src/jpt/proof_algorithm.rs b/bindings/wasm/identity_wasm/src/jpt/proof_algorithm.rs similarity index 100% rename from bindings/wasm/src/jpt/proof_algorithm.rs rename to bindings/wasm/identity_wasm/src/jpt/proof_algorithm.rs diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/identity_wasm/src/lib.rs similarity index 91% rename from bindings/wasm/src/lib.rs rename to bindings/wasm/identity_wasm/src/lib.rs index b7a0f08ea5..1c988f0840 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/identity_wasm/src/lib.rs @@ -31,6 +31,9 @@ pub mod sd_jwt_vc; pub mod storage; pub mod verification; +// Currently it's unclear if this module will be removed or can be used for integration or unit tests. +pub(crate) mod rebased; + /// Initializes the console error panic hook for better error messages #[wasm_bindgen(start)] pub fn start() -> Result<(), JsValue> { diff --git a/bindings/wasm/src/macros.rs b/bindings/wasm/identity_wasm/src/macros.rs similarity index 75% rename from bindings/wasm/src/macros.rs rename to bindings/wasm/identity_wasm/src/macros.rs index 26cb197993..b59a1dd499 100644 --- a/bindings/wasm/src/macros.rs +++ b/bindings/wasm/identity_wasm/src/macros.rs @@ -1,6 +1,8 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use wasm_bindgen::prelude::wasm_bindgen; + #[macro_export] macro_rules! log { ($($tt:tt)*) => { @@ -8,6 +10,21 @@ macro_rules! log { } } +/// Log to console utility without the need for web_sys dependency +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console, js_name = log)] + pub fn console_log(s: &str); +} + +/// Logging macro without the need for web_sys dependency +#[macro_export] +macro_rules! console_log { + ($($tt:tt)*) => { + $crate::macros::console_log((format!($($tt)*)).as_str()) + } +} + #[macro_export] macro_rules! impl_wasm_clone { ($wasm_class:ident, $js_class:ident) => { diff --git a/bindings/wasm/identity_wasm/src/rebased/identity.rs b/bindings/wasm/identity_wasm/src/rebased/identity.rs new file mode 100644 index 0000000000..a8ddef35bb --- /dev/null +++ b/bindings/wasm/identity_wasm/src/rebased/identity.rs @@ -0,0 +1,245 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::rc::Rc; + +use identity_iota::iota::rebased::migration::CreateIdentityTx; +use identity_iota::iota::rebased::migration::IdentityBuilder; +use identity_iota::iota::rebased::migration::OnChainIdentity; +use identity_iota::iota::rebased::transaction::TransactionInternal; +use identity_iota::iota::rebased::transaction::TransactionOutputInternal; +use identity_iota::iota::IotaDocument; +use iota_interaction_ts::NativeTransactionBlockResponse; +use tokio::sync::RwLock; +use wasm_bindgen::prelude::*; + +use iota_interaction_ts::bindings::WasmIotaObjectData; + +use crate::error::wasm_error; +use crate::error::Result; +use crate::error::WasmResult; +use crate::iota::WasmIotaDocument; + +use super::proposals::StringCouple; +use super::proposals::WasmConfigChange; +use super::proposals::WasmCreateConfigChangeProposalTx; +use super::proposals::WasmCreateSendProposalTx; +use super::proposals::WasmCreateUpdateDidProposalTx; +use super::WasmIdentityClient; +use super::WasmIotaAddress; + +/// Helper type for `WasmIdentityBuilder::controllers`. +/// Has getters to support `Clone` for serialization +#[derive(Debug)] +#[wasm_bindgen(getter_with_clone)] +pub struct ControllerAndVotingPower(pub WasmIotaAddress, pub u64); + +#[wasm_bindgen(js_class = ControllerAndVotingPower)] +impl ControllerAndVotingPower { + #[wasm_bindgen(constructor)] + pub fn new(address: WasmIotaAddress, voting_power: u64) -> Self { + Self(address, voting_power) + } +} + +#[wasm_bindgen(js_name = OnChainIdentity)] +#[derive(Clone)] +pub struct WasmOnChainIdentity(pub(crate) Rc>); + +#[wasm_bindgen(js_class = OnChainIdentity)] +impl WasmOnChainIdentity { + pub(crate) fn new(identity: OnChainIdentity) -> Self { + Self(Rc::new(RwLock::new(identity))) + } + + #[wasm_bindgen] + pub fn id(&self) -> Result { + Ok(self.0.try_read().wasm_result()?.id().to_string()) + } + + #[wasm_bindgen(js_name = didDocument)] + pub fn did_document(&self) -> Result { + let inner_doc = self.0.try_read().wasm_result()?.did_document().clone(); + Ok(WasmIotaDocument::from(inner_doc)) + } + + #[wasm_bindgen(js_name = isShared)] + pub fn is_shared(&self) -> Result { + Ok(self.0.try_read().wasm_result()?.is_shared()) + } + + #[wasm_bindgen(skip_typescript)] // ts type in custom section below + pub fn proposals(&self) -> Result { + let lock = self.0.try_read().wasm_result()?; + let proposals = lock.proposals(); + serde_wasm_bindgen::to_value(proposals).map_err(wasm_error) + } + + #[wasm_bindgen(js_name = updateDidDocument)] + pub fn update_did_document( + &self, + updated_doc: &WasmIotaDocument, + expiration_epoch: Option, + ) -> WasmCreateUpdateDidProposalTx { + WasmCreateUpdateDidProposalTx::new(self, updated_doc.clone(), expiration_epoch) + } + + #[wasm_bindgen(js_name = deactivateDid)] + pub fn deactivate_did(&self, expiration_epoch: Option) -> WasmCreateUpdateDidProposalTx { + WasmCreateUpdateDidProposalTx::deactivate(self, expiration_epoch) + } + + #[wasm_bindgen(js_name = updateConfig)] + pub fn update_config( + &self, + config: WasmConfigChange, + expiration_epoch: Option, + ) -> WasmCreateConfigChangeProposalTx { + WasmCreateConfigChangeProposalTx::new(self, config, expiration_epoch) + } + + #[wasm_bindgen(js_name = sendAssets)] + pub fn send_assets( + &self, + transfer_map: Vec, + expiration_epoch: Option, + ) -> WasmCreateSendProposalTx { + WasmCreateSendProposalTx::new(self, transfer_map, expiration_epoch) + } + + #[allow(unused)] // API will be updated in the future + #[wasm_bindgen(js_name = getHistory, skip_typescript)] // ts type in custom section below + pub async fn get_history( + &self, + _client: WasmIdentityClient, + _last_version: Option, + _page_size: Option, + ) -> Result { + unimplemented!("WasmOnChainIdentity::get_history"); + // let rs_history = self + // .0 + // .get_history( + // &client.0, + // last_version.map(|lv| into_sdk_type(lv).unwrap()).as_ref(), + // page_size, + // ) + // .await + // .map_err(wasm_error)?; + // serde_wasm_bindgen::to_value(&rs_history).map_err(wasm_error) + } +} + +// Manually add the method to the interface. +#[wasm_bindgen(typescript_custom_section)] +const WASM_ON_CHAIN_IDENTITY_TYPES: &str = r###" + export interface OnChainIdentity { + proposals(): Map; + getHistory(): Map; + } +"###; + +// TODO: remove the following comment and commented out code if we don't run into a rename issue +// -> in case `serde(rename` runs into issues with properties with renamed types still having the +// original type, see [here](https://github.com/madonoharu/tsify/issues/43) for an example +// #[declare] +// pub type ProposalAction = WasmProposalAction; + +#[wasm_bindgen(js_name = IdentityBuilder)] +pub struct WasmIdentityBuilder(pub(crate) IdentityBuilder); + +#[wasm_bindgen(js_class = IdentityBuilder)] +impl WasmIdentityBuilder { + #[wasm_bindgen(constructor)] + pub fn new(did_doc: &WasmIotaDocument) -> Result { + let document: IotaDocument = did_doc.0.try_read().unwrap().clone(); + Ok(WasmIdentityBuilder(IdentityBuilder::new(document))) + } + + pub fn controller(self, address: WasmIotaAddress, voting_power: u64) -> Self { + Self( + self.0.controller( + address + .parse() + .expect("Parameter address could not be parsed into valid IotaAddress"), + voting_power, + ), + ) + } + + pub fn threshold(self, threshold: u64) -> Self { + Self(self.0.threshold(threshold)) + } + + pub fn controllers(self, controllers: Vec) -> Self { + Self( + self.0.controllers( + controllers + .into_iter() + .map(|v| { + ( + v.0 + .parse() + .expect("controller can not be parsed into valid IotaAddress"), + v.1, + ) + }) + .collect::>(), + ), + ) + } + + #[wasm_bindgen] + pub fn finish(self) -> WasmCreateIdentityTx { + WasmCreateIdentityTx::new(self.0.finish()) + } +} + +#[wasm_bindgen(js_name = CreateIdentityTx)] +pub struct WasmCreateIdentityTx { + pub(crate) tx: CreateIdentityTx, + gas_budget: Option, +} + +#[wasm_bindgen(js_class = CreateIdentityTx)] +impl WasmCreateIdentityTx { + fn new(tx: CreateIdentityTx) -> Self { + Self { tx, gas_budget: None } + } + + #[wasm_bindgen(js_name = withGasBudget)] + pub fn with_gas_budget(mut self, budget: u64) -> Self { + self.gas_budget = Some(budget); + self + } + + #[wasm_bindgen(setter, js_name = gasBudget)] + pub fn set_gas_budget(&mut self, budget: u64) { + self.gas_budget = Some(budget); + } + + #[wasm_bindgen] + pub async fn execute(self, client: &WasmIdentityClient) -> Result { + let output = self + .tx + .execute_with_opt_gas_internal(self.gas_budget, &client.0) + .await + .map_err(wasm_error)?; + Ok(WasmTransactionOutputInternalOnChainIdentity(output)) + } +} + +#[wasm_bindgen(js_name = TransactionOutputInternalOnChainIdentity)] +pub struct WasmTransactionOutputInternalOnChainIdentity(pub(crate) TransactionOutputInternal); + +#[wasm_bindgen(js_class = TransactionOutputInternalOnChainIdentity)] +impl WasmTransactionOutputInternalOnChainIdentity { + #[wasm_bindgen(getter)] + pub fn output(&self) -> WasmOnChainIdentity { + WasmOnChainIdentity(Rc::new(RwLock::new(self.0.output.clone()))) + } + + #[wasm_bindgen(getter)] + pub fn response(&self) -> NativeTransactionBlockResponse { + self.0.response.clone_native_response() + } +} diff --git a/bindings/wasm/identity_wasm/src/rebased/mod.rs b/bindings/wasm/identity_wasm/src/rebased/mod.rs new file mode 100644 index 0000000000..95907a37a5 --- /dev/null +++ b/bindings/wasm/identity_wasm/src/rebased/mod.rs @@ -0,0 +1,14 @@ +// Copyright 2020-2022 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod identity; +mod proposals; +mod wasm_identity_client; +mod wasm_identity_client_read_only; + +pub use identity::*; +pub use wasm_identity_client::*; +pub use wasm_identity_client_read_only::*; + +pub type WasmIotaAddress = String; +pub type WasmObjectID = String; diff --git a/bindings/wasm/identity_wasm/src/rebased/proposals/config_change.rs b/bindings/wasm/identity_wasm/src/rebased/proposals/config_change.rs new file mode 100644 index 0000000000..12d1f9b506 --- /dev/null +++ b/bindings/wasm/identity_wasm/src/rebased/proposals/config_change.rs @@ -0,0 +1,335 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::collections::HashSet; +use std::rc::Rc; + +use identity_iota::iota::rebased::migration::Proposal; +use identity_iota::iota::rebased::proposals::ConfigChange; +use identity_iota::iota::rebased::proposals::ProposalResult; +use identity_iota::iota::rebased::proposals::ProposalT; +use identity_iota::iota::rebased::transaction::TransactionInternal; +use identity_iota::iota::rebased::transaction::TransactionOutputInternal; +use iota_interaction_ts::NativeTransactionBlockResponse; +use tokio::sync::RwLock; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::prelude::JsCast; +use wasm_bindgen::JsValue; + +use super::MapStringNumber; +use super::StringSet; +use crate::error::Result; +use crate::error::WasmResult; +use crate::rebased::WasmIdentityClient; +use crate::rebased::WasmOnChainIdentity; + +#[wasm_bindgen(js_name = ConfigChange, inspectable, getter_with_clone)] +pub struct WasmConfigChange { + pub threshold: Option, + #[wasm_bindgen(js_name = controllersToAdd)] + pub controllers_to_add: Option, + #[wasm_bindgen(js_name = controllersToRemove)] + pub controllers_to_remove: Option, + #[wasm_bindgen(js_name = controllersToUpdate)] + pub controllers_to_update: Option, +} + +impl TryFrom for WasmConfigChange { + type Error = JsValue; + fn try_from(value: ConfigChange) -> std::result::Result { + let threshold = value.threshold(); + let controllers_to_add = if value.controllers_to_add().is_empty() { + None + } else { + Some(value.controllers_to_add().try_into()?) + }; + let controllers_to_remove = if value.controllers_to_remove().is_empty() { + None + } else { + Some(value.controllers_to_remove().try_into()?) + }; + let controllers_to_update = if value.controllers_to_update().is_empty() { + None + } else { + Some(value.controllers_to_update().try_into()?) + }; + + Ok(Self { + threshold, + controllers_to_add, + controllers_to_remove, + controllers_to_update, + }) + } +} + +#[wasm_bindgen(js_name = ConfigChangeProposal)] +#[derive(Clone)] +pub struct WasmConfigChangeProposal(pub(crate) Rc>>); + +#[wasm_bindgen(js_class = ConfigChangeProposal)] +impl WasmConfigChangeProposal { + fn new(proposal: Proposal) -> Self { + Self(Rc::new(RwLock::new(proposal))) + } + + #[wasm_bindgen(getter)] + pub fn id(&self) -> Result { + self + .0 + .try_read() + .wasm_result() + .map(|proposal| proposal.id().to_string()) + } + + #[wasm_bindgen(getter)] + pub fn action(&self) -> Result { + self + .0 + .try_read() + .wasm_result() + .and_then(|proposal| proposal.action().clone().try_into()) + } + + #[wasm_bindgen(getter)] + pub fn expiration_epoch(&self) -> Result> { + self + .0 + .try_read() + .wasm_result() + .map(|proposal| proposal.expiration_epoch()) + } + + #[wasm_bindgen(getter)] + pub fn votes(&self) -> Result { + self.0.try_read().wasm_result().map(|proposal| proposal.votes()) + } + + #[wasm_bindgen(getter)] + pub fn voters(&self) -> Result { + let js_set = self + .0 + .try_read() + .wasm_result()? + .voters() + .iter() + .map(ToString::to_string) + .map(js_sys::JsString::from) + .fold(js_sys::Set::default(), |set, value| { + set.add(&value); + set + }) + .unchecked_into(); + + Ok(js_set) + } + + #[wasm_bindgen] + pub fn approve(&self, identity: &WasmOnChainIdentity) -> WasmApproveConfigChangeProposalTx { + WasmApproveConfigChangeProposalTx::new(self, identity) + } + + #[wasm_bindgen(js_name = intoTx)] + pub fn into_tx(self, identity: &WasmOnChainIdentity) -> WasmExecuteConfigChangeProposalTx { + WasmExecuteConfigChangeProposalTx::new(self, identity) + } +} + +#[wasm_bindgen(js_name = ApproveConfigChangeProposalTx)] +pub struct WasmApproveConfigChangeProposalTx { + proposal: WasmConfigChangeProposal, + identity: WasmOnChainIdentity, + gas_budget: Option, +} + +#[wasm_bindgen(js_class = ApproveConfigChangeProposalTx)] +impl WasmApproveConfigChangeProposalTx { + fn new(proposal: &WasmConfigChangeProposal, identity: &WasmOnChainIdentity) -> Self { + Self { + proposal: proposal.clone(), + identity: identity.clone(), + gas_budget: None, + } + } + + #[wasm_bindgen(js_name = withGasBudget)] + pub fn with_gas_budget(mut self, budget: u64) -> Self { + self.gas_budget = Some(budget); + self + } + + #[wasm_bindgen(setter, js_name = gasBudget)] + pub fn set_gas_budget(&mut self, budget: u64) { + self.gas_budget = Some(budget); + } + + #[wasm_bindgen] + pub async fn execute(self, client: &WasmIdentityClient) -> Result { + let identity_ref = self.identity.0.read().await; + self + .proposal + .0 + .write() + .await + .approve(&identity_ref) + .execute_with_opt_gas_internal(self.gas_budget, &client.0) + .await + .wasm_result() + .map(|tx_output| tx_output.response.clone_native_response()) + } +} + +#[wasm_bindgen(js_name = ExecuteConfigChangeProposalTx)] +pub struct WasmExecuteConfigChangeProposalTx { + proposal: WasmConfigChangeProposal, + identity: WasmOnChainIdentity, + gas_budget: Option, +} + +#[wasm_bindgen(js_class = ExecuteConfigChangeProposalTx)] +impl WasmExecuteConfigChangeProposalTx { + fn new(proposal: WasmConfigChangeProposal, identity: &WasmOnChainIdentity) -> Self { + Self { + proposal, + identity: identity.clone(), + gas_budget: None, + } + } + + #[wasm_bindgen(js_name = withGasBudget)] + pub fn with_gas_budget(mut self, budget: u64) -> Self { + self.gas_budget = Some(budget); + self + } + + #[wasm_bindgen(setter, js_name = gasBudget)] + pub fn set_gas_budget(&mut self, budget: u64) { + self.gas_budget = Some(budget); + } + + #[wasm_bindgen] + pub async fn execute(self, client: &WasmIdentityClient) -> Result { + let mut identity_ref = self.identity.0.write().await; + let proposal = Rc::into_inner(self.proposal.0) + .ok_or_else(|| js_sys::Error::new("cannot consume proposal; try to drop all other references to it"))? + .into_inner(); + + proposal + .into_tx(&mut identity_ref, client) + .await + .wasm_result()? + .execute_with_opt_gas_internal(self.gas_budget, client) + .await + .wasm_result() + .map(|tx_output| tx_output.response.clone_native_response()) + } +} + +#[wasm_bindgen(js_name = CreateConfigChangeProposalTxOutput, inspectable, getter_with_clone)] +pub struct WasmCreateConfigChangeProposalTxOutput { + pub output: Option, + pub response: NativeTransactionBlockResponse, +} + +impl From>>> + for WasmCreateConfigChangeProposalTxOutput +{ + fn from(tx_output: TransactionOutputInternal>>) -> Self { + let output = match tx_output.output { + ProposalResult::Pending(proposal) => Some(WasmConfigChangeProposal::new(proposal)), + ProposalResult::Executed(_) => None, + }; + let response = tx_output.response.clone_native_response(); + Self { output, response } + } +} + +#[wasm_bindgen(js_name = CreateConfigChangeProposalTx)] +pub struct WasmCreateConfigChangeProposalTx { + identity: WasmOnChainIdentity, + threshold: Option, + controllers_to_add: Option, + controllers_to_remove: Option, + controllers_to_update: Option, + expiration_epoch: Option, + gas_budget: Option, +} + +#[wasm_bindgen(js_class = CreateConfigChangeProposalTx)] +impl WasmCreateConfigChangeProposalTx { + pub(crate) fn new(identity: &WasmOnChainIdentity, config: WasmConfigChange, expiration_epoch: Option) -> Self { + let WasmConfigChange { + controllers_to_add, + controllers_to_remove, + controllers_to_update, + threshold, + } = config; + Self { + identity: identity.clone(), + threshold, + controllers_to_add, + controllers_to_remove, + controllers_to_update, + expiration_epoch, + gas_budget: None, + } + } + + #[wasm_bindgen(js_name = withGasBudget)] + pub fn with_gas_budget(mut self, budget: u64) -> Self { + self.gas_budget = Some(budget); + self + } + + #[wasm_bindgen(setter, js_name = gasBudget)] + pub fn set_gas_budget(&mut self, budget: u64) { + self.gas_budget = Some(budget); + } + + #[wasm_bindgen] + pub async fn execute(self, client: &WasmIdentityClient) -> Result { + let mut identity_ref = self.identity.0.write().await; + let controllers_to_add = self + .controllers_to_add + .map(HashMap::try_from) + .transpose()? + .unwrap_or_default(); + let controllers_to_remove = self + .controllers_to_remove + .map(HashSet::try_from) + .transpose()? + .unwrap_or_default(); + let controllers_to_update = self + .controllers_to_update + .map(HashMap::try_from) + .transpose()? + .unwrap_or_default(); + let builder = identity_ref + .update_config() + .add_multiple_controllers(controllers_to_add) + .remove_multiple_controllers(controllers_to_remove) + .update_multiple_controllers(controllers_to_update); + + let builder = if let Some(exp) = self.expiration_epoch { + builder.expiration_epoch(exp) + } else { + builder + }; + let builder = if let Some(threshold) = self.threshold { + builder.threshold(threshold) + } else { + builder + }; + + let tx_output = builder + .finish(client) + .await + .wasm_result()? + .execute_with_opt_gas_internal(self.gas_budget, client) + .await + .wasm_result()?; + + Ok(tx_output.into()) + } +} diff --git a/bindings/wasm/identity_wasm/src/rebased/proposals/mod.rs b/bindings/wasm/identity_wasm/src/rebased/proposals/mod.rs new file mode 100644 index 0000000000..e6721728df --- /dev/null +++ b/bindings/wasm/identity_wasm/src/rebased/proposals/mod.rs @@ -0,0 +1,103 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod config_change; +mod send; +mod update_did; + +pub use config_change::*; +pub use send::*; +pub use update_did::*; + +use std::collections::HashMap; +use std::collections::HashSet; + +use identity_iota::iota_interaction::types::base_types::IotaAddress; +use identity_iota::iota_interaction::types::base_types::ObjectID; +use js_sys::JsString; +use js_sys::Reflect; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast as _; +use wasm_bindgen::JsValue; + +#[wasm_bindgen] +extern "C" { + #[derive(Clone)] + #[wasm_bindgen(typescript_type = "Set")] + pub type StringSet; + + #[wasm_bindgen(typescript_type = "[string, string]")] + pub type StringCouple; + + #[derive(Clone)] + #[wasm_bindgen(typescript_type = "Map")] + pub type MapStringNumber; +} + +impl From for (String, String) { + fn from(value: StringCouple) -> Self { + let first = Reflect::get_u32(&value, 0) + .expect("[string, string] has property 0") + .unchecked_into::() + .into(); + let second = Reflect::get_u32(&value, 1) + .expect("[string, string] has property 1") + .unchecked_into::() + .into(); + + (first, second) + } +} + +impl From<(String, String)> for StringCouple { + fn from(value: (String, String)) -> Self { + serde_wasm_bindgen::to_value(&value) + .expect("a string couple can be serialized to JS") + .unchecked_into() + } +} + +impl TryFrom for HashMap { + type Error = JsValue; + fn try_from(value: MapStringNumber) -> Result { + Ok(serde_wasm_bindgen::from_value(value.into())?) + } +} + +impl TryFrom<&'_ HashMap> for MapStringNumber { + type Error = JsValue; + fn try_from(value: &'_ HashMap) -> Result { + let js_value = serde_wasm_bindgen::to_value(value)?; + js_value.dyn_into() + } +} + +impl TryFrom for HashMap { + type Error = JsValue; + fn try_from(value: MapStringNumber) -> Result { + Ok(serde_wasm_bindgen::from_value(value.into())?) + } +} + +impl TryFrom<&'_ HashMap> for MapStringNumber { + type Error = JsValue; + fn try_from(value: &'_ HashMap) -> Result { + let js_value = serde_wasm_bindgen::to_value(value)?; + js_value.dyn_into() + } +} + +impl TryFrom for HashSet { + type Error = JsValue; + fn try_from(value: StringSet) -> Result { + Ok(serde_wasm_bindgen::from_value(value.into())?) + } +} + +impl TryFrom<&'_ HashSet> for StringSet { + type Error = JsValue; + fn try_from(value: &'_ HashSet) -> Result { + let js_value = serde_wasm_bindgen::to_value(value)?; + js_value.dyn_into::() + } +} diff --git a/bindings/wasm/identity_wasm/src/rebased/proposals/send.rs b/bindings/wasm/identity_wasm/src/rebased/proposals/send.rs new file mode 100644 index 0000000000..39507dd9f7 --- /dev/null +++ b/bindings/wasm/identity_wasm/src/rebased/proposals/send.rs @@ -0,0 +1,293 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::rc::Rc; + +use identity_iota::iota::rebased::migration::Proposal; +use identity_iota::iota::rebased::proposals::ProposalResult; +use identity_iota::iota::rebased::proposals::ProposalT; +use identity_iota::iota::rebased::proposals::SendAction; +use identity_iota::iota::rebased::transaction::TransactionInternal; +use identity_iota::iota::rebased::transaction::TransactionOutputInternal; +use iota_interaction_ts::NativeTransactionBlockResponse; +use tokio::sync::RwLock; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::prelude::JsCast; + +use super::StringCouple; +use super::StringSet; +use crate::error::Result; +use crate::error::WasmResult; +use crate::rebased::WasmIdentityClient; +use crate::rebased::WasmOnChainIdentity; + +#[wasm_bindgen(js_name = SendAction)] +#[derive(Clone)] +pub struct WasmSendAction(pub(crate) SendAction); + +#[wasm_bindgen(js_class = SendAction)] +impl WasmSendAction { + #[wasm_bindgen(getter, js_name = objectRecipientMap)] + pub fn object_recipient_map(&self) -> Vec { + self + .0 + .as_ref() + .iter() + .map(|(obj, rec)| (obj.to_string(), rec.to_string()).into()) + .collect() + } +} + +#[wasm_bindgen(js_name = SendProposal)] +#[derive(Clone)] +pub struct WasmProposalSend(pub(crate) Rc>>); + +#[wasm_bindgen(js_class = SendProposal)] +impl WasmProposalSend { + fn new(proposal: Proposal) -> Self { + Self(Rc::new(RwLock::new(proposal))) + } + + #[wasm_bindgen(getter)] + pub fn id(&self) -> Result { + self + .0 + .try_read() + .wasm_result() + .map(|proposal| proposal.id().to_string()) + } + + #[wasm_bindgen(getter)] + pub fn action(&self) -> Result { + self + .0 + .try_read() + .wasm_result() + .map(|proposal| WasmSendAction(proposal.action().clone())) + } + + #[wasm_bindgen(getter)] + pub fn expiration_epoch(&self) -> Result> { + self + .0 + .try_read() + .wasm_result() + .map(|proposal| proposal.expiration_epoch()) + } + + #[wasm_bindgen(getter)] + pub fn votes(&self) -> Result { + self.0.try_read().wasm_result().map(|proposal| proposal.votes()) + } + + #[wasm_bindgen(getter)] + pub fn voters(&self) -> Result { + let js_set = self + .0 + .try_read() + .wasm_result()? + .voters() + .iter() + .map(ToString::to_string) + .map(js_sys::JsString::from) + .fold(js_sys::Set::default(), |set, value| { + set.add(&value); + set + }) + .unchecked_into(); + + Ok(js_set) + } + + #[wasm_bindgen] + pub fn approve(&self, identity: &WasmOnChainIdentity) -> WasmApproveSendProposalTx { + WasmApproveSendProposalTx::new(self, identity) + } + + #[wasm_bindgen(js_name = intoTx)] + pub fn into_tx(self, identity: &WasmOnChainIdentity) -> WasmExecuteSendProposalTx { + WasmExecuteSendProposalTx::new(self, identity) + } +} + +#[wasm_bindgen(js_name = ApproveSendProposalTx)] +pub struct WasmApproveSendProposalTx { + proposal: WasmProposalSend, + identity: WasmOnChainIdentity, + gas_budget: Option, +} + +#[wasm_bindgen(js_class = ApproveSendProposalTx)] +impl WasmApproveSendProposalTx { + fn new(proposal: &WasmProposalSend, identity: &WasmOnChainIdentity) -> Self { + Self { + proposal: proposal.clone(), + identity: identity.clone(), + gas_budget: None, + } + } + + #[wasm_bindgen(js_name = withGasBudget)] + pub fn with_gas_budget(mut self, budget: u64) -> Self { + self.gas_budget = Some(budget); + self + } + + #[wasm_bindgen(setter, js_name = gasBudget)] + pub fn set_gas_budget(&mut self, budget: u64) { + self.gas_budget = Some(budget); + } + + #[wasm_bindgen] + pub async fn execute(self, client: &WasmIdentityClient) -> Result { + let identity_ref = self.identity.0.read().await; + self + .proposal + .0 + .write() + .await + .approve(&identity_ref) + .execute_with_opt_gas_internal(self.gas_budget, &client.0) + .await + .wasm_result() + .map(|tx_output| tx_output.response.clone_native_response()) + } +} + +#[wasm_bindgen(js_name = ExecuteSendProposalTx)] +pub struct WasmExecuteSendProposalTx { + proposal: WasmProposalSend, + identity: WasmOnChainIdentity, + gas_budget: Option, +} + +#[wasm_bindgen(js_class = ExecuteSendProposalTx)] +impl WasmExecuteSendProposalTx { + fn new(proposal: WasmProposalSend, identity: &WasmOnChainIdentity) -> Self { + Self { + proposal, + identity: identity.clone(), + gas_budget: None, + } + } + + #[wasm_bindgen(js_name = withGasBudget)] + pub fn with_gas_budget(mut self, budget: u64) -> Self { + self.gas_budget = Some(budget); + self + } + + #[wasm_bindgen(setter, js_name = gasBudget)] + pub fn set_gas_budget(&mut self, budget: u64) { + self.gas_budget = Some(budget); + } + + #[wasm_bindgen] + pub async fn execute(self, client: &WasmIdentityClient) -> Result { + let mut identity_ref = self.identity.0.write().await; + let proposal = Rc::into_inner(self.proposal.0) + .ok_or_else(|| js_sys::Error::new("cannot consume proposal; try to drop all other references to it"))? + .into_inner(); + + proposal + .into_tx(&mut identity_ref, client) + .await + .wasm_result()? + .execute_with_opt_gas_internal(self.gas_budget, client) + .await + .wasm_result() + .map(|tx_output| tx_output.response.clone_native_response()) + } +} + +#[wasm_bindgen(js_name = CreateSendProposalTxOutput, inspectable, getter_with_clone)] +pub struct WasmCreateSendProposalTxOutput { + pub output: Option, + pub response: NativeTransactionBlockResponse, +} + +impl From>>> for WasmCreateSendProposalTxOutput { + fn from(tx_output: TransactionOutputInternal>>) -> Self { + let output = match tx_output.output { + ProposalResult::Pending(proposal) => Some(WasmProposalSend::new(proposal)), + ProposalResult::Executed(_) => None, + }; + let response = tx_output.response.clone_native_response(); + Self { output, response } + } +} + +#[wasm_bindgen(js_name = CreateSendProposalTx)] +pub struct WasmCreateSendProposalTx { + identity: WasmOnChainIdentity, + object_recipient_map: Vec, + expiration_epoch: Option, + gas_budget: Option, +} + +#[wasm_bindgen(js_class = CreateSendProposalTx)] +impl WasmCreateSendProposalTx { + pub(crate) fn new( + identity: &WasmOnChainIdentity, + object_recipient_map: Vec, + expiration_epoch: Option, + ) -> Self { + Self { + identity: identity.clone(), + object_recipient_map, + expiration_epoch, + gas_budget: None, + } + } + + #[wasm_bindgen(js_name = withGasBudget)] + pub fn with_gas_budget(mut self, budget: u64) -> Self { + self.gas_budget = Some(budget); + self + } + + #[wasm_bindgen(setter, js_name = gasBudget)] + pub fn set_gas_budget(&mut self, budget: u64) { + self.gas_budget = Some(budget); + } + + #[wasm_bindgen] + pub async fn execute(self, client: &WasmIdentityClient) -> Result { + let mut identity_ref = self.identity.0.write().await; + let builder = self + .object_recipient_map + .into_iter() + .map(Into::into) + .map(|(obj_id_str, address_str)| { + let obj_id = obj_id_str + .parse() + .map_err(|_| js_sys::TypeError::new("invalid object ID"))?; + let address = address_str + .parse() + .map_err(|_| js_sys::TypeError::new("invalid IOTA address"))?; + + Result::Ok((obj_id, address)) + }) + .try_fold(identity_ref.send_assets(), |builder, maybe_obj_address| { + let (obj_id, address) = maybe_obj_address?; + Result::Ok(builder.object(obj_id, address)) + })?; + + // identity_ref.deactivate_did(); + let builder = if let Some(exp) = self.expiration_epoch { + builder.expiration_epoch(exp) + } else { + builder + }; + + let tx_output = builder + .finish(client) + .await + .wasm_result()? + .execute_with_opt_gas_internal(self.gas_budget, client) + .await + .wasm_result()?; + + Ok(tx_output.into()) + } +} diff --git a/bindings/wasm/identity_wasm/src/rebased/proposals/update_did.rs b/bindings/wasm/identity_wasm/src/rebased/proposals/update_did.rs new file mode 100644 index 0000000000..9889fca1d4 --- /dev/null +++ b/bindings/wasm/identity_wasm/src/rebased/proposals/update_did.rs @@ -0,0 +1,304 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::rc::Rc; + +use identity_iota::iota::rebased::migration::Proposal; +use identity_iota::iota::rebased::proposals::ProposalResult; +use identity_iota::iota::rebased::proposals::ProposalT; +use identity_iota::iota::rebased::proposals::UpdateDidDocument; +use identity_iota::iota::rebased::transaction::TransactionInternal; +use identity_iota::iota::rebased::transaction::TransactionOutputInternal; +use identity_iota::iota::StateMetadataDocument; +use iota_interaction_ts::NativeTransactionBlockResponse; +use tokio::sync::RwLock; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::prelude::JsCast; + +use super::StringSet; +use crate::error::Result; +use crate::error::WasmResult; +use crate::iota::WasmIotaDocument; +use crate::rebased::WasmIdentityClient; +use crate::rebased::WasmOnChainIdentity; + +#[wasm_bindgen(js_name = UpdateDid)] +pub struct WasmUpdateDid(pub(crate) UpdateDidDocument); + +#[wasm_bindgen(js_class = UpdateDid)] +impl WasmUpdateDid { + #[wasm_bindgen(js_name = isDeactivation)] + pub fn is_deactivation(&self) -> bool { + matches!(self.0.did_document_bytes(), Some(&[])) + } + + #[wasm_bindgen(getter, js_name = didDocument)] + pub fn did_document(&self) -> Result> { + self + .0 + .did_document_bytes() + .filter(|bytes| !bytes.is_empty()) + .map(|did_doc_bytes| { + StateMetadataDocument::unpack(did_doc_bytes) + .map(StateMetadataDocument::into_iota_document_with_placeholders) + .map(WasmIotaDocument::from) + }) + .transpose() + .wasm_result() + } +} + +#[wasm_bindgen(js_name = UpdateDidProposal)] +#[derive(Clone)] +pub struct WasmProposalUpdateDid(pub(crate) Rc>>); + +#[wasm_bindgen(js_class = UpdatedDidProposal)] +impl WasmProposalUpdateDid { + fn new(proposal: Proposal) -> Self { + Self(Rc::new(RwLock::new(proposal))) + } + + #[wasm_bindgen(getter)] + pub fn id(&self) -> Result { + self + .0 + .try_read() + .wasm_result() + .map(|proposal| proposal.id().to_string()) + } + + #[wasm_bindgen(getter)] + pub fn action(&self) -> Result { + self + .0 + .try_read() + .wasm_result() + .map(|proposal| proposal.action().clone()) + .map(WasmUpdateDid) + } + + #[wasm_bindgen(getter)] + pub fn expiration_epoch(&self) -> Result> { + self + .0 + .try_read() + .wasm_result() + .map(|proposal| proposal.expiration_epoch()) + } + + #[wasm_bindgen(getter)] + pub fn votes(&self) -> Result { + self.0.try_read().wasm_result().map(|proposal| proposal.votes()) + } + + #[wasm_bindgen(getter)] + pub fn voters(&self) -> Result { + let js_set = self + .0 + .try_read() + .wasm_result()? + .voters() + .iter() + .map(ToString::to_string) + .map(js_sys::JsString::from) + .fold(js_sys::Set::default(), |set, value| { + set.add(&value); + set + }) + .unchecked_into(); + + Ok(js_set) + } + + #[wasm_bindgen] + pub fn approve(&self, identity: &WasmOnChainIdentity) -> WasmApproveUpdateDidDocumentProposalTx { + WasmApproveUpdateDidDocumentProposalTx::new(self, identity) + } + + #[wasm_bindgen(js_name = intoTx)] + pub fn into_tx(self, identity: &WasmOnChainIdentity) -> WasmExecuteUpdateDidDocumentProposalTx { + WasmExecuteUpdateDidDocumentProposalTx::new(self, identity) + } +} + +#[wasm_bindgen(js_name = ApproveUpdateDidDocumentProposalTx)] +pub struct WasmApproveUpdateDidDocumentProposalTx { + proposal: WasmProposalUpdateDid, + identity: WasmOnChainIdentity, + gas_budget: Option, +} + +#[wasm_bindgen(js_class = ApproveUpdateDidDocumentProposalTx)] +impl WasmApproveUpdateDidDocumentProposalTx { + fn new(proposal: &WasmProposalUpdateDid, identity: &WasmOnChainIdentity) -> Self { + Self { + proposal: proposal.clone(), + identity: identity.clone(), + gas_budget: None, + } + } + + #[wasm_bindgen(js_name = withGasBudget)] + pub fn with_gas_budget(mut self, budget: u64) -> Self { + self.gas_budget = Some(budget); + self + } + + #[wasm_bindgen(setter, js_name = gasBudget)] + pub fn set_gas_budget(&mut self, budget: u64) { + self.gas_budget = Some(budget); + } + + #[wasm_bindgen] + pub async fn execute(self, client: &WasmIdentityClient) -> Result { + let identity_ref = self.identity.0.read().await; + self + .proposal + .0 + .write() + .await + .approve(&identity_ref) + .execute_with_opt_gas_internal(self.gas_budget, &client.0) + .await + .wasm_result() + .map(|tx_output| tx_output.response.clone_native_response()) + } +} + +#[wasm_bindgen(js_name = ExecuteUpdateDidProposalTx)] +pub struct WasmExecuteUpdateDidDocumentProposalTx { + proposal: WasmProposalUpdateDid, + identity: WasmOnChainIdentity, + gas_budget: Option, +} + +#[wasm_bindgen(js_class = ExecuteUpdateDidProposalTx)] +impl WasmExecuteUpdateDidDocumentProposalTx { + fn new(proposal: WasmProposalUpdateDid, identity: &WasmOnChainIdentity) -> Self { + Self { + proposal, + identity: identity.clone(), + gas_budget: None, + } + } + + #[wasm_bindgen(js_name = withGasBudget)] + pub fn with_gas_budget(mut self, budget: u64) -> Self { + self.gas_budget = Some(budget); + self + } + + #[wasm_bindgen(setter, js_name = gasBudget)] + pub fn set_gas_budget(&mut self, budget: u64) { + self.gas_budget = Some(budget); + } + + #[wasm_bindgen] + pub async fn execute(self, client: &WasmIdentityClient) -> Result { + let mut identity_ref = self.identity.0.write().await; + let proposal = Rc::into_inner(self.proposal.0) + .ok_or_else(|| js_sys::Error::new("cannot consume proposal; try to drop all other references to it"))? + .into_inner(); + + proposal + .into_tx(&mut identity_ref, client) + .await + .wasm_result()? + .execute_with_opt_gas_internal(self.gas_budget, client) + .await + .wasm_result() + .map(|tx_output| tx_output.response.clone_native_response()) + } +} + +#[wasm_bindgen(js_name = CreateUpdateDidProposalTxOutput, inspectable, getter_with_clone)] +pub struct WasmCreateUpdateDidProposalTxOutput { + pub output: Option, + pub response: NativeTransactionBlockResponse, +} + +impl From>>> + for WasmCreateUpdateDidProposalTxOutput +{ + fn from(tx_output: TransactionOutputInternal>>) -> Self { + let output = match tx_output.output { + ProposalResult::Pending(proposal) => Some(WasmProposalUpdateDid::new(proposal)), + ProposalResult::Executed(_) => None, + }; + let response = tx_output.response.clone_native_response(); + Self { output, response } + } +} + +#[wasm_bindgen(js_name = CreateUpdateDidProposalTx)] +pub struct WasmCreateUpdateDidProposalTx { + identity: WasmOnChainIdentity, + updated_did_doc: Option, + #[allow(dead_code)] + delete: bool, + expiration_epoch: Option, + gas_budget: Option, +} + +#[wasm_bindgen(js_class = CreateUpdateDidProposalTx)] +impl WasmCreateUpdateDidProposalTx { + pub(crate) fn new( + identity: &WasmOnChainIdentity, + updated_did_doc: WasmIotaDocument, + expiration_epoch: Option, + ) -> Self { + Self { + identity: identity.clone(), + updated_did_doc: Some(updated_did_doc), + delete: false, + expiration_epoch, + gas_budget: None, + } + } + + pub(crate) fn deactivate(identity: &WasmOnChainIdentity, expiration_epoch: Option) -> Self { + Self { + identity: identity.clone(), + expiration_epoch, + updated_did_doc: None, + delete: false, + gas_budget: None, + } + } + + #[wasm_bindgen(js_name = withGasBudget)] + pub fn with_gas_budget(mut self, budget: u64) -> Self { + self.gas_budget = Some(budget); + self + } + + #[wasm_bindgen(setter, js_name = gasBudget)] + pub fn set_gas_budget(&mut self, budget: u64) { + self.gas_budget = Some(budget); + } + + #[wasm_bindgen] + pub async fn execute(self, client: &WasmIdentityClient) -> Result { + let mut identity_ref = self.identity.0.write().await; + let builder = if let Some(updated_did_document) = self.updated_did_doc { + identity_ref.update_did_document(updated_did_document.0.read().await.clone()) + } else { + identity_ref.deactivate_did() + }; + let builder = if let Some(exp) = self.expiration_epoch { + builder.expiration_epoch(exp) + } else { + builder + }; + + let tx_output = builder + .finish(client) + .await + .wasm_result()? + .execute_with_opt_gas_internal(self.gas_budget, client) + .await + .wasm_result()?; + + Ok(tx_output.into()) + } +} diff --git a/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client.rs b/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client.rs new file mode 100644 index 0000000000..3d81b7989d --- /dev/null +++ b/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client.rs @@ -0,0 +1,204 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; +use std::rc::Rc; + +use identity_iota::iota::rebased::client::IdentityClient; +use identity_iota::iota::rebased::client::PublishDidTx; +use identity_iota::iota::rebased::transaction::TransactionInternal; +use identity_iota::iota::rebased::transaction::TransactionOutputInternal; + +use iota_interaction_ts::bindings::WasmExecutionStatus; +use iota_interaction_ts::bindings::WasmOwnedObjectRef; +use iota_interaction_ts::WasmPublicKey; + +use identity_iota::iota::rebased::Error; +use iota_interaction_ts::NativeTransactionBlockResponse; + +use super::IdentityContainer; +use super::WasmIdentityBuilder; +use super::WasmIdentityClientReadOnly; +use super::WasmIotaAddress; +use super::WasmObjectID; + +use crate::error::wasm_error; +use crate::iota::IotaDocumentLock; +use crate::iota::WasmIotaDID; +use crate::iota::WasmIotaDocument; +use crate::storage::WasmTransactionSigner; +use identity_iota::iota::IotaDocument; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(getter_with_clone, inspectable, js_name = IotaTransactionBlockResponseEssence)] +pub struct WasmIotaTransactionBlockResponseEssence { + #[wasm_bindgen(js_name = effectsExist)] + pub effects_exist: bool, + pub effects: String, + #[wasm_bindgen(js_name = effectsExecutionStatus)] + pub effects_execution_status: Option, + #[wasm_bindgen(js_name = effectsCreated)] + pub effects_created: Option>, +} + +/// A client to interact with identities on the IOTA chain. +/// +/// Used for read and write operations. If you just want read capabilities, +/// you can also use {@link IdentityClientReadOnly}, which does not need an account and signing capabilities. +#[wasm_bindgen(js_name = IdentityClient)] +pub struct WasmIdentityClient(pub(crate) IdentityClient); + +impl Deref for WasmIdentityClient { + type Target = IdentityClient; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[wasm_bindgen(js_class = IdentityClient)] +impl WasmIdentityClient { + #[wasm_bindgen(js_name = create)] + pub async fn new( + client: WasmIdentityClientReadOnly, + signer: WasmTransactionSigner, + ) -> Result { + let inner_client = IdentityClient::new(client.0, signer).await?; + Ok(WasmIdentityClient(inner_client)) + } + + #[wasm_bindgen(js_name = senderPublicKey)] + pub fn sender_public_key(&self) -> Result { + self.0.sender_public_key().try_into() + } + + #[wasm_bindgen(js_name = senderAddress)] + pub fn sender_address(&self) -> WasmIotaAddress { + self.0.sender_address().to_string() + } + + #[wasm_bindgen(js_name = network)] + pub fn network(&self) -> String { + self.0.network().to_string() + } + + #[wasm_bindgen(js_name = migrationRegistryId)] + pub fn migration_registry_id(&self) -> String { + self.0.migration_registry_id().to_string() + } + + #[wasm_bindgen(js_name = createIdentity)] + pub fn create_identity(&self, iota_document: &WasmIotaDocument) -> Result { + WasmIdentityBuilder::new(iota_document) + .map_err(|err| JsError::new(&format!("failed to initialize new identity builder; {err:?}"))) + } + + #[wasm_bindgen(js_name = getIdentity)] + pub async fn get_identity(&self, object_id: WasmObjectID) -> Result { + let inner_value = self.0.get_identity(object_id.parse()?).await.unwrap(); + Ok(IdentityContainer(inner_value)) + } + + #[wasm_bindgen(js_name = packageId)] + pub fn package_id(&self) -> String { + self.0.package_id().to_string() + } + + #[wasm_bindgen(js_name = resolveDid)] + pub async fn resolve_did(&self, did: &WasmIotaDID) -> Result { + let document = self.0.resolve_did(&did.0).await.map_err(JsError::from)?; + Ok(WasmIotaDocument(Rc::new(IotaDocumentLock::new(document)))) + } + + #[wasm_bindgen(js_name = publishDidDocument)] + pub fn publish_did_document(&self, document: &WasmIotaDocument) -> Result { + let doc: IotaDocument = document + .0 + .try_read() + .map_err(|err| JsError::new(&format!("failed to read DID document; {err:?}")))? + .clone(); + + Ok(WasmPublishDidTx::new(self.0.publish_did_document(doc))) + } + + #[wasm_bindgen(js_name = publishDidDocumentUpdate)] + pub async fn publish_did_document_update( + &self, + document: &WasmIotaDocument, + gas_budget: u64, + ) -> Result { + let doc: IotaDocument = document + .0 + .try_read() + .map_err(|err| JsError::new(&format!("failed to read DID document; {err:?}")))? + .clone(); + let document = self + .0 + .publish_did_document_update(doc, gas_budget) + .await + .map_err(>::into)?; + + Ok(WasmIotaDocument(Rc::new(IotaDocumentLock::new(document)))) + } + + #[wasm_bindgen(js_name = deactivateDidOutput)] + pub async fn deactivate_did_output(&self, did: &WasmIotaDID, gas_budget: u64) -> Result<(), JsError> { + self + .0 + .deactivate_did_output(&did.0, gas_budget) + .await + .map_err(>::into)?; + + Ok(()) + } +} + +// TODO: rethink how to organize the following types and impls +#[wasm_bindgen(js_name = PublishDidTx)] +pub struct WasmPublishDidTx { + pub(crate) tx: PublishDidTx, + gas_budget: Option, +} + +#[wasm_bindgen(js_class = PublishDidTx)] +impl WasmPublishDidTx { + fn new(tx: PublishDidTx) -> Self { + Self { tx, gas_budget: None } + } + + #[wasm_bindgen(js_name = withGasBudget)] + pub fn with_gas_budget(mut self, budget: u64) -> Self { + self.gas_budget = Some(budget); + self + } + + #[wasm_bindgen(setter, js_name = gasBudget)] + pub fn set_gas_budget(&mut self, budget: u64) { + self.gas_budget = Some(budget); + } + + #[wasm_bindgen] + pub async fn execute(self, client: &WasmIdentityClient) -> Result { + let output = self + .tx + .execute_with_opt_gas_internal(self.gas_budget, &client.0) + .await + .map_err(wasm_error)?; + Ok(WasmTransactionOutputPublishDid(output)) + } +} + +#[wasm_bindgen(js_name = TransactionOutputInternalIotaDocument)] +pub struct WasmTransactionOutputPublishDid(pub(crate) TransactionOutputInternal); + +#[wasm_bindgen(js_class = TransactionOutputInternalIotaDocument)] +impl WasmTransactionOutputPublishDid { + #[wasm_bindgen(getter)] + pub fn output(&self) -> WasmIotaDocument { + self.0.output.clone().into() + } + + #[wasm_bindgen(getter)] + pub fn response(&self) -> NativeTransactionBlockResponse { + self.0.response.clone_native_response() + } +} diff --git a/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client_builder.rs b/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client_builder.rs new file mode 100644 index 0000000000..7217dd3e4a --- /dev/null +++ b/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client_builder.rs @@ -0,0 +1,69 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use wasm_bindgen::prelude::*; + +use identity_iota::iota_interaction::types::base_types::IotaAddress; +use iota_interaction_ts::bindings::WasmIotaClient; +use iota_interaction_ts::iota_client_ts_sdk::IotaClientTsSdk; + +use super::client_dummy::IdentityClientBuilder; + +use crate::error::wasm_error; +use crate::error::Result; + +use super::types::WasmObjectID; +use super::WasmIotaAddress; +use super::WasmIdentityClient; + +#[derive(Default)] +#[wasm_bindgen(js_name = IdentityClientBuilder)] +pub struct WasmIdentityClientBuilder(pub(crate) IdentityClientBuilder); + +#[wasm_bindgen(js_class = IdentityClientBuilder)] +impl WasmIdentityClientBuilder { + #[wasm_bindgen(js_name = identityIotaPackageId)] + pub fn identity_iota_package_id(self, value: WasmObjectID) -> Self { + Self( + self.0.identity_iota_package_id( + value + .parse() + .expect("failed to parse identity_iota_package_id value into ObjectID"), + ), + ) + } + + #[wasm_bindgen(js_name = senderPublicKey)] + pub fn sender_public_key(self, value: &[u8]) -> Self { + Self(self.0.sender_public_key(value)) + } + + #[wasm_bindgen(js_name = senderAddress)] + pub fn sender_address(self, value: WasmIotaAddress) -> Self { + Self( + self + .0 + .sender_address(&IotaAddress::from_str(&value).expect("failed to parse sender_address value into IotaAddress")), + ) + } + + #[wasm_bindgen(js_name = iotaClient)] + pub fn iota_client(self, value: WasmIotaClient) -> Self { + Self( + self + .0 + .iota_client(IotaClientTsSdk::new(value).expect("IotaClientTsSdk could not be initialized")), + ) + } + + #[wasm_bindgen(js_name = networkName)] + pub fn network_name(self, value: &str) -> Self { + Self(self.0.network_name(value)) + } + + pub fn build(self) -> Result { + Ok(WasmIdentityClient(self.0.build().map_err(wasm_error)?)) + } +} diff --git a/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client_read_only.rs b/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client_read_only.rs new file mode 100644 index 0000000000..2af8ac4cd9 --- /dev/null +++ b/bindings/wasm/identity_wasm/src/rebased/wasm_identity_client_read_only.rs @@ -0,0 +1,114 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::rc::Rc; +use std::str::FromStr; + +use identity_iota::iota::rebased::client::IdentityClientReadOnly; +use identity_iota::iota::rebased::migration::Identity; +use identity_iota::iota_interaction::types::base_types::ObjectID; +use iota_interaction_ts::bindings::WasmIotaClient; +use wasm_bindgen::prelude::*; + +use super::WasmObjectID; +use super::WasmOnChainIdentity; +use crate::iota::IotaDocumentLock; +use crate::iota::WasmIotaDID; +use crate::iota::WasmIotaDocument; + +#[wasm_bindgen(js_name = Identity)] +pub struct IdentityContainer(pub(crate) Identity); +#[wasm_bindgen(js_class = Identity)] +impl IdentityContainer { + /// TODO: check if we can actually do this like this w/o consuming the container on the 1st try + /// TODO: add support for unmigrated aliases + #[wasm_bindgen(js_name = toFullFledged)] + pub fn to_full_fledged(&self) -> Option { + match self.0.clone() { + Identity::FullFledged(v) => Some(WasmOnChainIdentity::new(v)), + _ => None, + } + } + + // #[wasm_bindgen(js_name = toLegacy)] + // pub fn to_legacy(self) -> Option { + // match self.0 { + // Identity::Legacy (v) => Some(v), + // _ => None, + // } + // } +} + +/// A client to interact with identities on the IOTA chain. +/// +/// Used for read operations, so does not need an account and signing capabilities. +/// If you want to write to the chain, use {@link IdentityClient}. +#[wasm_bindgen(js_name = IdentityClientReadOnly)] +pub struct WasmIdentityClientReadOnly(pub(crate) IdentityClientReadOnly); + +// builder related functions +#[wasm_bindgen(js_class = IdentityClientReadOnly)] +impl WasmIdentityClientReadOnly { + #[wasm_bindgen(js_name = create)] + pub async fn new(iota_client: WasmIotaClient) -> Result { + let inner_client = IdentityClientReadOnly::new(iota_client).await?; + Ok(WasmIdentityClientReadOnly(inner_client)) + } + + #[wasm_bindgen(js_name = createWithPkgId)] + pub async fn new_new_with_pkg_id( + iota_client: WasmIotaClient, + iota_identity_pkg_id: String, + ) -> Result { + let inner_client = + IdentityClientReadOnly::new_with_pkg_id(iota_client, ObjectID::from_str(&iota_identity_pkg_id)?).await?; + Ok(WasmIdentityClientReadOnly(inner_client)) + } + + #[wasm_bindgen(js_name = packageId)] + pub fn package_id(&self) -> String { + self.0.package_id().to_string() + } + + #[wasm_bindgen] + pub fn network(&self) -> String { + self.0.network().to_string() + } + + #[wasm_bindgen(js_name = migrationRegistryId)] + pub fn migration_registry_id(&self) -> String { + self.0.migration_registry_id().to_string() + } + + // TODO: implement later on + // < + + // pub async fn get_object_by_id(&self, id: ObjectID) -> Result where T: DeserializeOwned {} + + // pub async fn get_object_ref_by_id(&self, obj: ObjectID) -> Result, Error> {} + + // pub async fn find_owned_ref_for_address

( + // &self, + // address: IotaAddress, + // tag: StructTag, + // predicate: P, + // ) -> Result, Error> {} + + // > + + #[wasm_bindgen(js_name = resolveDid)] + pub async fn resolve_did(&self, did: &WasmIotaDID) -> Result { + let document = self.0.resolve_did(&did.0).await.map_err(JsError::from)?; + Ok(WasmIotaDocument(Rc::new(IotaDocumentLock::new(document)))) + } + + #[wasm_bindgen(js_name = getIdentity)] + pub async fn get_identity(&self, object_id: WasmObjectID) -> Result { + let inner_value = self + .0 + .get_identity(object_id.parse()?) + .await + .map_err(|err| JsError::new(&format!("failed to resolve identity by object id; {err:?}")))?; + Ok(IdentityContainer(inner_value)) + } +} diff --git a/bindings/wasm/src/resolver/mod.rs b/bindings/wasm/identity_wasm/src/resolver/mod.rs similarity index 63% rename from bindings/wasm/src/resolver/mod.rs rename to bindings/wasm/identity_wasm/src/resolver/mod.rs index 28d622e26f..636bc6b85d 100644 --- a/bindings/wasm/src/resolver/mod.rs +++ b/bindings/wasm/identity_wasm/src/resolver/mod.rs @@ -3,6 +3,8 @@ mod resolver_config; mod resolver_types; +mod wasm_did_resolution_handler; mod wasm_resolver; pub use resolver_types::*; +pub use wasm_did_resolution_handler::WasmDidResolutionHandler; diff --git a/bindings/wasm/src/resolver/resolver_config.rs b/bindings/wasm/identity_wasm/src/resolver/resolver_config.rs similarity index 83% rename from bindings/wasm/src/resolver/resolver_config.rs rename to bindings/wasm/identity_wasm/src/resolver/resolver_config.rs index a5ff9a74de..4094060030 100644 --- a/bindings/wasm/src/resolver/resolver_config.rs +++ b/bindings/wasm/identity_wasm/src/resolver/resolver_config.rs @@ -3,7 +3,7 @@ use wasm_bindgen::prelude::*; -use crate::iota::WasmIotaIdentityClient; +use super::WasmDidResolutionHandler; #[wasm_bindgen] extern "C" { @@ -14,11 +14,10 @@ extern "C" { pub type ResolverConfig; #[wasm_bindgen(method, getter)] - pub(crate) fn client(this: &ResolverConfig) -> Option; + pub(crate) fn client(this: &ResolverConfig) -> Option; #[wasm_bindgen(method, getter)] pub(crate) fn handlers(this: &ResolverConfig) -> Option; - } // Workaround because JSDocs does not support arrows (=>) while TS does not support the "function" word in type @@ -34,9 +33,9 @@ const TS_RESOLVER_CONFIG: &'static str = r#" */ export type ResolverConfig = { /** - * Client for resolving DIDs of the iota method. + * Client for resolving DIDs of the iota method, usually an {@link IdentityClient} or an {@link IdentityClientReadOnly} */ - client?: IIotaIdentityClient, + client?: WasmDidResolutionHandler, /** * Handlers for resolving DIDs from arbitrary DID methods. diff --git a/bindings/wasm/src/resolver/resolver_types.rs b/bindings/wasm/identity_wasm/src/resolver/resolver_types.rs similarity index 100% rename from bindings/wasm/src/resolver/resolver_types.rs rename to bindings/wasm/identity_wasm/src/resolver/resolver_types.rs diff --git a/bindings/wasm/identity_wasm/src/resolver/wasm_did_resolution_handler.rs b/bindings/wasm/identity_wasm/src/resolver/wasm_did_resolution_handler.rs new file mode 100644 index 0000000000..c5413afa26 --- /dev/null +++ b/bindings/wasm/identity_wasm/src/resolver/wasm_did_resolution_handler.rs @@ -0,0 +1,45 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::iota::DidResolutionHandler; +use identity_iota::iota::IotaDID; +use identity_iota::iota::IotaDocument; +use js_sys::Promise; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; +use wasm_bindgen_futures::JsFuture; + +use crate::error::JsValueResult; +use crate::iota::PromiseIotaDocument; +use crate::iota::WasmIotaDID; + +#[wasm_bindgen(typescript_custom_section)] +const WASM_DID_RESOLUTION_HANDLER: &str = r#" +interface WasmDidResolutionHandler { + resolveDid: (did: IotaDID) => Promise; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "WasmDidResolutionHandler")] + pub type WasmDidResolutionHandler; + + #[wasm_bindgen(js_name = "resolveDid", method)] + pub fn resolve_did(this: &WasmDidResolutionHandler, did: WasmIotaDID) -> PromiseIotaDocument; +} + +#[async_trait::async_trait(?Send)] +impl DidResolutionHandler for WasmDidResolutionHandler { + async fn resolve_did(&self, did: &IotaDID) -> Result { + let promise: Promise = Promise::resolve(&self.resolve_did(WasmIotaDID(did.clone()))); + let result: JsValueResult = JsFuture::from(promise).await.into(); + let js_value: JsValue = result.to_iota_core_error()?; + + js_value.into_serde().map_err(|err| { + identity_iota::iota::Error::JsError(format!( + "failed to parse resolved DID document to `IotaDocument`: {err}" + )) + }) + } +} diff --git a/bindings/wasm/src/resolver/wasm_resolver.rs b/bindings/wasm/identity_wasm/src/resolver/wasm_resolver.rs similarity index 93% rename from bindings/wasm/src/resolver/wasm_resolver.rs rename to bindings/wasm/identity_wasm/src/resolver/wasm_resolver.rs index 090e95e461..aeb041220e 100644 --- a/bindings/wasm/src/resolver/wasm_resolver.rs +++ b/bindings/wasm/identity_wasm/src/resolver/wasm_resolver.rs @@ -6,8 +6,8 @@ use std::rc::Rc; use identity_iota::did::CoreDID; use identity_iota::did::DID; +use identity_iota::iota::DidResolutionHandler; use identity_iota::iota::IotaDID; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::resolver::SingleThreadedResolver; use js_sys::Array; use js_sys::Function; @@ -19,13 +19,12 @@ use wasm_bindgen_futures::JsFuture; use crate::common::ArrayString; use crate::error::JsValueResult; use crate::error::WasmError; -use crate::iota::IotaDocumentLock; use crate::iota::WasmIotaDID; use crate::iota::WasmIotaDocument; -use crate::iota::WasmIotaIdentityClient; use crate::resolver::resolver_config::MapResolutionHandler; use crate::resolver::resolver_config::ResolverConfig; use crate::resolver::PromiseArrayIToCoreDocument; +use crate::resolver::WasmDidResolutionHandler; use super::resolver_types::PromiseIToCoreDocument; use crate::error::Result; @@ -35,6 +34,7 @@ use wasm_bindgen::JsCast; use wasm_bindgen_futures::future_to_promise; type JsDocumentResolver = SingleThreadedResolver; + /// Convenience type for resolving DID documents from different DID methods. /// /// Also provides methods for resolving DID Documents associated with @@ -59,7 +59,7 @@ impl WasmResolver { let mut attached_iota_method = false; let resolution_handlers: Option = config.handlers(); - let client: Option = config.client(); + let client: Option = config.client(); if let Some(handlers) = resolution_handlers { let map: &Map = handlers.dyn_ref::().ok_or_else(|| { @@ -82,11 +82,11 @@ impl WasmResolver { ))?; } - let rc_client: Rc = Rc::new(wasm_client); + let rc_client: Rc = Rc::new(wasm_client); // Take CoreDID (instead of IotaDID) to avoid inconsistent error messages between the // cases when the iota handler is attached by passing a client or directly as a handler. let handler = move |did: CoreDID| { - let rc_client_clone: Rc = rc_client.clone(); + let rc_client_clone: Rc = rc_client.clone(); async move { let iota_did: IotaDID = IotaDID::parse(did).map_err(identity_iota::iota::Error::DIDSyntaxError)?; Self::client_as_handler(rc_client_clone.as_ref(), iota_did.into()).await @@ -98,13 +98,14 @@ impl WasmResolver { Ok(Self(Rc::new(resolver))) } - pub(crate) async fn client_as_handler( - client: &WasmIotaIdentityClient, + pub(crate) async fn client_as_handler( + client: &H, did: WasmIotaDID, - ) -> std::result::Result { - Ok(WasmIotaDocument(Rc::new(IotaDocumentLock::new( - client.resolve_did(&did.0).await?, - )))) + ) -> std::result::Result + where + H: DidResolutionHandler, + { + Ok(WasmIotaDocument::from(client.resolve_did(&did.0).await?)) } /// attempts to extract (method, handler) pairs from the entries of a map and attaches them to the resolver. diff --git a/bindings/wasm/src/revocation/bitmap.rs b/bindings/wasm/identity_wasm/src/revocation/bitmap.rs similarity index 100% rename from bindings/wasm/src/revocation/bitmap.rs rename to bindings/wasm/identity_wasm/src/revocation/bitmap.rs diff --git a/bindings/wasm/src/revocation/mod.rs b/bindings/wasm/identity_wasm/src/revocation/mod.rs similarity index 100% rename from bindings/wasm/src/revocation/mod.rs rename to bindings/wasm/identity_wasm/src/revocation/mod.rs diff --git a/bindings/wasm/src/sd_jwt/decoder.rs b/bindings/wasm/identity_wasm/src/sd_jwt/decoder.rs similarity index 100% rename from bindings/wasm/src/sd_jwt/decoder.rs rename to bindings/wasm/identity_wasm/src/sd_jwt/decoder.rs diff --git a/bindings/wasm/src/sd_jwt/disclosure.rs b/bindings/wasm/identity_wasm/src/sd_jwt/disclosure.rs similarity index 100% rename from bindings/wasm/src/sd_jwt/disclosure.rs rename to bindings/wasm/identity_wasm/src/sd_jwt/disclosure.rs diff --git a/bindings/wasm/src/sd_jwt/encoder.rs b/bindings/wasm/identity_wasm/src/sd_jwt/encoder.rs similarity index 100% rename from bindings/wasm/src/sd_jwt/encoder.rs rename to bindings/wasm/identity_wasm/src/sd_jwt/encoder.rs diff --git a/bindings/wasm/src/sd_jwt/key_binding_jwt_claims.rs b/bindings/wasm/identity_wasm/src/sd_jwt/key_binding_jwt_claims.rs similarity index 100% rename from bindings/wasm/src/sd_jwt/key_binding_jwt_claims.rs rename to bindings/wasm/identity_wasm/src/sd_jwt/key_binding_jwt_claims.rs diff --git a/bindings/wasm/src/sd_jwt/mod.rs b/bindings/wasm/identity_wasm/src/sd_jwt/mod.rs similarity index 100% rename from bindings/wasm/src/sd_jwt/mod.rs rename to bindings/wasm/identity_wasm/src/sd_jwt/mod.rs diff --git a/bindings/wasm/src/sd_jwt/wasm_sd_jwt.rs b/bindings/wasm/identity_wasm/src/sd_jwt/wasm_sd_jwt.rs similarity index 100% rename from bindings/wasm/src/sd_jwt/wasm_sd_jwt.rs rename to bindings/wasm/identity_wasm/src/sd_jwt/wasm_sd_jwt.rs diff --git a/bindings/wasm/src/sd_jwt_vc/builder.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/builder.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/builder.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/builder.rs diff --git a/bindings/wasm/src/sd_jwt_vc/claims.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/claims.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/claims.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/claims.rs diff --git a/bindings/wasm/src/sd_jwt_vc/metadata/claim.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/metadata/claim.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/metadata/claim.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/metadata/claim.rs diff --git a/bindings/wasm/src/sd_jwt_vc/metadata/issuer.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/metadata/issuer.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/metadata/issuer.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/metadata/issuer.rs diff --git a/bindings/wasm/src/sd_jwt_vc/metadata/mod.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/metadata/mod.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/metadata/mod.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/metadata/mod.rs diff --git a/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/metadata/vc_type.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/metadata/vc_type.rs diff --git a/bindings/wasm/src/sd_jwt_vc/mod.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/mod.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/mod.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/mod.rs diff --git a/bindings/wasm/src/sd_jwt_vc/presentation.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/presentation.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/presentation.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/presentation.rs diff --git a/bindings/wasm/src/sd_jwt_vc/resolver.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/resolver.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/resolver.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/resolver.rs diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs diff --git a/bindings/wasm/src/sd_jwt_vc/status.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/status.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/status.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/status.rs diff --git a/bindings/wasm/src/sd_jwt_vc/token.rs b/bindings/wasm/identity_wasm/src/sd_jwt_vc/token.rs similarity index 100% rename from bindings/wasm/src/sd_jwt_vc/token.rs rename to bindings/wasm/identity_wasm/src/sd_jwt_vc/token.rs diff --git a/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs b/bindings/wasm/identity_wasm/src/storage/jpt_timeframe_revocation_ext.rs similarity index 100% rename from bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs rename to bindings/wasm/identity_wasm/src/storage/jpt_timeframe_revocation_ext.rs diff --git a/bindings/wasm/src/storage/jwk_gen_output.rs b/bindings/wasm/identity_wasm/src/storage/jwk_gen_output.rs similarity index 100% rename from bindings/wasm/src/storage/jwk_gen_output.rs rename to bindings/wasm/identity_wasm/src/storage/jwk_gen_output.rs diff --git a/bindings/wasm/src/storage/jwk_storage.rs b/bindings/wasm/identity_wasm/src/storage/jwk_storage.rs similarity index 100% rename from bindings/wasm/src/storage/jwk_storage.rs rename to bindings/wasm/identity_wasm/src/storage/jwk_storage.rs diff --git a/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs b/bindings/wasm/identity_wasm/src/storage/jwk_storage_bbs_plus_ext.rs similarity index 100% rename from bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs rename to bindings/wasm/identity_wasm/src/storage/jwk_storage_bbs_plus_ext.rs diff --git a/bindings/wasm/src/storage/jwt_presentation_options.rs b/bindings/wasm/identity_wasm/src/storage/jwt_presentation_options.rs similarity index 100% rename from bindings/wasm/src/storage/jwt_presentation_options.rs rename to bindings/wasm/identity_wasm/src/storage/jwt_presentation_options.rs diff --git a/bindings/wasm/src/storage/key_id_storage.rs b/bindings/wasm/identity_wasm/src/storage/key_id_storage.rs similarity index 100% rename from bindings/wasm/src/storage/key_id_storage.rs rename to bindings/wasm/identity_wasm/src/storage/key_id_storage.rs diff --git a/bindings/wasm/src/storage/method_digest.rs b/bindings/wasm/identity_wasm/src/storage/method_digest.rs similarity index 100% rename from bindings/wasm/src/storage/method_digest.rs rename to bindings/wasm/identity_wasm/src/storage/method_digest.rs diff --git a/bindings/wasm/src/storage/mod.rs b/bindings/wasm/identity_wasm/src/storage/mod.rs similarity index 81% rename from bindings/wasm/src/storage/mod.rs rename to bindings/wasm/identity_wasm/src/storage/mod.rs index fe54110e9d..cb97eec532 100644 --- a/bindings/wasm/src/storage/mod.rs +++ b/bindings/wasm/identity_wasm/src/storage/mod.rs @@ -10,6 +10,8 @@ mod key_id_storage; mod method_digest; mod signature_options; mod wasm_storage; +mod wasm_storage_signer; +mod wasm_transaction_signer; pub use jpt_timeframe_revocation_ext::*; pub use jwk_gen_output::*; @@ -19,3 +21,5 @@ pub use key_id_storage::*; pub use method_digest::*; pub use signature_options::*; pub use wasm_storage::*; +pub use wasm_storage_signer::*; +pub use wasm_transaction_signer::*; diff --git a/bindings/wasm/src/storage/signature_options.rs b/bindings/wasm/identity_wasm/src/storage/signature_options.rs similarity index 98% rename from bindings/wasm/src/storage/signature_options.rs rename to bindings/wasm/identity_wasm/src/storage/signature_options.rs index c293866ab6..588948e5f8 100644 --- a/bindings/wasm/src/storage/signature_options.rs +++ b/bindings/wasm/identity_wasm/src/storage/signature_options.rs @@ -8,6 +8,7 @@ use identity_iota::core::Url; use identity_iota::storage::JwsSignatureOptions; use wasm_bindgen::prelude::*; +/// Options for creating a JSON Web Signature. #[wasm_bindgen(js_name = JwsSignatureOptions, inspectable)] pub struct WasmJwsSignatureOptions(pub(crate) JwsSignatureOptions); diff --git a/bindings/wasm/src/storage/wasm_storage.rs b/bindings/wasm/identity_wasm/src/storage/wasm_storage.rs similarity index 98% rename from bindings/wasm/src/storage/wasm_storage.rs rename to bindings/wasm/identity_wasm/src/storage/wasm_storage.rs index 72d2657950..7f07beccb5 100644 --- a/bindings/wasm/src/storage/wasm_storage.rs +++ b/bindings/wasm/identity_wasm/src/storage/wasm_storage.rs @@ -1,6 +1,5 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 - use std::rc::Rc; use identity_iota::storage::storage::Storage; @@ -14,6 +13,7 @@ pub(crate) type WasmStorageInner = Storage; /// A type wrapping a `JwkStorage` and `KeyIdStorage` that should always be used together when /// working with storage backed DID documents. #[wasm_bindgen(js_name = Storage)] +#[derive(Clone)] pub struct WasmStorage(pub(crate) Rc); #[wasm_bindgen(js_class = Storage)] diff --git a/bindings/wasm/identity_wasm/src/storage/wasm_storage_signer.rs b/bindings/wasm/identity_wasm/src/storage/wasm_storage_signer.rs new file mode 100644 index 0000000000..5690c33025 --- /dev/null +++ b/bindings/wasm/identity_wasm/src/storage/wasm_storage_signer.rs @@ -0,0 +1,71 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::storage::KeyId; +use identity_iota::storage::StorageSigner; +use iota_interaction_ts::WasmIotaSignature; +use iota_interaction_ts::WasmPublicKey; +use secret_storage::Signer; +use wasm_bindgen::prelude::*; + +use crate::error::Result; +use crate::error::WasmResult; +use crate::jose::WasmJwk; +use crate::storage::WasmJwkStorage; +use crate::storage::WasmKeyIdStorage; +use crate::storage::WasmStorage; + +#[wasm_bindgen(js_name = StorageSigner)] +pub struct WasmStorageSigner { + storage: WasmStorage, + key_id: KeyId, + public_key: WasmJwk, +} + +impl WasmStorageSigner { + fn signer(&self) -> StorageSigner<'_, WasmJwkStorage, WasmKeyIdStorage> { + StorageSigner::new(&self.storage.0, self.key_id.clone(), self.public_key.0.clone()) + } +} + +#[wasm_bindgen(js_class = StorageSigner)] +impl WasmStorageSigner { + #[wasm_bindgen(constructor)] + pub fn new(storage: &WasmStorage, key_id: String, public_key: WasmJwk) -> Self { + Self { + storage: storage.clone(), + key_id: KeyId::new(key_id), + public_key, + } + } + + #[wasm_bindgen(js_name = keyId)] + pub fn key_id(&self) -> String { + self.key_id.to_string() + } + + #[wasm_bindgen(js_name = sign)] + pub async fn sign(&self, data: Vec) -> Result { + let sig = self.signer().sign(&data).await.wasm_result()?; + sig.try_into() + } + + #[wasm_bindgen(js_name = publicKey)] + pub async fn public_key(&self) -> Result { + Signer::public_key(&self.signer()) + .await + .wasm_result() + .and_then(|pk| WasmPublicKey::try_from(&pk)) + .inspect(|wasm_pk| console_log!("WasmStorageSigner's PK: {:?}", &wasm_pk.to_raw_bytes())) + } + + #[wasm_bindgen(js_name = iotaPublicKeyBytes)] + pub async fn iota_public_key_bytes(&self) -> Result> { + Signer::public_key(&self.signer()).await.wasm_result().map(|pk| { + let mut bytes: Vec = Vec::new(); + bytes.extend_from_slice(&[pk.flag()]); + bytes.extend_from_slice(pk.as_ref()); + bytes + }) + } +} diff --git a/bindings/wasm/identity_wasm/src/storage/wasm_transaction_signer.rs b/bindings/wasm/identity_wasm/src/storage/wasm_transaction_signer.rs new file mode 100644 index 0000000000..f08e2b79ec --- /dev/null +++ b/bindings/wasm/identity_wasm/src/storage/wasm_transaction_signer.rs @@ -0,0 +1,81 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use identity_iota::iota::rebased::client::IotaKeySignature; +use identity_iota::iota_interaction::types::crypto::PublicKey; +use identity_iota::iota_interaction::types::crypto::Signature; +use identity_iota::iota_interaction::types::crypto::SignatureScheme; +use iota_interaction_ts::WasmIotaSignature; +use secret_storage::Error as SecretStorageError; +use secret_storage::Signer; +use wasm_bindgen::prelude::wasm_bindgen; + +use crate::error::Result; + +#[wasm_bindgen(typescript_custom_section)] +const I_TX_SIGNER: &str = r#" +import { PublicKey } from "@iota/iota-sdk/cryptography"; +import { Signature } from "@iota/iota-sdk/client"; + +interface TransactionSigner { + sign: (data: Uint8Array) => Promise; + publicKey: () => Promise; + iotaPublicKeyBytes: () => Promise; + keyId: () => string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TransactionSigner")] + pub type WasmTransactionSigner; + + #[wasm_bindgen(method, structural, catch)] + pub async fn sign(this: &WasmTransactionSigner, data: &[u8]) -> Result; + + #[wasm_bindgen(js_name = "iotaPublicKeyBytes", method, structural, catch)] + pub async fn iota_public_key_bytes(this: &WasmTransactionSigner) -> Result; + + #[wasm_bindgen(js_name = "keyId", method, structural)] + pub fn key_id(this: &WasmTransactionSigner) -> String; +} + +#[async_trait(?Send)] +impl Signer for WasmTransactionSigner { + type KeyId = String; + + async fn sign(&self, data: &Vec) -> std::result::Result { + self.sign(data).await.and_then(|v| v.try_into()).map_err(|err| { + let details = err.as_string().map(|v| format!("; {}", v)).unwrap_or_default(); + let message = format!("could not sign data{details}"); + SecretStorageError::Other(anyhow::anyhow!(message)) + }) + } + + async fn public_key(&self) -> std::result::Result { + let uint8_array = self.iota_public_key_bytes().await.map_err(|err| { + let details = err.as_string().map(|v| format!("; {}", v)).unwrap_or_default(); + let message = format!("could not get public key{details}"); + SecretStorageError::KeyNotFound(message) + })?; + + let raw_bytes = uint8_array.to_vec(); + let signature_scheme = SignatureScheme::from_flag_byte(&raw_bytes[0]).map_err(|err| { + let details = format!("; {}", err); + let message = format!("could parse scheme flag of public key, {details}"); + SecretStorageError::Other(anyhow::anyhow!(message)) + })?; + + PublicKey::try_from_bytes(signature_scheme, &raw_bytes[1..]).map_err(|err| { + let details = format!("; {}", err); + let message = format!("could parse public key from bytes, {details}"); + SecretStorageError::Other(anyhow::anyhow!(message)) + }) + } + + fn key_id(&self) -> &String { + // TODO: Yikes! Find a way to work around this. + Box::leak(Box::new(self.key_id())) + } +} diff --git a/bindings/wasm/src/verification/custom_verification.rs b/bindings/wasm/identity_wasm/src/verification/custom_verification.rs similarity index 100% rename from bindings/wasm/src/verification/custom_verification.rs rename to bindings/wasm/identity_wasm/src/verification/custom_verification.rs diff --git a/bindings/wasm/src/verification/jws_verifier.rs b/bindings/wasm/identity_wasm/src/verification/jws_verifier.rs similarity index 100% rename from bindings/wasm/src/verification/jws_verifier.rs rename to bindings/wasm/identity_wasm/src/verification/jws_verifier.rs diff --git a/bindings/wasm/src/verification/mod.rs b/bindings/wasm/identity_wasm/src/verification/mod.rs similarity index 100% rename from bindings/wasm/src/verification/mod.rs rename to bindings/wasm/identity_wasm/src/verification/mod.rs diff --git a/bindings/wasm/src/verification/wasm_method_data.rs b/bindings/wasm/identity_wasm/src/verification/wasm_method_data.rs similarity index 100% rename from bindings/wasm/src/verification/wasm_method_data.rs rename to bindings/wasm/identity_wasm/src/verification/wasm_method_data.rs diff --git a/bindings/wasm/src/verification/wasm_method_relationship.rs b/bindings/wasm/identity_wasm/src/verification/wasm_method_relationship.rs similarity index 100% rename from bindings/wasm/src/verification/wasm_method_relationship.rs rename to bindings/wasm/identity_wasm/src/verification/wasm_method_relationship.rs diff --git a/bindings/wasm/src/verification/wasm_method_scope.rs b/bindings/wasm/identity_wasm/src/verification/wasm_method_scope.rs similarity index 100% rename from bindings/wasm/src/verification/wasm_method_scope.rs rename to bindings/wasm/identity_wasm/src/verification/wasm_method_scope.rs diff --git a/bindings/wasm/src/verification/wasm_method_type.rs b/bindings/wasm/identity_wasm/src/verification/wasm_method_type.rs similarity index 100% rename from bindings/wasm/src/verification/wasm_method_type.rs rename to bindings/wasm/identity_wasm/src/verification/wasm_method_type.rs diff --git a/bindings/wasm/src/verification/wasm_verification_method.rs b/bindings/wasm/identity_wasm/src/verification/wasm_verification_method.rs similarity index 100% rename from bindings/wasm/src/verification/wasm_verification_method.rs rename to bindings/wasm/identity_wasm/src/verification/wasm_verification_method.rs diff --git a/bindings/wasm/tests/core.ts b/bindings/wasm/identity_wasm/tests/core.ts similarity index 100% rename from bindings/wasm/tests/core.ts rename to bindings/wasm/identity_wasm/tests/core.ts diff --git a/bindings/wasm/tests/credentials.ts b/bindings/wasm/identity_wasm/tests/credentials.ts similarity index 100% rename from bindings/wasm/tests/credentials.ts rename to bindings/wasm/identity_wasm/tests/credentials.ts diff --git a/bindings/wasm/tests/iota.ts b/bindings/wasm/identity_wasm/tests/iota.ts similarity index 100% rename from bindings/wasm/tests/iota.ts rename to bindings/wasm/identity_wasm/tests/iota.ts diff --git a/bindings/wasm/tests/jose.ts b/bindings/wasm/identity_wasm/tests/jose.ts similarity index 100% rename from bindings/wasm/tests/jose.ts rename to bindings/wasm/identity_wasm/tests/jose.ts diff --git a/bindings/wasm/tests/jwk_storage.ts b/bindings/wasm/identity_wasm/tests/jwk_storage.ts similarity index 100% rename from bindings/wasm/tests/jwk_storage.ts rename to bindings/wasm/identity_wasm/tests/jwk_storage.ts diff --git a/bindings/wasm/tests/key_id_storage.ts b/bindings/wasm/identity_wasm/tests/key_id_storage.ts similarity index 100% rename from bindings/wasm/tests/key_id_storage.ts rename to bindings/wasm/identity_wasm/tests/key_id_storage.ts diff --git a/bindings/wasm/tests/resolver.ts b/bindings/wasm/identity_wasm/tests/resolver.ts similarity index 100% rename from bindings/wasm/tests/resolver.ts rename to bindings/wasm/identity_wasm/tests/resolver.ts diff --git a/bindings/wasm/tests/sd_jwt.ts b/bindings/wasm/identity_wasm/tests/sd_jwt.ts similarity index 100% rename from bindings/wasm/tests/sd_jwt.ts rename to bindings/wasm/identity_wasm/tests/sd_jwt.ts diff --git a/bindings/wasm/tests/storage.ts b/bindings/wasm/identity_wasm/tests/storage.ts similarity index 100% rename from bindings/wasm/tests/storage.ts rename to bindings/wasm/identity_wasm/tests/storage.ts diff --git a/bindings/wasm/tests/txm_readme.js b/bindings/wasm/identity_wasm/tests/txm_readme.js similarity index 94% rename from bindings/wasm/tests/txm_readme.js rename to bindings/wasm/identity_wasm/tests/txm_readme.js index 2a388c2dba..e912404751 100644 --- a/bindings/wasm/tests/txm_readme.js +++ b/bindings/wasm/identity_wasm/tests/txm_readme.js @@ -1,7 +1,7 @@ const assert = require("assert"); const spawn = require("child_process").spawn; -describe("Test TXM", () => { +describe.skip("Test TXM", () => { before((done) => { let process = spawn("txm", ["README.md"]); process.stdout.on("data", function(data) { diff --git a/bindings/wasm/tests/txm_readme_rust.js b/bindings/wasm/identity_wasm/tests/txm_readme_rust.js similarity index 90% rename from bindings/wasm/tests/txm_readme_rust.js rename to bindings/wasm/identity_wasm/tests/txm_readme_rust.js index d024653fe2..35a49748a2 100644 --- a/bindings/wasm/tests/txm_readme_rust.js +++ b/bindings/wasm/identity_wasm/tests/txm_readme_rust.js @@ -3,7 +3,7 @@ const spawn = require("child_process").spawn; describe("Test TXM", () => { before((done) => { - let process = spawn("txm", ["../../README.md"]); + let process = spawn("txm", ["../../../README.md"]); process.stdout.on("data", function(data) { console.log(data.toString()); }); diff --git a/bindings/wasm/identity_wasm/tsconfig.json b/bindings/wasm/identity_wasm/tsconfig.json new file mode 100644 index 0000000000..de65c6be7e --- /dev/null +++ b/bindings/wasm/identity_wasm/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@iota/identity-wasm/*": [ + "./*" + ] + } + } +} diff --git a/bindings/wasm/identity_wasm/tsconfig.node.json b/bindings/wasm/identity_wasm/tsconfig.node.json new file mode 100644 index 0000000000..7eaa618102 --- /dev/null +++ b/bindings/wasm/identity_wasm/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "commonjs" + } +} diff --git a/bindings/wasm/tsconfig.typedoc.json b/bindings/wasm/identity_wasm/tsconfig.typedoc.json similarity index 53% rename from bindings/wasm/tsconfig.typedoc.json rename to bindings/wasm/identity_wasm/tsconfig.typedoc.json index 02841974ec..bfc43be938 100644 --- a/bindings/wasm/tsconfig.typedoc.json +++ b/bindings/wasm/identity_wasm/tsconfig.typedoc.json @@ -1,4 +1,6 @@ { "extends": "./tsconfig.node.json", - "include": ["node/**/*"] + "include": [ + "node/**/*" + ] } diff --git a/bindings/wasm/identity_wasm/typedoc.json b/bindings/wasm/identity_wasm/typedoc.json new file mode 100644 index 0000000000..0ab32c41d4 --- /dev/null +++ b/bindings/wasm/identity_wasm/typedoc.json @@ -0,0 +1,11 @@ +{ + "name": "@iota/identity-wasm API documentation", + "extends": [ + "../typedoc.json" + ], + "entryPoints": [ + "./node/" + ], + "tsconfig": "./tsconfig.typedoc.json", + "out": "./docs/wasm" +} \ No newline at end of file diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/iota_interaction_ts/Cargo.toml similarity index 60% rename from bindings/wasm/Cargo.toml rename to bindings/wasm/iota_interaction_ts/Cargo.toml index e153f56331..9f4cf839e6 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/iota_interaction_ts/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "identity_wasm" +name = "iota_interaction_ts" version = "1.5.0" authors = ["IOTA Stiftung"] edition = "2021" @@ -8,9 +8,8 @@ keywords = ["iota", "tangle", "identity", "wasm"] license = "Apache-2.0" publish = false readme = "README.md" -repository = "https://github.com/iotaledger/identity.rs" resolver = "2" -description = "Web Assembly bindings for the identity-rs crate." +description = "identity_iota_interaction Adapters using Web Assembly bindings." [lib] crate-type = ["cdylib", "rlib"] @@ -18,38 +17,29 @@ crate-type = ["cdylib", "rlib"] [dependencies] anyhow = { version = "1.0.94", features = ["std"] } async-trait = { version = "0.1", default-features = false } +bcs = "0.1.6" bls12_381_plus = "0.8.17" +cfg-if = "1.0.0" console_error_panic_hook = { version = "0.1" } +eyre = "0.6.12" +fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "5f2c63266a065996d53f98156f0412782b468597", package = "fastcrypto" } futures = { version = "0.3" } -identity_ecdsa_verifier = { path = "../../identity_ecdsa_verifier", default-features = false, features = ["es256", "es256k"] } -identity_eddsa_verifier = { path = "../../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } +identity_core = { version = "=1.5.0", path = "../../../identity_core" } +identity_iota_interaction = { version = "=1.5.0", path = "../../../identity_iota_interaction", default-features = false } js-sys = { version = "0.3.61" } -json-proof-token = "0.3.4" -proc_typescript = { version = "0.1.0", path = "./proc_typescript" } +secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", default-features = false, tag = "v0.2.0" } serde = { version = "1.0", features = ["derive"] } serde-wasm-bindgen = "0.6.5" -serde_json = { version = "1.0", default-features = false } +serde_json.workspace = true serde_repr = { version = "0.1", default-features = false } +thiserror.workspace = true # Want to use the nice API of tokio::sync::RwLock for now even though we can't use threads. tokio = { version = "1.29", default-features = false, features = ["sync"] } -wasm-bindgen = { version = "0.2.85", features = ["serde-serialize"] } +tsify = "0.4.5" +wasm-bindgen = { version = "0.2.100", features = ["serde-serialize"] } wasm-bindgen-futures = { version = "0.4", default-features = false } zkryptium = "0.2.2" -[dependencies.identity_iota] -path = "../../identity_iota" -default-features = false -features = [ - "client", - "revocation-bitmap", - "resolver", - "domain-linkage", - "sd-jwt", - "status-list-2021", - "jpt-bbs-plus", - "sd-jwt-vc", -] - [dev-dependencies] rand = "0.8.5" @@ -57,11 +47,10 @@ rand = "0.8.5" getrandom = { version = "0.2", default-features = false, features = ["js"] } instant = { version = "0.1", default-features = false, features = ["wasm-bindgen"] } -[profile.release] -opt-level = 's' -lto = true - [lints.clippy] # can be removed as soon as fix has been added to clippy # see https://github.com/rust-lang/rust-clippy/issues/12377 empty_docs = "allow" + +[features] +keytool-signer = ["identity_iota_interaction/keytool-signer"] diff --git a/bindings/wasm/iota_interaction_ts/LICENSE b/bindings/wasm/iota_interaction_ts/LICENSE new file mode 100644 index 0000000000..4947287f7b --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/bindings/wasm/iota_interaction_ts/README.md b/bindings/wasm/iota_interaction_ts/README.md new file mode 100644 index 0000000000..6f7839c274 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/README.md @@ -0,0 +1,3 @@ +# Web Assembly adapters for identity_iota_interaction + +WASM bindings importing types from the IOTA Client typescript SDK to be used in the Identity library Rust code. \ No newline at end of file diff --git a/bindings/wasm/iota_interaction_ts/lib/index.ts b/bindings/wasm/iota_interaction_ts/lib/index.ts new file mode 100644 index 0000000000..1a201cc257 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/index.ts @@ -0,0 +1,6 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from "~iota_interaction_ts"; +export * as iota_client_helpers from "./iota_client_helpers"; +export * as move_calls from "./move_calls"; diff --git a/bindings/wasm/iota_interaction_ts/lib/iota_client_helpers.ts b/bindings/wasm/iota_interaction_ts/lib/iota_client_helpers.ts new file mode 100644 index 0000000000..26c13f7152 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/iota_client_helpers.ts @@ -0,0 +1,217 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { + CoinStruct, + ExecutionStatus, + IotaClient, + IotaTransactionBlockResponse, + OwnedObjectRef, + Signature, +} from "@iota/iota-sdk/client"; +import { GasData, TransactionDataBuilder } from "@iota/iota-sdk/transactions"; + +export type Signer = { sign(data: Uint8Array): Promise }; + +const MINIMUM_BALANCE_FOR_COIN = BigInt(1_000_000_000); + +export class WasmIotaTransactionBlockResponseWrapper { + response: IotaTransactionBlockResponse; + + constructor(response: IotaTransactionBlockResponse) { + this.response = response; + } + + effects_is_none(): boolean { + return this.response.effects == null; + } + + effects_is_some(): boolean { + return !(typeof this.response.effects == null); + } + + to_string(): string { + return JSON.stringify(this.response); + } + + effects_execution_status_inner(): null | ExecutionStatus { + return this.response.effects != null ? this.response.effects.status : null; + } + + effects_created_inner(): null | OwnedObjectRef[] { + return this.response.effects != null && this.response.effects.created != null + ? this.response.effects.created + : null; + } + + get_response(): IotaTransactionBlockResponse { + return this.response; + } + + get_digest(): string { + return this.response.digest; + } +} + +function byHighestBalance({ balance: a }: T, { balance: b }: T) { + if (a > b) { + return -1; + } + if (a < b) { + return 1; + } + return 0; +} + +async function getCoinForTransaction(iotaClient: IotaClient, senderAddress: string): Promise { + let cursor: string | null | undefined = undefined; + do { + const response = await iotaClient.getCoins({ owner: senderAddress, cursor }); + if (response.data.length === 0) { + throw new Error( + `no coin found with minimum required balance of ${MINIMUM_BALANCE_FOR_COIN} for address ${senderAddress}"`, + ); + } + + let sortedValidCoins = response.data + .map((coin) => ({ coin, balance: BigInt(coin.balance) })) + .filter(({ balance }) => balance >= MINIMUM_BALANCE_FOR_COIN) + .sort(byHighestBalance); + + if (sortedValidCoins.length >= 1) { + return sortedValidCoins[0].coin; + } + + cursor = response.nextCursor; + } while (cursor); + + throw new Error( + `no coin found with minimum required balance of ${MINIMUM_BALANCE_FOR_COIN} for address ${senderAddress}"`, + ); +} + +/** + * Inserts these values into the transaction and replaces placeholder values. + * + * - sender (overwritten as we assume a placeholder to be used in prepared transaction) + * - gas budget (value determined automatically if not provided) + * - gas price (value determined automatically) + * - gas coin / payment object (fetched automatically) + * - gas owner (equals sender) + * + * @param iotaClient client instance + * @param senderAddress transaction sender (and the one paying for it) + * @param txBcs transaction data serialized to bcs, most probably having placeholder values + * @param gasBudget optional fixed gas budget, determined automatically with a dry run if not provided + * @returns updated transaction data + */ +export async function addGasDataToTransaction( + iotaClient: IotaClient, + senderAddress: string, + txBcs: Uint8Array, + gasBudget?: bigint, +): Promise { + const gasPrice = await iotaClient.getReferenceGasPrice(); + const gasCoin = await getCoinForTransaction(iotaClient, senderAddress); + const txData = TransactionDataBuilder.fromBytes(txBcs); + const gasData: GasData = { + budget: gasBudget ? gasBudget.toString() : "50000000", // 50_000_000 + owner: senderAddress, + payment: [{ + objectId: gasCoin.coinObjectId, + version: gasCoin.version, + digest: gasCoin.digest, + }], + price: gasPrice.toString(), + }; + const overrides = { + gasData, + sender: senderAddress, + }; + // TODO: check why `.build` with `overrides` doesn't override these values + txData.sender = overrides.sender; + txData.gasData = overrides.gasData; + let builtTx = txData.build({ overrides }); + + if (!gasBudget) { + // no budget given, so we have to estimate gas usage + const dryRunGasResult = (await iotaClient + .dryRunTransactionBlock({ transactionBlock: builtTx })).effects; + if (dryRunGasResult.status.status === "failure") { + throw new Error("transaction returned an unexpected response; " + dryRunGasResult.status.error); + } + + const gasSummary = dryRunGasResult.gasUsed; + const overhead = gasPrice * BigInt(1000); + let netUsed = BigInt(gasSummary.computationCost) + + BigInt(gasSummary.storageCost) + - BigInt(gasSummary.storageRebate); + netUsed = netUsed >= 0 ? netUsed : BigInt(0); + const computation = BigInt(gasSummary.computationCost); + const maxCost = netUsed > computation ? netUsed : computation; + const budget = overhead + maxCost; + + overrides.gasData.budget = budget.toString(); + txData.gasData.budget = budget.toString(); + + builtTx = txData.build({ overrides }); + } + + return builtTx; +} + +// estimate gas, get coin, execute tx here +export async function executeTransaction( + iotaClient: IotaClient, + senderAddress: string, + txBcs: Uint8Array, + signer: Signer, + gasBudget?: bigint, +): Promise { + const txWithGasData = await addGasDataToTransaction(iotaClient, senderAddress, txBcs, gasBudget); + const signature = await signer.sign(txWithGasData); + const base64signature = getSignatureValue(signature); + + const response = await iotaClient.executeTransactionBlock({ + transactionBlock: txWithGasData, + signature: base64signature, + options: { // equivalent of `IotaTransactionBlockResponseOptions::full_content()` + showEffects: true, + showInput: true, + showRawInput: true, + showEvents: true, + showObjectChanges: true, + showBalanceChanges: true, + showRawEffects: false, + }, + }); + + if (response?.effects?.status.status === "failure") { + throw new Error(`transaction returned an unexpected response; ${response?.effects?.status.error}`); + } + + return new WasmIotaTransactionBlockResponseWrapper(response); +} + +function getSignatureValue(signature: Signature): string { + if ("Ed25519IotaSignature" in signature) { + return signature.Ed25519IotaSignature; + } + if ("Secp256k1IotaSignature" in signature) { + return signature.Secp256k1IotaSignature; + } + if ("Secp256r1IotaSignature" in signature) { + return signature.Secp256r1IotaSignature; + } + + throw new Error("invalid `Signature` value given"); +} + +/** + * Helper function to pause execution. + * + * @param durationMs time to sleep in ms + */ +export function sleep(durationMs: number) { + return new Promise(resolve => setTimeout(resolve, durationMs)); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/create.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/create.ts new file mode 100644 index 0000000000..9aee87fa2c --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/create.ts @@ -0,0 +1,27 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { Transaction } from "@iota/iota-sdk/transactions"; + +export function create( + inner_bytes: Uint8Array, + inner_type: string, + mutable: boolean, + transferable: boolean, + deletable: boolean, + packageId: string, +): Promise { + const tx = new Transaction(); + const inner_arg = tx.pure(inner_bytes); + const mutableArg = tx.pure.bool(mutable); + const transferableArg = tx.pure.bool(transferable); + const deletableArg = tx.pure.bool(deletable); + + tx.moveCall({ + target: `${packageId}::asset::new_with_config`, + typeArguments: [inner_type], + arguments: [inner_arg, mutableArg, transferableArg, deletableArg], + }); + + return tx.build(); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/delete.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/delete.ts new file mode 100644 index 0000000000..04b4a10f99 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/delete.ts @@ -0,0 +1,21 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { ObjectRef, Transaction } from "@iota/iota-sdk/transactions"; + +export function remove( + asset: ObjectRef, + asset_type: string, + packageId: string, +): Promise { + const tx = new Transaction(); + const asset_arg = tx.objectRef(asset); + + tx.moveCall({ + target: `${packageId}::asset::delete`, + typeArguments: [asset_type], + arguments: [asset_arg], + }); + + return tx.build(); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/index.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/index.ts new file mode 100644 index 0000000000..feaf3adfc2 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/index.ts @@ -0,0 +1,7 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from "./create"; +export * from "./delete"; +export * from "./transfer"; +export * from "./update"; diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/transfer.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/transfer.ts new file mode 100644 index 0000000000..5ab2b7ad7d --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/transfer.ts @@ -0,0 +1,80 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { SharedObjectRef } from "@iota/iota-sdk/dist/cjs/bcs/types"; +import { ObjectRef, Transaction } from "@iota/iota-sdk/transactions"; + +export function transfer( + asset: ObjectRef, + assetType: string, + recipient: string, + packageId: string, +): Promise { + const tx = new Transaction(); + const assetArg = tx.objectRef(asset); + const recipientArg = tx.pure.address(recipient); + + tx.moveCall({ + target: `${packageId}::asset::transfer`, + typeArguments: [assetType], + arguments: [assetArg, recipientArg], + }); + + return tx.build(); +} + +function makeTx( + proposal: SharedObjectRef, + cap: ObjectRef, + asset: ObjectRef, + assetType: string, + packageId: string, + functionName: string, +): Promise { + const tx = new Transaction(); + const proposalArg = tx.sharedObjectRef(proposal); + const capArg = tx.objectRef(cap); + const assetArg = tx.objectRef(asset); + + tx.moveCall({ + target: `${packageId}::asset::${functionName}`, + typeArguments: [assetType], + arguments: [proposalArg, capArg, assetArg], + }); + + return tx.build(); +} + +export function acceptProposal( + proposal: SharedObjectRef, + recipientCap: ObjectRef, + asset: ObjectRef, + assetType: string, + packageId: string, +): Promise { + return makeTx( + proposal, + recipientCap, + asset, + assetType, + packageId, + "accept", + ); +} + +export function concludeOrCancel( + proposal: SharedObjectRef, + senderCap: ObjectRef, + asset: ObjectRef, + assetType: string, + packageId: string, +): Promise { + return makeTx( + proposal, + senderCap, + asset, + assetType, + packageId, + "conclude_or_cancel", + ); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/update.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/update.ts new file mode 100644 index 0000000000..c80f52f9b3 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/asset/update.ts @@ -0,0 +1,23 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { ObjectRef, Transaction } from "@iota/iota-sdk/transactions"; + +export function update( + asset: ObjectRef, + content: Uint8Array, + contentType: string, + packageId: string, +): Promise { + const tx = new Transaction(); + const contentArg = tx.pure(content); + const assetArg = tx.objectRef(asset); + + tx.moveCall({ + target: `${packageId}::asset::update`, + typeArguments: [contentType], + arguments: [assetArg, contentArg], + }); + + return tx.build(); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/borrow_asset.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/borrow_asset.ts new file mode 100644 index 0000000000..774eb6eeec --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/borrow_asset.ts @@ -0,0 +1,140 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { SharedObjectRef } from "@iota/iota-sdk/dist/cjs/bcs/types"; +import { IotaObjectData } from "@iota/iota-sdk/dist/cjs/client"; +import { ObjectRef, Transaction, TransactionArgument } from "@iota/iota-sdk/transactions"; +import { getControllerDelegation, putBackDelegationToken } from "../utils"; + +export function proposeBorrow( + identity: SharedObjectRef, + capability: ObjectRef, + objects: string[], + packageId: string, + expiration?: number, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const [delegationToken, borrow] = getControllerDelegation(tx, cap, packageId); + const identityArg = tx.sharedObjectRef(identity); + const exp = tx.pure.option("u64", expiration); + const objectsArg = tx.pure.vector("id", objects); + + tx.moveCall({ + target: `${packageId}::identity::propose_borrow`, + arguments: [identityArg, delegationToken, exp, objectsArg], + }); + + putBackDelegationToken(tx, cap, delegationToken, borrow, packageId); + + return tx.build(); +} + +export function executeBorrow( + identity: SharedObjectRef, + capability: ObjectRef, + proposalId: string, + objects: IotaObjectData[], + intentFn: (arg0: Transaction, arg1: Map) => void, + packageId: string, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const [delegationToken, borrow] = getControllerDelegation(tx, cap, packageId); + const proposal = tx.pure.id(proposalId); + const identityArg = tx.sharedObjectRef(identity); + + let action = tx.moveCall({ + target: `${packageId}::identity::execute_proposal`, + typeArguments: [`${packageId}::borrow_proposal::Borrow`], + arguments: [identityArg, delegationToken, proposal], + }); + + putBackDelegationToken(tx, cap, delegationToken, borrow, packageId); + + const objectArgMap = new Map(); + for (const obj of objects) { + const recvObj = tx.receivingRef(obj); + const objArg = tx.moveCall({ + target: `${packageId}::identity::execute_borrow`, + typeArguments: [obj.type!], + arguments: [identityArg, action, recvObj], + }); + + objectArgMap.set(obj.objectId, [objArg, obj]); + } + + intentFn(tx, objectArgMap); + + for (const [obj, objData] of objectArgMap.values()) { + tx.moveCall({ + target: `${packageId}::borrow_proposal::put_back`, + typeArguments: [objData.type!], + arguments: [action, obj], + }); + } + + tx.moveCall({ + target: `${packageId}::transfer_proposal::conclude_borrow`, + arguments: [action], + }); + + return tx.build(); +} + +export function createAndExecuteBorrow( + identity: SharedObjectRef, + capability: ObjectRef, + objects: IotaObjectData[], + intentFn: (arg0: Transaction, arg1: Map) => void, + packageId: string, + expiration?: number, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const [delegationToken, borrow] = getControllerDelegation(tx, cap, packageId); + const identityArg = tx.sharedObjectRef(identity); + const exp = tx.pure.option("u64", expiration); + const objectsArg = tx.pure.vector("id", objects.map(obj => obj.objectId)); + + const proposal = tx.moveCall({ + target: `${packageId}::identity::propose_borrow`, + arguments: [identityArg, delegationToken, exp, objectsArg], + }); + + let action = tx.moveCall({ + target: `${packageId}::identity::execute_proposal`, + typeArguments: [`${packageId}::borrow_proposal::Borrow`], + arguments: [identityArg, delegationToken, proposal], + }); + + putBackDelegationToken(tx, cap, delegationToken, borrow, packageId); + + const objectArgMap = new Map(); + for (const obj of objects) { + const recvObj = tx.receivingRef(obj); + const objArg = tx.moveCall({ + target: `${packageId}::identity::execute_borrow`, + typeArguments: [obj.type!], + arguments: [identityArg, action, recvObj], + }); + + objectArgMap.set(obj.objectId, [objArg, obj]); + } + + intentFn(tx, objectArgMap); + + for (const [obj, objData] of objectArgMap.values()) { + tx.moveCall({ + target: `${packageId}::borrow_proposal::put_back`, + typeArguments: [objData.type!], + arguments: [action, obj], + }); + } + + tx.moveCall({ + target: `${packageId}::transfer_proposal::conclude_borrow`, + arguments: [action], + }); + return tx.build(); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/config.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/config.ts new file mode 100644 index 0000000000..666b0022eb --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/config.ts @@ -0,0 +1,73 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { SharedObjectRef } from "@iota/iota-sdk/dist/cjs/bcs/types"; +import { ObjectRef, Transaction } from "@iota/iota-sdk/transactions"; +import { getControllerDelegation, putBackDelegationToken } from "../utils"; + +export function proposeConfigChange( + identity: SharedObjectRef, + controllerCap: ObjectRef, + controllersToAdd: [string, number][], + controllersToRemove: string[], + controllersToUpdate: [string, number][], + packageId: string, + expiration?: number, + threshold?: number, +): Promise { + const tx = new Transaction(); + const addressesToAdd = tx.pure.vector("address", controllersToAdd.map(c => c[0])); + const vpsToAdd = tx.pure.vector("u64", controllersToAdd.map(c => c[1])); + const controllersToAddArg = tx.moveCall({ + target: `${packageId}::utils::vec_map_from_keys_values`, + typeArguments: ["address", "u64"], + arguments: [addressesToAdd, vpsToAdd], + }); + + const idsToUpdate = tx.pure.vector("id", controllersToUpdate.map(c => c[0])); + const vpsToUpdate = tx.pure.vector("u64", controllersToUpdate.map(c => c[1])); + const controllersToUpdateArg = tx.moveCall({ + target: `${packageId}::utils::vec_map_from_keys_values`, + typeArguments: ["id", "u64"], + arguments: [idsToUpdate, vpsToUpdate], + }); + + const identityArg = tx.sharedObjectRef(identity); + const cap = tx.objectRef(controllerCap); + const [delegationToken, borrow] = getControllerDelegation(tx, cap, packageId); + const thresholdArg = tx.pure.option("u64", threshold); + const exp = tx.pure.option("u64", expiration); + const controllersToRemoveArg = tx.pure.vector("id", controllersToRemove); + + tx.moveCall({ + target: `${packageId}::identity::propose_config_change`, + arguments: [identityArg, delegationToken, exp, thresholdArg, controllersToAddArg, controllersToRemoveArg, + controllersToUpdateArg], + }); + + putBackDelegationToken(tx, cap, delegationToken, borrow, packageId); + + return tx.build(); +} + +export function executeConfigChange( + identity: SharedObjectRef, + capability: ObjectRef, + proposalId: string, + packageId: string, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const [delegationToken, borrow] = getControllerDelegation(tx, cap, packageId); + const proposal = tx.pure.id(proposalId); + const identityArg = tx.sharedObjectRef(identity); + + tx.moveCall({ + target: `${packageId}::identity::execute_config_change`, + arguments: [identityArg, delegationToken, proposal], + }); + + putBackDelegationToken(tx, cap, delegationToken, borrow, packageId); + + return tx.build(); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/controller_execution.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/controller_execution.ts new file mode 100644 index 0000000000..332daa33a4 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/controller_execution.ts @@ -0,0 +1,112 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { SharedObjectRef } from "@iota/iota-sdk/dist/cjs/bcs/types"; +import { ObjectRef, Transaction, TransactionArgument } from "@iota/iota-sdk/transactions"; +import { getControllerDelegation, putBackDelegationToken } from "../utils"; + +export function proposeControllerExecution( + identity: SharedObjectRef, + capability: ObjectRef, + controllerCapId: string, + packageId: string, + expiration?: number, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const [delegationToken, borrow] = getControllerDelegation(tx, cap, packageId); + const identityArg = tx.sharedObjectRef(identity); + const exp = tx.pure.option("u64", expiration); + const controllerCapIdArg = tx.pure.id(controllerCapId); + + tx.moveCall({ + target: `${packageId}::identity::propose_controller_execution`, + arguments: [identityArg, delegationToken, controllerCapIdArg, exp], + }); + + putBackDelegationToken(tx, cap, delegationToken, borrow, packageId); + + return tx.build(); +} + +export function executeControllerExecution( + identity: SharedObjectRef, + capability: ObjectRef, + proposalId: string, + controllerCapRef: ObjectRef, + intentFn: (arg0: Transaction, arg1: TransactionArgument) => void, + packageId: string, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const [delegationToken, borrow] = getControllerDelegation(tx, cap, packageId); + const proposal = tx.pure.id(proposalId); + const identityArg = tx.sharedObjectRef(identity); + + let action = tx.moveCall({ + target: `${packageId}::identity::execute_proposal`, + typeArguments: [`${packageId}::borrow_proposal::Borrow`], + arguments: [identityArg, delegationToken, proposal], + }); + + putBackDelegationToken(tx, cap, delegationToken, borrow, packageId); + + const receiving = tx.receivingRef(controllerCapRef); + const BorrowedControllerCap = tx.moveCall({ + target: `${packageId}::identity::borrow_controller_cap`, + arguments: [identityArg, action, receiving], + }); + + intentFn(tx, BorrowedControllerCap); + + tx.moveCall({ + target: `${packageId}::controller_proposal::put_back`, + arguments: [action, BorrowedControllerCap], + }); + + return tx.build(); +} + +export function createAndExecuteControllerExecution( + identity: SharedObjectRef, + capability: ObjectRef, + controllerCapRef: ObjectRef, + intentFn: (arg0: Transaction, arg1: TransactionArgument) => void, + packageId: string, + expiration?: number, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const [delegationToken, borrow] = getControllerDelegation(tx, cap, packageId); + const identityArg = tx.sharedObjectRef(identity); + const exp = tx.pure.option("u64", expiration); + const controller_cap_id = tx.pure.id(controllerCapRef.objectId); + + const proposal = tx.moveCall({ + target: `${packageId}::identity::propose_controller_execution`, + arguments: [identityArg, delegationToken, controller_cap_id, exp], + }); + + let action = tx.moveCall({ + target: `${packageId}::identity::execute_proposal`, + typeArguments: [`${packageId}::borrow_proposal::Borrow`], + arguments: [identityArg, delegationToken, proposal], + }); + + putBackDelegationToken(tx, cap, delegationToken, borrow, packageId); + + const receiving = tx.receivingRef(controllerCapRef); + const borrowedControllerCap = tx.moveCall({ + target: `${packageId}::identity::borrow_controller_cap`, + arguments: [identityArg, action, receiving], + }); + + intentFn(tx, borrowedControllerCap); + + tx.moveCall({ + target: `${packageId}::controller_proposal::put_back`, + arguments: [action, borrowedControllerCap], + }); + + return tx.build(); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/create.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/create.ts new file mode 100644 index 0000000000..ee8eed79f2 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/create.ts @@ -0,0 +1,52 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { bcs } from "@iota/iota-sdk/bcs"; +import { Transaction } from "@iota/iota-sdk/transactions"; +import { getClockRef, insertPlaceholders } from "../utils"; + +export async function create(didDoc: Uint8Array | undefined, packageId: string): Promise { + const tx = new Transaction(); + const didDocArg = tx.pure(bcs.option(bcs.vector(bcs.U8)).serialize(didDoc)); + const clock = getClockRef(tx); + + tx.moveCall({ + target: `${packageId}::identity::new`, + arguments: [didDocArg, clock], + }); + + insertPlaceholders(tx); + + return tx.build(); +} + +export function newWithControllers( + didDoc: Uint8Array | undefined, + controllers: [string, number][], + threshold: number, + packageId: string, +): Promise { + const tx = new Transaction(); + const ids = tx.pure.vector("address", controllers.map(controller => controller[0])); + const vps = tx.pure.vector("u64", controllers.map(controller => controller[1])); + const controllersArg = tx.moveCall({ + target: `${packageId}::utils::vec_map_from_keys_values`, + typeArguments: ["address", "u64"], + arguments: [ids, vps], + }); + const controllersThatCanDelegate = tx.moveCall({ + target: "0x2::vec_map::empty", + typeArguments: ["address", "u64"], + arguments: [], + }); + const didDocArg = tx.pure(bcs.option(bcs.vector(bcs.U8)).serialize(didDoc)); + const clock = getClockRef(tx); + const thresholdArg = tx.pure.u64(threshold); + + tx.moveCall({ + target: `${packageId}::identity::new_with_controllers`, + arguments: [didDocArg, controllersArg, controllersThatCanDelegate, thresholdArg, clock], + }); + + return tx.build(); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/index.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/index.ts new file mode 100644 index 0000000000..8d50ae0187 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/index.ts @@ -0,0 +1,11 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * from "./borrow_asset"; +export * from "./config"; +export * from "./controller_execution"; +export * from "./create"; +export * from "./proposal"; +export * from "./send_asset"; +export * from "./update"; +export * from "./upgrade"; diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/proposal.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/proposal.ts new file mode 100644 index 0000000000..6a683ced37 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/proposal.ts @@ -0,0 +1,30 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { SharedObjectRef } from "@iota/iota-sdk/dist/cjs/bcs/types"; +import { ObjectRef, Transaction } from "@iota/iota-sdk/transactions"; +import { getControllerDelegation, putBackDelegationToken } from "../utils"; + +export function approve( + identity: SharedObjectRef, + capability: ObjectRef, + proposalId: string, + proposalType: string, + packageId: string, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const [delegationToken, borrow] = getControllerDelegation(tx, cap, packageId); + const identityArg = tx.sharedObjectRef(identity); + const proposal = tx.pure.id(proposalId); + + tx.moveCall({ + target: `${packageId}::identity::approve_proposal`, + typeArguments: [proposalType], + arguments: [identityArg, delegationToken, proposal], + }); + + putBackDelegationToken(tx, cap, delegationToken, borrow, packageId); + + return tx.build(); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/send_asset.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/send_asset.ts new file mode 100644 index 0000000000..2688a0d4fb --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/send_asset.ts @@ -0,0 +1,69 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { SharedObjectRef } from "@iota/iota-sdk/dist/cjs/bcs/types"; +import { ObjectRef, Transaction } from "@iota/iota-sdk/transactions"; +import { getControllerDelegation, putBackDelegationToken } from "../utils"; + +export function proposeSend( + identity: SharedObjectRef, + capability: ObjectRef, + transferMap: [string, string][], + packageId: string, + expiration?: number, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const [delegationToken, borrow] = getControllerDelegation(tx, cap, packageId); + const identityArg = tx.sharedObjectRef(identity); + const exp = tx.pure.option("u64", expiration); + const objects = tx.pure.vector("id", transferMap.map(t => t[0])); + const recipients = tx.pure.vector("address", transferMap.map(t => t[1])); + + tx.moveCall({ + target: `${packageId}::identity::propose_send`, + arguments: [identityArg, delegationToken, exp, objects, recipients], + }); + + putBackDelegationToken(tx, cap, delegationToken, borrow, packageId); + + return tx.build(); +} + +export function executeSend( + identity: SharedObjectRef, + capability: ObjectRef, + proposalId: string, + objects: [ObjectRef, string][], + packageId: string, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const [delegationToken, borrow] = getControllerDelegation(tx, cap, packageId); + const proposal = tx.pure.id(proposalId); + const identityArg = tx.sharedObjectRef(identity); + + let action = tx.moveCall({ + target: `${packageId}::identity::execute_proposal`, + typeArguments: [`${packageId}::transfer_proposal::Send`], + arguments: [identityArg, delegationToken, proposal], + }); + + putBackDelegationToken(tx, cap, delegationToken, borrow, packageId); + + for (const [obj, objType] of objects) { + const recv_obj = tx.receivingRef(obj); + tx.moveCall({ + target: `${packageId}::identity::execute_send`, + typeArguments: [objType], + arguments: [identityArg, action, recv_obj], + }); + } + + tx.moveCall({ + target: `${packageId}::transfer_proposal::complete_send`, + arguments: [action], + }); + + return tx.build(); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/update.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/update.ts new file mode 100644 index 0000000000..b664944d62 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/update.ts @@ -0,0 +1,57 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { bcs } from "@iota/iota-sdk/bcs"; +import { SharedObjectRef } from "@iota/iota-sdk/dist/cjs/bcs/types"; +import { ObjectRef, Transaction } from "@iota/iota-sdk/transactions"; +import { getClockRef, getControllerDelegation, insertPlaceholders, putBackDelegationToken } from "../utils"; + +export function proposeUpdate( + identity: SharedObjectRef, + capability: ObjectRef, + didDoc: Uint8Array | undefined, + packageId: string, + expiration?: number, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const [delegationToken, borrow] = getControllerDelegation(tx, cap, packageId); + const identityArg = tx.sharedObjectRef(identity); + const exp = tx.pure.option("u64", expiration); + const doc = tx.pure(bcs.option(bcs.vector(bcs.U8)).serialize(didDoc)); + const clock = getClockRef(tx); + + tx.moveCall({ + target: `${packageId}::identity::propose_update`, + arguments: [identityArg, delegationToken, doc, exp, clock], + }); + + putBackDelegationToken(tx, cap, delegationToken, borrow, packageId); + + insertPlaceholders(tx); + + return tx.build(); +} + +export function executeUpdate( + identity: SharedObjectRef, + capability: ObjectRef, + proposalId: string, + packageId: string, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const [delegationToken, borrow] = getControllerDelegation(tx, cap, packageId); + const proposal = tx.pure.id(proposalId); + const identityArg = tx.sharedObjectRef(identity); + const clock = getClockRef(tx); + + tx.moveCall({ + target: `${packageId}::identity::execute_update`, + arguments: [identityArg, delegationToken, proposal, clock], + }); + + putBackDelegationToken(tx, cap, delegationToken, borrow, packageId); + + return tx.build(); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/upgrade.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/upgrade.ts new file mode 100644 index 0000000000..cc8622bfec --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/identity/upgrade.ts @@ -0,0 +1,43 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { SharedObjectRef } from "@iota/iota-sdk/dist/cjs/bcs/types"; +import { ObjectRef, Transaction } from "@iota/iota-sdk/transactions"; + +export function proposeUpgrade( + identity: SharedObjectRef, + capability: ObjectRef, + packageId: string, + expiration?: number, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const identityArg = tx.sharedObjectRef(identity); + const exp = tx.pure.option("u64", expiration); + + tx.moveCall({ + target: `${packageId}::identity::propose_upgrade`, + arguments: [identityArg, cap, exp], + }); + + return tx.build(); +} + +export function executeUpgrade( + identity: SharedObjectRef, + capability: ObjectRef, + proposalId: string, + packageId: string, +): Promise { + const tx = new Transaction(); + const cap = tx.objectRef(capability); + const proposal = tx.pure.id(proposalId); + const identityArg = tx.sharedObjectRef(identity); + + tx.moveCall({ + target: `${packageId}::identity::execute_upgrade`, + arguments: [identityArg, cap, proposal], + }); + + return tx.build(); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/index.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/index.ts new file mode 100644 index 0000000000..7c8b6e125f --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/index.ts @@ -0,0 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +export * as asset from "./asset"; +export * as identity from "./identity"; +export * from "./migration"; diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/migration.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/migration.ts new file mode 100644 index 0000000000..e29cd620fa --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/migration.ts @@ -0,0 +1,34 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { SharedObjectRef } from "@iota/iota-sdk/dist/cjs/bcs/types"; +import { ObjectRef, Transaction } from "@iota/iota-sdk/transactions"; +import { getClockRef } from "./utils"; + +export function migrateDidOutput( + didOutput: ObjectRef, + migrationRegistry: SharedObjectRef, + packageId: string, + creationTimestamp?: number, +): Promise { + const tx = new Transaction(); + const did_output = tx.objectRef(didOutput); + const migration_registry = tx.sharedObjectRef(migrationRegistry); + const clock = getClockRef(tx); + let timestamp; + if (creationTimestamp) { + timestamp = tx.pure.u64(creationTimestamp); + } else { + timestamp = tx.moveCall({ + target: "0x2::clock::timestamp_ms", + arguments: [clock], + }); + } + + tx.moveCall({ + target: `${packageId}::migration::migrate_alias_output`, + arguments: [did_output, migration_registry, timestamp, clock], + }); + + return tx.build(); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/move_calls/utils.ts b/bindings/wasm/iota_interaction_ts/lib/move_calls/utils.ts new file mode 100644 index 0000000000..80c49b4624 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/move_calls/utils.ts @@ -0,0 +1,50 @@ +import { ObjectRef, Transaction, TransactionArgument } from "@iota/iota-sdk/transactions"; +import { IOTA_CLOCK_OBJECT_ID } from "@iota/iota-sdk/utils"; + +const PLACEHOLDER_SENDER = "0x00000000000000090807060504030201"; +const PLACEHOLDER_GAS_BUDGET = 9; +const PLACEHOLDER_GAS_PRICE = 8; +const PLACEHOLDER_GAS_PAYMENT: ObjectRef[] = []; + +export function getClockRef(tx: Transaction): TransactionArgument { + return tx.sharedObjectRef({ objectId: IOTA_CLOCK_OBJECT_ID, initialSharedVersion: 1, mutable: false }); +} + +export function getControllerDelegation( + tx: Transaction, + controllerCap: TransactionArgument, + packageId: string, +): [TransactionArgument, TransactionArgument] { + const [token, borrow] = tx.moveCall({ + target: `${packageId}::controller::borrow`, + arguments: [controllerCap], + }); + return [token, borrow]; +} + +export function putBackDelegationToken( + tx: Transaction, + controllerCap: TransactionArgument, + delegationToken: TransactionArgument, + borrow: TransactionArgument, + packageId: string, +) { + tx.moveCall({ + target: `${packageId}::controller::put_back`, + arguments: [controllerCap, delegationToken, borrow], + }); +} + +/** + * Inserts placeholders related to sender and payment into transaction. + * + * This is required if wanting to call `tx.build`, as this will check if these values have been set. + * + * @param tx transaction to update + */ +export function insertPlaceholders(tx: Transaction) { + tx.setGasPrice(PLACEHOLDER_GAS_PRICE); + tx.setGasBudget(PLACEHOLDER_GAS_BUDGET); + tx.setGasPayment([...PLACEHOLDER_GAS_PAYMENT]); // make sure, we're not sharing the array between tx + tx.setSender(PLACEHOLDER_SENDER); +} diff --git a/bindings/wasm/iota_interaction_ts/lib/tsconfig.json b/bindings/wasm/iota_interaction_ts/lib/tsconfig.json new file mode 100644 index 0000000000..9bee396afd --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/lib/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.node.json", + "compilerOptions": { + "baseUrl": "./", + "paths": { + "../lib": [ + "." + ], + "~iota_interaction_ts": [ + "../node/iota_interaction_ts", + "./iota_interaction_ts.js" + ] + }, + "outDir": "../node", + "declarationDir": "../node" + } +} diff --git a/bindings/wasm/lib/tsconfig.web.json b/bindings/wasm/iota_interaction_ts/lib/tsconfig.web.json similarity index 51% rename from bindings/wasm/lib/tsconfig.web.json rename to bindings/wasm/iota_interaction_ts/lib/tsconfig.web.json index 7216ccd75c..b1937c9cbb 100644 --- a/bindings/wasm/lib/tsconfig.web.json +++ b/bindings/wasm/iota_interaction_ts/lib/tsconfig.web.json @@ -1,11 +1,16 @@ { "extends": "../tsconfig.json", "compilerOptions": { + "target": "ES2020", "baseUrl": "./", "paths": { - "~identity_wasm": ["../web/identity_wasm", "./identity_wasm.js"], - "~sdk-wasm": ["../node_modules/@iota/sdk-wasm/web", "@iota/sdk-wasm/web"], - "../lib": ["."] + "../lib": [ + "." + ], + "~iota_interaction_ts": [ + "../web/iota_interaction_ts", + "./iota_interaction_ts.js" + ] }, "outDir": "../web", "declarationDir": "../web", diff --git a/bindings/wasm/iota_interaction_ts/package-lock.json b/bindings/wasm/iota_interaction_ts/package-lock.json new file mode 100644 index 0000000000..bb88483005 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/package-lock.json @@ -0,0 +1,1068 @@ +{ + "name": "@iota/iota-interaction-ts", + "version": "0.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@iota/iota-interaction-ts", + "version": "0.3.0", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^22.0.0", + "dprint": "^0.33.0", + "rimraf": "^6.0.1", + "tsconfig-paths": "^4.1.0", + "typescript": "^5.7.3", + "wasm-opt": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@iota/iota-sdk": "^0.5.0" + } + }, + "node_modules/@0no-co/graphql.web": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.0.13.tgz", + "integrity": "sha512-jqYxOevheVTU1S36ZdzAkJIdvRp2m3OYIG5SEoKDw5NI8eVwkoI0D/Q3DYNGmXCxkA6CQuoa7zvMiDPTLqUNuw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, + "node_modules/@0no-co/graphqlsp": { + "version": "1.12.16", + "resolved": "https://registry.npmjs.org/@0no-co/graphqlsp/-/graphqlsp-1.12.16.tgz", + "integrity": "sha512-B5pyYVH93Etv7xjT6IfB7QtMBdaaC07yjbhN6v8H7KgFStMkPvi+oWYBTibMFRMY89qwc9H8YixXg8SXDVgYWw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@gql.tada/internal": "^1.0.0", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@gql.tada/cli-utils": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@gql.tada/cli-utils/-/cli-utils-1.6.3.tgz", + "integrity": "sha512-jFFSY8OxYeBxdKi58UzeMXG1tdm4FVjXa8WHIi66Gzu9JWtCE6mqom3a8xkmSw+mVaybFW5EN2WXf1WztJVNyQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@0no-co/graphqlsp": "^1.12.13", + "@gql.tada/internal": "1.0.8", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "@0no-co/graphqlsp": "^1.12.13", + "@gql.tada/svelte-support": "1.0.1", + "@gql.tada/vue-support": "1.0.1", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "@gql.tada/svelte-support": { + "optional": true + }, + "@gql.tada/vue-support": { + "optional": true + } + } + }, + "node_modules/@gql.tada/internal": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@gql.tada/internal/-/internal-1.0.8.tgz", + "integrity": "sha512-XYdxJhtHC5WtZfdDqtKjcQ4d7R1s0d1rnlSs3OcBEUbYiPoJJfZU7tWsVXuv047Z6msvmr4ompJ7eLSK5Km57g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@0no-co/graphql.web": "^1.0.5" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@iota/bcs": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@iota/bcs/-/bcs-0.2.1.tgz", + "integrity": "sha512-T+iv5gZhUZP7BiDY7+Ir4MA2rYmyGNZA2b+nxjv219Fp8klFt+l38OWA+1RgJXrCmzuZ+M4hbMAeHhHziURX6Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "bs58": "^6.0.0" + } + }, + "node_modules/@iota/iota-sdk": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@iota/iota-sdk/-/iota-sdk-0.5.0.tgz", + "integrity": "sha512-ZFg4C5EuHV55fHITKOO6Mg1dLqgojZqJsDsR3SRt8W9Ofzbjt8shlM2uLNRwDpiM7GzTb4UUFcKXpOt//5gEmQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "@iota/bcs": "0.2.1", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@scure/bip32": "^1.4.0", + "@scure/bip39": "^1.3.0", + "@suchipi/femver": "^1.0.0", + "bech32": "^2.0.0", + "gql.tada": "^1.8.2", + "graphql": "^16.9.0", + "tweetnacl": "^1.0.3", + "valibot": "^0.36.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@iota/iota-sdk/node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense", + "peer": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@noble/curves": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "1.7.1" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.4.tgz", + "integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", + "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", + "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.4" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@suchipi/femver": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@suchipi/femver/-/femver-1.0.0.tgz", + "integrity": "sha512-bprE8+K5V+DPX7q2e2K57ImqNBdfGHDIWaGI5xHxZoxbKOuQZn4wzPiUxOAHnsUr3w3xHrWXwN7gnG/iIuEMIg==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base-x": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz", + "integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==", + "license": "MIT", + "peer": true + }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "license": "MIT", + "peer": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "peer": true, + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dprint": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/dprint/-/dprint-0.33.0.tgz", + "integrity": "sha512-VploASP7wL1HAYe5xWZKRwp8gW5zTdcG3Tb60DASv6QLnGKsl+OS+bY7wsXFrS4UcIbUNujXdsNG5FxBfRJIQg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "yauzl": "=2.10.0" + }, + "bin": { + "dprint": "bin.js" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/gql.tada": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/gql.tada/-/gql.tada-1.8.10.tgz", + "integrity": "sha512-FrvSxgz838FYVPgZHGOSgbpOjhR+yq44rCzww3oOPJYi0OvBJjAgCiP6LEokZIYND2fUTXzQAyLgcvgw1yNP5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@0no-co/graphql.web": "^1.0.5", + "@0no-co/graphqlsp": "^1.12.13", + "@gql.tada/cli-utils": "1.6.3", + "@gql.tada/internal": "1.0.8" + }, + "bin": { + "gql-tada": "bin/cli.js", + "gql.tada": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } + }, + "node_modules/graphql": { + "version": "16.10.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz", + "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/valibot": { + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.36.0.tgz", + "integrity": "sha512-CjF1XN4sUce8sBK9TixrDqFM7RwNkuXdJu174/AwmQUB62QbCQADg5lLe8ldBalFgtj1uKj+pKwDJiNo4Mn+eQ==", + "license": "MIT", + "peer": true + }, + "node_modules/wasm-opt": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/wasm-opt/-/wasm-opt-1.4.0.tgz", + "integrity": "sha512-wIsxxp0/FOSphokH4VOONy1zPkVREQfALN+/JTvJPK8gFSKbsmrcfECu2hT7OowqPfb4WEMSMceHgNL0ipFRyw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "node-fetch": "^2.6.9", + "tar": "^6.1.13" + }, + "bin": { + "wasm-opt": "bin/wasm-opt.js" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/bindings/wasm/iota_interaction_ts/package.json b/bindings/wasm/iota_interaction_ts/package.json new file mode 100644 index 0000000000..8e6c1fbfaa --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/package.json @@ -0,0 +1,48 @@ +{ + "name": "@iota/iota-interaction-ts", + "author": "IOTA Foundation ", + "description": "WASM bindings importing types from the IOTA Client typescript SDK to be used in Rust", + "homepage": "https://www.iota.org", + "version": "0.3.0", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/iotaledger/identity.rs.git" + }, + "scripts": { + "build:src": "cargo build --lib --release --target wasm32-unknown-unknown --target-dir ../target", + "build:src:node": "cargo build --lib --release --target wasm32-unknown-unknown --features keytool-signer --target-dir ../target", + "prebundle:nodejs": "rimraf node", + "bundle:nodejs": "wasm-bindgen ../target/wasm32-unknown-unknown/release/iota_interaction_ts.wasm --typescript --weak-refs --target nodejs --out-dir node && node ../build/node iota_interaction_ts && tsc --project ./lib/tsconfig.json && node ../build/replace_paths ./lib/tsconfig.json node iota_interaction_ts", + "prebundle:web": "rimraf web", + "bundle:web": "wasm-bindgen ../target/wasm32-unknown-unknown/release/iota_interaction_ts.wasm --typescript --target web --out-dir web && node ../build/web iota_interaction_ts && tsc --project ./lib/tsconfig.web.json && node ../build/replace_paths ./lib/tsconfig.web.json web iota_interaction_ts", + "build:nodejs": "npm run build:src:node && npm run bundle:nodejs", + "build:web": "npm run build:src && npm run bundle:web", + "build": "npm run build:web && npm run build:nodejs", + "fmt": "dprint fmt" + }, + "bugs": { + "url": "https://github.com/iotaledger/identity.rs/issues" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "web/*", + "node/*" + ], + "devDependencies": { + "@types/node": "^22.0.0", + "dprint": "^0.33.0", + "rimraf": "^6.0.1", + "tsconfig-paths": "^4.1.0", + "typescript": "^5.7.3", + "wasm-opt": "^1.4.0" + }, + "peerDependencies": { + "@iota/iota-sdk": "^0.5.0" + }, + "engines": { + "node": ">=20" + } +} diff --git a/bindings/wasm/iota_interaction_ts/src/asset_move_calls.rs b/bindings/wasm/iota_interaction_ts/src/asset_move_calls.rs new file mode 100644 index 0000000000..d7a5f64780 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/asset_move_calls.rs @@ -0,0 +1,193 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::types::execution_status::CommandArgumentError; +use js_sys::Uint8Array; +use serde::Serialize; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; + +use crate::bindings::WasmObjectRef; +use crate::bindings::WasmSharedObjectRef; +use crate::error::TsSdkError; +use crate::error::WasmError; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::base_types::SequenceNumber; +use identity_iota_interaction::types::TypeTag; +use identity_iota_interaction::AssetMoveCalls; +use identity_iota_interaction::MoveType; +use identity_iota_interaction::ProgrammableTransactionBcs; + +#[wasm_bindgen(module = "@iota/iota-interaction-ts/move_calls/asset")] +extern "C" { + #[wasm_bindgen(js_name = "create", catch)] + pub(crate) async fn new_asset( + inner_bytes: &[u8], + inner_type: &str, + mutable: bool, + transferable: bool, + deletable: bool, + package: &str, + ) -> Result; + + #[wasm_bindgen(catch, js_name = "remove")] + pub(crate) async fn delete(asset: WasmObjectRef, asset_type: &str, package: &str) -> Result; + + #[wasm_bindgen(catch)] + pub(crate) async fn update( + asset: WasmObjectRef, + content: &[u8], + content_type: &str, + package: &str, + ) -> Result; + + #[wasm_bindgen(catch)] + pub(crate) async fn transfer( + asset: WasmObjectRef, + asset_type: &str, + recipient: &str, + package: &str, + ) -> Result; + + #[wasm_bindgen(js_name = "acceptProposal", catch)] + pub(crate) async fn accept_proposal( + proposal: WasmSharedObjectRef, + recipient_cap: WasmObjectRef, + asset: WasmObjectRef, + asset_type: &str, + package: &str, + ) -> Result; + + #[wasm_bindgen(js_name = "concludeOrCancel", catch)] + pub(crate) async fn conclude_or_cancel( + proposal: WasmSharedObjectRef, + sender_cap: WasmObjectRef, + asset: WasmObjectRef, + asset_type: &str, + package: &str, + ) -> Result; +} + +pub struct AssetMoveCallsTsSdk {} + +impl AssetMoveCalls for AssetMoveCallsTsSdk { + type Error = TsSdkError; + + fn new_asset( + inner: T, + mutable: bool, + transferable: bool, + deletable: bool, + package: ObjectID, + ) -> Result { + let inner_bytes = bcs::to_bytes(&inner).map_err(|_| CommandArgumentError::InvalidBCSBytes)?; + let inner_type = T::move_type(package).to_string(); + let package = package.to_string(); + + futures::executor::block_on(new_asset( + &inner_bytes, + &inner_type, + mutable, + transferable, + deletable, + &package, + )) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn delete(asset: ObjectRef, package: ObjectID) -> Result { + let asset = asset.into(); + let asset_type = T::move_type(package).to_string(); + let package = package.to_string(); + + futures::executor::block_on(delete(asset, &asset_type, &package)) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn transfer( + asset: ObjectRef, + recipient: IotaAddress, + package: ObjectID, + ) -> Result { + let asset = asset.into(); + let asset_type = T::move_type(package).to_string(); + let recipient = recipient.to_string(); + let package = package.to_string(); + + futures::executor::block_on(transfer(asset, &asset_type, &recipient, &package)) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn make_tx( + _proposal: (ObjectID, SequenceNumber), + _cap: ObjectRef, + _asset: ObjectRef, + _asset_type_param: TypeTag, + _package: ObjectID, + _function_name: &'static str, + ) -> Result { + unimplemented!(); + } + + fn accept_proposal( + proposal: (ObjectID, SequenceNumber), + recipient_cap: ObjectRef, + asset: ObjectRef, + asset_type: TypeTag, + package: ObjectID, + ) -> Result { + let proposal = (proposal.0, proposal.1, true).into(); + let asset = asset.into(); + let asset_type = asset_type.to_canonical_string(true); + let recipient = recipient_cap.into(); + let package = package.to_string(); + + futures::executor::block_on(accept_proposal(proposal, recipient, asset, &asset_type, &package)) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn conclude_or_cancel( + proposal: (ObjectID, SequenceNumber), + sender_cap: ObjectRef, + asset: ObjectRef, + asset_type: TypeTag, + package: ObjectID, + ) -> Result { + let proposal = (proposal.0, proposal.1, true).into(); + let asset = asset.into(); + let asset_type = asset_type.to_canonical_string(true); + let sender = sender_cap.into(); + let package = package.to_string(); + + futures::executor::block_on(conclude_or_cancel(proposal, sender, asset, &asset_type, &package)) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn update( + asset: ObjectRef, + new_content: T, + package: ObjectID, + ) -> Result { + let asset = asset.into(); + let content_type = T::move_type(package).to_string(); + let content = bcs::to_bytes(&new_content).map_err(|_| CommandArgumentError::InvalidBCSBytes)?; + let package = package.to_string(); + + futures::executor::block_on(update(asset, &content, &content_type, &package)) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } +} diff --git a/bindings/wasm/iota_interaction_ts/src/bindings/keytool_signer.rs b/bindings/wasm/iota_interaction_ts/src/bindings/keytool_signer.rs new file mode 100644 index 0000000000..9538f8967a --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/bindings/keytool_signer.rs @@ -0,0 +1,103 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::KeytoolSigner; +use identity_iota_interaction::KeytoolSignerBuilder; +use secret_storage::Signer; +use serde_json::Value; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsError; + +use super::WasmIotaSignature; +use super::WasmPublicKey; + +#[wasm_bindgen(module = buffer)] +extern "C" { + #[wasm_bindgen(typescript_type = Buffer)] + type NodeBuffer; + #[wasm_bindgen(method, js_name = toString)] + fn to_string(this: &NodeBuffer) -> String; +} + +#[wasm_bindgen(module = child_process)] +extern "C" { + #[wasm_bindgen(js_name = execSync, catch)] + fn exec_cli_cmd(cmd: &str) -> Result; +} + +#[wasm_bindgen(js_name = KeytoolSigner)] +pub struct WasmKeytoolSigner(pub(crate) KeytoolSigner); + +#[wasm_bindgen(js_class = KeytoolSigner)] +impl WasmKeytoolSigner { + #[wasm_bindgen(js_name = create)] + pub async fn new(address: Option, iota_bin_location: Option) -> Result { + let address = address + .as_deref() + .map(IotaAddress::from_str) + .transpose() + .wasm_result()?; + + let builder = address + .map(|address| KeytoolSignerBuilder::new().with_address(address)) + .unwrap_or_default(); + let builder = if let Some(iota_bin_location) = iota_bin_location { + builder.iota_bin_location(iota_bin_location) + } else { + builder + }; + + Ok(WasmKeytoolSigner(builder.build().await.wasm_result()?)) + } + + #[wasm_bindgen] + pub fn address(&self) -> String { + self.0.address().to_string() + } + + // These method definition are needed to make sure `KeytoolSigner` + // implements `Signer` interface. + + #[wasm_bindgen(js_name = keyId)] + pub fn key_id(&self) -> String { + self.address() + } + + #[wasm_bindgen(js_name = publicKey)] + pub async fn public_key(&self) -> Result { + self.0.public_key().try_into() + } + + #[wasm_bindgen] + pub async fn sign(&self, data: Vec) -> Result { + self + .0 + .sign(&data) + .await + .map_err(|e| JsError::new(&e.to_string()).into()) + .and_then(|sig| sig.try_into()) + } + + #[wasm_bindgen(js_name = iotaPublicKeyBytes)] + pub async fn iota_public_key_bytes(&self) -> Vec { + let pk = self.0.public_key(); + let mut bytes = vec![pk.flag()]; + bytes.extend_from_slice(pk.as_ref()); + + bytes + } +} + +// This is used in KeytoolSigner implementation to issue CLI commands. +#[no_mangle] +pub extern "Rust" fn __wasm_exec_iota_cmd(cmd: &str) -> anyhow::Result { + let output = exec_cli_cmd(cmd) + .map_err(|e| anyhow::anyhow!("exec failed: {e:?}"))? + .to_string(); + serde_json::from_str(&output).map_err(|_| anyhow::anyhow!("failed to deserialize JSON object from command output")) +} diff --git a/bindings/wasm/iota_interaction_ts/src/bindings/mod.rs b/bindings/wasm/iota_interaction_ts/src/bindings/mod.rs new file mode 100644 index 0000000000..582053e30d --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/bindings/mod.rs @@ -0,0 +1,13 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "keytool-signer")] +mod keytool_signer; +mod types; +mod wasm_iota_client; +mod wasm_types; + +#[cfg(feature = "keytool-signer")] +pub use keytool_signer::*; +pub use types::*; +pub use wasm_iota_client::*; diff --git a/bindings/wasm/iota_interaction_ts/src/bindings/types.rs b/bindings/wasm/iota_interaction_ts/src/bindings/types.rs new file mode 100644 index 0000000000..fe0645f61b --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/bindings/types.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use wasm_bindgen::prelude::*; + +pub use super::wasm_types::*; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "Promise")] + pub type PromiseBalance; +} diff --git a/bindings/wasm/iota_interaction_ts/src/bindings/wasm_iota_client.rs b/bindings/wasm/iota_interaction_ts/src/bindings/wasm_iota_client.rs new file mode 100644 index 0000000000..f41b201a22 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/bindings/wasm_iota_client.rs @@ -0,0 +1,431 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::error::Error as IotaRpcError; +use identity_iota_interaction::error::IotaRpcResult; +use identity_iota_interaction::generated_types::ExecuteTransactionBlockParams; +use identity_iota_interaction::generated_types::GetCoinsParams; +use identity_iota_interaction::generated_types::GetDynamicFieldObjectParams; +use identity_iota_interaction::generated_types::GetObjectParams; +use identity_iota_interaction::generated_types::GetOwnedObjectsParams; +use identity_iota_interaction::generated_types::GetTransactionBlockParams; +use identity_iota_interaction::generated_types::QueryEventsParams; +use identity_iota_interaction::generated_types::SortOrder; +use identity_iota_interaction::generated_types::WaitForTransactionParams; +use identity_iota_interaction::rpc_types::CoinPage; +use identity_iota_interaction::rpc_types::EventFilter; +use identity_iota_interaction::rpc_types::EventPage; +use identity_iota_interaction::rpc_types::IotaObjectDataOptions; +use identity_iota_interaction::rpc_types::IotaObjectResponse; +use identity_iota_interaction::rpc_types::IotaObjectResponseQuery; +use identity_iota_interaction::rpc_types::IotaPastObjectResponse; +use identity_iota_interaction::rpc_types::IotaTransactionBlockResponseOptions; +use identity_iota_interaction::rpc_types::ObjectsPage; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::SequenceNumber; +use identity_iota_interaction::types::digests::TransactionDigest; +use identity_iota_interaction::types::dynamic_field::DynamicFieldName; +use identity_iota_interaction::types::event::EventID; +use identity_iota_interaction::types::quorum_driver_types::ExecuteTransactionRequestType; +use identity_iota_interaction::SignatureBcs; +use identity_iota_interaction::TransactionDataBcs; +use js_sys::Promise; +use serde::Serialize; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; + +use super::wasm_types::PromiseIotaTransactionBlockResponse; +use super::wasm_types::WasmExecuteTransactionBlockParams; +use super::wasm_types::WasmIotaTransactionBlockResponseWrapper; +use super::WasmWaitForTransactionParams; + +use crate::bindings::PromiseIotaObjectResponse; +use crate::bindings::PromiseObjectRead; +use crate::bindings::PromisePaginatedCoins; +use crate::bindings::PromisePaginatedEvents; +use crate::bindings::PromisePaginatedObjectsResponse; +use crate::bindings::WasmGetCoinsParams; +use crate::bindings::WasmGetDynamicFieldObjectParams; +use crate::bindings::WasmGetObjectParams; +use crate::bindings::WasmGetOwnedObjectsParams; +use crate::bindings::WasmGetTransactionBlockParams; +use crate::bindings::WasmQueryEventsParams; +use crate::bindings::WasmTryGetPastObjectParams; +use crate::common::types::PromiseString; +use crate::common::PromiseBigint; +use crate::console_log; +use crate::error::into_ts_sdk_result; +use crate::error::TsSdkError; + +// This file contains the wasm-bindgen 'glue code' providing +// the interface of the TS Iota client to rust code. + +// The typescript declarations imported in the following typescript_custom_section +// can be used as arguments for rust functions via the typescript_type annotation. +// In other words: The typescript_type "IotaClient" is imported here to be bound +// to the WasmIotaClient functions below. +// TODO: check why this isn't done by `module` macro attribute for `WasmIotaClient` +#[wasm_bindgen(typescript_custom_section)] +const IOTA_CLIENT_TYPE: &'static str = r#" + import { IotaClient } from "@iota/iota-sdk/client"; +"#; + +#[wasm_bindgen(module = "@iota/iota-sdk/client")] +extern "C" { + #[wasm_bindgen(typescript_type = "IotaClient")] + #[derive(Clone)] + pub type WasmIotaClient; + + #[wasm_bindgen(method, js_name = getChainIdentifier)] + pub fn get_chain_identifier(this: &WasmIotaClient) -> PromiseString; + + #[wasm_bindgen(method, js_name = executeTransactionBlock)] + pub fn execute_transaction_block( + this: &WasmIotaClient, + params: &WasmExecuteTransactionBlockParams, + ) -> PromiseIotaTransactionBlockResponse; + + #[wasm_bindgen(method, js_name = getDynamicFieldObject)] + pub fn get_dynamic_field_object( + this: &WasmIotaClient, + input: &WasmGetDynamicFieldObjectParams, + ) -> PromiseIotaObjectResponse; + + #[wasm_bindgen(method, js_name = getObject)] + pub fn get_object(this: &WasmIotaClient, input: &WasmGetObjectParams) -> PromiseIotaObjectResponse; + + #[wasm_bindgen(method, js_name = getOwnedObjects)] + pub fn get_owned_objects(this: &WasmIotaClient, input: &WasmGetOwnedObjectsParams) + -> PromisePaginatedObjectsResponse; + + #[wasm_bindgen(method, js_name = getTransactionBlock)] + pub fn get_transaction_block( + this: &WasmIotaClient, + input: &WasmGetTransactionBlockParams, + ) -> PromiseIotaTransactionBlockResponse; + + #[wasm_bindgen(method, js_name = getReferenceGasPrice)] + pub fn get_reference_gas_price(this: &WasmIotaClient) -> PromiseBigint; + + #[wasm_bindgen(method, js_name = tryGetPastObject)] + pub fn try_get_past_object(this: &WasmIotaClient, input: &WasmTryGetPastObjectParams) -> PromiseObjectRead; + + #[wasm_bindgen(method, js_name = queryEvents)] + pub fn query_events(this: &WasmIotaClient, input: &WasmQueryEventsParams) -> PromisePaginatedEvents; + + #[wasm_bindgen(method, js_name = getCoins)] + pub fn get_coins(this: &WasmIotaClient, input: &WasmGetCoinsParams) -> PromisePaginatedCoins; + + #[wasm_bindgen(method, js_name = waitForTransaction)] + pub fn wait_for_transaction( + this: &WasmIotaClient, + input: &WasmWaitForTransactionParams, + ) -> PromiseIotaTransactionBlockResponse; +} + +// Helper struct used to convert TYPESCRIPT types to RUST types +#[derive(Clone)] +pub struct ManagedWasmIotaClient(pub(crate) WasmIotaClient); + +// convert TYPESCRIPT types to RUST types +impl ManagedWasmIotaClient { + pub fn new(iota_client: WasmIotaClient) -> Self { + ManagedWasmIotaClient(iota_client) + } + + pub async fn get_chain_identifier(&self) -> Result { + let promise: Promise = Promise::resolve(&WasmIotaClient::get_chain_identifier(&self.0)); + into_ts_sdk_result(JsFuture::from(promise).await) + } + + pub async fn execute_transaction_block( + &self, + tx_data_bcs: &TransactionDataBcs, + signatures: &[SignatureBcs], + options: Option, + request_type: Option, + ) -> IotaRpcResult { + let ex_tx_params: WasmExecuteTransactionBlockParams = serde_wasm_bindgen::to_value( + &ExecuteTransactionBlockParams::new(tx_data_bcs, signatures, options, request_type), + ) + .map_err(|e| { + console_log!( + "Error executing serde_wasm_bindgen::to_value(ExecuteTransactionBlockParams): {:?}", + e + ); + IotaRpcError::FfiError(format!("{:?}", e)) + })? + .into(); + + let promise: Promise = Promise::resolve(&WasmIotaClient::execute_transaction_block(&self.0, &ex_tx_params)); + let result: JsValue = JsFuture::from(promise).await.map_err(|e| { + console_log!("Error executing JsFuture::from(promise): {:?}", e); + IotaRpcError::FfiError(format!("{:?}", e)) + })?; + Ok(WasmIotaTransactionBlockResponseWrapper::new(result.into())) + } + + /** + * Return the dynamic field object information for a specified object + */ + pub async fn get_dynamic_field_object( + &self, + parent_object_id: ObjectID, + name: DynamicFieldName, + ) -> IotaRpcResult { + let params: WasmGetDynamicFieldObjectParams = + serde_wasm_bindgen::to_value(&GetDynamicFieldObjectParams::new(parent_object_id.to_string(), name)) + .map_err(|e| { + console_log!( + "Error executing serde_wasm_bindgen::to_value(WasmGetDynamicFieldObjectParams): {:?}", + e + ); + IotaRpcError::FfiError(format!("{:?}", e)) + })? + .into(); + + let promise: Promise = Promise::resolve(&WasmIotaClient::get_dynamic_field_object(&self.0, ¶ms)); + let result: JsValue = JsFuture::from(promise).await.map_err(|e| { + console_log!("Error executing JsFuture::from(promise): {:?}", e); + IotaRpcError::FfiError(format!("{:?}", e)) + })?; + + #[allow(deprecated)] // will be refactored + Ok(result.into_serde()?) + } + + pub async fn get_object_with_options( + &self, + object_id: ObjectID, + options: IotaObjectDataOptions, + ) -> IotaRpcResult { + let params: WasmGetObjectParams = + serde_wasm_bindgen::to_value(&GetObjectParams::new(object_id.to_string(), Some(options))) + .map_err(|e| { + console_log!( + "Error executing serde_wasm_bindgen::to_value(WasmIotaObjectDataOptions): {:?}", + e + ); + IotaRpcError::FfiError(format!("{:?}", e)) + })? + .into(); + + let promise: Promise = Promise::resolve(&WasmIotaClient::get_object(&self.0, ¶ms)); + let result: JsValue = JsFuture::from(promise).await.map_err(|e| { + console_log!("Error executing JsFuture::from(promise): {:?}", e); + IotaRpcError::FfiError(format!("{:?}", e)) + })?; + + #[allow(deprecated)] // will be refactored + Ok(result.into_serde()?) + } + + pub async fn get_owned_objects( + &self, + address: IotaAddress, + query: Option, + cursor: Option, + limit: Option, + ) -> IotaRpcResult { + let params: WasmGetOwnedObjectsParams = serde_wasm_bindgen::to_value(&GetOwnedObjectsParams::new( + address.to_string(), + cursor.map(|v| v.to_string()), + limit, + query.clone().map(|v| v.filter).flatten(), + query.clone().map(|v| v.options).flatten(), + )) + .map_err(|e| { + console_log!( + "Error executing serde_wasm_bindgen::to_value(WasmIotaObjectDataOptions): {:?}", + e + ); + IotaRpcError::FfiError(format!("{:?}", e)) + })? + .into(); + + let promise: Promise = Promise::resolve(&WasmIotaClient::get_owned_objects(&self.0, ¶ms)); + let result: JsValue = JsFuture::from(promise).await.map_err(|e| { + console_log!("Error executing JsFuture::from(promise): {:?}", e); + IotaRpcError::FfiError(format!("{:?}", e)) + })?; + + #[allow(deprecated)] // will be refactored + Ok(result.into_serde()?) + } + + pub async fn get_transaction_with_options( + &self, + digest: TransactionDigest, + options: IotaTransactionBlockResponseOptions, + ) -> IotaRpcResult { + let params: WasmGetTransactionBlockParams = + serde_wasm_bindgen::to_value(&GetTransactionBlockParams::new(digest.to_string(), Some(options))) + .map_err(|e| { + console_log!( + "Error executing serde_wasm_bindgen::to_value(WasmIotaObjectDataOptions): {:?}", + e + ); + IotaRpcError::FfiError(format!("{:?}", e)) + })? + .into(); + + // Rust `ReadApi::get_transaction_with_options` calls `get_transaction_block` via http while + // TypeScript uses the name `getTransactionBlock` directly, so we have to call this function + let promise: Promise = Promise::resolve(&WasmIotaClient::get_transaction_block(&self.0, ¶ms)); + let result: JsValue = JsFuture::from(promise).await.map_err(|e| { + console_log!("Error executing JsFuture::from(promise): {:?}", e); + IotaRpcError::FfiError(format!("{:?}", e)) + })?; + + Ok(WasmIotaTransactionBlockResponseWrapper::new(result.into())) + } + + pub async fn get_reference_gas_price(&self) -> IotaRpcResult { + let promise: Promise = Promise::resolve(&WasmIotaClient::get_reference_gas_price(&self.0)); + let result: JsValue = JsFuture::from(promise).await.map_err(|e| { + console_log!("Error executing JsFuture::from(promise): {:?}", e); + IotaRpcError::FfiError(format!("{:?}", e)) + })?; + + #[allow(deprecated)] // will be refactored + Ok(result.into_serde()?) + } + + pub async fn try_get_parsed_past_object( + &self, + _object_id: ObjectID, + _version: SequenceNumber, + _options: IotaObjectDataOptions, + ) -> IotaRpcResult { + // TODO: does not work anymore, find out, why we need to pass a different `SequenceNumber` now + unimplemented!("try_get_parsed_past_object"); + // let params: WasmTryGetPastObjectParams = serde_wasm_bindgen::to_value(&TryGetPastObjectParams::new( + // object_id.to_string(), + // version, + // Some(options), + // )) + // .map_err(|e| { + // console_log!( + // "Error executing serde_wasm_bindgen::to_value(WasmIotaObjectDataOptions): {:?}", + // e + // ); + // IotaRpcError::FfiError(format!("{:?}", e)) + // })? + // .into(); + + // let promise: Promise = Promise::resolve(&WasmIotaClient::try_get_past_object(&self.0, ¶ms)); + // let result: JsValue = JsFuture::from(promise).await.map_err(|e| { + // console_log!("Error executing JsFuture::from(promise): {:?}", e); + // IotaRpcError::FfiError(format!("{:?}", e)) + // })?; + + // Ok(result.into_serde()?) + } + + pub async fn query_events( + &self, + query: EventFilter, + cursor: Option, + limit: Option, + descending_order: bool, + ) -> IotaRpcResult { + let params: WasmQueryEventsParams = serde_wasm_bindgen::to_value(&QueryEventsParams::new( + query, + cursor, + limit, + Some(SortOrder::new(descending_order)), + )) + .map_err(|e| { + console_log!( + "Error executing serde_wasm_bindgen::to_value(WasmIotaObjectDataOptions): {:?}", + e + ); + IotaRpcError::FfiError(format!("{:?}", e)) + })? + .into(); + + let promise: Promise = Promise::resolve(&WasmIotaClient::query_events(&self.0, ¶ms)); + let result: JsValue = JsFuture::from(promise).await.map_err(|e| { + console_log!("Error executing JsFuture::from(promise): {:?}", e); + IotaRpcError::FfiError(format!("{:?}", e)) + })?; + + #[allow(deprecated)] // will be refactored + Ok(result.into_serde()?) + } + + pub async fn get_coins( + &self, + owner: IotaAddress, + coin_type: Option, + cursor: Option, + limit: Option, + ) -> IotaRpcResult { + let params: WasmGetCoinsParams = serde_wasm_bindgen::to_value(&GetCoinsParams::new( + owner.to_string(), + coin_type.map(|v| v.to_string()), + cursor.map(|v| v.to_string()), + limit, + )) + .map_err(|e| { + console_log!( + "Error executing serde_wasm_bindgen::to_value(WasmIotaObjectDataOptions): {:?}", + e + ); + IotaRpcError::FfiError(format!("{:?}", e)) + })? + .into(); + + let promise: Promise = Promise::resolve(&WasmIotaClient::get_coins(&self.0, ¶ms)); + let result: JsValue = JsFuture::from(promise).await.map_err(|e| { + console_log!("Error executing JsFuture::from(promise): {:?}", e); + IotaRpcError::FfiError(format!("{:?}", e)) + })?; + + #[allow(deprecated)] // will be refactored + Ok(result.into_serde()?) + } + + /// Wait for a transaction block result to be available over the API. + /// This can be used in conjunction with `execute_transaction_block` to wait for the transaction to + /// be available via the API. + /// This currently polls the `getTransactionBlock` API to check for the transaction. + /// + /// # Arguments + /// + /// * `digest` - The digest of the queried transaction. + /// * `options` - Options for specifying the content to be returned. + /// * `timeout` - The amount of time to wait for a transaction block. Defaults to one minute. + /// * `poll_interval` - The amount of time to wait between checks for the transaction block. Defaults to 2 seconds. + pub async fn wait_for_transaction( + &self, + digest: TransactionDigest, + options: Option, + timeout: Option, + poll_interval: Option, + ) -> IotaRpcResult { + let params_object = WaitForTransactionParams::new(digest.to_string(), options, timeout, poll_interval); + let params: WasmWaitForTransactionParams = serde_json::to_value(¶ms_object) + .map_err(|e| { + console_log!("Error serializing WaitForTransactionParams to Value: {:?}", e); + IotaRpcError::FfiError(format!("{:?}", e)) + }) + .and_then(|v| { + v.serialize(&serde_wasm_bindgen::Serializer::json_compatible()) + .map_err(|e| { + console_log!("Error serializing Value to WasmWaitForTransactionParams: {:?}", e); + IotaRpcError::FfiError(format!("{:?}", e)) + }) + })? + .into(); + + let promise: Promise = Promise::resolve(&WasmIotaClient::wait_for_transaction(&self.0, ¶ms)); + let result: JsValue = JsFuture::from(promise).await.map_err(|e| { + console_log!("Error executing JsFuture::from(promise): {:?}", e); + IotaRpcError::FfiError(format!("{:?}", e)) + })?; + + Ok(WasmIotaTransactionBlockResponseWrapper::new(result.into())) + } +} diff --git a/bindings/wasm/iota_interaction_ts/src/bindings/wasm_types.rs b/bindings/wasm/iota_interaction_ts/src/bindings/wasm_types.rs new file mode 100644 index 0000000000..83c1a5d91d --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/bindings/wasm_types.rs @@ -0,0 +1,534 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +use std::str::FromStr; + +use fastcrypto::encoding::Base64; +use fastcrypto::encoding::Encoding; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::base_types::SequenceNumber; +use identity_iota_interaction::types::crypto::PublicKey; +use identity_iota_interaction::types::crypto::Signature; +use identity_iota_interaction::types::crypto::SignatureScheme; +use identity_iota_interaction::types::digests::TransactionDigest; +use identity_iota_interaction::types::execution_status::CommandArgumentError; +use identity_iota_interaction::types::execution_status::ExecutionStatus; +use identity_iota_interaction::types::object::Owner; +use identity_iota_interaction::ProgrammableTransactionBcs; +use js_sys::Promise; +use js_sys::Uint8Array; +use serde::Deserialize; +use serde::Serialize; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use wasm_bindgen::JsError; +use wasm_bindgen::JsValue; +use wasm_bindgen_futures::JsFuture; + +use crate::bindings::WasmIotaClient; +use crate::common::into_sdk_type; +use crate::common::PromiseUint8Array; +use crate::console_log; +use crate::error::TsSdkError; +use crate::error::WasmError; + +// TODO: fix/add signer or remove functions relying on it +type WasmStorageSigner = (); + +#[wasm_bindgen(typescript_custom_section)] +const TS_SDK_TYPES: &str = r#" + import { + Balance, + ExecuteTransactionBlockParams, + GetCoinsParams, + GetDynamicFieldObjectParams, + GetObjectParams, + GetOwnedObjectsParams, + GetTransactionBlockParams, + IotaClient, + IotaObjectData, + IotaObjectResponse, + IotaTransactionBlockResponse, + IotaTransactionBlockResponseOptions, + ObjectRead, + PaginatedCoins, + PaginatedEvents, + PaginatedObjectsResponse, + QueryEventsParams, + TryGetPastObjectParams, + } from "@iota/iota-sdk/client"; + import { bcs } from "@iota/iota-sdk/bcs"; + import { + executeTransaction, + WasmIotaTransactionBlockResponseWrapper, + } from "./iota_client_helpers" +"#; + +#[wasm_bindgen(module = "@iota/iota-sdk/client")] +extern "C" { + #[wasm_bindgen(typescript_type = "Promise")] + pub type PromiseBalance; + + #[wasm_bindgen(typescript_type = "TransactionArgument")] + pub type WasmTransactionArgument; + + #[wasm_bindgen(typescript_type = "IotaObjectData")] + pub type WasmIotaObjectData; + + #[wasm_bindgen(typescript_type = "ExecuteTransactionBlockParams")] + #[derive(Clone)] + pub type WasmExecuteTransactionBlockParams; + + #[wasm_bindgen(typescript_type = "IotaTransactionBlockResponseOptions")] + #[derive(Clone)] + pub type WasmIotaTransactionBlockResponseOptions; + + #[wasm_bindgen(typescript_type = "IotaTransactionBlockResponse")] + #[derive(Clone)] + pub type WasmIotaTransactionBlockResponse; + + #[wasm_bindgen(typescript_type = "GetDynamicFieldObjectParams")] + #[derive(Clone)] + pub type WasmGetDynamicFieldObjectParams; + + #[wasm_bindgen(typescript_type = "GetObjectParams")] + #[derive(Clone)] + pub type WasmGetObjectParams; + + #[wasm_bindgen(typescript_type = "Promise")] + #[derive(Clone)] + pub type PromiseIotaTransactionBlockResponse; + + #[wasm_bindgen(typescript_type = "Promise")] + #[derive(Clone)] + pub type PromiseIotaObjectResponse; + + #[wasm_bindgen(typescript_type = "GetOwnedObjectsParams")] + #[derive(Clone)] + pub type WasmGetOwnedObjectsParams; + + #[wasm_bindgen(typescript_type = "GetTransactionBlockParams")] + #[derive(Clone)] + pub type WasmGetTransactionBlockParams; + + #[wasm_bindgen(typescript_type = "Promise")] + #[derive(Clone)] + pub type PromisePaginatedObjectsResponse; + + #[wasm_bindgen(typescript_type = "TryGetPastObjectParams")] + #[derive(Clone)] + pub type WasmTryGetPastObjectParams; + + #[wasm_bindgen(typescript_type = "Promise")] + #[derive(Clone)] + pub type PromiseObjectRead; + + #[wasm_bindgen(typescript_type = "ExecutionStatus")] + #[derive(Clone)] + pub type WasmExecutionStatus; + + #[wasm_bindgen(typescript_type = "ObjectRef")] + #[derive(Clone)] + pub type WasmObjectRef; + + #[wasm_bindgen(typescript_type = "SharedObjectRef")] + #[derive(Clone)] + pub type WasmSharedObjectRef; + + #[wasm_bindgen(typescript_type = "OwnedObjectRef")] + #[derive(Clone)] + pub type WasmOwnedObjectRef; + + #[wasm_bindgen(typescript_type = "QueryEventsParams")] + #[derive(Clone)] + pub type WasmQueryEventsParams; + + #[wasm_bindgen(typescript_type = "Promise")] + #[derive(Clone)] + pub type PromisePaginatedEvents; + + #[wasm_bindgen(typescript_type = "GetCoinsParams")] + #[derive(Clone)] + pub type WasmGetCoinsParams; + + #[wasm_bindgen(typescript_type = "Promise")] + #[derive(Clone)] + pub type PromisePaginatedCoins; + + #[wasm_bindgen(typescript_type = "Promise")] + #[derive(Clone)] + pub type PromiseIotaTransactionBlockResponseWrapper; + + #[wasm_bindgen(typescript_type = "Signature")] + pub type WasmIotaSignature; + + #[wasm_bindgen(typescript_type = "Parameters")] + #[derive(Clone, Debug)] + pub type WasmWaitForTransactionParams; +} + +#[derive(Serialize, Deserialize)] +enum IotaSignatureHelper { + Ed25519IotaSignature(String), + Secp256k1IotaSignature(String), + Secp256r1IotaSignature(String), +} + +impl TryFrom for WasmIotaSignature { + type Error = JsValue; + fn try_from(sig: Signature) -> Result { + let base64sig = Base64::encode(&sig); + let json_signature = match sig { + Signature::Ed25519IotaSignature(_) => IotaSignatureHelper::Ed25519IotaSignature(base64sig), + Signature::Secp256r1IotaSignature(_) => IotaSignatureHelper::Secp256r1IotaSignature(base64sig), + Signature::Secp256k1IotaSignature(_) => IotaSignatureHelper::Secp256k1IotaSignature(base64sig), + }; + + json_signature + .serialize(&serde_wasm_bindgen::Serializer::json_compatible()) + .map(JsCast::unchecked_into) + .map_err(|e| e.into()) + } +} + +impl TryFrom for Signature { + type Error = JsValue; + fn try_from(sig: WasmIotaSignature) -> Result { + let sig_helper = serde_wasm_bindgen::from_value(sig.into())?; + let base64sig = match sig_helper { + IotaSignatureHelper::Ed25519IotaSignature(s) => s, + IotaSignatureHelper::Secp256k1IotaSignature(s) => s, + IotaSignatureHelper::Secp256r1IotaSignature(s) => s, + }; + + base64sig + .parse() + .map_err(|e: eyre::Report| JsError::new(&e.to_string()).into()) + } +} + +#[wasm_bindgen(module = "@iota/iota-sdk/transactions")] +extern "C" { + #[wasm_bindgen(typescript_type = "Transaction")] + pub type WasmTransactionBuilder; + + #[wasm_bindgen(js_name = "from", js_class = "Transaction", static_method_of = WasmTransactionBuilder, catch)] + pub fn from_bcs_bytes(bytes: Uint8Array) -> Result; + + #[wasm_bindgen(method, structural, catch)] + pub async fn build(this: &WasmTransactionBuilder) -> Result; + + // TODO: decide if we need the following functions: "yagni" or not? + + // #[wasm_bindgen(js_name = "setSender", method, catch)] + // pub fn set_sender(this: &WasmTransactionBuilder, address: String) -> Result<(), JsValue>; + + // #[wasm_bindgen(js_name = "setGasOwner", method, catch)] + // pub fn set_gas_owner(this: &WasmTransactionBuilder, address: String) -> Result<(), JsValue>; + + // #[wasm_bindgen(js_name = "setGasPrice", method, catch)] + // pub fn set_gas_price(this: &WasmTransactionBuilder, price: u64) -> Result<(), JsValue>; + + // #[wasm_bindgen(js_name = "setGasPayment", method, catch)] + // pub fn set_gas_payment(this: &WasmTransactionBuilder, payments: Vec) -> Result<(), JsValue>; + + // #[wasm_bindgen(js_name = "setGasBudget", method, catch)] + // pub fn set_gas_budget(this: &WasmTransactionBuilder, budget: u64) -> Result<(), JsValue>; + + // #[wasm_bindgen(js_name = "getData", method, catch)] + // pub fn get_data(this: &WasmTransactionBuilder) -> Result; +} + +#[wasm_bindgen(module = "@iota/iota-sdk/cryptography")] +extern "C" { + #[wasm_bindgen(typescript_type = PublicKey)] + pub type WasmPublicKey; + + #[wasm_bindgen(js_name = toRawBytes, method)] + pub fn to_raw_bytes(this: &WasmPublicKey) -> Vec; + + #[wasm_bindgen(method)] + pub fn flag(this: &WasmPublicKey) -> u8; +} + +#[wasm_bindgen(module = "@iota/iota-sdk/keypairs/ed25519")] +extern "C" { + #[wasm_bindgen(extends = WasmPublicKey)] + pub type Ed25519PublicKey; + + #[wasm_bindgen(constructor, catch)] + pub fn new_ed25519_pk(bytes: &[u8]) -> Result; +} + +#[wasm_bindgen(module = "@iota/iota-sdk/keypairs/secp256r1")] +extern "C" { + #[wasm_bindgen(extends = WasmPublicKey)] + pub type Secp256r1PublicKey; + + #[wasm_bindgen(constructor, catch)] + pub fn new_secp256r1_pk(bytes: &[u8]) -> Result; +} + +#[wasm_bindgen(module = "@iota/iota-sdk/keypairs/secp256k1")] +extern "C" { + #[wasm_bindgen(extends = WasmPublicKey)] + pub type Secp256k1PublicKey; + + #[wasm_bindgen(constructor, catch)] + pub fn new_secp256k1_pk(bytes: &[u8]) -> Result; +} + +impl TryFrom<&'_ PublicKey> for WasmPublicKey { + type Error = JsValue; + fn try_from(pk: &PublicKey) -> Result { + let pk_bytes = pk.as_ref(); + let wasm_pk: WasmPublicKey = match pk { + PublicKey::Ed25519(_) => Ed25519PublicKey::new_ed25519_pk(pk_bytes)?.into(), + PublicKey::Secp256r1(_) => Secp256r1PublicKey::new_secp256r1_pk(pk_bytes)?.into(), + PublicKey::Secp256k1(_) => Secp256k1PublicKey::new_secp256k1_pk(pk_bytes)?.into(), + _ => return Err(JsError::new("unsupported PublicKey type").into()), + }; + + assert_eq!(pk_bytes, &wasm_pk.to_raw_bytes()); + + Ok(wasm_pk) + } +} + +impl TryFrom for PublicKey { + type Error = JsValue; + fn try_from(pk: WasmPublicKey) -> Result { + let key_bytes = pk.to_raw_bytes(); + let key_flag = pk.flag(); + let signature_scheme = SignatureScheme::from_flag_byte(&key_flag).map_err(|e| JsError::new(&e.to_string()))?; + let public_key = + PublicKey::try_from_bytes(signature_scheme, &key_bytes).map_err(|e| JsError::new(&e.to_string()))?; + + assert_eq!(&key_bytes, public_key.as_ref()); + + Ok(public_key) + } +} + +impl From for WasmObjectRef { + fn from(value: ObjectRef) -> Self { + let json_obj = serde_json::json!({ + "objectId": value.0, + "version": value.1, + "digest": value.2, + }); + + json_obj + .serialize(&serde_wasm_bindgen::Serializer::json_compatible()) + .expect("a JSON object is a JS value") + // safety: `json_obj` was constructed following TS ObjectRef's interface. + .unchecked_into() + } +} + +impl From<(ObjectID, SequenceNumber, bool)> for WasmSharedObjectRef { + fn from(value: (ObjectID, SequenceNumber, bool)) -> Self { + let json_obj = serde_json::json!({ + "objectId": value.0, + "initialSharedVersion": value.1, + "mutable": value.2, + }); + + json_obj + .serialize(&serde_wasm_bindgen::Serializer::json_compatible()) + .expect("a JSON object is a JS value") + // safety: `json_obj` was constructed following TS SharedObjectRef's interface. + .unchecked_into() + } +} + +impl TryFrom for WasmSharedObjectRef { + type Error = TsSdkError; + fn try_from(value: OwnedObjectRef) -> Result { + let Owner::Shared { initial_shared_version } = value.owner else { + return Err(TsSdkError::CommandArgumentError(CommandArgumentError::TypeMismatch)); + }; + let obj_id = value.object_id(); + + Ok((obj_id, initial_shared_version, true).into()) + } +} + +impl WasmSharedObjectRef { + #[allow(dead_code)] + pub(crate) fn immutable(self) -> Self { + const JS_FALSE: JsValue = JsValue::from_bool(false); + + let _ = js_sys::Reflect::set(&self, &JsValue::from_str("mutable"), &JS_FALSE); + self + } +} + +#[wasm_bindgen(module = "@iota/iota-interaction-ts/iota_client_helpers")] +extern "C" { + // Please note: For unclear reasons the `typescript_type` name and the `pub type` name defined + // in wasm_bindgen extern "C" scopes must be equal. Otherwise, the JS constructor will not be + // found in the generated js code. + #[wasm_bindgen(typescript_type = "WasmIotaTransactionBlockResponseWrapper")] + #[derive(Clone)] + pub type WasmIotaTransactionBlockResponseWrapper; + + #[wasm_bindgen(constructor)] + pub fn new(response: WasmIotaTransactionBlockResponse) -> WasmIotaTransactionBlockResponseWrapper; + + #[wasm_bindgen(method)] + pub fn effects_is_none(this: &WasmIotaTransactionBlockResponseWrapper) -> bool; + + #[wasm_bindgen(method)] + pub fn effects_is_some(this: &WasmIotaTransactionBlockResponseWrapper) -> bool; + + #[wasm_bindgen(method)] + pub fn to_string(this: &WasmIotaTransactionBlockResponseWrapper) -> String; + + #[wasm_bindgen(method)] + fn effects_execution_status_inner(this: &WasmIotaTransactionBlockResponseWrapper) -> Option; + + #[wasm_bindgen(method)] + fn effects_created_inner(this: &WasmIotaTransactionBlockResponseWrapper) -> Option>; + + #[wasm_bindgen(method, js_name = "get_digest")] + fn digest_inner(this: &WasmIotaTransactionBlockResponseWrapper) -> String; + + #[wasm_bindgen(method, js_name = "get_response")] + fn response(this: &WasmIotaTransactionBlockResponseWrapper) -> WasmIotaTransactionBlockResponse; + + #[wasm_bindgen(js_name = executeTransaction)] + fn execute_transaction_inner( + iota_client: &WasmIotaClient, // --> TypeScript: IotaClient + sender_address: String, // --> TypeScript: string + tx_bcs: Vec, // --> TypeScript: Uint8Array, + signer: WasmStorageSigner, // --> TypeScript: Signer (iota_client_helpers module) + gas_budget: Option, // --> TypeScript: optional bigint + ) -> PromiseIotaTransactionBlockResponseWrapper; + + #[wasm_bindgen(js_name = "addGasDataToTransaction")] + fn add_gas_data_to_transaction_inner( + iota_client: &WasmIotaClient, // --> TypeScript: IotaClient + sender_address: String, // --> TypeScript: string + tx_bcs: Vec, // --> TypeScript: Uint8Array, + gas_budget: Option, // --> TypeScript: optional bigint + ) -> PromiseUint8Array; + + #[wasm_bindgen(js_name = "sleep")] + fn sleep_inner(ms: i32) -> Promise; +} + +/// Inserts these values into the transaction and replaces placeholder values. +/// +/// - sender (overwritten as we assume a placeholder to be used in prepared transaction) +/// - gas budget (value determined automatically if not provided) +/// - gas price (value determined automatically) +/// - gas coin / payment object (fetched automatically) +/// - gas owner (equals sender) +/// +/// # Arguments +/// +/// * `iota_client` - client instance +/// * `sender_address` - transaction sender (and the one paying for it) +/// * `tx_bcs` - transaction data serialized to bcs, most probably having placeholder values +/// * `gas_budget` - optional fixed gas budget, determined automatically with a dry run if not provided +pub(crate) async fn add_gas_data_to_transaction( + iota_client: &WasmIotaClient, + sender_address: IotaAddress, + tx_bcs: Vec, + gas_budget: Option, +) -> Result, TsSdkError> { + let promise: Promise = Promise::resolve(&add_gas_data_to_transaction_inner( + iota_client, + sender_address.to_string(), + tx_bcs, + gas_budget, + )); + let value: JsValue = JsFuture::from(promise).await.map_err(|e| { + let message = "Error executing JsFuture::from(promise) for `add_gas_data_to_transaction`"; + let details = format!("{e:?}"); + console_log!("{message}; {details}"); + TsSdkError::WasmError(message.to_string(), details.to_string()) + })?; + + Ok(Uint8Array::new(&value).to_vec()) +} + +/// Helper function to pause execution. +pub async fn sleep(duration_ms: i32) -> Result<(), JsValue> { + let promise = sleep_inner(duration_ms); + let js_fut = JsFuture::from(promise); + js_fut.await?; + Ok(()) +} + +#[derive(Deserialize)] +struct WasmExecutionStatusAdapter { + status: ExecutionStatus, +} + +impl WasmIotaTransactionBlockResponseWrapper { + pub fn effects_execution_status(&self) -> Option { + self.effects_execution_status_inner().map(|s| { + let state: WasmExecutionStatusAdapter = + into_sdk_type(s).expect("[WasmIotaTransactionBlockResponseWrapper] Failed to convert WasmExecutionStatus"); + state.status + }) + } + + pub fn effects_created(&self) -> Option> { + self.effects_created_inner().map(|vex_obj_ref| { + vex_obj_ref + .into_iter() + .map(|obj| { + into_sdk_type(obj).expect("[WasmIotaTransactionBlockResponseWrapper] Failed to convert WasmOwnedObjectRef") + }) + .collect() + }) + } + + pub fn digest(&self) -> Result { + TransactionDigest::from_str(&self.digest_inner()) + .map_err(|err| TsSdkError::WasmError("Failed to parse transaction block digest".to_string(), err.to_string())) + } +} + +pub async fn execute_transaction( + iota_client: &WasmIotaClient, // --> Binding: WasmIotaClient + sender_address: IotaAddress, // --> Binding: String + tx_bcs: ProgrammableTransactionBcs, // --> Binding: Vec + signer: WasmStorageSigner, // --> Binding: WasmStorageSigner + gas_budget: Option, // --> Binding: Option, +) -> Result { + let promise: Promise = Promise::resolve(&execute_transaction_inner( + iota_client, + sender_address.to_string(), + tx_bcs, + signer, + gas_budget, + )); + let result: JsValue = JsFuture::from(promise).await.map_err(|e| { + let message = "Error executing JsFuture::from(promise) for `execute_transaction`"; + let details = format!("{e:?}"); + console_log!("{message}; {details}"); + TsSdkError::WasmError(message.to_string(), details.to_string()) + })?; + + Ok(WasmIotaTransactionBlockResponseWrapper::new(result.into())) +} + +#[derive(Deserialize)] +#[serde(try_from = "Vec")] +pub struct ProgrammableTransaction(pub(crate) WasmTransactionBuilder); +impl TryFrom> for ProgrammableTransaction { + type Error = TsSdkError; + fn try_from(value: Vec) -> Result { + let uint8array: Uint8Array = value.as_slice().into(); + WasmTransactionBuilder::from_bcs_bytes(uint8array) + .map(Self) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } +} diff --git a/bindings/wasm/iota_interaction_ts/src/common/macros.rs b/bindings/wasm/iota_interaction_ts/src/common/macros.rs new file mode 100644 index 0000000000..259fc54171 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/common/macros.rs @@ -0,0 +1,62 @@ +// Copyright 2020-2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use wasm_bindgen::prelude::wasm_bindgen; + +#[macro_export] +macro_rules! log { + ($($tt:tt)*) => { + web_sys::console::log_1(&format!($($tt)*).into()); + } +} + +/// Log to console utility without the need for web_sys dependency +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console, js_name = log)] + pub fn console_log(s: &str); +} + +/// Logging macro without the need for web_sys dependency +#[macro_export] +macro_rules! console_log { + ($($tt:tt)*) => { + crate::common::macros::console_log((format!($($tt)*)).as_str()) + } +} + +#[macro_export] +macro_rules! impl_wasm_clone { + ($wasm_class:ident, $js_class:ident) => { + #[wasm_bindgen(js_class = $js_class)] + impl $wasm_class { + /// Deep clones the object. + #[wasm_bindgen(js_name = clone)] + pub fn deep_clone(&self) -> $wasm_class { + return $wasm_class(self.0.clone()); + } + } + }; +} + +#[macro_export] +macro_rules! impl_wasm_json { + ($wasm_class:ident, $js_class:ident) => { + #[wasm_bindgen(js_class = $js_class)] + impl $wasm_class { + /// Serializes this to a JSON object. + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> $crate::error::Result { + use $crate::error::WasmResult; + JsValue::from_serde(&self.0).wasm_result() + } + + /// Deserializes an instance from a JSON object. + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(json: &JsValue) -> $crate::error::Result<$wasm_class> { + use $crate::error::WasmResult; + json.into_serde().map(Self).wasm_result() + } + } + }; +} diff --git a/bindings/wasm/iota_interaction_ts/src/common/mod.rs b/bindings/wasm/iota_interaction_ts/src/common/mod.rs new file mode 100644 index 0000000000..f764977e2d --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/common/mod.rs @@ -0,0 +1,11 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#[macro_use] +pub mod macros; + +pub mod types; +pub mod utils; + +pub use types::*; +pub use utils::*; diff --git a/bindings/wasm/iota_interaction_ts/src/common/types.rs b/bindings/wasm/iota_interaction_ts/src/common/types.rs new file mode 100644 index 0000000000..aadea097dd --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/common/types.rs @@ -0,0 +1,105 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Object; +use identity_iota_interaction::ProgrammableTransactionBcs; +use js_sys::Promise; +use js_sys::Uint8Array; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::JsFuture; + +use crate::error::TsSdkError; +use crate::error::TsSdkResult; +use crate::error::WasmError; +use crate::error::WasmResult; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "Promise")] + pub type PromiseVoid; + + #[wasm_bindgen(typescript_type = "Promise")] + pub type PromiseBigint; + + #[wasm_bindgen(typescript_type = "Promise")] + pub type PromiseBool; + + #[wasm_bindgen(typescript_type = "Promise")] + pub type PromiseString; + + #[wasm_bindgen(typescript_type = "Promise")] + pub type PromiseOptionString; + + #[wasm_bindgen(typescript_type = "Promise")] + pub type PromiseUint8Array; + + #[wasm_bindgen(typescript_type = "Array")] + pub type ArrayString; + + #[wasm_bindgen(typescript_type = "Map")] + pub type MapStringAny; + + #[wasm_bindgen(typescript_type = "Record")] + pub type RecordStringAny; + + #[wasm_bindgen(typescript_type = "number | number[]")] + pub type UOneOrManyNumber; + + #[wasm_bindgen(typescript_type = "string | string[] | null")] + pub type OptionOneOrManyString; + + #[wasm_bindgen(typescript_type = "VerificationMethod[]")] + pub type ArrayVerificationMethod; + + #[wasm_bindgen(typescript_type = "Array")] + pub type ArrayCoreMethodRef; + + #[wasm_bindgen(typescript_type = "DIDUrl | string")] + pub type UDIDUrlQuery; + + #[wasm_bindgen(typescript_type = "Service[]")] + pub type ArrayService; +} + +impl TryFrom for MapStringAny { + type Error = JsValue; + + fn try_from(properties: Object) -> Result { + MapStringAny::try_from(&properties) + } +} + +impl TryFrom<&Object> for MapStringAny { + type Error = JsValue; + + fn try_from(properties: &Object) -> Result { + let map: js_sys::Map = js_sys::Map::new(); + for (key, value) in properties.iter() { + map.set( + &JsValue::from_str(key.as_str()), + #[allow(deprecated)] // will be refactored + &JsValue::from_serde(&value).wasm_result()?, + ); + } + Ok(map.unchecked_into::()) + } +} + +impl Default for MapStringAny { + fn default() -> Self { + js_sys::Map::new().unchecked_into() + } +} + +impl PromiseUint8Array { + /// Helper function to convert Uint8 arrays from contract calls to the internal `ProgrammableTransactionBcs` type. + pub async fn to_transaction_bcs(&self) -> TsSdkResult { + let promise: Promise = Promise::resolve(self); + JsFuture::from(promise) + .await + .map(|v| Uint8Array::from(v).to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } +} diff --git a/bindings/wasm/iota_interaction_ts/src/common/utils.rs b/bindings/wasm/iota_interaction_ts/src/common/utils.rs new file mode 100644 index 0000000000..3ed8447a1b --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/common/utils.rs @@ -0,0 +1,26 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::de::DeserializeOwned; +use wasm_bindgen::prelude::*; + +use crate::error::WasmError; + +pub fn into_sdk_type<'a, T: DeserializeOwned, W: Into>( + wasm_type_instance: W, +) -> core::result::Result> { + let js_value: JsValue = wasm_type_instance.into(); + match serde_wasm_bindgen::from_value::(js_value.clone()) { + Ok(ret_val) => Ok(ret_val), + Err(e) => { + // TODO: Replace all console_log! usages by proper Error management and Result types. + // Use console_log! only for debug purposes + console_log!( + "[iota_interaction_ts::common::utils - fn into_sdk_type]\n js_value: {:?}\n Error: {:?}", + js_value, + e + ); + Err(e.into()) + } + } +} diff --git a/bindings/wasm/iota_interaction_ts/src/error.rs b/bindings/wasm/iota_interaction_ts/src/error.rs new file mode 100644 index 0000000000..e29a098370 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/error.rs @@ -0,0 +1,223 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::result::Result as StdResult; + +use serde::de::DeserializeOwned; +use std::borrow::Cow; +use std::fmt::Debug; +use std::fmt::Display; +use wasm_bindgen::JsValue; + +use crate::common::into_sdk_type; +use identity_iota_interaction::types::execution_status::CommandArgumentError; +use identity_iota_interaction::types::execution_status::ExecutionFailureStatus; +use identity_iota_interaction::types::execution_status::PackageUpgradeError; +use identity_iota_interaction::types::execution_status::TypeArgumentError; +use thiserror::Error as ThisError; + +/// Convenience wrapper for `Result`. +/// +/// All exported errors must be converted to [`JsValue`] when using wasm_bindgen. +/// See: https://rustwasm.github.io/docs/wasm-bindgen/reference/types/result.html +pub type Result = core::result::Result; + +/// Convert an error into an idiomatic [js_sys::Error]. +pub fn wasm_error<'a, E>(error: E) -> JsValue +where + E: Into>, +{ + let wasm_err: WasmError<'_> = error.into(); + JsValue::from(wasm_err) +} + +/// Convenience trait to simplify `result.map_err(wasm_error)` to `result.wasm_result()` +pub trait WasmResult { + fn wasm_result(self) -> Result; +} + +impl<'a, T, E> WasmResult for core::result::Result +where + E: Into>, +{ + fn wasm_result(self) -> Result { + self.map_err(wasm_error) + } +} + +/// Convenience struct to convert internal errors to [js_sys::Error]. Uses [std::borrow::Cow] +/// internally to avoid unnecessary clones. +/// +/// This is a workaround for orphan rules so we can implement [core::convert::From] on errors from +/// dependencies. +#[derive(Debug, Clone)] +pub struct WasmError<'a> { + pub name: Cow<'a, str>, + pub message: Cow<'a, str>, +} + +impl<'a> WasmError<'a> { + pub fn new(name: Cow<'a, str>, message: Cow<'a, str>) -> Self { + Self { name, message } + } +} + +/// Convert [WasmError] into [js_sys::Error] for idiomatic error handling. +impl From> for js_sys::Error { + fn from(error: WasmError<'_>) -> Self { + let js_error = js_sys::Error::new(&error.message); + js_error.set_name(&error.name); + js_error + } +} + +/// Convert [WasmError] into [wasm_bindgen::JsValue]. +impl From> for JsValue { + fn from(error: WasmError<'_>) -> Self { + JsValue::from(js_sys::Error::from(error)) + } +} + +/// Implement WasmError for each type individually rather than a trait due to Rust's orphan rules. +/// Each type must implement `Into<&'static str> + Display`. The `Into<&'static str>` trait can be +/// derived using `strum::IntoStaticStr`. +#[macro_export] +macro_rules! impl_wasm_error_from { + ( $($t:ty),* ) => { + $(impl From<$t> for WasmError<'_> { + fn from(error: $t) -> Self { + Self { + message: Cow::Owned(ErrorMessage(&error).to_string()), + name: Cow::Borrowed(error.into()), + } + } + })* + } +} + +// Similar to `impl_wasm_error_from`, but uses the types name instead of requiring/calling Into &'static str +#[macro_export] +macro_rules! impl_wasm_error_from_with_struct_name { + ( $($t:ty),* ) => { + $(impl From<$t> for WasmError<'_> { + fn from(error: $t) -> Self { + Self { + message: Cow::Owned(error.to_string()), + name: Cow::Borrowed(stringify!($t)), + } + } + })* + } +} + +impl From for WasmError<'_> { + fn from(error: JsValue) -> Self { + let js_err = js_sys::Error::from(error); + let name: String = js_err.name().into(); + let message: String = js_err.message().into(); + WasmError::new(name.into(), message.into()) + } +} + +// identity_iota::iota now has some errors where the error message does not include the source error's error message. +// This is in compliance with the Rust error handling project group's recommendation: +// * An error type with a source error should either return that error via source or include that source's error message +// in its own Display output, but never both. * +// See https://blog.rust-lang.org/inside-rust/2021/07/01/What-the-error-handling-project-group-is-working-towards.html#guidelines-for-implementing-displayfmt-and-errorsource. +// +// However in WasmError we want the display message of the entire error chain. We introduce a workaround here that let's +// us display the entire display chain for new variants that don't include the error message of the source error in its +// own display. + +// the following function is inspired by https://www.lpalmieri.com/posts/error-handling-rust/#error-source +fn error_chain_fmt(e: &impl std::error::Error, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{e}. ")?; + let mut current = e.source(); + while let Some(cause) = current { + write!(f, "Caused by: {cause}. ")?; + current = cause.source(); + } + Ok(()) +} + +struct ErrorMessage<'a, E: std::error::Error>(&'a E); + +impl<'a, E: std::error::Error> Display for ErrorMessage<'a, E> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + error_chain_fmt(self.0, f) + } +} + +impl From for WasmError<'_> { + fn from(error: serde_json::Error) -> Self { + Self { + name: Cow::Borrowed("serde_json::Error"), // the exact error code is embedded in the message + message: Cow::Owned(error.to_string()), + } + } +} + +impl From for WasmError<'_> { + fn from(error: serde_wasm_bindgen::Error) -> Self { + Self { + name: Cow::Borrowed("serde_wasm_bindgen::Error"), + message: Cow::Owned(ErrorMessage(&error).to_string()), + } + } +} + +impl From for WasmError<'_> { + fn from(error: anyhow::Error) -> Self { + Self { + name: Cow::Borrowed("anyhow::Error"), + message: Cow::Owned(error.to_string()), + } + } +} + +/// Consumes the struct and returns a Result<_, String>, leaving an `Ok` value untouched. +pub fn stringify_js_error(result: Result) -> StdResult { + result.map_err(|js_value| { + let error_string: String = match wasm_bindgen::JsCast::dyn_into::(js_value) { + Ok(js_err) => ToString::to_string(&js_err.to_string()), + Err(js_val) => { + // Fall back to debug formatting if this is not a proper JS Error instance. + format!("{js_val:?}") + } + }; + error_string + }) +} + +#[derive(ThisError, Debug)] +pub enum TsSdkError { + #[error("[TsSdkError] PackageUpgradeError: {0}")] + PackageUpgradeError(#[from] PackageUpgradeError), + #[error("[TsSdkError] CommandArgumentError: {0}")] + CommandArgumentError(#[from] CommandArgumentError), + #[error("[TsSdkError] ExecutionFailureStatus: {0}")] + ExecutionFailureStatus(#[from] ExecutionFailureStatus), + #[error("[TsSdkError] TypeArgumentError: {0}")] + TypeArgumentError(#[from] TypeArgumentError), + #[error("[TsSdkError] WasmError:{{\n name: {0},\n message: {1}\n}}")] + WasmError(String, String), + #[error("[TsSdkError] JsSysError: {0}")] + JsSysError(String), + #[error("[TsSdkError] TransactionSerializationError: {0}")] + TransactionSerializationError(String), +} + +pub type TsSdkResult = core::result::Result; + +impl From> for TsSdkError { + fn from(err: WasmError<'_>) -> Self { + TsSdkError::WasmError(err.name.to_string(), err.message.to_string()) + } +} + +pub fn into_ts_sdk_result(result: Result) -> TsSdkResult { + let result_str = stringify_js_error(result); + let js_value = result_str.map_err(|e| TsSdkError::JsSysError(e))?; + let ret_val: T = into_sdk_type(js_value)?; + Ok(ret_val) +} diff --git a/bindings/wasm/iota_interaction_ts/src/identity_move_calls.rs b/bindings/wasm/iota_interaction_ts/src/identity_move_calls.rs new file mode 100644 index 0000000000..1ae753783e --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/identity_move_calls.rs @@ -0,0 +1,664 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use js_sys::Uint8Array; +use std::cell::Cell; +use std::collections::HashSet; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use wasm_bindgen::JsValue; + +use crate::bindings::WasmIotaObjectData; +use crate::bindings::WasmObjectRef; +use crate::bindings::WasmSharedObjectRef; +use crate::bindings::WasmTransactionArgument; +use crate::bindings::WasmTransactionBuilder; +use crate::common::PromiseUint8Array; +use crate::error::TsSdkError; +use crate::error::WasmError; +use crate::transaction_builder::TransactionBuilderTsSdk; +use identity_iota_interaction::rpc_types::IotaObjectData; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::TypeTag; +use identity_iota_interaction::BorrowIntentFnInternalT; +use identity_iota_interaction::ControllerIntentFnInternalT; +use identity_iota_interaction::IdentityMoveCalls; +use identity_iota_interaction::MoveType; +use identity_iota_interaction::ProgrammableTransactionBcs; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "[string, number]")] + pub(crate) type WasmControllerCouple; + + #[wasm_bindgen(typescript_type = "[string, string]")] + pub(crate) type WasmTransferCouple; + + #[wasm_bindgen(typescript_type = "[ObjectRef, string]")] + pub(crate) type WasmObjectRefAndType; + + #[wasm_bindgen(typescript_type = "Map")] + pub(crate) type WasmTxArgumentMap; +} + +#[wasm_bindgen(module = "@iota/iota-interaction-ts/move_calls/identity")] +extern "C" { + #[wasm_bindgen(js_name = "create", catch)] + fn identity_new(did: Option<&[u8]>, package: &str) -> Result; + + #[wasm_bindgen(js_name = "newWithControllers", catch)] + async fn identity_new_with_controllers( + did: Option<&[u8]>, + controllers: Vec, + threshold: u64, + package: &str, + ) -> Result; + + #[wasm_bindgen(js_name = "approve", catch)] + async fn approve_proposal( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + proposal: &str, + proposal_type: &str, + package: &str, + ) -> Result; + + #[wasm_bindgen(js_name = "proposeDeactivation", catch)] + fn propose_deactivation( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + package: &str, + expiration: Option, + ) -> Result; + + #[wasm_bindgen(js_name = "executeDeactivation", catch)] + async fn execute_deactivation( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + proposal: &str, + package: &str, + ) -> Result; + + #[wasm_bindgen(js_name = "proposeUpgrade", catch)] + async fn propose_upgrade( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + package: &str, + expiration: Option, + ) -> Result; + + #[wasm_bindgen(js_name = "executeUpgrade", catch)] + async fn execute_upgrade( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + proposal: &str, + package: &str, + ) -> Result; + + #[wasm_bindgen(js_name = "proposeSend", catch)] + async fn propose_send( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + assets: Vec, + package: &str, + expiration: Option, + ) -> Result; + + #[wasm_bindgen(js_name = "executeSend", catch)] + async fn execute_send( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + proposal: &str, + assets: Vec, + package: &str, + ) -> Result; + + #[wasm_bindgen(js_name = "proposeUpdate", catch)] + fn propose_update( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + did_doc: Option<&[u8]>, + package: &str, + expiration: Option, + ) -> Result; + + #[wasm_bindgen(js_name = "executeUpdate", catch)] + async fn execute_update( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + proposal: &str, + package: &str, + ) -> Result; + + #[wasm_bindgen(js_name = "proposeBorrow", catch)] + async fn propose_borrow( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + objects: Vec, + package: &str, + expiration: Option, + ) -> Result; + + #[wasm_bindgen(js_name = "executeBorrow", catch)] + async fn execute_borrow( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + proposal: &str, + objects: Vec, + intent_fn: &dyn Fn(WasmTransactionBuilder, WasmTxArgumentMap), + package: &str, + ) -> Result; + + #[wasm_bindgen(js_name = "createAndExecuteBorrow", catch)] + async fn create_and_execute_borrow( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + objects: Vec, + intent_fn: &dyn Fn(WasmTransactionBuilder, WasmTxArgumentMap), + package: &str, + expiration: Option, + ) -> Result; + + #[wasm_bindgen(js_name = "proposeConfigChange", catch)] + async fn propose_config_change( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + controllers_to_add: Vec, + controllers_to_remove: Vec, + controllers_to_update: Vec, + package: &str, + expiration: Option, + threshold: Option, + ) -> Result; + + #[wasm_bindgen(js_name = "executeConfigChange", catch)] + async fn execute_config_change( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + proposal: &str, + package: &str, + ) -> Result; + + #[wasm_bindgen(js_name = "proposeControllerExecution", catch)] + async fn propose_controller_execution( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + controller_cap_id: &str, + package: &str, + expiration: Option, + ) -> Result; + + #[wasm_bindgen(js_name = "executeControllerExecution", catch)] + async fn execute_controller_execution( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + proposal: &str, + controller_cap_ref: WasmObjectRef, + intent_fn: &dyn Fn(WasmTransactionBuilder, WasmTransactionArgument), + package: &str, + ) -> Result; + + #[wasm_bindgen(js_name = "createAndExecuteControllerExecution", catch)] + async fn create_and_execute_controller_execution( + identity: WasmSharedObjectRef, + capability: WasmObjectRef, + controller_cap_ref: WasmObjectRef, + intent_fn: &dyn Fn(WasmTransactionBuilder, WasmTransactionArgument), + package: &str, + expiration: Option, + ) -> Result; +} + +pub struct IdentityMoveCallsTsSdk {} + +#[async_trait(?Send)] +impl IdentityMoveCalls for IdentityMoveCallsTsSdk { + type Error = TsSdkError; + type NativeTxBuilder = WasmTransactionBuilder; + + fn propose_borrow( + identity: OwnedObjectRef, + capability: ObjectRef, + objects: Vec, + expiration: Option, + package_id: ObjectID, + ) -> Result { + let identity = identity.try_into()?; + let controller_cap = capability.into(); + let package_id = package_id.to_string(); + let objects = objects.into_iter().map(|obj| obj.to_string()).collect(); + + futures::executor::block_on(propose_borrow( + identity, + controller_cap, + objects, + &package_id, + expiration, + )) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn execute_borrow>( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + objects: Vec, + intent_fn: F, + package: ObjectID, + ) -> Result { + let identity = identity.try_into()?; + let capability = capability.into(); + let proposal = proposal_id.to_string(); + let package = package.to_string(); + let objects = objects + .into_iter() + .map(|obj| serde_wasm_bindgen::to_value(&obj).map(WasmIotaObjectData::from)) + .collect::, _>>() + .map_err(WasmError::from)?; + + // Use cell to move `intent_fn` inside `closure` without actually moving it. + // This ensures that `closure` is an `impl Fn(..)` instead of `impl FnOnce(..)` like `intent_fn`. + let wrapped_intent_fn = Cell::new(Some(intent_fn)); + let closure = |tx_builder: WasmTransactionBuilder, args: WasmTxArgumentMap| { + let mut builder = TransactionBuilderTsSdk::new(tx_builder); + let args = serde_wasm_bindgen::from_value(args.into()).expect("failed to convert JS argument map"); + wrapped_intent_fn.take().unwrap()(&mut builder, &args); + }; + + futures::executor::block_on(execute_borrow( + identity, capability, &proposal, objects, &closure, &package, + )) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn create_and_execute_borrow>( + identity: OwnedObjectRef, + capability: ObjectRef, + objects: Vec, + intent_fn: F, + expiration: Option, + package_id: ObjectID, + ) -> anyhow::Result { + let identity = identity.try_into()?; + let capability = capability.into(); + let package = package_id.to_string(); + let objects = objects + .into_iter() + .map(|obj| serde_wasm_bindgen::to_value(&obj).map(WasmIotaObjectData::from)) + .collect::, _>>() + .map_err(WasmError::from)?; + + // Use cell to move `intent_fn` inside `closure` without actually moving it. + // This ensures that `closure` is an `impl Fn(..)` instead of `impl FnOnce(..)` like `intent_fn`. + let wrapped_intent_fn = Cell::new(Some(intent_fn)); + let closure = |tx_builder: WasmTransactionBuilder, args: WasmTxArgumentMap| { + let mut builder = TransactionBuilderTsSdk::new(tx_builder); + let args = serde_wasm_bindgen::from_value(args.into()).expect("failed to convert JS argument map"); + wrapped_intent_fn.take().unwrap()(&mut builder, &args); + }; + + futures::executor::block_on(create_and_execute_borrow( + identity, capability, objects, &closure, &package, expiration, + )) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn propose_config_change( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + expiration: Option, + threshold: Option, + controllers_to_add: I1, + controllers_to_remove: HashSet, + controllers_to_update: I2, + package: ObjectID, + ) -> Result + where + I1: IntoIterator, + I2: IntoIterator, + { + let identity = identity.try_into()?; + let capability = controller_cap.into(); + let package = package.to_string(); + + let controllers_to_add = controllers_to_add + .into_iter() + .map(|controller| serde_wasm_bindgen::to_value(&controller).map(WasmControllerCouple::from)) + .collect::, _>>() + .map_err(WasmError::from)?; + let controllers_to_remove = controllers_to_remove + .into_iter() + .map(|controller| controller.to_string()) + .collect(); + let controllers_to_update = controllers_to_update + .into_iter() + .map(|controller| serde_wasm_bindgen::to_value(&controller).map(WasmControllerCouple::from)) + .collect::, _>>() + .map_err(WasmError::from)?; + + futures::executor::block_on(propose_config_change( + identity, + capability, + controllers_to_add, + controllers_to_remove, + controllers_to_update, + &package, + expiration, + threshold, + )) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn execute_config_change( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + proposal_id: ObjectID, + package: ObjectID, + ) -> Result { + let identity = identity.try_into()?; + let capability = controller_cap.into(); + let proposal = proposal_id.to_string(); + let package = package.to_string(); + + futures::executor::block_on(execute_config_change(identity, capability, &proposal, &package)) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn propose_controller_execution( + identity: OwnedObjectRef, + capability: ObjectRef, + controller_cap_id: ObjectID, + expiration: Option, + package_id: ObjectID, + ) -> Result { + let identity = identity.try_into()?; + let controller_cap = capability.into(); + let package_id = package_id.to_string(); + let borrowed_cap = controller_cap_id.to_string(); + + futures::executor::block_on(propose_controller_execution( + identity, + controller_cap, + &borrowed_cap, + &package_id, + expiration, + )) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn execute_controller_execution>( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + borrowing_controller_cap_ref: ObjectRef, + intent_fn: F, + package: ObjectID, + ) -> Result { + let identity = identity.try_into()?; + let capability = capability.into(); + let proposal = proposal_id.to_string(); + let package = package.to_string(); + let borrowing_cap = borrowing_controller_cap_ref.into(); + + // Use cell to move `intent_fn` inside `closure` without actually moving it. + // This ensures that `closure` is an `impl Fn(..)` instead of `impl FnOnce(..)` like `intent_fn`. + let wrapped_intent_fn = Cell::new(Some(intent_fn)); + let closure = |tx_builder: WasmTransactionBuilder, args: WasmTransactionArgument| { + let mut builder = TransactionBuilderTsSdk::new(tx_builder); + let args = serde_wasm_bindgen::from_value(args.into()).expect("failed to convert JS argument map"); + wrapped_intent_fn.take().unwrap()(&mut builder, &args); + }; + + futures::executor::block_on(execute_controller_execution( + identity, + capability, + &proposal, + borrowing_cap, + &closure, + &package, + )) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn create_and_execute_controller_execution( + identity: OwnedObjectRef, + capability: ObjectRef, + expiration: Option, + borrowing_controller_cap_ref: ObjectRef, + intent_fn: F, + package_id: ObjectID, + ) -> Result + where + F: ControllerIntentFnInternalT, + { + let identity = identity.try_into()?; + let capability = capability.into(); + let package = package_id.to_string(); + let borrowing_cap = borrowing_controller_cap_ref.into(); + + // Use cell to move `intent_fn` inside `closure` without actually moving it. + // This ensures that `closure` is an `impl Fn(..)` instead of `impl FnOnce(..)` like `intent_fn`. + let wrapped_intent_fn = Cell::new(Some(intent_fn)); + let closure = |tx_builder: WasmTransactionBuilder, args: WasmTransactionArgument| { + let mut builder = TransactionBuilderTsSdk::new(tx_builder); + let args = serde_wasm_bindgen::from_value(args.into()).expect("failed to convert JS argument map"); + wrapped_intent_fn.take().unwrap()(&mut builder, &args); + }; + + futures::executor::block_on(create_and_execute_controller_execution( + identity, + capability, + borrowing_cap, + &closure, + &package, + expiration, + )) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + async fn new_identity( + did_doc: Option<&[u8]>, + package_id: ObjectID, + ) -> Result { + let package = package_id.to_string(); + + identity_new(did_doc, &package) + .map_err(WasmError::from)? + .to_transaction_bcs() + .await + } + + fn new_with_controllers( + did_doc: Option<&[u8]>, + controllers: C, + threshold: u64, + package_id: ObjectID, + ) -> Result + where + C: IntoIterator, + { + let package = package_id.to_string(); + let controllers = controllers + .into_iter() + .map(|controller| serde_wasm_bindgen::to_value(&controller).map(|js_value| js_value.unchecked_into())) + .collect::, _>>() + .map_err(|e| WasmError::from(e))?; + + futures::executor::block_on(identity_new_with_controllers(did_doc, controllers, threshold, &package)) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn approve_proposal( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + proposal_id: ObjectID, + package: ObjectID, + ) -> Result { + let identity = identity.try_into()?; + let controller_cap = controller_cap.into(); + let proposal_id = proposal_id.to_string(); + let package_id = package.to_string(); + + futures::executor::block_on(approve_proposal( + identity, + controller_cap, + &proposal_id, + &T::move_type(package).to_canonical_string(true), + &package_id, + )) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn propose_send( + identity: OwnedObjectRef, + capability: ObjectRef, + transfer_map: Vec<(ObjectID, IotaAddress)>, + expiration: Option, + package_id: ObjectID, + ) -> Result { + let identity = identity.try_into()?; + let controller_cap = capability.into(); + let package_id = package_id.to_string(); + let transfer_map = transfer_map + .into_iter() + .map(|tx| serde_wasm_bindgen::to_value(&tx).map(JsValue::into)) + .collect::, _>>() + .map_err(|e| WasmError::from(e))?; + + futures::executor::block_on(propose_send( + identity, + controller_cap, + transfer_map, + &package_id, + expiration, + )) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn create_and_execute_send( + _identity: OwnedObjectRef, + _capability: ObjectRef, + _transfer_map: Vec<(ObjectID, IotaAddress)>, + _expiration: Option, + _objects: Vec<(ObjectRef, TypeTag)>, + _package: ObjectID, + ) -> anyhow::Result { + todo!() + } + + fn execute_send( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + objects: Vec<(ObjectRef, TypeTag)>, + package: ObjectID, + ) -> Result { + let identity = identity.try_into()?; + let controller_cap = capability.into(); + let proposal = proposal_id.to_string(); + let package_id = package.to_string(); + let objects = objects + .into_iter() + .map(|tx| serde_wasm_bindgen::to_value(&tx).map(JsValue::into)) + .collect::, _>>() + .map_err(|e| WasmError::from(e))?; + + futures::executor::block_on(execute_send(identity, controller_cap, &proposal, objects, &package_id)) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + async fn propose_update( + identity: OwnedObjectRef, + capability: ObjectRef, + did_doc: Option<&[u8]>, + expiration: Option, + package_id: ObjectID, + ) -> Result { + let identity = identity.try_into()?; + let controller_cap = capability.into(); + let package_id = package_id.to_string(); + + propose_update(identity, controller_cap, did_doc, &package_id, expiration) + .map_err(WasmError::from)? + .to_transaction_bcs() + .await + } + + fn execute_update( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + package_id: ObjectID, + ) -> Result { + let identity = identity.try_into()?; + let controller_cap = capability.into(); + let proposal = proposal_id.to_string(); + let package_id = package_id.to_string(); + + futures::executor::block_on(execute_update(identity, controller_cap, &proposal, &package_id)) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn propose_upgrade( + identity: OwnedObjectRef, + capability: ObjectRef, + expiration: Option, + package_id: ObjectID, + ) -> Result { + let identity = identity.try_into()?; + let capability = capability.into(); + let package = package_id.to_string(); + + futures::executor::block_on(propose_upgrade(identity, capability, &package, expiration)) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } + + fn execute_upgrade( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + package_id: ObjectID, + ) -> Result { + let identity = identity.try_into()?; + let capability = capability.into(); + let proposal = proposal_id.to_string(); + let package = package_id.to_string(); + + futures::executor::block_on(execute_upgrade(identity, capability, &proposal, &package)) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(TsSdkError::from) + } +} diff --git a/bindings/wasm/iota_interaction_ts/src/iota_client_ts_sdk.rs b/bindings/wasm/iota_interaction_ts/src/iota_client_ts_sdk.rs new file mode 100644 index 0000000000..3bd24ab90a --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/iota_client_ts_sdk.rs @@ -0,0 +1,471 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::boxed::Box; +use std::option::Option; +use std::result::Result; + +use fastcrypto::traits::ToFromBytes; +use identity_iota_interaction::types::digests::TransactionDigest; +use identity_iota_interaction::types::dynamic_field::DynamicFieldName; +use secret_storage::Signer; + +use identity_iota_interaction::error::IotaRpcResult; +use identity_iota_interaction::rpc_types::CoinPage; +use identity_iota_interaction::rpc_types::EventFilter; +use identity_iota_interaction::rpc_types::EventPage; +use identity_iota_interaction::rpc_types::IotaExecutionStatus; +use identity_iota_interaction::rpc_types::IotaObjectData; +use identity_iota_interaction::rpc_types::IotaObjectDataOptions; +use identity_iota_interaction::rpc_types::IotaObjectResponse; +use identity_iota_interaction::rpc_types::IotaObjectResponseQuery; +use identity_iota_interaction::rpc_types::IotaPastObjectResponse; +use identity_iota_interaction::rpc_types::IotaTransactionBlockResponseOptions; +use identity_iota_interaction::rpc_types::ObjectsPage; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::SequenceNumber; +use identity_iota_interaction::types::event::EventID; +use identity_iota_interaction::types::quorum_driver_types::ExecuteTransactionRequestType; +use identity_iota_interaction::CoinReadTrait; +use identity_iota_interaction::EventTrait; +use identity_iota_interaction::IotaClientTrait; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::IotaTransactionBlockResponseT; +use identity_iota_interaction::ProgrammableTransactionBcs; +use identity_iota_interaction::QuorumDriverTrait; +use identity_iota_interaction::ReadTrait; +use identity_iota_interaction::SignatureBcs; +use identity_iota_interaction::TransactionDataBcs; + +use crate::bindings::add_gas_data_to_transaction; +use crate::bindings::ManagedWasmIotaClient; +use crate::bindings::WasmIotaClient; +use crate::bindings::WasmIotaTransactionBlockResponseWrapper; +use crate::error::TsSdkError; +use crate::error::WasmError; +use crate::ProgrammableTransaction; + +#[allow(dead_code)] +pub trait IotaTransactionBlockResponseAdaptedT: + IotaTransactionBlockResponseT +{ +} +impl IotaTransactionBlockResponseAdaptedT for T where + T: IotaTransactionBlockResponseT +{ +} +#[allow(dead_code)] +pub type IotaTransactionBlockResponseAdaptedTraitObj = + Box>; + +#[allow(dead_code)] +pub trait QuorumDriverApiAdaptedT: + QuorumDriverTrait +{ +} +impl QuorumDriverApiAdaptedT for T where + T: QuorumDriverTrait +{ +} +#[allow(dead_code)] +pub type QuorumDriverApiAdaptedTraitObj = + Box>; + +#[allow(dead_code)] +pub trait ReadApiAdaptedT: + ReadTrait +{ +} +impl ReadApiAdaptedT for T where + T: ReadTrait +{ +} +#[allow(dead_code)] +pub type ReadApiAdaptedTraitObj = + Box>; + +#[allow(dead_code)] +pub trait CoinReadApiAdaptedT: CoinReadTrait {} +impl CoinReadApiAdaptedT for T where T: CoinReadTrait {} +#[allow(dead_code)] +pub type CoinReadApiAdaptedTraitObj = Box>; + +#[allow(dead_code)] +pub trait EventApiAdaptedT: EventTrait {} +impl EventApiAdaptedT for T where T: EventTrait {} +#[allow(dead_code)] +pub type EventApiAdaptedTraitObj = Box>; + +#[allow(dead_code)] +pub trait IotaClientAdaptedT: + IotaClientTrait +{ +} +impl IotaClientAdaptedT for T where + T: IotaClientTrait +{ +} +#[allow(dead_code)] +pub type IotaClientAdaptedTraitObj = + Box>; + +pub struct IotaTransactionBlockResponseProvider { + response: WasmIotaTransactionBlockResponseWrapper, +} + +impl IotaTransactionBlockResponseProvider { + pub fn new(response: WasmIotaTransactionBlockResponseWrapper) -> Self { + IotaTransactionBlockResponseProvider { response } + } +} + +#[async_trait::async_trait(?Send)] +impl IotaTransactionBlockResponseT for IotaTransactionBlockResponseProvider { + type Error = TsSdkError; + type NativeResponse = WasmIotaTransactionBlockResponseWrapper; + + fn effects_is_none(&self) -> bool { + self.response.effects_is_none() + } + + fn effects_is_some(&self) -> bool { + self.response.effects_is_some() + } + + fn to_string(&self) -> String { + format!("{:?}", self.response.to_string()) + } + + fn effects_execution_status(&self) -> Option { + self + .response + .effects_execution_status() + .map(|wasm_status| wasm_status.into()) + } + + fn effects_created(&self) -> Option> { + self + .response + .effects_created() + .map(|wasm_o_ref_vec| wasm_o_ref_vec.into()) + } + + fn as_native_response(&self) -> &Self::NativeResponse { + &self.response + } + + fn as_mut_native_response(&mut self) -> &mut Self::NativeResponse { + &mut self.response + } + + fn clone_native_response(&self) -> Self::NativeResponse { + self.response.clone() + } + + fn digest(&self) -> Result { + self.response.digest() + } +} + +pub struct ReadAdapter { + client: ManagedWasmIotaClient, +} + +#[async_trait::async_trait(?Send)] +impl ReadTrait for ReadAdapter { + type Error = TsSdkError; + type NativeResponse = WasmIotaTransactionBlockResponseWrapper; + + async fn get_chain_identifier(&self) -> Result { + Ok(self.client.get_chain_identifier().await.unwrap()) + } + + async fn get_dynamic_field_object( + &self, + parent_object_id: ObjectID, + name: DynamicFieldName, + ) -> IotaRpcResult { + self.client.get_dynamic_field_object(parent_object_id, name).await + } + + async fn get_object_with_options( + &self, + object_id: ObjectID, + options: IotaObjectDataOptions, + ) -> IotaRpcResult { + self.client.get_object_with_options(object_id, options).await + } + + async fn get_owned_objects( + &self, + address: IotaAddress, + query: Option, + cursor: Option, + limit: Option, + ) -> IotaRpcResult { + self.client.get_owned_objects(address, query, cursor, limit).await + } + + async fn get_reference_gas_price(&self) -> IotaRpcResult { + self.client.get_reference_gas_price().await + } + + async fn get_transaction_with_options( + &self, + digest: TransactionDigest, + options: IotaTransactionBlockResponseOptions, + ) -> IotaRpcResult { + let wasm_response = self.client.get_transaction_with_options(digest, options).await?; + + Ok(Box::new(IotaTransactionBlockResponseProvider::new(wasm_response))) + } + + async fn try_get_parsed_past_object( + &self, + _object_id: ObjectID, + _version: SequenceNumber, + _options: IotaObjectDataOptions, + ) -> IotaRpcResult { + // TODO: does not work anymore, find out, why we need to pass a different `SequenceNumber` now + unimplemented!("try_get_parsed_past_object"); + // self + // .client + // .try_get_parsed_past_object(object_id, version, options) + // .await + } +} + +pub struct QuorumDriverAdapter { + client: ManagedWasmIotaClient, +} + +#[async_trait::async_trait(?Send)] +impl QuorumDriverTrait for QuorumDriverAdapter { + type Error = TsSdkError; + type NativeResponse = WasmIotaTransactionBlockResponseWrapper; + + async fn execute_transaction_block( + &self, + tx_data_bcs: &TransactionDataBcs, + signatures: &[SignatureBcs], + options: Option, + request_type: Option, + ) -> IotaRpcResult { + let wasm_response = self + .client + .execute_transaction_block(tx_data_bcs, signatures, options, request_type) + .await?; + Ok(Box::new(IotaTransactionBlockResponseProvider::new(wasm_response))) + } +} + +pub struct EventAdapter { + client: ManagedWasmIotaClient, +} + +#[async_trait::async_trait(?Send)] +impl EventTrait for EventAdapter { + type Error = TsSdkError; + + async fn query_events( + &self, + query: EventFilter, + cursor: Option, + limit: Option, + descending_order: bool, + ) -> IotaRpcResult { + self.client.query_events(query, cursor, limit, descending_order).await + } +} + +pub struct CoinReadAdapter { + client: ManagedWasmIotaClient, +} + +#[async_trait::async_trait(?Send)] +impl CoinReadTrait for CoinReadAdapter { + type Error = TsSdkError; + + async fn get_coins( + &self, + owner: IotaAddress, + coin_type: Option, + cursor: Option, + limit: Option, + ) -> IotaRpcResult { + self.client.get_coins(owner, coin_type, cursor, limit).await + } +} + +#[derive(Clone)] +pub struct IotaClientTsSdk { + iota_client: ManagedWasmIotaClient, +} + +#[async_trait::async_trait(?Send)] +impl IotaClientTrait for IotaClientTsSdk { + type Error = TsSdkError; + type NativeResponse = WasmIotaTransactionBlockResponseWrapper; + + fn quorum_driver_api(&self) -> QuorumDriverApiAdaptedTraitObj { + Box::new(QuorumDriverAdapter { + client: self.iota_client.clone(), + }) + } + + fn read_api(&self) -> ReadApiAdaptedTraitObj { + Box::new(ReadAdapter { + client: self.iota_client.clone(), + }) + } + + fn coin_read_api(&self) -> Box + '_> { + Box::new(CoinReadAdapter { + client: self.iota_client.clone(), + }) + } + + fn event_api(&self) -> Box + '_> { + Box::new(EventAdapter { + client: self.iota_client.clone(), + }) + } + + async fn execute_transaction>( + &self, + tx_bcs: ProgrammableTransactionBcs, + gas_budget: Option, + signer: &S, + ) -> Result< + Box>, + Self::Error, + > { + let tx: ProgrammableTransaction = tx_bcs.try_into()?; + let response = self.sdk_execute_transaction(tx, gas_budget, signer).await?; + + // wait until new transaction block is available + self + .iota_client + .wait_for_transaction( + response.digest()?, + Some(IotaTransactionBlockResponseOptions::new()), + None, + None, + ) + .await + .unwrap(); + + Ok(Box::new(response)) + } + + async fn default_gas_budget( + &self, + _sender_address: IotaAddress, + _tx_bcs: &ProgrammableTransactionBcs, + ) -> Result { + unimplemented!(); + } + + async fn get_previous_version(&self, _iod: IotaObjectData) -> Result, Self::Error> { + unimplemented!(); + } + + async fn get_past_object( + &self, + object_id: ObjectID, + version: SequenceNumber, + ) -> Result { + self + .iota_client + .try_get_parsed_past_object(object_id, version, IotaObjectDataOptions::full_content()) + .await + .map_err(|err| { + // TODO: check error variant here, selection has been reduced / focused + // Self::Error::InvalidIdentityHistory(format!("could not look up object {object_id} version {version}; {err}")) + Self::Error::JsSysError(format!("could not look up object {object_id} version {version}; {err}")) + }) + } +} + +impl IotaClientTsSdk { + pub fn new(iota_client: WasmIotaClient) -> Result { + Ok(Self { + iota_client: ManagedWasmIotaClient::new(iota_client), + }) + } + + /// Builds message with `TransactionData` intent, hashes it, and constructs full signature. + async fn get_transaction_signature_bytes(signer: &S, tx_data: &[u8]) -> Result, TsSdkError> + where + S: Signer, + { + signer + .sign(&tx_data.to_vec()) + .await + .map(|sig| sig.as_bytes().to_vec()) + .map_err(|err| TsSdkError::TransactionSerializationError(format!("could not sign transaction message; {err}"))) + } + + /// Inserts these values into the transaction and replaces placeholder values. + /// + /// - sender (overwritten as we assume a placeholder to be used in prepared transaction) + /// - gas budget (value determined automatically if not provided) + /// - gas price (value determined automatically) + /// - gas coin / payment object (fetched automatically) + /// - gas owner (equals sender) + /// + /// # Arguments + /// + /// * `iota_client` - client instance + /// * `sender_address` - transaction sender (and the one paying for it) + /// * `tx_bcs` - transaction data serialized to bcs, most probably having placeholder values + /// * `gas_budget` - optional fixed gas budget, determined automatically with a dry run if not provided + async fn replace_transaction_placeholder_values( + &self, + tx: ProgrammableTransaction, + sender_address: IotaAddress, + gas_budget: Option, + ) -> Result, TsSdkError> { + let tx_bcs = tx.0.build().await.map_err(WasmError::from)?.to_vec(); + let updated = add_gas_data_to_transaction(&self.iota_client.0, sender_address, tx_bcs, gas_budget).await?; + + Ok(updated) + } + + // Submit tx to IOTA client, also: + // - ensures, `gas_budget` is set + // - signs tx + // - calls execute_transaction_block to submit tx (with signatures created here) + async fn sdk_execute_transaction>( + &self, + tx: ProgrammableTransaction, + gas_budget: Option, + signer: &S, + ) -> Result { + let sender_public_key = signer + .public_key() + .await + .map_err(|e| TsSdkError::WasmError(String::from("SecretStorage"), e.to_string()))?; + let sender_address = IotaAddress::from(&sender_public_key); + let final_tx = self + .replace_transaction_placeholder_values(tx, sender_address, gas_budget) + .await?; + let signature = Self::get_transaction_signature_bytes(signer, &final_tx).await?; + + let wasm_response = self + .quorum_driver_api() + .execute_transaction_block( + &final_tx, + &vec![signature], + Some(IotaTransactionBlockResponseOptions::full_content()), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + let native = wasm_response.clone_native_response(); + + Ok(IotaTransactionBlockResponseProvider::new(native)) + } +} diff --git a/bindings/wasm/iota_interaction_ts/src/lib.rs b/bindings/wasm/iota_interaction_ts/src/lib.rs new file mode 100644 index 0000000000..d876dc6a24 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/lib.rs @@ -0,0 +1,57 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(target_arch = "wasm32")] +pub mod bindings; + +#[cfg(target_arch = "wasm32")] +pub mod asset_move_calls; +#[cfg(target_arch = "wasm32")] +pub mod common; +#[cfg(target_arch = "wasm32")] +pub mod error; +#[cfg(target_arch = "wasm32")] +pub mod identity_move_calls; +#[cfg(target_arch = "wasm32")] +pub mod iota_client_ts_sdk; +#[cfg(target_arch = "wasm32")] +mod migration_move_calls; +#[cfg(target_arch = "wasm32")] +pub mod transaction_builder; + +cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + #[allow(unused_imports)] pub use error::TsSdkError as AdapterError; + #[allow(unused_imports)] pub use asset_move_calls::AssetMoveCallsTsSdk as AssetMoveCallsAdapter; + #[allow(unused_imports)] pub use identity_move_calls::IdentityMoveCallsTsSdk as IdentityMoveCallsAdapter; + #[allow(unused_imports)] pub use iota_client_ts_sdk::IotaClientTsSdk as IotaClientAdapter; + #[allow(unused_imports)] pub use iota_client_ts_sdk::IotaTransactionBlockResponseProvider as IotaTransactionBlockResponseAdapter; + #[allow(unused_imports)] pub use bindings::WasmIotaTransactionBlockResponseWrapper as NativeTransactionBlockResponse; + #[allow(unused_imports)] pub use migration_move_calls::MigrationMoveCallsTsSdk as MigrationMoveCallsAdapter; + #[allow(unused_imports)] pub use transaction_builder::TransactionBuilderTsSdk as TransactionBuilderAdapter; + + #[allow(unused_imports)] pub use iota_client_ts_sdk::IotaTransactionBlockResponseAdaptedT; + #[allow(unused_imports)] pub use iota_client_ts_sdk::IotaTransactionBlockResponseAdaptedTraitObj; + #[allow(unused_imports)] pub use iota_client_ts_sdk::QuorumDriverApiAdaptedT; + #[allow(unused_imports)] pub use iota_client_ts_sdk::QuorumDriverApiAdaptedTraitObj; + #[allow(unused_imports)] pub use iota_client_ts_sdk::ReadApiAdaptedT; + #[allow(unused_imports)] pub use iota_client_ts_sdk::ReadApiAdaptedTraitObj; + #[allow(unused_imports)] pub use iota_client_ts_sdk::CoinReadApiAdaptedT; + #[allow(unused_imports)] pub use iota_client_ts_sdk::CoinReadApiAdaptedTraitObj; + #[allow(unused_imports)] pub use iota_client_ts_sdk::EventApiAdaptedT; + #[allow(unused_imports)] pub use iota_client_ts_sdk::EventApiAdaptedTraitObj; + #[allow(unused_imports)] pub use iota_client_ts_sdk::IotaClientAdaptedT; + #[allow(unused_imports)] pub use iota_client_ts_sdk::IotaClientAdaptedTraitObj; + + #[allow(unused_imports)] pub use bindings::ProgrammableTransaction; + #[allow(unused_imports)] pub use bindings::WasmPublicKey; + #[allow(unused_imports)] pub use bindings::Ed25519PublicKey as WasmEd25519PublicKey; + #[allow(unused_imports)] pub use bindings::Secp256r1PublicKey as WasmSecp256r1PublicKey; + #[allow(unused_imports)] pub use bindings::Secp256k1PublicKey as WasmSecp256k1PublicKey; + #[allow(unused_imports)] pub use bindings::WasmIotaSignature; + #[cfg(feature = "keytool-signer")] + pub use bindings::WasmKeytoolSigner; + + #[allow(unused_imports)] pub use transaction_builder::NativeTsTransactionBuilderBindingWrapper; + } +} diff --git a/bindings/wasm/iota_interaction_ts/src/migration_move_calls.rs b/bindings/wasm/iota_interaction_ts/src/migration_move_calls.rs new file mode 100644 index 0000000000..5000374fbc --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/migration_move_calls.rs @@ -0,0 +1,54 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::MigrationMoveCalls; +use identity_iota_interaction::ProgrammableTransactionBcs; +use js_sys::Uint8Array; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; + +use crate::bindings::WasmObjectRef; +use crate::bindings::WasmSharedObjectRef; +use crate::error::TsSdkError; +use crate::error::WasmError; + +#[wasm_bindgen(module = "@iota/iota-interaction-ts/move_calls")] +extern "C" { + #[wasm_bindgen(js_name = "migrateDidOutput", catch)] + async fn migrate_did_output_impl( + did_output: WasmObjectRef, + migration_registry: WasmSharedObjectRef, + package: &str, + creation_timestamp: Option, + ) -> Result; +} + +pub struct MigrationMoveCallsTsSdk {} + +impl MigrationMoveCalls for MigrationMoveCallsTsSdk { + type Error = TsSdkError; + + fn migrate_did_output( + did_output: ObjectRef, + creation_timestamp: Option, + migration_registry: OwnedObjectRef, + package: ObjectID, + ) -> anyhow::Result { + let did_output = did_output.into(); + let package = package.to_string(); + let migration_registry = migration_registry.try_into()?; + + futures::executor::block_on(migrate_did_output_impl( + did_output, + migration_registry, + &package, + creation_timestamp, + )) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(Self::Error::from) + } +} diff --git a/bindings/wasm/iota_interaction_ts/src/transaction_builder.rs b/bindings/wasm/iota_interaction_ts/src/transaction_builder.rs new file mode 100644 index 0000000000..0d95bb7406 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/src/transaction_builder.rs @@ -0,0 +1,63 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; +use std::ops::DerefMut; + +use crate::bindings::WasmTransactionBuilder; +use crate::error::TsSdkError; +use crate::error::WasmError; +use identity_iota_interaction::ProgrammableTransactionBcs; +use identity_iota_interaction::TransactionBuilderT; + +pub type NativeTsTransactionBuilderBindingWrapper = WasmTransactionBuilder; + +pub struct TransactionBuilderTsSdk { + pub(crate) builder: NativeTsTransactionBuilderBindingWrapper, +} + +impl TransactionBuilderTsSdk { + pub fn new(builder: NativeTsTransactionBuilderBindingWrapper) -> Self { + TransactionBuilderTsSdk { builder } + } +} + +impl TransactionBuilderT for TransactionBuilderTsSdk { + type Error = TsSdkError; + type NativeTxBuilder = NativeTsTransactionBuilderBindingWrapper; + + fn finish(self) -> Result { + futures::executor::block_on(self.builder.build()) + .map(|js_arr| js_arr.to_vec()) + .map_err(WasmError::from) + .map_err(Self::Error::from) + } + + fn as_native_tx_builder(&mut self) -> &mut Self::NativeTxBuilder { + &mut self.builder + } + + fn into_native_tx_builder(self) -> Self::NativeTxBuilder { + self.builder + } +} + +impl Default for TransactionBuilderTsSdk { + fn default() -> Self { + unimplemented!(); + } +} + +impl Deref for TransactionBuilderTsSdk { + type Target = NativeTsTransactionBuilderBindingWrapper; + + fn deref(&self) -> &Self::Target { + &self.builder + } +} + +impl DerefMut for TransactionBuilderTsSdk { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.builder + } +} diff --git a/bindings/wasm/iota_interaction_ts/tsconfig.json b/bindings/wasm/iota_interaction_ts/tsconfig.json new file mode 100644 index 0000000000..e8a42d2a39 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "entryPoints": [ + "./node/" + ], + "out": "./docs/wasm", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@iota/iota-interaction-ts/*": [ + "./*" + ], + } + } +} diff --git a/bindings/wasm/iota_interaction_ts/tsconfig.node.json b/bindings/wasm/iota_interaction_ts/tsconfig.node.json new file mode 100644 index 0000000000..c75065fb27 --- /dev/null +++ b/bindings/wasm/iota_interaction_ts/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "commonjs" + } +} \ No newline at end of file diff --git a/bindings/wasm/lib/iota_identity_client.ts b/bindings/wasm/lib/iota_identity_client.ts deleted file mode 100644 index c17dd511b9..0000000000 --- a/bindings/wasm/lib/iota_identity_client.ts +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -import { IIotaIdentityClient, IotaDID, IotaDocument, IotaIdentityClientExt } from "~identity_wasm"; - -import { - Address, - AddressUnlockCondition, - AliasOutput, - AliasOutputBuilderParams, - Client, - INodeInfoProtocol, - INodeInfoWrapper, - IRent, - OutputResponse, - OutputType, - SecretManagerType, - UTXOInput, -} from "~sdk-wasm"; - -/** Provides operations for IOTA DID Documents with Alias Outputs. */ -export class IotaIdentityClient implements IIotaIdentityClient { - client: Client; - - constructor(client: Client) { - this.client = client; - } - - async getNetworkHrp() { - return await this.client.getBech32Hrp(); - } - - async getAliasOutput(aliasId: string) { - // Lookup latest OutputId from the indexer plugin. - const outputId = await this.client.aliasOutputId(aliasId); - - // Fetch AliasOutput. - const outputResponse: OutputResponse = await this.client.getOutput(outputId); - const output = outputResponse.output; - if (output.getType() != OutputType.Alias) { - throw new Error("AliasId '" + aliasId + "' returned incorrect output type '" + output.getType() + "'"); - } - // Coerce to tuple instead of an array. - // Cast of output is fine as we checked the type earlier. - const ret: [string, AliasOutput] = [outputId, output as AliasOutput]; - return ret; - } - - async getRentStructure(): Promise { - const info: INodeInfoWrapper = await this.client.getInfo(); - return info.nodeInfo.protocol.rentStructure; - } - - async getTokenSupply(): Promise { - return await this.client.getTokenSupply(); - } - - async getProtocolParameters(): Promise { - const protocolParameters: INodeInfoProtocol = await this.client.getProtocolParameters(); - return protocolParameters; - } - - /** Create a DID with a new Alias Output containing the given `document`. - * - * The `address` will be set as the state controller and governor unlock conditions. - * The minimum required token deposit amount will be set according to the given - * `rent_structure`, which will be fetched from the node if not provided. - * The returned Alias Output can be further customized before publication, if desired. - * - * NOTE: this does *not* publish the Alias Output. - */ - async newDidOutput(address: Address, document: IotaDocument, rentStructure?: IRent): Promise { - const aliasOutputParams: AliasOutputBuilderParams = await IotaIdentityClientExt.newDidOutput( - this, - address, - document, - rentStructure, - ); - return await this.client.buildAliasOutput(aliasOutputParams); - } - - /** Fetches the associated Alias Output and updates it with `document` in its state metadata. - * The storage deposit on the output is left unchanged. If the size of the document increased, - * the amount should be increased manually. - * - * NOTE: this does *not* publish the updated Alias Output. - */ - async updateDidOutput(document: IotaDocument): Promise { - const aliasOutputParams: AliasOutputBuilderParams = await IotaIdentityClientExt.updateDidOutput(this, document); - return await this.client.buildAliasOutput(aliasOutputParams); - } - - /** Removes the DID document from the state metadata of its Alias Output, - * effectively deactivating it. The storage deposit on the output is left unchanged, - * and should be reallocated manually. - * - * Deactivating does not destroy the output. Hence, it can be re-activated by publishing - * an update containing a DID document. - * - * NOTE: this does *not* publish the updated Alias Output. - */ - async deactivateDidOutput(did: IotaDID): Promise { - const aliasOutputParams: AliasOutputBuilderParams = await IotaIdentityClientExt.deactivateDidOutput(this, did); - return await this.client.buildAliasOutput(aliasOutputParams); - } - - /** Resolve a {@link IotaDocument}. Returns an empty, deactivated document if the state - * metadata of the Alias Output is empty. - */ - async resolveDid(did: IotaDID): Promise { - return await IotaIdentityClientExt.resolveDid(this, did); - } - - /** Fetches the Alias Output associated with the given DID. */ - async resolveDidOutput(did: IotaDID): Promise { - const aliasOutputParams: AliasOutputBuilderParams = await IotaIdentityClientExt.resolveDidOutput(this, did); - return await this.client.buildAliasOutput(aliasOutputParams); - } - - /** Publish the given `aliasOutput` with the provided `secretManager`, and returns - * the DID document extracted from the published block. - * - * Note that only the state controller of an Alias Output is allowed to update its state. - * This will attempt to move tokens to or from the state controller address to match - * the storage deposit amount specified on `aliasOutput`. - * - * This method modifies the on-ledger state. - */ - async publishDidOutput(secretManager: SecretManagerType, aliasOutput: AliasOutput): Promise { - const networkHrp = await this.getNetworkHrp(); - // Publish block. - const [blockId, block] = await this.client.buildAndPostBlock(secretManager, { - outputs: [aliasOutput], - }); - await this.client.retryUntilIncluded(blockId); - - // Extract document with computed AliasId. - const documents = IotaDocument.unpackFromBlock(networkHrp, block); - if (documents.length < 1) { - throw new Error("publishDidOutput: no DID document in transaction payload"); - } - return documents[0]; - } - - /** Destroy the Alias Output containing the given `did`, sending its tokens to a new Basic Output - * unlockable by the given address. - * - * Note that only the governor of an Alias Output is allowed to destroy it. - * - * ### WARNING - * - * This destroys the Alias Output and DID document, rendering them permanently unrecoverable. - */ - async deleteDidOutput(secretManager: SecretManagerType, address: Address, did: IotaDID) { - const networkHrp = await this.getNetworkHrp(); - if (networkHrp !== did.network()) { - throw new Error( - "deleteDidOutput: DID network mismatch, client expected `" + networkHrp + "`, DID network is `" - + did.network() + "`", - ); - } - - const aliasId: string = did.tag(); - const [outputId, aliasOutput] = await this.getAliasOutput(aliasId); - const aliasInput: UTXOInput = UTXOInput.fromOutputId(outputId); - - // Send funds to the address. - const basicOutput = await this.client.buildBasicOutput({ - amount: aliasOutput.getAmount(), - nativeTokens: aliasOutput.getNativeTokens(), - unlockConditions: [ - new AddressUnlockCondition(address), - ], - }); - - // Publish block. - const [blockId, _block] = await this.client.buildAndPostBlock(secretManager, { - inputs: [aliasInput], - outputs: [basicOutput], - burn: { - aliases: [aliasId], - }, - }); - await this.client.retryUntilIncluded(blockId); - } -} diff --git a/bindings/wasm/lib/tsconfig.json b/bindings/wasm/lib/tsconfig.json deleted file mode 100644 index f656dc1cc7..0000000000 --- a/bindings/wasm/lib/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../tsconfig.node.json", - "compilerOptions": { - "baseUrl": "./", - "paths": { - "~identity_wasm": ["../node/identity_wasm", "./identity_wasm.js"], - "~sdk-wasm": ["../node_modules/@iota/sdk-wasm/node", "@iota/sdk-wasm/node"], - "../lib": ["."] - }, - "outDir": "../node", - "declarationDir": "../node" - } -} diff --git a/bindings/wasm/src/iota/identity_client.rs b/bindings/wasm/src/iota/identity_client.rs deleted file mode 100644 index 5076a877da..0000000000 --- a/bindings/wasm/src/iota/identity_client.rs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use core::fmt::Debug; -use core::fmt::Formatter; - -use identity_iota::iota::block::output::dto::AliasOutputDto; -use identity_iota::iota::block::output::AliasId; -use identity_iota::iota::block::output::AliasOutput; -use identity_iota::iota::block::output::OutputId; -use identity_iota::iota::block::protocol::ProtocolParameters; -use identity_iota::iota::block::TryFromDto; -use identity_iota::iota::IotaIdentityClient; -use js_sys::Promise; -use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::JsFuture; - -use crate::error::JsValueResult; - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(typescript_type = "IIotaIdentityClient")] - pub type WasmIotaIdentityClient; - - #[allow(non_snake_case)] - #[wasm_bindgen(method, js_name = getAliasOutput)] - pub fn get_alias_output(this: &WasmIotaIdentityClient, aliasId: String) -> JsValue; - - #[wasm_bindgen(method, js_name = getProtocolParameters)] - pub fn get_protocol_parameters(this: &WasmIotaIdentityClient) -> JsValue; -} - -impl Debug for WasmIotaIdentityClient { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.write_str("WasmIotaIdentityClient") - } -} - -#[async_trait::async_trait(?Send)] -impl IotaIdentityClient for WasmIotaIdentityClient { - async fn get_alias_output(&self, id: AliasId) -> Result<(OutputId, AliasOutput), identity_iota::iota::Error> { - let promise: Promise = Promise::resolve(&WasmIotaIdentityClient::get_alias_output(self, id.to_string())); - let result: JsValueResult = JsFuture::from(promise).await.into(); - let tuple: js_sys::Array = js_sys::Array::from(&result.to_iota_core_error()?); - - let mut iter: js_sys::ArrayIter<'_> = tuple.iter(); - - let output_id: OutputId = iter - .next() - .ok_or_else(|| identity_iota::iota::Error::JsError("get_alias_output expected a tuple of size 2".to_owned()))? - .into_serde() - .map_err(|err| { - identity_iota::iota::Error::JsError(format!("get_alias_output failed to deserialize OutputId: {err}")) - })?; - let alias_dto: AliasOutputDto = iter - .next() - .ok_or_else(|| identity_iota::iota::Error::JsError("get_alias_output expected a tuple of size 2".to_owned()))? - .into_serde() - .map_err(|err| { - identity_iota::iota::Error::JsError(format!("get_alias_output failed to deserialize AliasOutputDto: {err}")) - })?; - - let alias_output = AliasOutput::try_from_dto(alias_dto).map_err(|err| { - identity_iota::iota::Error::JsError(format!("get_alias_output failed to convert AliasOutputDto: {err}")) - })?; - Ok((output_id, alias_output)) - } - - async fn get_protocol_parameters(&self) -> Result { - let promise: Promise = Promise::resolve(&WasmIotaIdentityClient::get_protocol_parameters(self)); - let result: JsValueResult = JsFuture::from(promise).await.into(); - let protocol_parameters: ProtocolParameters = result.to_iota_core_error().and_then(|parameters| { - parameters - .into_serde() - .map_err(|err| identity_iota::iota::Error::JsError(format!("could not obtain protocol parameters: {err}"))) - })?; - - Ok(protocol_parameters) - } -} - -#[wasm_bindgen(typescript_custom_section)] -const I_IOTA_IDENTITY_CLIENT: &'static str = r#" -import type { AliasOutput } from '~sdk-wasm'; -/** Helper interface necessary for `IotaIdentityClientExt`. */ -interface IIotaIdentityClient { - - /** Resolve an Alias identifier, returning its latest `OutputId` and `AliasOutput`. */ - getAliasOutput(aliasId: string): Promise<[string, AliasOutput]>; - - /** Returns the protocol parameters. */ - getProtocolParameters(): Promise; -}"#; diff --git a/bindings/wasm/src/iota/identity_client_ext.rs b/bindings/wasm/src/iota/identity_client_ext.rs deleted file mode 100644 index cdb0ae7ec4..0000000000 --- a/bindings/wasm/src/iota/identity_client_ext.rs +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use identity_iota::iota::block::address::dto::AddressDto; -use identity_iota::iota::block::address::Address; -use identity_iota::iota::block::output::dto::AliasOutputDto; -use identity_iota::iota::block::output::AliasOutput; -use identity_iota::iota::block::output::RentStructure; -use identity_iota::iota::IotaDID; -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use js_sys::Promise; -use wasm_bindgen::prelude::*; -use wasm_bindgen::JsCast; -use wasm_bindgen_futures::future_to_promise; - -use crate::error::Result; -use crate::error::WasmResult; -use crate::iota::identity_client::WasmIotaIdentityClient; -use crate::iota::WasmIotaDID; -use crate::iota::WasmIotaDocument; - -// `IAliasOutput`, `AddressTypes`, and `IRent` are external interfaces. -// See the custom TypeScript section in `identity_client.rs` for the first import statement. -#[wasm_bindgen(typescript_custom_section)] -const TYPESCRIPT_IMPORTS: &'static str = - r#"import type { AliasOutputBuilderParams, Address, IRent } from '~sdk-wasm';"#; -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(typescript_type = "Promise")] - pub type PromiseAliasOutputBuilderParams; - - #[wasm_bindgen(typescript_type = "Promise")] - pub type PromiseIotaDocument; - - #[wasm_bindgen(typescript_type = "Address")] - pub type WasmAddress; - - #[wasm_bindgen(typescript_type = "AliasOutputBuilderParams")] - pub type WasmAliasOutput; - - #[wasm_bindgen(typescript_type = "IRent")] - pub type IRent; -} - -/// An extension interface that provides helper functions for publication -/// and resolution of DID documents in Alias Outputs. -#[wasm_bindgen(js_name = IotaIdentityClientExt)] -pub struct WasmIotaIdentityClientExt; - -#[wasm_bindgen(js_class = IotaIdentityClientExt)] -impl WasmIotaIdentityClientExt { - /// Create a DID with a new Alias Output containing the given `document`. - /// - /// The `address` will be set as the state controller and governor unlock conditions. - /// The minimum required token deposit amount will be set according to the given - /// `rent_structure`, which will be fetched from the node if not provided. - /// The returned Alias Output can be further customised before publication, if desired. - /// - /// NOTE: this does *not* publish the Alias Output. - #[allow(non_snake_case)] - #[wasm_bindgen(js_name = newDidOutput)] - pub fn new_did_output( - client: WasmIotaIdentityClient, - address: WasmAddress, - document: &WasmIotaDocument, - rentStructure: Option, - ) -> Result { - let address_dto: AddressDto = address.into_serde().wasm_result()?; - let address: Address = Address::try_from(address_dto.clone()) - .map_err(|err| { - identity_iota::iota::Error::JsError(format!("newDidOutput failed to decode Address: {err}: {address_dto:?}")) - }) - .wasm_result()?; - let doc: IotaDocument = document.0.try_read()?.clone(); - - let promise: Promise = future_to_promise(async move { - let rent_structure: Option = rentStructure - .map(|rent| rent.into_serde::()) - .transpose() - .wasm_result()?; - - let output: AliasOutput = IotaIdentityClientExt::new_did_output(&client, address, doc, rent_structure) - .await - .wasm_result()?; - // Use DTO for correct serialization. - let dto: AliasOutputDto = AliasOutputDto::from(&output); - JsValue::from_serde(&dto).wasm_result() - }); - - // WARNING: this does not validate the return type. Check carefully. - Ok(promise.unchecked_into::()) - } - - /// Fetches the associated Alias Output and updates it with `document` in its state metadata. - /// The storage deposit on the output is left unchanged. If the size of the document increased, - /// the amount should be increased manually. - /// - /// NOTE: this does *not* publish the updated Alias Output. - #[wasm_bindgen(js_name = updateDidOutput)] - pub fn update_did_output( - client: WasmIotaIdentityClient, - document: &WasmIotaDocument, - ) -> Result { - let document: IotaDocument = document.0.try_read()?.clone(); - let promise: Promise = future_to_promise(async move { - let output: AliasOutput = IotaIdentityClientExt::update_did_output(&client, document) - .await - .wasm_result()?; - // Use DTO for correct serialization. - let dto: AliasOutputDto = AliasOutputDto::from(&output); - JsValue::from_serde(&dto).wasm_result() - }); - - // WARNING: this does not validate the return type. Check carefully. - Ok(promise.unchecked_into::()) - } - - /// Removes the DID document from the state metadata of its Alias Output, - /// effectively deactivating it. The storage deposit on the output is left unchanged, - /// and should be reallocated manually. - /// - /// Deactivating does not destroy the output. Hence, it can be re-activated by publishing - /// an update containing a DID document. - /// - /// NOTE: this does *not* publish the updated Alias Output. - #[wasm_bindgen(js_name = deactivateDidOutput)] - pub fn deactivate_did_output( - client: WasmIotaIdentityClient, - did: &WasmIotaDID, - ) -> Result { - let did: IotaDID = did.0.clone(); - let promise: Promise = future_to_promise(async move { - let output: AliasOutput = IotaIdentityClientExt::deactivate_did_output(&client, &did) - .await - .wasm_result()?; - // Use DTO for correct serialization. - let dto: AliasOutputDto = AliasOutputDto::from(&output); - JsValue::from_serde(&dto).wasm_result() - }); - - // WARNING: this does not validate the return type. Check carefully. - Ok(promise.unchecked_into::()) - } - - /// Resolve a {@link IotaDocument}. Returns an empty, deactivated document if the state metadata - /// of the Alias Output is empty. - #[wasm_bindgen(js_name = resolveDid)] - pub fn resolve_did(client: WasmIotaIdentityClient, did: &WasmIotaDID) -> Result { - let did: IotaDID = did.0.clone(); - let promise: Promise = future_to_promise(async move { - IotaIdentityClientExt::resolve_did(&client, &did) - .await - .map(WasmIotaDocument::from) - .map(Into::into) - .wasm_result() - }); - - // WARNING: this does not validate the return type. Check carefully. - Ok(promise.unchecked_into::()) - } - - /// Fetches the `IAliasOutput` associated with the given DID. - #[wasm_bindgen(js_name = resolveDidOutput)] - pub fn resolve_did_output( - client: WasmIotaIdentityClient, - did: &WasmIotaDID, - ) -> Result { - let did: IotaDID = did.0.clone(); - let promise: Promise = future_to_promise(async move { - let output: AliasOutput = IotaIdentityClientExt::resolve_did_output(&client, &did) - .await - .wasm_result()?; - // Use DTO for correct serialization. - let dto: AliasOutputDto = AliasOutputDto::from(&output); - JsValue::from_serde(&dto).wasm_result() - }); - - // WARNING: this does not validate the return type. Check carefully. - Ok(promise.unchecked_into::()) - } -} diff --git a/bindings/wasm/tsconfig.json b/bindings/wasm/tsconfig.json index ae8ae8a323..a240a86235 100644 --- a/bindings/wasm/tsconfig.json +++ b/bindings/wasm/tsconfig.json @@ -1,17 +1,19 @@ { - "compilerOptions": { - "baseUrl": ".", - "lib": ["ES2020", "DOM"], - "declaration": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "node", - "noImplicitAny": true, - "preserveConstEnums": true, - "forceConsistentCasingInFileNames": true, - "paths": { - "@iota/identity-wasm/*": ["./*"] - } - }, - "exclude": ["node_modules"] + "compilerOptions": { + "lib": [ + "ES2020", + "DOM" + ], + "declaration": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "node", + "noImplicitAny": true, + "preserveConstEnums": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "exclude": [ + "node_modules" + ] } diff --git a/bindings/wasm/tsconfig.node.json b/bindings/wasm/tsconfig.node.json deleted file mode 100644 index 6e8349baee..0000000000 --- a/bindings/wasm/tsconfig.node.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "target": "ES2020", - "esModuleInterop": true, - "module": "commonjs" - } -} diff --git a/bindings/wasm/typedoc.json b/bindings/wasm/typedoc.json index c6874ebe33..fb75ecc3e2 100644 --- a/bindings/wasm/typedoc.json +++ b/bindings/wasm/typedoc.json @@ -1,23 +1,27 @@ { - "$schema": "https://typedoc.org/schema.json", - "disableSources": true, - "excludePrivate": true, - "excludeInternal": true, - "excludeNotDocumented": true, - "excludeExternals": true, - "entryPoints": ["./node/"], - "entryPointStrategy": "expand", - "tsconfig": "./tsconfig.typedoc.json", - "out": "./docs/wasm", - "plugin": ["typedoc-plugin-markdown"], - "readme": "none", - "githubPages": false, - "theme": "markdown", - "entryDocument": "api_ref.md", - "hideBreadcrumbs": true, - "hideGenerator": true, - "sort": ["source-order"], - "compilerOptions": { - "skipLibCheck": true, - } + "$schema": "https://typedoc.org/schema.json", + "disableSources": true, + "excludePrivate": true, + "excludeInternal": true, + "excludeNotDocumented": true, + "excludeExternals": true, + "entryPointStrategy": "expand", + "plugin": [ + "typedoc-plugin-markdown" + ], + "readme": "none", + "githubPages": false, + "theme": "markdown", + "entryFileName": "api_ref.md", + "hideBreadcrumbs": true, + "hideGenerator": true, + "sort": [ + "source-order" + ], + "compilerOptions": { + "skipLibCheck": true + }, + "validation": { + "notDocumented": true, + } } diff --git a/dprint.json b/dprint.json index 97825bb4b5..9ce73b412d 100644 --- a/dprint.json +++ b/dprint.json @@ -12,7 +12,8 @@ "excludes": [ "documentation", "**/{node_modules,target}", - "bindings/wasm/{node,web}/**/*.{js,ts}" + "bindings/wasm/identity_wasm/{node,web}/**/*.{js,ts}", + "bindings/wasm/iota_interaction_ts/{node,web}/**/*.{js,ts}" ], "plugins": [ "https://plugins.dprint.dev/toml-0.5.1.wasm", diff --git a/examples/0_basic/0_create_did.rs b/examples/0_basic/0_create_did.rs index 61f157cb37..35a61a52aa 100644 --- a/examples/0_basic/0_create_did.rs +++ b/examples/0_basic/0_create_did.rs @@ -1,26 +1,12 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::get_address_with_funds; -use examples::random_stronghold_path; -use examples::MemStorage; -use identity_iota::iota::IotaClientExt; -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; -use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use identity_iota::verification::jws::JwsAlgorithm; -use identity_iota::verification::MethodScope; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; +use examples::create_did_document; +use examples::get_funded_client; -/// Demonstrates how to create a DID Document and publish it in a new Alias Output. +use examples::get_memstorage; + +/// Demonstrates how to create a DID Document and publish it on chain. /// /// In this example we connect to a locally running private network, but it can be adapted /// to run on any IOTA node by setting the network and faucet endpoints. @@ -29,54 +15,17 @@ use iota_sdk::types::block::output::AliasOutput; /// https://github.com/iotaledger/hornet/tree/develop/private_tangle #[tokio::main] async fn main() -> anyhow::Result<()> { - // The API endpoint of an IOTA node, e.g. Hornet. - let api_endpoint: &str = "http://localhost"; - - // The faucet endpoint allows requesting funds for testing purposes. - let faucet_endpoint: &str = "http://localhost/faucet/api/enqueue"; - - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(api_endpoint, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Get an address with funds for testing. - let address: Address = get_address_with_funds(&client, &secret_manager, faucet_endpoint).await?; + // create new client to interact with chain and get funded account with keys + let storage = get_memstorage()?; + let identity_client = get_funded_client(&storage).await?; - // Get the Bech32 human-readable part (HRP) of the network. - let network_name: NetworkName = client.network_name().await?; - - // Create a new DID document with a placeholder DID. - // The DID will be derived from the Alias Id of the Alias Output after publishing. - let mut document: IotaDocument = IotaDocument::new(&network_name); - - // Insert a new Ed25519 verification method in the DID document. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - document - .generate_method( - &storage, - JwkMemStore::ED25519_KEY_TYPE, - JwsAlgorithm::EdDSA, - None, - MethodScope::VerificationMethod, - ) - .await?; - - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; - - // Publish the Alias Output and get the published DID document. - let document: IotaDocument = client.publish_did_output(&secret_manager, alias_output).await?; + // create new DID document and publish it + let (document, _) = create_did_document(&identity_client, &storage).await?; println!("Published DID document: {document:#}"); + // check if we can resolve it via client + let resolved = identity_client.resolve_did(document.id()).await?; + println!("Resolved DID document: {resolved:#}"); + Ok(()) } diff --git a/examples/0_basic/1_update_did.rs b/examples/0_basic/1_update_did.rs index 2b89b74348..e15d04a117 100644 --- a/examples/0_basic/1_update_did.rs +++ b/examples/0_basic/1_update_did.rs @@ -1,62 +1,39 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; +use examples::create_did_document; +use examples::get_funded_client; +use examples::get_memstorage; +use examples::TEST_GAS_BUDGET; use identity_iota::core::json; use identity_iota::core::FromJson; use identity_iota::core::Timestamp; use identity_iota::did::DIDUrl; use identity_iota::did::DID; use identity_iota::document::Service; -use identity_iota::iota::block::address::Address; -use identity_iota::iota::block::output::RentStructure; -use identity_iota::iota::IotaClientExt; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodRelationship; use identity_iota::verification::MethodScope; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -/// Demonstrates how to update a DID document in an existing Alias Output. +/// Demonstrates how to update a DID document in an existing identity. #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID in an Alias Output for us to modify. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, document, fragment_1): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; + // create new client to interact with chain and get funded account with keys + let storage = get_memstorage()?; + let identity_client = get_funded_client(&storage).await?; + // create new DID document and publish it + let (document, vm_fragment_1) = create_did_document(&identity_client, &storage).await?; let did: IotaDID = document.id().clone(); // Resolve the latest state of the document. - let mut document: IotaDocument = client.resolve_did(&did).await?; + let mut document: IotaDocument = identity_client.resolve_did(&did).await?; // Insert a new Ed25519 verification method in the DID document. - let fragment_2: String = document + let vm_fragment_2: String = document .generate_method( &storage, JwkMemStore::ED25519_KEY_TYPE, @@ -68,7 +45,7 @@ async fn main() -> anyhow::Result<()> { // Attach a new method relationship to the inserted method. document.attach_method_relationship( - &document.id().to_url().join(format!("#{fragment_2}"))?, + &document.id().to_url().join(format!("#{vm_fragment_2}"))?, MethodRelationship::Authentication, )?; @@ -82,22 +59,16 @@ async fn main() -> anyhow::Result<()> { document.metadata.updated = Some(Timestamp::now_utc()); // Remove a verification method. - let original_method: DIDUrl = document.resolve_method(fragment_1.as_str(), None).unwrap().id().clone(); + let original_method: DIDUrl = document.resolve_method(&vm_fragment_1, None).unwrap().id().clone(); document.purge_method(&storage, &original_method).await.unwrap(); - // Resolve the latest output and update it with the given document. - let alias_output: AliasOutput = client.update_did_output(document.clone()).await?; - - // Because the size of the DID document increased, we have to increase the allocated storage deposit. - // This increases the deposit amount to the new minimum. - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; + let updated = identity_client + .publish_did_document_update(document.clone(), TEST_GAS_BUDGET) + .await?; + println!("Updated DID document result: {updated:#}"); - // Publish the updated Alias Output. - let updated: IotaDocument = client.publish_did_output(&secret_manager, alias_output).await?; - println!("Updated DID document: {updated:#}"); + let resolved: IotaDocument = identity_client.resolve_did(&did).await?; + println!("Updated DID document resolved from chain: {resolved:#}"); Ok(()) } diff --git a/examples/0_basic/2_resolve_did.rs b/examples/0_basic/2_resolve_did.rs index 4e648f8370..233132b20d 100644 --- a/examples/0_basic/2_resolve_did.rs +++ b/examples/0_basic/2_resolve_did.rs @@ -1,48 +1,27 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use identity_iota::iota::block::address::Address; +use examples::create_did_document; +use examples::get_funded_client; +use examples::get_memstorage; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::prelude::Resolver; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::output::AliasOutput; -/// Demonstrates how to resolve an existing DID in an Alias Output. +/// Demonstrates how to resolve an existing DID in an identity. #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID in an Alias Output for us to resolve. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, document, _): (Address, IotaDocument, String) = create_did(&client, &mut secret_manager, &storage).await?; + // create new client to interact with chain and get funded account with keys + let storage = get_memstorage()?; + let identity_client = get_funded_client(&storage).await?; + // create new DID document and publish it + let (document, _) = create_did_document(&identity_client, &storage).await?; let did = document.id().clone(); - // We can resolve a `IotaDID` with the client itself. - // Resolve the associated Alias Output and extract the DID document from it. - let client_document: IotaDocument = client.resolve_did(&did).await?; + // We can resolve a `IotaDID` to bytes via client. + // Resolve the associated identity and extract the DID document from it. + let client_document: IotaDocument = identity_client.resolve_did(&did).await?; println!("Client resolved DID Document: {client_document:#}"); // We can also create a `Resolver` that has additional convenience methods, @@ -51,17 +30,12 @@ async fn main() -> anyhow::Result<()> { // We need to register a handler that can resolve IOTA DIDs. // This convenience method only requires us to provide a client. - resolver.attach_iota_handler(client.clone()); + resolver.attach_iota_handler((*identity_client).clone()); let resolver_document: IotaDocument = resolver.resolve(&did).await.unwrap(); - // Client and Resolver resolve to the same document in this case. + // Client and Resolver resolve to the same document. assert_eq!(client_document, resolver_document); - // We can also resolve the Alias Output directly. - let alias_output: AliasOutput = client.resolve_did_output(&did).await?; - - println!("The Alias Output holds {} tokens", alias_output.amount()); - Ok(()) } diff --git a/examples/0_basic/3_deactivate_did.rs b/examples/0_basic/3_deactivate_did.rs index 1a4a5d998f..4d3b12c601 100644 --- a/examples/0_basic/3_deactivate_did.rs +++ b/examples/0_basic/3_deactivate_did.rs @@ -1,82 +1,46 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use identity_iota::iota::block::address::Address; -use identity_iota::iota::IotaClientExt; +use examples::create_did_document; +use examples::get_funded_client; +use examples::get_memstorage; +use examples::TEST_GAS_BUDGET; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -/// Demonstrates how to deactivate a DID in an Alias Output. +/// Demonstrates how to deactivate a DID in an identity. #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; + // create new client to interact with chain and get funded account with keys + let storage = get_memstorage()?; + let identity_client = get_funded_client(&storage).await?; - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); + // create new DID document and publish it + let (document, _) = create_did_document(&identity_client, &storage).await?; - // Create a new DID in an Alias Output for us to modify. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, document, _): (Address, IotaDocument, String) = create_did(&client, &mut secret_manager, &storage).await?; - let did: IotaDID = document.id().clone(); + println!("Published DID document: {document:#}"); - // Resolve the latest state of the DID document. - let document: IotaDocument = client.resolve_did(&did).await?; + let did: IotaDID = document.id().clone(); // Deactivate the DID by publishing an empty document. - // This process can be reversed since the Alias Output is not destroyed. - // Deactivation may only be performed by the state controller of the Alias Output. - let deactivated_output: AliasOutput = client.deactivate_did_output(&did).await?; - - // Optional: reduce and reclaim the storage deposit, sending the tokens to the state controller. - let rent_structure = client.get_rent_structure().await?; - let deactivated_output = AliasOutputBuilder::from(&deactivated_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - - // Publish the deactivated DID document. - let _ = client.publish_did_output(&secret_manager, deactivated_output).await?; + // This process can be reversed since the identity is not destroyed. + // Deactivation may only be performed by a controller of the identity. + identity_client.deactivate_did_output(&did, TEST_GAS_BUDGET).await?; // Resolving a deactivated DID returns an empty DID document // with its `deactivated` metadata field set to `true`. - let deactivated: IotaDocument = client.resolve_did(&did).await?; + let deactivated: IotaDocument = identity_client.resolve_did(&did).await?; println!("Deactivated DID document: {deactivated:#}"); assert_eq!(deactivated.metadata.deactivated, Some(true)); // Re-activate the DID by publishing a valid DID document. - let reactivated_output: AliasOutput = client.update_did_output(document.clone()).await?; - - // Increase the storage deposit to the minimum again, if it was reclaimed during deactivation. - let rent_structure = client.get_rent_structure().await?; - let reactivated_output = AliasOutputBuilder::from(&reactivated_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - client.publish_did_output(&secret_manager, reactivated_output).await?; + let reactivated: IotaDocument = identity_client + .publish_did_document_update(document.clone(), TEST_GAS_BUDGET) + .await?; + println!("Reactivated DID document result: {reactivated:#}"); - // Resolve the reactivated DID document. - let reactivated: IotaDocument = client.resolve_did(&did).await?; - assert_eq!(document, reactivated); - assert!(!reactivated.metadata.deactivated.unwrap_or_default()); + let resolved: IotaDocument = identity_client.resolve_did(&did).await?; + println!("Reactivated DID document resolved from chain: {resolved:#}"); Ok(()) } diff --git a/examples/0_basic/4_delete_did.rs b/examples/0_basic/4_delete_did.rs deleted file mode 100644 index 738ba534aa..0000000000 --- a/examples/0_basic/4_delete_did.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use identity_iota::iota::Error; -use identity_iota::iota::IotaClientExt; - -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; - -/// Demonstrates how to delete a DID in an Alias Output, reclaiming the storage deposit. -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID in an Alias Output for us to modify. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (address, document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; - let did = document.id().clone(); - - // Deletes the Alias Output and its contained DID Document, rendering the DID permanently destroyed. - // This operation is *not* reversible. - // Deletion can only be done by the governor of the Alias Output. - client.delete_did_output(&secret_manager, address, &did).await?; - - // Attempting to resolve a deleted DID results in a `NoOutput` error. - let error: Error = client.resolve_did(&did).await.unwrap_err(); - - assert!(matches!( - error, - identity_iota::iota::Error::DIDResolutionError(iota_sdk::client::Error::Node( - iota_sdk::client::node_api::error::Error::NotFound(..) - )) - )); - - Ok(()) -} diff --git a/examples/0_basic/5_create_vc.rs b/examples/0_basic/5_create_vc.rs index 3a14e262e2..65ed7255cd 100644 --- a/examples/0_basic/5_create_vc.rs +++ b/examples/0_basic/5_create_vc.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 //! This example shows how to create a Verifiable Credential and validate it. @@ -9,8 +9,9 @@ //! //! cargo run --release --example 5_create_vc -use examples::create_did; -use examples::MemStorage; +use examples::create_did_document; +use examples::get_funded_client; +use examples::get_memstorage; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::Object; @@ -19,17 +20,8 @@ use identity_iota::credential::Jwt; use identity_iota::credential::JwtCredentialValidationOptions; use identity_iota::credential::JwtCredentialValidator; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use examples::random_stronghold_path; -use examples::API_ENDPOINT; use identity_iota::core::json; use identity_iota::core::FromJson; use identity_iota::core::Url; @@ -38,39 +30,22 @@ use identity_iota::credential::CredentialBuilder; use identity_iota::credential::FailFast; use identity_iota::credential::Subject; use identity_iota::did::DID; -use identity_iota::iota::IotaDocument; #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create an identity for the issuer with one verification method `key-1`. - let mut secret_manager_issuer: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); - let issuer_storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, issuer_document, fragment): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_issuer, &issuer_storage).await?; + // create new issuer account with did document + let issuer_storage = get_memstorage()?; + let issuer_identity_client = get_funded_client(&issuer_storage).await?; + let (issuer_document, issuer_vm_fragment) = create_did_document(&issuer_identity_client, &issuer_storage).await?; - // Create an identity for the holder, in this case also the subject. - let mut secret_manager_alice: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_2".to_owned())) - .build(random_stronghold_path())?, - ); - let alice_storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, alice_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_alice, &alice_storage).await?; + // create new holder account with did document + let holder_storage = get_memstorage()?; + let holder_identity_client = get_funded_client(&holder_storage).await?; + let (holder_document, _) = create_did_document(&holder_identity_client, &holder_storage).await?; // Create a credential subject indicating the degree earned by Alice. let subject: Subject = Subject::from_json_value(json!({ - "id": alice_document.id().as_str(), + "id": holder_document.id().as_str(), "name": "Alice", "degree": { "type": "BachelorDegree", @@ -91,7 +66,7 @@ async fn main() -> anyhow::Result<()> { .create_credential_jwt( &credential, &issuer_storage, - &fragment, + &issuer_vm_fragment, &JwsSignatureOptions::default(), None, ) diff --git a/examples/0_basic/6_create_vp.rs b/examples/0_basic/6_create_vp.rs index 8c157295ef..3f9f2b49fe 100644 --- a/examples/0_basic/6_create_vp.rs +++ b/examples/0_basic/6_create_vp.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 //! This example shows how to create a Verifiable Presentation and validate it. @@ -9,8 +9,9 @@ use std::collections::HashMap; -use examples::create_did; -use examples::MemStorage; +use examples::create_did_document; +use examples::get_funded_client; +use examples::get_memstorage; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::Object; use identity_iota::credential::DecodedJwtCredential; @@ -26,17 +27,8 @@ use identity_iota::credential::PresentationBuilder; use identity_iota::did::CoreDID; use identity_iota::document::verifiable::JwsVerificationOptions; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; - -use examples::random_stronghold_path; -use examples::API_ENDPOINT; + use identity_iota::core::json; use identity_iota::core::Duration; use identity_iota::core::FromJson; @@ -59,31 +51,20 @@ async fn main() -> anyhow::Result<()> { // Step 1: Create identities for the issuer and the holder. // =========================================================================== - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; + // create new issuer account with did document + let issuer_storage = get_memstorage()?; + let issuer_identity_client = get_funded_client(&issuer_storage).await?; + let (issuer_document, issuer_vm_fragment) = create_did_document(&issuer_identity_client, &issuer_storage).await?; + + // create new holder account with did document + let holder_storage = get_memstorage()?; + let holder_identity_client = get_funded_client(&holder_storage).await?; + let (holder_document, holder_vm_fragment) = create_did_document(&holder_identity_client, &holder_storage).await?; - // Create an identity for the issuer with one verification method `key-1`. - let mut secret_manager_issuer: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); - let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, issuer_document, fragment_issuer): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_issuer, &storage_issuer).await?; - - // Create an identity for the holder, in this case also the subject. - let mut secret_manager_alice: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_2".to_owned())) - .build(random_stronghold_path())?, - ); - let storage_alice: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, alice_document, fragment_alice): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_alice, &storage_alice).await?; + // create new client for verifier + // new client actually not necessary, but shows, that client is independent from issuer and holder + let verifier_storage = &get_memstorage()?; + let verifier_client = get_funded_client(verifier_storage).await?; // =========================================================================== // Step 2: Issuer creates and signs a Verifiable Credential. @@ -91,7 +72,7 @@ async fn main() -> anyhow::Result<()> { // Create a credential subject indicating the degree earned by Alice. let subject: Subject = Subject::from_json_value(json!({ - "id": alice_document.id().as_str(), + "id": holder_document.id().as_str(), "name": "Alice", "degree": { "type": "BachelorDegree", @@ -111,8 +92,8 @@ async fn main() -> anyhow::Result<()> { let credential_jwt: Jwt = issuer_document .create_credential_jwt( &credential, - &storage_issuer, - &fragment_issuer, + &issuer_storage, + &issuer_vm_fragment, &JwsSignatureOptions::default(), None, ) @@ -156,17 +137,17 @@ async fn main() -> anyhow::Result<()> { // Create an unsigned Presentation from the previously issued Verifiable Credential. let presentation: Presentation = - PresentationBuilder::new(alice_document.id().to_url().into(), Default::default()) + PresentationBuilder::new(holder_document.id().to_url().into(), Default::default()) .credential(credential_jwt) .build()?; // Create a JWT verifiable presentation using the holder's verification method // and include the requested challenge and expiry timestamp. - let presentation_jwt: Jwt = alice_document + let presentation_jwt: Jwt = holder_document .create_presentation_jwt( &presentation, - &storage_alice, - &fragment_alice, + &holder_storage, + &holder_vm_fragment, &JwsSignatureOptions::default().nonce(challenge.to_owned()), &JwtPresentationOptions::default().expiration_date(expires), ) @@ -191,7 +172,7 @@ async fn main() -> anyhow::Result<()> { JwsVerificationOptions::default().nonce(challenge.to_owned()); let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client); + resolver.attach_iota_handler((*verifier_client).clone()); // Resolve the holder's document. let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; @@ -231,7 +212,7 @@ async fn main() -> anyhow::Result<()> { // Since no errors were thrown by `verify_presentation` we know that the validation was successful. println!("VP successfully validated: {:#?}", presentation.presentation); - // Note that we did not declare a latest allowed issuance date for credentials. This is because we only want to check - // that the credentials do not have an issuance date in the future which is a default check. + // Note that we did not declare a latest allowed issuance date for credentials. This is because we only want to + // check // that the credentials do not have an issuance date in the future which is a default check. Ok(()) } diff --git a/examples/0_basic/7_revoke_vc.rs b/examples/0_basic/7_revoke_vc.rs index 864041f3e3..e22c5f61be 100644 --- a/examples/0_basic/7_revoke_vc.rs +++ b/examples/0_basic/7_revoke_vc.rs @@ -11,10 +11,10 @@ //! cargo run --release --example 7_revoke_vc use anyhow::anyhow; -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; +use examples::create_did_document; +use examples::get_funded_client; +use examples::get_memstorage; +use examples::TEST_GAS_BUDGET; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::json; use identity_iota::core::FromJson; @@ -37,23 +37,11 @@ use identity_iota::credential::Subject; use identity_iota::did::DIDUrl; use identity_iota::did::DID; use identity_iota::document::Service; -use identity_iota::iota::IotaClientExt; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::prelude::IotaDID; use identity_iota::resolver::Resolver; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::RentStructure; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -61,32 +49,15 @@ async fn main() -> anyhow::Result<()> { // Create a Verifiable Credential. // =========================================================================== - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - let mut secret_manager_issuer: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); + // create new issuer account with did document + let issuer_storage = get_memstorage()?; + let issuer_identity_client = get_funded_client(&issuer_storage).await?; + let (mut issuer_document, issuer_vm_fragment) = create_did_document(&issuer_identity_client, &issuer_storage).await?; - // Create an identity for the issuer with one verification method `key-1`. - let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, mut issuer_document, fragment_issuer): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_issuer, &storage_issuer).await?; - - // Create an identity for the holder, in this case also the subject. - let mut secret_manager_alice: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_2".to_owned())) - .build(random_stronghold_path())?, - ); - let storage_alice: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, alice_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_alice, &storage_alice).await?; + // create new holder account with did document + let holder_storage = get_memstorage()?; + let holder_identity_client = get_funded_client(&holder_storage).await?; + let (holder_document, _) = create_did_document(&holder_identity_client, &holder_storage).await?; // Create a new empty revocation bitmap. No credential is revoked yet. let revocation_bitmap: RevocationBitmap = RevocationBitmap::new(); @@ -98,23 +69,15 @@ async fn main() -> anyhow::Result<()> { assert!(issuer_document.insert_service(service).is_ok()); // Resolve the latest output and update it with the given document. - let alias_output: AliasOutput = client.update_did_output(issuer_document.clone()).await?; - - // Because the size of the DID document increased, we have to increase the allocated storage deposit. - // This increases the deposit amount to the new minimum. - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - - // Publish the updated Alias Output. - issuer_document = client.publish_did_output(&secret_manager_issuer, alias_output).await?; + issuer_document = issuer_identity_client + .publish_did_document_update(issuer_document.clone(), TEST_GAS_BUDGET) + .await?; println!("DID Document > {issuer_document:#}"); // Create a credential subject indicating the degree earned by Alice. let subject: Subject = Subject::from_json_value(json!({ - "id": alice_document.id().as_str(), + "id": holder_document.id().as_str(), "name": "Alice", "degree": { "type": "BachelorDegree", @@ -129,7 +92,7 @@ async fn main() -> anyhow::Result<()> { let credential_index: u32 = 5; let status: Status = RevocationBitmapStatus::new(service_url, credential_index).into(); - // Build credential using subject above, status, and issuer. + // Build credential using subject above and issuer. let credential: Credential = CredentialBuilder::default() .id(Url::parse("https://example.edu/credentials/3732")?) .issuer(Url::parse(issuer_document.id().as_str())?) @@ -143,8 +106,8 @@ async fn main() -> anyhow::Result<()> { let credential_jwt: Jwt = issuer_document .create_credential_jwt( &credential, - &storage_issuer, - &fragment_issuer, + &issuer_storage, + &issuer_vm_fragment, &JwsSignatureOptions::default(), None, ) @@ -169,12 +132,9 @@ async fn main() -> anyhow::Result<()> { issuer_document.revoke_credentials("my-revocation-service", &[credential_index])?; // Publish the changes. - let alias_output: AliasOutput = client.update_did_output(issuer_document.clone()).await?; - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - issuer_document = client.publish_did_output(&secret_manager_issuer, alias_output).await?; + issuer_document = issuer_identity_client + .publish_did_document_update(issuer_document.clone(), TEST_GAS_BUDGET) + .await?; let validation_result: std::result::Result = validator .validate( @@ -197,7 +157,7 @@ async fn main() -> anyhow::Result<()> { // By removing the verification method, that signed the credential, from the issuer's DID document, // we effectively revoke the credential, as it will no longer be possible to validate the signature. let original_method: DIDUrl = issuer_document - .resolve_method(&fragment_issuer, None) + .resolve_method(&issuer_vm_fragment, None) .ok_or_else(|| anyhow!("expected method to exist"))? .id() .clone(); @@ -206,13 +166,13 @@ async fn main() -> anyhow::Result<()> { .ok_or_else(|| anyhow!("expected method to exist"))?; // Publish the changes. - let alias_output: AliasOutput = client.update_did_output(issuer_document.clone()).await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output).finish()?; - client.publish_did_output(&secret_manager_issuer, alias_output).await?; + issuer_identity_client + .publish_did_document_update(issuer_document.clone(), TEST_GAS_BUDGET) + .await?; // We expect the verifiable credential to be revoked. let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client); + resolver.attach_iota_handler((*holder_identity_client).clone()); let resolved_issuer_did: IotaDID = JwtCredentialValidatorUtils::extract_issuer_from_jwt(&credential_jwt)?; let resolved_issuer_doc: IotaDocument = resolver.resolve(&resolved_issuer_did).await?; diff --git a/examples/0_basic/8_legacy_stronghold.rs b/examples/0_basic/8_legacy_stronghold.rs new file mode 100644 index 0000000000..86776a87a4 --- /dev/null +++ b/examples/0_basic/8_legacy_stronghold.rs @@ -0,0 +1,61 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use examples::create_did_document; +use examples::get_funded_client; +use examples::get_stronghold_storage; +use examples::random_stronghold_path; +use identity_eddsa_verifier::EdDSAJwsVerifier; +use identity_iota::credential::Jws; +use identity_iota::document::verifiable::JwsVerificationOptions; +use identity_iota::iota::IotaDocument; +use identity_iota::resolver::Resolver; +use identity_iota::storage::JwkDocumentExt; +use identity_iota::storage::JwsSignatureOptions; +use identity_iota::verification::jws::DecodedJws; + +/// Demonstrates how to use stronghold for secure storage. +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Create storage for key-ids and JWKs. + // + // In this example, the same stronghold file that is used to store + // key-ids as well as the JWKs. + let path = random_stronghold_path(); + let storage = get_stronghold_storage(Some(path.clone()))?; + + // use stronghold storage to create new client to interact with chain and get funded account with keys + let identity_client = get_funded_client(&storage).await?; + // create and publish document with stronghold storage + let (document, vm_fragment) = create_did_document(&identity_client, &storage).await?; + + // Resolve the published DID Document. + let mut resolver = Resolver::::new(); + resolver.attach_iota_handler((*identity_client).clone()); + let resolved_document: IotaDocument = resolver.resolve(document.id()).await.unwrap(); + + drop(storage); + + // Create the storage again to demonstrate that data are read from the existing stronghold file. + let storage = get_stronghold_storage(Some(path))?; + + // Sign data with the created verification method. + let data = b"test_data"; + let jws: Jws = resolved_document + .create_jws(&storage, &vm_fragment, data, &JwsSignatureOptions::default()) + .await?; + + // Verify Signature. + let decoded_jws: DecodedJws = resolved_document.verify_jws( + &jws, + None, + &EdDSAJwsVerifier::default(), + &JwsVerificationOptions::default(), + )?; + + assert_eq!(String::from_utf8_lossy(decoded_jws.claims.as_ref()), "test_data"); + + println!("successfully verified signature"); + + Ok(()) +} diff --git a/examples/0_basic/8_stronghold.rs b/examples/0_basic/8_stronghold.rs deleted file mode 100644 index 0681e5b612..0000000000 --- a/examples/0_basic/8_stronghold.rs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use examples::get_address_with_funds; -use examples::random_stronghold_path; -use identity_eddsa_verifier::EdDSAJwsVerifier; -use identity_iota::credential::Jws; -use identity_iota::document::verifiable::JwsVerificationOptions; -use identity_iota::iota::IotaClientExt; -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; -use identity_iota::resolver::Resolver; -use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::Storage; -use identity_iota::verification::jws::DecodedJws; -use identity_iota::verification::jws::JwsAlgorithm; -use identity_iota::verification::MethodScope; -use identity_stronghold::StrongholdStorage; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; - -/// Demonstrates how to use stronghold for secure storage. -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // The API endpoint of an IOTA node, e.g. Hornet. - let api_endpoint: &str = "http://localhost"; - - // The faucet endpoint allows requesting funds for testing purposes. - let faucet_endpoint: &str = "http://localhost/faucet/api/enqueue"; - - // Stronghold snapshot path. - let path = random_stronghold_path(); - - // Stronghold password. - let password = Password::from("secure_password".to_owned()); - - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(api_endpoint, None)? - .finish() - .await?; - - let stronghold = StrongholdSecretManager::builder() - .password(password.clone()) - .build(path.clone())?; - - // Create a `StrongholdStorage`. - // `StrongholdStorage` creates internally a `SecretManager` that can be - // referenced to avoid creating multiple instances around the same stronghold snapshot. - let stronghold_storage = StrongholdStorage::new(stronghold); - - // Create a DID document. - let address: Address = - get_address_with_funds(&client, stronghold_storage.as_secret_manager(), faucet_endpoint).await?; - let network_name: NetworkName = client.network_name().await?; - let mut document: IotaDocument = IotaDocument::new(&network_name); - - // Create storage for key-ids and JWKs. - // - // In this example, the same stronghold file that is used to store - // key-ids as well as the JWKs. - let storage = Storage::new(stronghold_storage.clone(), stronghold_storage.clone()); - - // Generates a verification method. This will store the key-id as well as the private key - // in the stronghold file. - let fragment = document - .generate_method( - &storage, - JwkMemStore::ED25519_KEY_TYPE, - JwsAlgorithm::EdDSA, - None, - MethodScope::VerificationMethod, - ) - .await?; - - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; - - // Publish the Alias Output and get the published DID document. - let document: IotaDocument = client - .publish_did_output(stronghold_storage.as_secret_manager(), alias_output) - .await?; - - // Resolve the published DID Document. - let mut resolver = Resolver::::new(); - resolver.attach_iota_handler(client.clone()); - let resolved_document: IotaDocument = resolver.resolve(document.id()).await.unwrap(); - - drop(stronghold_storage); - - // Create the storage again to demonstrate that data are read from the stronghold file. - let stronghold = StrongholdSecretManager::builder() - .password(password.clone()) - .build(path.clone())?; - let stronghold_storage = StrongholdStorage::new(stronghold); - let storage = Storage::new(stronghold_storage.clone(), stronghold_storage.clone()); - - // Sign data with the created verification method. - let data = b"test_data"; - let jws: Jws = resolved_document - .create_jws(&storage, &fragment, data, &JwsSignatureOptions::default()) - .await?; - - // Verify Signature. - let decoded_jws: DecodedJws = resolved_document.verify_jws( - &jws, - None, - &EdDSAJwsVerifier::default(), - &JwsVerificationOptions::default(), - )?; - - assert_eq!(String::from_utf8_lossy(decoded_jws.claims.as_ref()), "test_data"); - - Ok(()) -} diff --git a/examples/1_advanced/0_did_controls_did.rs b/examples/1_advanced/0_did_controls_did.rs deleted file mode 100644 index 86cf4eb8b8..0000000000 --- a/examples/1_advanced/0_did_controls_did.rs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::ops::Deref; - -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use identity_iota::iota::block::output::AliasId; -use identity_iota::iota::block::output::UnlockCondition; -use identity_iota::iota::IotaClientExt; -use identity_iota::iota::IotaDID; -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; -use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use identity_iota::verification::jws::JwsAlgorithm; -use identity_iota::verification::MethodScope; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::address::AliasAddress; -use iota_sdk::types::block::output::feature::IssuerFeature; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::RentStructure; - -/// Demonstrates how an identity can control another identity. -/// -/// For this example, we consider the case where a parent company's DID controls the DID of a subsidiary. -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // ======================================================== - // Create the company's and subsidiary's Alias Output DIDs. - // ======================================================== - - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID for the company. - let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, company_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage_issuer).await?; - let company_did = company_document.id().clone(); - - // Get the current byte costs and network name. - let rent_structure: RentStructure = client.get_rent_structure().await?; - let network_name: NetworkName = client.network_name().await?; - - // Construct a new DID document for the subsidiary. - let subsidiary_document: IotaDocument = IotaDocument::new(&network_name); - - // Create a DID for the subsidiary that is controlled by the parent company's DID. - // This means the subsidiary's Alias Output can only be updated or destroyed by - // the state controller or governor of the company's Alias Output respectively. - let subsidiary_alias: AliasOutput = client - .new_did_output( - Address::Alias(AliasAddress::new(AliasId::from(&company_did))), - subsidiary_document, - Some(rent_structure), - ) - .await?; - - let subsidiary_alias: AliasOutput = AliasOutputBuilder::from(&subsidiary_alias) - // Optionally, we can mark the company as the issuer of the subsidiary DID. - // This allows to verify trust relationships between DIDs, as a resolver can - // verify that the subsidiary DID was created by the parent company. - .add_immutable_feature(IssuerFeature::new(AliasAddress::new(AliasId::from(&company_did)))) - // Adding the issuer feature means we have to recalculate the required storage deposit. - .with_minimum_storage_deposit(rent_structure) - .finish()?; - - // Publish the subsidiary's DID. - let mut subsidiary_document: IotaDocument = client.publish_did_output(&secret_manager, subsidiary_alias).await?; - - // ===================================== - // Update the subsidiary's Alias Output. - // ===================================== - - // Add a verification method to the subsidiary. - // This only serves as an example for updating the subsidiary DID. - - let storage_subsidary: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - subsidiary_document - .generate_method( - &storage_subsidary, - JwkMemStore::ED25519_KEY_TYPE, - JwsAlgorithm::EdDSA, - None, - MethodScope::VerificationMethod, - ) - .await?; - - // Update the subsidiary's Alias Output with the updated document - // and increase the storage deposit. - let subsidiary_alias: AliasOutput = client.update_did_output(subsidiary_document).await?; - let subsidiary_alias: AliasOutput = AliasOutputBuilder::from(&subsidiary_alias) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - - // Publish the updated subsidiary's DID. - // - // This works because `secret_manager` can unlock the company's Alias Output, - // which is required in order to update the subsidiary's Alias Output. - let subsidiary_document: IotaDocument = client.publish_did_output(&secret_manager, subsidiary_alias).await?; - - // =================================================================== - // Determine the controlling company's DID given the subsidiary's DID. - // =================================================================== - - // Resolve the subsidiary's Alias Output. - let subsidiary_output: AliasOutput = client.resolve_did_output(subsidiary_document.id()).await?; - - // Extract the company's Alias Id from the state controller unlock condition. - // - // If instead we wanted to determine the original creator of the DID, - // we could inspect the issuer feature. This feature needs to be set when creating the DID. - let company_alias_id: AliasId = if let Some(UnlockCondition::StateControllerAddress(address)) = - subsidiary_output.unlock_conditions().iter().next() - { - if let Address::Alias(alias) = *address.address() { - *alias.alias_id() - } else { - anyhow::bail!("expected an alias address as the state controller"); - } - } else { - anyhow::bail!("expected two unlock conditions"); - }; - - // Reconstruct the company's DID from the Alias Id and the network. - let company_did = IotaDID::new(company_alias_id.deref(), &network_name); - - // Resolve the company's DID document. - let company_document: IotaDocument = client.resolve_did(&company_did).await?; - - println!("Company: {company_document:#}"); - println!("Subsidiary: {subsidiary_document:#}"); - - Ok(()) -} diff --git a/examples/1_advanced/10_zkp_revocation.rs b/examples/1_advanced/10_zkp_revocation.rs index a78dea0e76..807db667ab 100644 --- a/examples/1_advanced/10_zkp_revocation.rs +++ b/examples/1_advanced/10_zkp_revocation.rs @@ -1,11 +1,10 @@ // Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 -use examples::get_address_with_funds; -use examples::random_stronghold_path; +use examples::get_funded_client; use examples::MemStorage; -use examples::API_ENDPOINT; -use examples::FAUCET_ENDPOINT; +use examples::TEST_GAS_BUDGET; + use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::json; use identity_iota::core::Duration; @@ -46,10 +45,13 @@ use identity_iota::did::DIDUrl; use identity_iota::did::DID; use identity_iota::document::verifiable::JwsVerificationOptions; use identity_iota::document::Service; -use identity_iota::iota::IotaClientExt; +use identity_iota::iota::rebased::client::IdentityClient; +use identity_iota::iota::rebased::client::IotaKeySignature; +use identity_iota::iota::rebased::transaction::Transaction; +use identity_iota::iota::rebased::transaction::TransactionOutput; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::iota::NetworkName; +use identity_iota::iota_interaction::OptionalSync; use identity_iota::resolver::Resolver; use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkMemStore; @@ -60,43 +62,37 @@ use identity_iota::storage::KeyType; use identity_iota::storage::TimeframeRevocationExtension; use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodScope; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::RentStructure; +use identity_storage::Storage; use jsonprooftoken::jpa::algs::ProofAlgorithm; +use secret_storage::Signer; use std::thread; use std::time::Duration as SleepDuration; -async fn create_did( - client: &Client, - secret_manager: &SecretManager, - storage: &MemStorage, +async fn create_did( + identity_client: &IdentityClient, + storage: &Storage, key_type: KeyType, alg: Option, proof_alg: Option, -) -> anyhow::Result<(Address, IotaDocument, String)> { - // Get an address with funds for testing. - let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT).await?; - - // Get the Bech32 human-readable part (HRP) of the network. - let network_name: NetworkName = client.network_name().await?; +) -> anyhow::Result<(IotaDocument, String)> +where + K: identity_storage::JwkStorage + identity_storage::JwkStorageBbsPlusExt, + I: identity_storage::KeyIdStorage, + S: Signer + OptionalSync, +{ + // Get the network name. + let network_name: &NetworkName = identity_client.network(); // Create a new DID document with a placeholder DID. - // The DID will be derived from the Alias Id of the Alias Output after publishing. - let mut document: IotaDocument = IotaDocument::new(&network_name); + let mut unpublished: IotaDocument = IotaDocument::new(network_name); // New Verification Method containing a BBS+ key let fragment = if let Some(alg) = alg { - document + unpublished .generate_method(storage, key_type, alg, None, MethodScope::VerificationMethod) .await? } else if let Some(proof_alg) = proof_alg { - let fragment = document + let fragment = unpublished .generate_method_jwp(storage, key_type, proof_alg, None, MethodScope::VerificationMethod) .await?; @@ -104,55 +100,42 @@ async fn create_did( let revocation_bitmap: RevocationBitmap = RevocationBitmap::new(); // Add the revocation bitmap to the DID document of the issuer as a service. - let service_id: DIDUrl = document.id().to_url().join("#my-revocation-service")?; + let service_id: DIDUrl = unpublished.id().to_url().join("#my-revocation-service")?; let service: Service = revocation_bitmap.to_service(service_id)?; - assert!(document.insert_service(service).is_ok()); + assert!(unpublished.insert_service(service).is_ok()); fragment } else { return Err(anyhow::Error::msg("You have to pass at least one algorithm")); }; - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; + let TransactionOutput:: { output: document, .. } = identity_client + .publish_did_document(unpublished) + .execute_with_gas(TEST_GAS_BUDGET, identity_client) + .await?; - // Publish the Alias Output and get the published DID document. - let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?; println!("Published DID document: {document:#}"); - Ok((address, document, fragment)) + Ok((document, fragment)) } -/// Demonstrates how to create an Anonymous Credential with BBS+. +/// Demonstrates how to revoke a credential. #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - let secret_manager_issuer = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); - + // =========================================================================== + // Step 1: Create identities and for the issuer and the holder. + // =========================================================================== let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let secret_manager_holder = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_2".to_owned())) - .build(random_stronghold_path())?, - ); - let storage_holder: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, mut issuer_document, fragment_issuer): (Address, IotaDocument, String) = create_did( - &client, - &secret_manager_issuer, + let issuer_identity_client = get_funded_client(&storage_issuer).await?; + + let holder_identity_client = get_funded_client(&storage_holder).await?; + + let (mut issuer_document, fragment_issuer): (IotaDocument, String) = create_did( + &issuer_identity_client, &storage_issuer, JwkMemStore::BLS12381G2_KEY_TYPE, None, @@ -160,9 +143,8 @@ async fn main() -> anyhow::Result<()> { ) .await?; - let (_, holder_document, fragment_holder): (Address, IotaDocument, String) = create_did( - &client, - &secret_manager_holder, + let (holder_document, fragment_holder): (IotaDocument, String) = create_did( + &holder_identity_client, &storage_holder, JwkMemStore::ED25519_KEY_TYPE, Some(JwsAlgorithm::EdDSA), @@ -414,7 +396,7 @@ async fn main() -> anyhow::Result<()> { JwsVerificationOptions::default().nonce(challenge.to_owned()); let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client.clone()); + resolver.attach_iota_handler((*holder_identity_client).clone()); // Resolve the holder's document. let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; @@ -515,12 +497,9 @@ async fn main() -> anyhow::Result<()> { issuer_document.revoke_credentials("my-revocation-service", &[credential_index])?; // Publish the changes. - let alias_output: AliasOutput = client.update_did_output(issuer_document.clone()).await?; - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - issuer_document = client.publish_did_output(&secret_manager_issuer, alias_output).await?; + issuer_identity_client + .publish_did_document_update(issuer_document.clone(), TEST_GAS_BUDGET) + .await?; // Holder checks if his credential has been revoked by the Issuer let revocation_result = JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024( @@ -529,6 +508,7 @@ async fn main() -> anyhow::Result<()> { StatusCheck::Strict, ); assert!(revocation_result.is_err_and(|e| matches!(e, JwtValidationError::Revoked))); + println!("Credential Revoked!"); Ok(()) } diff --git a/examples/1_advanced/11_linked_verifiable_presentation.rs b/examples/1_advanced/11_linked_verifiable_presentation.rs index 550bad3d41..97b50e8bcf 100644 --- a/examples/1_advanced/11_linked_verifiable_presentation.rs +++ b/examples/1_advanced/11_linked_verifiable_presentation.rs @@ -2,10 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use anyhow::Context; -use examples::create_did; -use examples::random_stronghold_path; + +use examples::create_did_document; +use examples::get_funded_client; +use examples::get_memstorage; use examples::MemStorage; -use examples::API_ENDPOINT; +use examples::TEST_GAS_BUDGET; + use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::FromJson; use identity_iota::core::Object; @@ -26,45 +29,27 @@ use identity_iota::did::CoreDID; use identity_iota::did::DIDUrl; use identity_iota::did::DID; use identity_iota::document::verifiable::JwsVerificationOptions; -use identity_iota::iota::IotaClientExt; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::resolver::Resolver; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::RentStructure; #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - let stronghold_path = random_stronghold_path(); - - println!("Using stronghold path: {stronghold_path:?}"); - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(stronghold_path)?, - ); - - // Create a DID for the entity that will be the holder of the Verifiable Presentation. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, mut did_document, fragment): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; + // =========================================================================== + // Step 1: Create identities and Client + // =========================================================================== + + let storage = get_memstorage()?; + + let identity_client = get_funded_client(&storage).await?; + + // create new DID document and publish it + let (mut did_document, fragment) = create_did_document(&identity_client, &storage).await?; + + println!("Published DID document: {did_document:#}"); + let did: IotaDID = did_document.id().clone(); // ===================================================== @@ -85,7 +70,10 @@ async fn main() -> anyhow::Result<()> { let linked_verifiable_presentation_service = LinkedVerifiablePresentationService::new(service_url, verifiable_presentation_urls, Object::new())?; did_document.insert_service(linked_verifiable_presentation_service.into())?; - let updated_did_document: IotaDocument = publish_document(client.clone(), secret_manager, did_document).await?; + + let updated_did_document: IotaDocument = identity_client + .publish_did_document_update(did_document, TEST_GAS_BUDGET) + .await?; println!("DID document with linked verifiable presentation service: {updated_did_document:#}"); @@ -95,7 +83,7 @@ async fn main() -> anyhow::Result<()> { // Init a resolver for resolving DID Documents. let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client.clone()); + resolver.attach_iota_handler((*identity_client).clone()); // Resolve the DID Document of the DID that issued the credential. let did_document: IotaDocument = resolver.resolve(&did).await?; @@ -107,6 +95,7 @@ async fn main() -> anyhow::Result<()> { .cloned() .filter_map(|service| LinkedVerifiablePresentationService::try_from(service).ok()) .collect(); + assert_eq!(linked_verifiable_presentation_services.len(), 1); // Get the VPs included in the service. @@ -139,25 +128,6 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -async fn publish_document( - client: Client, - secret_manager: SecretManager, - document: IotaDocument, -) -> anyhow::Result { - // Resolve the latest output and update it with the given document. - let alias_output: AliasOutput = client.update_did_output(document.clone()).await?; - - // Because the size of the DID document increased, we have to increase the allocated storage deposit. - // This increases the deposit amount to the new minimum. - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - - // Publish the updated Alias Output. - Ok(client.publish_did_output(&secret_manager, alias_output).await?) -} - async fn make_vp_jwt(did_doc: &IotaDocument, storage: &MemStorage, fragment: &str) -> anyhow::Result { // first we create a credential encoding it as jwt let credential = CredentialBuilder::new(Object::default()) diff --git a/examples/1_advanced/1_did_issues_nft.rs b/examples/1_advanced/1_did_issues_nft.rs deleted file mode 100644 index 6509032b74..0000000000 --- a/examples/1_advanced/1_did_issues_nft.rs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use identity_iota::iota::block::output::feature::MetadataFeature; -use identity_iota::iota::IotaDID; -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::address::AliasAddress; -use iota_sdk::types::block::output::feature::IssuerFeature; -use iota_sdk::types::block::output::unlock_condition::AddressUnlockCondition; -use iota_sdk::types::block::output::AliasId; -use iota_sdk::types::block::output::Feature; -use iota_sdk::types::block::output::NftId; -use iota_sdk::types::block::output::NftOutput; -use iota_sdk::types::block::output::NftOutputBuilder; -use iota_sdk::types::block::output::Output; -use iota_sdk::types::block::output::OutputId; -use iota_sdk::types::block::output::RentStructure; -use iota_sdk::types::block::output::UnlockCondition; -use iota_sdk::types::block::payload::transaction::TransactionEssence; -use iota_sdk::types::block::payload::Payload; -use iota_sdk::types::block::Block; - -/// Demonstrates how an identity can issue and own NFTs, -/// and how observers can verify the issuer of the NFT. -/// -/// For this example, we consider the case where a manufacturer issues -/// a digital product passport (DPP) as an NFT. -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // ============================================== - // Create the manufacturer's DID and the DPP NFT. - // ============================================== - - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID for the manufacturer. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, manufacturer_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; - let manufacturer_did = manufacturer_document.id().clone(); - - // Get the current byte cost. - let rent_structure: RentStructure = client.get_rent_structure().await?; - - // Create a Digital Product Passport NFT issued by the manufacturer. - let product_passport_nft: NftOutput = - NftOutputBuilder::new_with_minimum_storage_deposit(rent_structure, NftId::null()) - // The NFT will initially be owned by the manufacturer. - .add_unlock_condition(UnlockCondition::Address(AddressUnlockCondition::new(Address::Alias( - AliasAddress::new(AliasId::from(&manufacturer_did)), - )))) - // Set the manufacturer as the immutable issuer. - .add_immutable_feature(Feature::Issuer(IssuerFeature::new(Address::Alias(AliasAddress::new( - AliasId::from(&manufacturer_did), - ))))) - // A proper DPP would hold its metadata here. - .add_immutable_feature(Feature::Metadata(MetadataFeature::new( - b"Digital Product Passport Metadata".to_vec(), - )?)) - .finish()?; - - // Publish the NFT. - let block: Block = client - .build_block() - .with_secret_manager(&secret_manager) - .with_outputs(vec![product_passport_nft.into()])? - .finish() - .await?; - let _ = client.retry_until_included(&block.id(), None, None).await?; - - // ======================================================== - // Resolve the Digital Product Passport NFT and its issuer. - // ======================================================== - - // Extract the identifier of the NFT from the published block. - let nft_id: NftId = NftId::from(&get_nft_output_id( - block - .payload() - .ok_or_else(|| anyhow::anyhow!("expected block to contain a payload"))?, - )?); - - // Fetch the NFT Output. - let nft_output_id: OutputId = client.nft_output_id(nft_id).await?; - let output: Output = client.get_output(&nft_output_id).await?.into_output(); - - // Extract the issuer of the NFT. - let nft_output: NftOutput = if let Output::Nft(nft_output) = output { - nft_output - } else { - anyhow::bail!("expected NFT output") - }; - - let issuer_address: Address = if let Some(Feature::Issuer(issuer)) = nft_output.immutable_features().iter().next() { - *issuer.address() - } else { - anyhow::bail!("expected an issuer feature") - }; - - let manufacturer_alias_id: AliasId = if let Address::Alias(alias_address) = issuer_address { - *alias_address.alias_id() - } else { - anyhow::bail!("expected an Alias Address") - }; - - // Reconstruct the manufacturer's DID from the Alias Id. - let network: NetworkName = client.network_name().await?; - let manufacturer_did: IotaDID = IotaDID::new(&manufacturer_alias_id, &network); - - // Resolve the issuer of the NFT. - let manufacturer_document: IotaDocument = client.resolve_did(&manufacturer_did).await?; - - println!("The issuer of the Digital Product Passport NFT is: {manufacturer_document:#}"); - - Ok(()) -} - -// Helper function to get the output id for the first NFT output in a Block. -fn get_nft_output_id(payload: &Payload) -> anyhow::Result { - match payload { - Payload::Transaction(tx_payload) => { - let TransactionEssence::Regular(regular) = tx_payload.essence(); - for (index, output) in regular.outputs().iter().enumerate() { - if let Output::Nft(_nft_output) = output { - return Ok(OutputId::new(tx_payload.id(), index.try_into().unwrap())?); - } - } - anyhow::bail!("no NFT output in transaction essence") - } - _ => anyhow::bail!("No transaction payload"), - } -} diff --git a/examples/1_advanced/2_nft_owns_did.rs b/examples/1_advanced/2_nft_owns_did.rs deleted file mode 100644 index 435964097c..0000000000 --- a/examples/1_advanced/2_nft_owns_did.rs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use examples::create_did_document; -use examples::get_address_with_funds; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use examples::FAUCET_ENDPOINT; -use identity_iota::iota::block::address::NftAddress; -use identity_iota::iota::block::output::AliasOutput; -use identity_iota::iota::IotaClientExt; -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::unlock_condition::AddressUnlockCondition; -use iota_sdk::types::block::output::NftId; -use iota_sdk::types::block::output::NftOutput; -use iota_sdk::types::block::output::NftOutputBuilder; -use iota_sdk::types::block::output::Output; -use iota_sdk::types::block::output::OutputId; -use iota_sdk::types::block::output::RentStructure; -use iota_sdk::types::block::output::UnlockCondition; -use iota_sdk::types::block::payload::transaction::TransactionEssence; -use iota_sdk::types::block::payload::Payload; -use iota_sdk::types::block::Block; - -/// Demonstrates how an identity can be owned by NFTs, -/// and how observers can verify that relationship. -/// -/// For this example, we consider the case where a car's NFT owns -/// the DID of the car, so that transferring the NFT also transfers DID ownership. -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // ============================= - // Create the car's NFT and DID. - // ============================= - - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Get an address with funds for testing. - let address: Address = get_address_with_funds(&client, &secret_manager, FAUCET_ENDPOINT).await?; - - // Get the current byte cost. - let rent_structure: RentStructure = client.get_rent_structure().await?; - - // Create the car NFT with an Ed25519 address as the unlock condition. - let car_nft: NftOutput = NftOutputBuilder::new_with_minimum_storage_deposit(rent_structure, NftId::null()) - .add_unlock_condition(UnlockCondition::Address(AddressUnlockCondition::new(address))) - .finish()?; - - // Publish the NFT output. - let block: Block = client - .build_block() - .with_secret_manager(&secret_manager) - .with_outputs(vec![car_nft.into()])? - .finish() - .await?; - let _ = client.retry_until_included(&block.id(), None, None).await?; - - let car_nft_id: NftId = NftId::from(&get_nft_output_id( - block - .payload() - .ok_or_else(|| anyhow::anyhow!("expected the block to contain a payload"))?, - )?); - - let network: NetworkName = client.network_name().await?; - - // Construct a DID document for the car. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (car_document, _): (IotaDocument, _) = create_did_document(&network, &storage).await?; - - // Create a new DID for the car that is owned by the car NFT. - let car_did_output: AliasOutput = client - .new_did_output(Address::Nft(car_nft_id.into()), car_document, Some(rent_structure)) - .await?; - - // Publish the car DID. - let car_document: IotaDocument = client.publish_did_output(&secret_manager, car_did_output).await?; - - // ============================================ - // Determine the car's NFT given the car's DID. - // ============================================ - - // Resolve the Alias Output of the DID. - let output: AliasOutput = client.resolve_did_output(car_document.id()).await?; - - // Extract the NFT address from the state controller unlock condition. - let unlock_condition: &UnlockCondition = output - .unlock_conditions() - .iter() - .next() - .ok_or_else(|| anyhow::anyhow!("expected at least one unlock condition"))?; - - let car_nft_address: NftAddress = - if let UnlockCondition::StateControllerAddress(state_controller_unlock_condition) = unlock_condition { - if let Address::Nft(nft_address) = state_controller_unlock_condition.address() { - *nft_address - } else { - anyhow::bail!("expected an NFT address as the unlock condition"); - } - } else { - anyhow::bail!("expected an Address as the unlock condition"); - }; - - // Retrieve the NFT Output of the car. - let car_nft_id: &NftId = car_nft_address.nft_id(); - let output_id: OutputId = client.nft_output_id(*car_nft_id).await?; - let output: Output = client.get_output(&output_id).await?.into_output(); - - let car_nft: NftOutput = if let Output::Nft(nft_output) = output { - nft_output - } else { - anyhow::bail!("expected an NFT output"); - }; - - println!("The car's DID is: {car_document:#}"); - println!("The car's NFT is: {car_nft:#?}"); - - Ok(()) -} - -// Helper function to get the output id for the first NFT output in a Block. -fn get_nft_output_id(payload: &Payload) -> anyhow::Result { - match payload { - Payload::Transaction(tx_payload) => { - let TransactionEssence::Regular(regular) = tx_payload.essence(); - for (index, output) in regular.outputs().iter().enumerate() { - if let Output::Nft(_nft_output) = output { - return Ok(OutputId::new(tx_payload.id(), index.try_into().unwrap())?); - } - } - anyhow::bail!("no NFT output in transaction essence") - } - _ => anyhow::bail!("No transaction payload"), - } -} diff --git a/examples/1_advanced/3_did_issues_tokens.rs b/examples/1_advanced/3_did_issues_tokens.rs deleted file mode 100644 index 3ebfeb5018..0000000000 --- a/examples/1_advanced/3_did_issues_tokens.rs +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::ops::Deref; - -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use identity_iota::core::Duration; -use identity_iota::core::Timestamp; -use identity_iota::iota::block::output::unlock_condition::AddressUnlockCondition; -use identity_iota::iota::block::output::unlock_condition::ExpirationUnlockCondition; -use identity_iota::iota::block::output::BasicOutput; -use identity_iota::iota::block::output::BasicOutputBuilder; -use identity_iota::iota::block::output::Output; -use identity_iota::iota::block::output::OutputId; -use identity_iota::iota::IotaDID; -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::api::GetAddressesOptions; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::address::AliasAddress; -use iota_sdk::types::block::address::ToBech32Ext; -use iota_sdk::types::block::output::unlock_condition::ImmutableAliasAddressUnlockCondition; -use iota_sdk::types::block::output::AliasId; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::FoundryId; -use iota_sdk::types::block::output::FoundryOutput; -use iota_sdk::types::block::output::FoundryOutputBuilder; -use iota_sdk::types::block::output::NativeToken; -use iota_sdk::types::block::output::RentStructure; -use iota_sdk::types::block::output::SimpleTokenScheme; -use iota_sdk::types::block::output::TokenId; -use iota_sdk::types::block::output::TokenScheme; -use iota_sdk::types::block::output::UnlockCondition; -use iota_sdk::types::block::Block; -use primitive_types::U256; - -/// Demonstrates how an identity can issue and control -/// a Token Foundry and its tokens. -/// -/// For this example, we consider the case where an authority issues -/// carbon credits that can be used to pay for carbon emissions or traded on a marketplace. -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // =========================================== - // Create the authority's DID and the foundry. - // =========================================== - - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID for the authority. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, authority_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; - let authority_did = authority_document.id().clone(); - - let rent_structure: RentStructure = client.get_rent_structure().await?; - - // We want to update the foundry counter of the authority's Alias Output, so we create an - // updated version of the output. We pass in the previous document, - // because we don't want to modify it in this update. - let authority_document: IotaDocument = client.resolve_did(&authority_did).await?; - let authority_alias_output: AliasOutput = client.update_did_output(authority_document).await?; - - // We will add one foundry to this Alias Output. - let authority_alias_output = AliasOutputBuilder::from(&authority_alias_output) - .with_foundry_counter(1) - .finish()?; - - // Create a token foundry that represents carbon credits. - let token_scheme = TokenScheme::Simple(SimpleTokenScheme::new( - U256::from(500_000u32), - U256::from(0u8), - U256::from(1_000_000u32), - )?); - - // Create the identifier of the foundry, which is partially derived from the Alias Address. - let foundry_id = FoundryId::build( - &AliasAddress::new(AliasId::from(&authority_did)), - 1, - token_scheme.kind(), - ); - - // Create the Foundry Output. - let carbon_credits_foundry: FoundryOutput = - FoundryOutputBuilder::new_with_minimum_storage_deposit(rent_structure, 1, token_scheme) - // Initially, all carbon credits are owned by the foundry. - .add_native_token(NativeToken::new(TokenId::from(foundry_id), U256::from(500_000u32))?) - // The authority is set as the immutable owner. - .add_unlock_condition(UnlockCondition::ImmutableAliasAddress( - ImmutableAliasAddressUnlockCondition::new(AliasAddress::new(AliasId::from(&authority_did))), - )) - .finish()?; - - let carbon_credits_foundry_id: FoundryId = carbon_credits_foundry.id(); - - // Publish all outputs. - let block: Block = client - .build_block() - .with_secret_manager(&secret_manager) - .with_outputs(vec![authority_alias_output.into(), carbon_credits_foundry.into()])? - .finish() - .await?; - let _ = client.retry_until_included(&block.id(), None, None).await?; - - // =================================== - // Resolve Foundry and its issuer DID. - // =================================== - - // Get the latest output that contains the foundry. - let foundry_output_id: OutputId = client.foundry_output_id(carbon_credits_foundry_id).await?; - let carbon_credits_foundry: Output = client.get_output(&foundry_output_id).await?.into_output(); - - let carbon_credits_foundry: FoundryOutput = if let Output::Foundry(foundry_output) = carbon_credits_foundry { - foundry_output - } else { - anyhow::bail!("expected foundry output") - }; - - // Get the Alias Id of the authority that issued the carbon credits foundry. - let authority_alias_id: &AliasId = carbon_credits_foundry.alias_address().alias_id(); - - // Reconstruct the DID of the authority. - let network: NetworkName = client.network_name().await?; - let authority_did: IotaDID = IotaDID::new(authority_alias_id.deref(), &network); - - // Resolve the authority's DID document. - let authority_document: IotaDocument = client.resolve_did(&authority_did).await?; - - println!("The authority's DID is: {authority_document:#}"); - - // ========================================================= - // Transfer 1000 carbon credits to the address of a company. - // ========================================================= - - // Create a new address that represents the company. - let company_address: Address = *secret_manager - .generate_ed25519_addresses( - GetAddressesOptions::default() - .with_bech32_hrp((&network).try_into()?) - .with_range(1..2), - ) - .await?[0]; - - // Create the timestamp at which the basic output will expire. - let tomorrow: u32 = Timestamp::now_utc() - .checked_add(Duration::seconds(60 * 60 * 24)) - .ok_or_else(|| anyhow::anyhow!("timestamp overflow"))? - .to_unix() - .try_into() - .map_err(|err| anyhow::anyhow!("cannot fit timestamp into u32: {err}"))?; - - // Create a basic output containing our carbon credits that we'll send to the company's address. - let basic_output: BasicOutput = BasicOutputBuilder::new_with_minimum_storage_deposit(rent_structure) - .add_unlock_condition(UnlockCondition::Address(AddressUnlockCondition::new(company_address))) - .add_native_token(NativeToken::new(carbon_credits_foundry.token_id(), U256::from(1000))?) - // Allow the company to claim the credits within 24 hours by using an expiration unlock condition. - .add_unlock_condition(UnlockCondition::Expiration(ExpirationUnlockCondition::new( - Address::Alias(AliasAddress::new(*authority_alias_id)), - tomorrow, - )?)) - .finish()?; - - // Reduce the carbon credits in the foundry by the amount that is sent to the company. - let carbon_credits_foundry = FoundryOutputBuilder::from(&carbon_credits_foundry) - .with_native_tokens(vec![NativeToken::new( - carbon_credits_foundry.token_id(), - U256::from(499_000u32), - )?]) - .finish()?; - - // Publish the output, transferring the carbon credits. - let block: Block = client - .build_block() - .with_secret_manager(&secret_manager) - .with_outputs(vec![basic_output.into(), carbon_credits_foundry.into()])? - .finish() - .await?; - let _ = client.retry_until_included(&block.id(), None, None).await?; - - println!( - "Sent carbon credits to {}", - company_address.to_bech32((&network).try_into()?) - ); - - Ok(()) -} diff --git a/examples/1_advanced/4_alias_output_history.rs b/examples/1_advanced/4_alias_output_history.rs deleted file mode 100644 index b46b05dd6c..0000000000 --- a/examples/1_advanced/4_alias_output_history.rs +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use anyhow::Context; -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use identity_iota::core::json; -use identity_iota::core::FromJson; -use identity_iota::core::Timestamp; -use identity_iota::did::DID; -use identity_iota::document::Service; -use identity_iota::iota::block::address::Address; -use identity_iota::iota::block::output::RentStructure; -use identity_iota::iota::IotaClientExt; -use identity_iota::iota::IotaDID; -use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClient; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use identity_iota::verification::MethodRelationship; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::input::Input; -use iota_sdk::types::block::output::AliasId; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::Output; -use iota_sdk::types::block::output::OutputId; -use iota_sdk::types::block::output::OutputMetadata; -use iota_sdk::types::block::payload::transaction::TransactionEssence; -use iota_sdk::types::block::payload::Payload; -use iota_sdk::types::block::Block; - -/// Demonstrates how to obtain the alias output history. -#[tokio::main] -async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - // NOTE: a permanode is required to fetch older output histories. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID in an Alias Output for us to modify. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, document, fragment): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; - let did: IotaDID = document.id().clone(); - - // Resolve the latest state of the document. - let mut document: IotaDocument = client.resolve_did(&did).await?; - - // Attach a new method relationship to the existing method. - document.attach_method_relationship( - &document.id().to_url().join(format!("#{fragment}"))?, - MethodRelationship::Authentication, - )?; - - // Adding multiple services. - let services = [ - json!({"id": document.id().to_url().join("#my-service-0")?, "type": "MyService", "serviceEndpoint": "https://iota.org/"}), - ]; - for service in services { - let service: Service = Service::from_json_value(service)?; - assert!(document.insert_service(service).is_ok()); - document.metadata.updated = Some(Timestamp::now_utc()); - - // Increase the storage deposit and publish the update. - let alias_output: AliasOutput = client.update_did_output(document.clone()).await?; - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - client.publish_did_output(&secret_manager, alias_output).await?; - } - - // ==================================== - // Retrieving the Alias Output History - // ==================================== - let mut alias_history: Vec = Vec::new(); - - // Step 0 - Get the latest Alias Output - let alias_id: AliasId = AliasId::from(client.resolve_did(&did).await?.id()); - let (mut output_id, mut alias_output): (OutputId, AliasOutput) = client.get_alias_output(alias_id).await?; - - while alias_output.state_index() != 0 { - // Step 1 - Get the current block - let block: Block = current_block(&client, &output_id).await?; - // Step 2 - Get the OutputId of the previous block - output_id = previous_output_id(&block)?; - // Step 3 - Get the Alias Output from the block - alias_output = block_alias_output(&block, &alias_id)?; - alias_history.push(alias_output.clone()); - } - - println!("Alias History: {alias_history:?}"); - - Ok(()) -} - -async fn current_block(client: &Client, output_id: &OutputId) -> anyhow::Result { - let output_metadata: OutputMetadata = client.get_output_metadata(output_id).await?; - let block: Block = client.get_block(output_metadata.block_id()).await?; - Ok(block) -} - -fn previous_output_id(block: &Block) -> anyhow::Result { - match block - .payload() - .context("expected a transaction payload, but no payload was found")? - { - Payload::Transaction(transaction_payload) => match transaction_payload.essence() { - TransactionEssence::Regular(regular_transaction_essence) => { - match regular_transaction_essence - .inputs() - .first() - .context("expected an utxo for the block, but no input was found")? - { - Input::Utxo(utxo_input) => Ok(*utxo_input.output_id()), - Input::Treasury(_) => { - anyhow::bail!("expected an utxo input, found a treasury input"); - } - } - } - }, - Payload::Milestone(_) | Payload::TreasuryTransaction(_) | Payload::TaggedData(_) => { - anyhow::bail!("expected a transaction payload"); - } - } -} - -fn block_alias_output(block: &Block, alias_id: &AliasId) -> anyhow::Result { - match block - .payload() - .context("expected a transaction payload, but no payload was found")? - { - Payload::Transaction(transaction_payload) => match transaction_payload.essence() { - TransactionEssence::Regular(regular_transaction_essence) => { - for (index, output) in regular_transaction_essence.outputs().iter().enumerate() { - match output { - Output::Alias(alias_output) => { - if &alias_output.alias_id().or_from_output_id( - &OutputId::new( - transaction_payload.id(), - index.try_into().context("output index must fit into a u16")?, - ) - .context("failed to create OutputId")?, - ) == alias_id - { - return Ok(alias_output.clone()); - } - } - Output::Basic(_) | Output::Foundry(_) | Output::Nft(_) | Output::Treasury(_) => continue, - } - } - } - }, - Payload::Milestone(_) | Payload::TreasuryTransaction(_) | Payload::TaggedData(_) => { - anyhow::bail!("expected a transaction payload"); - } - } - anyhow::bail!("no alias output has been found"); -} diff --git a/examples/1_advanced/4_identity_history.rs b/examples/1_advanced/4_identity_history.rs new file mode 100644 index 0000000000..87f5efd690 --- /dev/null +++ b/examples/1_advanced/4_identity_history.rs @@ -0,0 +1,113 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use examples::create_did_document; +use examples::get_funded_client; +use examples::get_memstorage; +use examples::TEST_GAS_BUDGET; +use identity_iota::core::json; +use identity_iota::core::FromJson; +use identity_iota::core::Timestamp; +use identity_iota::did::DID; +use identity_iota::document::Service; +use identity_iota::iota::rebased::client::get_object_id_from_did; +use identity_iota::iota::rebased::migration::has_previous_version; +use identity_iota::iota::rebased::migration::Identity; +use identity_iota::iota::IotaDID; +use identity_iota::iota::IotaDocument; +use identity_iota::verification::MethodRelationship; +use iota_sdk::rpc_types::IotaObjectData; + +/// Demonstrates how to obtain the identity history. +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Create a new client to interact with the IOTA ledger. + // NOTE: a permanode is required to fetch older output histories. + let storage = get_memstorage()?; + let identity_client = get_funded_client(&storage).await?; + // create new DID document and publish it + let (document, vm_fragment_1) = create_did_document(&identity_client, &storage).await?; + let did: IotaDID = document.id().clone(); + + // Resolve the latest state of the document. + let mut document: IotaDocument = identity_client.resolve_did(&did).await?; + + // Attach a new method relationship to the existing method. + document.attach_method_relationship( + &document.id().to_url().join(format!("#{vm_fragment_1}"))?, + MethodRelationship::Authentication, + )?; + + // Adding multiple services. + let services = [ + json!({"id": document.id().to_url().join("#my-service-0")?, "type": "MyService", "serviceEndpoint": "https://iota.org/"}), + ]; + for service in services { + let service: Service = Service::from_json_value(service)?; + assert!(document.insert_service(service).is_ok()); + document.metadata.updated = Some(Timestamp::now_utc()); + + identity_client + .publish_did_document_update(document.clone(), TEST_GAS_BUDGET) + .await?; + } + + // ==================================== + // Retrieving the identity History + // ==================================== + + // Step 1 - Get the latest identity + let identity = identity_client.get_identity(get_object_id_from_did(&did)?).await?; + let onchain_identity = if let Identity::FullFledged(value) = identity { + value + } else { + anyhow::bail!("history only available for onchain identities"); + }; + + // Step 2 - Get history + let history = onchain_identity.get_history(&identity_client, None, None).await?; + println!("Identity History has {} entries", history.len()); + + // Optional step - Parse to documents + let documents: Vec = history + .into_iter() + .map(|data| IotaDocument::unpack_from_iota_object_data(&did, &data, true)) + .collect::>()?; + println!("Current version: {}", documents[0]); + println!("Previous version: {}", documents[1]); + + // Depending on your use case, you can also page through the results + // Alternative Step 2 - Page by looping until no result is returned (here with page size 1) + let mut current_item: Option<&IotaObjectData> = None; + let mut history: Vec; + loop { + history = onchain_identity + .get_history(&identity_client, current_item, Some(1)) + .await?; + if history.is_empty() { + break; + } + current_item = history.first(); + let IotaObjectData { object_id, version, .. } = current_item.unwrap(); + println!("Identity History entry: object_id: {object_id}, version: {version}"); + } + + // Alternative Step 2 - Page by looping with pre-fetch next page check (again with page size 1) + let mut current_item: Option<&IotaObjectData> = None; + let mut history: Vec; + loop { + history = onchain_identity + .get_history(&identity_client, current_item, Some(1)) + .await?; + + current_item = history.first(); + let IotaObjectData { object_id, version, .. } = current_item.unwrap(); + println!("Identity History entry: object_id: {object_id}, version: {version}"); + + if !has_previous_version(current_item.unwrap())? { + break; + } + } + + Ok(()) +} diff --git a/examples/1_advanced/5_custom_resolution.rs b/examples/1_advanced/5_custom_resolution.rs index b0675c8dd5..c466744bd8 100644 --- a/examples/1_advanced/5_custom_resolution.rs +++ b/examples/1_advanced/5_custom_resolution.rs @@ -1,25 +1,16 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; +use examples::create_did_document; +use examples::get_funded_client; +use examples::get_memstorage; use identity_iota::core::FromJson; use identity_iota::core::ToJson; use identity_iota::did::CoreDID; use identity_iota::did::DID; use identity_iota::document::CoreDocument; -use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; use identity_iota::resolver::Resolver; -use identity_iota::storage::JwkMemStore; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; /// Demonstrates how to set up a resolver using custom handlers. /// @@ -27,41 +18,28 @@ use iota_sdk::types::block::address::Address; /// Resolver in this example and just worked with `CoreDocument` representations throughout. #[tokio::main] async fn main() -> anyhow::Result<()> { + // create new client to interact with chain and get funded account with keys + let storage = get_memstorage()?; + let identity_client = get_funded_client(&storage).await?; + // create new DID document and publish it + let (document, _) = create_did_document(&identity_client, &storage).await?; + // Create a resolver returning an enum of the documents we are interested in and attach handlers for the "foo" and // "iota" methods. let mut resolver: Resolver = Resolver::new(); - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - // This is a convenience method for attaching a handler for the "iota" method by providing just a client. - resolver.attach_iota_handler(client.clone()); + resolver.attach_iota_handler((*identity_client).clone()); resolver.attach_handler("foo".to_owned(), resolve_did_foo); // A fake did:foo DID for demonstration purposes. let did_foo: CoreDID = "did:foo:0e9c8294eeafee326a4e96d65dbeaca0".parse()?; - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create a new DID for us to resolve. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, iota_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; - let iota_did: IotaDID = iota_document.id().clone(); - // Resolve did_foo to get an abstract document. let did_foo_doc: Document = resolver.resolve(&did_foo).await?; // Resolve iota_did to get an abstract document. - let iota_doc: Document = resolver.resolve(&iota_did).await?; + let iota_doc: Document = resolver.resolve(&document.id().clone()).await?; // The Resolver is mainly meant for validating presentations, but here we will just // check that the resolved documents match our expectations. diff --git a/examples/1_advanced/6_domain_linkage.rs b/examples/1_advanced/6_domain_linkage.rs index 03d0472a37..ca7e69ccd7 100644 --- a/examples/1_advanced/6_domain_linkage.rs +++ b/examples/1_advanced/6_domain_linkage.rs @@ -1,10 +1,10 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; +use examples::create_did_document; +use examples::get_funded_client; +use examples::get_memstorage; +use examples::TEST_GAS_BUDGET; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::Duration; use identity_iota::core::FromJson; @@ -24,46 +24,20 @@ use identity_iota::credential::LinkedDomainService; use identity_iota::did::CoreDID; use identity_iota::did::DIDUrl; use identity_iota::did::DID; -use identity_iota::iota::IotaClientExt; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; use identity_iota::resolver::Resolver; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::AliasOutputBuilder; -use iota_sdk::types::block::output::RentStructure; #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - let stronghold_path = random_stronghold_path(); - - println!("Using stronghold path: {stronghold_path:?}"); - // Create a new secret manager backed by a Stronghold. - let mut secret_manager: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password".to_owned())) - .build(stronghold_path)?, - ); - + // Create new client to interact with chain and get funded account with keys. + let storage = get_memstorage()?; + let identity_client = get_funded_client(&storage).await?; // Create a DID for the entity that will issue the Domain Linkage Credential. - let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, mut did_document, fragment): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager, &storage).await?; - let did: IotaDID = did_document.id().clone(); + let (mut document, vm_fragment_1) = create_did_document(&identity_client, &storage).await?; + let did: IotaDID = document.id().clone(); // ===================================================== // Create Linked Domain service @@ -81,8 +55,10 @@ async fn main() -> anyhow::Result<()> { // This is optional since it is not a hard requirement by the specs. let service_url: DIDUrl = did.clone().join("#domain-linkage")?; let linked_domain_service: LinkedDomainService = LinkedDomainService::new(service_url, domains, Object::new())?; - did_document.insert_service(linked_domain_service.into())?; - let updated_did_document: IotaDocument = publish_document(client.clone(), secret_manager, did_document).await?; + document.insert_service(linked_domain_service.into())?; + let updated_did_document: IotaDocument = identity_client + .publish_did_document_update(document.clone(), TEST_GAS_BUDGET) + .await?; println!("DID document with linked domain service: {updated_did_document:#}"); @@ -112,7 +88,7 @@ async fn main() -> anyhow::Result<()> { .create_credential_jwt( &domain_linkage_credential, &storage, - &fragment, + &vm_fragment_1, &JwsSignatureOptions::default(), None, ) @@ -138,7 +114,7 @@ async fn main() -> anyhow::Result<()> { // Init a resolver for resolving DID Documents. let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client.clone()); + resolver.attach_iota_handler((*identity_client).clone()); // ===================================================== // → Case 1: starting from domain @@ -212,22 +188,3 @@ async fn main() -> anyhow::Result<()> { assert!(validation_result.is_ok()); Ok(()) } - -async fn publish_document( - client: Client, - secret_manager: SecretManager, - document: IotaDocument, -) -> anyhow::Result { - // Resolve the latest output and update it with the given document. - let alias_output: AliasOutput = client.update_did_output(document.clone()).await?; - - // Because the size of the DID document increased, we have to increase the allocated storage deposit. - // This increases the deposit amount to the new minimum. - let rent_structure: RentStructure = client.get_rent_structure().await?; - let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) - .with_minimum_storage_deposit(rent_structure) - .finish()?; - - // Publish the updated Alias Output. - Ok(client.publish_did_output(&secret_manager, alias_output).await?) -} diff --git a/examples/1_advanced/7_sd_jwt.rs b/examples/1_advanced/7_sd_jwt.rs index 2d2a4665ee..0f3199d727 100644 --- a/examples/1_advanced/7_sd_jwt.rs +++ b/examples/1_advanced/7_sd_jwt.rs @@ -6,11 +6,10 @@ //! //! cargo run --release --example 7_sd_jwt -use examples::create_did; +use examples::create_did_document; +use examples::get_funded_client; +use examples::get_memstorage; use examples::pretty_print_json; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::json; use identity_iota::core::FromJson; @@ -27,16 +26,8 @@ use identity_iota::credential::KeyBindingJWTValidationOptions; use identity_iota::credential::SdJwtCredentialValidator; use identity_iota::credential::Subject; use identity_iota::did::DID; -use identity_iota::iota::IotaDocument; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; use sd_jwt_payload::KeyBindingJwtClaims; use sd_jwt_payload::SdJwt; use sd_jwt_payload::SdObjectDecoder; @@ -49,31 +40,15 @@ async fn main() -> anyhow::Result<()> { // Step 1: Create identities for the issuer and the holder. // =========================================================================== - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - // Create an identity for the issuer with one verification method `key-1`. - let mut secret_manager_issuer: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); - let issuer_storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, issuer_document, fragment): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_issuer, &issuer_storage).await?; + let issuer_storage = get_memstorage()?; + let issuer_identity_client = get_funded_client(&issuer_storage).await?; + let (issuer_document, issuer_vm_fragment) = create_did_document(&issuer_identity_client, &issuer_storage).await?; // Create an identity for the holder, in this case also the subject. - let mut secret_manager_alice: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_2".to_owned())) - .build(random_stronghold_path())?, - ); - let alice_storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, alice_document, alice_fragment): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_alice, &alice_storage).await?; + let holder_storage = get_memstorage()?; + let holder_identity_client = get_funded_client(&holder_storage).await?; + let (holder_document, holder_vm_fragment) = create_did_document(&holder_identity_client, &holder_storage).await?; // =========================================================================== // Step 2: Issuer creates and signs a selectively disclosable JWT verifiable credential. @@ -81,7 +56,7 @@ async fn main() -> anyhow::Result<()> { // Create an address credential subject. let subject: Subject = Subject::from_json_value(json!({ - "id": alice_document.id().as_str(), + "id": holder_document.id().as_str(), "name": "Alice", "address": { "locality": "Maxstadt", @@ -128,7 +103,7 @@ async fn main() -> anyhow::Result<()> { let jwt: Jws = issuer_document .create_jws( &issuer_storage, - &fragment, + &issuer_vm_fragment, encoded_payload.as_bytes(), &JwsSignatureOptions::default(), ) @@ -183,8 +158,13 @@ async fn main() -> anyhow::Result<()> { let options = JwsSignatureOptions::new().typ(KeyBindingJwtClaims::KB_JWT_HEADER_TYP); // Create the KB-JWT. - let kb_jwt: Jws = alice_document - .create_jws(&alice_storage, &alice_fragment, binding_claims.as_bytes(), &options) + let kb_jwt: Jws = holder_document + .create_jws( + &holder_storage, + &holder_vm_fragment, + binding_claims.as_bytes(), + &options, + ) .await?; // Create the final SD-JWT. @@ -219,7 +199,7 @@ async fn main() -> anyhow::Result<()> { // Verify the Key Binding JWT. let options = KeyBindingJWTValidationOptions::new().nonce(nonce).aud(VERIFIER_DID); - let _kb_validation = validator.validate_key_binding_jwt(&sd_jwt_obj, &alice_document, &options)?; + let _kb_validation = validator.validate_key_binding_jwt(&sd_jwt_obj, &holder_document, &options)?; println!("Key Binding JWT successfully validated"); diff --git a/examples/1_advanced/8_status_list_2021.rs b/examples/1_advanced/8_status_list_2021.rs index 0a70690e91..1256f23e31 100644 --- a/examples/1_advanced/8_status_list_2021.rs +++ b/examples/1_advanced/8_status_list_2021.rs @@ -1,10 +1,9 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use examples::create_did; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; +use examples::create_did_document; +use examples::get_funded_client; +use examples::get_memstorage; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::FromJson; use identity_iota::core::Object; @@ -28,16 +27,9 @@ use identity_iota::credential::Status; use identity_iota::credential::StatusCheck; use identity_iota::credential::Subject; use identity_iota::did::DID; -use identity_iota::iota::IotaDocument; use identity_iota::storage::JwkDocumentExt; -use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; -use identity_iota::storage::KeyIdMemstore; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; + use serde_json::json; #[tokio::main] @@ -46,32 +38,15 @@ async fn main() -> anyhow::Result<()> { // Create a Verifiable Credential. // =========================================================================== - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - let mut secret_manager_issuer: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); - - // Create an identity for the issuer with one verification method `key-1`. - let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, issuer_document, fragment_issuer): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_issuer, &storage_issuer).await?; + // create new issuer account with did document + let issuer_storage = get_memstorage()?; + let issuer_identity_client = get_funded_client(&issuer_storage).await?; + let (issuer_document, issuer_vm_fragment) = create_did_document(&issuer_identity_client, &issuer_storage).await?; - // Create an identity for the holder, in this case also the subject. - let mut secret_manager_alice: SecretManager = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_2".to_owned())) - .build(random_stronghold_path())?, - ); - let storage_alice: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let (_, alice_document, _): (Address, IotaDocument, String) = - create_did(&client, &mut secret_manager_alice, &storage_alice).await?; + // create new holder account with did document + let holder_storage = get_memstorage()?; + let holder_identity_client = get_funded_client(&holder_storage).await?; + let (holder_document, _) = create_did_document(&holder_identity_client, &holder_storage).await?; // Create a new empty status list. No credentials have been revoked yet. let status_list: StatusList2021 = StatusList2021::default(); @@ -89,7 +64,7 @@ async fn main() -> anyhow::Result<()> { // Create a credential subject indicating the degree earned by Alice. let subject: Subject = Subject::from_json_value(json!({ - "id": alice_document.id().as_str(), + "id": holder_document.id().as_str(), "name": "Alice", "degree": { "type": "BachelorDegree", @@ -123,8 +98,8 @@ async fn main() -> anyhow::Result<()> { let credential_jwt: Jwt = issuer_document .create_credential_jwt( &credential, - &storage_issuer, - &fragment_issuer, + &issuer_storage, + &issuer_vm_fragment, &JwsSignatureOptions::default(), None, ) @@ -133,7 +108,7 @@ async fn main() -> anyhow::Result<()> { let validator: JwtCredentialValidator = JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); - // The validator has no way of retriving the status list to check for the + // The validator has no way of retrieving the status list to check for the // revocation of the credential. Let's skip that pass and perform the operation manually. let mut validation_options = JwtCredentialValidationOptions::default(); validation_options.status = StatusCheck::SkipUnsupported; diff --git a/examples/1_advanced/9_zkp.rs b/examples/1_advanced/9_zkp.rs index eeb4246280..9d03b44f37 100644 --- a/examples/1_advanced/9_zkp.rs +++ b/examples/1_advanced/9_zkp.rs @@ -1,11 +1,10 @@ // Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 -use examples::get_address_with_funds; -use examples::random_stronghold_path; -use examples::MemStorage; -use examples::API_ENDPOINT; -use examples::FAUCET_ENDPOINT; +use examples::get_funded_client; + +use examples::get_memstorage; +use examples::TEST_GAS_BUDGET; use identity_iota::core::json; use identity_iota::core::FromJson; use identity_iota::core::Object; @@ -26,81 +25,63 @@ use identity_iota::credential::SelectiveDisclosurePresentation; use identity_iota::credential::Subject; use identity_iota::did::CoreDID; use identity_iota::did::DID; -use identity_iota::iota::IotaClientExt; +use identity_iota::iota_interaction::OptionalSync; + +use identity_iota::iota::rebased::transaction::TransactionOutput; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; use identity_iota::resolver::Resolver; use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwpDocumentExt; -use identity_iota::storage::KeyIdMemstore; use identity_iota::storage::KeyType; use identity_iota::verification::MethodScope; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::client::Password; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::output::AliasOutput; + +use identity_iota::iota::rebased::client::IdentityClient; +use identity_iota::iota::rebased::client::IotaKeySignature; +use identity_iota::iota::rebased::transaction::Transaction; +use identity_storage::Storage; use jsonprooftoken::jpa::algs::ProofAlgorithm; +use secret_storage::Signer; // Creates a DID with a JWP verification method. -async fn create_did( - client: &Client, - secret_manager: &SecretManager, - storage: &MemStorage, +pub async fn create_did( + identity_client: &IdentityClient, + storage: &Storage, key_type: KeyType, alg: ProofAlgorithm, -) -> anyhow::Result<(Address, IotaDocument, String)> { - // Get an address with funds for testing. - let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT).await?; - - // Get the Bech32 human-readable part (HRP) of the network. - let network_name: NetworkName = client.network_name().await?; - +) -> anyhow::Result<(IotaDocument, String)> +where + K: identity_storage::JwkStorage + identity_storage::JwkStorageBbsPlusExt, + I: identity_storage::KeyIdStorage, + S: Signer + OptionalSync, +{ // Create a new DID document with a placeholder DID. - // The DID will be derived from the Alias Id of the Alias Output after publishing. - let mut document: IotaDocument = IotaDocument::new(&network_name); + let mut unpublished: IotaDocument = IotaDocument::new(identity_client.network()); - let fragment = document + let verification_method_fragment = unpublished .generate_method_jwp(storage, key_type, alg, None, MethodScope::VerificationMethod) .await?; - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; - - // Publish the Alias Output and get the published DID document. - let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?; - println!("Published DID document: {document:#}"); + let TransactionOutput:: { output: document, .. } = identity_client + .publish_did_document(unpublished) + .execute_with_gas(TEST_GAS_BUDGET, identity_client) + .await?; - Ok((address, document, fragment)) + Ok((document, verification_method_fragment)) } /// Demonstrates how to create an Anonymous Credential with BBS+. #[tokio::main] async fn main() -> anyhow::Result<()> { // =========================================================================== - // Step 1: Create identity for the issuer. + // Step 1: Create identities and Client // =========================================================================== - // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await?; - - let secret_manager_issuer = SecretManager::Stronghold( - StrongholdSecretManager::builder() - .password(Password::from("secure_password_1".to_owned())) - .build(random_stronghold_path())?, - ); + let storage_issuer = get_memstorage()?; - let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); + let identity_client = get_funded_client(&storage_issuer).await?; - let (_, issuer_document, fragment_issuer): (Address, IotaDocument, String) = create_did( - &client, - &secret_manager_issuer, + let (issuer_document, fragment_issuer): (IotaDocument, String) = create_did( + &identity_client, &storage_issuer, JwkMemStore::BLS12381G2_KEY_TYPE, ProofAlgorithm::BLS12381_SHA256, @@ -165,7 +146,7 @@ async fn main() -> anyhow::Result<()> { // ============================================================================================ let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client); + resolver.attach_iota_handler((*identity_client).clone()); // Holder resolves issuer's DID let issuer: CoreDID = JptCredentialValidatorUtils::extract_issuer_from_issued_jpt(&credential_jpt).unwrap(); diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 9d675b4ce7..9027819b7a 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,17 +7,32 @@ publish = false [dependencies] anyhow = "1.0.62" -bls12_381_plus.workspace = true -identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false } -identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021", "jpt-bbs-plus", "resolver"] } -identity_stronghold = { path = "../identity_stronghold", default-features = false, features = ["bbs-plus"] } -iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } +identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } +identity_storage = { path = "../identity_storage" } +identity_stronghold = { path = "../identity_stronghold", default-features = false, features = ["send-sync-storage"] } +iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v0.9.2-rc" } +iota-sdk-legacy = { package = "iota-sdk", version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } json-proof-token.workspace = true -primitive-types = "0.12.1" rand = "0.8.5" sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"] } +secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", tag = "v0.2.0" } serde_json = { version = "1.0", default-features = false } -tokio = { version = "1.29", default-features = false, features = ["rt"] } +tokio = { version = "1.29", default-features = false, features = ["rt", "macros"] } + +[dependencies.identity_iota] +path = "../identity_iota" +default-features = false +features = [ + "domain-linkage", + "jpt-bbs-plus", + "iota-client", + "send-sync-storage", + "memstore", + "resolver", + "revocation-bitmap", + "sd-jwt", + "status-list-2021", +] [lib] path = "utils/utils.rs" @@ -38,10 +53,6 @@ name = "2_resolve_did" path = "0_basic/3_deactivate_did.rs" name = "3_deactivate_did" -[[example]] -path = "0_basic/4_delete_did.rs" -name = "4_delete_did" - [[example]] path = "0_basic/5_create_vc.rs" name = "5_create_vc" @@ -55,28 +66,12 @@ path = "0_basic/7_revoke_vc.rs" name = "7_revoke_vc" [[example]] -path = "0_basic/8_stronghold.rs" -name = "8_stronghold" - -[[example]] -path = "1_advanced/0_did_controls_did.rs" -name = "0_did_controls_did" - -[[example]] -path = "1_advanced/1_did_issues_nft.rs" -name = "1_did_issues_nft" - -[[example]] -path = "1_advanced/2_nft_owns_did.rs" -name = "2_nft_owns_did" - -[[example]] -path = "1_advanced/3_did_issues_tokens.rs" -name = "3_did_issues_tokens" +path = "0_basic/8_legacy_stronghold.rs" +name = "8_legacy_stronghold" [[example]] -path = "1_advanced/4_alias_output_history.rs" -name = "4_alias_output_history" +path = "1_advanced/4_identity_history.rs" +name = "4_identity_history" [[example]] path = "1_advanced/5_custom_resolution.rs" diff --git a/examples/README.md b/examples/README.md index 8ea9ab2145..162782b8ef 100644 --- a/examples/README.md +++ b/examples/README.md @@ -22,16 +22,16 @@ cargo run --release --example 0_create_did The following basic CRUD (Create, Read, Update, Delete) examples are available: -| Name | Information | -| :------------------------------------------------ | :----------------------------------------------------------------------------------- | -| [0_create_did](./0_basic/0_create_did.rs) | Demonstrates how to create a DID Document and publish it in a new Alias Output. | -| [1_update_did](./0_basic/1_update_did.rs) | Demonstrates how to update a DID document in an existing Alias Output. | -| [2_resolve_did](./0_basic/2_resolve_did.rs) | Demonstrates how to resolve an existing DID in an Alias Output. | -| [3_deactivate_did](./0_basic/3_deactivate_did.rs) | Demonstrates how to deactivate a DID in an Alias Output. | -| [4_delete_did](./0_basic/4_delete_did.rs) | Demonstrates how to delete a DID in an Alias Output, reclaiming the storage deposit. | -| [5_create_vc](./0_basic/5_create_vc.rs) | Demonstrates how to create and verify verifiable credentials. | -| [6_create_vp](./0_basic/6_create_vp.rs) | Demonstrates how to create and verify verifiable presentations. | -| [7_revoke_vc](./0_basic/7_revoke_vc.rs) | Demonstrates how to revoke a verifiable credential. | +| Name | Information | +| :------------------------------------------------------ | :----------------------------------------------------------------------------------- | +| [0_create_did](./0_basic/0_create_did.rs) | Demonstrates how to create a DID Document and publish it in a new identity. | +| [1_update_did](./0_basic/1_update_did.rs) | Demonstrates how to update a DID document in an existing identity. | +| [2_resolve_did](./0_basic/2_resolve_did.rs) | Demonstrates how to resolve an existing DID in an identity. | +| [3_deactivate_did](./0_basic/3_deactivate_did.rs) | Demonstrates how to deactivate a DID in an identity. | +| [5_create_vc](./0_basic/5_create_vc.rs) | Demonstrates how to create and verify verifiable credentials. | +| [6_create_vp](./0_basic/6_create_vp.rs) | Demonstrates how to create and verify verifiable presentations. | +| [7_revoke_vc](./0_basic/7_revoke_vc.rs) | Demonstrates how to revoke a verifiable credential. | +| [8_legacy_stronghold](./0_basic/8_legacy_stronghold.rs) | Demonstrates how to use stronghold for secure storage.. | ## Advanced Examples @@ -39,13 +39,11 @@ The following advanced examples are available: | Name | Information | | :------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- | -| [0_did_controls_did](./1_advanced/0_did_controls_did.rs) | Demonstrates how an identity can control another identity. | -| [1_did_issues_nft](./1_advanced/1_did_issues_nft.rs) | Demonstrates how an identity can issue and own NFTs, and how observers can verify the issuer of the NFT. | -| [2_nft_owns_did](./1_advanced/2_nft_owns_did.rs) | Demonstrates how an identity can be owned by NFTs, and how observers can verify that relationship. | -| [3_did_issues_tokens](./1_advanced/3_did_issues_tokens.rs) | Demonstrates how an identity can issue and control a Token Foundry and its tokens. | -| [4_alias_output_history](./1_advanced/4_alias_output_history.rs) | Demonstrates fetching the history of an Alias Output. | +| [4_identity_history](./1_advanced/4_identity_history.rs) | Demonstrates fetching the history of an identity. | | [5_custom_resolution](./1_advanced/5_custom_resolution.rs) | Demonstrates how to set up a resolver using custom handlers. | | [6_domain_linkage](./1_advanced/6_domain_linkage) | Demonstrates how to link a domain and a DID and verify the linkage. | | [7_sd_jwt](./1_advanced/7_sd_jwt) | Demonstrates how to create and verify selective disclosure verifiable credentials. | | [8_status_list_2021](./1_advanced/8_status_list_2021.rs) | Demonstrates how to revoke a credential using `StatusList2021`. | +| [9_zkp](./1_advanced/9_zkp.rs) | Demonstrates how to create an Anonymous Credential with BBS+. | +| [10_zkp_revocation](./1_advanced/10_zkp_revocation.rs) | Demonstrates how to revoke a credential. | | [11_linked_verifiable_presentation](./1_advanced/11_linked_verifiable_presentation.rs) | Demonstrates how to link a public Verifiable Presentation to an identity and how it can be verified. | diff --git a/examples/utils/utils.rs b/examples/utils/utils.rs index a79a74312e..f0d185bb03 100644 --- a/examples/utils/utils.rs +++ b/examples/utils/utils.rs @@ -4,70 +4,50 @@ use std::path::PathBuf; use anyhow::Context; - -use identity_iota::iota::block::output::AliasOutput; -use identity_iota::iota::IotaClientExt; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; +use identity_iota::iota_interaction::OptionalSync; use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkMemStore; use identity_iota::storage::KeyIdMemstore; use identity_iota::storage::Storage; +use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodScope; -use identity_iota::verification::jws::JwsAlgorithm; -use iota_sdk::client::api::GetAddressesOptions; -use iota_sdk::client::node_api::indexer::query_parameters::QueryParameter; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::crypto::keys::bip39; -use iota_sdk::types::block::address::Address; -use iota_sdk::types::block::address::Bech32Address; -use iota_sdk::types::block::address::Hrp; +use identity_iota::iota::rebased::client::IdentityClient; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; +use identity_iota::iota::rebased::client::IotaKeySignature; +use identity_iota::iota::rebased::transaction::Transaction; +use identity_iota::iota::rebased::utils::request_funds; +use identity_storage::JwkStorage; +use identity_storage::KeyIdStorage; +use identity_storage::KeyType; +use identity_storage::StorageSigner; +use identity_stronghold::StrongholdStorage; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::IotaClientBuilder; +use iota_sdk::IOTA_LOCAL_NETWORK_URL; +use iota_sdk_legacy::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk_legacy::client::Password; use rand::distributions::DistString; +use secret_storage::Signer; use serde_json::Value; -pub static API_ENDPOINT: &str = "http://localhost"; -pub static FAUCET_ENDPOINT: &str = "http://localhost/faucet/api/enqueue"; +pub const TEST_GAS_BUDGET: u64 = 50_000_000; pub type MemStorage = Storage; -/// Creates a DID Document and publishes it in a new Alias Output. -/// -/// Its functionality is equivalent to the "create DID" example -/// and exists for convenient calling from the other examples. -pub async fn create_did( - client: &Client, - secret_manager: &mut SecretManager, - storage: &MemStorage, -) -> anyhow::Result<(Address, IotaDocument, String)> { - let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT) - .await - .context("failed to get address with funds")?; - - let network_name: NetworkName = client.network_name().await?; - - let (document, fragment): (IotaDocument, String) = create_did_document(&network_name, storage).await?; - - let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; - - let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?; - - Ok((address, document, fragment)) -} - -/// Creates an example DID document with the given `network_name`. -/// -/// Its functionality is equivalent to the "create DID" example -/// and exists for convenient calling from the other examples. -pub async fn create_did_document( - network_name: &NetworkName, - storage: &MemStorage, -) -> anyhow::Result<(IotaDocument, String)> { - let mut document: IotaDocument = IotaDocument::new(network_name); - - let fragment: String = document +pub async fn create_did_document( + identity_client: &IdentityClient, + storage: &Storage, +) -> anyhow::Result<(IotaDocument, String)> +where + K: identity_storage::JwkStorage, + I: identity_storage::KeyIdStorage, + S: Signer + OptionalSync, +{ + // Create a new DID document with a placeholder DID. + let mut unpublished: IotaDocument = IotaDocument::new(identity_client.network()); + let verification_method_fragment = unpublished .generate_method( storage, JwkMemStore::ED25519_KEY_TYPE, @@ -77,104 +57,83 @@ pub async fn create_did_document( ) .await?; - Ok((document, fragment)) + let document = identity_client + .publish_did_document(unpublished) + .execute_with_gas(TEST_GAS_BUDGET, identity_client) + .await? + .output; + + Ok((document, verification_method_fragment)) } -/// Generates an address from the given [`SecretManager`] and adds funds from the faucet. -pub async fn get_address_with_funds( - client: &Client, - stronghold: &SecretManager, - faucet_endpoint: &str, -) -> anyhow::Result
{ - let address: Bech32Address = get_address(client, stronghold).await?; +/// Creates a random stronghold path in the temporary directory, whose exact location is OS-dependent. +pub fn random_stronghold_path() -> PathBuf { + let mut file = std::env::temp_dir(); + file.push("test_strongholds"); + file.push(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)); + file.set_extension("stronghold"); + file.to_owned() +} - request_faucet_funds(client, address, faucet_endpoint) +pub async fn get_funded_client( + storage: &Storage, +) -> Result>, anyhow::Error> +where + K: JwkStorage, + I: KeyIdStorage, +{ + let api_endpoint = std::env::var("API_ENDPOINT").unwrap_or_else(|_| IOTA_LOCAL_NETWORK_URL.to_string()); + let iota_client = IotaClientBuilder::default() + .build(&api_endpoint) .await - .context("failed to request faucet funds")?; + .map_err(|err| anyhow::anyhow!(format!("failed to connect to network; {}", err)))?; - Ok(*address) -} + // generate new key + let generate = storage + .key_storage() + .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) + .await?; + let public_key_jwk = generate.jwk.to_public().expect("public components should be derivable"); + let signer = StorageSigner::new(storage, generate.key_id, public_key_jwk); + let sender_address = IotaAddress::from(&Signer::public_key(&signer).await?); -/// Initializes the [`SecretManager`] with a new mnemonic, if necessary, -/// and generates an address from the given [`SecretManager`]. -pub async fn get_address(client: &Client, secret_manager: &SecretManager) -> anyhow::Result { - let random: [u8; 32] = rand::random(); - let mnemonic = bip39::wordlist::encode(random.as_ref(), &bip39::wordlist::ENGLISH) - .map_err(|err| anyhow::anyhow!(format!("{err:?}")))?; - - if let SecretManager::Stronghold(ref stronghold) = secret_manager { - match stronghold.store_mnemonic(mnemonic).await { - Ok(()) => (), - Err(iota_sdk::client::stronghold::Error::MnemonicAlreadyStored) => (), - Err(err) => anyhow::bail!(err), - } - } else { - anyhow::bail!("expected a `StrongholdSecretManager`"); - } - - let bech32_hrp: Hrp = client.get_bech32_hrp().await?; - let address: Bech32Address = secret_manager - .generate_ed25519_addresses( - GetAddressesOptions::default() - .with_range(0..1) - .with_bech32_hrp(bech32_hrp), - ) - .await?[0]; + request_funds(&sender_address).await?; + let package_id = std::env::var("IOTA_IDENTITY_PKG_ID") + .map_err(|e| { + anyhow::anyhow!("env variable IOTA_IDENTITY_PKG_ID must be set in order to run the examples").context(e) + }) + .and_then(|pkg_str| pkg_str.parse().context("invalid package id"))?; + + let read_only_client = IdentityClientReadOnly::new_with_pkg_id(iota_client, package_id).await?; - Ok(address) + let identity_client = IdentityClient::new(read_only_client, signer).await?; + + Ok(identity_client) } -/// Requests funds from the faucet for the given `address`. -async fn request_faucet_funds(client: &Client, address: Bech32Address, faucet_endpoint: &str) -> anyhow::Result<()> { - iota_sdk::client::request_funds_from_faucet(faucet_endpoint, &address).await?; - - tokio::time::timeout(std::time::Duration::from_secs(45), async { - loop { - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - - let balance = get_address_balance(client, &address) - .await - .context("failed to get address balance")?; - if balance > 0 { - break; - } - } - Ok::<(), anyhow::Error>(()) - }) - .await - .context("maximum timeout exceeded")??; - - Ok(()) +pub fn get_memstorage() -> Result { + Ok(MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new())) } -/// Returns the balance of the given Bech32-encoded `address`. -async fn get_address_balance(client: &Client, address: &Bech32Address) -> anyhow::Result { - let output_ids = client - .basic_output_ids(vec![ - QueryParameter::Address(address.to_owned()), - QueryParameter::HasExpiration(false), - QueryParameter::HasTimelock(false), - QueryParameter::HasStorageDepositReturn(false), - ]) - .await?; +pub fn get_stronghold_storage( + path: Option, +) -> Result, anyhow::Error> { + // Stronghold snapshot path. + let path = path.unwrap_or_else(random_stronghold_path); - let outputs = client.get_outputs(&output_ids).await?; + // Stronghold password. + let password = Password::from("secure_password".to_owned()); - let mut total_amount = 0; - for output_response in outputs { - total_amount += output_response.output().amount(); - } + let stronghold = StrongholdSecretManager::builder() + .password(password.clone()) + .build(path.clone())?; - Ok(total_amount) -} + // Create a `StrongholdStorage`. + // `StrongholdStorage` creates internally a `SecretManager` that can be + // referenced to avoid creating multiple instances around the same stronghold snapshot. + let stronghold_storage = StrongholdStorage::new(stronghold); -/// Creates a random stronghold path in the temporary directory, whose exact location is OS-dependent. -pub fn random_stronghold_path() -> PathBuf { - let mut file = std::env::temp_dir(); - file.push("test_strongholds"); - file.push(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)); - file.set_extension("stronghold"); - file.to_owned() + Ok(Storage::new(stronghold_storage.clone(), stronghold_storage.clone())) } pub fn pretty_print_json(label: &str, value: &str) { diff --git a/identity_core/Cargo.toml b/identity_core/Cargo.toml index c8efda7236..eb3df8ea7f 100644 --- a/identity_core/Cargo.toml +++ b/identity_core/Cargo.toml @@ -18,7 +18,6 @@ strum.workspace = true thiserror.workspace = true time = { version = "0.3.23", default-features = false, features = ["std", "serde", "parsing", "formatting"] } url = { version = "2.4", default-features = false, features = ["serde"] } -zeroize = { version = "1.6", default-features = false } [target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi")))'.dependencies] js-sys = { version = "0.3.55", default-features = false, optional = true } diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs index feb3de531d..e763a53858 100644 --- a/identity_credential/src/credential/jwt_serialization.rs +++ b/identity_credential/src/credential/jwt_serialization.rs @@ -27,6 +27,19 @@ use crate::credential::Subject; use crate::Error; use crate::Result; +/// A JWT representing a Verifiable Credential. +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct JwtCredential(CredentialJwtClaims<'static>); + +#[cfg(feature = "validator")] +impl TryFrom for Credential { + type Error = Error; + fn try_from(value: JwtCredential) -> std::result::Result { + value.0.try_into_credential() + } +} + /// Implementation of JWT Encoding/Decoding according to [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). /// /// This type is opinionated in the following ways: diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index 07b15f4eba..3d7422c83b 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -37,6 +37,7 @@ pub use self::jpt::Jpt; pub use self::jwp_credential_options::JwpCredentialOptions; pub use self::jws::Jws; pub use self::jwt::Jwt; +pub use self::jwt_serialization::JwtCredential; pub use self::linked_domain_service::LinkedDomainService; pub use self::linked_verifiable_presentation_service::LinkedVerifiablePresentationService; pub use self::policy::Policy; diff --git a/identity_credential/src/validator/jwt_credential_validation/error.rs b/identity_credential/src/validator/jwt_credential_validation/error.rs index a531f088d7..3fb0211ee2 100644 --- a/identity_credential/src/validator/jwt_credential_validation/error.rs +++ b/identity_credential/src/validator/jwt_credential_validation/error.rs @@ -48,7 +48,7 @@ pub enum JwtValidationError { IssuanceDate, /// Indicates that the credential's (resp. presentation's) signature could not be verified using /// the issuer's (resp. holder's) DID Document. - #[error("could not verify the {signer_ctx}'s signature")] + #[error("could not verify the {signer_ctx}'s signature; {source}")] #[non_exhaustive] Signature { /// Signature verification error. diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index bf8cab86b4..10a66a4cae 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -20,20 +20,30 @@ identity_resolver = { version = "=1.5.0", path = "../identity_resolver", default identity_storage = { version = "=1.5.0", path = "../identity_storage", default-features = false, features = ["iota-document"] } identity_verification = { version = "=1.5.0", path = "../identity_verification", default-features = false } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +identity_iota_interaction = { version = "=1.5.0", path = "../identity_iota_interaction" } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +identity_iota_interaction = { version = "=1.5.0", path = "../identity_iota_interaction", default-features = false } + [dev-dependencies] +# required for doc test anyhow = "1.0.64" -iota-sdk = { version = "1.1.5", default-features = false, features = ["tls", "client"] } +identity_iota = { version = "=1.5.0", path = "./", features = ["memstore"] } +iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v0.9.2-rc" } rand = "0.8.5" +secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", tag = "v0.2.0" } tokio = { version = "1.29.0", features = ["full"] } [features] -default = ["revocation-bitmap", "client", "iota-client", "resolver"] +default = ["revocation-bitmap", "iota-client", "send-sync", "resolver"] -# Exposes the `IotaIdentityClient` and `IotaIdentityClientExt` traits. -client = ["identity_iota_core/client"] - -# Enables the iota-client integration, the client trait implementations for it, and the `IotaClientExt` trait. -iota-client = ["identity_iota_core/iota-client", "identity_resolver/iota"] +# Enables the IOTA client integration, and the `DidResolutionHandler` trait. +iota-client = [ + "identity_iota_core/iota-client", + "identity_resolver/iota", + "identity_storage/storage-signer", +] # Enables revocation with `RevocationBitmap2022`. revocation-bitmap = [ @@ -47,8 +57,12 @@ status-list-2021 = ["revocation-bitmap", "identity_credential/status-list-2021"] # Enables support for the `Resolver`. resolver = ["dep:identity_resolver"] +# Enables `Send` + `Sync` bounds for the storage and client interaction traits. +send-sync = ["send-sync-storage", "send-sync-client"] # Enables `Send` + `Sync` bounds for the storage traits. send-sync-storage = ["identity_storage/send-sync-storage"] +# Enables `Send` + `Sync` bounds for IOTA client interaction traits. +send-sync-client = ["identity_iota_core/send-sync-client-ext"] # Enables domain linkage support. domain-linkage = ["identity_credential/domain-linkage"] diff --git a/identity_iota/README.md b/identity_iota/README.md index 407314d782..9afec2c421 100644 --- a/identity_iota/README.md +++ b/identity_iota/README.md @@ -23,33 +23,51 @@ --- > [!NOTE] -> This version of the library is compatible with IOTA Stardust networks, for a version of the library compatible with IOTA Rebased networks check [here](https://github.com/iotaledger/identity.rs/tree/feat/identity-rebased-alpha/) +> This version of the library is compatible with IOTA Rebased networks and in active development, for a version of the library compatible with IOTA Stardust networks check [here](https://github.com/iotaledger/identity.rs/) ## Introduction -IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentralized digital identity, also known as Self-Sovereign Identity (SSI). It implements the W3C [Decentralized Identifiers (DID)](https://www.w3.org/TR/did-core/) and [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) specifications. This library can be used to create, resolve and authenticate digital identities and to create verifiable credentials and presentations in order to share information in a verifiable manner and establish trust in the digital world. It does so while supporting secure storage of cryptographic keys, which can be implemented for your preferred key management system. Many of the individual libraries (Rust crates) are agnostic over the concrete DID method, with the exception of some libraries dedicated to implement the [IOTA DID method](https://wiki.iota.org/identity.rs/specs/did/iota_did_method_spec/), which is an implementation of decentralized digital identity on the IOTA and Shimmer networks. Written in stable Rust, IOTA Identity has strong guarantees of memory safety and process integrity while maintaining exceptional performance. +IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentralized digital identity, also known as Self-Sovereign Identity (SSI). It implements the W3C [Decentralized Identifiers (DID)](https://www.w3.org/TR/did-core/) and [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) specifications. This library can be used to create, resolve and authenticate digital identities and to create verifiable credentials and presentations in order to share information in a verifiable manner and establish trust in the digital world. It does so while supporting secure storage of cryptographic keys, which can be implemented for your preferred key management system. Many of the individual libraries (Rust crates) are agnostic over the concrete DID method, with the exception of some libraries dedicated to implement the [IOTA DID method](https://docs.iota.org/references/iota-identity/iota-did-method-spec/), which is an implementation of decentralized digital identity on IOTA Rebased networks. Written in stable Rust, IOTA Identity has strong guarantees of memory safety and process integrity while maintaining exceptional performance. -## Bindings + ## gRPC -We provide a collection of experimental [gRPC services](https://github.com/iotaledger/identity.rs/blob/HEAD/bindings/grpc/) +We provide a collection of experimental [gRPC services](https://github.com/iotaledger/identity.rs/blob/feat/identity-rebased-alpha/bindings/grpc/) ## Documentation and Resources - API References: - - [Rust API Reference](https://docs.rs/identity_iota/latest/identity_iota/): Package documentation (cargo docs). - - [Wasm API Reference](https://wiki.iota.org/identity.rs/libraries/wasm/api_reference/): Wasm Package documentation. -- [Identity Documentation Pages](https://wiki.iota.org/identity.rs/introduction): Supplementing documentation with context around identity and simple examples on library usage. -- [Examples](https://github.com/iotaledger/identity.rs/blob/HEAD/examples): Practical code snippets to get you started with the library. + - [Rust API Reference](https://iotaledger.github.io/identity.rs/identity_iota/index.html): Package documentation (cargo docs). + +- [Identity Documentation Pages](https://docs.iota.org/iota-identity): Supplementing documentation with context around identity and simple examples on library usage. +- [Examples](https://github.com/iotaledger/identity.rs/blob/feat/identity-rebased-alpha/examples): Practical code snippets to get you started with the library. + +## Universal Resolver + +IOTA Identity includes a [Universal Resolver](https://github.com/decentralized-identity/universal-resolver/) driver implementation for the `did:iota` method. The Universal Resolver is a crucial component that enables the resolution of DIDs across different DID methods. + +Our implementation allows for resolving IOTA DIDs through the standardized Universal Resolver interface, supporting multiple networks including testnet, devnet, and custom networks. The resolver is available as a Docker container for easy deployment and integration. + +For more information and implementation details, visit our [Universal Resolver Driver Repository](https://github.com/iotaledger/uni-resolver-driver-iota). + +### Quick Start with Docker + +```bash +# Pull and run the Universal Resolver driver +docker run -p 8080:8080 iotaledger/uni-resolver-driver-iota + +# Resolve a DID +curl -X GET http://localhost:8080/1.0/identifiers/did:iota:0xf4d6f08f5a1b80dd578da7dc1b49c886d580acd4cf7d48119dfeb82b538ad88a +``` ## Prerequisites -- [Rust](https://www.rust-lang.org/) (>= 1.65) -- [Cargo](https://doc.rust-lang.org/cargo/) (>= 1.65) +- [Rust](https://www.rust-lang.org/) (>= 1.83) +- [Cargo](https://doc.rust-lang.org/cargo/) (>= 1.83) ## Getting Started @@ -57,19 +75,23 @@ If you want to include IOTA Identity in your project, simply add it as a depende ```toml [dependencies] -identity_iota = { version = "1.5.0" } +identity_iota = { git = "https://github.com/iotaledger/identity.rs.git", tag = "v1.6.0-alpha" } ``` -To try out the [examples](https://github.com/iotaledger/identity.rs/blob/HEAD/examples), you can also do this: +To try out the [examples](https://github.com/iotaledger/identity.rs/blob/feat/identity-rebased-alpha/examples), you can also do this: 1. Clone the repository, e.g. through `git clone https://github.com/iotaledger/identity.rs` -2. Start IOTA Sandbox as described in the [next section](#example-creating-an-identity) -3. Run the example to create a DID using `cargo run --release --example 0_create_did` +2. Get the [IOTA binaries](https://github.com/iotaledger/iota/releases). +3. Start a local network for testing with `iota start --force-regenesis --with-faucet`. +4. Request funds with `iota client faucet`. +5. Publish a test identity package to your local network: `./identity_iota_core/scripts/publish_identity_package.sh`. +6. Get the `packageId` value from the output (the entry with `"type": "published"`) and pass this as `IOTA_IDENTITY_PKG_ID` env value. +7. Run the example to create a DID using `IOTA_IDENTITY_PKG_ID=(the value from previous step) run --release --example 0_create_did` ## Example: Creating an Identity The following code creates and publishes a new IOTA DID Document to a locally running private network. -See the [instructions](https://github.com/iotaledger/iota-sandbox) on running your own private network for development. +See the [instructions](https://github.com/iotaledger/iota/docker/iota-private-network) on running your own private network for development. _Cargo.toml_ @@ -77,9 +99,9 @@ _Cargo.toml_ Test this example using https://github.com/anko/txm: `txm README.md` !test program -cd ../.. +cd ../../.. mkdir tmp -cat | sed -e 's#identity_iota = { version = "[^"]*"#identity_iota = { path = "../identity_iota"#' > tmp/Cargo.toml +cat | sed -e 's#identity_iota = { git = "[^"]*", tag = "[^"]*"#identity_iota = { path = "../identity_iota"#' > tmp/Cargo.toml echo '[workspace]' >>tmp/Cargo.toml --> @@ -91,11 +113,12 @@ version = "1.0.0" edition = "2021" [dependencies] -identity_iota = { version = "1.5.0", features = ["memstore"] } -iota-sdk = { version = "1.0.2", default-features = true, features = ["tls", "client", "stronghold"] } -tokio = { version = "1", features = ["full"] } anyhow = "1.0.62" +identity_iota = { git = "https://github.com/iotaledger/identity.rs.git", tag = "v1.6.0-alpha", features = ["memstore"] } +secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", tag = "v0.2.0" } +iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v0.9.2-rc" } rand = "0.8.5" +tokio = { version = "1", features = ["full"] } ``` _main.__rs_ @@ -112,78 +135,64 @@ timeout 360 cargo build || (echo "Process timed out after 360 seconds" && exit 1 --> - ```rust,no_run -use identity_iota::core::ToJson; -use identity_iota::iota::IotaClientExt; +use anyhow::Context; use identity_iota::iota::IotaDocument; -use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::iota::NetworkName; +use identity_iota::iota::rebased::client::IdentityClient; +use identity_iota::iota::rebased::client::IdentityClientReadOnly; +use identity_iota::iota::rebased::transaction::Transaction; use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkMemStore; +use identity_iota::storage::JwkStorage; use identity_iota::storage::KeyIdMemstore; +use identity_iota::storage::KeyType; use identity_iota::storage::Storage; +use identity_iota::storage::StorageSigner; use identity_iota::verification::jws::JwsAlgorithm; use identity_iota::verification::MethodScope; -use iota_sdk::client::api::GetAddressesOptions; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::crypto::keys::bip39; -use iota_sdk::types::block::address::Bech32Address; -use iota_sdk::types::block::output::AliasOutput; -use iota_sdk::types::block::output::dto::AliasOutputDto; +use iota_sdk::IotaClientBuilder; +use iota_sdk::types::base_types::IotaAddress; +use secret_storage::Signer; use tokio::io::AsyncReadExt; -// The endpoint of the IOTA node to use. -static API_ENDPOINT: &str = "http://localhost"; - -/// Demonstrates how to create a DID Document and publish it in a new Alias Output. +/// Demonstrates how to create a DID Document and publish it in a new identity. #[tokio::main] async fn main() -> anyhow::Result<()> { // Create a new client to interact with the IOTA ledger. - let client: Client = Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() + let iota_client = IotaClientBuilder::default() + .build_localnet() + .await + .map_err(|err| anyhow::anyhow!(format!("failed to connect to network; {}", err)))?; + + // Create new storage and generate new key. + let storage = Storage::new(JwkMemStore::new(), KeyIdMemstore::new()); + let generate = storage + .key_storage() + .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) .await?; - - // Create a new Stronghold. - let stronghold = StrongholdSecretManager::builder() - .password("secure_password".to_owned()) - .build("./example-strong.hodl")?; - - // Generate a mnemonic and store it in the Stronghold. - let random: [u8; 32] = rand::random(); - let mnemonic = - bip39::wordlist::encode(random.as_ref(), &bip39::wordlist::ENGLISH).map_err(|err| anyhow::anyhow!("{err:?}"))?; - stronghold.store_mnemonic(mnemonic).await?; - - // Create a new secret manager backed by the Stronghold. - let secret_manager: SecretManager = SecretManager::Stronghold(stronghold); - - // Get the Bech32 human-readable part (HRP) of the network. - let network_name: NetworkName = client.network_name().await?; - - // Get an address from the secret manager. - let address: Bech32Address = secret_manager - .generate_ed25519_addresses( - GetAddressesOptions::default() - .with_range(0..1) - .with_bech32_hrp((&network_name).try_into()?), - ) - .await?[0]; - - println!("Your wallet address is: {}", address); - println!("Please request funds from http://localhost/faucet/, wait for a couple of seconds and then press Enter."); + let public_key_jwk = generate.jwk.to_public().expect("public components should be derivable"); + let signer = StorageSigner::new(&storage, generate.key_id, public_key_jwk); + let sender_address = { + let public_key = Signer::public_key(&signer).await?; + IotaAddress::from(&public_key) + }; + let package_id = std::env::var("IOTA_IDENTITY_PKG_ID") + .map_err(|e| { + anyhow::anyhow!("env variable IOTA_IDENTITY_PKG_ID must be set in order to run the examples").context(e) + }) + .and_then(|pkg_str| pkg_str.parse().context("invalid package id"))?; + + // Create identity client with signing capabilities. + let read_only_client = IdentityClientReadOnly::new_with_pkg_id(iota_client, package_id).await?; + let identity_client = IdentityClient::new(read_only_client, signer).await?; + + println!("Your wallet address is: {}", sender_address); + println!("Please request funds from http://127.0.0.1:9123/gas, wait for a couple of seconds and then press Enter."); tokio::io::stdin().read_u8().await?; // Create a new DID document with a placeholder DID. - // The DID will be derived from the Alias Id of the Alias Output after publishing. - let mut document: IotaDocument = IotaDocument::new(&network_name); - - // Insert a new Ed25519 verification method in the DID document. - let storage: Storage = Storage::new(JwkMemStore::new(), KeyIdMemstore::new()); - document + let mut unpublished: IotaDocument = IotaDocument::new(identity_client.network()); + unpublished .generate_method( &storage, JwkMemStore::ED25519_KEY_TYPE, @@ -193,13 +202,13 @@ async fn main() -> anyhow::Result<()> { ) .await?; - // Construct an Alias Output containing the DID document, with the wallet address - // set as both the state controller and governor. - let alias_output: AliasOutput = client.new_did_output(address.into(), document, None).await?; - println!("Alias Output: {}", AliasOutputDto::from(&alias_output).to_json_pretty()?); + // Publish new DID document. + let document = identity_client + .publish_did_document(unpublished) + .execute(&identity_client) + .await? + .output; - // Publish the Alias Output and get the published DID document. - let document: IotaDocument = client.publish_did_output(&secret_manager, alias_output).await?; println!("Published DID document: {:#}", document); Ok(()) @@ -230,8 +239,6 @@ _Example output_ "meta": { "created": "2023-08-29T14:47:26Z", "updated": "2023-08-29T14:47:26Z", - "governorAddress": "tst1qqd7kyu8xadzx9vutznu72336npqpj92jtp27uyu2tj2sa5hx6n3k0vrzwv", - "stateControllerAddress": "tst1qqd7kyu8xadzx9vutznu72336npqpj92jtp27uyu2tj2sa5hx6n3k0vrzwv" } } ``` diff --git a/identity_iota/src/lib.rs b/identity_iota/src/lib.rs index 2117a0867a..0d93d605c0 100644 --- a/identity_iota/src/lib.rs +++ b/identity_iota/src/lib.rs @@ -17,6 +17,8 @@ clippy::missing_errors_doc )] +pub use identity_iota_interaction as iota_interaction; + pub mod core { //! Core Traits and Types @@ -77,15 +79,9 @@ pub mod prelude { pub use identity_iota_core::IotaDID; pub use identity_iota_core::IotaDocument; - #[cfg(feature = "iota-client")] - #[cfg_attr(docsrs, doc(cfg(feature = "iota-client")))] - pub use identity_iota_core::IotaClientExt; - #[cfg(feature = "client")] - #[cfg_attr(docsrs, doc(cfg(feature = "client")))] - pub use identity_iota_core::IotaIdentityClient; - #[cfg(feature = "client")] - #[cfg_attr(docsrs, doc(cfg(feature = "client")))] - pub use identity_iota_core::IotaIdentityClientExt; + #[cfg(all(feature = "resolver", not(target_arch = "wasm32")))] + #[cfg_attr(docsrs, doc(cfg(all(feature = "resolver", not(target_arch = "wasm32")))))] + pub use identity_iota_core::DidResolutionHandler; #[cfg(feature = "resolver")] #[cfg_attr(docsrs, doc(cfg(feature = "resolver")))] diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index 34b637c1e4..9fc90749c3 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -4,35 +4,68 @@ version = "1.5.0" authors.workspace = true edition.workspace = true homepage.workspace = true -keywords = ["iota", "tangle", "utxo", "shimmer", "identity"] +keywords = ["iota", "tangle", "utxo", "identity"] license.workspace = true readme = "./README.md" repository.workspace = true description = "An IOTA Ledger integration for the IOTA DID Method." [dependencies] -async-trait = { version = "0.1.56", default-features = false, optional = true } +anyhow = "1.0.75" +async-trait = { version = "0.1.81", default-features = false, optional = true } +cfg-if = "1.0.0" futures = { version = "0.3", default-features = false } identity_core = { version = "=1.5.0", path = "../identity_core", default-features = false } identity_credential = { version = "=1.5.0", path = "../identity_credential", default-features = false, features = ["validator"] } identity_did = { version = "=1.5.0", path = "../identity_did", default-features = false } identity_document = { version = "=1.5.0", path = "../identity_document", default-features = false } identity_verification = { version = "=1.5.0", path = "../identity_verification", default-features = false } -iota-sdk = { version = "1.1.5", default-features = false, features = ["serde", "std"], optional = true } num-derive = { version = "0.4", default-features = false } num-traits = { version = "0.2", default-features = false, features = ["std"] } once_cell = { version = "1.18", default-features = false, features = ["std"] } prefix-hex = { version = "0.7", default-features = false } ref-cast = { version = "1.0.14", default-features = false } serde.workspace = true +serde_json.workspace = true strum.workspace = true thiserror.workspace = true +# for feature `iota-client` +bcs = { version = "0.1.4", optional = true } +fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "5f2c63266a065996d53f98156f0412782b468597", package = "fastcrypto", optional = true } +identity_eddsa_verifier = { version = "=1.5.0", path = "../identity_eddsa_verifier", optional = true } +identity_jose = { version = "=1.5.0", path = "../identity_jose", optional = true } +iota-crypto = { version = "0.23", optional = true } +itertools = { version = "0.13.0", optional = true } +phf = { version = "0.11.2", features = ["macros"] } +rand = { version = "0.8.5", optional = true } +secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", tag = "v0.2.0", default-features = false, optional = true } +serde-aux = { version = "4.5.0", optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +identity_iota_interaction = { version = "=1.5.0", path = "../identity_iota_interaction", optional = true } +iota-config = { git = "https://github.com/iotaledger/iota.git", package = "iota-config", tag = "v0.9.2-rc", optional = true } +iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v0.9.2-rc", optional = true } +move-core-types = { git = "https://github.com/iotaledger/iota.git", package = "move-core-types", tag = "v0.9.2-rc", optional = true } +shared-crypto = { git = "https://github.com/iotaledger/iota.git", package = "shared-crypto", tag = "v0.9.2-rc", optional = true } +tokio = { version = "1.29.0", default-features = false, optional = true, features = ["macros", "sync", "rt", "process"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +identity_iota_interaction = { version = "=1.5.0", path = "../identity_iota_interaction", default-features = false, optional = true } +# Dependency iota_interaction_ts is always used on wasm32 platform. It is not controlled by the "iota-client" feature +# because it's unclear how to implement this. wasm32 build will most probably always use the "iota-client" feature +# so this seems to be tolerable for now. +iota_interaction_ts = { version = "=1.5.0", path = "../bindings/wasm/iota_interaction_ts" } + [dev-dependencies] -anyhow = { version = "1.0.57" } -iota-crypto = { version = "0.23.2", default-features = false, features = ["bip39", "bip39-en"] } +iota-crypto = { version = "0.23", default-features = false, features = ["bip39", "bip39-en"] } proptest = { version = "1.0.0", default-features = false, features = ["std"] } -tokio = { version = "1.29.0", default-features = false, features = ["rt-multi-thread", "macros"] } + +# for feature iota-client tests +identity_iota_core = { path = ".", features = ["iota-client"] } # enable for e2e tests +identity_storage = { path = "../identity_storage", features = ["send-sync-storage", "storage-signer"] } +lazy_static = "1.5.0" +serial_test = "3.1.1" [package.metadata.docs.rs] # To build locally: @@ -41,17 +74,37 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["client", "iota-client", "revocation-bitmap", "send-sync-client-ext"] -# Exposes the IotaIdentityClient and IotaIdentityClientExt traits. -client = ["dep:async-trait", "iota-sdk"] -# Enables the implementation of the extension traits on the iota-sdk's Client. -iota-client = ["client", "iota-sdk/client", "iota-sdk/tls"] +default = ["iota-client", "revocation-bitmap", "send-sync", "keytool-signer"] +# Enables the IOTA Client related components, and dependencies. +iota-client = [ + "dep:async-trait", + "dep:bcs", + "dep:fastcrypto", + "dep:identity_eddsa_verifier", + "dep:identity_iota_interaction", + "dep:identity_jose", + "dep:iota-config", + "dep:iota-crypto", + "dep:iota-sdk", + "dep:itertools", + "dep:move-core-types", + "dep:rand", + "dep:secret-storage", + "dep:serde-aux", + "dep:shared-crypto", + "dep:tokio", +] + # Enables revocation with `RevocationBitmap2022`. revocation-bitmap = ["identity_credential/revocation-bitmap"] -# Adds Send bounds on the futures produces by the client extension traits. + +# Enables `Send` + `Sync` bounds for the storage and client interaction traits. +send-sync = ["send-sync-storage", "send-sync-client-ext"] +# Enables `Send` + `Sync` bounds for the storage traits. +send-sync-storage = ["secret-storage?/send-sync-storage"] +# Enables `Send` + `Sync` bounds for IOTA client interaction traits. send-sync-client-ext = [] -# Disables the blanket implementation of `IotaIdentityClientExt`. -test = ["client"] +keytool-signer = ["identity_iota_interaction/keytool-signer", "iota-client"] [lints] workspace = true diff --git a/identity_iota_core/README.md b/identity_iota_core/README.md index 6bfab1fa53..2a3862d52b 100644 --- a/identity_iota_core/README.md +++ b/identity_iota_core/README.md @@ -1,4 +1,34 @@ IOTA Identity === -This crate provides the core data structures for the [IOTA DID Method Specification](https://wiki.iota.org/shimmer/identity.rs/specs/did/iota_did_method_spec). It provides interfaces for publishing and resolving DID Documents to and from the Tangle according to the IOTA DID Method Specification. +## About +This crate provides the core data structures for the [IOTA DID Method Specification](https://wiki.iota.org/identity.rs/references/specifications/iota-did-method-spec/). It provides interfaces for publishing and resolving DID Documents according to the IOTA DID Method Specification. + +## Running the tests +You can run the tests as usual with: + +```sh +cargo test +``` + +The e2e should be run against a [local network](https://docs.iota.org/developer/getting-started/local-network), as this makes funding way more easy, as the local faucet can be used deliberately. + +### Running the tests with active-address-funding +When you're not running the tests locally, you might notice some restrictions in regards of interactions with the faucet. The current e2e test setup creates new test accounts for every test to avoid test pollution, but those accounts request funds from a faucet. That faucet might have restrictions on how much funds an IP can request in a certain time range. For example, this might happen when trying to run the tests against `devnet`. + +As we want to verify that our API works as expected on this environment as well, a toggle has been added to change the behavior in the tests to not request the faucet for funds, but use the active account of the IOTA CLI to send funds to new test users. This is not the default test behavior and should only be used in edge cases, as it comes with a few caveats, that might not be desired to have in the tests: + +- The active address must be well funded, the current active-address-funding transfers 500_000_000 NANOS to new test accounts. So make sure, this account has enough funds to support a few 2e2 tests with one or more accounts. +- The tests will take longer, as they have to be run sequentially to avoid collisions between the fund sending transactions. + +You can run a tests with active-address-funding with: + +```sh +IOTA_IDENTITY_FUND_WITH_ACTIVE_ADDRESS=true cargo test -- --test-threads=1 +``` + +To check your active account's funds, you can use + +```sh +iota client gas +``` diff --git a/identity_iota_core/packages/iota_identity/Move.toml b/identity_iota_core/packages/iota_identity/Move.toml new file mode 100644 index 0000000000..eb98201a8f --- /dev/null +++ b/identity_iota_core/packages/iota_identity/Move.toml @@ -0,0 +1,20 @@ +# Copyright (c) 2024 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "IotaIdentity" +edition = "2024.beta" + +[dependencies] +# 'tag' keyword not supported here, therefore use rev instead +# 'tag = "v0.8.1-rc"' equals 'rev = "92e66937111c27820e09502dbcb75382835a56e6"', which we use here +MoveStdlib = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/move-stdlib", rev = "45df8ac5533251237ce34f7c6e5cbc099805895e" } +Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "45df8ac5533251237ce34f7c6e5cbc099805895e" } +Stardust = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/stardust", rev = "45df8ac5533251237ce34f7c6e5cbc099805895e" } + +[addresses] +iota_identity = "0x0" + +[dev-dependencies] + +[dev-addresses] diff --git a/identity_iota_core/packages/iota_identity/sources/asset.move b/identity_iota_core/packages/iota_identity/sources/asset.move new file mode 100644 index 0000000000..055d456927 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/asset.move @@ -0,0 +1,317 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::asset { + public use fun delete_recipient_cap as RecipientCap.delete; + + const EImmutable: u64 = 0; + const ENonTransferable: u64 = 1; + const ENonDeletable: u64 = 2; + const EInvalidRecipient: u64 = 3; + const EInvalidSender: u64 = 4; + const EInvalidAsset: u64 = 5; + + // ===== Events ===== + + /// Event emitted when the owner of an `AuthenticatedAsset` + /// proposes its transfer to a new address. + public struct AssetTransferCreated has copy, drop { + asset: ID, + proposal: ID, + sender: address, + recipient: address, + } + + /// Event emitted when an active transfer is concluded, + /// either canceled or completed. + public struct AssetTransferConcluded has copy, drop { + asset: ID, + proposal: ID, + sender: address, + recipient: address, + concluded: bool, + } + + /// Structures that couples some data `T` with well known + /// ownership and origin, along configurable abilities e.g. + /// transferability, mutability and deletability. + public struct AuthenticatedAsset has key { + id: UID, + inner: T, + origin: address, + owner: address, + mutable: bool, + transferable: bool, + deletable: bool, + } + + /// Creates a new `AuthenticatedAsset` with default configuration: immutable, non-transferable, non-deletable; + /// and sends it to the tx's sender. + public fun new(inner: T, ctx: &mut TxContext) { + new_with_address(inner, ctx.sender(), false, false, false, ctx); + } + + /// Creates a new `AuthenticatedAsset` with configurable properties and sends it to the tx's sender. + public fun new_with_config( + inner: T, + mutable: bool, + transferable: bool, + deletable: bool, + ctx: &mut TxContext + ) { + new_with_address(inner, ctx.sender(), mutable, transferable, deletable, ctx); + } + + /// Returns the address that created this `AuthenticatedAsset`. + public fun origin(self: &AuthenticatedAsset): address { + self.origin + } + + /// Immutably borrow the content of an `AuthenticatedAsset` + public fun borrow(self: &AuthenticatedAsset): &T { + &self.inner + } + + /// Mutably borrow the content of an `AuthenticatedAsset`. + /// This operation will fail if `AuthenticatedAsset` is configured as non-mutable. + public fun borrow_mut(self: &mut AuthenticatedAsset): &mut T { + assert!(self.mutable, EImmutable); + &mut self.inner + } + + /// Updates the value of the stored content. Fails if this `AuthenticatedAsset` is immutable. + public fun set_content(self: &mut AuthenticatedAsset, new_content: T) { + assert!(self.mutable, EImmutable); + self.inner = new_content; + } + + public fun delete(self: AuthenticatedAsset) { + assert!(self.deletable, ENonDeletable); + let AuthenticatedAsset { + id, + inner: _, + origin: _, + owner: _, + mutable: _, + transferable: _, + deletable: _, + } = self; + object::delete(id); + } + + public(package) fun new_with_address( + inner: T, + addr: address, + mutable: bool, + transferable: bool, + deletable: bool, + ctx: &mut TxContext, + ) { + let asset = AuthenticatedAsset { + id: object::new(ctx), + inner, + origin: addr, + owner: addr, + mutable, + transferable, + deletable, + }; + transfer::transfer(asset, addr); + } + + public fun transfer( + asset: AuthenticatedAsset, + recipient: address, + ctx: &mut TxContext, + ) { + assert!(asset.transferable, ENonTransferable); + let proposal_id = object::new(ctx); + let sender_cap = SenderCap { + id: object::new(ctx), + transfer_id: proposal_id.to_inner(), + }; + let recipient_cap = RecipientCap { + id: object::new(ctx), + transfer_id: proposal_id.to_inner(), + }; + let proposal = TransferProposal { + id: proposal_id, + asset_id: object::id(&asset), + sender_cap_id: object::id(&sender_cap), + sender_address: asset.owner, + recipient_cap_id: object::id(&recipient_cap), + recipient_address: recipient, + done: false, + }; + + iota::event::emit(AssetTransferCreated { + proposal: object::id(&proposal), + asset: object::id(&asset), + sender: asset.owner, + recipient, + }); + + transfer::transfer(sender_cap, asset.owner); + transfer::transfer(recipient_cap, recipient); + transfer::transfer(asset, proposal.id.to_address()); + + transfer::share_object(proposal); + } + + /// Structure that encodes the logic required to transfer an `AuthenticatedAsset` + /// from one address to another. The transfer can be refused by the recipient. + public struct TransferProposal has key { + id: UID, + asset_id: ID, + sender_address: address, + sender_cap_id: ID, + recipient_address: address, + recipient_cap_id: ID, + done: bool, + } + + public struct SenderCap has key { + id: UID, + transfer_id: ID, + } + + public struct RecipientCap has key { + id: UID, + transfer_id: ID, + } + + /// Accept the transfer of the asset. + public fun accept( + self: &mut TransferProposal, + cap: RecipientCap, + asset: transfer::Receiving> + ) { + assert!(self.recipient_cap_id == object::id(&cap), EInvalidRecipient); + let mut asset = transfer::receive(&mut self.id, asset); + assert!(self.asset_id == object::id(&asset), EInvalidAsset); + + asset.owner = self.recipient_address; + transfer::transfer(asset, self.recipient_address); + cap.delete(); + + self.done = true; + + iota::event::emit(AssetTransferConcluded { + proposal: self.id.to_inner(), + asset: self.asset_id, + sender: self.sender_address, + recipient: self.recipient_address, + concluded: true, + }) + } + + /// The sender of the asset consumes the `TransferProposal` to either + /// cancel it or to conclude it. + public fun conclude_or_cancel( + mut proposal: TransferProposal, + cap: SenderCap, + asset: transfer::Receiving>, + ) { + assert!(proposal.sender_cap_id == object::id(&cap), EInvalidSender); + if (!proposal.done) { + let asset = transfer::receive(&mut proposal.id, asset); + assert!(proposal.asset_id == object::id(&asset), EInvalidAsset); + transfer::transfer(asset, proposal.sender_address); + + iota::event::emit(AssetTransferConcluded { + proposal: proposal.id.to_inner(), + asset: proposal.asset_id, + sender: proposal.sender_address, + recipient: proposal.recipient_address, + concluded: false, + }) + }; + + delete_transfer(proposal); + delete_sender_cap(cap); + } + + public(package) fun delete_sender_cap(cap: SenderCap) { + let SenderCap { + id, + .. + } = cap; + object::delete(id); + } + + public fun delete_recipient_cap(cap: RecipientCap) { + let RecipientCap { + id, + .. + } = cap; + object::delete(id); + } + + public(package) fun delete_transfer(self: TransferProposal) { + let TransferProposal { + id, + asset_id: _, + sender_cap_id: _, + recipient_cap_id: _, + sender_address: _, + recipient_address: _, + done: _, + } = self; + object::delete(id); + } +} + +#[test_only] +module iota_identity::asset_tests { + use iota_identity::asset::{Self, AuthenticatedAsset, EImmutable, ENonTransferable, ENonDeletable}; + use iota::test_scenario; + + const ALICE: address = @0x471c3; + const BOB: address = @0xb0b; + + #[test, expected_failure(abort_code = EImmutable)] + fun authenticated_asset_is_immutable_by_default() { + // Alice creates a new asset with default a configuration. + let mut scenario = test_scenario::begin(ALICE); + asset::new(42, scenario.ctx()); + scenario.next_tx(ALICE); + + // Alice fetches her newly created asset and attempts to modify it. + let mut asset = scenario.take_from_address>(ALICE); + *asset.borrow_mut() = 420; + + scenario.next_tx(ALICE); + scenario.return_to_sender(asset); + scenario.end(); + } + + #[test, expected_failure(abort_code = ENonTransferable)] + fun authenticated_asset_is_non_transferable_by_default() { + // Alice creates a new asset with default a configuration. + let mut scenario = test_scenario::begin(ALICE); + asset::new(42, scenario.ctx()); + scenario.next_tx(ALICE); + + // Alice fetches her newly created asset and attempts to send it to Bob. + let asset = scenario.take_from_address>(ALICE); + asset.transfer(BOB, scenario.ctx()); + + scenario.next_tx(ALICE); + scenario.end(); + } + + #[test, expected_failure(abort_code = ENonDeletable)] + fun authenticated_asset_is_non_deletable_by_default() { + // Alice creates a new asset with default a configuration. + let mut scenario = test_scenario::begin(ALICE); + asset::new(42, scenario.ctx()); + scenario.next_tx(ALICE); + + // Alice fetches her newly created asset and attempts to delete it. + let asset = scenario.take_from_address>(ALICE); + asset.delete(); + + scenario.next_tx(ALICE); + scenario.end(); + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/controller.move b/identity_iota_core/packages/iota_identity/sources/controller.move new file mode 100644 index 0000000000..2e993f5f33 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/controller.move @@ -0,0 +1,352 @@ +module iota_identity::controller { + use iota::transfer::Receiving; + use iota::borrow::{Self, Referent, Borrow}; + use iota_identity::permissions; + + public use fun delete_controller_cap as ControllerCap.delete; + public use fun delete_delegation_token as DelegationToken.delete; + public use fun delegation_token_id as DelegationToken.id; + public use fun delegation_token_controller_of as DelegationToken.controller_of; + + /// This `ControllerCap` cannot delegate access. + const ECannotDelegate: u64 = 0; + // The permission of the provided `DelegationToken` are not + // valid to perform this operation. + const EInvalidPermissions: u64 = 1; + + /// Event that is created when a new `DelegationToken` is minted. + public struct NewDelegationTokenEvent has copy, drop { + controller: ID, + token: ID, + permissions: u32, + } + + /// Capability that allows to access mutative APIs of a `Multicontroller`. + public struct ControllerCap has key { + id: UID, + controller_of: ID, + can_delegate: bool, + access_token: Referent, + } + + public fun id(self: &ControllerCap): &UID { + &self.id + } + + /// Returns the ID of the object controller by this token. + public fun controller_of(self: &ControllerCap): ID { + self.controller_of + } + + /// Borrows this `ControllerCap`'s access token. + public fun borrow(self: &mut ControllerCap): (DelegationToken, Borrow) { + self.access_token.borrow() + } + + /// Returns the borrowed access token together with the hot potato. + public fun put_back(self: &mut ControllerCap, token: DelegationToken, borrow: Borrow) { + self.access_token.put_back(token, borrow); + } + + /// Creates a delegation token for this controller. The created `DelegationToken` + /// will have full permissions. Use `delegate_with_permissions` to set or unset + /// specific permissions. + public fun delegate(self: &ControllerCap, ctx: &mut TxContext): DelegationToken { + assert!(self.can_delegate, ECannotDelegate); + new_delegation_token(self.id.to_inner(), self.controller_of, permissions::all(), ctx) + } + + /// Creates a delegation token for this controller, specifying the delegate's permissions. + public fun delegate_with_permissions(self: &ControllerCap, permissions: u32, ctx: &mut TxContext): DelegationToken { + assert!(self.can_delegate, ECannotDelegate); + new_delegation_token(self.id.to_inner(), self.controller_of, permissions, ctx) + } + + /// A token that allows an entity to act in a Controller's stead. + public struct DelegationToken has key, store { + id: UID, + permissions: u32, + controller: ID, + controller_of: ID, + } + + /// Returns the ID of this `DelegationToken`. + public fun delegation_token_id(self: &DelegationToken): ID { + self.id.to_inner() + } + + /// Returns the controller's ID of this `DelegationToken`. + public fun controller(self: &DelegationToken): ID { + self.controller + } + + public fun delegation_token_controller_of(self: &DelegationToken): ID { + self.controller_of + } + + /// Returns the permissions of this `DelegationToken`. + public fun permissions(self: &DelegationToken): u32 { + self.permissions + } + + /// Returns true if this `DelegationToken` has permission `permission`. + public fun has_permission(self: &DelegationToken, permission: u32): bool { + self.permissions & permission != 0 + } + + /// Aborts if this `DelegationToken` doesn't have permission `permission`. + public fun assert_has_permission(self: &DelegationToken, permission: u32) { + assert!(self.has_permission(permission), EInvalidPermissions) + } + + /// Creates a new `ControllerCap`. + public(package) fun new(can_delegate: bool, controller_of: ID, ctx: &mut TxContext): ControllerCap { + let id = object::new(ctx); + let access_token = borrow::new(new_delegation_token( + id.to_inner(), + controller_of, + permissions::all(), + ctx + ), ctx); + + ControllerCap { + id, + access_token, + controller_of, + can_delegate, + } + } + + /// Transfer a `ControllerCap`. + public(package) fun transfer(cap: ControllerCap, recipient: address) { + transfer::transfer(cap, recipient) + } + + /// Receives a `ControllerCap`. + public(package) fun receive(owner: &mut UID, cap: Receiving): ControllerCap { + transfer::receive(owner, cap) + } + + public(package) fun new_delegation_token( + controller: ID, + controller_of: ID, + permissions: u32, + ctx: &mut TxContext + ): DelegationToken { + let id = object::new(ctx); + + iota::event::emit(NewDelegationTokenEvent { + controller, + token: id.to_inner(), + permissions, + }); + + DelegationToken { + id, + controller, + controller_of, + permissions, + } + } + + public(package) fun delete_controller_cap(cap: ControllerCap) { + let ControllerCap { + access_token, + id, + .. + } = cap; + + delete_delegation_token(access_token.destroy()); + object::delete(id); + } + + public(package) fun delete_delegation_token(token: DelegationToken) { + let DelegationToken { + id, + .. + } = token; + object::delete(id); + } +} + +#[test_only] +module iota_identity::controller_tests { + use iota::test_scenario; + use iota_identity::controller::{Self, ControllerCap, ECannotDelegate, EInvalidPermissions}; + use iota_identity::permissions; + use iota_identity::multicontroller::{Self, Multicontroller}; + + fun controllee_id(): ID { + object::id_from_address(@0x123456) + } + + #[test, expected_failure(abort_code = ECannotDelegate)] + fun test_only_delegatable_controllers_can_create_delegation_tokens() { + let owner = @0x1; + let mut scenario = test_scenario::begin(owner); + + let non_delegatable = controller::new(false, controllee_id(), scenario.ctx()); + let delegation_token = non_delegatable.delegate(scenario.ctx()); + + delegation_token.delete(); + non_delegatable.delete(); + scenario.end(); + } + + #[test, expected_failure(abort_code = EInvalidPermissions)] + fun delegate_cannot_create_proposal_when_missing_permission() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + + let mut multicontroller: Multicontroller = multicontroller::new(0, true, controllee_id(), scenario.ctx()); + scenario.next_tx(controller); + + let controller_cap = scenario.take_from_address(controller); + let delegation_token = controller_cap.delegate_with_permissions( + permissions::all() & permissions::not(permissions::can_create_proposal()), + scenario.ctx(), + ); + + scenario.next_tx(controller); + + multicontroller.create_proposal<_, u64>( + &delegation_token, + 0, + option::none(), + scenario.ctx(), + ); + + abort(0) + } + + #[test, expected_failure(abort_code = EInvalidPermissions)] + fun delegate_cannot_execute_proposal_when_missing_permission() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + + let mut multicontroller: Multicontroller = multicontroller::new(0, true, controllee_id(), scenario.ctx()); + scenario.next_tx(controller); + + let controller_cap = scenario.take_from_address(controller); + let delegation_token = controller_cap.delegate_with_permissions( + permissions::all() & permissions::not(permissions::can_execute_proposal()), + scenario.ctx(), + ); + + scenario.next_tx(controller); + + let proposal_id = multicontroller.create_proposal<_, u64>( + &delegation_token, + 0, + option::none(), + scenario.ctx(), + ); + + multicontroller.execute_proposal<_, u64>( + &delegation_token, + proposal_id, + scenario.ctx(), + ).unwrap(); + + abort(0) + } + + #[test, expected_failure(abort_code = EInvalidPermissions)] + fun delegate_cannot_approve_proposal_when_missing_permission() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + + let mut multicontroller: Multicontroller = multicontroller::new(0, true, controllee_id(), scenario.ctx()); + scenario.next_tx(controller); + + let controller_cap = scenario.take_from_address(controller); + let delegation_token = controller_cap.delegate_with_permissions( + permissions::all() & permissions::not(permissions::can_approve_proposal()), + scenario.ctx(), + ); + + scenario.next_tx(controller); + + let proposal_id = multicontroller.create_proposal<_, u64>( + &delegation_token, + 0, + option::none(), + scenario.ctx(), + ); + + multicontroller.approve_proposal<_, u64>( + &delegation_token, + proposal_id, + ); + + abort(0) + } + + #[test, expected_failure(abort_code = EInvalidPermissions)] + fun delegate_cannot_remove_approval_when_missing_permission() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + + let mut multicontroller: Multicontroller = multicontroller::new(0, true, controllee_id(), scenario.ctx()); + scenario.next_tx(controller); + + let controller_cap = scenario.take_from_address(controller); + let delegation_token = controller_cap.delegate_with_permissions( + permissions::all() & permissions::not(permissions::can_remove_approval()), + scenario.ctx(), + ); + + scenario.next_tx(controller); + + let proposal_id = multicontroller.create_proposal<_, u64>( + &delegation_token, + 0, + option::none(), + scenario.ctx(), + ); + + multicontroller.remove_approval<_, u64>( + &delegation_token, + proposal_id, + ); + + abort(0) + } + + #[test, expected_failure(abort_code = EInvalidPermissions)] + fun delegate_cannot_delete_proposal_when_missing_permission() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + + let mut multicontroller: Multicontroller = multicontroller::new(0, true, controllee_id(), scenario.ctx()); + scenario.next_tx(controller); + + let controller_cap = scenario.take_from_address(controller); + let delegation_token = controller_cap.delegate_with_permissions( + permissions::all() & permissions::not(permissions::can_remove_approval()), + scenario.ctx(), + ); + + scenario.next_tx(controller); + + let proposal_id = multicontroller.create_proposal<_, u64>( + &delegation_token, + 0, + option::none(), + scenario.ctx(), + ); + + multicontroller.remove_approval<_, u64>( + &delegation_token, + proposal_id, + ); + + multicontroller.delete_proposal<_, u64>( + &delegation_token, + proposal_id, + scenario.ctx() + ); + + abort(0) + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/identity.move b/identity_iota_core/packages/iota_identity/sources/identity.move new file mode 100644 index 0000000000..857d949b49 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/identity.move @@ -0,0 +1,1098 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::identity { + use iota::{ + transfer::Receiving, + vec_map::{Self, VecMap}, + clock::Clock, + }; + use iota_identity::{ + multicontroller::{Self, Multicontroller, Action}, + controller::{DelegationToken, ControllerCap}, + update_value_proposal::{Self, UpdateValue}, + config_proposal, + transfer_proposal::{Self, Send}, + borrow_proposal::{Self, Borrow}, + controller_proposal::{Self, ControllerExecution}, + upgrade_proposal::{Self, Upgrade}, + delete_proposal::{Self, Delete}, + }; + + const ENotADidDocument: u64 = 0; + const EInvalidTimestamp: u64 = 1; + /// The threshold specified upon document creation was not valid. + /// Threshold must be greater than or equal to 1. + const EInvalidThreshold: u64 = 2; + /// The controller list must contain at least 1 element. + const EInvalidControllersList: u64 = 3; + /// There's no upgrade available for this identity. + const ENoUpgrade: u64 = 4; + /// Cannot delete identity. + const ECannotDelete: u64 = 5; + /// Identity had been deleted. + const EDeletedIdentity: u64 = 6; + + const PACKAGE_VERSION: u64 = 0; + + // ===== Events ====== + /// Event emitted when an `identity`'s `Proposal` with `ID` `proposal` is created or executed by `controller`. + public struct ProposalEvent has copy, drop { + identity: ID, + controller: ID, + proposal: ID, + // Set to `true` if `proposal` has been executed. + executed: bool, + } + + /// Event emitted when a `Proposal` has reached the AC threshold and + /// can now be executed. + public struct ProposalApproved has copy, drop { + /// ID of the `Identity` owning the proposal. + identity: ID, + /// ID of the created `Proposal`. + proposal: ID, + } + + /// On-chain Identity. + public struct Identity has key { + id: UID, + /// Same as stardust `state_metadata`. + did_doc: Multicontroller>>, + /// If this `Identity` has been migrated from a Stardust + /// AliasOutput, this field must be set with its AliasID. + legacy_id: Option, + /// Timestamp of this Identity's creation. + created: u64, + /// Timestamp of this Identity's last update. + updated: u64, + /// Package version used by this object. + version: u64, + /// Flag to verify if this Identity has been deleted. + /// Once an Identity has been deleted it CANNOT be activated again. + deleted: bool, + /// Set when the DID Document of this Identity has been deleted. + /// Once a DID Document has been deleted it CANNOT be activated again. + deleted_did: bool, + } + + /// Creates an `Identity` with a single controller. + public fun new( + doc: Option>, + clock: &Clock, + ctx: &mut TxContext + ): ID { + new_with_controller(doc, ctx.sender(), false, clock, ctx) + } + + /// Creates an identity specifying its `created` timestamp. + /// Should only be used for migration! + public(package) fun new_with_migration_data( + doc: Option>, + creation_timestamp: u64, + legacy_id: ID, + clock: &Clock, + ctx: &mut TxContext + ): ID { + let now = clock.timestamp_ms(); + assert!(now >= creation_timestamp, EInvalidTimestamp); + let id = object::new(ctx); + let identity_id = id.to_inner(); + let identity = Identity { + id, + did_doc: multicontroller::new_with_controller(doc, ctx.sender(), false, identity_id, ctx), + legacy_id: option::some(legacy_id), + created: creation_timestamp, + updated: now, + version: PACKAGE_VERSION, + deleted: false, + deleted_did: false, + }; + let id = object::id(&identity); + transfer::share_object(identity); + + id + } + + /// Creates a new `Identity` wrapping DID DOC `doc` and controller by + /// a single address `controller`. + public fun new_with_controller( + doc: Option>, + controller: address, + can_delegate: bool, + clock: &Clock, + ctx: &mut TxContext, + ): ID { + let now = clock.timestamp_ms(); + let id = object::new(ctx); + let identity_id = id.to_inner(); + + let identity = Identity { + id, + did_doc: multicontroller::new_with_controller(doc, controller, can_delegate, identity_id, ctx), + legacy_id: option::none(), + created: now, + updated: now, + version: PACKAGE_VERSION, + deleted: false, + deleted_did: false, + }; + let id = object::id(&identity); + transfer::share_object(identity); + + id + } + + /// Creates an [`Identity`] controlled by multiple controllers. + /// The `weights` vectors is used to create a vector of `ControllerCap`s `controller_caps`, + /// where `controller_caps[i].weight = weights[i]` for all `i`s in `[0, weights.length())`. + public fun new_with_controllers( + doc: Option>, + controllers: VecMap, + controllers_that_can_delegate: VecMap, + threshold: u64, + clock: &Clock, + ctx: &mut TxContext, + ): ID { + assert!(threshold >= 1, EInvalidThreshold); + assert!(controllers.size() > 0, EInvalidControllersList); + if (doc.is_some()) { + assert!(is_did_output(doc.borrow()), ENotADidDocument); + }; + + let now = clock.timestamp_ms(); + let id = object::new(ctx); + let identity_id = id.to_inner(); + let identity = Identity { + id, + did_doc: multicontroller::new_with_controllers(doc, controllers, controllers_that_can_delegate, threshold, identity_id, ctx), + legacy_id: option::none(), + created: now, + updated: now, + version: PACKAGE_VERSION, + deleted: false, + deleted_did: false, + }; + let id = object::id(&identity); + + transfer::share_object(identity); + id + } + + /// Returns a reference to the `UID` of an `Identity`. + public fun id(self: &Identity): &UID { + &self.id + } + + /// Returns a reference to the optional legacy ID of this `Identity`. + /// Only `Identity`s that had been migrated from Stardust AliasOutputs + /// will have `legacy_id` set. + public fun legacy_id(self: &Identity): &Option { + &self.legacy_id + } + + /// Returns the unsigned amount of milliseconds + /// that passed from the UNIX epoch to the creation of this `Identity`. + public fun created(self: &Identity): u64 { + self.created + } + + /// Returns the unsigned amount of milliseconds + /// that passed from the UNIX epoch to the last update on this `Identity`. + public fun updated(self: &Identity): u64 { + self.updated + } + + /// Returns the value of the flag `deleted`. + public fun deleted(self: &Identity): bool { + self.deleted + } + + /// Returns the value of the flag `deleted_did`. + public fun deleted_did(self: &Identity): bool { + self.deleted_did + } + + /// Returns this `Identity`'s threshold. + public fun threshold(self: &Identity): u64 { + self.did_doc.threshold() + } + + /// Approve an `Identity`'s `Proposal`. + public fun approve_proposal( + self: &mut Identity, + cap: &DelegationToken, + proposal_id: ID, + ) { + self.did_doc.approve_proposal<_, T>(cap, proposal_id); + // If proposal is ready to be executed send an event. + if (self.did_doc.is_proposal_approved<_, T>(proposal_id)) { + iota::event::emit(ProposalApproved { + identity: self.id().to_inner(), + proposal: proposal_id, + }) + } + } + + /// Proposes the deletion of this `Identity`. + public fun propose_deletion( + self: &mut Identity, + cap: &DelegationToken, + expiration: Option, + clock: &Clock, + ctx: &mut TxContext, + ): Option { + assert!(!self.deleted, EDeletedIdentity); + + let proposal_id = self.did_doc.create_proposal( + cap, + delete_proposal::new(), + expiration, + ctx, + ); + let is_approved = self + .did_doc + .is_proposal_approved<_, Delete>(proposal_id); + + if (is_approved) { + self.execute_deletion(cap, proposal_id, clock, ctx); + option::none() + } else { + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); + option::some(proposal_id) + } + } + + /// Executes a proposal to delete this `Identity`'s DID document. + public fun execute_deletion( + self: &mut Identity, + cap: &DelegationToken, + proposal_id: ID, + clock: &Clock, + ctx: &mut TxContext, + ) { + assert!(!self.deleted, EDeletedIdentity); + let _ = self.execute_proposal( + cap, + proposal_id, + ctx, + ).unwrap(); + self.deleted = true; + self.did_doc.set_controlled_value(option::none()); + self.updated = clock.timestamp_ms(); + + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, true); + } + + /// Creates a new `ControllerExecution` proposal. + public fun propose_controller_execution( + self: &mut Identity, + cap: &DelegationToken, + controller_cap_id: ID, + expiration: Option, + ctx: &mut TxContext, + ): ID { + assert!(!self.deleted, EDeletedIdentity); + let identity_address = self.id().to_address(); + let proposal_id = self.did_doc.create_proposal( + cap, + controller_proposal::new(controller_cap_id, identity_address), + expiration, + ctx, + ); + + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); + proposal_id + } + + /// Borrow the identity-owned controller cap specified in `ControllerExecution`. + /// The borrowed cap must be put back by calling `controller_proposal::put_back`. + public fun borrow_controller_cap( + self: &mut Identity, + action: &mut Action, + receiving: Receiving, + ): ControllerCap { + controller_proposal::receive(action, &mut self.id, receiving) + } + + /// Proposes to upgrade this `Identity` to this package's version. + public fun propose_upgrade( + self: &mut Identity, + cap: &DelegationToken, + expiration: Option, + ctx: &mut TxContext, + ): Option { + assert!(!self.deleted, EDeletedIdentity); + assert!(self.version < PACKAGE_VERSION, ENoUpgrade); + let proposal_id = self.did_doc.create_proposal( + cap, + upgrade_proposal::new(), + expiration, + ctx + ); + let is_approved = self + .did_doc + .is_proposal_approved<_, Upgrade>(proposal_id); + if (is_approved) { + self.execute_upgrade(cap, proposal_id, ctx); + option::none() + } else { + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); + option::some(proposal_id) + } + } + + /// Consumes a `Proposal` that migrates `Identity` to this + /// package's version. + public fun execute_upgrade( + self: &mut Identity, + cap: &DelegationToken, + proposal_id: ID, + ctx: &mut TxContext, + ) { + assert!(!self.deleted, EDeletedIdentity); + self.execute_proposal(cap, proposal_id, ctx).unwrap(); + self.migrate(); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, true); + } + + /// Migrates this `Identity` to this package's version. + fun migrate(self: &mut Identity) { + // ADD migration logic when needed! + self.version = PACKAGE_VERSION; + } + + /// Proposes an update to the DID Document contained in this `Identity`. + /// This function can update the DID Document right away if `cap` has + /// enough voting power. + public fun propose_update( + self: &mut Identity, + cap: &DelegationToken, + updated_doc: Option>, + expiration: Option, + clock: &Clock, + ctx: &mut TxContext, + ): Option { + assert!(!self.deleted && !self.deleted_did, EDeletedIdentity); + if (updated_doc.is_some()) { + let doc = updated_doc.borrow(); + assert!(doc.is_empty() || is_did_output(doc), ENotADidDocument); + }; + let proposal_id = update_value_proposal::propose_update( + &mut self.did_doc, + cap, + updated_doc, + expiration, + ctx, + ); + + let is_approved = self + .did_doc + .is_proposal_approved<_, update_value_proposal::UpdateValue>>>(proposal_id); + if (is_approved) { + self.execute_update(cap, proposal_id, clock, ctx); + option::none() + } else { + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); + option::some(proposal_id) + } + } + + /// Executes a proposal to update the DID Document contained in this `Identity`. + public fun execute_update( + self: &mut Identity, + cap: &DelegationToken, + proposal_id: ID, + clock: &Clock, + ctx: &mut TxContext, + ) { + assert!(!self.deleted && !self.deleted_did, EDeletedIdentity); + let updated_did_value = self + .execute_proposal>>>(cap, proposal_id, ctx) + .unpack_action() + .into_inner(); + + if (updated_did_value.is_none()) { + self.deleted_did = true; + }; + + self.did_doc.set_controlled_value(updated_did_value); + + self.updated = clock.timestamp_ms(); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, true); + } + + /// Proposes to update this `Identity`'s AC. + /// This operation might be carried out right away if `cap` + /// has enough voting power. + public fun propose_config_change( + self: &mut Identity, + cap: &DelegationToken, + expiration: Option, + threshold: Option, + controllers_to_add: VecMap, + controllers_to_remove: vector, + controllers_to_update: VecMap, + ctx: &mut TxContext, + ): Option { + assert!(!self.deleted, EDeletedIdentity); + let proposal_id = config_proposal::propose_modify( + &mut self.did_doc, + cap, + expiration, + threshold, + controllers_to_add, + controllers_to_remove, + controllers_to_update, + ctx + ); + + let is_approved = self + .did_doc + .is_proposal_approved<_, config_proposal::Modify>(proposal_id); + if (is_approved) { + self.execute_config_change(cap, proposal_id, ctx); + option::none() + } else { + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); + option::some(proposal_id) + } + } + + /// Execute a proposal to change this `Identity`'s AC. + public fun execute_config_change( + self: &mut Identity, + cap: &DelegationToken, + proposal_id: ID, + ctx: &mut TxContext + ) { + assert!(!self.deleted, EDeletedIdentity); + config_proposal::execute_modify( + &mut self.did_doc, + cap, + proposal_id, + ctx, + ); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, true); + } + + /// Proposes the transfer of a set of objects owned by this `Identity`. + public fun propose_send( + self: &mut Identity, + cap: &DelegationToken, + expiration: Option, + objects: vector, + recipients: vector
, + ctx: &mut TxContext, + ): ID { + assert!(!self.deleted, EDeletedIdentity); + let proposal_id = transfer_proposal::propose_send( + &mut self.did_doc, + cap, + expiration, + objects, + recipients, + ctx + ); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); + proposal_id + } + + /// Sends one object among the one specified in a `Send` proposal. + public fun execute_send( + self: &mut Identity, + send_action: &mut Action, + receiving: Receiving, + ) { + transfer_proposal::send(send_action, &mut self.id, receiving); + } + + /// Requests the borrowing of a set of assets + /// in order to use them in a transaction. Borrowed assets must be returned. + public fun propose_borrow( + self: &mut Identity, + cap: &DelegationToken, + expiration: Option, + objects: vector, + ctx: &mut TxContext, + ): ID { + assert!(!self.deleted, EDeletedIdentity); + let identity_address = self.id().to_address(); + let proposal_id = borrow_proposal::propose_borrow( + &mut self.did_doc, + cap, + expiration, + objects, + identity_address, + ctx, + ); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, false); + proposal_id + } + + /// Takes one of the borrowed assets. + public fun execute_borrow( + self: &mut Identity, + borrow_action: &mut Action, + receiving: Receiving, + ): T { + borrow_proposal::borrow(borrow_action, &mut self.id, receiving) + } + + /// Simplified version of `Identity::propose_config_change` that allows + /// to add a new controller. + public fun propose_new_controller( + self: &mut Identity, + cap: &DelegationToken, + expiration: Option, + new_controller_addr: address, + voting_power: u64, + ctx: &mut TxContext, + ): Option { + assert!(!self.deleted, EDeletedIdentity); + let mut new_controllers = vec_map::empty(); + new_controllers.insert(new_controller_addr, voting_power); + + self.propose_config_change(cap, expiration, option::none(), new_controllers, vector[], vec_map::empty(), ctx) + } + + /// Executes an `Identity`'s proposal. + public fun execute_proposal( + self: &mut Identity, + cap: &DelegationToken, + proposal_id: ID, + ctx: &mut TxContext, + ): Action { + assert!(!self.deleted, EDeletedIdentity); + emit_proposal_event(self.id().to_inner(), cap.id(), proposal_id, true); + self.did_doc.execute_proposal(cap, proposal_id, ctx) + } + + /// Deletes an `Identity`'s proposal. Proposals can only be deleted if they have no votes, if they are expired, + // or if the identity is deleted. + public fun delete_proposal( + self: &mut Identity, + cap: &DelegationToken, + proposal_id: ID, + ctx: &mut TxContext, + ) { + if (self.deleted) { + self.did_doc.force_delete_proposal<_, T>(proposal_id); + } else { + self.did_doc.delete_proposal<_, T>(cap, proposal_id, ctx); + } + } + + /// revoke the `DelegationToken` with `ID` `deny_id`. Only controllers can perform this operation. + public fun revoke_token(self: &mut Identity, cap: &ControllerCap, deny_id: ID) { + self.did_doc.revoke_token(cap, deny_id); + } + + /// Un-revoke a `DelegationToken`. + public fun unrevoke_token(self: &mut Identity, cap: &ControllerCap, token_id: ID) { + self.did_doc.unrevoke_token(cap, token_id); + } + + /// Destroys a `ControllerCap`. Can only be used after a controller has been removed from + /// the controller committee OR if `Identity`'s `deleted` flag is set. + public fun destroy_controller_cap(self: &mut Identity, cap: ControllerCap) { + if (self.deleted) { + self.did_doc.remove_and_destroy_controller(cap); + } else { + self.did_doc.destroy_controller_cap(cap); + } + } + + /// Destroys a `DelegationToken`. + public fun destroy_delegation_token(self: &mut Identity, token: DelegationToken) { + self.did_doc.destroy_delegation_token(token); + } + + /// Deletes this Identity. + /// Calls to this method will succeed only if + /// the `Identity` has no controllers left and its `deleted` flag had been + /// set to `true`. + public fun delete(self: Identity) { + assert!(self.deleted && self.did_doc.controllers().is_empty(), ECannotDelete); + let Identity { + id, + did_doc, + .. + } = self; + object::delete(id); + did_doc.delete(); + } + + /// Checks if `data` is a state metadata representing a DID. + /// i.e. starts with the bytes b"DID". + public(package) fun is_did_output(data: &vector): bool { + data[0] == 0x44 && // b'D' + data[1] == 0x49 && // b'I' + data[2] == 0x44 // b'D' + } + + public(package) fun did_doc(self: &Identity): &Multicontroller>> { + &self.did_doc + } + + #[test_only] + public(package) fun to_address(self: &Identity): address { + self.id().to_inner().id_to_address() + } + + public(package) fun emit_proposal_event( + identity: ID, + controller: ID, + proposal: ID, + executed: bool, + ) { + iota::event::emit(ProposalEvent { + identity, + controller, + proposal, + executed, + }) + } +} + + +#[test_only] +module iota_identity::identity_tests { + use iota::test_scenario; + use iota_identity::identity::{new, ENotADidDocument, Identity, new_with_controllers, EDeletedIdentity}; + use iota_identity::config_proposal::Modify; + use iota_identity::multicontroller::{EExpiredProposal, EThresholdNotReached}; + use iota_identity::controller::ControllerCap; + use iota::vec_map; + use iota::clock; + + #[test] + fun adding_a_controller_works() { + let controller1 = @0x1; + let controller2 = @0x2; + let mut scenario = test_scenario::begin(controller1); + let clock = clock::create_for_testing(scenario.ctx()); + + + // Create a DID document with no funds and 1 controller with a weight of 1 and a threshold of 1. + // Share the document and send the controller capability to `controller1`. + let _identity_id = new(option::some(b"DID"), &clock, scenario.ctx()); + + scenario.next_tx(controller1); + + // Create a request to add a second controller. + let mut identity = scenario.take_shared(); + let mut controller1_cap = scenario.take_from_address(controller1); + let (token, borrow) = controller1_cap.borrow(); + // This is carried out immediately. + identity.propose_new_controller(&token, option::none(), controller2, 1, scenario.ctx()); + controller1_cap.put_back(token, borrow); + + scenario.next_tx(controller2); + + let mut controller2_cap = scenario.take_from_address(controller2); + let (token, borrow) = controller2_cap.borrow(); + + identity.did_doc().assert_is_member(&token); + controller2_cap.put_back(token, borrow); + // Cleanup + test_scenario::return_to_address(controller1, controller1_cap); + test_scenario::return_to_address(controller2, controller2_cap); + test_scenario::return_shared(identity); + + let _ = scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test] + fun removing_a_controller_works() { + let controller1 = @0x1; + let controller2 = @0x2; + let controller3 = @0x3; + let mut scenario = test_scenario::begin(controller1); + let clock = clock::create_for_testing(scenario.ctx()); + + let mut controllers = vec_map::empty(); + controllers.insert(controller1, 1); + controllers.insert(controller2, 1); + controllers.insert(controller3, 1); + + // Create an identity shared by `controller1`, `controller2`, `controller3`. + let _identity_id = new_with_controllers( + option::some(b"DID"), + controllers, + vec_map::empty(), + 2, + &clock, + scenario.ctx(), + ); + + scenario.next_tx(controller1); + + // `controller1` creates a request to remove `controller3`. + let mut identity = scenario.take_shared(); + let mut controller1_cap = scenario.take_from_address(controller1); + let controller3_cap = scenario.take_from_address(controller3); + + let (token, borrow) = controller1_cap.borrow(); + let proposal_id = identity.propose_config_change( + &token, + option::none(), + option::none(), + vec_map::empty(), + vector[controller3_cap.id().to_inner()], + vec_map::empty(), + scenario.ctx() + ).destroy_some(); + controller1_cap.put_back(token, borrow); + + scenario.next_tx(controller2); + + // `controller2` also approves the removal of `controller3`. + let mut controller2_cap = scenario.take_from_address(controller2); + let (token, borrow) = controller2_cap.borrow(); + identity.approve_proposal(&token, proposal_id); + controller2_cap.put_back(token, borrow); + + scenario.next_tx(controller2); + + // `controller3` is removed. + let (token, borrow) = controller2_cap.borrow(); + identity.execute_config_change(&token, proposal_id, scenario.ctx()); + controller2_cap.put_back(token, borrow); + assert!(!identity.did_doc().controllers().contains(&controller3_cap.id().to_inner()), 0); + + // cleanup. + test_scenario::return_to_address(controller1, controller1_cap); + test_scenario::return_to_address(controller2, controller2_cap); + test_scenario::return_to_address(controller3, controller3_cap); + test_scenario::return_shared(identity); + + let _ = scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test, expected_failure(abort_code = EThresholdNotReached)] + fun test_controller_addition_fails_when_threshold_not_met() { + let controller_a = @0x1; + let controller_b = @0x2; + let controller_c = @0x3; + + // The controller that is not part of the ACL. + let controller_d = @0x4; + + let mut scenario = test_scenario::begin(controller_a); + let clock = clock::create_for_testing(scenario.ctx()); + + let mut controllers = vec_map::empty(); + controllers.insert(controller_a, 10); + controllers.insert(controller_b, 5); + controllers.insert(controller_c, 5); + + // === First transaction === + // Controller A can execute config changes + { + let _ = new_with_controllers( + option::some(b"DID"), + controllers, + vec_map::empty(), + 10, + &clock, + scenario.ctx(), + ); + scenario.next_tx(controller_a); + + // Controller A alone should be able to do anything. + let mut identity = scenario.take_shared(); + let mut controller_a_cap = scenario.take_from_address(controller_a); + let (token, borrow) = controller_a_cap.borrow(); + + // Create a request to add a new controller. This is carried out immediately as controller_a has enough voting power + identity.propose_new_controller(&token, option::none(), controller_d, 1, scenario.ctx()); + controller_a_cap.put_back(token, borrow); + + scenario.next_tx(controller_d); + + let mut controller_d_cap = scenario.take_from_address(controller_d); + let (token, borrow) = controller_d_cap.borrow(); + + identity.did_doc().assert_is_member(&token); + controller_d_cap.put_back(token, borrow); + + test_scenario::return_shared(identity); + test_scenario::return_to_address(controller_a, controller_a_cap); + test_scenario::return_to_address(controller_d, controller_d_cap); + }; + + + // Controller B alone should not be able to make changes. + { + let _ = new_with_controllers( + option::some(b"DID"), + controllers, + vec_map::empty(), + 10, + &clock, + scenario.ctx(), + ); + scenario.next_tx(controller_a); + + let mut identity = scenario.take_shared(); + let mut controller_b_cap = scenario.take_from_address(controller_b); + let (token, borrow) = controller_b_cap.borrow(); + + let proposal_id = identity.propose_new_controller(&token, option::none(), controller_d, 1, scenario.ctx()).destroy_some(); + + scenario.next_tx(controller_b); + identity.execute_config_change(&token, proposal_id, scenario.ctx()); + controller_b_cap.put_back(token, borrow); + scenario.next_tx(controller_d); + + let controller_d_cap = scenario.take_from_address(controller_d); + assert!(!identity.did_doc().controllers().contains(&controller_d_cap.id().to_inner()), 0); + + test_scenario::return_to_address(controller_b, controller_b_cap); + test_scenario::return_to_address(controller_d, controller_d_cap); + test_scenario::return_shared(identity); + }; + let _ = scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test] + fun test_controller_addition_works_when_threshold_met() { + let controller_a = @0x1; + let controller_b = @0x2; + let controller_c = @0x3; + + // The controller that is not part of the ACL. + let controller_d = @0x4; + + let mut scenario = test_scenario::begin(controller_b); + let clock = clock::create_for_testing(scenario.ctx()); + + let mut controllers = vec_map::empty(); + controllers.insert(controller_a, 10); + controllers.insert(controller_b, 5); + controllers.insert(controller_c, 5); + + // === First transaction === + // Controller B & C can execute config changes + let _ = new_with_controllers( + option::some(b"DID"), + controllers, + vec_map::empty(), + 10, + &clock, + scenario.ctx(), + ); + scenario.next_tx(controller_b); + + let mut identity = scenario.take_shared(); + let mut controller_b_cap = scenario.take_from_address(controller_b); + let (token, borrow) = controller_b_cap.borrow(); + + // Create a request to add a new controller. + let proposal_id = identity.propose_new_controller(&token, option::none(), controller_d, 10, scenario.ctx()).destroy_some(); + controller_b_cap.put_back(token, borrow); + + scenario.next_tx(controller_b); + let mut controller_c_cap = scenario.take_from_address(controller_c); + let (token, borrow) = controller_c_cap.borrow(); + identity.approve_proposal(&token, proposal_id); + + scenario.next_tx(controller_a); + identity.execute_config_change(&token, proposal_id, scenario.ctx()); + controller_c_cap.put_back(token, borrow); + + scenario.next_tx(controller_d); + + let mut controller_d_cap = scenario.take_from_address(controller_d); + let (token, borrow) = controller_d_cap.borrow(); + identity.did_doc().assert_is_member(&token); + controller_d_cap.put_back(token, borrow); + + test_scenario::return_shared(identity); + test_scenario::return_to_address(controller_b, controller_b_cap); + test_scenario::return_to_address(controller_c, controller_c_cap); + test_scenario::return_to_address(controller_d, controller_d_cap); + + let _ = scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test] + fun check_identity_can_own_another_identity() { + let controller_a = @0x1; + let mut scenario = test_scenario::begin(controller_a); + let clock = clock::create_for_testing(scenario.ctx()); + + let _ = new(option::some(b"DID"), &clock, scenario.ctx()); + + scenario.next_tx(controller_a); + let first_identity = scenario.take_shared(); + + let mut controllers = vec_map::empty(); + controllers.insert(first_identity.to_address(), 10); + + // Create a second identity. + let _ = new_with_controllers( + option::some(b"DID"), + controllers, + vec_map::empty(), + 10, + &clock, + scenario.ctx(), + ); + + scenario.next_tx(first_identity.to_address()); + let mut first_identity_cap = scenario.take_from_address(first_identity.to_address()); + let (token, borrow) = first_identity_cap.borrow(); + + let mut second_identity = scenario.take_shared(); + + assert!(second_identity.did_doc().controllers().contains(&first_identity_cap.id().to_inner()), 0); + + second_identity.propose_new_controller(&token, option::none(), controller_a, 10, scenario.ctx()).destroy_none(); + first_identity_cap.put_back(token, borrow); + + scenario.next_tx(controller_a); + let mut controller_a_cap = scenario.take_from_address(controller_a); + let (token, borrow) = controller_a_cap.borrow(); + + second_identity.did_doc().assert_is_member(&token); + controller_a_cap.put_back(token, borrow); + + test_scenario::return_shared(second_identity); + test_scenario::return_to_address(controller_a, controller_a_cap); + test_scenario::return_to_address(first_identity.to_address(), first_identity_cap); + test_scenario::return_shared(first_identity); + + let _ = scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test, expected_failure(abort_code = ENotADidDocument)] + fun test_update_proposal_cannot_propose_non_did_doc() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + let clock = clock::create_for_testing(scenario.ctx()); + + let _ = new(option::some(b"DID"), &clock, scenario.ctx()); + + scenario.next_tx(controller); + + // Propose a change for updating the did document + let mut identity = scenario.take_shared(); + let mut cap = scenario.take_from_address(controller); + let (token, borrow) = cap.borrow(); + + let _proposal_id = identity.propose_update(&token, option::some(b"NOT DID"), option::none(), &clock, scenario.ctx()); + cap.put_back(token, borrow); + + test_scenario::return_to_address(controller, cap); + test_scenario::return_shared(identity); + + scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test, expected_failure(abort_code = EExpiredProposal)] + fun expired_proposals_cannot_be_executed() { + let controller_a = @0x1; + let controller_b = @0x2; + let new_controller = @0x3; + let mut scenario = test_scenario::begin(controller_a); + let expiration_epoch = scenario.ctx().epoch(); + let clock = clock::create_for_testing(scenario.ctx()); + + let mut controllers = vec_map::empty(); + controllers.insert(controller_a, 1); + controllers.insert(controller_b, 1); + + let _ = new_with_controllers(option::some(b"DID"), controllers, vec_map::empty(), 2, &clock, scenario.ctx()); + + scenario.next_tx(controller_a); + + let mut identity = scenario.take_shared(); + let mut cap = scenario.take_from_address(controller_a); + let (token, borrow) = cap.borrow(); + let proposal_id = identity.propose_new_controller(&token, option::some(expiration_epoch), new_controller, 1, scenario.ctx()).destroy_some(); + cap.put_back(token, borrow); + + scenario.next_tx(controller_b); + let mut cap_b = scenario.take_from_address(controller_b); + let (token, borrow) = cap_b.borrow(); + identity.approve_proposal(&token, proposal_id); + cap_b.put_back(token, borrow); + + scenario.later_epoch(100, controller_a); + // this should fail! + let (token, borrow) = cap.borrow(); + identity.execute_config_change(&token, proposal_id, scenario.ctx()); + cap.put_back(token, borrow); + + test_scenario::return_to_address(controller_a, cap); + test_scenario::return_to_address(controller_b, cap_b); + test_scenario::return_shared(identity); + + scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test] + fun identity_can_be_deleted() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + let clock = clock::create_for_testing(scenario.ctx()); + + let _ = new(option::some(b"DID"), &clock, scenario.ctx()); + + scenario.next_tx(controller); + + let mut identity = scenario.take_shared(); + let mut cap = scenario.take_from_address(controller); + let (token, borrow) = cap.borrow(); + identity.propose_deletion(&token, option::none(), &clock, scenario.ctx()); + cap.put_back(token, borrow); + + scenario.next_tx(controller); + identity.destroy_controller_cap(cap); + + assert!(identity.deleted()); + identity.delete(); + + scenario.end(); + clock::destroy_for_testing(clock); + } + + #[test, expected_failure(abort_code = EDeletedIdentity)] + fun updating_did_with_none_deletes_it() { + let controller = @0x1; + let mut scenario = test_scenario::begin(controller); + let clock = clock::create_for_testing(scenario.ctx()); + + let _ = new(option::some(b"DID"), &clock, scenario.ctx()); + + scenario.next_tx(controller); + + let mut identity = scenario.take_shared(); + let mut cap = scenario.take_from_address(controller); + let (token, borrow) = cap.borrow(); + identity.propose_update(&token, option::none(), option::none(), &clock, scenario.ctx()); + + assert!(identity.deleted_did()); + + scenario.next_tx(controller); + + // This should fail + identity.propose_update(&token, option::some(b"DID"), option::none(), &clock, scenario.ctx()); + + cap.put_back(token, borrow); + test_scenario::return_to_address(controller, cap); + test_scenario::return_shared(identity); + + scenario.end(); + clock::destroy_for_testing(clock); + } +} diff --git a/identity_iota_core/packages/iota_identity/sources/migration.move b/identity_iota_core/packages/iota_identity/sources/migration.move new file mode 100644 index 0000000000..bd9af925b9 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/migration.move @@ -0,0 +1,140 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::migration { + use iota_identity::{migration_registry::MigrationRegistry, identity}; + use stardust::{alias::Alias, alias_output::AliasOutput}; + use iota::{coin, iota::IOTA, clock::Clock}; + + const ENotADidOutput: u64 = 1; + + #[allow(lint(share_owned))] + public fun migrate_alias( + alias: Alias, + migration_registry: &mut MigrationRegistry, + creation_timestamp: u64, + clock: &Clock, + ctx: &mut TxContext, + ): address { + // Extract needed data from `alias`. + let alias_id = object::id(&alias); + let mut state_metadata = *alias.state_metadata(); + // `alias` is not needed anymore, destroy it. + alias.destroy(); + + // Check if `state_metadata` contains a DID document. + assert!(state_metadata.is_some() && identity::is_did_output(state_metadata.borrow()), ENotADidOutput); + + let identity_id = identity::new_with_migration_data( + option::some(state_metadata.extract()), + creation_timestamp, + alias_id, + clock, + ctx + ); + + // Add a migration record. + migration_registry.add(alias_id, identity_id); + + identity_id.to_address() + } + + /// Creates a new `Identity` from an Iota 1.0 legacy `AliasOutput` containing a DID Document. + public fun migrate_alias_output( + alias_output: AliasOutput, + migration_registry: &mut MigrationRegistry, + creation_timestamp: u64, + clock: &Clock, + ctx: &mut TxContext + ) { + // Extract required data from output. + let (iota, native_tokens, alias_data) = alias_output.extract_assets(); + + let identity_addr = migrate_alias( + alias_data, + migration_registry, + creation_timestamp, + clock, + ctx + ); + + let coin = coin::from_balance(iota, ctx); + transfer::public_transfer(coin, identity_addr); + transfer::public_transfer(native_tokens, identity_addr); + } +} + + +#[test_only] +module iota_identity::migration_tests { + use iota::{test_scenario, balance, bag, iota::IOTA, clock}; + use stardust::alias_output::{Self, AliasOutput}; + use iota_identity::identity::{Identity}; + use iota_identity::migration::migrate_alias_output; + use stardust::alias::{Self, Alias}; + use iota_identity::migration_registry::{MigrationRegistry, init_testing}; + use iota_identity::controller::ControllerCap; + + fun create_did_alias(ctx: &mut TxContext): Alias { + let sender = ctx.sender(); + alias::create_for_testing( + sender, + 1, + option::some(b"DID"), + option::some(sender), + option::none(), + option::none(), + option::none(), + ctx + ) + } + + fun create_empty_did_output(ctx: &mut TxContext): (AliasOutput, ID) { + let mut alias_output = alias_output::create_for_testing(balance::zero(), bag::new(ctx), ctx); + let alias = create_did_alias(ctx); + let alias_id = object::id(&alias); + alias_output.attach_alias(alias); + + (alias_output, alias_id) + } + + #[test] + fun test_migration_of_legacy_did_output() { + let controller_a = @0x1; + let mut scenario = test_scenario::begin(controller_a); + let clock = clock::create_for_testing(scenario.ctx()); + + let (did_output, alias_id) = create_empty_did_output(scenario.ctx()); + + init_testing(scenario.ctx()); + + scenario.next_tx(controller_a); + let mut registry = scenario.take_shared(); + + migrate_alias_output(did_output, &mut registry, clock.timestamp_ms(), &clock, scenario.ctx()); + + scenario.next_tx(controller_a); + let identity = scenario.take_shared(); + let mut controller_a_cap = scenario.take_from_address(controller_a); + let (token, borrow) = controller_a_cap.borrow(); + + // Assert correct binding in migration registry + assert!(registry.lookup(alias_id) == identity.id().to_inner(), 0); + // Assert correct backward-binding in Identity + assert!(*identity.legacy_id().borrow() == alias_id, 0); + + // Assert the sender is controller + identity.did_doc().assert_is_member(&token); + controller_a_cap.put_back(token, borrow); + + // assert the metadata is b"DID" + let did = identity.did_doc().value().borrow(); + assert!(did == &b"DID", 0); + + test_scenario::return_to_address(controller_a, controller_a_cap); + test_scenario::return_shared(registry); + test_scenario::return_shared(identity); + let _ = scenario.end(); + clock::destroy_for_testing(clock); + } +} diff --git a/identity_iota_core/packages/iota_identity/sources/migration_registry.move b/identity_iota_core/packages/iota_identity/sources/migration_registry.move new file mode 100644 index 0000000000..25161bd9c2 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/migration_registry.move @@ -0,0 +1,54 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::migration_registry { + use iota::{dynamic_field as field, transfer::share_object, event}; + + const BEACON_BYTES: vector = b"identity.rs_pkg"; + + /// One time witness needed to construct a singleton `MigrationRegistry`. + public struct MIGRATION_REGISTRY has drop {} + + /// Event type that is fired upon creation of a `MigrationRegistry`. + public struct MigrationRegistryCreated has copy, drop { + id: ID, + beacon: vector, + } + + /// Object that tracks migrated alias outputs to their corresponding object IDs. + public struct MigrationRegistry has key { + id: UID, + } + + /// Creates a singleton instance of `MigrationRegistry` when publishing this package. + fun init(_otw: MIGRATION_REGISTRY, ctx: &mut TxContext) { + let id = object::new(ctx); + let registry_id = id.to_inner(); + let registry = MigrationRegistry { + id, + }; + share_object(registry); + // Signal the creation of a migration registry. + event::emit(MigrationRegistryCreated { id: registry_id, beacon: BEACON_BYTES }); + } + + /// Checks whether the given alias ID exists in the migration registry. + public fun exists(self: &MigrationRegistry, alias_id: ID): bool { + field::exists_(&self.id, alias_id) + } + + /// Lookup an alias ID into the migration registry. + public fun lookup(self: &MigrationRegistry, alias_id: ID): ID { + *field::borrow(&self.id, alias_id) + } + + /// Adds a new Alias ID -> Object ID binding to the regitry. + public(package) fun add(self: &mut MigrationRegistry, alias_id: ID, identity_id: ID) { + field::add(&mut self.id, alias_id, identity_id); + } + + #[test_only] + public fun init_testing(ctx: &mut TxContext) { + init(MIGRATION_REGISTRY {}, ctx); + } +} diff --git a/identity_iota_core/packages/iota_identity/sources/multicontroller.move b/identity_iota_core/packages/iota_identity/sources/multicontroller.move new file mode 100644 index 0000000000..0027510afd --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/multicontroller.move @@ -0,0 +1,432 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::multicontroller { + use iota::{object_bag::{Self, ObjectBag}, vec_map::{Self, VecMap}, vec_set::{Self, VecSet}}; + use iota_identity::controller::{Self, DelegationToken, ControllerCap}; + use iota_identity::permissions; + + const EInvalidController: u64 = 0; + const EControllerAlreadyVoted: u64 = 1; + const EThresholdNotReached: u64 = 2; + const EInvalidThreshold: u64 = 3; + const EExpiredProposal: u64 = 4; + const ENotVotedYet: u64 = 5; + const EProposalNotFound: u64 = 6; + const ECannotDelete: u64 = 7; + + /// Shares control of a value `V` with multiple entities called controllers. + public struct Multicontroller has store { + threshold: u64, + owner: ID, + controllers: VecMap, + controlled_value: V, + active_proposals: vector, + proposals: ObjectBag, + revoked_tokens: VecSet, + } + + /// Wraps a `V` in `Multicontroller`, making the tx's sender a controller with + /// voting power 1. + public fun new( + controlled_value: V, + can_delegate: bool, + owner: ID, + ctx: &mut TxContext + ): Multicontroller { + new_with_controller(controlled_value, ctx.sender(), can_delegate, owner, ctx) + } + + /// Wraps a `V` in `Multicontroller` and sends `controller` a `ControllerCap`. + public fun new_with_controller( + controlled_value: V, + controller: address, + can_delegate: bool, + owner: ID, + ctx: &mut TxContext + ): Multicontroller { + let mut controllers = vec_map::empty(); + controllers.insert(controller, 1); + + if (can_delegate) { + new_with_controllers(controlled_value, vec_map::empty(), controllers, 1, owner, ctx) + } else { + new_with_controllers(controlled_value, controllers, vec_map::empty(), 1, owner, ctx) + } + } + + /// Wraps a `V` in `Multicontroller`, settings `threshold` as the threshold, + /// and using `controllers` to set controllers: i.e. each `(recipient, voting power)` + /// in `controllers` results in `recipient` obtaining a `ControllerCap` with the + /// specified voting power. + /// Controllers that are able to delegate their access, should be passed through + /// `controllers_that_can_delegate` parameter. + public fun new_with_controllers( + controlled_value: V, + controllers: VecMap, + controllers_that_can_delegate: VecMap, + threshold: u64, + owner: ID, + ctx: &mut TxContext, + ): Multicontroller { + let (mut addrs, mut vps) = controllers.into_keys_values(); + let mut controllers = vec_map::empty(); + while(!addrs.is_empty()) { + let addr = addrs.pop_back(); + let vp = vps.pop_back(); + + let cap = controller::new(false, owner, ctx); + controllers.insert(cap.id().to_inner(), vp); + + cap.transfer(addr) + }; + + let (mut addrs, mut vps) = controllers_that_can_delegate.into_keys_values(); + while(!addrs.is_empty()) { + let addr = addrs.pop_back(); + let vp = vps.pop_back(); + + let cap = controller::new(true, owner, ctx); + controllers.insert(cap.id().to_inner(), vp); + + cap.transfer(addr) + }; + + let mut multi = Multicontroller { + controlled_value, + controllers, + owner, + threshold, + active_proposals: vector[], + proposals: object_bag::new(ctx), + revoked_tokens: vec_set::empty(), + }; + multi.set_threshold(threshold); + + multi + } + + /// Structure that encapsulates the logic required to make changes + /// to a multicontrolled value. + public struct Proposal has key, store { + id: UID, + votes: u64, + voters: VecSet, + expiration_epoch: Option, + action: T, + } + + /// Returns `true` if `Proposal` `self` is expired. + public fun is_expired(self: &Proposal, ctx: &mut TxContext): bool { + if (self.expiration_epoch.is_some()) { + let expiration = *self.expiration_epoch.borrow(); + expiration < ctx.epoch() + } else { + false + } + } + + /// Structure that encapsulate the kind of change that will be performed + /// when a proposal is carried out. + public struct Action { + inner: T, + } + + /// Consumes `Action` returning the inner value. + public fun unwrap(action: Action): T { + let Action { inner } = action; + inner + } + + /// Borrows the content of `action`. + public fun borrow(action: &Action): &T { + &action.inner + } + + /// Mutably borrows the content of `action`. + public fun borrow_mut(action: &mut Action): &mut T { + &mut action.inner + } + + public(package) fun assert_is_member(multi: &Multicontroller, cap: &DelegationToken) { + assert!(multi.controllers.contains(&cap.controller()), EInvalidController); + } + + /// Creates a new proposal for `Multicontroller` `multi`. + public fun create_proposal( + multi: &mut Multicontroller, + cap: &DelegationToken, + action: T, + expiration_epoch: Option, + ctx: &mut TxContext, + ): ID { + multi.assert_is_member(cap); + cap.assert_has_permission(permissions::can_create_proposal()); + + let cap_id = cap.controller(); + let voting_power = multi.voting_power(cap_id); + + let proposal = Proposal { + id: object::new(ctx), + votes: voting_power, + voters: vec_set::singleton(cap_id), + expiration_epoch, + action, + }; + + let proposal_id = object::id(&proposal); + multi.proposals.add(proposal_id, proposal); + multi.active_proposals.push_back(proposal_id); + proposal_id + } + + /// Approves an active `Proposal` in `multi`. + public fun approve_proposal( + multi: &mut Multicontroller, + cap: &DelegationToken, + proposal_id: ID, + ) { + multi.assert_is_member(cap); + cap.assert_has_permission(permissions::can_approve_proposal()); + + let cap_id = cap.controller(); + let voting_power = multi.voting_power(cap_id); + + let proposal = multi.proposals.borrow_mut>(proposal_id); + assert!(!proposal.voters.contains(&cap_id), EControllerAlreadyVoted); + + proposal.votes = proposal.votes + voting_power; + proposal.voters.insert(cap_id); + } + + /// Consumes the `multi`'s active `Proposal` with id `proposal_id`, + /// returning its inner `Action`. + /// This call fails if `multi`'s threshold has not been reached. + public fun execute_proposal( + multi: &mut Multicontroller, + cap: &DelegationToken, + proposal_id: ID, + ctx: &mut TxContext, + ): Action { + multi.assert_is_member(cap); + cap.assert_has_permission(permissions::can_execute_proposal()); + + let proposal = multi.proposals.remove>(proposal_id); + assert!(proposal.votes >= multi.threshold, EThresholdNotReached); + assert!(!proposal.is_expired(ctx), EExpiredProposal); + + let Proposal { + id, + votes: _, + voters: _, + expiration_epoch: _, + action: inner, + } = proposal; + + id.delete(); + + let (present, i) = multi.active_proposals.index_of(&proposal_id); + assert!(present, EProposalNotFound); + + multi.active_proposals.remove(i); + + Action { inner } + } + + /// Removes the approval given by the controller owning `cap` on `Proposal` + /// `proposal_id`. + public fun remove_approval( + multi: &mut Multicontroller, + cap: &DelegationToken, + proposal_id: ID, + ) { + cap.assert_has_permission(permissions::can_remove_approval()); + + let cap_id = cap.controller(); + let vp = multi.voting_power(cap_id); + + let proposal = multi.proposals.borrow_mut>(proposal_id); + assert!(proposal.voters.contains(&cap_id), ENotVotedYet); + + proposal.voters.remove(&cap_id); + proposal.votes = proposal.votes - vp; + } + + /// Removes a proposal no one has voted for. + public fun delete_proposal( + multi: &mut Multicontroller, + cap: &DelegationToken, + proposal_id: ID, + ctx: &mut TxContext, + ) { + cap.assert_has_permission(permissions::can_delete_proposal()); + + let proposal = multi.proposals.remove>(proposal_id); + assert!(proposal.votes == 0 || proposal.is_expired(ctx), ECannotDelete); + + let Proposal { + id, + votes: _, + voters: _, + expiration_epoch: _, + action: _, + } = proposal; + + id.delete(); + + let (present, i) = multi.active_proposals.index_of(&proposal_id); + assert!(present, EProposalNotFound); + + multi.active_proposals.remove(i); + } + + /// Returns a reference to `multi`'s value. + public fun value(multi: &Multicontroller): &V { + &multi.controlled_value + } + + /// Returns the list of `multi`'s controllers - i.e. the `ID` of its `ControllerCap`s. + public fun controllers(multi: &Multicontroller): vector { + multi.controllers.keys() + } + + /// Returns `multi`'s threshold. + public fun threshold(multi: &Multicontroller): u64 { + multi.threshold + } + + /// Returns the voting power of a given controller, identified by its `ID`. + public fun voting_power(multi: &Multicontroller, controller_id: ID): u64 { + *multi.controllers.get(&controller_id) + } + + public(package) fun set_voting_power(multi: &mut Multicontroller, controller_id: ID, vp: u64) { + assert!(multi.controllers().contains(&controller_id), EInvalidController); + *multi.controllers.get_mut(&controller_id) = vp; + } + + /// Returns the sum of all controllers voting powers. + public fun max_votes(multi: &Multicontroller): u64 { + let (_, mut values) = multi.controllers.into_keys_values(); + let mut sum = 0; + while (!values.is_empty()) { + sum = sum + values.pop_back(); + }; + + sum + } + + /// Revoke the `DelegationToken` with `ID` `deny_id`. Only controllers can perform this operation. + public fun revoke_token(self: &mut Multicontroller, cap: &ControllerCap, deny_id: ID) { + assert!(self.controllers.contains(object::borrow_id(cap)), EInvalidController); + self.revoked_tokens.insert(deny_id); + } + + /// Un-revoke a `DelegationToken`. + public fun unrevoke_token(self: &mut Multicontroller, cap: &ControllerCap, token_id: ID) { + assert!(self.controllers.contains(object::borrow_id(cap)), EInvalidController); + self.revoked_tokens.remove(&token_id); + } + + /// Destroys a `ControllerCap`. Can only be used after a controller has been removed from + /// the controller committee. + public fun destroy_controller_cap(self: &mut Multicontroller, cap: ControllerCap) { + assert!(!self.controllers.contains(&cap.id().to_inner()), EInvalidController); + assert!(cap.controller_of() == self.owner, EInvalidController); + + cap.delete(); + } + + public fun remove_and_destroy_controller(self: &mut Multicontroller, cap: ControllerCap) { + assert!(cap.controller_of() == self.owner, EInvalidController); + + let controller_id = object::id(&cap); + if (self.controllers.contains(&controller_id)) { + self.controllers.remove(&controller_id); + }; + + cap.delete(); + } + + /// Destroys a `DelegationToken`. + public fun destroy_delegation_token(self: &mut Multicontroller, token: DelegationToken) { + let token_id = object::id(&token); + let is_revoked = self.revoked_tokens.contains(&token_id); + if (is_revoked) { + self.revoked_tokens.remove(&token_id); + }; + + token.delete(); + } + + /// Deletes this `Multicontroller` returning the wrapped value. + /// This function can only be called if there are no active proposals. + public fun delete(self: Multicontroller): V { + assert!(self.active_proposals.is_empty(), ECannotDelete); + + let Multicontroller { + controlled_value, + proposals, + .. + } = self; + + proposals.destroy_empty(); + controlled_value + } + + public(package) fun unpack_action(action: Action): T { + let Action { inner } = action; + inner + } + + public(package) fun is_proposal_approved(multi: &Multicontroller, proposal_id: ID): bool { + let proposal = multi.proposals.borrow>(proposal_id); + proposal.votes >= multi.threshold + } + + public(package) fun add_members(multi: &mut Multicontroller, to_add: VecMap, ctx: &mut TxContext) { + let mut i = 0; + while (i < to_add.size()) { + let (addr, vp) = to_add.get_entry_by_idx(i); + let new_cap = controller::new(false, multi.owner, ctx); + multi.controllers.insert(new_cap.id().to_inner(), *vp); + new_cap.transfer(*addr); + i = i + 1; + } + } + + public(package) fun remove_members(multi: &mut Multicontroller, mut to_remove: vector) { + while (!to_remove.is_empty()) { + let id = to_remove.pop_back(); + multi.controllers.remove(&id); + } + } + + public(package) fun update_members(multi: &mut Multicontroller, mut to_update: VecMap) { + while (!to_update.is_empty()) { + let (controller, vp) = to_update.pop(); + + multi.set_voting_power(controller, vp); + } + } + + public(package) fun set_threshold(multi: &mut Multicontroller, threshold: u64) { + assert!(threshold <= multi.max_votes(), EInvalidThreshold); + multi.threshold = threshold; + } + + public(package) fun set_controlled_value(multi: &mut Multicontroller, controlled_value: V) { + multi.controlled_value = controlled_value; + } + + public(package) fun force_delete_proposal(self: &mut Multicontroller, proposal_id: ID) { + let proposal = self.proposals.remove>(proposal_id); + + let Proposal { + id, + .. + } = proposal; + + id.delete(); + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/permissions.move b/identity_iota_core/packages/iota_identity/sources/permissions.move new file mode 100644 index 0000000000..35dc601fcc --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/permissions.move @@ -0,0 +1,24 @@ +module iota_identity::permissions { + /// Permission that enables a controller's delegate to create proposals. + const CAN_CREATE_PROPOSAL: u32 = 0x1; + /// Permission that enables a controller's delegate to approve proposals. + const CAN_APPROVE_PROPOSAL: u32 = 0x1 << 1; + /// Permission that enables a controller's delegate to execute proposals. + const CAN_EXECUTE_PROPOSAL: u32 = 0x1 << 2; + /// Permission that enables a controller's delegate to delete proposals. + const CAN_DELETE_PROPOSAL: u32 = 0x1 << 3; + /// Permission that enables a controller's delegate to remove a proposal's approval. + const CAN_REMOVE_APPROVAL: u32 = 0x1 << 4; + const ALL_PERMISSIONS: u32 = 0xFFFFFFFF; + + public fun can_create_proposal(): u32 { CAN_CREATE_PROPOSAL } + public fun can_approve_proposal(): u32 { CAN_APPROVE_PROPOSAL } + public fun can_execute_proposal(): u32 { CAN_EXECUTE_PROPOSAL } + public fun can_delete_proposal(): u32 { CAN_DELETE_PROPOSAL } + public fun can_remove_approval(): u32 { CAN_REMOVE_APPROVAL } + public fun all(): u32 { ALL_PERMISSIONS } + /// Negate a permission + public fun not(permission: u32): u32 { + permission ^ all() + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move b/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move new file mode 100644 index 0000000000..00f2a16e31 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/borrow.move @@ -0,0 +1,75 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::borrow_proposal { + use iota_identity::multicontroller::{Multicontroller, Action}; + use iota_identity::controller::DelegationToken; + use iota::transfer::Receiving; + + const EInvalidObject: u64 = 0; + const EInvalidOwner: u64 = 1; + const EUnreturnedObjects: u64 = 2; + + /// Action used to "borrow" assets in a transaction - enforcing their return. + public struct Borrow has store, drop { + objects: vector, + objects_to_return: vector, + owner: address, + } + + /// Propose the borrowing of a set of assets owned by this multicontroller. + public fun propose_borrow( + multi: &mut Multicontroller, + cap: &DelegationToken, + expiration: Option, + objects: vector, + owner: address, + ctx: &mut TxContext, + ): ID { + let action = Borrow { objects, objects_to_return: vector::empty(), owner }; + + multi.create_proposal(cap, action, expiration, ctx) + } + + /// Borrows an asset from this action. This function will fail if: + /// - the received object is not among `Borrow::objects`; + /// - controllee does not have the same address as `Borrow::owner`; + public fun borrow( + action: &mut Action, + controllee: &mut UID, + receiving: Receiving, + ): T { + let borrow_action = action.borrow_mut(); + assert!(borrow_action.owner == controllee.to_address(), EInvalidOwner); + let receiving_object_id = receiving.receiving_object_id(); + let (obj_exists, obj_idx) = borrow_action.objects.index_of(&receiving_object_id); + assert!(obj_exists, EInvalidObject); + + borrow_action.objects.swap_remove(obj_idx); + borrow_action.objects_to_return.push_back(receiving_object_id); + + transfer::public_receive(controllee, receiving) + } + + /// Transfer a borrowed object back to its original owner. + public fun put_back( + action: &mut Action, + obj: T, + ) { + let borrow_action = action.borrow_mut(); + let object_id = object::id(&obj); + let (contains, obj_idx) = borrow_action.objects_to_return.index_of(&object_id); + assert!(contains, EInvalidObject); + + borrow_action.objects_to_return.swap_remove(obj_idx); + transfer::public_transfer(obj, borrow_action.owner); + } + + /// Consumes a borrow action. + public fun conclude_borrow( + action: Action + ) { + let Borrow { objects: _, objects_to_return, owner: _ } = action.unpack_action(); + assert!(objects_to_return.is_empty(), EUnreturnedObjects); + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/config.move b/identity_iota_core/packages/iota_identity/sources/proposals/config.move new file mode 100644 index 0000000000..a53067de9c --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/config.move @@ -0,0 +1,108 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::config_proposal { + use iota_identity::multicontroller::Multicontroller; + use iota_identity::controller::DelegationToken; + use iota::vec_map::VecMap; + + const ENotMember: u64 = 0; + const EInvalidThreshold: u64 = 1; + + public struct Modify has store, drop { + threshold: Option, + controllers_to_add: VecMap, + controllers_to_remove: vector, + controllers_to_update: VecMap, + } + + public fun propose_modify( + multi: &mut Multicontroller, + cap: &DelegationToken, + expiration: Option, + mut threshold: Option, + controllers_to_add: VecMap, + controllers_to_remove: vector, + controllers_to_update: VecMap, + ctx: &mut TxContext, + ): ID { + let mut max_votes = 0; + let (mut cs, mut vps) = controllers_to_update.into_keys_values(); + while (!cs.is_empty()) { + let c = cs.pop_back(); + let vp = vps.pop_back(); + assert!(multi.controllers().contains(&c), ENotMember); + max_votes = max_votes + vp; + }; + let (_, mut voting_powers) = controllers_to_add.into_keys_values(); + let mut voting_power_increase = 0; + while (!voting_powers.is_empty()) { + let voting_power = voting_powers.pop_back(); + + voting_power_increase = voting_power_increase + voting_power; + }; + voting_powers.destroy_empty(); + + let mut i = 0; + let mut voting_power_decrease = 0; + while (i < controllers_to_remove.length()) { + let controller_id = controllers_to_remove[i]; + assert!(multi.controllers().contains(&controller_id), ENotMember); + let mut vp = multi.voting_power(controller_id); + if (controllers_to_update.contains(&controller_id)) { + vp = *controllers_to_update.get(&controller_id); + }; + voting_power_decrease = voting_power_decrease + vp; + i = i + 1; + }; + + let mut i = 0; + while (i < multi.controllers().length()) { + let controller_id = multi.controllers()[i]; + if (!controllers_to_update.contains(&controller_id)) { + max_votes = max_votes + multi.voting_power(controller_id); + }; + i = i + 1; + }; + + let new_max_votes = max_votes + voting_power_increase - voting_power_decrease; + + let threshold = if (threshold.is_some()) { + let threshold = threshold.extract(); + threshold + } else { + multi.threshold() + }; + + assert!(threshold > 0 && threshold <= new_max_votes, EInvalidThreshold); + + let action = Modify { + threshold: option::some(threshold), + controllers_to_add, + controllers_to_remove, + controllers_to_update, + }; + + multi.create_proposal(cap, action, expiration, ctx) + } + + public fun execute_modify( + multi: &mut Multicontroller, + cap: &DelegationToken, + proposal_id: ID, + ctx: &mut TxContext, + ) { + let action = multi.execute_proposal(cap, proposal_id, ctx); + let Modify { + mut threshold, + controllers_to_add, + controllers_to_remove, + controllers_to_update + } = action.unpack_action(); + + if (threshold.is_some()) multi.set_threshold(threshold.extract()); + multi.update_members(controllers_to_update); + multi.add_members(controllers_to_add, ctx); + multi.remove_members(controllers_to_remove); + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/controller.move b/identity_iota_core/packages/iota_identity/sources/proposals/controller.move new file mode 100644 index 0000000000..f09944a93d --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/controller.move @@ -0,0 +1,56 @@ +module iota_identity::controller_proposal { + use iota::transfer::Receiving; + use iota_identity::controller::{Self, ControllerCap}; + use iota_identity::multicontroller::Action; + + /// The received `ControllerCap` does not match the one + /// specified in the `ControllerExecution` action. + const EControllerCapMismatch: u64 = 0; + /// The provided `UID` is not the `UID` of the `Identity` + /// specified in the action. + const EInvalidIdentityUID: u64 = 1; + + /// Borrow a given `ControllerCap` from an `Identity` for + /// a single transaction. + public struct ControllerExecution has store, drop { + /// ID of the `ControllerCap` to borrow. + controller_cap: ID, + /// The address of the `Identity` that owns + /// the `ControllerCap` we are borrowing. + identity: address, + } + + /// Returns a new `ControllerExecution` that - in a Proposal - allows whoever + /// executes it to receive `identity`'s `ControllerCap` (the one that has ID `controller_cap`) + /// for the duration of a single transaction. + public fun new(controller_cap: ID, identity: address): ControllerExecution { + ControllerExecution { + controller_cap, + identity, + } + } + + /// Returns the `ControllerCap` specified in this action. + public fun receive( + self: &mut Action, + identity: &mut UID, + cap: Receiving + ): ControllerCap { + assert!(identity.to_address() == self.borrow().identity, EInvalidIdentityUID); + assert!(cap.receiving_object_id() == self.borrow().controller_cap, EControllerCapMismatch); + + controller::receive(identity, cap) + } + + /// Consumes a `ControllerExecution` action by returning the borrowed `ControllerCap` + /// to the corresponding `Identity`. + public fun put_back( + action: Action, + cap: ControllerCap, + ) { + let ControllerExecution { identity, controller_cap } = action.unwrap(); + assert!(object::id(&cap) == controller_cap, EControllerCapMismatch); + + cap.transfer(identity); + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/delete.move b/identity_iota_core/packages/iota_identity/sources/proposals/delete.move new file mode 100644 index 0000000000..9cf908aaac --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/delete.move @@ -0,0 +1,11 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::delete_proposal { + public struct Delete has store, copy, drop {} + + public fun new(): Delete { + Delete {} + } +} + diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move b/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move new file mode 100644 index 0000000000..01f4153487 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/transfer.move @@ -0,0 +1,59 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::transfer_proposal { + use iota_identity::multicontroller::{Multicontroller, Action}; + use iota_identity::controller::DelegationToken; + use iota::transfer::Receiving; + + const EDifferentLength: u64 = 0; + const EUnsentAssets: u64 = 1; + const EInvalidObject: u64 = 2; + + public struct Send has store, drop { + objects: vector, + recipients: vector
, + } + + public fun propose_send( + multi: &mut Multicontroller, + cap: &DelegationToken, + expiration: Option, + objects: vector, + recipients: vector
, + ctx: &mut TxContext, + ): ID { + assert!(objects.length() == recipients.length(), EDifferentLength); + let action = Send { objects, recipients }; + + multi.create_proposal(cap, action,expiration, ctx) + } + + public fun send( + action: &mut Action, + controllee: &mut UID, + received: Receiving, + ) { + let send_action = action.borrow_mut(); + let object_id = received.receiving_object_id(); + let (object_exists, object_idx) = send_action.objects.index_of(&object_id); + // Check that the received object is among the objects that are actually supposed to be sent. + assert!(object_exists, EInvalidObject); + + let object = transfer::public_receive(controllee, received); + // Get the corresponding recipient. + let recipient = send_action.recipients.swap_remove(object_idx); + + transfer::public_transfer(object, recipient); + // Update the list of objects that have not been sent yet. + send_action.objects.swap_remove(object_idx); + } + + public fun complete_send(action: Action) { + let Send { objects, recipients } = action.unpack_action(); + assert!(recipients.is_empty() && objects.is_empty(), EUnsentAssets); + + recipients.destroy_empty(); + objects.destroy_empty(); + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/upgrade.move b/identity_iota_core/packages/iota_identity/sources/proposals/upgrade.move new file mode 100644 index 0000000000..0f7984a569 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/upgrade.move @@ -0,0 +1,12 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::upgrade_proposal { + /// Proposal's action used to upgrade an `Identity` to the package's current version. + public struct Upgrade has store, copy, drop {} + + /// Creates a new `Upgrade` action. + public fun new(): Upgrade { + Upgrade {} + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/proposals/value.move b/identity_iota_core/packages/iota_identity/sources/proposals/value.move new file mode 100644 index 0000000000..fb505c269b --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/proposals/value.move @@ -0,0 +1,39 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::update_value_proposal { + use iota_identity::multicontroller::Multicontroller; + use iota_identity::controller::DelegationToken; + + public struct UpdateValue has store, drop { + new_value: V, + } + + public fun propose_update( + multi: &mut Multicontroller, + cap: &DelegationToken, + new_value: V, + expiration: Option, + ctx: &mut TxContext, + ): ID { + let update_action = UpdateValue { new_value }; + multi.create_proposal(cap, update_action, expiration, ctx) + } + + public fun execute_update( + multi: &mut Multicontroller, + cap: &DelegationToken, + proposal_id: ID, + ctx: &mut TxContext, + ) { + let action = multi.execute_proposal(cap, proposal_id, ctx); + let UpdateValue { new_value } = action.unpack_action(); + + multi.set_controlled_value(new_value) + } + + public(package) fun into_inner(self: UpdateValue): V { + let UpdateValue { new_value } = self; + new_value + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/public_vc.move b/identity_iota_core/packages/iota_identity/sources/public_vc.move new file mode 100644 index 0000000000..ffe6138bbf --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/public_vc.move @@ -0,0 +1,20 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::public_vc { + public struct PublicVc has store { + data: vector, + } + + public fun new(data: vector): PublicVc { + PublicVc { data } + } + + public fun data(self: &PublicVc): &vector { + &self.data + } + + public fun set_data(self: &mut PublicVc, data: vector) { + self.data = data + } +} \ No newline at end of file diff --git a/identity_iota_core/packages/iota_identity/sources/utils.move b/identity_iota_core/packages/iota_identity/sources/utils.move new file mode 100644 index 0000000000..a7718a44a4 --- /dev/null +++ b/identity_iota_core/packages/iota_identity/sources/utils.move @@ -0,0 +1,35 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +module iota_identity::utils { + use iota::vec_map::{Self, VecMap}; + + const ELengthMismatch: u64 = 0; + + public fun vec_map_from_keys_values( + mut keys: vector, + mut values: vector, + ): VecMap { + assert!(keys.length() == values.length(), ELengthMismatch); + + let mut map = vec_map::empty(); + while (!keys.is_empty()) { + let key = keys.swap_remove(0); + let value = values.swap_remove(0); + map.insert(key, value); + }; + keys.destroy_empty(); + values.destroy_empty(); + + map + } + + #[test] + fun from_keys_values_works() { + let addresses = vector[@0x1, @0x2]; + let vps = vector[1, 1]; + + let map = vec_map_from_keys_values(addresses, vps); + assert!(map.size() == 2, 0); + } +} \ No newline at end of file diff --git a/identity_iota_core/scripts/publish_identity_package.sh b/identity_iota_core/scripts/publish_identity_package.sh new file mode 100755 index 0000000000..1886451d74 --- /dev/null +++ b/identity_iota_core/scripts/publish_identity_package.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Copyright 2020-2024 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +script_dir=$(cd "$(dirname $0)" && pwd) +package_dir=$script_dir/../packages/iota_identity + +# echo "publishing package from $package_dir" +RESPONSE=$(iota client publish --with-unpublished-dependencies --skip-dependency-verification --silence-warnings --json --gas-budget 500000000 $package_dir) +{ # try + PACKAGE_ID=$(echo $RESPONSE | jq --raw-output '.objectChanges[] | select(.type | contains("published")) | .packageId') +} || { # catch + echo $RESPONSE +} + +export IOTA_IDENTITY_PKG_ID=$PACKAGE_ID +echo "${IOTA_IDENTITY_PKG_ID}" diff --git a/identity_iota_core/src/client/identity_client.rs b/identity_iota_core/src/client/identity_client.rs deleted file mode 100644 index 34df1fd5f0..0000000000 --- a/identity_iota_core/src/client/identity_client.rs +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -#[cfg(feature = "test")] -use iota_sdk::client::Client; - -use crate::block::address::Address; -use crate::block::output::feature::SenderFeature; -use crate::block::output::unlock_condition::GovernorAddressUnlockCondition; -use crate::block::output::unlock_condition::StateControllerAddressUnlockCondition; -use crate::block::output::AliasId; -use crate::block::output::AliasOutput; -use crate::block::output::AliasOutputBuilder; -use crate::block::output::Feature; -use crate::block::output::OutputId; -use crate::block::output::RentStructure; -use crate::block::output::UnlockCondition; -use crate::block::protocol::ProtocolParameters; -use crate::Error; -use crate::IotaDID; -use crate::IotaDocument; -use crate::NetworkName; -use crate::Result; - -/// Helper functions necessary for the [`IotaIdentityClientExt`] trait. -#[cfg_attr(feature = "send-sync-client-ext", async_trait::async_trait)] -#[cfg_attr(not(feature = "send-sync-client-ext"), async_trait::async_trait(?Send))] -pub trait IotaIdentityClient { - /// Resolve an Alias identifier, returning its latest [`OutputId`] and [`AliasOutput`]. - async fn get_alias_output(&self, alias_id: AliasId) -> Result<(OutputId, AliasOutput)>; - /// Get the protocol parameters of the node we are trying to connect to. - async fn get_protocol_parameters(&self) -> Result; -} - -/// An extension trait that provides helper functions for publication -/// and resolution of DID documents in Alias Outputs. -/// -/// This trait is not intended to be implemented directly, a blanket implementation is -/// provided for [`IotaIdentityClient`] implementers. -#[cfg_attr(feature = "send-sync-client-ext", async_trait::async_trait)] -#[cfg_attr(not(feature = "send-sync-client-ext"), async_trait::async_trait(?Send))] -pub trait IotaIdentityClientExt: IotaIdentityClient { - /// Create a DID with a new Alias Output containing the given `document`. - /// - /// The `address` will be set as the state controller and governor unlock conditions. - /// The minimum required token deposit amount will be set according to the given - /// `rent_structure`, which will be fetched from the node if not provided. - /// The returned Alias Output can be further customised before publication, if desired. - /// - /// NOTE: This does *not* publish the Alias Output. - /// - /// # Errors - /// - /// - [`Error::DIDUpdateError`] when retrieving the `RentStructure` fails. - /// - [`Error::AliasOutputBuildError`] when building the Alias Output fails. - async fn new_did_output( - &self, - address: Address, - document: IotaDocument, - rent_structure: Option, - ) -> Result { - let rent_structure: RentStructure = if let Some(rent) = rent_structure { - rent - } else { - self.get_rent_structure().await? - }; - - AliasOutputBuilder::new_with_minimum_storage_deposit(rent_structure, AliasId::null()) - .with_state_index(0) - .with_foundry_counter(0) - .with_state_metadata(document.pack()?) - .add_feature(Feature::Sender(SenderFeature::new(address))) - .add_unlock_condition(UnlockCondition::StateControllerAddress( - StateControllerAddressUnlockCondition::new(address), - )) - .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new( - address, - ))) - .finish() - .map_err(Error::AliasOutputBuildError) - } - - /// Fetches the associated Alias Output and updates it with `document` in its state metadata. - /// The storage deposit on the output is left unchanged. If the size of the document increased, - /// the amount should be increased manually. - /// - /// NOTE: This does *not* publish the updated Alias Output. - /// - /// # Errors - /// - /// Returns `Err` when failing to resolve the DID contained in `document`. - async fn update_did_output(&self, document: IotaDocument) -> Result { - let id: AliasId = AliasId::from(document.id()); - let (_, alias_output) = self.get_alias_output(id).await?; - - let mut alias_output_builder: AliasOutputBuilder = AliasOutputBuilder::from(&alias_output) - .with_state_index(alias_output.state_index() + 1) - .with_state_metadata(document.pack()?); - - if alias_output.alias_id().is_null() { - alias_output_builder = alias_output_builder.with_alias_id(id); - } - - alias_output_builder.finish().map_err(Error::AliasOutputBuildError) - } - - /// Removes the DID document from the state metadata of its Alias Output, - /// effectively deactivating it. The storage deposit on the output is left unchanged, - /// and should be reallocated manually. - /// - /// Deactivating does not destroy the output. Hence, it can be re-activated by publishing - /// an update containing a DID document. - /// - /// NOTE: this does *not* publish the updated Alias Output. - /// - /// # Errors - /// - /// Returns `Err` when failing to resolve the `did`. - async fn deactivate_did_output(&self, did: &IotaDID) -> Result { - let alias_id: AliasId = AliasId::from(did); - let (_, alias_output) = self.get_alias_output(alias_id).await?; - - let mut alias_output_builder: AliasOutputBuilder = AliasOutputBuilder::from(&alias_output) - .with_state_index(alias_output.state_index() + 1) - .with_state_metadata(Vec::new()); - - if alias_output.alias_id().is_null() { - alias_output_builder = alias_output_builder.with_alias_id(alias_id); - } - - alias_output_builder.finish().map_err(Error::AliasOutputBuildError) - } - - /// Resolve a [`IotaDocument`]. Returns an empty, deactivated document if the state metadata - /// of the Alias Output is empty. - /// - /// # Errors - /// - /// - [`NetworkMismatch`](Error::NetworkMismatch) if the network of the DID and client differ. - /// - [`NotFound`](iota_sdk::client::Error::NoOutput) if the associated Alias Output was not found. - async fn resolve_did(&self, did: &IotaDID) -> Result { - validate_network(self, did).await?; - - let id: AliasId = AliasId::from(did); - let (_, alias_output) = self.get_alias_output(id).await?; - IotaDocument::unpack_from_output(did, &alias_output, true) - } - - /// Fetches the [`AliasOutput`] associated with the given DID. - /// - /// # Errors - /// - /// - [`NetworkMismatch`](Error::NetworkMismatch) if the network of the DID and client differ. - /// - [`NotFound`](iota_sdk::client::Error::NoOutput) if the associated Alias Output was not found. - async fn resolve_did_output(&self, did: &IotaDID) -> Result { - validate_network(self, did).await?; - - let id: AliasId = AliasId::from(did); - self.get_alias_output(id).await.map(|(_, alias_output)| alias_output) - } - - /// Returns the network name of the client, which is the - /// Bech32 human-readable part (HRP) of the network. - /// - /// E.g. "iota", "atoi", "smr", "rms". - async fn network_name(&self) -> Result { - self.get_network_hrp().await.and_then(NetworkName::try_from) - } - - /// Return the rent structure of the network, indicating the byte costs for outputs. - async fn get_rent_structure(&self) -> Result { - self - .get_protocol_parameters() - .await - .map(|parameters| *parameters.rent_structure()) - } - - /// Gets the token supply of the node we're connecting to. - async fn get_token_supply(&self) -> Result { - self - .get_protocol_parameters() - .await - .map(|parameters| parameters.token_supply()) - } - - /// Return the Bech32 human-readable part (HRP) of the network. - /// - /// E.g. "iota", "atoi", "smr", "rms". - async fn get_network_hrp(&self) -> Result { - self - .get_protocol_parameters() - .await - .map(|parameters| parameters.bech32_hrp().to_string()) - } -} - -#[cfg(not(feature = "test"))] -impl IotaIdentityClientExt for T where T: IotaIdentityClient {} -#[cfg(feature = "test")] -impl IotaIdentityClientExt for Client {} - -pub(super) async fn validate_network(client: &T, did: &IotaDID) -> Result<()> -where - T: IotaIdentityClient + ?Sized, -{ - let network_hrp: String = client - .get_protocol_parameters() - .await - .map(|parameters| parameters.bech32_hrp().to_string())?; - if did.network_str() != network_hrp.as_str() { - return Err(Error::NetworkMismatch { - expected: did.network_str().to_owned(), - actual: network_hrp, - }); - }; - Ok(()) -} diff --git a/identity_iota_core/src/client/iota_client.rs b/identity_iota_core/src/client/iota_client.rs deleted file mode 100644 index 8696fdf5e9..0000000000 --- a/identity_iota_core/src/client/iota_client.rs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::ops::Deref; - -use iota_sdk::client::api::input_selection::Burn; -use iota_sdk::client::secret::SecretManager; -use iota_sdk::client::Client; -use iota_sdk::types::block::protocol::ProtocolParameters; - -use crate::block::address::Address; -use crate::block::output::unlock_condition::AddressUnlockCondition; -use crate::block::output::AliasId; -use crate::block::output::AliasOutput; -use crate::block::output::BasicOutputBuilder; -use crate::block::output::Output; -use crate::block::output::OutputId; -use crate::block::output::UnlockCondition; -use crate::block::Block; -use crate::client::identity_client::validate_network; -use crate::error::Result; -use crate::Error; -use crate::IotaDID; -use crate::IotaDocument; -use crate::IotaIdentityClient; -use crate::IotaIdentityClientExt; -use crate::NetworkName; - -/// An extension trait for [`Client`] that provides helper functions for publication -/// and deletion of DID documents in Alias Outputs. -#[cfg_attr(feature = "send-sync-client-ext", async_trait::async_trait)] -#[cfg_attr(not(feature = "send-sync-client-ext"), async_trait::async_trait(?Send))] -pub trait IotaClientExt: IotaIdentityClient { - /// Publish the given `alias_output` with the provided `secret_manager`, and returns - /// the DID document extracted from the published block. - /// - /// Note that only the state controller of an Alias Output is allowed to update its state. - /// This will attempt to move tokens to or from the state controller address to match - /// the storage deposit amount specified on `alias_output`. - /// - /// This method modifies the on-ledger state. - async fn publish_did_output(&self, secret_manager: &SecretManager, alias_output: AliasOutput) - -> Result; - - /// Destroy the Alias Output containing the given `did`, sending its tokens to a new Basic Output - /// unlockable by `address`. - /// - /// Note that only the governor of an Alias Output is allowed to destroy it. - /// - /// # WARNING - /// - /// This destroys the Alias Output and DID document, rendering them permanently unrecoverable. - async fn delete_did_output(&self, secret_manager: &SecretManager, address: Address, did: &IotaDID) -> Result<()>; -} - -#[cfg_attr(feature = "send-sync-client-ext", async_trait::async_trait)] -#[cfg_attr(not(feature = "send-sync-client-ext"), async_trait::async_trait(?Send))] -impl IotaClientExt for Client { - async fn publish_did_output( - &self, - secret_manager: &SecretManager, - alias_output: AliasOutput, - ) -> Result { - let block: Block = publish_output(self, secret_manager, alias_output) - .await - .map_err(|err| Error::DIDUpdateError("publish_did_output: publish failed", Some(Box::new(err))))?; - let network: NetworkName = self.network_name().await?; - - IotaDocument::unpack_from_block(&network, &block)? - .into_iter() - .next() - .ok_or(Error::DIDUpdateError( - "publish_did_output: no document found in published block", - None, - )) - } - - async fn delete_did_output(&self, secret_manager: &SecretManager, address: Address, did: &IotaDID) -> Result<()> { - validate_network(self, did).await?; - - let alias_id: AliasId = AliasId::from(did); - let (output_id, alias_output) = self.get_alias_output(alias_id).await?; - - let basic_output = BasicOutputBuilder::new_with_amount(alias_output.amount()) - .with_native_tokens(alias_output.native_tokens().clone()) - .add_unlock_condition(UnlockCondition::Address(AddressUnlockCondition::new(address))) - .finish_output(self.deref().get_token_supply().await.map_err(Error::TokenSupplyError)?) - .map_err(Error::BasicOutputBuildError)?; - - let block: Block = self - .build_block() - .with_secret_manager(secret_manager) - .with_input(output_id.into()) - .map_err(|err| Error::DIDUpdateError("delete_did_output: invalid block input", Some(Box::new(err))))? - .with_outputs(vec![basic_output]) - .map_err(|err| Error::DIDUpdateError("delete_did_output: invalid block output", Some(Box::new(err))))? - .with_burn(Burn::new().add_alias(alias_id)) - .finish() - .await - .map_err(|err| Error::DIDUpdateError("delete_did_output: publish failed", Some(Box::new(err))))?; - let _ = self - .retry_until_included(&block.id(), None, None) - .await - .map_err(|err| { - Error::DIDUpdateError( - "delete_did_output: publish retry failed or timed-out", - Some(Box::new(err)), - ) - })?; - - Ok(()) - } -} - -#[cfg_attr(feature = "send-sync-client-ext", async_trait::async_trait)] -#[cfg_attr(not(feature = "send-sync-client-ext"), async_trait::async_trait(?Send))] -impl IotaIdentityClient for Client { - async fn get_protocol_parameters(&self) -> Result { - self - .deref() - .get_protocol_parameters() - .await - .map_err(Error::ProtocolParametersError) - } - - async fn get_alias_output(&self, id: AliasId) -> Result<(OutputId, AliasOutput)> { - let output_id: OutputId = self.alias_output_id(id).await.map_err(Error::DIDResolutionError)?; - let output: Output = self - .get_output(&output_id) - .await - .map_err(Error::DIDResolutionError)? - .into_output(); - - if let Output::Alias(alias_output) = output { - Ok((output_id, alias_output)) - } else { - Err(Error::NotAnAliasOutput(output_id)) - } - } -} - -/// Publishes an `alias_output`. -/// Returns the block that the output was included in. -async fn publish_output( - client: &Client, - secret_manager: &SecretManager, - alias_output: AliasOutput, -) -> iota_sdk::client::error::Result { - let block: Block = client - .build_block() - .with_secret_manager(secret_manager) - .with_outputs(vec![alias_output.into()])? - .finish() - .await?; - - let _ = client.retry_until_included(&block.id(), None, None).await?; - - Ok(block) -} diff --git a/identity_iota_core/src/client/mod.rs b/identity_iota_core/src/client/mod.rs deleted file mode 100644 index b1cfb4b17f..0000000000 --- a/identity_iota_core/src/client/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -pub use identity_client::IotaIdentityClient; -pub use identity_client::IotaIdentityClientExt; - -#[cfg(feature = "iota-client")] -pub use self::iota_client::IotaClientExt; - -mod identity_client; -#[cfg(feature = "iota-client")] -mod iota_client; diff --git a/identity_iota_core/src/did/iota_did.rs b/identity_iota_core/src/did/iota_did.rs index 2dfbf5b1a8..14de0f5e40 100644 --- a/identity_iota_core/src/did/iota_did.rs +++ b/identity_iota_core/src/did/iota_did.rs @@ -39,12 +39,14 @@ impl IotaDID { pub const METHOD: &'static str = "iota"; /// The default network name (`"iota"`). + // TODO: replace this with main net chain ID + // as soon as IOTA rebased lands on mainnet. pub const DEFAULT_NETWORK: &'static str = "iota"; /// The tag of the placeholder DID. pub const PLACEHOLDER_TAG: &'static str = "0x0000000000000000000000000000000000000000000000000000000000000000"; - /// The length of an Alias ID, which is a BLAKE2b-256 hash (32-bytes). + /// The length of an identity's object id, which is a BLAKE2b-256 hash (32-bytes). pub(crate) const TAG_BYTES_LEN: usize = 32; /// Convert a `CoreDID` reference to an `IotaDID` reference without checking the referenced value. @@ -84,10 +86,9 @@ impl IotaDID { Self::parse(did).expect("DIDs constructed with new should be valid") } - /// Constructs a new [`IotaDID`] from a hex representation of an Alias Id and the given - /// `network_name`. - pub fn from_alias_id(alias_id: &str, network_name: &NetworkName) -> Self { - let did: String = format!("did:{}:{}:{}", Self::METHOD, network_name, alias_id); + /// Constructs a new [`IotaDID`] from an identity's object id and the given `network_name`. + pub fn from_object_id(object_id: &str, network_name: &NetworkName) -> Self { + let did: String = format!("did:{}:{}:{}", Self::METHOD, network_name, object_id); Self::parse(did).expect("DIDs constructed with new should be valid") } @@ -151,7 +152,7 @@ impl IotaDID { Self::denormalized_components(self.method_id()).0 } - /// Returns the tag of the `DID`, which is a hex-encoded Alias ID. + /// Returns the tag of the `DID`, which is an identity's object id. pub fn tag_str(&self) -> &str { Self::denormalized_components(self.method_id()).1 } @@ -326,21 +327,6 @@ impl KeyComparable for IotaDID { } } -#[cfg(feature = "client")] -mod __iota_did_client { - use crate::block::output::AliasId; - use crate::IotaDID; - - impl From<&IotaDID> for AliasId { - /// Creates an [`AliasId`] from the DID tag. - fn from(did: &IotaDID) -> Self { - let tag_bytes: [u8; IotaDID::TAG_BYTES_LEN] = prefix_hex::decode(did.tag_str()) - .expect("being able to successfully decode the tag should be checked during DID creation"); - AliasId::new(tag_bytes) - } - } -} - #[cfg(test)] mod tests { use identity_did::DIDUrl; @@ -354,14 +340,11 @@ mod tests { // Reusable constants and statics // =========================================================================================================================== - // obtained AliasID from a valid OutputID string - // output_id copied from https://github.com/iotaledger/bee/blob/30cab4f02e9f5d72ffe137fd9eb09723b4f0fdb6/bee-block/tests/output_id.rs - // value of AliasID computed from AliasId::from(OutputId).to_string() - const VALID_ALIAS_ID_STR: &str = "0xf29dd16310c2100fd1bf568b345fb1cc14d71caa3bd9b5ad735d2bd6d455ca3b"; + const VALID_OBJECT_ID_STR: &str = "0xf29dd16310c2100fd1bf568b345fb1cc14d71caa3bd9b5ad735d2bd6d455ca3b"; - const LEN_VALID_ALIAS_STR: usize = VALID_ALIAS_ID_STR.len(); + const LEN_VALID_OBJECT_ID_STR: usize = VALID_OBJECT_ID_STR.len(); - static VALID_IOTA_DID_STRING: Lazy = Lazy::new(|| format!("did:{}:{}", IotaDID::METHOD, VALID_ALIAS_ID_STR)); + static VALID_IOTA_DID_STRING: Lazy = Lazy::new(|| format!("did:{}:{}", IotaDID::METHOD, VALID_OBJECT_ID_STR)); // Rules are: at least one character, at most six characters and may only contain digits and/or lowercase ascii // characters. @@ -387,7 +370,7 @@ mod tests { let valid_strings: Vec = VALID_NETWORK_NAMES .iter() .flat_map(|network| { - [VALID_ALIAS_ID_STR, IotaDID::PLACEHOLDER_TAG] + [VALID_OBJECT_ID_STR, IotaDID::PLACEHOLDER_TAG] .iter() .map(move |tag| network_tag_to_did(network, tag)) }) @@ -450,7 +433,16 @@ mod tests { let mut check_network_executed: bool = false; const INVALID_NETWORK_NAMES: [&str; 10] = [ - "Main", "fOo", "deV", "féta", "", " ", "foo ", " foo", "1234567", "foobar0", + "Main", + "fOo", + "deV", + "féta", + "", + " ", + "foo ", + " foo", + "123456789", + "foobar123", ]; for network_name in INVALID_NETWORK_NAMES { let did_string: String = format!("did:method:{network_name}:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"); @@ -481,9 +473,9 @@ mod tests { } // Should also work for DID's of the form: did::: - let did_other_string: String = format!("did:method:{VALID_ALIAS_ID_STR}"); - let did_other_with_network: String = format!("did:method:test:{VALID_ALIAS_ID_STR}"); + // nothing/normalized)>: + let did_other_string: String = format!("did:method:{VALID_OBJECT_ID_STR}"); + let did_other_with_network: String = format!("did:method:test:{VALID_OBJECT_ID_STR}"); let did_other_core: CoreDID = CoreDID::parse(did_other_string).unwrap(); let did_other_with_network_core: CoreDID = CoreDID::parse(did_other_with_network).unwrap(); @@ -495,16 +487,16 @@ mod tests { fn invalid_check_tag() { let invalid_method_id_strings = [ // Too many segments - format!("did:method:main:test:{VALID_ALIAS_ID_STR}"), + format!("did:method:main:test:{VALID_OBJECT_ID_STR}"), // Tag is not prefixed - format!("did:method:{}", &VALID_ALIAS_ID_STR.strip_prefix("0x").unwrap()), + format!("did:method:{}", &VALID_OBJECT_ID_STR.strip_prefix("0x").unwrap()), // Tag is too long format!( "did:method:{}", - &VALID_ALIAS_ID_STR.chars().chain("a".chars()).collect::() + &VALID_OBJECT_ID_STR.chars().chain("a".chars()).collect::() ), // Tag is too short (omit last character) - format!("did:method:main:{}", &VALID_ALIAS_ID_STR[..65]), + format!("did:method:main:{}", &VALID_OBJECT_ID_STR[..65]), ]; for input in invalid_method_id_strings { @@ -547,10 +539,10 @@ mod tests { "did:{}:{}:{}", IotaDID::METHOD, IotaDID::DEFAULT_NETWORK, - VALID_ALIAS_ID_STR + VALID_OBJECT_ID_STR ); let expected_normalization_string_representation: String = - format!("did:{}:{}", IotaDID::METHOD, VALID_ALIAS_ID_STR); + format!("did:{}:{}", IotaDID::METHOD, VALID_OBJECT_ID_STR); assert_eq!( IotaDID::parse(did_with_default_network_string).unwrap().as_str(), @@ -567,26 +559,26 @@ mod tests { #[test] fn parse_invalid() { - let execute_assertions = |valid_alias_id: &str| { + let execute_assertions = |valid_object_id: &str| { assert!(matches!( - IotaDID::parse(format!("dod:{}:{}", IotaDID::METHOD, valid_alias_id)), + IotaDID::parse(format!("dod:{}:{}", IotaDID::METHOD, valid_object_id)), Err(DIDError::InvalidScheme) )); assert!(matches!( - IotaDID::parse(format!("did:key:{valid_alias_id}")), + IotaDID::parse(format!("did:key:{valid_object_id}")), Err(DIDError::InvalidMethodName) )); - // invalid network name (exceeded six characters) + // invalid network name (exceeded eight characters) assert!(matches!( - IotaDID::parse(format!("did:{}:1234567:{}", IotaDID::METHOD, valid_alias_id)), + IotaDID::parse(format!("did:{}:123456789:{}", IotaDID::METHOD, valid_object_id)), Err(DIDError::Other(_)) )); // invalid network name (contains non ascii character é) assert!(matches!( - IotaDID::parse(format!("did:{}:féta:{}", IotaDID::METHOD, valid_alias_id)), + IotaDID::parse(format!("did:{}:féta:{}", IotaDID::METHOD, valid_object_id)), Err(DIDError::InvalidMethodId) )); @@ -598,41 +590,19 @@ mod tests { // too many segments in method_id assert!(matches!( - IotaDID::parse(format!("did:{}:test:foo:{}", IotaDID::METHOD, valid_alias_id)), + IotaDID::parse(format!("did:{}:test:foo:{}", IotaDID::METHOD, valid_object_id)), Err(DIDError::InvalidMethodId) )); }; execute_assertions(IotaDID::PLACEHOLDER_TAG); - execute_assertions(VALID_ALIAS_ID_STR); + execute_assertions(VALID_OBJECT_ID_STR); } // =========================================================================================================================== // Test constructors with randomly generated input // =========================================================================================================================== - #[cfg(feature = "iota-client")] - fn arbitrary_alias_id() -> impl Strategy { - ( - proptest::prelude::any::<[u8; 32]>(), - iota_sdk::types::block::output::OUTPUT_INDEX_RANGE, - ) - .prop_map(|(bytes, idx)| { - let transaction_id = iota_sdk::types::block::payload::transaction::TransactionId::new(bytes); - let output_id = iota_sdk::types::block::output::OutputId::new(transaction_id, idx).unwrap(); - iota_sdk::types::block::output::AliasId::from(&output_id) - }) - } - - #[cfg(feature = "iota-client")] - proptest! { - #[test] - fn property_based_valid_parse(alias_id in arbitrary_alias_id()) { - let did: String = format!("did:{}:{}",IotaDID::METHOD, alias_id); - assert!(IotaDID::parse(did).is_ok()); - } - } - #[cfg(feature = "iota-client")] proptest! { #[test] @@ -644,27 +614,14 @@ mod tests { } } - #[cfg(feature = "iota-client")] - proptest! { - #[test] - fn property_based_alias_id_string_representation_roundtrip(alias_id in arbitrary_alias_id()) { - for network_name in VALID_NETWORK_NAMES.iter().map(|name| NetworkName::try_from(*name).unwrap()) { - assert_eq!( - iota_sdk::types::block::output::AliasId::from_str(IotaDID::new(&alias_id, &network_name).tag_str()).unwrap(), - alias_id - ); - } - } - } - - fn arbitrary_alias_id_string_replica() -> impl Strategy { - proptest::string::string_regex(&format!("0x([a-f]|[0-9]){{{}}}", (LEN_VALID_ALIAS_STR - 2))) + fn arbitrary_object_id_string_replica() -> impl Strategy { + proptest::string::string_regex(&format!("0x([a-f]|[0-9]){{{}}}", (LEN_VALID_OBJECT_ID_STR - 2))) .expect("regex should be ok") } proptest! { #[test] - fn valid_alias_id_string_replicas(tag in arbitrary_alias_id_string_replica()) { + fn valid_object_id_string_replicas(tag in arbitrary_object_id_string_replica()) { let did : String = format!("did:{}:{}", IotaDID::METHOD, tag); assert!( IotaDID::parse(did).is_ok() @@ -679,7 +636,7 @@ mod tests { if arb_string .chars() .all(|c| c.is_ascii_hexdigit() && c.is_ascii_lowercase()) - && arb_string.len() == LEN_VALID_ALIAS_STR + && arb_string.len() == LEN_VALID_OBJECT_ID_STR && arb_string.starts_with("0x") { // this means we are in the rare case of generating a valid string hence we replace the last 0 with the non @@ -730,58 +687,58 @@ mod tests { // =========================================================================================================================== #[test] fn test_network() { - let execute_assertions = |valid_alias_id: &str| { - let did: IotaDID = format!("did:{}:{}", IotaDID::METHOD, valid_alias_id).parse().unwrap(); + let execute_assertions = |valid_object_id: &str| { + let did: IotaDID = format!("did:{}:{}", IotaDID::METHOD, valid_object_id).parse().unwrap(); assert_eq!(did.network_str(), IotaDID::DEFAULT_NETWORK); - let did: IotaDID = format!("did:{}:dev:{}", IotaDID::METHOD, valid_alias_id) + let did: IotaDID = format!("did:{}:dev:{}", IotaDID::METHOD, valid_object_id) .parse() .unwrap(); assert_eq!(did.network_str(), "dev"); - let did: IotaDID = format!("did:{}:test:{}", IotaDID::METHOD, valid_alias_id) + let did: IotaDID = format!("did:{}:test:{}", IotaDID::METHOD, valid_object_id) .parse() .unwrap(); assert_eq!(did.network_str(), "test"); - let did: IotaDID = format!("did:{}:custom:{}", IotaDID::METHOD, valid_alias_id) + let did: IotaDID = format!("did:{}:custom:{}", IotaDID::METHOD, valid_object_id) .parse() .unwrap(); assert_eq!(did.network_str(), "custom"); }; execute_assertions(IotaDID::PLACEHOLDER_TAG); - execute_assertions(VALID_ALIAS_ID_STR); + execute_assertions(VALID_OBJECT_ID_STR); } #[test] fn test_tag() { - let execute_assertions = |valid_alias_id: &str| { - let did: IotaDID = format!("did:{}:{}", IotaDID::METHOD, valid_alias_id).parse().unwrap(); - assert_eq!(did.tag_str(), valid_alias_id); + let execute_assertions = |valid_object_id: &str| { + let did: IotaDID = format!("did:{}:{}", IotaDID::METHOD, valid_object_id).parse().unwrap(); + assert_eq!(did.tag_str(), valid_object_id); let did: IotaDID = format!( "did:{}:{}:{}", IotaDID::METHOD, IotaDID::DEFAULT_NETWORK, - valid_alias_id + valid_object_id ) .parse() .unwrap(); - assert_eq!(did.tag_str(), valid_alias_id); + assert_eq!(did.tag_str(), valid_object_id); - let did: IotaDID = format!("did:{}:dev:{}", IotaDID::METHOD, valid_alias_id) + let did: IotaDID = format!("did:{}:dev:{}", IotaDID::METHOD, valid_object_id) .parse() .unwrap(); - assert_eq!(did.tag_str(), valid_alias_id); + assert_eq!(did.tag_str(), valid_object_id); - let did: IotaDID = format!("did:{}:custom:{}", IotaDID::METHOD, valid_alias_id) + let did: IotaDID = format!("did:{}:custom:{}", IotaDID::METHOD, valid_object_id) .parse() .unwrap(); - assert_eq!(did.tag_str(), valid_alias_id); + assert_eq!(did.tag_str(), valid_object_id); }; execute_assertions(IotaDID::PLACEHOLDER_TAG); - execute_assertions(VALID_ALIAS_ID_STR); + execute_assertions(VALID_OBJECT_ID_STR); } // =========================================================================================================================== @@ -790,75 +747,75 @@ mod tests { #[test] fn test_parse_did_url_valid() { - let execute_assertions = |valid_alias_id: &str| { - assert!(DIDUrl::parse(format!("did:{}:{}", IotaDID::METHOD, valid_alias_id)).is_ok()); - assert!(DIDUrl::parse(format!("did:{}:{}#fragment", IotaDID::METHOD, valid_alias_id)).is_ok()); + let execute_assertions = |valid_object_id: &str| { + assert!(DIDUrl::parse(format!("did:{}:{}", IotaDID::METHOD, valid_object_id)).is_ok()); + assert!(DIDUrl::parse(format!("did:{}:{}#fragment", IotaDID::METHOD, valid_object_id)).is_ok()); assert!(DIDUrl::parse(format!( "did:{}:{}?somequery=somevalue", IotaDID::METHOD, - valid_alias_id + valid_object_id )) .is_ok()); assert!(DIDUrl::parse(format!( "did:{}:{}?somequery=somevalue#fragment", IotaDID::METHOD, - valid_alias_id + valid_object_id )) .is_ok()); - assert!(DIDUrl::parse(format!("did:{}:main:{}", IotaDID::METHOD, valid_alias_id)).is_ok()); - assert!(DIDUrl::parse(format!("did:{}:main:{}#fragment", IotaDID::METHOD, valid_alias_id)).is_ok()); + assert!(DIDUrl::parse(format!("did:{}:main:{}", IotaDID::METHOD, valid_object_id)).is_ok()); + assert!(DIDUrl::parse(format!("did:{}:main:{}#fragment", IotaDID::METHOD, valid_object_id)).is_ok()); assert!(DIDUrl::parse(format!( "did:{}:main:{}?somequery=somevalue", IotaDID::METHOD, - valid_alias_id + valid_object_id )) .is_ok()); assert!(DIDUrl::parse(format!( "did:{}:main:{}?somequery=somevalue#fragment", IotaDID::METHOD, - valid_alias_id + valid_object_id )) .is_ok()); - assert!(DIDUrl::parse(format!("did:{}:dev:{}", IotaDID::METHOD, valid_alias_id)).is_ok()); - assert!(DIDUrl::parse(format!("did:{}:dev:{}#fragment", IotaDID::METHOD, valid_alias_id)).is_ok()); + assert!(DIDUrl::parse(format!("did:{}:dev:{}", IotaDID::METHOD, valid_object_id)).is_ok()); + assert!(DIDUrl::parse(format!("did:{}:dev:{}#fragment", IotaDID::METHOD, valid_object_id)).is_ok()); assert!(DIDUrl::parse(format!( "did:{}:dev:{}?somequery=somevalue", IotaDID::METHOD, - valid_alias_id + valid_object_id )) .is_ok()); assert!(DIDUrl::parse(format!( "did:{}:dev:{}?somequery=somevalue#fragment", IotaDID::METHOD, - valid_alias_id + valid_object_id )) .is_ok()); - assert!(DIDUrl::parse(format!("did:{}:custom:{}", IotaDID::METHOD, valid_alias_id)).is_ok()); - assert!(DIDUrl::parse(format!("did:{}:custom:{}#fragment", IotaDID::METHOD, valid_alias_id)).is_ok()); + assert!(DIDUrl::parse(format!("did:{}:custom:{}", IotaDID::METHOD, valid_object_id)).is_ok()); + assert!(DIDUrl::parse(format!("did:{}:custom:{}#fragment", IotaDID::METHOD, valid_object_id)).is_ok()); assert!(DIDUrl::parse(format!( "did:{}:custom:{}?somequery=somevalue", IotaDID::METHOD, - valid_alias_id + valid_object_id )) .is_ok()); assert!(DIDUrl::parse(format!( "did:{}:custom:{}?somequery=somevalue#fragment", IotaDID::METHOD, - valid_alias_id + valid_object_id )) .is_ok()); }; execute_assertions(IotaDID::PLACEHOLDER_TAG); - execute_assertions(VALID_ALIAS_ID_STR); + execute_assertions(VALID_OBJECT_ID_STR); } #[test] fn valid_url_setters() { - let execute_assertions = |valid_alias_id: &str| { - let mut did_url: DIDUrl = IotaDID::parse(format!("did:{}:{}", IotaDID::METHOD, valid_alias_id)) + let execute_assertions = |valid_object_id: &str| { + let mut did_url: DIDUrl = IotaDID::parse(format!("did:{}:{}", IotaDID::METHOD, valid_object_id)) .unwrap() .into_url(); @@ -871,6 +828,6 @@ mod tests { assert_eq!(did_url.fragment(), Some("foo")); }; execute_assertions(IotaDID::PLACEHOLDER_TAG); - execute_assertions(VALID_ALIAS_ID_STR); + execute_assertions(VALID_OBJECT_ID_STR); } } diff --git a/identity_iota_core/src/did_resolution/did_resolution_handler.rs b/identity_iota_core/src/did_resolution/did_resolution_handler.rs new file mode 100644 index 0000000000..254ea62df8 --- /dev/null +++ b/identity_iota_core/src/did_resolution/did_resolution_handler.rs @@ -0,0 +1,36 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::rebased::client::IdentityClientReadOnly; +use crate::Error; +use crate::IotaDID; +use crate::IotaDocument; +use crate::Result; + +/// An extension trait that provides helper functions for publication +/// and resolution of DID documents in identities. +/// +/// This trait is not intended to be implemented directly, a blanket implementation is +/// provided for [`IotaIdentityClient`] implementers. +#[cfg_attr(feature = "send-sync-client-ext", async_trait::async_trait)] +#[cfg_attr(not(feature = "send-sync-client-ext"), async_trait::async_trait(?Send))] +pub trait DidResolutionHandler { + /// Resolve a [`IotaDocument`]. Returns an empty, deactivated document if the state metadata + /// of the identity is empty. + /// + /// # Errors + /// + /// - [`DID resolution failed`](Error::DIDResolutionError) if the DID could not be resolved. + async fn resolve_did(&self, did: &IotaDID) -> Result; +} + +#[cfg_attr(feature = "send-sync-client-ext", async_trait::async_trait)] +#[cfg_attr(not(feature = "send-sync-client-ext"), async_trait::async_trait(?Send))] +impl DidResolutionHandler for IdentityClientReadOnly { + async fn resolve_did(&self, did: &IotaDID) -> Result { + self + .resolve_did(did) + .await + .map_err(|err| Error::DIDResolutionError(err.to_string())) + } +} diff --git a/identity_iota_core/src/did_resolution/mod.rs b/identity_iota_core/src/did_resolution/mod.rs new file mode 100644 index 0000000000..45926c6d0b --- /dev/null +++ b/identity_iota_core/src/did_resolution/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2020-2022 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub use did_resolution_handler::DidResolutionHandler; + +mod did_resolution_handler; diff --git a/identity_iota_core/src/document/iota_document.rs b/identity_iota_core/src/document/iota_document.rs index 5c0813f28c..0434a5c906 100644 --- a/identity_iota_core/src/document/iota_document.rs +++ b/identity_iota_core/src/document/iota_document.rs @@ -382,34 +382,25 @@ impl IotaDocument { // Packing // =========================================================================== - /// Serializes the document for inclusion in an Alias Output's state metadata + /// Serializes the document storing it in an identity. /// with the default [`StateMetadataEncoding`]. pub fn pack(self) -> Result> { self.pack_with_encoding(StateMetadataEncoding::default()) } - /// Serializes the document for inclusion in an Alias Output's state metadata. + /// Serializes the document for storing it in an identity. pub fn pack_with_encoding(self, encoding: StateMetadataEncoding) -> Result> { StateMetadataDocument::from(self).pack(encoding) } } -#[cfg(feature = "client")] +#[cfg(feature = "iota-client")] mod client_document { - use iota_sdk::types::block::address::Hrp; - use iota_sdk::types::block::address::ToBech32Ext; - - use crate::block::address::Address; - use crate::block::output::AliasId; - use crate::block::output::AliasOutput; - use crate::block::output::Output; - use crate::block::output::OutputId; - use crate::block::payload::transaction::TransactionEssence; - use crate::block::payload::Payload; - use crate::block::Block; - use crate::error::Result; - use crate::Error; - use crate::NetworkName; + use identity_core::common::Timestamp; + use identity_did::DID; + use identity_iota_interaction::rpc_types::IotaObjectData; + + use crate::rebased::migration::unpack_identity_data; use super::*; @@ -418,90 +409,86 @@ mod client_document { // Unpacking // =========================================================================== - /// Deserializes the document from an Alias Output. + /// Deserializes the document from an `IotaObjectData` instance. /// /// If `allow_empty` is true, this will return an empty DID document marked as `deactivated` /// if `state_metadata` is empty. /// /// NOTE: `did` is required since it is omitted from the serialized DID Document and /// cannot be inferred from the state metadata. It also indicates the network, which is not - /// encoded in the `AliasId` alone. - pub fn unpack_from_output(did: &IotaDID, alias_output: &AliasOutput, allow_empty: bool) -> Result { - let mut document: IotaDocument = if alias_output.state_metadata().is_empty() && allow_empty { - let mut empty_document = IotaDocument::new_with_id(did.clone()); - empty_document.metadata.created = None; - empty_document.metadata.updated = None; - empty_document.metadata.deactivated = Some(true); - empty_document - } else { - StateMetadataDocument::unpack(alias_output.state_metadata()).and_then(|doc| doc.into_iota_document(did))? + /// encoded in the object id alone. + pub fn unpack_from_iota_object_data( + did: &IotaDID, + data: &IotaObjectData, + allow_empty: bool, + ) -> Result { + let unpacked = unpack_identity_data(did, data).map_err(|_| { + Error::InvalidDoc(identity_document::Error::InvalidDocument( + "could not unpack identity data from IotaObjectData", + None, + )) + })?; + let (_, multi_controller, legacy_id, created, updated, _) = match unpacked { + Some(data) => data, + None => { + return Err(Error::InvalidDoc(identity_document::Error::InvalidDocument( + "given IotaObjectData did not contain a document", + None, + ))); + } }; - - document.set_controller_and_governor_addresses(alias_output, &did.network_str().to_owned().try_into()?)?; - - Ok(document) + let did_network = did + .network_str() + .to_string() + .try_into() + .expect("did's network is a valid NetworkName"); + let legacy_did = legacy_id.map(|id| IotaDID::new(&id.into_bytes(), &did_network)); + let did_doc_bytes = multi_controller + .controlled_value() + .as_deref() + .ok_or_else(|| Error::DIDResolutionError("requested DID Document doesn't exist".to_string()))?; + let did_doc = Self::from_iota_document_data(did_doc_bytes, allow_empty, did, legacy_did, created, updated)?; + + Ok(did_doc) } - fn set_controller_and_governor_addresses( - &mut self, - alias_output: &AliasOutput, - network_name: &NetworkName, - ) -> Result<()> { - let hrp: Hrp = network_name.try_into()?; - self.metadata.governor_address = Some(alias_output.governor_address().to_bech32(hrp).to_string()); - self.metadata.state_controller_address = Some(alias_output.state_controller_address().to_bech32(hrp).to_string()); - - // Overwrite the DID Document controller. - let controller_did: Option = match alias_output.state_controller_address() { - Address::Alias(alias_address) => Some(IotaDID::new(alias_address.alias_id(), network_name)), - _ => None, + /// Parse given Bytes into a `IotaDocument`. + /// + /// Requires a valid document in `data` unless `allow_empty` is `true`, in which case + /// an empty, deactivated document is returned + /// + /// # Errors: + /// * document related parsing Errors from `StateMetadataDocument::unpack` + /// * possible parsing errors when trying to parse `created` and `updated` to a `Timestamp` + pub fn from_iota_document_data( + data: &[u8], + allow_empty: bool, + did: &IotaDID, + alternative_did: Option, + created: Timestamp, + updated: Timestamp, + ) -> Result { + // check if DID has been deactivated + let mut did_doc = if data.is_empty() && allow_empty { + // DID has been deactivated by setting controlled value empty, therefore craft an empty document + let mut empty_document = Self::new_with_id(did.clone()); + empty_document.metadata.deactivated = Some(true); + empty_document + } else { + // we have a value, therefore unpack it + StateMetadataDocument::unpack(data).and_then(|state_metadata_doc| state_metadata_doc.into_iota_document(did))? }; - if let Some(controller_did) = controller_did { - match self.core_document_mut().controller_mut() { - Some(controllers) => { - controllers.append(CoreDID::from(controller_did)); - } - None => *self.core_document_mut().controller_mut() = Some(OneOrSet::new_one(CoreDID::from(controller_did))), - } + // Set the `alsoKnownAs` property if a legacy DID is present. + if let Some(alternative_did) = alternative_did { + did_doc.also_known_as_mut().prepend(alternative_did.into_url().into()); } - Ok(()) - } + // Overwrite `created` and `updated` with given timestamps + did_doc.metadata.created = Some(created); + did_doc.metadata.updated = Some(updated); - /// Returns all DID documents of the Alias Outputs contained in the block's transaction payload - /// outputs, if any. - /// - /// Errors if any Alias Output does not contain a valid or empty DID Document. - pub fn unpack_from_block(network: &NetworkName, block: &Block) -> Result> { - let mut documents = Vec::new(); - - if let Some(Payload::Transaction(tx_payload)) = block.payload() { - let TransactionEssence::Regular(regular) = tx_payload.essence(); - - for (index, output) in regular.outputs().iter().enumerate() { - if let Output::Alias(alias_output) = output { - let alias_id = if alias_output.alias_id().is_null() { - AliasId::from( - &OutputId::new( - tx_payload.id(), - index - .try_into() - .map_err(|_| Error::OutputIdConversionError(format!("output index {index} must fit into a u16")))?, - ) - .map_err(|err| Error::OutputIdConversionError(err.to_string()))?, - ) - } else { - alias_output.alias_id().to_owned() - }; - - let did: IotaDID = IotaDID::new(&alias_id, network); - documents.push(IotaDocument::unpack_from_output(&did, alias_output, true)?); - } - } - } - - Ok(documents) + Ok(did_doc) } } } @@ -594,15 +581,6 @@ mod tests { use identity_core::convert::ToJson; use identity_did::DID; - use crate::block::address::Address; - use crate::block::address::AliasAddress; - use crate::block::output::unlock_condition::GovernorAddressUnlockCondition; - use crate::block::output::unlock_condition::StateControllerAddressUnlockCondition; - use crate::block::output::AliasId; - use crate::block::output::AliasOutput; - use crate::block::output::AliasOutputBuilder; - use crate::block::output::UnlockCondition; - use super::*; use crate::test_utils::generate_method; @@ -762,136 +740,46 @@ mod tests { assert_eq!(doc1, doc2); } - #[test] - fn test_unpack_no_external_controller() { - let document_did: IotaDID = "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - .parse() - .unwrap(); - let alias_controller: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" - .parse() - .unwrap(); - - let mut original_doc: IotaDocument = IotaDocument::new_with_id(document_did.clone()); - original_doc.set_controller([]); - - let alias_output: AliasOutput = AliasOutputBuilder::new_with_amount(1, AliasId::from(&document_did)) - .with_state_metadata(original_doc.pack().unwrap()) - .add_unlock_condition(UnlockCondition::StateControllerAddress( - StateControllerAddressUnlockCondition::new(Address::Alias(AliasAddress::new(AliasId::from(&alias_controller)))), - )) - .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new( - Address::Alias(AliasAddress::new(AliasId::from(&alias_controller))), - ))) - .finish() - .unwrap(); - - let document: IotaDocument = IotaDocument::unpack_from_output(&document_did, &alias_output, true).unwrap(); - let controllers: Vec = document.controller().cloned().collect::>(); - assert_eq!(controllers.first().unwrap(), &alias_controller); - assert_eq!(controllers.len(), 1); - } - - #[test] - fn test_unpack_with_duplicate_controller() { - let document_did: IotaDID = "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - .parse() - .unwrap(); - let alias_controller: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" - .parse() - .unwrap(); - - let mut original_doc: IotaDocument = IotaDocument::new_with_id(document_did.clone()); - original_doc.set_controller([alias_controller.clone()]); - - let alias_output: AliasOutput = AliasOutputBuilder::new_with_amount(1, AliasId::from(&document_did)) - .with_state_metadata(original_doc.pack().unwrap()) - .add_unlock_condition(UnlockCondition::StateControllerAddress( - StateControllerAddressUnlockCondition::new(Address::Alias(AliasAddress::new(AliasId::from(&alias_controller)))), - )) - .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new( - Address::Alias(AliasAddress::new(AliasId::from(&alias_controller))), - ))) - .finish() - .unwrap(); - - let document: IotaDocument = IotaDocument::unpack_from_output(&document_did, &alias_output, true).unwrap(); - let controllers: Vec = document.controller().cloned().collect::>(); - assert_eq!(controllers.first().unwrap(), &alias_controller); - assert_eq!(controllers.len(), 1); - } - - #[test] - fn test_unpack_with_external_controller() { - let document_did: IotaDID = "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - .parse() - .unwrap(); - let alias_controller: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" - .parse() - .unwrap(); - let external_controller_did: IotaDID = - "did:iota:0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" - .parse() - .unwrap(); - - let mut original_doc: IotaDocument = IotaDocument::new_with_id(document_did.clone()); - original_doc.set_controller([external_controller_did.clone()]); - - let alias_output: AliasOutput = AliasOutputBuilder::new_with_amount(1, AliasId::from(&document_did)) - .with_state_metadata(original_doc.pack().unwrap()) - .add_unlock_condition(UnlockCondition::StateControllerAddress( - StateControllerAddressUnlockCondition::new(Address::Alias(AliasAddress::new(AliasId::from(&alias_controller)))), - )) - .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new( - Address::Alias(AliasAddress::new(AliasId::from(&alias_controller))), - ))) - .finish() - .unwrap(); - - let document: IotaDocument = IotaDocument::unpack_from_output(&document_did, &alias_output, true).unwrap(); - let controllers: Vec = document.controller().cloned().collect::>(); - assert_eq!(controllers.first().unwrap(), &external_controller_did); - assert_eq!(controllers.get(1).unwrap(), &alias_controller); - assert_eq!(controllers.len(), 2); - } - #[test] fn test_unpack_empty() { - let controller_did: IotaDID = valid_did(); - // VALID: unpack empty, deactivated document. let did: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" .parse() .unwrap(); - let alias_output: AliasOutput = AliasOutputBuilder::new_with_amount(1, AliasId::from(&did)) - .add_unlock_condition(UnlockCondition::StateControllerAddress( - StateControllerAddressUnlockCondition::new(Address::Alias(AliasAddress::new(AliasId::from(&controller_did)))), - )) - .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new( - Address::Alias(AliasAddress::new(AliasId::from(&controller_did))), - ))) - .finish() - .unwrap(); - let document: IotaDocument = IotaDocument::unpack_from_output(&did, &alias_output, true).unwrap(); + let document = IotaDocument::from_iota_document_data( + &[], + true, + &did, + None, + Timestamp::from_unix(12).unwrap(), + Timestamp::from_unix(34).unwrap(), + ) + .unwrap(); assert_eq!(document.id(), &did); assert_eq!(document.metadata.deactivated, Some(true)); - // Ensure no other fields are injected. + // // Ensure no other fields are injected. let json: String = format!( - r#"{{"doc":{{"id":"{did}","controller":"{controller_did}"}},"meta":{{"deactivated":true,"governorAddress":"iota1pz424242424242424242424242424242424242424242424242425ryaqzy","stateControllerAddress":"iota1pz424242424242424242424242424242424242424242424242425ryaqzy"}}}}"# + r#"{{"doc":{{"id":"{did}"}},"meta":{{"created":"1970-01-01T00:00:12Z","updated":"1970-01-01T00:00:34Z","deactivated":true}}}}"# ); assert_eq!(document.to_json().unwrap(), json); // INVALID: reject empty document. - assert!(IotaDocument::unpack_from_output(&did, &alias_output, false).is_err()); - - // Ensure re-packing removes the controller, state controller address, and governor address. + assert!(IotaDocument::from_iota_document_data( + &[], + false, + &did, + None, + Timestamp::from_unix(12).unwrap(), + Timestamp::from_unix(34).unwrap() + ) + .is_err()); + + // Ensure re-packing keeps the controller, state controller address, and governor address as None let packed: Vec = document.pack_with_encoding(StateMetadataEncoding::Json).unwrap(); let state_metadata_document: StateMetadataDocument = StateMetadataDocument::unpack(&packed).unwrap(); let unpacked_document: IotaDocument = state_metadata_document.into_iota_document(&did).unwrap(); - assert_eq!( - unpacked_document.document.controller().unwrap().get(0).unwrap().clone(), - CoreDID::from(controller_did) - ); + assert!(unpacked_document.document.controller().is_none()); assert!(unpacked_document.metadata.state_controller_address.is_none()); assert!(unpacked_document.metadata.governor_address.is_none()); } diff --git a/identity_iota_core/src/error.rs b/identity_iota_core/src/error.rs index 2a5e16ef34..8917ef0e1f 100644 --- a/identity_iota_core/src/error.rs +++ b/identity_iota_core/src/error.rs @@ -17,25 +17,12 @@ pub enum Error { /// Caused by an invalid DID document. #[error("invalid document")] InvalidDoc(#[source] identity_document::Error), - #[cfg(feature = "iota-client")] - /// Caused by a client failure during publishing. - #[error("DID update: {0}")] - DIDUpdateError(&'static str, #[source] Option>), - #[cfg(feature = "iota-client")] /// Caused by a client failure during resolution. - #[error("DID resolution failed")] - DIDResolutionError(#[source] iota_sdk::client::error::Error), - #[cfg(feature = "iota-client")] - /// Caused by an error when building a basic output. - #[error("basic output build error")] - BasicOutputBuildError(#[source] iota_sdk::types::block::Error), + #[error("DID resolution failed; {0}")] + DIDResolutionError(String), /// Caused by an invalid network name. #[error("\"{0}\" is not a valid network name in the context of the `iota` did method")] InvalidNetworkName(String), - #[cfg(feature = "iota-client")] - /// Caused by a failure to retrieve the token supply. - #[error("unable to obtain the token supply from the client")] - TokenSupplyError(#[source] iota_sdk::client::Error), /// Caused by a mismatch of the DID's network and the network the client is connected with. #[error("unable to resolve a `{expected}` DID on network `{actual}`")] NetworkMismatch { @@ -44,10 +31,6 @@ pub enum Error { /// The network the client is connected with. actual: String, }, - #[cfg(feature = "iota-client")] - /// Caused by an error when fetching protocol parameters from a node. - #[error("could not fetch protocol parameters")] - ProtocolParametersError(#[source] iota_sdk::client::Error), /// Caused by an attempt to read state metadata that does not adhere to the IOTA DID method specification. #[error("invalid state metadata {0}")] InvalidStateMetadata(&'static str), @@ -55,14 +38,6 @@ pub enum Error { /// Caused by a failure during (un)revocation of credentials. #[error("credential revocation error")] RevocationError(#[source] identity_credential::revocation::RevocationError), - #[cfg(feature = "client")] - /// Caused by an error when building an alias output. - #[error("alias output build error")] - AliasOutputBuildError(#[source] crate::block::Error), - #[cfg(feature = "iota-client")] - /// Caused by retrieving an output that is expected to be an alias output but is not. - #[error("output with id `{0}` is not an alias output")] - NotAnAliasOutput(iota_sdk::types::block::output::OutputId), /// Caused by an error when constructing an output id. #[error("conversion to an OutputId failed: {0}")] OutputIdConversionError(String), diff --git a/identity_iota_core/src/iota_interaction_adapter.rs b/identity_iota_core/src/iota_interaction_adapter.rs new file mode 100644 index 0000000000..c0b97a4e38 --- /dev/null +++ b/identity_iota_core/src/iota_interaction_adapter.rs @@ -0,0 +1,15 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +// The following platform compile switch provides all the +// ...Adapter types from iota_interaction_rust or iota_interaction_ts +// like IotaClientAdapter, AssetMoveCallsAdapter, IdentityMoveCallsAdapter, +// TransactionBuilderAdapter, MigrationMoveCallsAdapter, ... and so on + +cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + pub(crate) use iota_interaction_ts::*; + } else { + pub(crate) use crate::iota_interaction_rust::*; + } +} diff --git a/identity_iota_core/src/iota_interaction_rust/asset_move_calls.rs b/identity_iota_core/src/iota_interaction_rust/asset_move_calls.rs new file mode 100644 index 0000000000..bf134d16a0 --- /dev/null +++ b/identity_iota_core/src/iota_interaction_rust/asset_move_calls.rs @@ -0,0 +1,203 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::Serialize; + +use crate::rebased::Error; +use identity_iota_interaction::ident_str; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::base_types::SequenceNumber; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::Command; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::TypeTag; +use identity_iota_interaction::AssetMoveCalls; +use identity_iota_interaction::MoveType; +use identity_iota_interaction::ProgrammableTransactionBcs; +use identity_iota_interaction::TypedValue; +use iota_sdk::types::transaction::Argument; +use iota_sdk::types::transaction::ProgrammableMoveCall; + +fn try_to_argument( + content: &T, + ptb: &mut ProgrammableTransactionBuilder, + package: ObjectID, +) -> Result { + match content.get_typed_value(package) { + TypedValue::IotaVerifiableCredential(value) => { + let values = ptb + .pure(value.data()) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + Ok(ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package, + module: ident_str!("public_vc").into(), + function: ident_str!("new").into(), + type_arguments: vec![], + arguments: vec![values], + })))) + } + TypedValue::Other(value) => ptb.pure(value).map_err(|e| Error::InvalidArgument(e.to_string())), + } +} + +pub(crate) struct AssetMoveCallsRustSdk {} + +impl AssetMoveCalls for AssetMoveCallsRustSdk { + type Error = Error; + + fn new_asset( + inner: T, + mutable: bool, + transferable: bool, + deletable: bool, + package: ObjectID, + ) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let inner = try_to_argument(&inner, &mut ptb, package)?; + let mutable = ptb.pure(mutable).map_err(|e| Error::InvalidArgument(e.to_string()))?; + let transferable = ptb + .pure(transferable) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let deletable = ptb.pure(deletable).map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package, + module: ident_str!("asset").into(), + function: ident_str!("new_with_config").into(), + type_arguments: vec![T::move_type(package)], + arguments: vec![inner, mutable, transferable, deletable], + }))); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn delete(asset: ObjectRef, package: ObjectID) -> Result + where + T: MoveType, + { + let mut ptb = ProgrammableTransactionBuilder::new(); + + let asset = ptb + .obj(ObjectArg::ImmOrOwnedObject(asset)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::move_call( + package, + ident_str!("asset").into(), + ident_str!("delete").into(), + vec![T::move_type(package)], + vec![asset], + )); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn transfer( + asset: ObjectRef, + recipient: IotaAddress, + package: ObjectID, + ) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let asset = ptb + .obj(ObjectArg::ImmOrOwnedObject(asset)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let recipient = ptb.pure(recipient).map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::move_call( + package, + ident_str!("asset").into(), + ident_str!("transfer").into(), + vec![T::move_type(package)], + vec![asset, recipient], + )); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn make_tx( + proposal: (ObjectID, SequenceNumber), + cap: ObjectRef, + asset: ObjectRef, + asset_type_param: TypeTag, + package: ObjectID, + function_name: &'static str, + ) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let proposal = ptb + .obj(ObjectArg::SharedObject { + id: proposal.0, + initial_shared_version: proposal.1, + mutable: true, + }) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let cap = ptb + .obj(ObjectArg::ImmOrOwnedObject(cap)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let asset = ptb + .obj(ObjectArg::Receiving(asset)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::move_call( + package, + ident_str!("asset").into(), + ident_str!(function_name).into(), + vec![asset_type_param], + vec![proposal, cap, asset], + )); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn accept_proposal( + proposal: (ObjectID, SequenceNumber), + recipient_cap: ObjectRef, + asset: ObjectRef, + asset_type_param: TypeTag, + package: ObjectID, + ) -> Result { + Self::make_tx(proposal, recipient_cap, asset, asset_type_param, package, "accept") + } + + fn conclude_or_cancel( + proposal: (ObjectID, SequenceNumber), + sender_cap: ObjectRef, + asset: ObjectRef, + asset_type_param: TypeTag, + package: ObjectID, + ) -> Result { + Self::make_tx( + proposal, + sender_cap, + asset, + asset_type_param, + package, + "conclude_or_cancel", + ) + } + + fn update(asset: ObjectRef, new_content: T, package: ObjectID) -> Result + where + T: MoveType + Serialize, + { + let mut ptb = ProgrammableTransactionBuilder::new(); + + let asset = ptb + .obj(ObjectArg::ImmOrOwnedObject(asset)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let new_content = ptb + .pure(new_content) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::move_call( + package, + ident_str!("asset").into(), + ident_str!("set_content").into(), + vec![T::move_type(package)], + vec![asset, new_content], + )); + + Ok(bcs::to_bytes(&ptb.finish())?) + } +} diff --git a/identity_iota_core/src/iota_interaction_rust/identity_move_calls.rs b/identity_iota_core/src/iota_interaction_rust/identity_move_calls.rs new file mode 100644 index 0000000000..38e47ce871 --- /dev/null +++ b/identity_iota_core/src/iota_interaction_rust/identity_move_calls.rs @@ -0,0 +1,850 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use itertools::Itertools; + +use std::collections::HashSet; +use std::str::FromStr; + +use identity_iota_interaction::ident_str; +use identity_iota_interaction::rpc_types::IotaObjectData; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::base_types::ObjectType; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder as PrgrTxBuilder; +use identity_iota_interaction::types::transaction::Argument; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::TypeTag; +use identity_iota_interaction::types::IOTA_FRAMEWORK_PACKAGE_ID; +use identity_iota_interaction::BorrowIntentFnInternalT; +use identity_iota_interaction::ControllerIntentFnInternalT; +use identity_iota_interaction::IdentityMoveCalls; +use identity_iota_interaction::MoveType; +use identity_iota_interaction::ProgrammableTransactionBcs; +use identity_iota_interaction::TransactionBuilderT; + +use super::transaction_builder::TransactionBuilderRustSdk; +use super::utils; + +use crate::rebased::proposals::BorrowAction; +use crate::rebased::proposals::ControllerExecution; +use crate::rebased::proposals::SendAction; +use crate::rebased::rebased_err; +use crate::rebased::Error; + +struct ProposalContext { + ptb: PrgrTxBuilder, + controller_cap: Argument, + delegation_token: Argument, + borrow: Argument, + identity: Argument, + proposal_id: Argument, +} + +fn borrow_proposal_impl( + identity: OwnedObjectRef, + capability: ObjectRef, + objects: Vec, + expiration: Option, + package_id: ObjectID, +) -> anyhow::Result { + let mut ptb = PrgrTxBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; + let objects_arg = ptb.pure(objects)?; + + let proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_borrow").into(), + vec![], + vec![identity_arg, delegation_token, exp_arg, objects_arg], + ); + + Ok(ProposalContext { + ptb, + identity: identity_arg, + controller_cap: cap_arg, + delegation_token, + borrow, + proposal_id, + }) +} + +fn execute_borrow_impl>( + ptb: &mut PrgrTxBuilder, + identity: Argument, + delegation_token: Argument, + proposal_id: Argument, + objects: Vec, + intent_fn: F, + package: ObjectID, +) -> anyhow::Result<()> { + // Get the proposal's action as argument. + let borrow_action = ptb.programmable_move_call( + package, + move_core_types::ident_str!("identity").into(), + move_core_types::ident_str!("execute_proposal").into(), + vec![BorrowAction::move_type(package)], + vec![identity, delegation_token, proposal_id], + ); + + // Borrow all the objects specified in the action. + let obj_arg_map = objects + .into_iter() + .map(|obj_data| { + let obj_ref = obj_data.object_ref(); + let ObjectType::Struct(obj_type) = obj_data.object_type()? else { + unreachable!("move packages cannot be borrowed to begin with"); + }; + let recv_obj = ptb.obj(ObjectArg::Receiving(obj_ref))?; + + let obj_arg = ptb.programmable_move_call( + package, + move_core_types::ident_str!("identity").into(), + move_core_types::ident_str!("execute_borrow").into(), + vec![obj_type.into()], + vec![identity, borrow_action, recv_obj], + ); + + Ok((obj_ref.0, (obj_arg, obj_data))) + }) + .collect::>()?; + + // Apply the user-defined operation. + intent_fn(ptb, &obj_arg_map); + + // Put back all the objects. + obj_arg_map.into_values().for_each(|(obj_arg, obj_data)| { + let ObjectType::Struct(obj_type) = obj_data.object_type().expect("checked above") else { + unreachable!("move packages cannot be borrowed to begin with"); + }; + ptb.programmable_move_call( + package, + move_core_types::ident_str!("borrow_proposal").into(), + move_core_types::ident_str!("put_back").into(), + vec![obj_type.into()], + vec![borrow_action, obj_arg], + ); + }); + + // Consume the now empty borrow_action + ptb.programmable_move_call( + package, + move_core_types::ident_str!("borrow_proposal").into(), + move_core_types::ident_str!("conclude_borrow").into(), + vec![], + vec![borrow_action], + ); + + Ok(()) +} + +fn controller_execution_impl( + identity: OwnedObjectRef, + capability: ObjectRef, + controller_cap_id: ObjectID, + expiration: Option, + package_id: ObjectID, +) -> anyhow::Result { + let mut ptb = PrgrTxBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap_id = ptb.pure(controller_cap_id)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; + + let proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_controller_execution").into(), + vec![], + vec![identity_arg, delegation_token, controller_cap_id, exp_arg], + ); + + Ok(ProposalContext { + ptb, + controller_cap: cap_arg, + delegation_token, + borrow, + identity: identity_arg, + proposal_id, + }) +} + +fn execute_controller_execution_impl>( + ptb: &mut PrgrTxBuilder, + identity: Argument, + proposal_id: Argument, + delegation_token: Argument, + borrowing_controller_cap_ref: ObjectRef, + intent_fn: F, + package: ObjectID, +) -> anyhow::Result<()> { + // Get the proposal's action as argument. + let controller_execution_action = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_proposal").into(), + vec![ControllerExecution::move_type(package)], + vec![identity, delegation_token, proposal_id], + ); + + // Borrow the controller cap into this transaction. + let receiving = ptb.obj(ObjectArg::Receiving(borrowing_controller_cap_ref))?; + let borrowed_controller_cap = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("borrow_controller_cap").into(), + vec![], + vec![identity, controller_execution_action, receiving], + ); + + // Apply the user-defined operation. + intent_fn(ptb, &borrowed_controller_cap); + + // Put back the borrowed controller cap. + ptb.programmable_move_call( + package, + ident_str!("controller_proposal").into(), + ident_str!("put_back").into(), + vec![], + vec![controller_execution_action, borrowed_controller_cap], + ); + + Ok(()) +} + +fn send_proposal_impl( + identity: OwnedObjectRef, + capability: ObjectRef, + transfer_map: Vec<(ObjectID, IotaAddress)>, + expiration: Option, + package_id: ObjectID, +) -> anyhow::Result { + let mut ptb = PrgrTxBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; + let (objects, recipients) = { + let (objects, recipients): (Vec<_>, Vec<_>) = transfer_map.into_iter().unzip(); + let objects = ptb.pure(objects)?; + let recipients = ptb.pure(recipients)?; + + (objects, recipients) + }; + + let proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_send").into(), + vec![], + vec![identity_arg, delegation_token, exp_arg, objects, recipients], + ); + + Ok(ProposalContext { + ptb, + identity: identity_arg, + controller_cap: cap_arg, + delegation_token, + borrow, + proposal_id, + }) +} + +fn execute_send_impl( + ptb: &mut PrgrTxBuilder, + identity: Argument, + delegation_token: Argument, + proposal_id: Argument, + objects: Vec<(ObjectRef, TypeTag)>, + package: ObjectID, +) -> anyhow::Result<()> { + // Get the proposal's action as argument. + let send_action = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_proposal").into(), + vec![SendAction::move_type(package)], + vec![identity, delegation_token, proposal_id], + ); + + // Send each object in this send action. + // Traversing the map in reverse reduces the number of operations on the move side. + for (obj, obj_type) in objects.into_iter().rev() { + let recv_obj = ptb.obj(ObjectArg::Receiving(obj))?; + + ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_send").into(), + vec![obj_type], + vec![identity, send_action, recv_obj], + ); + } + + // Consume the now empty send_action + ptb.programmable_move_call( + package, + ident_str!("transfer_proposal").into(), + ident_str!("complete_send").into(), + vec![], + vec![send_action], + ); + + Ok(()) +} + +#[derive(Clone)] +pub(crate) struct IdentityMoveCallsRustSdk {} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl IdentityMoveCalls for IdentityMoveCallsRustSdk { + type Error = Error; + type NativeTxBuilder = PrgrTxBuilder; + + fn propose_borrow( + identity: OwnedObjectRef, + capability: ObjectRef, + objects: Vec, + expiration: Option, + package_id: ObjectID, + ) -> Result { + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + .. + } = borrow_proposal_impl(identity, capability, objects, expiration, package_id)?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn execute_borrow>( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + objects: Vec, + intent_fn: F, + package: ObjectID, + ) -> Result { + let mut internal_ptb = TransactionBuilderRustSdk::new(PrgrTxBuilder::new()); + let ptb = internal_ptb.as_native_tx_builder(); + let identity = utils::owned_ref_to_shared_object_arg(identity, ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(ptb, controller_cap, package); + let proposal_id = ptb.pure(proposal_id)?; + + execute_borrow_impl( + ptb, + identity, + delegation_token, + proposal_id, + objects, + intent_fn, + package, + )?; + + utils::put_back_delegation_token(ptb, controller_cap, delegation_token, borrow, package); + + internal_ptb.finish() + } + + fn create_and_execute_borrow>( + identity: OwnedObjectRef, + capability: ObjectRef, + objects: Vec, + intent_fn: F, + expiration: Option, + package_id: ObjectID, + ) -> anyhow::Result { + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + identity, + proposal_id, + } = borrow_proposal_impl( + identity, + capability, + objects.iter().map(|obj_data| obj_data.object_id).collect_vec(), + expiration, + package_id, + )?; + + execute_borrow_impl( + &mut ptb, + identity, + delegation_token, + proposal_id, + objects, + intent_fn, + package_id, + )?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn propose_config_change( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + expiration: Option, + threshold: Option, + controllers_to_add: I1, + controllers_to_remove: HashSet, + controllers_to_update: I2, + package: ObjectID, + ) -> Result + where + I1: IntoIterator, + I2: IntoIterator, + { + let mut ptb = PrgrTxBuilder::new(); + + let controllers_to_add = { + let (addresses, vps): (Vec, Vec) = controllers_to_add.into_iter().unzip(); + let addresses = ptb.pure(addresses).map_err(rebased_err)?; + let vps = ptb.pure(vps).map_err(rebased_err)?; + + ptb.programmable_move_call( + package, + ident_str!("utils").into(), + ident_str!("vec_map_from_keys_values").into(), + vec![TypeTag::Address, TypeTag::U64], + vec![addresses, vps], + ) + }; + let controllers_to_update = { + let (ids, vps): (Vec, Vec) = controllers_to_update.into_iter().unzip(); + let ids = ptb.pure(ids).map_err(rebased_err)?; + let vps = ptb.pure(vps).map_err(rebased_err)?; + + ptb.programmable_move_call( + package, + ident_str!("utils").into(), + ident_str!("vec_map_from_keys_values").into(), + vec![TypeTag::from_str("0x2::object::ID").expect("valid utf8"), TypeTag::U64], + vec![ids, vps], + ) + }; + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true).map_err(rebased_err)?; + let controller_cap = ptb + .obj(ObjectArg::ImmOrOwnedObject(controller_cap)) + .map_err(rebased_err)?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); + let expiration = utils::option_to_move(expiration, &mut ptb, package).map_err(rebased_err)?; + let threshold = utils::option_to_move(threshold, &mut ptb, package).map_err(rebased_err)?; + let controllers_to_remove = ptb.pure(controllers_to_remove).map_err(rebased_err)?; + + let _proposal_id = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("propose_config_change").into(), + vec![], + vec![ + identity, + delegation_token, + expiration, + threshold, + controllers_to_add, + controllers_to_remove, + controllers_to_update, + ], + ); + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn execute_config_change( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + proposal_id: ObjectID, + package: ObjectID, + ) -> Result { + let mut ptb = PrgrTxBuilder::new(); + + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true).map_err(rebased_err)?; + let controller_cap = ptb + .obj(ObjectArg::ImmOrOwnedObject(controller_cap)) + .map_err(rebased_err)?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); + let proposal_id = ptb.pure(proposal_id).map_err(rebased_err)?; + ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_config_change").into(), + vec![], + vec![identity, delegation_token, proposal_id], + ); + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn propose_controller_execution( + identity: OwnedObjectRef, + capability: ObjectRef, + controller_cap_id: ObjectID, + expiration: Option, + package_id: ObjectID, + ) -> Result { + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + .. + } = controller_execution_impl(identity, capability, controller_cap_id, expiration, package_id)?; + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn execute_controller_execution>( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + borrowing_controller_cap_ref: ObjectRef, + intent_fn: F, + package: ObjectID, + ) -> Result { + let mut ptb = PrgrTxBuilder::new(); + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); + let proposal_id = ptb.pure(proposal_id)?; + + execute_controller_execution_impl( + &mut ptb, + identity, + proposal_id, + delegation_token, + borrowing_controller_cap_ref, + intent_fn, + package, + )?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn create_and_execute_controller_execution>( + identity: OwnedObjectRef, + capability: ObjectRef, + expiration: Option, + borrowing_controller_cap_ref: ObjectRef, + intent_fn: F, + package_id: ObjectID, + ) -> Result { + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + proposal_id, + identity, + } = controller_execution_impl( + identity, + capability, + borrowing_controller_cap_ref.0, + expiration, + package_id, + )?; + + execute_controller_execution_impl( + &mut ptb, + identity, + proposal_id, + delegation_token, + borrowing_controller_cap_ref, + intent_fn, + package_id, + )?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + async fn new_identity( + did_doc: Option<&[u8]>, + package_id: ObjectID, + ) -> Result { + let mut ptb = PrgrTxBuilder::new(); + let doc_arg = utils::ptb_pure(&mut ptb, "did_doc", did_doc)?; + let clock = utils::get_clock_ref(&mut ptb); + + // Create a new identity, sending its capability to the tx's sender. + let _identity_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("new").into(), + vec![], + vec![doc_arg, clock], + ); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn new_with_controllers( + did_doc: Option<&[u8]>, + controllers: C, + threshold: u64, + package_id: ObjectID, + ) -> Result + where + C: IntoIterator, + { + let mut ptb = PrgrTxBuilder::new(); + + let controllers = { + let (ids, vps): (Vec, Vec) = controllers.into_iter().unzip(); + let ids = ptb.pure(ids).map_err(|e| Error::InvalidArgument(e.to_string()))?; + let vps = ptb.pure(vps).map_err(|e| Error::InvalidArgument(e.to_string()))?; + ptb.programmable_move_call( + package_id, + ident_str!("utils").into(), + ident_str!("vec_map_from_keys_values").into(), + vec![TypeTag::Address, TypeTag::U64], + vec![ids, vps], + ) + }; + + let controllers_that_can_delegate = ptb.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("vec_map").into(), + ident_str!("empty").into(), + vec![TypeTag::Address, TypeTag::U64], + vec![], + ); + let doc_arg = ptb.pure(did_doc).map_err(|e| Error::InvalidArgument(e.to_string()))?; + let threshold_arg = ptb.pure(threshold).map_err(|e| Error::InvalidArgument(e.to_string()))?; + let clock = utils::get_clock_ref(&mut ptb); + + // Create a new identity, sending its capabilities to the specified controllers. + let _identity_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("new_with_controllers").into(), + vec![], + vec![ + doc_arg, + controllers, + controllers_that_can_delegate, + threshold_arg, + clock, + ], + ); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn approve_proposal( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + proposal_id: ObjectID, + package: ObjectID, + ) -> Result { + let mut ptb = PrgrTxBuilder::new(); + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + let controller_cap = ptb + .obj(ObjectArg::ImmOrOwnedObject(controller_cap)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); + let proposal_id = ptb + .pure(proposal_id) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("approve_proposal").into(), + vec![T::move_type(package)], + vec![identity, delegation_token, proposal_id], + ); + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn propose_send( + identity: OwnedObjectRef, + capability: ObjectRef, + transfer_map: Vec<(ObjectID, IotaAddress)>, + expiration: Option, + package_id: ObjectID, + ) -> Result { + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + .. + } = send_proposal_impl(identity, capability, transfer_map, expiration, package_id)?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn execute_send( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + objects: Vec<(ObjectRef, TypeTag)>, + package: ObjectID, + ) -> Result { + let mut ptb = PrgrTxBuilder::new(); + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); + let proposal_id = ptb.pure(proposal_id)?; + + execute_send_impl(&mut ptb, identity, delegation_token, proposal_id, objects, package)?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn create_and_execute_send( + identity: OwnedObjectRef, + capability: ObjectRef, + transfer_map: Vec<(ObjectID, IotaAddress)>, + expiration: Option, + objects: Vec<(ObjectRef, TypeTag)>, + package: ObjectID, + ) -> anyhow::Result { + let ProposalContext { + mut ptb, + identity, + controller_cap, + delegation_token, + borrow, + proposal_id, + } = send_proposal_impl(identity, capability, transfer_map, expiration, package)?; + + execute_send_impl(&mut ptb, identity, delegation_token, proposal_id, objects, package)?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + async fn propose_update( + identity: OwnedObjectRef, + capability: ObjectRef, + did_doc: Option<&[u8]>, + expiration: Option, + package_id: ObjectID, + ) -> Result { + let mut ptb = PrgrTxBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability)).map_err(rebased_err)?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true).map_err(rebased_err)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id).map_err(rebased_err)?; + let doc_arg = ptb.pure(did_doc).map_err(rebased_err)?; + let clock = utils::get_clock_ref(&mut ptb); + + let _proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_update").into(), + vec![], + vec![identity_arg, delegation_token, doc_arg, exp_arg, clock], + ); + + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn execute_update( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + package_id: ObjectID, + ) -> Result { + let mut ptb = PrgrTxBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability)).map_err(rebased_err)?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let proposal_id = ptb.pure(proposal_id).map_err(rebased_err)?; + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true).map_err(rebased_err)?; + let clock = utils::get_clock_ref(&mut ptb); + + let _ = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("execute_update").into(), + vec![], + vec![identity_arg, delegation_token, proposal_id, clock], + ); + + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn propose_upgrade( + identity: OwnedObjectRef, + capability: ObjectRef, + expiration: Option, + package_id: ObjectID, + ) -> Result { + let mut ptb = PrgrTxBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability)).map_err(rebased_err)?; + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true).map_err(rebased_err)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id).map_err(rebased_err)?; + + let _proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_upgrade").into(), + vec![], + vec![identity_arg, cap_arg, exp_arg], + ); + + Ok(bcs::to_bytes(&ptb.finish())?) + } + + fn execute_upgrade( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + package_id: ObjectID, + ) -> Result { + let mut ptb = PrgrTxBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability)).map_err(rebased_err)?; + let proposal_id = ptb.pure(proposal_id).map_err(rebased_err)?; + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true).map_err(rebased_err)?; + + let _ = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("execute_upgrade").into(), + vec![], + vec![identity_arg, cap_arg, proposal_id], + ); + + Ok(bcs::to_bytes(&ptb.finish())?) + } +} diff --git a/identity_iota_core/src/iota_interaction_rust/iota_client_rust_sdk.rs b/identity_iota_core/src/iota_interaction_rust/iota_client_rust_sdk.rs new file mode 100644 index 0000000000..8a50c8c7d7 --- /dev/null +++ b/identity_iota_core/src/iota_interaction_rust/iota_client_rust_sdk.rs @@ -0,0 +1,594 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use std::boxed::Box; +use std::marker::Send; +use std::option::Option; +use std::result::Result; + +use secret_storage::Signer; + +use crate::rebased::Error; +use identity_iota_interaction::apis::CoinReadApi; +use identity_iota_interaction::apis::EventApi; +use identity_iota_interaction::apis::QuorumDriverApi; +use identity_iota_interaction::apis::ReadApi; +use identity_iota_interaction::error::IotaRpcResult; +use identity_iota_interaction::rpc_types::Coin; +use identity_iota_interaction::rpc_types::CoinPage; +use identity_iota_interaction::rpc_types::EventFilter; +use identity_iota_interaction::rpc_types::EventPage; +use identity_iota_interaction::rpc_types::IotaExecutionStatus; +use identity_iota_interaction::rpc_types::IotaObjectData; +use identity_iota_interaction::rpc_types::IotaObjectDataOptions; +use identity_iota_interaction::rpc_types::IotaObjectResponse; +use identity_iota_interaction::rpc_types::IotaObjectResponseQuery; +use identity_iota_interaction::rpc_types::IotaPastObjectResponse; +use identity_iota_interaction::rpc_types::IotaTransactionBlockEffects; +use identity_iota_interaction::rpc_types::IotaTransactionBlockEffectsAPI; +use identity_iota_interaction::rpc_types::IotaTransactionBlockEffectsV1; +use identity_iota_interaction::rpc_types::IotaTransactionBlockResponse; +use identity_iota_interaction::rpc_types::IotaTransactionBlockResponseOptions; +use identity_iota_interaction::rpc_types::ObjectChange; +use identity_iota_interaction::rpc_types::ObjectsPage; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::SequenceNumber; +use identity_iota_interaction::types::crypto::Signature; +use identity_iota_interaction::types::digests::TransactionDigest; +use identity_iota_interaction::types::dynamic_field::DynamicFieldName; +use identity_iota_interaction::types::event::EventID; +use identity_iota_interaction::types::quorum_driver_types::ExecuteTransactionRequestType; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::types::transaction::Transaction; +use identity_iota_interaction::types::transaction::TransactionData; +use identity_iota_interaction::CoinReadTrait; +use identity_iota_interaction::EventTrait; +use identity_iota_interaction::IotaClient; +use identity_iota_interaction::IotaClientTrait; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::IotaTransactionBlockResponseT; +use identity_iota_interaction::OptionalSync; +use identity_iota_interaction::ProgrammableTransactionBcs; +use identity_iota_interaction::QuorumDriverTrait; +use identity_iota_interaction::ReadTrait; +use identity_iota_interaction::SignatureBcs; +use identity_iota_interaction::TransactionDataBcs; + +/// The minimum balance required to execute a transaction. +pub(crate) const MINIMUM_BALANCE: u64 = 1_000_000_000; + +#[allow(unreachable_pub, dead_code)] +pub trait IotaTransactionBlockResponseAdaptedT: + IotaTransactionBlockResponseT +{ +} +impl IotaTransactionBlockResponseAdaptedT for T where + T: IotaTransactionBlockResponseT +{ +} +#[allow(unreachable_pub, dead_code)] +pub type IotaTransactionBlockResponseAdaptedTraitObj = + Box>; + +#[allow(unreachable_pub, dead_code)] +pub trait QuorumDriverApiAdaptedT: + QuorumDriverTrait +{ +} +impl QuorumDriverApiAdaptedT for T where + T: QuorumDriverTrait +{ +} +#[allow(unreachable_pub, dead_code)] +pub type QuorumDriverApiAdaptedTraitObj = + Box>; + +#[allow(unreachable_pub, dead_code)] +pub trait ReadApiAdaptedT: ReadTrait {} +impl ReadApiAdaptedT for T where T: ReadTrait {} +#[allow(unreachable_pub, dead_code)] +pub type ReadApiAdaptedTraitObj = Box>; + +#[allow(unreachable_pub, dead_code)] +pub trait CoinReadApiAdaptedT: CoinReadTrait {} +impl CoinReadApiAdaptedT for T where T: CoinReadTrait {} +#[allow(unreachable_pub, dead_code)] +pub type CoinReadApiAdaptedTraitObj = Box>; + +#[allow(unreachable_pub, dead_code)] +pub trait EventApiAdaptedT: EventTrait {} +impl EventApiAdaptedT for T where T: EventTrait {} +#[allow(unreachable_pub, dead_code)] +pub type EventApiAdaptedTraitObj = Box>; + +#[allow(unreachable_pub, dead_code)] +pub trait IotaClientAdaptedT: IotaClientTrait {} +impl IotaClientAdaptedT for T where T: IotaClientTrait {} +#[allow(unreachable_pub, dead_code)] +pub type IotaClientAdaptedTraitObj = + Box>; + +pub struct IotaTransactionBlockResponseProvider { + response: IotaTransactionBlockResponse, +} + +impl IotaTransactionBlockResponseProvider { + pub(crate) fn new(response: IotaTransactionBlockResponse) -> Self { + IotaTransactionBlockResponseProvider { response } + } +} + +impl IotaTransactionBlockResponseT for IotaTransactionBlockResponseProvider { + type Error = Error; + type NativeResponse = IotaTransactionBlockResponse; + + fn effects_is_none(&self) -> bool { + self.response.effects.is_none() + } + + fn effects_is_some(&self) -> bool { + self.response.effects.is_some() + } + + fn to_string(&self) -> String { + format!("{:?}", self.response) + } + + fn effects_execution_status(&self) -> Option { + self.response.effects.as_ref().map(|effects| effects.status().clone()) + } + + fn effects_created(&self) -> Option> { + self.response.effects.as_ref().map(|effects| effects.created().to_vec()) + } + + fn as_native_response(&self) -> &Self::NativeResponse { + &self.response + } + + fn as_mut_native_response(&mut self) -> &mut Self::NativeResponse { + &mut self.response + } + + fn clone_native_response(&self) -> Self::NativeResponse { + self.response.clone() + } + + fn digest(&self) -> Result { + Ok(self.response.digest) + } +} + +pub(crate) struct QuorumDriverAdapter<'a> { + api: &'a QuorumDriverApi, +} + +#[async_trait::async_trait()] +impl QuorumDriverTrait for QuorumDriverAdapter<'_> { + type Error = Error; + type NativeResponse = IotaTransactionBlockResponse; + + async fn execute_transaction_block( + &self, + tx_data_bcs: &TransactionDataBcs, + signatures: &[SignatureBcs], + options: Option, + request_type: Option, + ) -> IotaRpcResult { + let tx_data = bcs::from_bytes::(tx_data_bcs.as_slice())?; + let signatures_vec = signatures + .iter() + .map(|signature_bcs| bcs::from_bytes::(signature_bcs.as_slice())) + .collect::, _>>()?; + let tx = Transaction::from_data(tx_data, signatures_vec); + let response = self + .api + .execute_transaction_block(tx, options.unwrap_or_default(), request_type) + .await?; + Ok(Box::new(IotaTransactionBlockResponseProvider::new(response))) + } +} + +pub(crate) struct ReadAdapter<'a> { + api: &'a ReadApi, +} + +#[async_trait::async_trait()] +impl ReadTrait for ReadAdapter<'_> { + type Error = Error; + type NativeResponse = IotaTransactionBlockResponse; + + async fn get_chain_identifier(&self) -> Result { + self + .api + .get_chain_identifier() + .await + .map_err(|e| Error::Network("SDK get_chain_identifier() call failed".to_string(), e)) + } + + async fn get_dynamic_field_object( + &self, + parent_object_id: ObjectID, + name: DynamicFieldName, + ) -> IotaRpcResult { + self.api.get_dynamic_field_object(parent_object_id, name).await + } + + async fn get_object_with_options( + &self, + object_id: ObjectID, + options: IotaObjectDataOptions, + ) -> IotaRpcResult { + self.api.get_object_with_options(object_id, options).await + } + + async fn get_owned_objects( + &self, + address: IotaAddress, + query: Option, + cursor: Option, + limit: Option, + ) -> IotaRpcResult { + self.api.get_owned_objects(address, query, cursor, limit).await + } + + async fn get_reference_gas_price(&self) -> IotaRpcResult { + self.api.get_reference_gas_price().await + } + + async fn get_transaction_with_options( + &self, + digest: TransactionDigest, + options: IotaTransactionBlockResponseOptions, + ) -> IotaRpcResult { + let response = self.api.get_transaction_with_options(digest, options).await?; + Ok(Box::new(IotaTransactionBlockResponseProvider::new(response))) + } + + async fn try_get_parsed_past_object( + &self, + object_id: ObjectID, + version: SequenceNumber, + options: IotaObjectDataOptions, + ) -> IotaRpcResult { + self.api.try_get_parsed_past_object(object_id, version, options).await + } +} + +pub(crate) struct CoinReadAdapter<'a> { + api: &'a CoinReadApi, +} + +#[async_trait::async_trait()] +impl CoinReadTrait for CoinReadAdapter<'_> { + type Error = Error; + + async fn get_coins( + &self, + owner: IotaAddress, + coin_type: Option, + cursor: Option, + limit: Option, + ) -> IotaRpcResult { + self.api.get_coins(owner, coin_type, cursor, limit).await + } +} + +pub(crate) struct EventAdapter<'a> { + api: &'a EventApi, +} + +#[async_trait::async_trait()] +impl EventTrait for EventAdapter<'_> { + type Error = Error; + + async fn query_events( + &self, + query: EventFilter, + cursor: Option, + limit: Option, + descending_order: bool, + ) -> IotaRpcResult { + self.api.query_events(query, cursor, limit, descending_order).await + } +} + +#[derive(Clone)] +pub struct IotaClientRustSdk { + iota_client: IotaClient, +} + +#[async_trait] +impl IotaClientTrait for IotaClientRustSdk { + type Error = Error; + type NativeResponse = IotaTransactionBlockResponse; + + fn quorum_driver_api( + &self, + ) -> Box + Send + '_> { + Box::new(QuorumDriverAdapter { + api: self.iota_client.quorum_driver_api(), + }) + } + + fn read_api(&self) -> Box + Send + '_> { + Box::new(ReadAdapter { + api: self.iota_client.read_api(), + }) + } + + fn coin_read_api(&self) -> Box + Send + '_> { + Box::new(CoinReadAdapter { + api: self.iota_client.coin_read_api(), + }) + } + + fn event_api(&self) -> Box + Send + '_> { + Box::new(EventAdapter { + api: self.iota_client.event_api(), + }) + } + + async fn execute_transaction( + &self, + tx_bcs: ProgrammableTransactionBcs, + gas_budget: Option, + signer: &S, + ) -> Result + where + S: Signer + OptionalSync, + { + let tx = bcs::from_bytes::(tx_bcs.as_slice())?; + let response = self.sdk_execute_transaction(tx, gas_budget, signer).await?; + Ok(Box::new(IotaTransactionBlockResponseProvider::new(response))) + } + + async fn default_gas_budget( + &self, + sender_address: IotaAddress, + tx_bcs: &ProgrammableTransactionBcs, + ) -> Result { + let tx = bcs::from_bytes::(tx_bcs.as_slice())?; + self.sdk_default_gas_budget(sender_address, &tx).await + } + + async fn get_previous_version(&self, iod: IotaObjectData) -> Result, Error> { + // try to get digest of previous tx + // if we requested the prev tx and it isn't returned, this should be the oldest state + let prev_tx_digest = if let Some(value) = iod.previous_transaction { + value + } else { + return Ok(None); + }; + + // resolve previous tx + let prev_tx_response = self + .iota_client + .read_api() + .get_transaction_with_options( + prev_tx_digest, + IotaTransactionBlockResponseOptions::new().with_object_changes(), + ) + .await + .map_err(|err| { + Error::InvalidIdentityHistory(format!("could not get previous transaction {prev_tx_digest}; {err}")) + })?; + + // check for updated/created changes + let (created, other_changes): (Vec, _) = prev_tx_response + .clone() + .object_changes + .ok_or_else(|| { + Error::InvalidIdentityHistory(format!( + "could not find object changes for object {} in transaction {prev_tx_digest}", + iod.object_id + )) + })? + .into_iter() + .filter(|elem| iod.object_id.eq(&elem.object_id())) + .partition(|elem| matches!(elem, ObjectChange::Created { .. })); + + // previous tx contain create tx, so there is no previous version + if created.len() == 1 { + return Ok(None); + } + + let mut previous_versions: Vec = other_changes + .iter() + .filter_map(|elem| match elem { + ObjectChange::Mutated { previous_version, .. } => Some(*previous_version), + _ => None, + }) + .collect(); + + previous_versions.sort(); + + let earliest_previous = if let Some(value) = previous_versions.first() { + value + } else { + return Ok(None); // no mutations in prev tx, so no more versions can be found + }; + + let past_obj_response = self.get_past_object(iod.object_id, *earliest_previous).await?; + match past_obj_response { + IotaPastObjectResponse::VersionFound(value) => Ok(Some(value)), + _ => Err(Error::InvalidIdentityHistory(format!( + "could not find previous version, past object response: {past_obj_response:?}" + ))), + } + } + + async fn get_past_object( + &self, + object_id: ObjectID, + version: SequenceNumber, + ) -> Result { + self + .iota_client + .read_api() + .try_get_parsed_past_object(object_id, version, IotaObjectDataOptions::full_content()) + .await + .map_err(|err| { + Error::InvalidIdentityHistory(format!("could not look up object {object_id} version {version}; {err}")) + }) + } +} + +impl IotaClientRustSdk { + pub fn new(iota_client: IotaClient) -> Result { + Ok(Self { iota_client }) + } + + async fn sdk_execute_transaction>( + &self, + tx: ProgrammableTransaction, + gas_budget: Option, + signer: &S, + ) -> Result { + let public_key = signer + .public_key() + .await + .map_err(|e| Error::TransactionSigningFailed(e.to_string()))?; + let sender_address = IotaAddress::from(&public_key); + let gas_budget = match gas_budget { + Some(gas) => gas, + None => self.sdk_default_gas_budget(sender_address, &tx).await?, + }; + let tx_data = self.get_transaction_data(tx, gas_budget, sender_address).await?; + let signature = Self::sign_transaction_data(signer, &tx_data).await?; + + // execute tx + let response = self + .iota_client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(tx_data, vec![signature]), + IotaTransactionBlockResponseOptions::full_content(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .map_err(Error::TransactionExecutionFailed)?; + + if let Some(IotaTransactionBlockEffects::V1(IotaTransactionBlockEffectsV1 { + status: IotaExecutionStatus::Failure { error }, + .. + })) = &response.effects + { + Err(Error::TransactionUnexpectedResponse(error.to_string())) + } else { + Ok(response) + } + } + + async fn sdk_default_gas_budget( + &self, + sender_address: IotaAddress, + tx: &ProgrammableTransaction, + ) -> Result { + let gas_price = self + .iota_client + .read_api() + .get_reference_gas_price() + .await + .map_err(|e| Error::RpcError(e.to_string()))?; + let gas_coin = self.get_coin_for_transaction(sender_address).await?; + let tx_data = TransactionData::new_programmable( + sender_address, + vec![gas_coin.object_ref()], + tx.clone(), + 50_000_000, + gas_price, + ); + let dry_run_gas_result = self + .iota_client + .read_api() + .dry_run_transaction_block(tx_data) + .await? + .effects; + if dry_run_gas_result.status().is_err() { + let IotaExecutionStatus::Failure { error } = dry_run_gas_result.into_status() else { + unreachable!(); + }; + return Err(Error::TransactionUnexpectedResponse(error)); + } + let gas_summary = dry_run_gas_result.gas_cost_summary(); + let overhead = gas_price * 1000; + let net_used = gas_summary.net_gas_usage(); + let computation = gas_summary.computation_cost; + + let budget = overhead + (net_used.max(0) as u64).max(computation); + Ok(budget) + } + + async fn get_transaction_data( + &self, + programmable_transaction: ProgrammableTransaction, + gas_budget: u64, + sender_address: IotaAddress, + ) -> Result { + let gas_price = self + .iota_client + .read_api() + .get_reference_gas_price() + .await + .map_err(|err| Error::GasIssue(format!("could not get gas price; {err}")))?; + let coin = self.get_coin_for_transaction(sender_address).await?; + let tx_data = TransactionData::new_programmable( + sender_address, + vec![coin.object_ref()], + programmable_transaction, + gas_budget, + gas_price, + ); + + Ok(tx_data) + } + + async fn sign_transaction_data>( + signer: &S, + tx_data: &TransactionData, + ) -> Result { + signer + .sign(&bcs::to_bytes(tx_data)?) + .await + .map_err(|err| Error::TransactionSigningFailed(format!("could not sign transaction message; {err}"))) + } + + async fn get_coin_for_transaction(&self, sender_address: IotaAddress) -> Result { + const LIMIT: usize = 10; + let mut cursor = None; + + loop { + let coins = self + .iota_client + .coin_read_api() + .get_coins(sender_address, None, cursor, Some(LIMIT)) + .await?; + + let Some(coin) = coins.data.into_iter().max_by_key(|coin| coin.balance) else { + return Err(Error::GasIssue(format!( + "no coin found with minimum required balance of {} for address {}", + MINIMUM_BALANCE, sender_address + ))); + }; + + if coin.balance >= MINIMUM_BALANCE { + return Ok(coin); + } + + if !coins.has_next_page { + break; + } + + cursor = coins.next_cursor; + } + + Err(Error::GasIssue(format!( + "no coin found with minimum required balance of {} for address {}", + MINIMUM_BALANCE, sender_address + ))) + } +} diff --git a/identity_iota_core/src/iota_interaction_rust/migration_move_calls.rs b/identity_iota_core/src/iota_interaction_rust/migration_move_calls.rs new file mode 100644 index 0000000000..eaeb087cec --- /dev/null +++ b/identity_iota_core/src/iota_interaction_rust/migration_move_calls.rs @@ -0,0 +1,56 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb; + +use identity_iota_interaction::ident_str; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::IOTA_FRAMEWORK_PACKAGE_ID; +use identity_iota_interaction::MigrationMoveCalls; +use identity_iota_interaction::ProgrammableTransactionBcs; + +use crate::rebased::Error; + +use super::utils; + +pub(crate) struct MigrationMoveCallsRustSdk {} + +impl MigrationMoveCalls for MigrationMoveCallsRustSdk { + type Error = Error; + + fn migrate_did_output( + did_output: ObjectRef, + creation_timestamp: Option, + migration_registry: OwnedObjectRef, + package: ObjectID, + ) -> anyhow::Result { + let mut ptb = Ptb::new(); + let did_output = ptb.obj(ObjectArg::ImmOrOwnedObject(did_output))?; + let migration_registry = utils::owned_ref_to_shared_object_arg(migration_registry, &mut ptb, true)?; + let clock = utils::get_clock_ref(&mut ptb); + + let creation_timestamp = match creation_timestamp { + Some(timestamp) => ptb.pure(timestamp)?, + _ => ptb.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("clock").into(), + ident_str!("timestamp_ms").into(), + vec![], + vec![clock], + ), + }; + + ptb.programmable_move_call( + package, + ident_str!("migration").into(), + ident_str!("migrate_alias_output").into(), + vec![], + vec![did_output, migration_registry, creation_timestamp, clock], + ); + + Ok(bcs::to_bytes(&ptb.finish())?) + } +} diff --git a/identity_iota_core/src/iota_interaction_rust/mod.rs b/identity_iota_core/src/iota_interaction_rust/mod.rs new file mode 100644 index 0000000000..17e2980f3b --- /dev/null +++ b/identity_iota_core/src/iota_interaction_rust/mod.rs @@ -0,0 +1,44 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod asset_move_calls; +pub(crate) mod identity_move_calls; +pub(crate) mod iota_client_rust_sdk; +pub(crate) mod migration_move_calls; +pub(crate) mod transaction_builder; +mod utils; + +pub(crate) use super::rebased::Error as AdapterError; +pub(crate) use asset_move_calls::AssetMoveCallsRustSdk as AssetMoveCallsAdapter; +pub(crate) use identity_iota_interaction::rpc_types::IotaTransactionBlockResponse as NativeTransactionBlockResponse; +pub(crate) use identity_move_calls::IdentityMoveCallsRustSdk as IdentityMoveCallsAdapter; +pub(crate) use iota_client_rust_sdk::IotaClientRustSdk as IotaClientAdapter; +pub(crate) use iota_client_rust_sdk::IotaTransactionBlockResponseProvider as IotaTransactionBlockResponseAdapter; +pub(crate) use migration_move_calls::MigrationMoveCallsRustSdk as MigrationMoveCallsAdapter; +#[allow(unused_imports)] +pub(crate) use transaction_builder::TransactionBuilderRustSdk as TransactionBuilderAdapter; + +#[allow(unused_imports)] +pub(crate) use iota_client_rust_sdk::CoinReadApiAdaptedT; +#[allow(unused_imports)] +pub(crate) use iota_client_rust_sdk::CoinReadApiAdaptedTraitObj; +#[allow(unused_imports)] +pub(crate) use iota_client_rust_sdk::EventApiAdaptedT; +#[allow(unused_imports)] +pub(crate) use iota_client_rust_sdk::EventApiAdaptedTraitObj; +#[allow(unused_imports)] +pub(crate) use iota_client_rust_sdk::IotaClientAdaptedT; +#[allow(unused_imports)] +pub(crate) use iota_client_rust_sdk::IotaClientAdaptedTraitObj; +#[allow(unused_imports)] +pub(crate) use iota_client_rust_sdk::IotaTransactionBlockResponseAdaptedT; +#[allow(unused_imports)] +pub(crate) use iota_client_rust_sdk::IotaTransactionBlockResponseAdaptedTraitObj; +#[allow(unused_imports)] +pub(crate) use iota_client_rust_sdk::QuorumDriverApiAdaptedT; +#[allow(unused_imports)] +pub(crate) use iota_client_rust_sdk::QuorumDriverApiAdaptedTraitObj; +#[allow(unused_imports)] +pub(crate) use iota_client_rust_sdk::ReadApiAdaptedT; +#[allow(unused_imports)] +pub(crate) use iota_client_rust_sdk::ReadApiAdaptedTraitObj; diff --git a/identity_iota_core/src/iota_interaction_rust/transaction_builder.rs b/identity_iota_core/src/iota_interaction_rust/transaction_builder.rs new file mode 100644 index 0000000000..9854e1893a --- /dev/null +++ b/identity_iota_core/src/iota_interaction_rust/transaction_builder.rs @@ -0,0 +1,53 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; +use std::ops::DerefMut; + +use crate::rebased::Error; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::ProgrammableTransactionBcs; +use identity_iota_interaction::TransactionBuilderT; + +#[derive(Default)] +pub(crate) struct TransactionBuilderRustSdk { + pub(crate) builder: ProgrammableTransactionBuilder, +} + +impl TransactionBuilderRustSdk { + pub(crate) fn new(builder: ProgrammableTransactionBuilder) -> Self { + TransactionBuilderRustSdk { builder } + } +} + +impl TransactionBuilderT for TransactionBuilderRustSdk { + type Error = Error; + type NativeTxBuilder = ProgrammableTransactionBuilder; + + fn finish(self) -> Result { + let tx = self.builder.finish(); + Ok(bcs::to_bytes(&tx)?) + } + + fn as_native_tx_builder(&mut self) -> &mut Self::NativeTxBuilder { + &mut self.builder + } + + fn into_native_tx_builder(self) -> Self::NativeTxBuilder { + self.builder + } +} + +impl Deref for TransactionBuilderRustSdk { + type Target = ProgrammableTransactionBuilder; + + fn deref(&self) -> &Self::Target { + &self.builder + } +} + +impl DerefMut for TransactionBuilderRustSdk { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.builder + } +} diff --git a/identity_iota_core/src/iota_interaction_rust/utils.rs b/identity_iota_core/src/iota_interaction_rust/utils.rs new file mode 100644 index 0000000000..357d3647a3 --- /dev/null +++ b/identity_iota_core/src/iota_interaction_rust/utils.rs @@ -0,0 +1,122 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::rebased::Error; +use identity_iota_interaction::move_types::ident_str; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::STD_OPTION_MODULE_NAME; +use identity_iota_interaction::types::object::Owner; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb; +use identity_iota_interaction::types::transaction::Argument; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::IOTA_CLOCK_OBJECT_ID; +use identity_iota_interaction::types::IOTA_CLOCK_OBJECT_SHARED_VERSION; +use identity_iota_interaction::types::MOVE_STDLIB_PACKAGE_ID; +use identity_iota_interaction::MoveType; +use serde::Serialize; + +/// Adds a reference to the on-chain clock to `ptb`'s arguments. +pub(crate) fn get_clock_ref(ptb: &mut Ptb) -> Argument { + ptb + .obj(ObjectArg::SharedObject { + id: IOTA_CLOCK_OBJECT_ID, + initial_shared_version: IOTA_CLOCK_OBJECT_SHARED_VERSION, + mutable: false, + }) + .expect("network has a singleton clock instantiated") +} + +pub(crate) fn get_controller_delegation( + ptb: &mut Ptb, + controller_cap: Argument, + package: ObjectID, +) -> (Argument, Argument) { + let Argument::Result(idx) = ptb.programmable_move_call( + package, + ident_str!("controller").into(), + ident_str!("borrow").into(), + vec![], + vec![controller_cap], + ) else { + unreachable!("making move calls always return a result variant"); + }; + + (Argument::NestedResult(idx, 0), Argument::NestedResult(idx, 1)) +} + +pub(crate) fn put_back_delegation_token( + ptb: &mut Ptb, + controller_cap: Argument, + delegation_token: Argument, + borrow: Argument, + package: ObjectID, +) { + ptb.programmable_move_call( + package, + ident_str!("controller").into(), + ident_str!("put_back").into(), + vec![], + vec![controller_cap, delegation_token, borrow], + ); +} + +pub(crate) fn owned_ref_to_shared_object_arg( + owned_ref: OwnedObjectRef, + ptb: &mut Ptb, + mutable: bool, +) -> anyhow::Result { + let Owner::Shared { initial_shared_version } = owned_ref.owner else { + anyhow::bail!("Identity \"{}\" is not a shared object", owned_ref.object_id()); + }; + ptb.obj(ObjectArg::SharedObject { + id: owned_ref.object_id(), + initial_shared_version, + mutable, + }) +} + +pub(crate) fn option_to_move( + option: Option, + ptb: &mut Ptb, + package: ObjectID, +) -> Result { + let arg = if let Some(t) = option { + let t = ptb.pure(t)?; + ptb.programmable_move_call( + MOVE_STDLIB_PACKAGE_ID, + STD_OPTION_MODULE_NAME.into(), + ident_str!("some").into(), + vec![T::move_type(package)], + vec![t], + ) + } else { + ptb.programmable_move_call( + MOVE_STDLIB_PACKAGE_ID, + STD_OPTION_MODULE_NAME.into(), + ident_str!("none").into(), + vec![T::move_type(package)], + vec![], + ) + }; + + Ok(arg) +} + +pub(crate) fn ptb_pure(ptb: &mut Ptb, name: &str, value: T) -> Result +where + T: Serialize + core::fmt::Debug, +{ + ptb.pure(&value).map_err(|err| { + Error::InvalidArgument(format!( + r"could not serialize pure value {name} with value {value:?}; {err}" + )) + }) +} + +#[allow(dead_code)] +pub(crate) fn ptb_obj(ptb: &mut Ptb, name: &str, value: ObjectArg) -> Result { + ptb + .obj(value) + .map_err(|err| Error::InvalidArgument(format!("could not serialize object {name} {value:?}; {err}"))) +} diff --git a/identity_iota_core/src/lib.rs b/identity_iota_core/src/lib.rs index 6602fb10ba..012fe35d90 100644 --- a/identity_iota_core/src/lib.rs +++ b/identity_iota_core/src/lib.rs @@ -14,18 +14,9 @@ )] #![allow(clippy::upper_case_acronyms)] -// Re-export the `iota_types::block` module for implementer convenience. -#[cfg(any(feature = "client", feature = "iota-client"))] -pub mod block { - //! See [iota_sdk::types::block]. - - pub use iota_sdk::types::block::*; - pub use iota_sdk::types::TryFromDto; -} - -#[cfg(feature = "client")] -pub use client::*; pub use did::IotaDID; +#[cfg(feature = "iota-client")] +pub use did_resolution::DidResolutionHandler; pub use document::*; pub use network::NetworkName; pub use state_metadata::*; @@ -33,10 +24,19 @@ pub use state_metadata::*; pub use self::error::Error; pub use self::error::Result; -#[cfg(feature = "client")] -mod client; mod did; mod document; mod error; mod network; mod state_metadata; + +#[cfg(feature = "iota-client")] +mod did_resolution; +#[cfg(feature = "iota-client")] +mod iota_interaction_adapter; +#[cfg(all(feature = "iota-client", not(target_arch = "wasm32")))] +/// IOTA Rust SDK based implementation of the identity_iota_interaction interface for non wasm targets. +mod iota_interaction_rust; +#[cfg(feature = "iota-client")] +/// Contains the rebased Identity and the interaction with the IOTA Client. +pub mod rebased; diff --git a/identity_iota_core/src/network/network_name.rs b/identity_iota_core/src/network/network_name.rs index 24291fe8a5..10ae03039d 100644 --- a/identity_iota_core/src/network/network_name.rs +++ b/identity_iota_core/src/network/network_name.rs @@ -1,13 +1,12 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::borrow::Cow; - use core::convert::TryFrom; use core::fmt::Display; use core::fmt::Formatter; use core::ops::Deref; use std::fmt::Debug; +use std::str::FromStr; use serde::Deserialize; use serde::Serialize; @@ -18,21 +17,11 @@ use crate::error::Result; /// Network name compliant with the [`crate::IotaDID`] method specification. #[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] #[repr(transparent)] -pub struct NetworkName(Cow<'static, str>); +pub struct NetworkName(String); impl NetworkName { /// The maximum length of a network name. - pub const MAX_LENGTH: usize = 6; - - /// Creates a new [`NetworkName`] if the name passes validation. - pub fn try_from(name: T) -> Result - where - T: Into>, - { - let name_cow: Cow<'static, str> = name.into(); - Self::validate_network_name(&name_cow)?; - Ok(Self(name_cow)) - } + pub const MAX_LENGTH: usize = 8; /// Validates whether a string is a spec-compliant IOTA DID [`NetworkName`]. pub fn validate_network_name(name: &str) -> Result<()> { @@ -52,33 +41,34 @@ impl AsRef for NetworkName { } } -impl From for Cow<'static, str> { - fn from(network_name: NetworkName) -> Self { - network_name.0 - } -} - impl Deref for NetworkName { - type Target = Cow<'static, str>; + type Target = str; fn deref(&self) -> &Self::Target { &self.0 } } -impl TryFrom<&'static str> for NetworkName { +impl TryFrom for NetworkName { type Error = Error; - - fn try_from(name: &'static str) -> Result { - Self::try_from(Cow::Borrowed(name)) + fn try_from(value: String) -> Result { + Self::validate_network_name(&value)?; + Ok(Self(value)) } } -impl TryFrom for NetworkName { +impl<'a> TryFrom<&'a str> for NetworkName { type Error = Error; + fn try_from(value: &'a str) -> Result { + value.to_string().try_into() + } +} - fn try_from(name: String) -> Result { - Self::try_from(Cow::Owned(name)) +impl FromStr for NetworkName { + type Err = Error; + fn from_str(name: &str) -> Result { + Self::validate_network_name(name)?; + Ok(Self(name.to_string())) } } @@ -94,37 +84,18 @@ impl Display for NetworkName { } } -#[cfg(feature = "client")] -mod try_from_network_name { - use iota_sdk::types::block::address::Hrp; - - use crate::Error; - use crate::NetworkName; - use std::str::FromStr; - - impl TryFrom<&NetworkName> for Hrp { - type Error = Error; - - fn try_from(network_name: &NetworkName) -> std::result::Result { - Hrp::from_str(network_name.as_ref()) - .map_err(|err| Error::InvalidNetworkName(format!("could not convert network name to HRP: {err}"))) - } - } -} - #[cfg(test)] mod tests { use super::*; - // Rules are: at least one character, at most six characters and may only contain digits and/or lowercase ascii + // Rules are: at least one character, at most eight characters and may only contain digits and/or lowercase ascii // characters. - const VALID_NETWORK_NAMES: [&str; 12] = [ - "main", "dev", "smr", "rms", "test", "foo", "foobar", "123456", "0", "foo42", "bar123", "42foo", + const VALID_NETWORK_NAMES: &[&str] = &[ + "main", "dev", "smr", "rms", "test", "foo", "foobar", "123456", "0", "foo42", "bar123", "42foo", "1234567", + "foobar0", ]; - const INVALID_NETWORK_NAMES: [&str; 10] = [ - "Main", "fOo", "deV", "féta", "", " ", "foo ", " foo", "1234567", "foobar0", - ]; + const INVALID_NETWORK_NAMES: &[&str] = &["Main", "fOo", "deV", "féta", "", " ", "foo ", " foo"]; #[test] fn valid_validate_network_name() { diff --git a/identity_iota_core/src/rebased/assets/asset.rs b/identity_iota_core/src/rebased/assets/asset.rs new file mode 100644 index 0000000000..a560c76105 --- /dev/null +++ b/identity_iota_core/src/rebased/assets/asset.rs @@ -0,0 +1,631 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr as _; + +use crate::iota_interaction_adapter::AssetMoveCallsAdapter; +use crate::rebased::client::IdentityClient; +use crate::rebased::transaction::TransactionInternal; +use crate::rebased::transaction::TransactionOutputInternal; +use crate::rebased::Error; +use anyhow::anyhow; +use anyhow::Context; +use async_trait::async_trait; +use identity_iota_interaction::ident_str; +use identity_iota_interaction::move_types::language_storage::StructTag; +use identity_iota_interaction::rpc_types::IotaData as _; +use identity_iota_interaction::rpc_types::IotaExecutionStatus; +use identity_iota_interaction::rpc_types::IotaObjectDataOptions; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::base_types::SequenceNumber; +use identity_iota_interaction::types::id::UID; +use identity_iota_interaction::types::object::Owner; +use identity_iota_interaction::types::TypeTag; +use identity_iota_interaction::AssetMoveCalls; +use identity_iota_interaction::IotaClientTrait; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::MoveType; +use identity_iota_interaction::OptionalSync; +use secret_storage::Signer; +use serde::de::DeserializeOwned; +use serde::Deserialize; +use serde::Deserializer; +use serde::Serialize; + +/// An on-chain asset that carries information about its owned and its creator. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AuthenticatedAsset { + id: UID, + #[serde( + deserialize_with = "deserialize_inner", + bound(deserialize = "T: for<'a> Deserialize<'a>") + )] + inner: T, + owner: IotaAddress, + origin: IotaAddress, + mutable: bool, + transferable: bool, + deletable: bool, +} + +fn deserialize_inner<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: for<'a> Deserialize<'a>, +{ + use serde::de::Error as _; + + match std::any::type_name::() { + "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64" | "i128" | "isize" => { + String::deserialize(deserializer).and_then(|s| serde_json::from_str(&s).map_err(D::Error::custom)) + } + _ => T::deserialize(deserializer), + } +} + +impl AuthenticatedAsset +where + T: for<'a> Deserialize<'a>, +{ + /// Resolves an [`AuthenticatedAsset`] by its ID `id`. + pub async fn get_by_id(id: ObjectID, client: &IdentityClient) -> Result { + let res = client + .read_api() + .get_object_with_options(id, IotaObjectDataOptions::new().with_content()) + .await?; + let Some(data) = res.data else { + return Err(Error::ObjectLookup(res.error.map_or(String::new(), |e| e.to_string()))); + }; + data + .content + .ok_or_else(|| anyhow!("No content for object with ID {id}")) + .and_then(|content| content.try_into_move().context("not a Move object")) + .and_then(|obj_data| { + serde_json::from_value(obj_data.fields.to_json_value()).context("failed to deserialize move object") + }) + .map_err(|e| Error::ObjectLookup(e.to_string())) + } +} + +impl AuthenticatedAsset { + async fn object_ref(&self, client: &IdentityClient) -> Result { + client + .read_api() + .get_object_with_options(self.id(), IotaObjectDataOptions::default()) + .await? + .object_ref_if_exists() + .ok_or_else(|| Error::ObjectLookup("missing object reference in response".to_owned())) + } + + /// Returns this [`AuthenticatedAsset`]'s ID. + pub fn id(&self) -> ObjectID { + *self.id.object_id() + } + + /// Returns a reference to this [`AuthenticatedAsset`]'s content. + pub fn content(&self) -> &T { + &self.inner + } + + /// Transfers ownership of this [`AuthenticatedAsset`] to `recipient`. + /// # Notes + /// This function doesn't perform the transfer right away, but instead creates a [`Transaction`] that + /// can be executed to carry out the transfer. + /// # Failures + /// * Returns an [`Error::InvalidConfig`] if this asset is not transferable. + pub fn transfer(self, recipient: IotaAddress) -> Result, Error> { + if !self.transferable { + return Err(Error::InvalidConfig(format!( + "`AuthenticatedAsset` {} is not transferable", + self.id() + ))); + } + Ok(TransferAssetTx { asset: self, recipient }) + } + + /// Destroys this [`AuthenticatedAsset`]. + /// # Notes + /// This function doesn't delete the asset right away, but instead creates a [`Transaction`] that + /// can be executed in order to destroy the asset. + /// # Failures + /// * Returns an [`Error::InvalidConfig`] if this asset cannot be deleted. + pub fn delete(self) -> Result, Error> { + if !self.deletable { + return Err(Error::InvalidConfig(format!( + "`AuthenticatedAsset` {} cannot be deleted", + self.id() + ))); + } + + Ok(DeleteAssetTx(self)) + } + + /// Changes this [`AuthenticatedAsset`]'s content. + /// # Notes + /// This function doesn't update the asset right away, but instead creates a [`Transaction`] that + /// can be executed in order to update the asset's content. + /// # Failures + /// * Returns an [`Error::InvalidConfig`] if this asset cannot be updated. + pub fn set_content(&mut self, new_content: T) -> Result, Error> { + if !self.mutable { + return Err(Error::InvalidConfig(format!( + "`AuthenticatedAsset` {} is immutable", + self.id() + ))); + } + + Ok(UpdateContentTx { + asset: self, + new_content, + }) + } +} + +/// Builder-style struct to ease the creation of a new [`AuthenticatedAsset`]. +#[derive(Debug)] +pub struct AuthenticatedAssetBuilder { + inner: T, + mutable: bool, + transferable: bool, + deletable: bool, +} + +impl MoveType for AuthenticatedAsset { + fn move_type(package: ObjectID) -> TypeTag { + TypeTag::Struct(Box::new(StructTag { + address: package.into(), + module: ident_str!("asset").into(), + name: ident_str!("AuthenticatedAsset").into(), + type_params: vec![T::move_type(package)], + })) + } +} + +impl AuthenticatedAssetBuilder { + /// Initializes the builder with the asset's content. + pub fn new(content: T) -> Self { + Self { + inner: content, + mutable: false, + transferable: false, + deletable: false, + } + } + + /// Sets whether the new asset allows for its modification. + /// + /// By default an [`AuthenticatedAsset`] is **immutable**. + pub fn mutable(mut self, mutable: bool) -> Self { + self.mutable = mutable; + self + } + + /// Sets whether the new asset allows the transfer of its ownership. + /// + /// By default an [`AuthenticatedAsset`] **cannot** be transferred. + pub fn transferable(mut self, transferable: bool) -> Self { + self.transferable = transferable; + self + } + + /// Sets whether the new asset can be deleted. + /// + /// By default an [`AuthenticatedAsset`] **cannot** be deleted. + pub fn deletable(mut self, deletable: bool) -> Self { + self.deletable = deletable; + self + } + + /// Creates a [`Transaction`] that will create the specified [`AuthenticatedAsset`] when executed. + pub fn finish(self) -> CreateAssetTx { + CreateAssetTx(self) + } +} + +/// Proposal for the transfer of an [`AuthenticatedAsset`]'s ownership from one [`IotaAddress`] to another. +/// +/// # Detailed Workflow +/// A [`TransferProposal`] is a **shared** _Move_ object that represents a request to transfer ownership +/// of an [`AuthenticatedAsset`] to a new owner. +/// +/// When a [`TransferProposal`] is created, it will seize the asset and send a `SenderCap` token to the current asset's +/// owner and a `RecipientCap` to the specified `recipient` address. +/// `recipient` can accept the transfer by presenting its `RecipientCap` (this prevents other users from claiming the +/// asset for themselves). +/// The current owner can cancel the proposal at any time - given the transfer hasn't been concluded yet - by presenting +/// its `SenderCap`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransferProposal { + id: UID, + asset_id: ObjectID, + sender_cap_id: ObjectID, + sender_address: IotaAddress, + recipient_cap_id: ObjectID, + recipient_address: IotaAddress, + done: bool, +} + +impl MoveType for TransferProposal { + fn move_type(package: ObjectID) -> TypeTag { + TypeTag::Struct(Box::new(StructTag { + address: package.into(), + module: ident_str!("asset").into(), + name: ident_str!("TransferProposal").into(), + type_params: vec![], + })) + } +} + +impl TransferProposal { + /// Resolves a [`TransferProposal`] by its ID `id`. + pub async fn get_by_id(id: ObjectID, client: &IdentityClient) -> Result { + let res = client + .read_api() + .get_object_with_options(id, IotaObjectDataOptions::new().with_content()) + .await?; + let Some(data) = res.data else { + return Err(Error::ObjectLookup(res.error.map_or(String::new(), |e| e.to_string()))); + }; + data + .content + .ok_or_else(|| anyhow!("No content for object with ID {id}")) + .and_then(|content| content.try_into_move().context("not a Move object")) + .and_then(|obj_data| { + serde_json::from_value(obj_data.fields.to_json_value()).context("failed to deserialize move object") + }) + .map_err(|e| Error::ObjectLookup(e.to_string())) + } + + async fn get_cap(&self, cap_type: &str, client: &IdentityClient) -> Result { + let cap_tag = StructTag::from_str(&format!("{}::asset::{cap_type}", client.package_id())) + .map_err(|e| Error::ParsingFailed(e.to_string()))?; + client + .find_owned_ref(cap_tag, |obj_data| { + cap_type == "SenderCap" && self.sender_cap_id == obj_data.object_id + || cap_type == "RecipientCap" && self.recipient_cap_id == obj_data.object_id + }) + .await? + .ok_or_else(|| { + Error::MissingPermission(format!( + "no owned `{cap_type}` for transfer proposal {}", + self.id.object_id(), + )) + }) + } + + async fn asset_metadata(&self, client: &IdentityClient) -> anyhow::Result<(ObjectRef, TypeTag)> { + let res = client + .read_api() + .get_object_with_options(self.asset_id, IotaObjectDataOptions::default().with_type()) + .await?; + let asset_ref = res + .object_ref_if_exists() + .context("missing object reference in response")?; + let param_type = res + .data + .context("missing data") + .and_then(|data| data.type_.context("missing type")) + .and_then(StructTag::try_from) + .and_then(|mut tag| { + if tag.type_params.is_empty() { + anyhow::bail!("no type parameter") + } else { + Ok(tag.type_params.remove(0)) + } + })?; + + Ok((asset_ref, param_type)) + } + + async fn initial_shared_version(&self, client: &IdentityClient) -> anyhow::Result { + let owner = client + .read_api() + .get_object_with_options(*self.id.object_id(), IotaObjectDataOptions::default().with_owner()) + .await? + .owner() + .context("missing owner information")?; + match owner { + Owner::Shared { initial_shared_version } => Ok(initial_shared_version), + _ => anyhow::bail!("`TransferProposal` is not a shared object"), + } + } + + /// Accepts this [`TransferProposal`]. + /// # Warning + /// This operation only has an effects when it's invoked by this [`TransferProposal`]'s `recipient`. + pub fn accept(self) -> AcceptTransferTx { + AcceptTransferTx(self) + } + + /// Concludes or cancels this [`TransferProposal`]. + /// # Warning + /// * This operation only has an effects when it's invoked by this [`TransferProposal`]'s `sender`. + /// * Accepting a [`TransferProposal`] **doesn't** consume it from the ledger. This function must be used to correctly + /// consume both [`TransferProposal`] and `SenderCap`. + pub fn conclude_or_cancel(self) -> ConcludeTransferTx { + ConcludeTransferTx(self) + } + + /// Returns this [`TransferProposal`]'s ID. + pub fn id(&self) -> ObjectID { + *self.id.object_id() + } + + /// Returns this [`TransferProposal`]'s `sender`'s address. + pub fn sender(&self) -> IotaAddress { + self.sender_address + } + + /// Returns this [`TransferProposal`]'s `recipient`'s address. + pub fn recipient(&self) -> IotaAddress { + self.recipient_address + } + + /// Returns `true` if this [`TransferProposal`] is concluded. + pub fn is_concluded(&self) -> bool { + self.done + } +} + +/// A [`Transaction`] that updates an [`AuthenticatedAsset`]'s content. +#[derive(Debug)] +pub struct UpdateContentTx<'a, T> { + asset: &'a mut AuthenticatedAsset, + new_content: T, +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for UpdateContentTx<'_, T> +where + T: MoveType + Serialize + Clone + Send + Sync, +{ + type Output = (); + + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let tx = AssetMoveCallsAdapter::update( + self.asset.object_ref(client).await?, + self.new_content.clone(), + client.package_id(), + )?; + let response = client.execute_transaction(tx, gas_budget).await?; + let tx_status = response + .effects_execution_status() + .context("transaction had no effects") + .map_err(|e| Error::TransactionUnexpectedResponse(e.to_string()))?; + + if let IotaExecutionStatus::Failure { error } = tx_status { + return Err(Error::TransactionUnexpectedResponse(error.clone())); + } + + self.asset.inner = self.new_content; + + Ok(TransactionOutputInternal { output: (), response }) + } +} + +/// A [`Transaction`] that deletes an [`AuthenticatedAsset`]. +#[derive(Debug)] +pub struct DeleteAssetTx(AuthenticatedAsset); + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for DeleteAssetTx +where + T: MoveType + Send + Sync, +{ + type Output = (); + + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let asset_ref = self.0.object_ref(client).await?; + let tx = AssetMoveCallsAdapter::delete::(asset_ref, client.package_id())?; + + let response = client.execute_transaction(tx, gas_budget).await?; + Ok(TransactionOutputInternal { output: (), response }) + } +} +/// A [`Transaction`] that creates a new [`AuthenticatedAsset`]. +#[derive(Debug)] +pub struct CreateAssetTx(AuthenticatedAssetBuilder); + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for CreateAssetTx +where + T: MoveType + Serialize + DeserializeOwned + Send, +{ + type Output = AuthenticatedAsset; + + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let AuthenticatedAssetBuilder { + inner, + mutable, + transferable, + deletable, + } = self.0; + let tx = AssetMoveCallsAdapter::new_asset(inner, mutable, transferable, deletable, client.package_id())?; + + let response = client.execute_transaction(tx, gas_budget).await?; + + let created_asset_id = response + .effects_created() + .ok_or_else(|| Error::TransactionUnexpectedResponse("could not find effects in transaction response".to_owned()))? + .first() + .ok_or_else(|| Error::TransactionUnexpectedResponse("no object was created in this transaction".to_owned()))? + .object_id(); + + AuthenticatedAsset::get_by_id(created_asset_id, client) + .await + .map(move |output| TransactionOutputInternal { output, response }) + } +} + +/// A [`Transaction`] that proposes the transfer of an [`AuthenticatedAsset`]. +#[derive(Debug)] +pub struct TransferAssetTx { + asset: AuthenticatedAsset, + recipient: IotaAddress, +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for TransferAssetTx +where + T: MoveType + Send + Sync, +{ + type Output = TransferProposal; + + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let tx = AssetMoveCallsAdapter::transfer::( + self.asset.object_ref(client).await?, + self.recipient, + client.package_id(), + )?; + + let tx_result = client.execute_transaction(tx, gas_budget).await?; + let effects_created = tx_result.effects_created().ok_or_else(|| { + Error::TransactionUnexpectedResponse("could not find effects in transaction response".to_owned()) + })?; + let created_obj_ids = effects_created.iter().map(|obj| obj.reference.object_id); + for id in created_obj_ids { + let object_type = client + .read_api() + .get_object_with_options(id, IotaObjectDataOptions::new().with_type()) + .await? + .data + .context("no data in response") + .and_then(|data| Ok(data.object_type()?.to_string())) + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + + if object_type == TransferProposal::move_type(client.package_id()).to_string() { + return TransferProposal::get_by_id(id, client) + .await + .map(move |proposal| TransactionOutputInternal { + output: proposal, + response: tx_result, + }); + } + } + + Err(Error::TransactionUnexpectedResponse( + "no proposal was created in this transaction".to_owned(), + )) + } +} + +/// A [`Transaction`] that accepts the transfer of an [`AuthenticatedAsset`]. +#[derive(Debug)] +pub struct AcceptTransferTx(TransferProposal); + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for AcceptTransferTx { + type Output = (); + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + if self.0.done { + return Err(Error::TransactionBuildingFailed( + "the transfer has already been concluded".to_owned(), + )); + } + + let cap = self.0.get_cap("RecipientCap", client).await?; + let (asset_ref, param_type) = self + .0 + .asset_metadata(client) + .await + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + let initial_shared_version = self + .0 + .initial_shared_version(client) + .await + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + let tx = AssetMoveCallsAdapter::accept_proposal( + (self.0.id(), initial_shared_version), + cap, + asset_ref, + param_type, + client.package_id(), + )?; + + let response = client.execute_transaction(tx, gas_budget).await?; + Ok(TransactionOutputInternal { output: (), response }) + } +} + +/// A [`Transaction`] that concludes the transfer of an [`AuthenticatedAsset`]. +#[derive(Debug)] +pub struct ConcludeTransferTx(TransferProposal); + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for ConcludeTransferTx { + type Output = (); + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let cap = self.0.get_cap("SenderCap", client).await?; + let (asset_ref, param_type) = self + .0 + .asset_metadata(client) + .await + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + let initial_shared_version = self + .0 + .initial_shared_version(client) + .await + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + + let tx = AssetMoveCallsAdapter::conclude_or_cancel( + (self.0.id(), initial_shared_version), + cap, + asset_ref, + param_type, + client.package_id(), + )?; + + let response = client.execute_transaction(tx, gas_budget).await?; + Ok(TransactionOutputInternal { output: (), response }) + } +} diff --git a/identity_iota_core/src/rebased/assets/mod.rs b/identity_iota_core/src/rebased/assets/mod.rs new file mode 100644 index 0000000000..ba879d0181 --- /dev/null +++ b/identity_iota_core/src/rebased/assets/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod asset; +mod public_available_vc; + +pub use asset::*; +pub use public_available_vc::*; diff --git a/identity_iota_core/src/rebased/assets/public_available_vc.rs b/identity_iota_core/src/rebased/assets/public_available_vc.rs new file mode 100644 index 0000000000..e09a605083 --- /dev/null +++ b/identity_iota_core/src/rebased/assets/public_available_vc.rs @@ -0,0 +1,103 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; + +use anyhow::Context as _; +use identity_credential::credential::Credential; +use identity_credential::credential::Jwt; +use identity_credential::credential::JwtCredential; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::IotaVerifiableCredential; +use identity_iota_interaction::OptionalSync; +use identity_jose::jwt::JwtHeader; +use identity_jose::jwu; +use itertools::Itertools; +use secret_storage::Signer; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IdentityClientReadOnly; +use crate::rebased::transaction::TransactionInternal; + +use super::AuthenticatedAsset; +use super::AuthenticatedAssetBuilder; + +/// A publicly available verifiable credential. +#[derive(Debug, Clone)] +pub struct PublicAvailableVC { + asset: AuthenticatedAsset, + credential: Credential, +} + +impl Deref for PublicAvailableVC { + type Target = Credential; + fn deref(&self) -> &Self::Target { + &self.credential + } +} + +impl PublicAvailableVC { + /// Get the ID of the asset. + pub fn object_id(&self) -> ObjectID { + self.asset.id() + } + + /// Get the JWT of the credential. + pub fn jwt(&self) -> Jwt { + String::from_utf8(self.asset.content().data().clone()) + .map(Jwt::new) + .expect("JWT is valid UTF8") + } + + /// Create a new publicly available VC. + /// + /// # Returns + /// A new `PublicAvailableVC`. + pub async fn new(jwt: Jwt, gas_budget: Option, client: &IdentityClient) -> Result + where + S: Signer + OptionalSync, + { + let jwt_bytes = String::from(jwt).into_bytes(); + let credential = parse_jwt_credential(&jwt_bytes)?; + let asset = AuthenticatedAssetBuilder::new(IotaVerifiableCredential::new(jwt_bytes)) + .transferable(false) + .mutable(true) + .deletable(true) + .finish() + .execute_with_opt_gas_internal(gas_budget, client) + .await? + .output; + + Ok(Self { credential, asset }) + } + + /// Get a publicly available VC by its ID. + pub async fn get_by_id(id: ObjectID, client: &IdentityClientReadOnly) -> Result { + let asset = client + .get_object_by_id::>(id) + .await?; + Self::try_from_asset(asset).map_err(|e| { + crate::rebased::Error::ObjectLookup(format!( + "object at address {id} is not a valid publicly available VC: {e}" + )) + }) + } + + fn try_from_asset(asset: AuthenticatedAsset) -> Result { + let credential = parse_jwt_credential(asset.content().data())?; + Ok(Self { asset, credential }) + } +} + +fn parse_jwt_credential(bytes: &[u8]) -> Result { + let [header, payload, _signature]: [Vec; 3] = bytes + .split(|c| *c == b'.') + .map(jwu::decode_b64) + .try_collect::<_, Vec<_>, _>()? + .try_into() + .map_err(|_| anyhow::anyhow!("invalid JWT"))?; + let _header = serde_json::from_slice::(&header)?; + let credential_claims = serde_json::from_slice::(&payload)?; + credential_claims.try_into().context("invalid jwt credential claims") +} diff --git a/identity_iota_core/src/rebased/client/full_client.rs b/identity_iota_core/src/rebased/client/full_client.rs new file mode 100644 index 0000000000..90c125c862 --- /dev/null +++ b/identity_iota_core/src/rebased/client/full_client.rs @@ -0,0 +1,326 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; + +use crate::IotaDID; +use crate::IotaDocument; +use async_trait::async_trait; +use identity_iota_interaction::move_types::language_storage::StructTag; +use identity_iota_interaction::rpc_types::IotaObjectData; +use identity_iota_interaction::rpc_types::IotaObjectDataFilter; +use identity_iota_interaction::rpc_types::IotaObjectResponseQuery; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::crypto::PublicKey; +use identity_verification::jwk::Jwk; +use secret_storage::Signer; +use serde::de::DeserializeOwned; +use serde::Serialize; + +use crate::iota_interaction_adapter::IotaTransactionBlockResponseAdaptedTraitObj; +use crate::rebased::assets::AuthenticatedAssetBuilder; +use crate::rebased::migration::Identity; +use crate::rebased::migration::IdentityBuilder; +use crate::rebased::rebased_err; +use crate::rebased::Error; +use identity_iota_interaction::IotaClientTrait; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::MoveType; +use identity_iota_interaction::OptionalSync; +use identity_iota_interaction::ProgrammableTransactionBcs; + +use crate::rebased::transaction::TransactionOutputInternal; +cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + use crate::rebased::transaction::TransactionInternal as TransactionT; + type TransactionOutputT = TransactionOutputInternal; + } else { + use crate::rebased::transaction::TransactionInternal; + use crate::rebased::transaction::Transaction as TransactionT; + use crate::rebased::transaction::TransactionOutput as TransactionOutputT; + } +} + +use super::get_object_id_from_did; +use super::IdentityClientReadOnly; + +/// Mirrored types from identity_storage::KeyId +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +pub struct KeyId(String); + +impl KeyId { + /// Creates a new key identifier from a string. + pub fn new(id: impl Into) -> Self { + Self(id.into()) + } + + /// Returns string representation of the key id. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for KeyId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl From for String { + fn from(value: KeyId) -> Self { + value.0 + } +} + +/// A client for interacting with the IOTA network. +#[derive(Clone)] +pub struct IdentityClient { + /// [`IdentityClientReadOnly`] instance, used for read-only operations. + read_client: IdentityClientReadOnly, + /// The public key of the client. + public_key: PublicKey, + /// The signer of the client. + signer: S, +} + +impl Deref for IdentityClient { + type Target = IdentityClientReadOnly; + fn deref(&self) -> &Self::Target { + &self.read_client + } +} + +impl IdentityClient +where + S: Signer, +{ + /// Create a new [`IdentityClient`]. + pub async fn new(client: IdentityClientReadOnly, signer: S) -> Result { + let public_key = signer + .public_key() + .await + .map_err(|e| Error::InvalidKey(e.to_string()))?; + + Ok(Self { + public_key, + read_client: client, + signer, + }) + } +} + +impl IdentityClient +where + S: Signer + OptionalSync, +{ + pub(crate) async fn execute_transaction( + &self, + tx_bcs: ProgrammableTransactionBcs, + gas_budget: Option, + ) -> Result { + // This code looks like we would call execute_transaction() on + // self.read_client (which is an IdentityClientReadOnly). + // Actually we call execute_transaction() on self.read_client.iota_client + // which is an IotaClientAdapter instance now, provided via the Deref trait. + // TODO: Find a more transparent way to reference the + // IotaClientAdapter for readonly. + self + .read_client + .execute_transaction(tx_bcs, gas_budget, self.signer()) + .await + .map_err(rebased_err) + } +} + +impl IdentityClient { + /// Returns the bytes of the sender's public key. + pub fn sender_public_key(&self) -> &PublicKey { + &self.public_key + } + + /// Returns this [`IdentityClient`]'s sender address. + #[inline(always)] + pub fn sender_address(&self) -> IotaAddress { + IotaAddress::from(&self.public_key) + } + + /// Returns a reference to this [`IdentityClient`]'s [`Signer`]. + pub fn signer(&self) -> &S { + &self.signer + } + + /// Returns a new [`IdentityBuilder`] in order to build a new [`crate::rebased::migration::OnChainIdentity`]. + pub fn create_identity(&self, iota_document: IotaDocument) -> IdentityBuilder { + IdentityBuilder::new(iota_document) + } + + /// Returns a new [`IdentityBuilder`] in order to build a new [`crate::rebased::migration::OnChainIdentity`]. + pub fn create_authenticated_asset(&self, content: T) -> AuthenticatedAssetBuilder + where + T: MoveType + Serialize + DeserializeOwned, + { + AuthenticatedAssetBuilder::new(content) + } + + /// Query the objects owned by the address wrapped by this client to find the object of type `tag` + /// and that satisfies `predicate`. + pub async fn find_owned_ref

(&self, tag: StructTag, predicate: P) -> Result, Error> + where + P: Fn(&IotaObjectData) -> bool, + { + let filter = IotaObjectResponseQuery::new_with_filter(IotaObjectDataFilter::StructType(tag)); + + let mut cursor = None; + loop { + let mut page = self + .read_api() + .get_owned_objects(self.sender_address(), Some(filter.clone()), cursor, None) + .await?; + let obj_ref = std::mem::take(&mut page.data) + .into_iter() + .filter_map(|res| res.data) + .find(|obj| predicate(obj)) + .map(|obj_data| obj_data.object_ref()); + cursor = page.next_cursor; + + if obj_ref.is_some() { + return Ok(obj_ref); + } + if !page.has_next_page { + break; + } + } + + Ok(None) + } +} + +impl IdentityClient +where + S: Signer + OptionalSync, +{ + /// Returns [`Transaction`] [`PublishDidTx`] that - when executed - will publish a new DID Document on chain. + pub fn publish_did_document(&self, document: IotaDocument) -> PublishDidTx { + PublishDidTx(document) + } + + // TODO: define what happens for (legacy|migrated|new) documents + /// Updates a DID Document. + pub async fn publish_did_document_update( + &self, + document: IotaDocument, + gas_budget: u64, + ) -> Result { + let mut oci = + if let Identity::FullFledged(value) = self.get_identity(get_object_id_from_did(document.id())?).await? { + value + } else { + return Err(Error::Identity("only new identities can be updated".to_string())); + }; + + oci + .update_did_document(document.clone()) + .finish(self) + .await? + .execute_with_gas(gas_budget, self) + .await?; + + Ok(document) + } + + /// Deactivates a DID document. + pub async fn deactivate_did_output(&self, did: &IotaDID, gas_budget: u64) -> Result<(), Error> { + let mut oci = if let Identity::FullFledged(value) = self.get_identity(get_object_id_from_did(did)?).await? { + value + } else { + return Err(Error::Identity("only new identities can be deactivated".to_string())); + }; + + oci + .deactivate_did() + .finish(self) + .await? + .execute_with_gas(gas_budget, self) + .await?; + + Ok(()) + } +} + +/// Utility function that returns the key's bytes of a JWK encoded public ed25519 key. +pub fn get_sender_public_key(sender_public_jwk: &Jwk) -> Result, Error> { + let public_key_base_64 = &sender_public_jwk + .try_okp_params() + .map_err(|err| Error::InvalidKey(format!("key not of type `Okp`; {err}")))? + .x; + + identity_jose::jwu::decode_b64(public_key_base_64) + .map_err(|err| Error::InvalidKey(format!("could not decode base64 public key; {err}"))) +} + +/// Publishes a new DID Document on-chain. An [`crate::rebased::migration::OnChainIdentity`] will be created to contain +/// the provided document. +#[derive(Debug)] +pub struct PublishDidTx(IotaDocument); + +impl PublishDidTx { + async fn execute_publish_did_tx_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let TransactionOutputInternal { + output: identity, + response, + } = client + .create_identity(self.0) + .finish() + .execute_with_opt_gas_internal(gas_budget, client) + .await?; + + Ok(TransactionOutputInternal { + output: identity.did_doc, + response, + }) + } +} + +// #[cfg(not(target_arch = "wasm32"))] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionT for PublishDidTx { + type Output = IotaDocument; + + #[cfg(not(target_arch = "wasm32"))] + async fn execute_with_opt_gas( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + Ok( + self + .execute_publish_did_tx_with_opt_gas(gas_budget, client) + .await? + .into(), + ) + } + + #[cfg(target_arch = "wasm32")] + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + self.execute_publish_did_tx_with_opt_gas(gas_budget, client).await + } +} diff --git a/identity_iota_core/src/rebased/client/mod.rs b/identity_iota_core/src/rebased/client/mod.rs new file mode 100644 index 0000000000..d5b4f76b57 --- /dev/null +++ b/identity_iota_core/src/rebased/client/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod full_client; +mod read_only; + +pub use full_client::*; +pub use read_only::*; + +pub use identity_iota_interaction::IotaKeySignature; diff --git a/identity_iota_core/src/rebased/client/read_only.rs b/identity_iota_core/src/rebased/client/read_only.rs new file mode 100644 index 0000000000..d77d05a060 --- /dev/null +++ b/identity_iota_core/src/rebased/client/read_only.rs @@ -0,0 +1,390 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::future::Future; +use std::ops::Deref; +use std::pin::Pin; +use std::str::FromStr; + +use crate::rebased::iota; +use crate::IotaDID; +use crate::IotaDocument; +use crate::NetworkName; +use anyhow::anyhow; +use anyhow::Context as _; +use futures::stream::FuturesUnordered; + +use crate::iota_interaction_adapter::IotaClientAdapter; +use crate::rebased::migration::get_alias; +use crate::rebased::migration::get_identity; +use crate::rebased::migration::lookup; +use crate::rebased::migration::Identity; +use crate::rebased::Error; +use futures::StreamExt as _; +use identity_core::common::Url; +use identity_did::DID; +use identity_iota_interaction::move_types::language_storage::StructTag; +use identity_iota_interaction::rpc_types::EventFilter; +use identity_iota_interaction::rpc_types::IotaData as _; +use identity_iota_interaction::rpc_types::IotaObjectData; +use identity_iota_interaction::rpc_types::IotaObjectDataFilter; +use identity_iota_interaction::rpc_types::IotaObjectDataOptions; +use identity_iota_interaction::rpc_types::IotaObjectResponseQuery; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::IotaClientTrait; +use serde::de::DeserializeOwned; +use serde::Deserialize; + +#[cfg(not(target_arch = "wasm32"))] +use identity_iota_interaction::IotaClient; + +#[cfg(target_arch = "wasm32")] +use iota_interaction_ts::bindings::WasmIotaClient; + +/// An [`IotaClient`] enriched with identity-related +/// functionalities. +#[derive(Clone)] +pub struct IdentityClientReadOnly { + iota_client: IotaClientAdapter, + iota_identity_pkg_id: ObjectID, + migration_registry_id: ObjectID, + network: NetworkName, +} + +impl Deref for IdentityClientReadOnly { + type Target = IotaClientAdapter; + fn deref(&self) -> &Self::Target { + &self.iota_client + } +} + +impl IdentityClientReadOnly { + /// Returns `iota_identity`'s package ID. + /// The ID of the packages depends on the network + /// the client is connected to. + pub const fn package_id(&self) -> ObjectID { + self.iota_identity_pkg_id + } + + /// Returns the name of the network the client is + /// currently connected to. + pub const fn network(&self) -> &NetworkName { + &self.network + } + + /// Returns the migration registry's ID. + pub const fn migration_registry_id(&self) -> ObjectID { + self.migration_registry_id + } + + cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + /// Attempts to create a new [`IdentityClientReadOnly`] from a given [`IotaClient`]. + /// + /// # Failures + /// This function fails if the provided `iota_client` is connected to an unrecognized + /// network. + /// + /// # Notes + /// When trying to connect to a local or unofficial network prefer using + /// [`IdentityClientReadOnly::new_with_pkg_id`]. + pub async fn new(iota_client: WasmIotaClient) -> Result { + Self::new_internal(IotaClientAdapter::new(iota_client)?).await + } + } else { + /// Attempts to create a new [`IdentityClientReadOnly`] from a given [`IotaClient`]. + /// + /// # Failures + /// This function fails if the provided `iota_client` is connected to an unrecognized + /// network. + /// + /// # Notes + /// When trying to connect to a local or unofficial network prefer using + /// [`IdentityClientReadOnly::new_with_pkg_id`]. + pub async fn new(iota_client: IotaClient) -> Result { + Self::new_internal(IotaClientAdapter::new(iota_client)?).await + } + } + } + + async fn new_internal(iota_client: IotaClientAdapter) -> Result { + let network = network_id(&iota_client).await?; + let metadata = iota::well_known_networks::network_metadata(&network).ok_or_else(|| { + Error::InvalidConfig(format!( + "unrecognized network \"{network}\". Use `new_with_pkg_id` instead." + )) + })?; + // If the network has a well known alias use it otherwise default to the network's chain ID. + let network = metadata.network_alias().unwrap_or(network); + + let pkg_id = metadata.latest_pkg_id(); + + Ok(IdentityClientReadOnly { + iota_client, + iota_identity_pkg_id: pkg_id, + migration_registry_id: metadata.migration_registry(), + network, + }) + } + + cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + /// Attempts to create a new [`IdentityClientReadOnly`] from + /// the given [`IotaClient`]. + pub async fn new_with_pkg_id(iota_client: WasmIotaClient, iota_identity_pkg_id: ObjectID) -> Result { + Self::new_with_pkg_id_internal( + IotaClientAdapter::new(iota_client)?, + iota_identity_pkg_id + ).await + } + } else { + /// Attempts to create a new [`IdentityClientReadOnly`] from + /// the given [`IotaClient`]. + pub async fn new_with_pkg_id(iota_client: IotaClient, iota_identity_pkg_id: ObjectID) -> Result { + Self::new_with_pkg_id_internal( + IotaClientAdapter::new(iota_client)?, + iota_identity_pkg_id + ).await + } + } + } + + async fn new_with_pkg_id_internal( + iota_client: IotaClientAdapter, + iota_identity_pkg_id: ObjectID, + ) -> Result { + let IdentityPkgMetadata { + migration_registry_id, .. + } = identity_pkg_metadata(&iota_client, iota_identity_pkg_id).await?; + let network = network_id(&iota_client).await?; + Ok(Self { + iota_client, + iota_identity_pkg_id, + migration_registry_id, + network, + }) + } + + /// Resolves a _Move_ Object of ID `id` and parses it to a value of type `T`. + pub async fn get_object_by_id(&self, id: ObjectID) -> Result + where + T: DeserializeOwned, + { + self + .read_api() + .get_object_with_options(id, IotaObjectDataOptions::new().with_content()) + .await + .context("lookup request failed") + .and_then(|res| res.data.context("missing data in response")) + .and_then(|data| data.content.context("missing object content in data")) + .and_then(|content| content.try_into_move().context("not a move object")) + .and_then(|obj| { + serde_json::from_value(obj.fields.to_json_value()) + .map_err(|err| anyhow!("failed to deserialize move object; {err}")) + }) + .map_err(|e| Error::ObjectLookup(e.to_string())) + } + + /// Returns an object's [`OwnedObjectRef`], if any. + pub async fn get_object_ref_by_id(&self, obj: ObjectID) -> Result, Error> { + self + .read_api() + .get_object_with_options(obj, IotaObjectDataOptions::default().with_owner()) + .await + .map(|response| { + response.data.map(|obj_data| OwnedObjectRef { + owner: obj_data.owner.expect("requested data"), + reference: obj_data.object_ref().into(), + }) + }) + .map_err(Error::from) + } + + /// Queries the object owned by this sender address and returns the first one + /// that matches `tag` and for which `predicate` returns `true`. + pub async fn find_owned_ref_for_address

( + &self, + address: IotaAddress, + tag: StructTag, + predicate: P, + ) -> Result, Error> + where + P: Fn(&IotaObjectData) -> bool, + { + let filter = IotaObjectResponseQuery::new_with_filter(IotaObjectDataFilter::StructType(tag)); + + let mut cursor = None; + loop { + let mut page = self + .read_api() + .get_owned_objects(address, Some(filter.clone()), cursor, None) + .await?; + let obj_ref = std::mem::take(&mut page.data) + .into_iter() + .filter_map(|res| res.data) + .find(|obj| predicate(obj)) + .map(|obj_data| obj_data.object_ref()); + cursor = page.next_cursor; + + if obj_ref.is_some() { + return Ok(obj_ref); + } + if !page.has_next_page { + break; + } + } + + Ok(None) + } + + /// Queries an [`IotaDocument`] DID Document through its `did`. + pub async fn resolve_did(&self, did: &IotaDID) -> Result { + self + .get_identity(get_object_id_from_did(did)?) + .await? + .did_document(self.network()) + } + + /// Resolves an [`Identity`] from its ID `object_id`. + pub async fn get_identity(&self, object_id: ObjectID) -> Result { + // spawn all checks + cfg_if::cfg_if! { + // Unfortunately the compiler runs into lifetime problems if we try to use a 'type =' + // instead of the below ugly platform specific code + if #[cfg(target_arch = "wasm32")] { + let all_futures = FuturesUnordered::, Error>>>>>::new(); + } else { + let all_futures = FuturesUnordered::, Error>> + Send>>>::new(); + } + } + all_futures.push(Box::pin(resolve_new(self, object_id))); + all_futures.push(Box::pin(resolve_migrated(self, object_id))); + all_futures.push(Box::pin(resolve_unmigrated(self, object_id))); + + all_futures + .filter_map(|res| Box::pin(async move { res.ok().flatten() })) + .next() + .await + .ok_or_else(|| Error::DIDResolutionError(format!("could not find DID document for {object_id}"))) + } +} + +async fn network_id(iota_client: &IotaClientAdapter) -> Result { + let network_id = iota_client + .read_api() + .get_chain_identifier() + .await + .map_err(|e| Error::RpcError(e.to_string()))?; + Ok(network_id.try_into().expect("chain ID is a valid network name")) +} + +#[derive(Debug)] +struct IdentityPkgMetadata { + migration_registry_id: ObjectID, +} + +#[derive(Deserialize)] +struct MigrationRegistryCreatedEvent { + #[allow(dead_code)] + id: ObjectID, +} + +// TODO: remove argument `package_id` and use `EventFilter::MoveEventField` to find the beacon event and thus the +// package id. +// TODO: authenticate the beacon event with though sender's ID. +async fn identity_pkg_metadata( + iota_client: &IotaClientAdapter, + package_id: ObjectID, +) -> Result { + // const EVENT_BEACON_PATH: &str = "/beacon"; + // const EVENT_BEACON_VALUE: &[u8] = b"identity.rs_pkg"; + + // let event_filter = EventFilter::MoveEventField { + // path: EVENT_BEACON_PATH.to_string(), + // value: EVENT_BEACON_VALUE.to_json_value().expect("valid json representation"), + // }; + let event_filter = EventFilter::MoveEventType( + StructTag::from_str(&format!("{package_id}::migration_registry::MigrationRegistryCreated")).expect("valid utf8"), + ); + let mut returned_events = iota_client + .event_api() + .query_events(event_filter, None, Some(1), false) + .await + .map_err(|e| Error::RpcError(e.to_string()))? + .data; + let event = if !returned_events.is_empty() { + returned_events.swap_remove(0) + } else { + return Err(Error::InvalidConfig( + "no \"iota_identity\" package found on the provided network".to_string(), + )); + }; + + let registry_id = serde_json::from_value::(event.parsed_json) + .map(|e| e.id) + .map_err(|e| { + Error::MigrationRegistryNotFound(crate::rebased::migration::Error::NotFound(format!( + "Malformed \"MigrationRegistryEvent\": {}", + e + ))) + })?; + + Ok(IdentityPkgMetadata { + migration_registry_id: registry_id, + }) +} + +async fn resolve_new(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result, Error> { + let onchain_identity = get_identity(client, object_id).await.map_err(|err| { + Error::DIDResolutionError(format!( + "could not get identity document for object id {object_id}; {err}" + )) + })?; + Ok(onchain_identity.map(Identity::FullFledged)) +} + +async fn resolve_migrated(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result, Error> { + let onchain_identity = lookup(client, object_id).await.map_err(|err| { + Error::DIDResolutionError(format!( + "failed to look up object_id {object_id} in migration registry; {err}" + )) + })?; + let Some(mut onchain_identity) = onchain_identity else { + return Ok(None); + }; + let object_id_str = object_id.to_string(); + let queried_did = IotaDID::from_object_id(&object_id_str, &client.network); + let doc = onchain_identity.did_document_mut(); + let identity_did = doc.id().clone(); + // When querying a migrated identity we obtain a DID document with DID `identity_did` and the `alsoKnownAs` + // property containing `queried_did`. Since we are resolving `queried_did`, lets replace in the document these + // values. `queried_id` becomes the DID Document ID. + *doc.core_document_mut().id_mut_unchecked() = queried_did.clone().into(); + // The DID Document `alsoKnownAs` property is cleaned of its `queried_did` entry, + // which gets replaced by `identity_did`. + doc + .also_known_as_mut() + .replace::(&queried_did.into_url().into(), identity_did.into_url().into()); + + Ok(Some(Identity::FullFledged(onchain_identity))) +} + +async fn resolve_unmigrated(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result, Error> { + let unmigrated_alias = get_alias(client, object_id) + .await + .map_err(|err| Error::DIDResolutionError(format!("could no query for object id {object_id}; {err}")))?; + Ok(unmigrated_alias.map(Identity::Legacy)) +} + +/// Extracts the object ID from the given `IotaDID`. +/// +/// # Arguments +/// +/// * `did` - A reference to the `IotaDID` to be converted. +pub fn get_object_id_from_did(did: &IotaDID) -> Result { + ObjectID::from_str(did.tag_str()) + .map_err(|err| Error::DIDResolutionError(format!("could not parse object id from did {did}; {err}"))) +} diff --git a/identity_iota_core/src/rebased/error.rs b/identity_iota_core/src/rebased/error.rs new file mode 100644 index 0000000000..9186b98596 --- /dev/null +++ b/identity_iota_core/src/rebased/error.rs @@ -0,0 +1,93 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Errors that may occur for the rebased logic. + +#[cfg(target_arch = "wasm32")] +use iota_interaction_ts::error::TsSdkError; + +/// This type represents all possible errors that can occur in the library. +#[derive(Debug, thiserror::Error, strum::IntoStaticStr)] +#[non_exhaustive] +pub enum Error { + /// failed to connect to network. + #[error("failed to connect to iota network node; {0:?}")] + Network(String, #[source] identity_iota_interaction::error::Error), + /// could not lookup an object ID. + #[error("failed to lookup an object; {0}")] + ObjectLookup(String), + /// MigrationRegistry error. + #[error(transparent)] + MigrationRegistryNotFound(crate::rebased::migration::Error), + /// Caused by a look failures during resolution. + #[error("DID resolution failed: {0}")] + DIDResolutionError(String), + /// Caused by invalid or missing arguments. + #[error("invalid or missing argument: {0}")] + InvalidArgument(String), + /// Caused by invalid keys. + #[error("invalid key: {0}")] + InvalidKey(String), + /// Caused by issues with paying for transaction. + #[error("issue with gas for transaction: {0}")] + GasIssue(String), + /// Could not parse module, package, etc. + #[error("failed to parse {0}")] + ParsingFailed(String), + /// Could not build transaction. + #[error("failed to build transaction; {0}")] + TransactionBuildingFailed(String), + /// Could not sign transaction. + #[error("failed to sign transaction; {0}")] + TransactionSigningFailed(String), + /// Could not execute transaction. + #[error("transaction execution failed; {0}")] + TransactionExecutionFailed(#[from] identity_iota_interaction::error::Error), + /// Transaction yielded invalid response. This usually means that the transaction was executed but did not produce + /// the expected result. + #[error("transaction returned an unexpected response; {0}")] + TransactionUnexpectedResponse(String), + /// Config is invalid. + #[error("invalid config: {0}")] + InvalidConfig(String), + /// Failed to parse DID document. + #[error("failed to parse DID document; {0}")] + DidDocParsingFailed(String), + /// Failed to serialize DID document. + #[error("failed to serialize DID document; {0}")] + DidDocSerialization(String), + /// Identity related error. + #[error("identity error; {0}")] + Identity(String), + #[error("unexpected state when looking up identity history; {0}")] + /// Unexpected state when looking up identity history. + InvalidIdentityHistory(String), + /// An operation cannot be carried on for a lack of permissions - e.g. missing capability. + #[error("the requested operation cannot be performed for a lack of permissions; {0}")] + MissingPermission(String), + /// An error caused by either a connection issue or an invalid RPC call. + #[error("RPC error: {0}")] + RpcError(String), + /// An error caused by a bcs serialization or deserialization. + #[error("BCS error: {0}")] + BcsError(#[from] bcs::Error), + /// An anyhow::error. + #[error("Any error: {0}")] + AnyError(#[from] anyhow::Error), + /// An error caused by a foreign function interface call. + #[error("FFI error: {0}")] + FfiError(String), + #[cfg(target_arch = "wasm32")] + /// An error originating from IOTA typescript SDK import bindings + #[error("TsSdkError: {0}")] + TsSdkError(#[from] TsSdkError), +} + +/// Can be used for example like `map_err(rebased_err)` to convert other error +/// types to identity_iota_core::rebased::Error. +pub fn rebased_err(error: T) -> Error +where + Error: From, +{ + error.into() +} diff --git a/identity_iota_core/src/rebased/iota/mod.rs b/identity_iota_core/src/rebased/iota/mod.rs new file mode 100644 index 0000000000..2a71e571af --- /dev/null +++ b/identity_iota_core/src/rebased/iota/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod types; +pub(crate) mod well_known_networks; diff --git a/identity_iota_core/src/rebased/iota/move_calls/asset/create.rs b/identity_iota_core/src/rebased/iota/move_calls/asset/create.rs new file mode 100644 index 0000000000..63c402debc --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/asset/create.rs @@ -0,0 +1,40 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::Command; +use identity_iota_interaction::types::transaction::ProgrammableMoveCall; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::ident_str; +use serde::Serialize; + +use identity_iota_interaction::MoveType; +use crate::rebased::Error; +use super::try_to_argument; + +pub(crate) fn new( + inner: T, + mutable: bool, + transferable: bool, + deletable: bool, + package: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let inner = try_to_argument(&inner, &mut ptb, package)?; + let mutable = ptb.pure(mutable).map_err(|e| Error::InvalidArgument(e.to_string()))?; + let transferable = ptb + .pure(transferable) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let deletable = ptb.pure(deletable).map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package, + module: ident_str!("asset").into(), + function: ident_str!("new_with_config").into(), + type_arguments: vec![T::move_type(package)], + arguments: vec![inner, mutable, transferable, deletable], + }))); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/asset/delete.rs b/identity_iota_core/src/rebased/iota/move_calls/asset/delete.rs new file mode 100644 index 0000000000..5a2adc0538 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/asset/delete.rs @@ -0,0 +1,34 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::Command; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::ident_str; + +use identity_iota_interaction::MoveType; +use crate::rebased::Error; + +pub(crate) fn delete(asset: ObjectRef, package: ObjectID) -> Result +where + T: MoveType, +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + + let asset = ptb + .obj(ObjectArg::ImmOrOwnedObject(asset)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::move_call( + package, + ident_str!("asset").into(), + ident_str!("delete").into(), + vec![T::move_type(package)], + vec![asset], + )); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/asset/mod.rs b/identity_iota_core/src/rebased/iota/move_calls/asset/mod.rs new file mode 100644 index 0000000000..dc0271ad8b --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/asset/mod.rs @@ -0,0 +1,14 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod create; +mod delete; +mod transfer; +mod update; +mod try_to_argument; + +pub(crate) use create::*; +pub(crate) use delete::*; +pub(crate) use transfer::*; +pub(crate) use update::*; +pub(crate) use try_to_argument::try_to_argument; \ No newline at end of file diff --git a/identity_iota_core/src/rebased/iota/move_calls/asset/transfer.rs b/identity_iota_core/src/rebased/iota/move_calls/asset/transfer.rs new file mode 100644 index 0000000000..2505905a7c --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/asset/transfer.rs @@ -0,0 +1,99 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::base_types::SequenceNumber; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::Command; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::types::TypeTag; +use identity_iota_interaction::ident_str; + +use identity_iota_interaction::MoveType; +use crate::rebased::Error; + +pub(crate) fn transfer( + asset: ObjectRef, + recipient: IotaAddress, + package: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let asset = ptb + .obj(ObjectArg::ImmOrOwnedObject(asset)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let recipient = ptb.pure(recipient).map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::move_call( + package, + ident_str!("asset").into(), + ident_str!("transfer").into(), + vec![T::move_type(package)], + vec![asset, recipient], + )); + + Ok(ptb.finish()) +} + +fn make_tx( + proposal: (ObjectID, SequenceNumber), + cap: ObjectRef, + asset: ObjectRef, + asset_type_param: TypeTag, + package: ObjectID, + function_name: &'static str, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let proposal = ptb + .obj(ObjectArg::SharedObject { + id: proposal.0, + initial_shared_version: proposal.1, + mutable: true, + }) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let cap = ptb + .obj(ObjectArg::ImmOrOwnedObject(cap)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let asset = ptb + .obj(ObjectArg::Receiving(asset)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::move_call( + package, + ident_str!("asset").into(), + ident_str!(function_name).into(), + vec![asset_type_param], + vec![proposal, cap, asset], + )); + + Ok(ptb.finish()) +} + +pub(crate) fn accept_proposal( + proposal: (ObjectID, SequenceNumber), + recipient_cap: ObjectRef, + asset: ObjectRef, + asset_type_param: TypeTag, + package: ObjectID, +) -> Result { + make_tx(proposal, recipient_cap, asset, asset_type_param, package, "accept") +} + +pub(crate) fn conclude_or_cancel( + proposal: (ObjectID, SequenceNumber), + sender_cap: ObjectRef, + asset: ObjectRef, + asset_type_param: TypeTag, + package: ObjectID, +) -> Result { + make_tx( + proposal, + sender_cap, + asset, + asset_type_param, + package, + "conclude_or_cancel", + ) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/asset/try_to_argument.rs b/identity_iota_core/src/rebased/iota/move_calls/asset/try_to_argument.rs new file mode 100644 index 0000000000..31ce4134d7 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/asset/try_to_argument.rs @@ -0,0 +1,33 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::Serialize; +use identity_iota_interaction::{ident_str, MoveType, TypedValue}; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::transaction::{Argument, Command, ProgrammableMoveCall}; +use crate::rebased::Error; + +pub(crate) fn try_to_argument( + content: &T, + ptb: &mut ProgrammableTransactionBuilder, + package: ObjectID, +) -> Result { + match content.get_typed_value(package) { + TypedValue::IotaVerifiableCredential(value) => { + let values = ptb + .pure(value.data()) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + Ok(ptb.command(Command::MoveCall(Box::new(ProgrammableMoveCall { + package, + module: ident_str!("public_vc").into(), + function: ident_str!("new").into(), + type_arguments: vec![], + arguments: vec![values], + })))) + }, + TypedValue::Other(value) => { + ptb.pure(value).map_err(|e| Error::InvalidArgument(e.to_string())) + }, + } +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/asset/update.rs b/identity_iota_core/src/rebased/iota/move_calls/asset/update.rs new file mode 100644 index 0000000000..c94575e8b5 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/asset/update.rs @@ -0,0 +1,38 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::Command; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::ident_str; +use serde::Serialize; + +use identity_iota_interaction::MoveType; +use crate::rebased::Error; + +pub(crate) fn update(asset: ObjectRef, new_content: T, package: ObjectID) -> Result +where + T: MoveType + Serialize, +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + + let asset = ptb + .obj(ObjectArg::ImmOrOwnedObject(asset)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let new_content = ptb + .pure(new_content) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.command(Command::move_call( + package, + ident_str!("asset").into(), + ident_str!("set_content").into(), + vec![T::move_type(package)], + vec![asset, new_content], + )); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/borrow_asset.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/borrow_asset.rs new file mode 100644 index 0000000000..2071a5754c --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/borrow_asset.rs @@ -0,0 +1,219 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; + +use identity_iota_interaction::rpc_types::IotaObjectData; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::base_types::ObjectType; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::Argument; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::ident_str; +use itertools::Itertools; + +use crate::rebased::iota::move_calls::utils; +use crate::rebased::proposals::BorrowAction; +use identity_iota_interaction::MoveType; + +use super::ProposalContext; + +fn borrow_proposal_impl( + identity: OwnedObjectRef, + capability: ObjectRef, + objects: Vec, + expiration: Option, + package_id: ObjectID, +) -> anyhow::Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; + let objects_arg = ptb.pure(objects)?; + + let proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_borrow").into(), + vec![], + vec![identity_arg, delegation_token, exp_arg, objects_arg], + ); + + Ok(ProposalContext { + ptb, + identity: identity_arg, + controller_cap: cap_arg, + delegation_token, + borrow, + proposal_id, + }) +} + +pub(crate) fn propose_borrow( + identity: OwnedObjectRef, + capability: ObjectRef, + objects: Vec, + expiration: Option, + package_id: ObjectID, +) -> Result { + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + .. + } = borrow_proposal_impl(identity, capability, objects, expiration, package_id)?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} + +fn execute_borrow_impl( + ptb: &mut ProgrammableTransactionBuilder, + identity: Argument, + delegation_token: Argument, + proposal_id: Argument, + objects: Vec, + intent_fn: F, + package: ObjectID, +) -> anyhow::Result<()> +where + F: FnOnce(&mut ProgrammableTransactionBuilder, &HashMap), +{ + // Get the proposal's action as argument. + let borrow_action = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_proposal").into(), + vec![BorrowAction::move_type(package)], + vec![identity, delegation_token, proposal_id], + ); + + // Borrow all the objects specified in the action. + let obj_arg_map = objects + .into_iter() + .map(|obj_data| { + let obj_ref = obj_data.object_ref(); + let ObjectType::Struct(obj_type) = obj_data.object_type()? else { + unreachable!("move packages cannot be borrowed to begin with"); + }; + let recv_obj = ptb.obj(ObjectArg::Receiving(obj_ref))?; + + let obj_arg = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_borrow").into(), + vec![obj_type.into()], + vec![identity, borrow_action, recv_obj], + ); + + Ok((obj_ref.0, (obj_arg, obj_data))) + }) + .collect::>()?; + + // Apply the user-defined operation. + intent_fn(ptb, &obj_arg_map); + + // Put back all the objects. + obj_arg_map.into_values().for_each(|(obj_arg, obj_data)| { + let ObjectType::Struct(obj_type) = obj_data.object_type().expect("checked above") else { + unreachable!("move packages cannot be borrowed to begin with"); + }; + ptb.programmable_move_call( + package, + ident_str!("borrow_proposal").into(), + ident_str!("put_back").into(), + vec![obj_type.into()], + vec![borrow_action, obj_arg], + ); + }); + + // Consume the now empty borrow_action + ptb.programmable_move_call( + package, + ident_str!("borrow_proposal").into(), + ident_str!("conclude_borrow").into(), + vec![], + vec![borrow_action], + ); + + Ok(()) +} + +pub(crate) fn execute_borrow( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + objects: Vec, + intent_fn: F, + package: ObjectID, +) -> Result +where + F: FnOnce(&mut ProgrammableTransactionBuilder, &HashMap), +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); + let proposal_id = ptb.pure(proposal_id)?; + + execute_borrow_impl( + &mut ptb, + identity, + delegation_token, + proposal_id, + objects, + intent_fn, + package, + )?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(ptb.finish()) +} + +pub(crate) fn create_and_execute_borrow( + identity: OwnedObjectRef, + capability: ObjectRef, + objects: Vec, + intent_fn: F, + expiration: Option, + package_id: ObjectID, +) -> anyhow::Result +where + F: FnOnce(&mut ProgrammableTransactionBuilder, &HashMap), +{ + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + identity, + proposal_id, + } = borrow_proposal_impl( + identity, + capability, + objects.iter().map(|obj_data| obj_data.object_id).collect_vec(), + expiration, + package_id, + )?; + + execute_borrow_impl( + &mut ptb, + identity, + delegation_token, + proposal_id, + objects, + intent_fn, + package_id, + )?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/config.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/config.rs new file mode 100644 index 0000000000..dd560718d2 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/config.rs @@ -0,0 +1,113 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashSet; +use std::str::FromStr; + +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::types::TypeTag; +use identity_iota_interaction::ident_str; + +use super::super::utils; + +#[allow(clippy::too_many_arguments)] +pub(crate) fn propose_config_change( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + expiration: Option, + threshold: Option, + controllers_to_add: I1, + controllers_to_remove: HashSet, + controllers_to_update: I2, + package: ObjectID, +) -> anyhow::Result +where + I1: IntoIterator, + I2: IntoIterator, +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + + let controllers_to_add = { + let (addresses, vps): (Vec, Vec) = controllers_to_add.into_iter().unzip(); + let addresses = ptb.pure(addresses)?; + let vps = ptb.pure(vps)?; + + ptb.programmable_move_call( + package, + ident_str!("utils").into(), + ident_str!("vec_map_from_keys_values").into(), + vec![TypeTag::Address, TypeTag::U64], + vec![addresses, vps], + ) + }; + let controllers_to_update = { + let (ids, vps): (Vec, Vec) = controllers_to_update.into_iter().unzip(); + let ids = ptb.pure(ids)?; + let vps = ptb.pure(vps)?; + + ptb.programmable_move_call( + package, + ident_str!("utils").into(), + ident_str!("vec_map_from_keys_values").into(), + vec![TypeTag::from_str("0x2::object::ID").expect("valid utf8"), TypeTag::U64], + vec![ids, vps], + ) + }; + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(controller_cap))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); + let expiration = utils::option_to_move(expiration, &mut ptb, package)?; + let threshold = utils::option_to_move(threshold, &mut ptb, package)?; + let controllers_to_remove = ptb.pure(controllers_to_remove)?; + + let _proposal_id = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("propose_config_change").into(), + vec![], + vec![ + identity, + delegation_token, + expiration, + threshold, + controllers_to_add, + controllers_to_remove, + controllers_to_update, + ], + ); + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(ptb.finish()) +} + +pub(crate) fn execute_config_change( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + proposal_id: ObjectID, + package: ObjectID, +) -> anyhow::Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(controller_cap))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); + let proposal_id = ptb.pure(proposal_id)?; + ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_config_change").into(), + vec![], + vec![identity, delegation_token, proposal_id], + ); + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/controller_execution.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/controller_execution.rs new file mode 100644 index 0000000000..efde7e4da6 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/controller_execution.rs @@ -0,0 +1,187 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::Argument; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::ident_str; + +use crate::rebased::iota::move_calls::utils; +use crate::rebased::proposals::ControllerExecution; +use identity_iota_interaction::MoveType; + +use super::ProposalContext; + +fn controller_execution_impl( + identity: OwnedObjectRef, + capability: ObjectRef, + controller_cap_id: ObjectID, + expiration: Option, + package_id: ObjectID, +) -> anyhow::Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap_id = ptb.pure(controller_cap_id)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; + + let proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_controller_execution").into(), + vec![], + vec![identity_arg, delegation_token, controller_cap_id, exp_arg], + ); + + Ok(ProposalContext { + ptb, + controller_cap: cap_arg, + delegation_token, + borrow, + identity: identity_arg, + proposal_id, + }) +} + +pub(crate) fn propose_controller_execution( + identity: OwnedObjectRef, + capability: ObjectRef, + controller_cap_id: ObjectID, + expiration: Option, + package_id: ObjectID, +) -> Result { + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + .. + } = controller_execution_impl(identity, capability, controller_cap_id, expiration, package_id)?; + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} + +fn execute_controller_execution_impl( + ptb: &mut ProgrammableTransactionBuilder, + identity: Argument, + proposal_id: Argument, + delegation_token: Argument, + borrowing_controller_cap_ref: ObjectRef, + intent_fn: F, + package: ObjectID, +) -> anyhow::Result<()> +where + F: FnOnce(&mut ProgrammableTransactionBuilder, &Argument), +{ + // Get the proposal's action as argument. + let controller_execution_action = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_proposal").into(), + vec![ControllerExecution::move_type(package)], + vec![identity, delegation_token, proposal_id], + ); + + // Borrow the controller cap into this transaction. + let receiving = ptb.obj(ObjectArg::Receiving(borrowing_controller_cap_ref))?; + let borrowed_controller_cap = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("borrow_controller_cap").into(), + vec![], + vec![identity, controller_execution_action, receiving], + ); + + // Apply the user-defined operation. + intent_fn(ptb, &borrowed_controller_cap); + + // Put back the borrowed controller cap. + ptb.programmable_move_call( + package, + ident_str!("controller_proposal").into(), + ident_str!("put_back").into(), + vec![], + vec![controller_execution_action, borrowed_controller_cap], + ); + + Ok(()) +} + +pub(crate) fn execute_controller_execution( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + borrowing_controller_cap_ref: ObjectRef, + intent_fn: F, + package: ObjectID, +) -> Result +where + F: FnOnce(&mut ProgrammableTransactionBuilder, &Argument), +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); + let proposal_id = ptb.pure(proposal_id)?; + + execute_controller_execution_impl( + &mut ptb, + identity, + proposal_id, + delegation_token, + borrowing_controller_cap_ref, + intent_fn, + package, + )?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(ptb.finish()) +} + +pub(crate) fn create_and_execute_controller_execution( + identity: OwnedObjectRef, + capability: ObjectRef, + expiration: Option, + borrowing_controller_cap_ref: ObjectRef, + intent_fn: F, + package_id: ObjectID, +) -> anyhow::Result +where + F: FnOnce(&mut ProgrammableTransactionBuilder, &Argument), +{ + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + proposal_id, + identity, + } = controller_execution_impl( + identity, + capability, + borrowing_controller_cap_ref.0, + expiration, + package_id, + )?; + + execute_controller_execution_impl( + &mut ptb, + identity, + proposal_id, + delegation_token, + borrowing_controller_cap_ref, + intent_fn, + package_id, + )?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/create.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/create.rs new file mode 100644 index 0000000000..2d2d29b6c2 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/create.rs @@ -0,0 +1,84 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::types::TypeTag; +use identity_iota_interaction::types::IOTA_FRAMEWORK_PACKAGE_ID; +use identity_iota_interaction::ident_str; + +use crate::rebased::iota::move_calls::utils; +use crate::rebased::Error; + +/// Build a transaction that creates a new on-chain Identity containing `did_doc`. +pub(crate) fn new(did_doc: &[u8], package_id: ObjectID) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let doc_arg = utils::ptb_pure(&mut ptb, "did_doc", did_doc)?; + let clock = utils::get_clock_ref(&mut ptb); + + // Create a new identity, sending its capability to the tx's sender. + let _identity_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("new").into(), + vec![], + vec![doc_arg, clock], + ); + + Ok(ptb.finish()) +} + +pub(crate) fn new_with_controllers( + did_doc: &[u8], + controllers: C, + threshold: u64, + package_id: ObjectID, +) -> Result +where + C: IntoIterator, +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + + let controllers = { + let (ids, vps): (Vec, Vec) = controllers.into_iter().unzip(); + let ids = ptb.pure(ids).map_err(|e| Error::InvalidArgument(e.to_string()))?; + let vps = ptb.pure(vps).map_err(|e| Error::InvalidArgument(e.to_string()))?; + ptb.programmable_move_call( + package_id, + ident_str!("utils").into(), + ident_str!("vec_map_from_keys_values").into(), + vec![TypeTag::Address, TypeTag::U64], + vec![ids, vps], + ) + }; + + let controllers_that_can_delegate = ptb.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("vec_map").into(), + ident_str!("empty").into(), + vec![TypeTag::Address, TypeTag::U64], + vec![], + ); + let doc_arg = ptb.pure(did_doc).map_err(|e| Error::InvalidArgument(e.to_string()))?; + let threshold_arg = ptb.pure(threshold).map_err(|e| Error::InvalidArgument(e.to_string()))?; + let clock = utils::get_clock_ref(&mut ptb); + + // Create a new identity, sending its capabilities to the specified controllers. + let _identity_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("new_with_controllers").into(), + vec![], + vec![ + doc_arg, + controllers, + controllers_that_can_delegate, + threshold_arg, + clock, + ], + ); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/deactivate.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/deactivate.rs new file mode 100644 index 0000000000..bb7ddb9189 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/deactivate.rs @@ -0,0 +1,64 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::ident_str; + +use crate::rebased::iota::move_calls::utils; + +pub(crate) fn propose_deactivation( + identity: OwnedObjectRef, + capability: ObjectRef, + expiration: Option, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; + let clock = utils::get_clock_ref(&mut ptb); + + let _proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_deactivation").into(), + vec![], + vec![identity_arg, delegation_token, exp_arg, clock], + ); + + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} + +pub(crate) fn execute_deactivation( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let proposal_id = ptb.pure(proposal_id)?; + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let clock = utils::get_clock_ref(&mut ptb); + + let _ = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("execute_deactivation").into(), + vec![], + vec![identity_arg, delegation_token, proposal_id, clock], + ); + + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/mod.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/mod.rs new file mode 100644 index 0000000000..fa85ae61d0 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/mod.rs @@ -0,0 +1,32 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod borrow_asset; +mod config; +mod controller_execution; +mod create; +mod deactivate; +pub(crate) mod proposal; +mod send_asset; +mod update; +mod upgrade; + +pub(crate) use borrow_asset::*; +pub(crate) use config::*; +pub(crate) use controller_execution::*; +pub(crate) use create::*; +pub(crate) use deactivate::*; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb; +use iota_sdk::types::transaction::Argument; +pub(crate) use send_asset::*; +pub(crate) use update::*; +pub(crate) use upgrade::*; + +struct ProposalContext { + ptb: Ptb, + controller_cap: Argument, + delegation_token: Argument, + borrow: Argument, + identity: Argument, + proposal_id: Argument, +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/proposal.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/proposal.rs new file mode 100644 index 0000000000..64b4d32e40 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/proposal.rs @@ -0,0 +1,43 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::rebased::iota::move_calls::utils; +use identity_iota_interaction::MoveType; +use crate::rebased::Error; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::ident_str; + +pub(crate) fn approve( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + proposal_id: ObjectID, + package: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let identity = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + let controller_cap = ptb + .obj(ObjectArg::ImmOrOwnedObject(controller_cap)) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, controller_cap, package); + let proposal_id = ptb + .pure(proposal_id) + .map_err(|e| Error::InvalidArgument(e.to_string()))?; + + ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("approve_proposal").into(), + vec![T::move_type(package)], + vec![identity, delegation_token, proposal_id], + ); + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/send_asset.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/send_asset.rs new file mode 100644 index 0000000000..9fd39d6a9a --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/send_asset.rs @@ -0,0 +1,165 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::Argument; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::types::TypeTag; +use identity_iota_interaction::ident_str; + +use crate::rebased::iota::move_calls; +use crate::rebased::proposals::SendAction; +use identity_iota_interaction::MoveType; + +use self::move_calls::utils; +use super::ProposalContext; + +fn send_proposal_impl( + identity: OwnedObjectRef, + capability: ObjectRef, + transfer_map: Vec<(ObjectID, IotaAddress)>, + expiration: Option, + package_id: ObjectID, +) -> anyhow::Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = move_calls::utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let identity_arg = move_calls::utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let exp_arg = move_calls::utils::option_to_move(expiration, &mut ptb, package_id)?; + let (objects, recipients) = { + let (objects, recipients): (Vec<_>, Vec<_>) = transfer_map.into_iter().unzip(); + let objects = ptb.pure(objects)?; + let recipients = ptb.pure(recipients)?; + + (objects, recipients) + }; + + let proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_send").into(), + vec![], + vec![identity_arg, delegation_token, exp_arg, objects, recipients], + ); + + Ok(ProposalContext { + ptb, + identity: identity_arg, + controller_cap: cap_arg, + delegation_token, + borrow, + proposal_id, + }) +} + +pub(crate) fn propose_send( + identity: OwnedObjectRef, + capability: ObjectRef, + transfer_map: Vec<(ObjectID, IotaAddress)>, + expiration: Option, + package_id: ObjectID, +) -> Result { + let ProposalContext { + mut ptb, + controller_cap, + delegation_token, + borrow, + .. + } = send_proposal_impl(identity, capability, transfer_map, expiration, package_id)?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} + +fn execute_send_impl( + ptb: &mut ProgrammableTransactionBuilder, + identity: Argument, + delegation_token: Argument, + proposal_id: Argument, + objects: Vec<(ObjectRef, TypeTag)>, + package: ObjectID, +) -> anyhow::Result<()> { + // Get the proposal's action as argument. + let send_action = ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_proposal").into(), + vec![SendAction::move_type(package)], + vec![identity, delegation_token, proposal_id], + ); + + // Send each object in this send action. + // Traversing the map in reverse reduces the number of operations on the move side. + for (obj, obj_type) in objects.into_iter().rev() { + let recv_obj = ptb.obj(ObjectArg::Receiving(obj))?; + + ptb.programmable_move_call( + package, + ident_str!("identity").into(), + ident_str!("execute_send").into(), + vec![obj_type], + vec![identity, send_action, recv_obj], + ); + } + + // Consume the now empty send_action + ptb.programmable_move_call( + package, + ident_str!("transfer_proposal").into(), + ident_str!("complete_send").into(), + vec![], + vec![send_action], + ); + + Ok(()) +} + +pub(crate) fn execute_send( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + objects: Vec<(ObjectRef, TypeTag)>, + package: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let identity = move_calls::utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let controller_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = move_calls::utils::get_controller_delegation(&mut ptb, controller_cap, package); + let proposal_id = ptb.pure(proposal_id)?; + + execute_send_impl(&mut ptb, identity, delegation_token, proposal_id, objects, package)?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(ptb.finish()) +} + +pub(crate) fn create_and_execute_send( + identity: OwnedObjectRef, + capability: ObjectRef, + transfer_map: Vec<(ObjectID, IotaAddress)>, + expiration: Option, + objects: Vec<(ObjectRef, TypeTag)>, + package: ObjectID, +) -> anyhow::Result { + let ProposalContext { + mut ptb, + identity, + controller_cap, + delegation_token, + borrow, + proposal_id, + } = send_proposal_impl(identity, capability, transfer_map, expiration, package)?; + + execute_send_impl(&mut ptb, identity, delegation_token, proposal_id, objects, package)?; + + utils::put_back_delegation_token(&mut ptb, controller_cap, delegation_token, borrow, package); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/update.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/update.rs new file mode 100644 index 0000000000..59f7560e08 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/update.rs @@ -0,0 +1,66 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::ident_str; + +use crate::rebased::iota::move_calls::utils; + +pub(crate) fn propose_update( + identity: OwnedObjectRef, + capability: ObjectRef, + did_doc: impl AsRef<[u8]>, + expiration: Option, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; + let doc_arg = ptb.pure(did_doc.as_ref())?; + let clock = utils::get_clock_ref(&mut ptb); + + let _proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_update").into(), + vec![], + vec![identity_arg, delegation_token, doc_arg, exp_arg, clock], + ); + + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} + +pub(crate) fn execute_update( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let (delegation_token, borrow) = utils::get_controller_delegation(&mut ptb, cap_arg, package_id); + let proposal_id = ptb.pure(proposal_id)?; + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let clock = utils::get_clock_ref(&mut ptb); + + let _ = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("execute_update").into(), + vec![], + vec![identity_arg, delegation_token, proposal_id, clock], + ); + + utils::put_back_delegation_token(&mut ptb, cap_arg, delegation_token, borrow, package_id); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/identity/upgrade.rs b/identity_iota_core/src/rebased/iota/move_calls/identity/upgrade.rs new file mode 100644 index 0000000000..636e0d0a14 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/identity/upgrade.rs @@ -0,0 +1,56 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::ident_str; + +use crate::rebased::iota::move_calls::utils; + +pub(crate) fn propose_upgrade( + identity: OwnedObjectRef, + capability: ObjectRef, + expiration: Option, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + let exp_arg = utils::option_to_move(expiration, &mut ptb, package_id)?; + + let _proposal_id = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("propose_upgrade").into(), + vec![], + vec![identity_arg, cap_arg, exp_arg], + ); + + Ok(ptb.finish()) +} + +pub(crate) fn execute_upgrade( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + package_id: ObjectID, +) -> Result { + let mut ptb = ProgrammableTransactionBuilder::new(); + let cap_arg = ptb.obj(ObjectArg::ImmOrOwnedObject(capability))?; + let proposal_id = ptb.pure(proposal_id)?; + let identity_arg = utils::owned_ref_to_shared_object_arg(identity, &mut ptb, true)?; + + let _ = ptb.programmable_move_call( + package_id, + ident_str!("identity").into(), + ident_str!("execute_upgrade").into(), + vec![], + vec![identity_arg, cap_arg, proposal_id], + ); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/migration.rs b/identity_iota_core/src/rebased/iota/move_calls/migration.rs new file mode 100644 index 0000000000..6605ac0256 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/migration.rs @@ -0,0 +1,45 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use super::utils; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +use identity_iota_interaction::types::IOTA_FRAMEWORK_PACKAGE_ID; +use identity_iota_interaction::ident_str; + +pub(crate) fn migrate_did_output( + did_output: ObjectRef, + creation_timestamp: Option, + migration_registry: OwnedObjectRef, + package: ObjectID, +) -> anyhow::Result { + let mut ptb = Ptb::new(); + let did_output = ptb.obj(ObjectArg::ImmOrOwnedObject(did_output))?; + let migration_registry = utils::owned_ref_to_shared_object_arg(migration_registry, &mut ptb, true)?; + let clock = utils::get_clock_ref(&mut ptb); + + let creation_timestamp = match creation_timestamp { + Some(timestamp) => ptb.pure(timestamp)?, + _ => ptb.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("clock").into(), + ident_str!("timestamp_ms").into(), + vec![], + vec![clock], + ), + }; + + ptb.programmable_move_call( + package, + ident_str!("migration").into(), + ident_str!("migrate_alias_output").into(), + vec![], + vec![did_output, migration_registry, creation_timestamp, clock], + ); + + Ok(ptb.finish()) +} diff --git a/identity_iota_core/src/rebased/iota/move_calls/mod.rs b/identity_iota_core/src/rebased/iota/move_calls/mod.rs new file mode 100644 index 0000000000..761fffa132 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// Predefined `AuthenticatedAsset`-related PTBs. +pub(crate) mod asset; +/// Predefined `OnChainIdentity`-related PTBs. +pub(crate) mod identity; +/// Predefined PTBs used to migrate a legacy Stardust's AliasOutput +/// to an `OnChainIdentity`. +pub(crate) mod migration; + +mod utils; diff --git a/identity_iota_core/src/rebased/iota/move_calls/utils.rs b/identity_iota_core/src/rebased/iota/move_calls/utils.rs new file mode 100644 index 0000000000..a186236876 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/move_calls/utils.rs @@ -0,0 +1,122 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::MoveType; +use crate::rebased::Error; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::STD_OPTION_MODULE_NAME; +use identity_iota_interaction::types::object::Owner; +use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb; +use identity_iota_interaction::types::transaction::Argument; +use identity_iota_interaction::types::transaction::ObjectArg; +use identity_iota_interaction::types::IOTA_CLOCK_OBJECT_ID; +use identity_iota_interaction::types::IOTA_CLOCK_OBJECT_SHARED_VERSION; +use identity_iota_interaction::types::MOVE_STDLIB_PACKAGE_ID; +use identity_iota_interaction::ident_str; +use serde::Serialize; + +/// Adds a reference to the on-chain clock to `ptb`'s arguments. +pub(crate) fn get_clock_ref(ptb: &mut Ptb) -> Argument { + ptb + .obj(ObjectArg::SharedObject { + id: IOTA_CLOCK_OBJECT_ID, + initial_shared_version: IOTA_CLOCK_OBJECT_SHARED_VERSION, + mutable: false, + }) + .expect("network has a singleton clock instantiated") +} + +pub(crate) fn get_controller_delegation( + ptb: &mut Ptb, + controller_cap: Argument, + package: ObjectID, +) -> (Argument, Argument) { + let Argument::Result(idx) = ptb.programmable_move_call( + package, + ident_str!("controller").into(), + ident_str!("borrow").into(), + vec![], + vec![controller_cap], + ) else { + unreachable!("making move calls always return a result variant"); + }; + + (Argument::NestedResult(idx, 0), Argument::NestedResult(idx, 1)) +} + +pub(crate) fn put_back_delegation_token( + ptb: &mut Ptb, + controller_cap: Argument, + delegation_token: Argument, + borrow: Argument, + package: ObjectID, +) { + ptb.programmable_move_call( + package, + ident_str!("controller").into(), + ident_str!("put_back").into(), + vec![], + vec![controller_cap, delegation_token, borrow], + ); +} + +pub(crate) fn owned_ref_to_shared_object_arg( + owned_ref: OwnedObjectRef, + ptb: &mut Ptb, + mutable: bool, +) -> anyhow::Result { + let Owner::Shared { initial_shared_version } = owned_ref.owner else { + anyhow::bail!("Identity \"{}\" is not a shared object", owned_ref.object_id()); + }; + ptb.obj(ObjectArg::SharedObject { + id: owned_ref.object_id(), + initial_shared_version, + mutable, + }) +} + +pub(crate) fn option_to_move( + option: Option, + ptb: &mut Ptb, + package: ObjectID, +) -> Result { + let arg = if let Some(t) = option { + let t = ptb.pure(t)?; + ptb.programmable_move_call( + MOVE_STDLIB_PACKAGE_ID, + STD_OPTION_MODULE_NAME.into(), + ident_str!("some").into(), + vec![T::move_type(package)], + vec![t], + ) + } else { + ptb.programmable_move_call( + MOVE_STDLIB_PACKAGE_ID, + STD_OPTION_MODULE_NAME.into(), + ident_str!("none").into(), + vec![T::move_type(package)], + vec![], + ) + }; + + Ok(arg) +} + +pub(crate) fn ptb_pure(ptb: &mut Ptb, name: &str, value: T) -> Result +where + T: Serialize + core::fmt::Debug, +{ + ptb.pure(&value).map_err(|err| { + Error::InvalidArgument(format!( + r"could not serialize pure value {name} with value {value:?}; {err}" + )) + }) +} + +#[allow(dead_code)] +pub(crate) fn ptb_obj(ptb: &mut Ptb, name: &str, value: ObjectArg) -> Result { + ptb + .obj(value) + .map_err(|err| Error::InvalidArgument(format!("could not serialize object {name} {value:?}; {err}"))) +} diff --git a/identity_iota_core/src/rebased/iota/types/mod.rs b/identity_iota_core/src/rebased/iota/types/mod.rs new file mode 100644 index 0000000000..fdf352f8ff --- /dev/null +++ b/identity_iota_core/src/rebased/iota/types/mod.rs @@ -0,0 +1,26 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod number; + +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::id::UID; +pub(crate) use number::*; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct Bag { + pub id: UID, + #[serde(deserialize_with = "serde_aux::field_attributes::deserialize_number_from_string")] + pub size: u64, +} + +impl Default for Bag { + fn default() -> Self { + Self { + id: UID::new(ObjectID::ZERO), + size: 0, + } + } +} diff --git a/identity_iota_core/src/rebased/iota/types/number.rs b/identity_iota_core/src/rebased/iota/types/number.rs new file mode 100644 index 0000000000..2e561d0043 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/types/number.rs @@ -0,0 +1,47 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::Deserialize; +use serde::Serialize; +use std::str::FromStr; + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum Number { + Num(N), + Str(String), +} + +macro_rules! impl_conversions { + ($t:ty) => { + impl TryFrom> for $t { + type Error = <$t as FromStr>::Err; + fn try_from(value: Number<$t>) -> Result<$t, Self::Error> { + match value { + Number::Num(n) => Ok(n), + Number::Str(s) => s.parse(), + } + } + } + + impl From<$t> for Number<$t> { + fn from(value: $t) -> Number<$t> { + Number::Num(value) + } + } + }; +} + +impl_conversions!(u8); +impl_conversions!(u16); +impl_conversions!(u32); +impl_conversions!(u64); +impl_conversions!(u128); +impl_conversions!(usize); + +impl_conversions!(i8); +impl_conversions!(i16); +impl_conversions!(i32); +impl_conversions!(i64); +impl_conversions!(i128); +impl_conversions!(isize); diff --git a/identity_iota_core/src/rebased/iota/well_known_networks.rs b/identity_iota_core/src/rebased/iota/well_known_networks.rs new file mode 100644 index 0000000000..7d5a9e2a01 --- /dev/null +++ b/identity_iota_core/src/rebased/iota/well_known_networks.rs @@ -0,0 +1,91 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::types::base_types::ObjectID; +use phf::phf_map; +use phf::Map; + +use crate::NetworkName; + +/// A Mapping `network_id` -> metadata needed by the library. +pub(crate) static IOTA_NETWORKS: Map<&str, IdentityNetworkMetadata> = phf_map! { + "e678123a" => IdentityNetworkMetadata::new( + Some("devnet"), + &["0x03242ae6b87406bd0eb5d669fbe874ed4003694c0be9c6a9ee7c315e6461a553"], + "0x0x940ae1c2c48dade9ec01cc1eebab33ab6fecadda422ea18b105c47839fc64425", + ), + "2304aa97" => IdentityNetworkMetadata::new( + Some("testnet"), + &["0x222741bbdff74b42df48a7b4733185e9b24becb8ccfbafe8eac864ab4e4cc555"], + "0xaacb529c289aec9de2a474faaa4ef68b04632bb6a5d08372ca5b60e3df659f59", + ), +}; + +/// `iota_identity` package information for a given network. +#[derive(Debug)] +pub(crate) struct IdentityNetworkMetadata { + pub alias: Option<&'static str>, + /// `package[0]` is the current version, `package[1]` + /// is the version before, and so forth. + pub package: &'static [&'static str], + pub migration_registry: &'static str, +} + +/// Returns the [`IdentityNetworkMetadata`] for a given network, if any. +pub(crate) fn network_metadata(network_id: &str) -> Option<&'static IdentityNetworkMetadata> { + IOTA_NETWORKS.get(network_id) +} + +impl IdentityNetworkMetadata { + const fn new(alias: Option<&'static str>, pkgs: &'static [&'static str], migration_registry: &'static str) -> Self { + assert!(!pkgs.is_empty()); + Self { + alias, + package: pkgs, + migration_registry, + } + } + + /// Returns the latest `IotaIdentity` package ID on this network. + pub(crate) fn latest_pkg_id(&self) -> ObjectID { + self + .package + .first() + .expect("a package was published") + .parse() + .expect("valid package ID") + } + + /// Returns the ID for the `MigrationRegistry` on this network. + pub(crate) fn migration_registry(&self) -> ObjectID { + self.migration_registry.parse().expect("valid ObjectID") + } + + /// Returns a [`NetworkName`] if `alias` is set. + pub(crate) fn network_alias(&self) -> Option { + self.alias.map(|alias| { + NetworkName::try_from(alias).expect("an hardcoded network alias is valid (unless a dev messed it up)") + }) + } +} + +#[cfg(test)] +mod test { + use identity_iota_interaction::IotaClientBuilder; + + use crate::rebased::client::IdentityClientReadOnly; + + #[tokio::test] + async fn identity_client_connection_to_devnet_works() -> anyhow::Result<()> { + let client = IdentityClientReadOnly::new(IotaClientBuilder::default().build_devnet().await?).await?; + assert_eq!(client.network().as_ref(), "devnet"); + Ok(()) + } + + #[tokio::test] + async fn identity_client_connection_to_testnet_works() -> anyhow::Result<()> { + let client = IdentityClientReadOnly::new(IotaClientBuilder::default().build_testnet().await?).await?; + assert_eq!(client.network().as_ref(), "testnet"); + Ok(()) + } +} diff --git a/identity_iota_core/src/rebased/migration/alias.rs b/identity_iota_core/src/rebased/migration/alias.rs new file mode 100644 index 0000000000..ff9cc94707 --- /dev/null +++ b/identity_iota_core/src/rebased/migration/alias.rs @@ -0,0 +1,198 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use identity_iota_interaction::rpc_types::IotaExecutionStatus; +use identity_iota_interaction::rpc_types::IotaObjectDataOptions; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::id::UID; +use identity_iota_interaction::types::TypeTag; +use identity_iota_interaction::types::STARDUST_PACKAGE_ID; +use secret_storage::Signer; +use serde; +use serde::Deserialize; +use serde::Serialize; + +use crate::iota_interaction_adapter::MigrationMoveCallsAdapter; +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IdentityClientReadOnly; +use crate::rebased::transaction::TransactionInternal; +use crate::rebased::transaction::TransactionOutputInternal; +use crate::rebased::Error; +use identity_iota_interaction::IotaClientTrait; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::MigrationMoveCalls; +use identity_iota_interaction::MoveType; +use identity_iota_interaction::OptionalSync; +use identity_iota_interaction::ProgrammableTransactionBcs; + +use super::get_identity; +use super::Identity; +use super::OnChainIdentity; + +/// A legacy IOTA Stardust Output type, used to store DID Documents. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct UnmigratedAlias { + /// The ID of the Alias = hash of the Output ID that created the Alias Output in Stardust. + /// This is the AliasID from Stardust. + pub id: UID, + + /// The last State Controller address assigned before the migration. + pub legacy_state_controller: Option, + /// A counter increased by 1 every time the alias was state transitioned. + pub state_index: u32, + /// State metadata that can be used to store additional information. + pub state_metadata: Option>, + + /// The sender feature. + pub sender: Option, + + /// The immutable issuer feature. + pub immutable_issuer: Option, + /// The immutable metadata feature. + pub immutable_metadata: Option>, +} + +impl MoveType for UnmigratedAlias { + fn move_type(_: ObjectID) -> TypeTag { + format!("{STARDUST_PACKAGE_ID}::alias::Alias") + .parse() + .expect("valid move type") + } +} + +/// Resolves an [`UnmigratedAlias`] given its ID `object_id`. +pub async fn get_alias(client: &IdentityClientReadOnly, object_id: ObjectID) -> Result, Error> { + match client.get_object_by_id(object_id).await { + Ok(alias) => Ok(Some(alias)), + Err(Error::ObjectLookup(err_msg)) if err_msg.contains("missing data") => Ok(None), + Err(e) => Err(e), + } +} + +cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + // Add wasm32 compatible migrate() function wrapper here + } else { + use crate::rebased::transaction::Transaction; + impl UnmigratedAlias { + /// Returns a transaction that when executed migrates a legacy `Alias` + /// containing a DID Document to a new [`OnChainIdentity`]. + pub async fn migrate(self, client: &IdentityClientReadOnly) + -> Result, Error> { + self.migrate_internal(client).await + } + } + } +} + +impl UnmigratedAlias { + #[allow(unused)] // Currently not supported. + pub(crate) async fn migrate_internal( + self, + client: &IdentityClientReadOnly, + ) -> Result, Error> { + // Try to parse a StateMetadataDocument out of this alias. + let identity = Identity::Legacy(self); + let did_doc = identity.did_document(client.network())?; + let Identity::Legacy(alias) = identity else { + unreachable!("alias was wrapped by us") + }; + // Get the ID of the `AliasOutput` that owns this `Alias`. + let dynamic_field_wrapper = client + .read_api() + .get_object_with_options(*alias.id.object_id(), IotaObjectDataOptions::new().with_owner()) + .await + .map_err(|e| Error::RpcError(e.to_string()))? + .owner() + .expect("owner was requested") + .get_owner_address() + .expect("alias is a dynamic field") + .into(); + let alias_output_id = client + .read_api() + .get_object_with_options(dynamic_field_wrapper, IotaObjectDataOptions::new().with_owner()) + .await + .map_err(|e| Error::RpcError(e.to_string()))? + .owner() + .expect("owner was requested") + .get_owner_address() + .expect("alias is owned by an alias_output") + .into(); + // Get alias_output's ref. + let alias_output_ref = client + .read_api() + .get_object_with_options(alias_output_id, IotaObjectDataOptions::default()) + .await + .map_err(|e| Error::RpcError(e.to_string()))? + .object_ref_if_exists() + .expect("alias_output exists"); + // Get migration registry ref. + let migration_registry_ref = client + .get_object_ref_by_id(client.migration_registry_id()) + .await? + .expect("migration registry exists"); + + // Extract creation metadata + let created = did_doc + .metadata + .created + // `to_unix` returns the seconds since EPOCH; we need milliseconds. + .map(|timestamp| timestamp.to_unix() as u64 * 1000); + + // Build migration tx. + let tx = MigrationMoveCallsAdapter::migrate_did_output( + alias_output_ref, + created, + migration_registry_ref, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(MigrateLegacyAliasTx(tx)) + } +} + +#[derive(Debug)] +struct MigrateLegacyAliasTx(ProgrammableTransactionBcs); + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for MigrateLegacyAliasTx { + type Output = OnChainIdentity; + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let response = self.0.execute_with_opt_gas_internal(gas_budget, client).await?.response; + // Make sure the tx was successful. + let effects_execution_status = response + .effects_execution_status() + .ok_or_else(|| Error::TransactionUnexpectedResponse("transaction had no effects_execution_status".to_string()))?; + if let IotaExecutionStatus::Failure { error } = effects_execution_status { + Err(Error::TransactionUnexpectedResponse(error.to_string())) + } else { + let effects_created = response + .effects_created() + .ok_or_else(|| Error::TransactionUnexpectedResponse("transaction had no effects_created".to_string()))?; + let identity_ref = effects_created + .iter() + .find(|obj_ref| obj_ref.owner.is_shared()) + .ok_or_else(|| { + Error::TransactionUnexpectedResponse("Identity not found in transaction's results".to_string()) + })?; + + get_identity(client, identity_ref.object_id()) + .await + .map(move |identity| TransactionOutputInternal { + output: identity.expect("identity exists on-chain"), + response, + }) + } + } +} diff --git a/identity_iota_core/src/rebased/migration/identity.rs b/identity_iota_core/src/rebased/migration/identity.rs new file mode 100644 index 0000000000..21a83ea794 --- /dev/null +++ b/identity_iota_core/src/rebased/migration/identity.rs @@ -0,0 +1,508 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::collections::HashSet; +use std::str::FromStr; + +use crate::iota_interaction_adapter::IdentityMoveCallsAdapter; +use identity_iota_interaction::IdentityMoveCalls; + +use crate::rebased::iota::types::Number; +use crate::rebased::proposals::Upgrade; +use crate::IotaDID; +use crate::IotaDocument; +use crate::NetworkName; +use crate::StateMetadataDocument; +use crate::StateMetadataEncoding; +use async_trait::async_trait; +use identity_core::common::Timestamp; +use identity_iota_interaction::ident_str; +use identity_iota_interaction::move_types::language_storage::StructTag; +use identity_iota_interaction::rpc_types::IotaObjectData; +use identity_iota_interaction::rpc_types::IotaObjectDataOptions; +use identity_iota_interaction::rpc_types::IotaParsedData; +use identity_iota_interaction::rpc_types::IotaParsedMoveObject; +use identity_iota_interaction::rpc_types::IotaPastObjectResponse; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::id::UID; +use identity_iota_interaction::types::object::Owner; +use identity_iota_interaction::types::TypeTag; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::OptionalSync; +use secret_storage::Signer; +use serde; +use serde::Deserialize; +use serde::Serialize; + +use crate::rebased::client::IdentityClient; +use crate::rebased::client::IdentityClientReadOnly; +use crate::rebased::proposals::BorrowAction; +use crate::rebased::proposals::ConfigChange; +use crate::rebased::proposals::ControllerExecution; +use crate::rebased::proposals::ProposalBuilder; +use crate::rebased::proposals::SendAction; +use crate::rebased::proposals::UpdateDidDocument; +use crate::rebased::rebased_err; +use crate::rebased::transaction::TransactionInternal; +use crate::rebased::transaction::TransactionOutputInternal; +use crate::rebased::Error; +use identity_iota_interaction::IotaClientTrait; +use identity_iota_interaction::MoveType; + +use super::Multicontroller; +use super::UnmigratedAlias; + +const MODULE: &str = "identity"; +const NAME: &str = "Identity"; +const HISTORY_DEFAULT_PAGE_SIZE: usize = 10; + +/// The data stored in an on-chain identity. +pub type IdentityData = ( + UID, + Multicontroller>>, + Option, + Timestamp, + Timestamp, + u64, +); + +/// An on-chain object holding a DID Document. +#[derive(Clone)] +pub enum Identity { + /// A legacy IOTA Stardust's Identity. + Legacy(UnmigratedAlias), + /// An on-chain Identity. + FullFledged(OnChainIdentity), +} + +impl Identity { + /// Returns the [`IotaDocument`] DID Document stored inside this [`Identity`]. + pub fn did_document(&self, network: &NetworkName) -> Result { + match self { + Self::FullFledged(onchain_identity) => Ok(onchain_identity.did_doc.clone()), + Self::Legacy(alias) => { + let state_metadata = alias.state_metadata.as_deref().ok_or_else(|| { + Error::DidDocParsingFailed("legacy stardust alias doesn't contain a DID Document".to_string()) + })?; + let did = IotaDID::from_object_id(&alias.id.object_id().to_string(), network); + StateMetadataDocument::unpack(state_metadata) + .and_then(|state_metadata_doc| state_metadata_doc.into_iota_document(&did)) + .map_err(|e| Error::DidDocParsingFailed(e.to_string())) + } + } + } +} + +/// An on-chain entity that wraps an optional DID Document. +#[derive(Debug, Clone, Serialize)] +pub struct OnChainIdentity { + id: UID, + multi_controller: Multicontroller>>, + pub(crate) did_doc: IotaDocument, + version: u64, +} + +impl OnChainIdentity { + /// Returns the [`ObjectID`] of this [`OnChainIdentity`]. + pub fn id(&self) -> ObjectID { + *self.id.object_id() + } + + /// Returns the [`IotaDocument`] contained in this [`OnChainIdentity`]. + pub fn did_document(&self) -> &IotaDocument { + &self.did_doc + } + + pub(crate) fn did_document_mut(&mut self) -> &mut IotaDocument { + &mut self.did_doc + } + + /// Returns true if this [`OnChainIdentity`] is shared between multiple controllers. + pub fn is_shared(&self) -> bool { + self.multi_controller.controllers().len() > 1 + } + + /// Returns this [`OnChainIdentity`]'s list of active proposals. + pub fn proposals(&self) -> &HashSet { + self.multi_controller.proposals() + } + + /// Returns this [`OnChainIdentity`]'s controllers as the map: `controller_id -> controller_voting_power`. + pub fn controllers(&self) -> &HashMap { + self.multi_controller.controllers() + } + + /// Returns the threshold required by this [`OnChainIdentity`] for executing a proposal. + pub fn threshold(&self) -> u64 { + self.multi_controller.threshold() + } + + /// Returns the voting power of controller with ID `controller_id`, if any. + pub fn controller_voting_power(&self, controller_id: ObjectID) -> Option { + self.multi_controller.controller_voting_power(controller_id) + } + + pub(crate) fn multicontroller(&self) -> &Multicontroller>> { + &self.multi_controller + } + + pub(crate) async fn get_controller_cap(&self, client: &IdentityClient) -> Result { + let controller_cap_tag = StructTag::from_str(&format!("{}::controller::ControllerCap", client.package_id())) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + client + .find_owned_ref(controller_cap_tag, |obj_data| { + self.multi_controller.has_member(obj_data.object_id) + }) + .await? + .ok_or_else(|| Error::Identity("this address has no control over the requested identity".to_string())) + } + + /// Updates this [`OnChainIdentity`]'s DID Document. + pub fn update_did_document(&mut self, updated_doc: IotaDocument) -> ProposalBuilder<'_, UpdateDidDocument> { + ProposalBuilder::new(self, UpdateDidDocument::new(updated_doc)) + } + + /// Updates this [`OnChainIdentity`]'s configuration. + pub fn update_config(&mut self) -> ProposalBuilder<'_, ConfigChange> { + ProposalBuilder::new(self, ConfigChange::default()) + } + + /// Deactivates the DID Document represented by this [`OnChainIdentity`]. + pub fn deactivate_did(&mut self) -> ProposalBuilder<'_, UpdateDidDocument> { + ProposalBuilder::new(self, UpdateDidDocument::deactivate()) + } + + /// Upgrades this [`OnChainIdentity`]'s version to match the package's. + pub fn upgrade_version(&mut self) -> ProposalBuilder<'_, Upgrade> { + ProposalBuilder::new(self, Upgrade) + } + + /// Sends assets owned by this [`OnChainIdentity`] to other addresses. + pub fn send_assets(&mut self) -> ProposalBuilder<'_, SendAction> { + ProposalBuilder::new(self, SendAction::default()) + } + + /// Borrows assets owned by this [`OnChainIdentity`] to use them in a custom transaction. + pub fn borrow_assets(&mut self) -> ProposalBuilder<'_, BorrowAction> { + ProposalBuilder::new(self, BorrowAction::default()) + } + + /// Borrows a `ControllerCap` with ID `controller_cap` owned by this identity in a transaction. + /// This proposal is used to perform operation on a sub-identity controlled + /// by this one. + pub fn controller_execution(&mut self, controller_cap: ObjectID) -> ProposalBuilder<'_, ControllerExecution> { + let action = ControllerExecution::new(controller_cap, self); + ProposalBuilder::new(self, action) + } + + /// Returns historical data for this [`OnChainIdentity`]. + pub async fn get_history( + &self, + client: &IdentityClientReadOnly, + last_version: Option<&IotaObjectData>, + page_size: Option, + ) -> Result, Error> { + let identity_ref = client + .get_object_ref_by_id(self.id()) + .await? + .ok_or_else(|| Error::InvalidIdentityHistory("no reference to identity loaded".to_string()))?; + let object_id = identity_ref.object_id(); + + let mut history: Vec = vec![]; + let mut current_version = if let Some(last_version_value) = last_version { + // starting version given, this will be skipped in paging + last_version_value.clone() + } else { + // no version given, this version will be included in history + let version = identity_ref.version(); + let response = client.get_past_object(object_id, version).await?; + let latest_version = if let IotaPastObjectResponse::VersionFound(response_value) = response { + response_value + } else { + return Err(Error::InvalidIdentityHistory(format!( + "could not find current version {version} of object {object_id}, response {response:?}" + ))); + }; + history.push(latest_version.clone()); // include current version in history if we start from now + latest_version + }; + + // limit lookup count to prevent locking on large histories + let page_size = page_size.unwrap_or(HISTORY_DEFAULT_PAGE_SIZE); + while history.len() < page_size { + let lookup = get_previous_version(client, current_version).await?; + if let Some(value) = lookup { + current_version = value; + history.push(current_version.clone()); + } else { + break; + } + } + + Ok(history) + } +} + +/// Returns the previous version of the given `history_item`. +pub fn has_previous_version(history_item: &IotaObjectData) -> Result { + if let Some(Owner::Shared { initial_shared_version }) = history_item.owner { + Ok(history_item.version != initial_shared_version) + } else { + Err(Error::InvalidIdentityHistory(format!( + "provided history item does not seem to be a valid identity; {history_item}" + ))) + } +} + +async fn get_previous_version( + client: &IdentityClientReadOnly, + iod: IotaObjectData, +) -> Result, Error> { + client.get_previous_version(iod).await.map_err(rebased_err) +} + +/// Returns the [`OnChainIdentity`] having ID `object_id`, if it exists. +pub async fn get_identity( + client: &IdentityClientReadOnly, + object_id: ObjectID, +) -> Result, Error> { + let response = client + .read_api() + .get_object_with_options(object_id, IotaObjectDataOptions::new().with_content()) + .await + .map_err(|err| { + Error::ObjectLookup(format!( + "Could not get object with options for this object_id {object_id}; {err}" + )) + })?; + + // no issues with call but + let Some(data) = response.data else { + // call was successful but no data for alias id + return Ok(None); + }; + + let did = IotaDID::from_object_id(&object_id.to_string(), client.network()); + let Some((id, multi_controller, legacy_id, created, updated, version)) = unpack_identity_data(&did, &data)? else { + return Ok(None); + }; + let legacy_did = legacy_id.map(|legacy_id| IotaDID::from_object_id(&legacy_id.to_string(), client.network())); + + let did_doc = multi_controller + .controlled_value() + .as_deref() + .map(|did_doc_bytes| IotaDocument::from_iota_document_data(did_doc_bytes, true, &did, legacy_did, created, updated)) + .transpose() + .map_err(|e| Error::DidDocParsingFailed(e.to_string()))? + .ok_or_else(|| Error::DIDResolutionError("The requested Identity contains no DID Document".to_string()))?; + + Ok(Some(OnChainIdentity { + id, + multi_controller, + did_doc, + version, + })) +} + +fn is_identity(value: &IotaParsedMoveObject) -> bool { + // if available we might also check if object stems from expected module + // but how would this act upon package updates? + value.type_.module.as_ident_str().as_str() == MODULE && value.type_.name.as_ident_str().as_str() == NAME +} + +/// Unpack identity data from given `IotaObjectData` +/// +/// # Errors: +/// * in case given data for DID is not an object +/// * parsing identity data from object fails +pub(crate) fn unpack_identity_data(did: &IotaDID, data: &IotaObjectData) -> Result, Error> { + let content = data + .clone() + .content + .ok_or_else(|| Error::ObjectLookup(format!("no content in retrieved object in object id {did}")))?; + let IotaParsedData::MoveObject(value) = content else { + return Err(Error::ObjectLookup(format!( + "given data for DID {did} is not an object" + ))); + }; + if !is_identity(&value) { + return Ok(None); + } + + #[derive(Deserialize)] + struct TempOnChainIdentity { + id: UID, + did_doc: Multicontroller>>, + legacy_id: Option, + created: Number, + updated: Number, + version: Number, + } + + let TempOnChainIdentity { + id, + did_doc: multi_controller, + legacy_id, + created, + updated, + version, + } = serde_json::from_value::(value.fields.to_json_value()) + .map_err(|err| Error::ObjectLookup(format!("could not parse identity document with DID {did}; {err}")))?; + + // Parse DID document timestamps + let created = { + let timestamp_ms: u64 = created.try_into().expect("Move string-encoded u64 are valid u64"); + // `Timestamp` requires a timestamp expressed in seconds. + Timestamp::from_unix(timestamp_ms as i64 / 1000).expect("On-chain clock produces valid timestamps") + }; + let updated = { + let timestamp_ms: u64 = updated.try_into().expect("Move string-encoded u64 are valid u64"); + // `Timestamp` requires a timestamp expressed in seconds. + Timestamp::from_unix(timestamp_ms as i64 / 1000).expect("On-chain clock produces valid timestamps") + }; + let version = version.try_into().expect("Move string-encoded u64 are valid u64"); + + Ok(Some((id, multi_controller, legacy_id, created, updated, version))) +} + +/// Builder-style struct to create a new [`OnChainIdentity`]. +#[derive(Debug)] +pub struct IdentityBuilder { + did_doc: IotaDocument, + threshold: Option, + controllers: HashMap, +} + +impl IdentityBuilder { + /// Initializes a new builder for an [`OnChainIdentity`], where the passed `did_doc` will be + /// used as the identity's DID Document. + /// ## Warning + /// Validation of `did_doc` is deferred to [`CreateIdentityTx`]. + pub fn new(did_doc: IotaDocument) -> Self { + Self { + did_doc, + threshold: None, + controllers: HashMap::new(), + } + } + + /// Gives `address` the capability to act as a controller with voting power `voting_power`. + pub fn controller(mut self, address: IotaAddress, voting_power: u64) -> Self { + self.controllers.insert(address, voting_power); + self + } + + /// Sets the identity's threshold. + pub fn threshold(mut self, threshold: u64) -> Self { + self.threshold = Some(threshold); + self + } + + /// Sets multiple controllers in a single step. See [`IdentityBuilder::controller`]. + pub fn controllers(self, controllers: I) -> Self + where + I: IntoIterator, + { + controllers + .into_iter() + .fold(self, |builder, (addr, vp)| builder.controller(addr, vp)) + } + + /// Turns this builder into a [`Transaction`], ready to be executed. + pub fn finish(self) -> CreateIdentityTx { + CreateIdentityTx(self) + } +} + +impl MoveType for OnChainIdentity { + fn move_type(package: ObjectID) -> TypeTag { + TypeTag::Struct(Box::new(StructTag { + address: package.into(), + module: ident_str!("identity").into(), + name: ident_str!("Identity").into(), + type_params: vec![], + })) + } +} + +/// A [`Transaction`] for creating a new [`OnChainIdentity`] from an [`IdentityBuilder`]. +#[derive(Debug)] +pub struct CreateIdentityTx(IdentityBuilder); + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for CreateIdentityTx { + type Output = OnChainIdentity; + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let IdentityBuilder { + did_doc, + threshold, + controllers, + } = self.0; + let did_doc = StateMetadataDocument::from(did_doc) + .pack(StateMetadataEncoding::default()) + .map_err(|e| Error::DidDocSerialization(e.to_string()))?; + let programmable_transaction = if controllers.is_empty() { + IdentityMoveCallsAdapter::new_identity(Some(&did_doc), client.package_id()).await? + } else { + let threshold = match threshold { + Some(t) => t, + None if controllers.len() == 1 => *controllers + .values() + .next() + .ok_or_else(|| Error::Identity("could not get controller".to_string()))?, + None => { + return Err(Error::TransactionBuildingFailed( + "Missing field `threshold` in identity creation".to_owned(), + )) + } + }; + IdentityMoveCallsAdapter::new_with_controllers(Some(&did_doc), controllers, threshold, client.package_id())? + }; + + let response = client.execute_transaction(programmable_transaction, gas_budget).await?; + + let created = response.effects_created().ok_or_else(|| { + Error::TransactionUnexpectedResponse("could not find effects_created in transaction".to_string()) + })?; + let new_identities: Vec = created + .into_iter() + .filter(|elem| { + matches!( + elem.owner, + Owner::Shared { + initial_shared_version: _, + } + ) + }) + .collect(); + let new_identity_id = match &new_identities[..] { + [value] => value.object_id(), + _ => { + return Err(Error::TransactionUnexpectedResponse(format!( + "could not find new identity in response: {}", + response.to_string() + ))); + } + }; + + get_identity(client, new_identity_id) + .await + .and_then(|identity| identity.ok_or_else(|| Error::ObjectLookup(new_identity_id.to_string()))) + .map(move |identity| TransactionOutputInternal { + output: identity, + response, + }) + } +} diff --git a/identity_iota_core/src/rebased/migration/mod.rs b/identity_iota_core/src/rebased/migration/mod.rs new file mode 100644 index 0000000000..ae5470c042 --- /dev/null +++ b/identity_iota_core/src/rebased/migration/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod alias; +mod identity; +mod multicontroller; +mod registry; + +pub use alias::*; +pub use identity::*; +pub use multicontroller::*; +pub use registry::*; diff --git a/identity_iota_core/src/rebased/migration/multicontroller.rs b/identity_iota_core/src/rebased/migration/multicontroller.rs new file mode 100644 index 0000000000..5002fb67d8 --- /dev/null +++ b/identity_iota_core/src/rebased/migration/multicontroller.rs @@ -0,0 +1,205 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::collections::HashSet; + +use crate::rebased::iota::types::Bag; +use crate::rebased::iota::types::Number; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::collection_types::Entry; +use identity_iota_interaction::types::collection_types::VecMap; +use identity_iota_interaction::types::collection_types::VecSet; +use identity_iota_interaction::types::id::UID; +use serde::Deserialize; +use serde::Serialize; + +/// A [`Multicontroller`]'s proposal for changes. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde( + try_from = "IotaProposal::", + into = "IotaProposal::", + bound(serialize = "T: Serialize + Clone") +)] +pub struct Proposal { + id: UID, + expiration_epoch: Option, + votes: u64, + voters: HashSet, + pub(crate) action: T, +} + +impl Proposal { + /// Returns this [Proposal]'s ID. + pub fn id(&self) -> ObjectID { + *self.id.object_id() + } + + /// Returns the votes received by this [`Proposal`]. + pub fn votes(&self) -> u64 { + self.votes + } + + pub(crate) fn votes_mut(&mut self) -> &mut u64 { + &mut self.votes + } + + /// Returns a reference to the action contained by this [`Proposal`]. + pub fn action(&self) -> &T { + &self.action + } + + /// Consumes the [`Proposal`] returning its action. + pub fn into_action(self) -> T { + self.action + } + + /// Returns the set of voters' IDs. + pub fn voters(&self) -> &HashSet { + &self.voters + } + + /// Returns the epoch ID for this proposal's expiration. + pub fn expiration_epoch(&self) -> Option { + self.expiration_epoch + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct IotaProposal { + id: UID, + expiration_epoch: Option>, + votes: Number, + voters: VecSet, + action: T, +} + +impl TryFrom> for Proposal { + type Error = >>::Error; + fn try_from(proposal: IotaProposal) -> Result { + let IotaProposal { + id, + expiration_epoch, + votes, + voters, + action, + } = proposal; + let expiration_epoch = expiration_epoch.map(TryInto::try_into).transpose()?; + let votes = votes.try_into()?; + let voters = voters.contents.into_iter().collect(); + + Ok(Self { + id, + expiration_epoch, + votes, + voters, + action, + }) + } +} + +impl From> for IotaProposal { + fn from(value: Proposal) -> Self { + let Proposal { + id, + expiration_epoch, + votes, + voters, + action, + } = value; + let contents = voters.into_iter().collect(); + IotaProposal { + id, + expiration_epoch: expiration_epoch.map(Into::into), + votes: votes.into(), + voters: VecSet { contents }, + action, + } + } +} + +/// Representation of `identity.rs`'s `multicontroller::Multicontroller` Move type. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(try_from = "IotaMulticontroller::")] +pub struct Multicontroller { + controlled_value: T, + controllers: HashMap, + threshold: u64, + active_proposals: HashSet, + proposals: Bag, +} + +impl Multicontroller { + /// Returns a reference to the value that is shared between many controllers. + pub fn controlled_value(&self) -> &T { + &self.controlled_value + } + + /// Returns this [`Multicontroller`]'s threshold. + pub fn threshold(&self) -> u64 { + self.threshold + } + + /// Returns the lists of active [`Proposal`]s for this [`Multicontroller`]. + pub fn proposals(&self) -> &HashSet { + &self.active_proposals + } + + pub(crate) fn proposals_bag_id(&self) -> ObjectID { + *self.proposals.id.object_id() + } + + /// Returns the voting power for controller with ID `controller_cap_id`, if any. + pub fn controller_voting_power(&self, controller_cap_id: ObjectID) -> Option { + self.controllers.get(&controller_cap_id).copied() + } + + /// Consumes this [`Multicontroller`], returning the wrapped value. + pub fn into_inner(self) -> T { + self.controlled_value + } + + pub(crate) fn controllers(&self) -> &HashMap { + &self.controllers + } + + /// Returns `true` if `cap_id` is among this [`Multicontroller`]'s controllers' IDs. + pub fn has_member(&self, cap_id: ObjectID) -> bool { + self.controllers.contains_key(&cap_id) + } +} + +impl TryFrom> for Multicontroller { + type Error = >>::Error; + fn try_from(value: IotaMulticontroller) -> Result { + let IotaMulticontroller { + controlled_value, + controllers, + threshold, + active_proposals, + proposals, + } = value; + let controllers = controllers + .contents + .into_iter() + .map(|Entry { key: id, value: vp }| (u64::try_from(vp).map(|vp| (id, vp)))) + .collect::>()?; + + Ok(Multicontroller { + controlled_value, + controllers, + threshold: threshold.try_into()?, + active_proposals, + proposals, + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct IotaMulticontroller { + controlled_value: T, + controllers: VecMap>, + threshold: Number, + active_proposals: HashSet, + proposals: Bag, +} diff --git a/identity_iota_core/src/rebased/migration/registry.rs b/identity_iota_core/src/rebased/migration/registry.rs new file mode 100644 index 0000000000..196cf15e16 --- /dev/null +++ b/identity_iota_core/src/rebased/migration/registry.rs @@ -0,0 +1,60 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota_interaction::rpc_types::IotaData; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::id::ID; +use identity_iota_interaction::IotaClientTrait; + +use crate::rebased::client::IdentityClientReadOnly; + +use super::get_identity; +use super::OnChainIdentity; + +/// Errors that can occur during migration registry operations. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// An error occurred while interacting with the IOTA Client. + #[error(transparent)] + Client(anyhow::Error), + /// The MigrationRegistry object was not found. + #[error("could not locate MigrationRegistry object: {0}")] + NotFound(String), + /// The MigrationRegistry object is malformed. + #[error("malformed MigrationRegistry's entry: {0}")] + Malformed(String), +} + +/// Lookup a legacy `alias_id` into the migration registry +/// to get the UID of the corresponding migrated DID document if any. +pub async fn lookup(id_client: &IdentityClientReadOnly, alias_id: ObjectID) -> Result, Error> { + let dynamic_field_name = serde_json::from_value(serde_json::json!({ + "type": "0x2::object::ID", + "value": alias_id.to_string() + })) + .expect("valid move value"); + + let identity_id = id_client + .read_api() + .get_dynamic_field_object(id_client.migration_registry_id(), dynamic_field_name) + .await + .map_err(|e| Error::Client(e.into()))? + .data + .map(|data| { + data + .content + .and_then(|content| content.try_into_move()) + .and_then(|move_object| move_object.fields.to_json_value().get_mut("value").map(std::mem::take)) + .and_then(|value| serde_json::from_value::(value).map(|id| id.bytes).ok()) + .ok_or(Error::Malformed( + "invalid MigrationRegistry's Entry encoding".to_string(), + )) + }) + .transpose()?; + + if let Some(id) = identity_id { + get_identity(id_client, id).await.map_err(|e| Error::Client(e.into())) + } else { + Ok(None) + } +} diff --git a/identity_iota_core/src/rebased/mod.rs b/identity_iota_core/src/rebased/mod.rs new file mode 100644 index 0000000000..44661415da --- /dev/null +++ b/identity_iota_core/src/rebased/mod.rs @@ -0,0 +1,25 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// Module for handling assets. +pub mod assets; +/// Module for handling client operations. +pub mod client; +mod error; +mod iota; +/// Module for handling migration operations. +pub mod migration; +/// Contains the operations of proposals. +pub mod proposals; +/// Module for handling transactions. +pub mod transaction; +/// Contains utility functions. +#[cfg(not(target_arch = "wasm32"))] +pub mod utils; + +pub use assets::*; +pub use error::*; + +/// Integration with IOTA's Keytool. +#[cfg(feature = "keytool-signer")] +pub use identity_iota_interaction::keytool_signer::*; diff --git a/identity_iota_core/src/rebased/proposals/borrow.rs b/identity_iota_core/src/rebased/proposals/borrow.rs new file mode 100644 index 0000000000..762cee30c5 --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/borrow.rs @@ -0,0 +1,326 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::marker::PhantomData; + +use crate::iota_interaction_adapter::AdapterError; +use crate::iota_interaction_adapter::IdentityMoveCallsAdapter; +use crate::iota_interaction_adapter::IotaTransactionBlockResponseAdapter; +use crate::iota_interaction_adapter::NativeTransactionBlockResponse; +use identity_iota_interaction::IdentityMoveCalls; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::IotaTransactionBlockResponseT; + +use crate::rebased::client::IdentityClient; +use crate::rebased::migration::Proposal; +use crate::rebased::transaction::ProtoTransaction; +use crate::rebased::transaction::TransactionInternal; +use crate::rebased::transaction::TransactionOutputInternal; +use crate::rebased::Error; +use async_trait::async_trait; +use identity_iota_interaction::rpc_types::IotaObjectData; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::transaction::Argument; +use identity_iota_interaction::types::TypeTag; +use identity_iota_interaction::MoveType; +use identity_iota_interaction::OptionalSync; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::OnChainIdentity; +use super::ProposalBuilder; +use super::ProposalT; +use super::UserDrivenTx; + +cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + use iota_interaction_ts::NativeTsTransactionBuilderBindingWrapper as Ptb; + /// Instances of BorrowIntentFnT can be used as user-provided function to describe how + /// a borrowed assets shall be used. + pub trait BorrowIntentFnT: FnOnce(&mut Ptb, &HashMap) {} + impl BorrowIntentFnT for T where T: FnOnce(&mut Ptb, &HashMap) {} + /// Boxed dynamic trait object of {@link BorrowIntentFnT} + #[allow(unreachable_pub)] + pub type BorrowIntentFn = Box; + } else { + use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb; + /// Instances of BorrowIntentFnT can be used as user-provided function to describe how + /// a borrowed assets shall be used. + pub trait BorrowIntentFnT: FnOnce(&mut Ptb, &HashMap) {} + impl BorrowIntentFnT for T where T: FnOnce(&mut Ptb, &HashMap) {} + /// Boxed dynamic trait object of {@link BorrowIntentFnT} + #[allow(unreachable_pub)] + pub type BorrowIntentFn = Box; + } +} + +/// Action used to borrow in transaction [OnChainIdentity]'s assets. +#[derive(Deserialize, Serialize)] +pub struct BorrowAction { + objects: Vec, + #[serde(skip, default = "Option::default")] + intent_fn: Option, +} + +impl Default for BorrowAction { + fn default() -> Self { + Self { + objects: vec![], + intent_fn: None, + } + } +} + +/// A [`BorrowAction`] coupled with a user-provided function to describe how +/// the borrowed assets shall be used. +pub struct BorrowActionWithIntent(BorrowAction) +where + F: BorrowIntentFnT; + +impl MoveType for BorrowAction { + fn move_type(package: ObjectID) -> TypeTag { + use std::str::FromStr; + + TypeTag::from_str(&format!("{package}::borrow_proposal::Borrow")).expect("valid move type") + } +} + +impl BorrowAction { + /// Adds an object to the lists of objects that will be borrowed when executing + /// this action in a proposal. + pub fn borrow_object(&mut self, object_id: ObjectID) { + self.objects.push(object_id); + } + + /// Adds many objects. See [`BorrowAction::borrow_object`] for more details. + pub fn borrow_objects(&mut self, objects: I) + where + I: IntoIterator, + { + objects.into_iter().for_each(|obj_id| self.borrow_object(obj_id)); + } +} + +impl<'i, F> ProposalBuilder<'i, BorrowAction> { + /// Adds an object to the list of objects that will be borrowed when executing this action. + pub fn borrow(mut self, object_id: ObjectID) -> Self { + self.action.borrow_object(object_id); + self + } + /// Adds many objects. See [`BorrowAction::borrow_object`] for more details. + pub fn borrow_objects(self, objects: I) -> Self + where + I: IntoIterator, + { + objects.into_iter().fold(self, |builder, obj| builder.borrow(obj)) + } + + /// Specifies how to use the borrowed assets. This is only useful if the sender of this + /// transaction has enough voting power to execute this proposal right-away. + pub fn with_intent(self, intent_fn: F1) -> ProposalBuilder<'i, BorrowAction> + where + F1: FnOnce(&mut Ptb, &HashMap), + { + let ProposalBuilder { + identity, + expiration, + action: BorrowAction { objects, .. }, + } = self; + let intent_fn = Some(intent_fn); + ProposalBuilder { + identity, + expiration, + action: BorrowAction { objects, intent_fn }, + } + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl ProposalT for Proposal> +where + F: BorrowIntentFnT + Send, +{ + type Action = BorrowAction; + type Output = (); + type Response = IotaTransactionBlockResponseAdapter; + + async fn create<'i, S>( + action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + let can_execute = identity + .controller_voting_power(controller_cap_ref.0) + .expect("is a controller of identity") + >= identity.threshold(); + let chained_execution = can_execute && action.intent_fn.is_some(); + let tx = if chained_execution { + // Construct a list of `(ObjectRef, TypeTag)` from the list of objects to send. + let object_data_list = { + let mut object_data_list = vec![]; + for obj_id in action.objects { + let object_data = super::obj_data_for_id(client, obj_id) + .await + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + object_data_list.push(object_data); + } + object_data_list + }; + IdentityMoveCallsAdapter::create_and_execute_borrow( + identity_ref, + controller_cap_ref, + object_data_list, + action.intent_fn.unwrap(), + expiration, + client.package_id(), + ) + } else { + IdentityMoveCallsAdapter::propose_borrow( + identity_ref, + controller_cap_ref, + action.objects, + expiration, + client.package_id(), + ) + } + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(CreateProposalTx { + identity, + tx, + chained_execution, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + _: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let proposal_id = self.id(); + let borrow_action = self.into_action(); + + Ok(UserDrivenTx { + identity, + proposal_id, + action: borrow_action, + }) + } + + fn parse_tx_effects_internal( + _tx_response: &dyn IotaTransactionBlockResponseT< + Error = AdapterError, + NativeResponse = NativeTransactionBlockResponse, + >, + ) -> Result { + Ok(()) + } +} + +impl<'i, F> UserDrivenTx<'i, BorrowAction> { + /// Defines how the borrowed assets should be used. + pub fn with_intent(self, intent_fn: F1) -> UserDrivenTx<'i, BorrowActionWithIntent> + where + F1: BorrowIntentFnT, + { + let UserDrivenTx { + identity, + action: BorrowAction { objects, .. }, + proposal_id, + } = self; + let intent_fn = Some(intent_fn); + UserDrivenTx { + identity, + proposal_id, + action: BorrowActionWithIntent(BorrowAction { objects, intent_fn }), + } + } +} + +impl<'i, F> ProtoTransaction for UserDrivenTx<'i, BorrowAction> { + type Input = BorrowIntentFn; + type Tx = UserDrivenTx<'i, BorrowActionWithIntent>; + + fn with(self, input: Self::Input) -> Self::Tx { + self.with_intent(input) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for UserDrivenTx<'_, BorrowActionWithIntent> +where + F: BorrowIntentFnT + Send, +{ + type Output = (); + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let Self { + identity, + action: borrow_action, + proposal_id, + } = self; + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + // Construct a list of `(ObjectRef, TypeTag)` from the list of objects to send. + let object_data_list = { + let mut object_data_list = vec![]; + for obj_id in borrow_action.0.objects { + let object_data = super::obj_data_for_id(client, obj_id) + .await + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + object_data_list.push(object_data); + } + object_data_list + }; + + let tx = IdentityMoveCallsAdapter::execute_borrow( + identity_ref, + controller_cap_ref, + proposal_id, + object_data_list, + borrow_action + .0 + .intent_fn + .expect("BorrowActionWithIntent makes sure intent_fn is there"), + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + ExecuteProposalTx { + identity, + tx, + _action: PhantomData::, + } + .execute_with_opt_gas_internal(gas_budget, client) + .await + } +} diff --git a/identity_iota_core/src/rebased/proposals/config_change.rs b/identity_iota_core/src/rebased/proposals/config_change.rs new file mode 100644 index 0000000000..a049f7cd2e --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/config_change.rs @@ -0,0 +1,345 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::collections::HashSet; +use std::marker::PhantomData; +use std::ops::DerefMut as _; +use std::str::FromStr as _; + +use crate::iota_interaction_adapter::AdapterError; +use crate::iota_interaction_adapter::IdentityMoveCallsAdapter; +use crate::iota_interaction_adapter::IotaTransactionBlockResponseAdapter; +use crate::iota_interaction_adapter::NativeTransactionBlockResponse; +use identity_iota_interaction::IdentityMoveCalls; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::IotaTransactionBlockResponseT; +use identity_iota_interaction::OptionalSync; + +use crate::rebased::client::IdentityClient; +use crate::rebased::migration::Proposal; +use async_trait::async_trait; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::collection_types::Entry; +use identity_iota_interaction::types::collection_types::VecMap; +use identity_iota_interaction::types::TypeTag; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use crate::rebased::iota::types::Number; +use crate::rebased::migration::OnChainIdentity; +use crate::rebased::Error; +use identity_iota_interaction::MoveType; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::ProposalBuilder; +use super::ProposalT; + +/// [`Proposal`] action that modifies an [`OnChainIdentity`]'s configuration - e.g: +/// - remove controllers +/// - add controllers +/// - update controllers voting powers +/// - update threshold +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(try_from = "Modify")] +pub struct ConfigChange { + threshold: Option, + controllers_to_add: HashMap, + controllers_to_remove: HashSet, + controllers_voting_power: HashMap, +} + +impl MoveType for ConfigChange { + fn move_type(package: ObjectID) -> TypeTag { + TypeTag::from_str(&format!("{package}::config_proposal::Modify")).expect("valid type tag") + } +} + +impl ProposalBuilder<'_, ConfigChange> { + /// Sets a new value for the identity's threshold. + pub fn threshold(mut self, threshold: u64) -> Self { + self.set_threshold(threshold); + self + } + + /// Makes address `address` a new controller with voting power `voting_power`. + pub fn add_controller(mut self, address: IotaAddress, voting_power: u64) -> Self { + self.deref_mut().add_controller(address, voting_power); + self + } + + /// Adds multiple controllers. See [`ProposalBuilder::add_controller`]. + pub fn add_multiple_controllers(mut self, controllers: I) -> Self + where + I: IntoIterator, + { + self.deref_mut().add_multiple_controllers(controllers); + self + } + + /// Removes an existing controller. + pub fn remove_controller(mut self, controller_id: ObjectID) -> Self { + self.deref_mut().remove_controller(controller_id); + self + } + + /// Removes many controllers. + pub fn remove_multiple_controllers(mut self, controllers: I) -> Self + where + I: IntoIterator, + { + self.deref_mut().remove_multiple_controllers(controllers); + self + } + + /// Sets a new voting power for a controller. + pub fn update_controller(mut self, controller_id: ObjectID, voting_power: u64) -> Self { + self.action.controllers_voting_power.insert(controller_id, voting_power); + self + } + + /// Updates many controllers' voting power. + pub fn update_multiple_controllers(mut self, controllers: I) -> Self + where + I: IntoIterator, + { + let controllers_to_update = &mut self.action.controllers_voting_power; + for (id, vp) in controllers { + controllers_to_update.insert(id, vp); + } + + self + } +} + +impl ConfigChange { + /// Creates a new [`ConfigChange`] proposal action. + pub fn new() -> Self { + Self::default() + } + + /// Sets the new threshold. + pub fn set_threshold(&mut self, new_threshold: u64) { + self.threshold = Some(new_threshold); + } + + /// Returns the value for the new threshold. + pub fn threshold(&self) -> Option { + self.threshold + } + + /// Returns the controllers that will be added, as the map [IotaAddress] -> [u64]. + pub fn controllers_to_add(&self) -> &HashMap { + &self.controllers_to_add + } + + /// Returns the set of controllers that will be removed. + pub fn controllers_to_remove(&self) -> &HashSet { + &self.controllers_to_remove + } + + /// Returns the controllers that will be updated as the map [IotaAddress] -> [u64]. + pub fn controllers_to_update(&self) -> &HashMap { + &self.controllers_voting_power + } + + /// Adds a controller. + pub fn add_controller(&mut self, address: IotaAddress, voting_power: u64) { + self.controllers_to_add.insert(address, voting_power); + } + + /// Adds many controllers. + pub fn add_multiple_controllers(&mut self, controllers: I) + where + I: IntoIterator, + { + for (addr, vp) in controllers { + self.add_controller(addr, vp) + } + } + + /// Removes an existing controller. + pub fn remove_controller(&mut self, controller_id: ObjectID) { + self.controllers_to_remove.insert(controller_id); + } + + /// Removes many controllers. + pub fn remove_multiple_controllers(&mut self, controllers: I) + where + I: IntoIterator, + { + for controller in controllers { + self.remove_controller(controller) + } + } + + fn validate(&self, identity: &OnChainIdentity) -> Result<(), Error> { + let new_threshold = self.threshold.unwrap_or(identity.threshold()); + let mut controllers = identity.controllers().clone(); + // check if update voting powers is valid + for (controller, new_vp) in &self.controllers_voting_power { + match controllers.get_mut(controller) { + Some(vp) => *vp = *new_vp, + None => { + return Err(Error::InvalidConfig(format!( + "object \"{controller}\" is not among identity \"{}\"'s controllers", + identity.id() + ))) + } + } + } + // check if deleting controllers is valid + for controller in &self.controllers_to_remove { + if controllers.remove(controller).is_none() { + return Err(Error::InvalidConfig(format!( + "object \"{controller}\" is not among identity \"{}\"'s controllers", + identity.id() + ))); + } + } + // check if adding controllers is valid + for (controller, vp) in &self.controllers_to_add { + if controllers.insert((*controller).into(), *vp).is_some() { + return Err(Error::InvalidConfig(format!( + "object \"{controller}\" is already among identity \"{}\"'s controllers", + identity.id() + ))); + } + } + // check whether the new threshold allows to interact with the identity + if new_threshold > controllers.values().sum() { + return Err(Error::InvalidConfig( + "the resulting configuration will result in an unaccessible identity".to_string(), + )); + } + Ok(()) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl ProposalT for Proposal { + type Action = ConfigChange; + type Output = (); + type Response = IotaTransactionBlockResponseAdapter; + + async fn create<'i, S>( + action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + // Check the validity of the proposed changes. + action.validate(identity)?; + + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + let sender_vp = identity + .controller_voting_power(controller_cap_ref.0) + .expect("controller exists"); + let chained_execution = sender_vp >= identity.threshold(); + let tx = IdentityMoveCallsAdapter::propose_config_change( + identity_ref, + controller_cap_ref, + expiration, + action.threshold, + action.controllers_to_add, + action.controllers_to_remove, + action.controllers_voting_power, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(CreateProposalTx { + identity, + tx, + chained_execution, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let proposal_id = self.id(); + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + let tx = IdentityMoveCallsAdapter::execute_config_change( + identity_ref, + controller_cap_ref, + proposal_id, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(ExecuteProposalTx { + identity, + tx, + _action: PhantomData, + }) + } + + fn parse_tx_effects_internal( + _tx_response: &dyn IotaTransactionBlockResponseT< + Error = AdapterError, + NativeResponse = NativeTransactionBlockResponse, + >, + ) -> Result { + Ok(()) + } +} + +#[derive(Debug, Deserialize)] +struct Modify { + threshold: Option>, + controllers_to_add: VecMap>, + controllers_to_remove: HashSet, + controllers_to_update: VecMap>, +} + +impl TryFrom for ConfigChange { + type Error = >>::Error; + fn try_from(value: Modify) -> Result { + let Modify { + threshold, + controllers_to_add, + controllers_to_remove, + controllers_to_update, + } = value; + let threshold = threshold.map(|num| num.try_into()).transpose()?; + let controllers_to_add = controllers_to_add + .contents + .into_iter() + .map(|Entry { key, value }| value.try_into().map(|n| (key, n))) + .collect::>()?; + let controllers_to_update = controllers_to_update + .contents + .into_iter() + .map(|Entry { key, value }| value.try_into().map(|n| (key, n))) + .collect::>()?; + Ok(Self { + threshold, + controllers_to_add, + controllers_to_remove, + controllers_voting_power: controllers_to_update, + }) + } +} diff --git a/identity_iota_core/src/rebased/proposals/controller.rs b/identity_iota_core/src/rebased/proposals/controller.rs new file mode 100644 index 0000000000..c51b4d8057 --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/controller.rs @@ -0,0 +1,337 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::marker::PhantomData; + +use crate::iota_interaction_adapter::AdapterError; +use crate::iota_interaction_adapter::IdentityMoveCallsAdapter; +use crate::iota_interaction_adapter::IotaTransactionBlockResponseAdapter; +use crate::iota_interaction_adapter::NativeTransactionBlockResponse; +use identity_iota_interaction::IdentityMoveCalls; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::IotaTransactionBlockResponseT; + +use crate::rebased::client::IdentityClient; +use crate::rebased::migration::Proposal; +use crate::rebased::transaction::ProtoTransaction; +use crate::rebased::transaction::TransactionInternal; +use crate::rebased::transaction::TransactionOutputInternal; +use crate::rebased::Error; +use async_trait::async_trait; +use identity_iota_interaction::rpc_types::IotaObjectRef; +use identity_iota_interaction::rpc_types::OwnedObjectRef; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::transaction::Argument; +use identity_iota_interaction::types::TypeTag; +use identity_iota_interaction::MoveType; +use identity_iota_interaction::OptionalSync; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::OnChainIdentity; +use super::ProposalBuilder; +use super::ProposalT; +use super::UserDrivenTx; + +cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + use iota_interaction_ts::NativeTsTransactionBuilderBindingWrapper as Ptb; + /// Instances of ControllerIntentFnT can be used as user-provided function to describe how + /// a borrowed identity's controller capability will be used. + pub trait ControllerIntentFnT: FnOnce(&mut Ptb, &Argument) {} + impl ControllerIntentFnT for T where T: FnOnce(&mut Ptb, &Argument) {} + #[allow(unreachable_pub)] + /// Boxed dynamic trait object of {@link ControllerIntentFnT} + pub type ControllerIntentFn = Box; + } else { + use identity_iota_interaction::types::programmable_transaction_builder::ProgrammableTransactionBuilder as Ptb; + /// Instances of ControllerIntentFnT can be used as user-provided function to describe how + /// a borrowed identity's controller capability will be used. + pub trait ControllerIntentFnT: FnOnce(&mut Ptb, &Argument) {} + impl ControllerIntentFnT for T where T: FnOnce(&mut Ptb, &Argument) {} + #[allow(unreachable_pub)] + /// Boxed dynamic trait object of {@link ControllerIntentFnT} + pub type ControllerIntentFn = Box; + } +} + +/// Borrow an [`OnChainIdentity`]'s controller capability to exert control on +/// a sub-owned identity. +#[derive(Debug, Deserialize, Serialize)] +pub struct ControllerExecution { + controller_cap: ObjectID, + identity: IotaAddress, + #[serde(skip, default = "Option::default")] + intent_fn: Option, +} + +/// A [`ControllerExecution`] action coupled with a user-provided function to describe how +/// the borrowed identity's controller capability will be used. +pub struct ControllerExecutionWithIntent(ControllerExecution) +where + F: FnOnce(&mut Ptb, &Argument); + +impl ControllerExecutionWithIntent +where + F: ControllerIntentFnT, +{ + fn new(action: ControllerExecution) -> Self { + debug_assert!(action.intent_fn.is_some()); + Self(action) + } +} + +impl ControllerExecution { + /// Creates a new [`ControllerExecution`] action, allowing a controller of `identity` to + /// borrow `identity`'s controller cap for a transaction. + pub fn new(controller_cap: ObjectID, identity: &OnChainIdentity) -> Self { + Self { + controller_cap, + identity: identity.id().into(), + intent_fn: None, + } + } + + /// Specifies how the borrowed `ControllerCap` should be used in the transaction. + /// This is only useful if the controller creating this proposal has enough voting + /// power to carry out it out immediately. + pub fn with_intent(self, intent_fn: F1) -> ControllerExecution + where + F1: FnOnce(&mut Ptb, &Argument), + { + let Self { + controller_cap, + identity, + .. + } = self; + ControllerExecution { + controller_cap, + identity, + intent_fn: Some(intent_fn), + } + } +} + +impl<'i, F> ProposalBuilder<'i, ControllerExecution> { + /// Specifies how the borrowed `ControllerCap` should be used in the transaction. + /// This is only useful if the controller creating this proposal has enough voting + /// power to carry out it out immediately. + pub fn with_intent(self, intent_fn: F1) -> ProposalBuilder<'i, ControllerExecution> + where + F1: FnOnce(&mut Ptb, &Argument), + { + let ProposalBuilder { + identity, + expiration, + action, + } = self; + ProposalBuilder { + identity, + expiration, + action: action.with_intent(intent_fn), + } + } +} + +impl MoveType for ControllerExecution { + fn move_type(package: ObjectID) -> TypeTag { + use std::str::FromStr; + + TypeTag::from_str(&format!("{package}::controller_proposal::ControllerExecution")).expect("valid move type") + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl ProposalT for Proposal> +where + F: ControllerIntentFnT + Send, +{ + type Action = ControllerExecution; + type Output = (); + type Response = IotaTransactionBlockResponseAdapter; + + async fn create<'i, S>( + action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + let chained_execution = action.intent_fn.is_some() + && identity + .controller_voting_power(controller_cap_ref.0) + .expect("is an identity's controller") + >= identity.threshold(); + + let tx = if chained_execution { + let borrowing_controller_cap_ref = client + .get_object_ref_by_id(action.controller_cap) + .await? + .map(|OwnedObjectRef { reference, .. }| { + let IotaObjectRef { + object_id, + version, + digest, + } = reference; + (object_id, version, digest) + }) + .ok_or_else(|| Error::ObjectLookup(format!("object {} doesn't exist", action.controller_cap)))?; + + IdentityMoveCallsAdapter::create_and_execute_controller_execution( + identity_ref, + controller_cap_ref, + expiration, + borrowing_controller_cap_ref, + action.intent_fn.unwrap(), + client.package_id(), + ) + } else { + IdentityMoveCallsAdapter::propose_controller_execution( + identity_ref, + controller_cap_ref, + action.controller_cap, + expiration, + client.package_id(), + ) + } + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(CreateProposalTx { + identity, + tx, + chained_execution, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + _: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let proposal_id = self.id(); + let controller_execution_action = self.into_action(); + + Ok(UserDrivenTx { + identity, + proposal_id, + action: controller_execution_action, + }) + } + + fn parse_tx_effects_internal( + _tx_response: &dyn IotaTransactionBlockResponseT< + Error = AdapterError, + NativeResponse = NativeTransactionBlockResponse, + >, + ) -> Result { + Ok(()) + } +} + +impl<'i, F> UserDrivenTx<'i, ControllerExecution> { + /// Defines how the borrowed assets should be used. + pub fn with_intent(self, intent_fn: F1) -> UserDrivenTx<'i, ControllerExecutionWithIntent> + where + F1: ControllerIntentFnT, + { + let UserDrivenTx { + identity, + action, + proposal_id, + } = self; + + UserDrivenTx { + identity, + proposal_id, + action: ControllerExecutionWithIntent::new(action.with_intent(intent_fn)), + } + } +} + +impl<'i, F> ProtoTransaction for UserDrivenTx<'i, ControllerExecution> { + type Input = ControllerIntentFn; + type Tx = UserDrivenTx<'i, ControllerExecutionWithIntent>; + + fn with(self, input: Self::Input) -> Self::Tx { + self.with_intent(input) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for UserDrivenTx<'_, ControllerExecutionWithIntent> +where + F: ControllerIntentFnT + Send, +{ + type Output = (); + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let Self { + identity, + action, + proposal_id, + } = self; + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + let borrowing_cap_id = action.0.controller_cap; + let borrowing_controller_cap_ref = client + .get_object_ref_by_id(borrowing_cap_id) + .await? + .map(|OwnedObjectRef { reference, .. }| { + let IotaObjectRef { + object_id, + version, + digest, + } = reference; + (object_id, version, digest) + }) + .ok_or_else(|| Error::ObjectLookup(format!("object {borrowing_cap_id} doesn't exist")))?; + + let tx = IdentityMoveCallsAdapter::execute_controller_execution( + identity_ref, + controller_cap_ref, + proposal_id, + borrowing_controller_cap_ref, + action + .0 + .intent_fn + .expect("BorrowActionWithIntent makes sure intent_fn is present"), + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + ExecuteProposalTx { + identity, + tx, + _action: PhantomData::, + } + .execute_with_opt_gas_internal(gas_budget, client) + .await + } +} diff --git a/identity_iota_core/src/rebased/proposals/mod.rs b/identity_iota_core/src/rebased/proposals/mod.rs new file mode 100644 index 0000000000..d24fa1736e --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/mod.rs @@ -0,0 +1,395 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod borrow; +mod config_change; +mod controller; +mod send; +mod update_did_doc; +mod upgrade; + +use std::marker::PhantomData; +use std::ops::Deref; +use std::ops::DerefMut; + +cfg_if::cfg_if! { + if #[cfg(not(target_arch = "wasm32"))] { + use identity_iota_interaction::rpc_types::IotaTransactionBlockResponse; + use crate::rebased::transaction::Transaction; + use crate::iota_interaction_adapter::IotaTransactionBlockResponseAdapter; + } +} +use crate::iota_interaction_adapter::AdapterError; +use crate::iota_interaction_adapter::IdentityMoveCallsAdapter; +use crate::iota_interaction_adapter::NativeTransactionBlockResponse; + +use identity_iota_interaction::IdentityMoveCalls; +use identity_iota_interaction::IotaClientTrait; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::IotaTransactionBlockResponseT; +use identity_iota_interaction::OptionalSend; +use identity_iota_interaction::OptionalSync; +use identity_iota_interaction::ProgrammableTransactionBcs; + +use crate::rebased::client::IdentityClientReadOnly; +use crate::rebased::migration::get_identity; +use crate::rebased::transaction::ProtoTransaction; +use crate::rebased::transaction::TransactionInternal; +use crate::rebased::transaction::TransactionOutputInternal; +use async_trait::async_trait; +pub use borrow::*; +pub use config_change::*; +pub use controller::*; +use identity_iota_interaction::rpc_types::IotaExecutionStatus; +use identity_iota_interaction::rpc_types::IotaObjectData; +use identity_iota_interaction::rpc_types::IotaObjectDataOptions; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::base_types::ObjectRef; +use identity_iota_interaction::types::base_types::ObjectType; +use identity_iota_interaction::types::TypeTag; +use secret_storage::Signer; +pub use send::*; +use serde::de::DeserializeOwned; +pub use update_did_doc::*; +pub use upgrade::*; + +use crate::rebased::client::IdentityClient; +use crate::rebased::migration::OnChainIdentity; +use crate::rebased::migration::Proposal; +use crate::rebased::Error; +use identity_iota_interaction::MoveType; + +cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + /// The internally used [`Transaction`] resulting from a proposal + pub trait ResultingTransactionT: TransactionInternal {} + impl ResultingTransactionT for T where T: TransactionInternal {} + } else { + /// The [`Transaction`] resulting from a proposal + pub trait ResultingTransactionT: Transaction {} + impl ResultingTransactionT for T where T: Transaction {} + } +} + +/// Interface that allows the creation and execution of an [`OnChainIdentity`]'s [`Proposal`]s. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait ProposalT: Sized { + /// The [`Proposal`] action's type. + type Action; + /// The output of the [`Proposal`] + type Output; + /// Platform-agnostic type of the IotaTransactionBlockResponse + type Response: IotaTransactionBlockResponseT; + + /// Creates a new [`Proposal`] with the provided action and expiration. + async fn create<'i, S>( + action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result>, Error> + where + S: Signer + OptionalSync; + + /// Converts the [`Proposal`] into a transaction that can be executed. + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result + where + S: Signer + OptionalSync; + + #[cfg(not(target_arch = "wasm32"))] + /// Parses the transaction's effects and returns the output of the [`Proposal`]. + fn parse_tx_effects(tx_response: &IotaTransactionBlockResponse) -> Result { + let adapter = IotaTransactionBlockResponseAdapter::new(tx_response.clone()); + Self::parse_tx_effects_internal(&adapter) + } + + /// For internal platform-agnostic usage only. + fn parse_tx_effects_internal( + tx_response: &dyn IotaTransactionBlockResponseT< + Error = AdapterError, + NativeResponse = NativeTransactionBlockResponse, + >, + ) -> Result; +} + +impl Proposal { + /// Creates a new [`ApproveProposalTx`] for the provided [`Proposal`] + pub fn approve<'i>(&mut self, identity: &'i OnChainIdentity) -> ApproveProposalTx<'_, 'i, A> { + ApproveProposalTx { + proposal: self, + identity, + } + } +} + +/// A builder for creating a [`Proposal`]. +#[derive(Debug)] +pub struct ProposalBuilder<'i, A> { + identity: &'i mut OnChainIdentity, + expiration: Option, + action: A, +} + +impl Deref for ProposalBuilder<'_, A> { + type Target = A; + fn deref(&self) -> &Self::Target { + &self.action + } +} + +impl DerefMut for ProposalBuilder<'_, A> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.action + } +} + +impl<'i, A> ProposalBuilder<'i, A> { + pub(crate) fn new(identity: &'i mut OnChainIdentity, action: A) -> Self { + Self { + identity, + expiration: None, + action, + } + } + + /// Sets the expiration epoch for the [`Proposal`]. + pub fn expiration_epoch(mut self, exp: u64) -> Self { + self.expiration = Some(exp); + self + } + + /// Creates a [`Proposal`] with the provided arguments. If `forbid_chained_execution` is set to `true`, + /// the [`Proposal`] won't be executed even if creator alone has enough voting power. + pub async fn finish<'c, S>( + self, + client: &'c IdentityClient, + ) -> Result>> + use<'i, 'c, S, A>, Error> + where + Proposal: ProposalT, + S: Signer + OptionalSync, + A: 'c, + 'i: 'c, + { + let Self { + action, + expiration, + identity, + } = self; + Proposal::::create(action, expiration, identity, client).await + } +} + +#[derive(Debug)] +/// The result of creating a [`Proposal`]. When a [`Proposal`] is executed +/// in the same transaction as its creation, a [`ProposalResult::Executed`] is +/// returned. [`ProposalResult::Pending`] otherwise. +pub enum ProposalResult { + /// A [`Proposal`] that has yet to be executed. + Pending(P), + /// A [`Proposal`]'s execution output. + Executed(P::Output), +} + +/// A transaction to create a [`Proposal`]. +#[derive(Debug)] +pub struct CreateProposalTx<'i, A> { + identity: &'i mut OnChainIdentity, + tx: ProgrammableTransactionBcs, + chained_execution: bool, + _action: PhantomData, +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for CreateProposalTx<'_, A> +where + Proposal: ProposalT + DeserializeOwned, + A: Send, +{ + type Output = ProposalResult>; + + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result>>, Error> + where + S: Signer + OptionalSync, + { + let Self { + identity, + tx, + chained_execution, + .. + } = self; + let tx_response = client.execute_transaction(tx, gas_budget).await?; + let tx_effects_execution_status = tx_response + .effects_execution_status() + .ok_or_else(|| Error::TransactionUnexpectedResponse("missing transaction's effects".to_string()))?; + + if let IotaExecutionStatus::Failure { error } = tx_effects_execution_status { + return Err(Error::TransactionUnexpectedResponse(error.clone())); + } + + // Identity has been changed regardless of whether the proposal has been executed + // or simply created. Refetch it, to sync it with its on-chain state. + *identity = get_identity(client, identity.id()) + .await? + .expect("identity exists on-chain"); + + if chained_execution { + // The proposal has been created and executed right-away. Parse its effects. + Proposal::::parse_tx_effects_internal(tx_response.as_ref()).map(ProposalResult::Executed) + } else { + // 2 objects are created, one is the Bag's Field and the other is our Proposal. Proposal is not owned by the bag, + // but the field is. + let proposals_bag_id = identity.multicontroller().proposals_bag_id(); + let proposal_id = tx_response + .effects_created() + .ok_or_else(|| Error::TransactionUnexpectedResponse("transaction had no effects".to_string()))? + .iter() + .find(|obj_ref| obj_ref.owner != proposals_bag_id) + .expect("tx was successful") + .object_id(); + + client.get_object_by_id(proposal_id).await.map(ProposalResult::Pending) + } + .map(move |output| TransactionOutputInternal { + output, + response: tx_response, + }) + } +} + +/// A transaction to execute a [`Proposal`]. +#[derive(Debug)] +pub struct ExecuteProposalTx<'i, A> { + tx: ProgrammableTransactionBcs, + identity: &'i mut OnChainIdentity, + _action: PhantomData, +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for ExecuteProposalTx<'_, A> +where + Proposal: ProposalT, + A: OptionalSend, +{ + type Output = as ProposalT>::Output; + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let Self { identity, tx, .. } = self; + let tx_response = client.execute_transaction(tx, gas_budget).await?; + let tx_effects_execution_status = tx_response + .effects_execution_status() + .ok_or_else(|| Error::TransactionUnexpectedResponse("missing effects".to_string()))?; + + if let IotaExecutionStatus::Failure { error } = tx_effects_execution_status { + Err(Error::TransactionUnexpectedResponse(error.clone())) + } else { + *identity = get_identity(client, identity.id()) + .await? + .expect("identity exists on-chain"); + + Proposal::::parse_tx_effects_internal(tx_response.as_ref()).map(move |output| TransactionOutputInternal { + output, + response: tx_response, + }) + } + } +} + +/// A transaction to approve a [`Proposal`]. +#[derive(Debug)] +pub struct ApproveProposalTx<'p, 'i, A> { + proposal: &'p mut Proposal, + identity: &'i OnChainIdentity, +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for ApproveProposalTx<'_, '_, A> +where + Proposal: ProposalT, + A: MoveType + Send, +{ + type Output = (); + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let Self { proposal, identity, .. } = self; + let identity_ref = client.get_object_ref_by_id(identity.id()).await?.unwrap(); + let controller_cap = identity.get_controller_cap(client).await?; + let tx = ::approve_proposal::( + identity_ref.clone(), + controller_cap, + proposal.id(), + client.package_id(), + )?; + + let response = client.execute_transaction(tx, gas_budget).await?; + let tx_effects_execution_status = response + .effects_execution_status() + .ok_or_else(|| Error::TransactionUnexpectedResponse("missing effects".to_string()))?; + + if let IotaExecutionStatus::Failure { error } = tx_effects_execution_status { + return Err(Error::TransactionUnexpectedResponse(error.clone())); + } + + let vp = identity + .controller_voting_power(controller_cap.0) + .expect("is identity's controller"); + *proposal.votes_mut() = proposal.votes() + vp; + + Ok(TransactionOutputInternal { output: (), response }) + } +} + +async fn obj_data_for_id(client: &IdentityClientReadOnly, obj_id: ObjectID) -> anyhow::Result { + use anyhow::Context; + + client + .read_api() + .get_object_with_options(obj_id, IotaObjectDataOptions::default().with_type().with_owner()) + .await? + .into_object() + .context("no iota object in response") +} + +async fn obj_ref_and_type_for_id( + client: &IdentityClientReadOnly, + obj_id: ObjectID, +) -> anyhow::Result<(ObjectRef, TypeTag)> { + let res = obj_data_for_id(client, obj_id).await?; + let obj_ref = res.object_ref(); + let obj_type = match res.object_type().expect("object type is requested") { + ObjectType::Package => anyhow::bail!("a move package cannot be sent"), + ObjectType::Struct(type_) => type_.into(), + }; + + Ok((obj_ref, obj_type)) +} + +/// A transaction that requires user input in order to be executed. +pub struct UserDrivenTx<'i, A> { + identity: &'i mut OnChainIdentity, + action: A, + proposal_id: ObjectID, +} diff --git a/identity_iota_core/src/rebased/proposals/send.rs b/identity_iota_core/src/rebased/proposals/send.rs new file mode 100644 index 0000000000..7b302196e1 --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/send.rs @@ -0,0 +1,224 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::marker::PhantomData; + +use async_trait::async_trait; +use identity_iota_interaction::types::base_types::IotaAddress; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::TypeTag; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use crate::iota_interaction_adapter::AdapterError; +use crate::iota_interaction_adapter::IdentityMoveCallsAdapter; +use crate::iota_interaction_adapter::IotaTransactionBlockResponseAdapter; +use crate::iota_interaction_adapter::NativeTransactionBlockResponse; +use identity_iota_interaction::IdentityMoveCalls; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::IotaTransactionBlockResponseT; +use identity_iota_interaction::OptionalSync; + +use crate::rebased::client::IdentityClient; +use crate::rebased::migration::OnChainIdentity; +use crate::rebased::Error; +use identity_iota_interaction::MoveType; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::Proposal; +use super::ProposalBuilder; +use super::ProposalT; + +/// An action used to transfer [`crate::migration::OnChainIdentity`]-owned assets to other addresses. +#[derive(Debug, Clone, Deserialize, Default, Serialize)] +#[serde(from = "IotaSendAction", into = "IotaSendAction")] +pub struct SendAction(Vec<(ObjectID, IotaAddress)>); + +impl MoveType for SendAction { + fn move_type(package: ObjectID) -> TypeTag { + use std::str::FromStr; + + TypeTag::from_str(&format!("{package}::transfer_proposal::Send")).expect("valid move type") + } +} + +impl SendAction { + /// Adds to the list of object to send the object with ID `object_id` and send it to address `recipient`. + pub fn send_object(&mut self, object_id: ObjectID, recipient: IotaAddress) { + self.0.push((object_id, recipient)); + } + + /// Adds multiple objects to the list of objects to send. + pub fn send_objects(&mut self, objects: I) + where + I: IntoIterator, + { + objects + .into_iter() + .for_each(|(obj_id, recp)| self.send_object(obj_id, recp)); + } +} + +impl AsRef<[(ObjectID, IotaAddress)]> for SendAction { + fn as_ref(&self) -> &[(ObjectID, IotaAddress)] { + &self.0 + } +} + +impl ProposalBuilder<'_, SendAction> { + /// Adds one object to the list of objects to send. + pub fn object(mut self, object_id: ObjectID, recipient: IotaAddress) -> Self { + self.send_object(object_id, recipient); + self + } + + /// Adds multiple objects to the list of objects to send. + pub fn objects(mut self, objects: I) -> Self + where + I: IntoIterator, + { + self.send_objects(objects); + self + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl ProposalT for Proposal { + type Action = SendAction; + type Output = (); + type Response = IotaTransactionBlockResponseAdapter; + + async fn create<'i, S>( + action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + let can_execute = identity + .controller_voting_power(controller_cap_ref.0) + .expect("controller_cap is for this identity") + >= identity.threshold(); + let tx = if can_execute { + // Construct a list of `(ObjectRef, TypeTag)` from the list of objects to send. + let object_type_list = { + let ids = action.0.iter().map(|(obj_id, _rcp)| obj_id); + let mut object_and_type_list = vec![]; + for obj_id in ids { + let ref_and_type = super::obj_ref_and_type_for_id(client, *obj_id) + .await + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + object_and_type_list.push(ref_and_type); + } + object_and_type_list + }; + IdentityMoveCallsAdapter::create_and_execute_send( + identity_ref, + controller_cap_ref, + action.0, + expiration, + object_type_list, + client.package_id(), + ) + } else { + IdentityMoveCallsAdapter::propose_send( + identity_ref, + controller_cap_ref, + action.0, + expiration, + client.package_id(), + ) + } + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + Ok(CreateProposalTx { + identity, + tx, + chained_execution: can_execute, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let proposal_id = self.id(); + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + // Construct a list of `(ObjectRef, TypeTag)` from the list of objects to send. + let object_type_list = { + let ids = self.into_action().0.into_iter().map(|(obj_id, _rcp)| obj_id); + let mut object_and_type_list = vec![]; + for obj_id in ids { + let ref_and_type = super::obj_ref_and_type_for_id(client, obj_id) + .await + .map_err(|e| Error::ObjectLookup(e.to_string()))?; + object_and_type_list.push(ref_and_type); + } + object_and_type_list + }; + + let tx = IdentityMoveCallsAdapter::execute_send( + identity_ref, + controller_cap_ref, + proposal_id, + object_type_list, + client.package_id(), + ) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(ExecuteProposalTx { + identity, + tx, + _action: PhantomData, + }) + } + + fn parse_tx_effects_internal( + _tx_response: &dyn IotaTransactionBlockResponseT< + Error = AdapterError, + NativeResponse = NativeTransactionBlockResponse, + >, + ) -> Result { + Ok(()) + } +} + +#[derive(Debug, Deserialize, Serialize)] +struct IotaSendAction { + objects: Vec, + recipients: Vec, +} + +impl From for SendAction { + fn from(value: IotaSendAction) -> Self { + let IotaSendAction { objects, recipients } = value; + let transfer_map = objects.into_iter().zip(recipients).collect(); + SendAction(transfer_map) + } +} + +impl From for IotaSendAction { + fn from(action: SendAction) -> Self { + let (objects, recipients) = action.0.into_iter().unzip(); + Self { objects, recipients } + } +} diff --git a/identity_iota_core/src/rebased/proposals/update_did_doc.rs b/identity_iota_core/src/rebased/proposals/update_did_doc.rs new file mode 100644 index 0000000000..d672ff9bdd --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/update_did_doc.rs @@ -0,0 +1,165 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::marker::PhantomData; + +use crate::iota_interaction_adapter::AdapterError; +use crate::iota_interaction_adapter::IdentityMoveCallsAdapter; +use crate::iota_interaction_adapter::IotaTransactionBlockResponseAdapter; +use crate::iota_interaction_adapter::NativeTransactionBlockResponse; +use identity_iota_interaction::IdentityMoveCalls; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::IotaTransactionBlockResponseT; +use identity_iota_interaction::OptionalSync; + +use crate::rebased::client::IdentityClient; +use crate::IotaDocument; +use async_trait::async_trait; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::TypeTag; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use crate::rebased::migration::OnChainIdentity; +use crate::rebased::migration::Proposal; +use crate::rebased::Error; +use identity_iota_interaction::MoveType; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::ProposalT; + +/// Proposal's action for updating a DID Document. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(into = "UpdateValue::>>", from = "UpdateValue::>>")] +pub struct UpdateDidDocument(Option>); + +impl MoveType for UpdateDidDocument { + fn move_type(package: ObjectID) -> TypeTag { + use std::str::FromStr; + + TypeTag::from_str(&format!( + "{package}::update_value_proposal::UpdateValue<0x1::option::Option>>" + )) + .expect("valid TypeTag") + } +} + +impl UpdateDidDocument { + /// Creates a new [`UpdateDidDocument`] action. + pub fn new(document: IotaDocument) -> Self { + Self(Some(document.pack().expect("a valid IotaDocument is packable"))) + } + + /// Creates a new [`UpdateDidDocument`] action to deactivate the DID Document. + pub fn deactivate() -> Self { + Self(Some(vec![])) + } + + /// Creates a new [`UpdateDidDocument`] action to delete the DID Document. + pub fn delete() -> Self { + Self(None) + } + + /// Returns the serialized DID document bytes. + pub fn did_document_bytes(&self) -> Option<&[u8]> { + self.0.as_deref() + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl ProposalT for Proposal { + type Action = UpdateDidDocument; + type Output = (); + type Response = IotaTransactionBlockResponseAdapter; + + async fn create<'i, S>( + action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + let sender_vp = identity + .controller_voting_power(controller_cap_ref.0) + .expect("controller exists"); + let chained_execution = sender_vp >= identity.threshold(); + let tx = IdentityMoveCallsAdapter::propose_update( + identity_ref, + controller_cap_ref, + action.0.as_deref(), + expiration, + client.package_id(), + ) + .await + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(CreateProposalTx { + identity, + tx, + chained_execution, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let proposal_id = self.id(); + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + let tx = + IdentityMoveCallsAdapter::execute_update(identity_ref, controller_cap_ref, proposal_id, client.package_id()) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(ExecuteProposalTx { + identity, + tx, + _action: PhantomData, + }) + } + + fn parse_tx_effects_internal( + _tx_response: &dyn IotaTransactionBlockResponseT< + Error = AdapterError, + NativeResponse = NativeTransactionBlockResponse, + >, + ) -> Result { + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct UpdateValue { + new_value: V, +} + +impl From for UpdateValue>> { + fn from(value: UpdateDidDocument) -> Self { + Self { new_value: value.0 } + } +} + +impl From>>> for UpdateDidDocument { + fn from(value: UpdateValue>>) -> Self { + UpdateDidDocument(value.new_value) + } +} diff --git a/identity_iota_core/src/rebased/proposals/upgrade.rs b/identity_iota_core/src/rebased/proposals/upgrade.rs new file mode 100644 index 0000000000..c6d72df9a2 --- /dev/null +++ b/identity_iota_core/src/rebased/proposals/upgrade.rs @@ -0,0 +1,124 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::marker::PhantomData; + +use crate::iota_interaction_adapter::AdapterError; +use crate::iota_interaction_adapter::IdentityMoveCallsAdapter; +use crate::iota_interaction_adapter::IotaTransactionBlockResponseAdapter; +use crate::iota_interaction_adapter::NativeTransactionBlockResponse; +use identity_iota_interaction::IdentityMoveCalls; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::IotaTransactionBlockResponseT; +use identity_iota_interaction::OptionalSync; + +use crate::rebased::client::IdentityClient; +use async_trait::async_trait; +use identity_iota_interaction::types::base_types::ObjectID; +use identity_iota_interaction::types::TypeTag; +use secret_storage::Signer; +use serde::Deserialize; +use serde::Serialize; + +use crate::rebased::migration::OnChainIdentity; +use crate::rebased::migration::Proposal; +use crate::rebased::Error; +use identity_iota_interaction::MoveType; + +use super::CreateProposalTx; +use super::ExecuteProposalTx; +use super::ProposalT; + +/// Action for upgrading the version of an on-chain identity to the package's version. +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] +pub struct Upgrade; + +impl Upgrade { + /// Creates a new [`Upgrade`] action. + pub const fn new() -> Self { + Self + } +} + +impl MoveType for Upgrade { + fn move_type(package: ObjectID) -> TypeTag { + format!("{package}::upgrade_proposal::Upgrade") + .parse() + .expect("valid utf8") + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl ProposalT for Proposal { + type Action = Upgrade; + type Output = (); + type Response = IotaTransactionBlockResponseAdapter; + + async fn create<'i, S>( + _action: Self::Action, + expiration: Option, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + let sender_vp = identity + .controller_voting_power(controller_cap_ref.0) + .expect("controller exists"); + let chained_execution = sender_vp >= identity.threshold(); + let tx = + IdentityMoveCallsAdapter::propose_upgrade(identity_ref, controller_cap_ref, expiration, client.package_id()) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(CreateProposalTx { + identity, + tx, + chained_execution, + _action: PhantomData, + }) + } + + async fn into_tx<'i, S>( + self, + identity: &'i mut OnChainIdentity, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let proposal_id = self.id(); + let identity_ref = client + .get_object_ref_by_id(identity.id()) + .await? + .expect("identity exists on-chain"); + let controller_cap_ref = identity.get_controller_cap(client).await?; + + let tx = + IdentityMoveCallsAdapter::execute_upgrade(identity_ref, controller_cap_ref, proposal_id, client.package_id()) + .map_err(|e| Error::TransactionBuildingFailed(e.to_string()))?; + + Ok(ExecuteProposalTx { + identity, + tx, + _action: PhantomData, + }) + } + + fn parse_tx_effects_internal( + _tx_response: &dyn IotaTransactionBlockResponseT< + Error = AdapterError, + NativeResponse = NativeTransactionBlockResponse, + >, + ) -> Result { + Ok(()) + } +} diff --git a/identity_iota_core/src/rebased/transaction.rs b/identity_iota_core/src/rebased/transaction.rs new file mode 100644 index 0000000000..421307135c --- /dev/null +++ b/identity_iota_core/src/rebased/transaction.rs @@ -0,0 +1,229 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; + +#[cfg(not(target_arch = "wasm32"))] +use identity_iota_interaction::rpc_types::IotaTransactionBlockResponse; +#[cfg(not(target_arch = "wasm32"))] +use identity_iota_interaction::types::transaction::ProgrammableTransaction; +#[cfg(target_arch = "wasm32")] +use iota_interaction_ts::ProgrammableTransaction; + +use crate::iota_interaction_adapter::IotaTransactionBlockResponseAdaptedTraitObj; +use crate::rebased::client::IdentityClient; +use crate::rebased::Error; +use async_trait::async_trait; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::OptionalSync; +use identity_iota_interaction::ProgrammableTransactionBcs; +use secret_storage::Signer; + +/// The output type of a [`Transaction`]. +#[cfg(not(target_arch = "wasm32"))] +#[derive(Debug, Clone)] +pub struct TransactionOutput { + /// The parsed Transaction output. See [`Transaction::Output`]. + pub output: T, + /// The "raw" transaction execution response received. + pub response: IotaTransactionBlockResponse, +} + +#[cfg(not(target_arch = "wasm32"))] +impl Deref for TransactionOutput { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.output + } +} + +/// Interface for operations that interact with the ledger through transactions. +#[cfg(not(target_arch = "wasm32"))] +#[async_trait::async_trait] +pub trait Transaction: Sized { + /// The result of performing the operation. + type Output; + + /// Executes this operation using the given `client` and an optional `gas_budget`. + /// If no value for `gas_budget` is provided, an estimated value will be used. + async fn execute_with_opt_gas + OptionalSync>( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error>; + + /// Executes this operation using `client`. + async fn execute + OptionalSync>( + self, + client: &IdentityClient, + ) -> Result, Error> { + self.execute_with_opt_gas(None, client).await + } + + /// Executes this operation using `client` and a well defined `gas_budget`. + async fn execute_with_gas + OptionalSync>( + self, + gas_budget: u64, + client: &IdentityClient, + ) -> Result, Error> { + self.execute_with_opt_gas(Some(gas_budget), client).await + } +} + +/// The output type of a [`Transaction`]. +pub struct TransactionOutputInternal { + /// The parsed Transaction output. See [`Transaction::Output`]. + pub output: T, + /// The "raw" transaction execution response received. + pub response: IotaTransactionBlockResponseAdaptedTraitObj, +} + +impl Deref for TransactionOutputInternal { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.output + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl From> for TransactionOutput { + fn from(value: TransactionOutputInternal) -> Self { + let TransactionOutputInternal:: { + output: out, + response: internal_response, + } = value; + let response = internal_response.clone_native_response(); + TransactionOutput { output: out, response } + } +} + +/// Interface for operations that interact with the ledger through transactions. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait TransactionInternal: Sized { + /// The result of performing the operation. + type Output; + + /// Executes this operation using the given `client` and an optional `gas_budget`. + /// If no value for `gas_budget` is provided, an estimated value will be used. + async fn execute_with_opt_gas_internal + OptionalSync>( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error>; + + /// Executes this operation using `client`. + #[cfg(target_arch = "wasm32")] + async fn execute>( + self, + client: &IdentityClient, + ) -> Result, Error> { + self.execute_with_opt_gas_internal(None, client).await + } + + /// Executes this operation using `client` and a well defined `gas_budget`. + #[cfg(target_arch = "wasm32")] + async fn execute_with_gas + OptionalSync>( + self, + gas_budget: u64, + client: &IdentityClient, + ) -> Result, Error> { + self.execute_with_opt_gas_internal(Some(gas_budget), client).await + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[async_trait::async_trait] +impl + Send, O> Transaction for T { + type Output = O; + + async fn execute_with_opt_gas + OptionalSync>( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> { + let tx_output = self.execute_with_opt_gas_internal(gas_budget, client).await?; + Ok(tx_output.into()) + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[async_trait::async_trait] +impl TransactionInternal for ProgrammableTransaction { + type Output = (); + async fn execute_with_opt_gas_internal( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + let tx_bcs = bcs::to_bytes(&self)?; + let response = client.execute_transaction(tx_bcs, gas_budget).await?; + Ok(TransactionOutputInternal { output: (), response }) + } +} + +#[cfg(target_arch = "wasm32")] +#[async_trait::async_trait(?Send)] +impl TransactionInternal for ProgrammableTransaction { + type Output = (); + async fn execute_with_opt_gas_internal( + self, + _gas_budget: Option, + _client: &IdentityClient, + ) -> Result, Error> + where + S: Signer + OptionalSync, + { + unimplemented!("TransactionInternal::execute_with_opt_gas_internal for ProgrammableTransaction"); + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionInternal for ProgrammableTransactionBcs { + type Output = (); + + async fn execute_with_opt_gas_internal + OptionalSync>( + self, + gas_budget: Option, + client: &IdentityClient, + ) -> Result, Error> { + // For wasm32 targets, the following line will result in a compiler error[E0412] + // TODO: Implement wasm-bindings for the ProgrammableTransaction TS equivalent + // and use them to do the BCS serialization + let self_tx = bcs::from_bytes::(&self)?; + self_tx.execute_with_opt_gas_internal(gas_budget, client).await + } +} + +/// Interface to describe an operation that can eventually +/// be turned into a [`Transaction`], given the right input. +pub trait ProtoTransaction { + /// The input required by this operation. + type Input; + /// This operation's next state. Can either be another [`ProtoTransaction`] + /// or a whole [`Transaction`] ready to be executed. + type Tx: ProtoTransaction; + + /// Feed this operation with its required input, advancing its + /// state to another [`ProtoTransaction`] that may or may not + /// be ready for execution. + fn with(self, input: Self::Input) -> Self::Tx; +} + +// Every Transaction is a QuasiTransaction that requires no input +// and that has itself as its next state. +impl ProtoTransaction for T +where + T: TransactionInternal, +{ + type Input = (); + type Tx = Self; + + fn with(self, _: Self::Input) -> Self::Tx { + self + } +} diff --git a/identity_iota_core/src/rebased/utils.rs b/identity_iota_core/src/rebased/utils.rs new file mode 100644 index 0000000000..c12905d1a6 --- /dev/null +++ b/identity_iota_core/src/rebased/utils.rs @@ -0,0 +1,107 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::process::Output; + +use anyhow::Context as _; +use identity_iota_interaction::types::base_types::ObjectID; +use iota_sdk::IotaClient; +use iota_sdk::IotaClientBuilder; +use serde::Deserialize; +#[cfg(not(target_arch = "wasm32"))] +use tokio::process::Command; + +use crate::rebased::Error; +use identity_iota_interaction::types::base_types::IotaAddress; + +const FUND_WITH_ACTIVE_ADDRESS_FUNDING_TX_BUDGET: u64 = 5_000_000; +const FUND_WITH_ACTIVE_ADDRESS_FUNDING_VALUE: u64 = 500_000_000; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct CoinOutput { + gas_coin_id: ObjectID, + nanos_balance: u64, +} + +/// Builds an `IOTA` client for the given network. +pub async fn get_client(network: &str) -> Result { + let client = IotaClientBuilder::default() + .build(network) + .await + .map_err(|err| Error::Network(format!("failed to connect to {network}"), err))?; + + Ok(client) +} + +fn unpack_command_output(output: &Output, task: &str) -> anyhow::Result { + let stdout = std::str::from_utf8(&output.stdout)?; + if !output.status.success() { + let stderr = std::str::from_utf8(&output.stderr)?; + anyhow::bail!("Failed to {task}: {stdout}, {stderr}"); + } + + Ok(stdout.to_string()) +} + +/// Requests funds from the local IOTA client's configured faucet. +/// +/// This behavior can be changed to send funds with local IOTA client's active address to the given address. +/// For that the env variable `IOTA_IDENTITY_FUND_WITH_ACTIVE_ADDRESS` must be set to `true`. +/// Notice, that this is a setting mostly intended for internal test use and must be used with care. +/// For details refer to to `identity_iota_core`'s README.md. +#[cfg(not(target_arch = "wasm32"))] +pub async fn request_funds(address: &IotaAddress) -> anyhow::Result<()> { + let fund_with_active_address = std::env::var("IOTA_IDENTITY_FUND_WITH_ACTIVE_ADDRESS") + .map(|v| !v.is_empty() && v.to_lowercase() == "true") + .unwrap_or(false); + + if !fund_with_active_address { + let output = Command::new("iota") + .arg("client") + .arg("faucet") + .arg("--address") + .arg(address.to_string()) + .arg("--json") + .output() + .await + .context("Failed to execute command")?; + unpack_command_output(&output, "request funds from faucet")?; + } else { + let output = Command::new("iota") + .arg("client") + .arg("gas") + .arg("--json") + .output() + .await + .context("Failed to execute command")?; + let output_str = unpack_command_output(&output, "fetch active account's gas coins")?; + + let parsed: Vec = serde_json::from_str(&output_str)?; + let min_balance = FUND_WITH_ACTIVE_ADDRESS_FUNDING_VALUE + FUND_WITH_ACTIVE_ADDRESS_FUNDING_TX_BUDGET; + let matching = parsed.into_iter().find(|coin| coin.nanos_balance >= min_balance); + let Some(coin_to_use) = matching else { + anyhow::bail!("Failed to find coin object with enough funds to transfer to test account"); + }; + + let address_string = address.to_string(); + let output = Command::new("iota") + .arg("client") + .arg("pay-iota") + .arg("--recipients") + .arg(&address_string) + .arg("--input-coins") + .arg(coin_to_use.gas_coin_id.to_string()) + .arg("--amounts") + .arg(FUND_WITH_ACTIVE_ADDRESS_FUNDING_VALUE.to_string()) + .arg("--gas-budget") + .arg(FUND_WITH_ACTIVE_ADDRESS_FUNDING_TX_BUDGET.to_string()) + .arg("--json") + .output() + .await + .context("Failed to execute command")?; + unpack_command_output(&output, &format!("send funds from active account to {address_string}"))?; + } + + Ok(()) +} diff --git a/identity_iota_core/src/state_metadata/document.rs b/identity_iota_core/src/state_metadata/document.rs index 197f9befb8..6c54e32702 100644 --- a/identity_iota_core/src/state_metadata/document.rs +++ b/identity_iota_core/src/state_metadata/document.rs @@ -23,8 +23,7 @@ pub(crate) static PLACEHOLDER_DID: Lazy = Lazy::new(|| CoreDID::parse(" /// Magic bytes used to mark DID documents. const DID_MARKER: &[u8] = b"DID"; -/// Intermediate representation of the DID document as it is contained in the state metadata of -/// an Alias Output. +/// Intermediate representation of the DID document as it is contained in the identity. /// /// DID instances in the document are replaced by the `PLACEHOLDER_DID`. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] @@ -73,8 +72,16 @@ impl StateMetadataDocument { Ok(IotaDocument { document, metadata }) } - /// Pack a [`StateMetadataDocument`] into bytes, suitable for inclusion in - /// an Alias Output's state metadata, according to the given `encoding`. + /// Returns the corresponding [`IotaDocument`] with DID replaced by DID placeholder `did:0:0`. + pub fn into_iota_document_with_placeholders(self) -> IotaDocument { + IotaDocument { + document: self.document, + metadata: self.metadata, + } + } + + /// Pack a [`StateMetadataDocument`] into bytes, suitable for storing in an identity, + /// according to the given `encoding`. pub fn pack(mut self, encoding: StateMetadataEncoding) -> Result> { // Unset Governor and State Controller Addresses to avoid bloating the payload self.metadata.governor_address = None; diff --git a/identity_iota_core/tests/e2e/asset.rs b/identity_iota_core/tests/e2e/asset.rs new file mode 100644 index 0000000000..1312ff3b71 --- /dev/null +++ b/identity_iota_core/tests/e2e/asset.rs @@ -0,0 +1,245 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use crate::common::get_funded_test_client; +use crate::common::TEST_GAS_BUDGET; +use identity_core::common::Object; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use identity_credential::credential::CredentialBuilder; +use identity_credential::validator::FailFast; +use identity_credential::validator::JwtCredentialValidationOptions; +use identity_credential::validator::JwtCredentialValidator; +use identity_document::document::CoreDocument; +use identity_eddsa_verifier::EdDSAJwsVerifier; +use identity_iota_core::rebased::transaction::Transaction; +use identity_iota_core::rebased::AuthenticatedAsset; +use identity_iota_core::rebased::PublicAvailableVC; +use identity_iota_core::rebased::TransferProposal; +use identity_iota_core::IotaDID; +use identity_iota_core::IotaDocument; +use identity_iota_interaction::IotaClientTrait; +use identity_iota_interaction::MoveType as _; +use identity_storage::JwkDocumentExt; +use identity_storage::JwsSignatureOptions; +use identity_verification::VerificationMethod; +use iota_sdk::types::TypeTag; +use itertools::Itertools as _; +use move_core_types::language_storage::StructTag; + +#[tokio::test] +async fn creating_authenticated_asset_works() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let alice_client = test_client.new_user_client().await?; + + let asset = alice_client + .create_authenticated_asset::(42) + .finish() + .execute(&alice_client) + .await? + .output; + assert_eq!(asset.content(), &42); + + Ok(()) +} + +#[tokio::test] +async fn transferring_asset_works() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let alice_client = test_client.new_user_client().await?; + let bob_client = test_client.new_user_client().await?; + + // Alice creates a new asset. + let asset = alice_client + .create_authenticated_asset::(42) + .transferable(true) + .finish() + .execute(&alice_client) + .await? + .output; + let asset_id = asset.id(); + + // Alice propose to Bob the transfer of the asset. + let proposal = asset + .transfer(bob_client.sender_address())? + .execute(&alice_client) + .await? + .output; + let proposal_id = proposal.id(); + // Bob accepts the transfer. + proposal.accept().execute(&bob_client).await?; + let TypeTag::Struct(asset_type) = AuthenticatedAsset::::move_type(test_client.package_id()) else { + unreachable!("asset is a struct"); + }; + let bob_owns_asset = bob_client + .find_owned_ref(*asset_type, |obj| obj.object_id == asset_id) + .await? + .is_some(); + assert!(bob_owns_asset); + + // Alice concludes the transfer. + let proposal = TransferProposal::get_by_id(proposal_id, &alice_client).await?; + assert!(proposal.is_concluded()); + proposal.conclude_or_cancel().execute(&alice_client).await?; + + // After the transfer is concluded all capabilities as well as the proposal bound to the transfer are deleted. + let alice_has_sender_cap = alice_client + .find_owned_ref( + StructTag::from_str(&format!("{}::asset::SenderCap", test_client.package_id()))?, + |_| true, + ) + .await? + .is_some(); + assert!(!alice_has_sender_cap); + let bob_has_recipient_cap = bob_client + .find_owned_ref( + StructTag::from_str(&format!("{}::asset::RecipientCap", test_client.package_id()))?, + |_| true, + ) + .await? + .is_some(); + assert!(!bob_has_recipient_cap); + + Ok(()) +} + +#[tokio::test] +async fn accepting_the_transfer_of_an_asset_requires_capability() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let alice_client = test_client.new_user_client().await?; + let bob_client = test_client.new_user_client().await?; + let caty_client = test_client.new_user_client().await?; + + // Alice creates a new asset. + let asset = alice_client + .create_authenticated_asset::(42) + .transferable(true) + .finish() + .execute(&alice_client) + .await? + .output; + + // Alice propose to Bob the transfer of the asset. + let proposal = asset + .transfer(bob_client.sender_address())? + .execute(&alice_client) + .await? + .output; + + // Caty attempts to accept the transfer instead of Bob but gets an error + let error = proposal.accept().execute(&caty_client).await.unwrap_err(); + assert!(matches!( + error, + identity_iota_core::rebased::Error::MissingPermission(_) + )); + + Ok(()) +} + +#[tokio::test] +async fn modifying_mutable_asset_works() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let alice_client = test_client.new_user_client().await?; + + let mut asset = alice_client + .create_authenticated_asset::(42) + .mutable(true) + .finish() + .execute(&alice_client) + .await? + .output; + + asset.set_content(420)?.execute(&alice_client).await?; + assert_eq!(asset.content(), &420); + + Ok(()) +} + +#[tokio::test] +async fn deleting_asset_works() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let alice_client = test_client.new_user_client().await?; + + let asset = alice_client + .create_authenticated_asset::(42) + .deletable(true) + .finish() + .execute(&alice_client) + .await? + .output; + let asset_id = asset.id(); + + asset.delete()?.execute(&alice_client).await?; + let alice_owns_asset = alice_client + .read_api() + .get_owned_objects(alice_client.sender_address(), None, None, None) + .await? + .data + .into_iter() + .map(|obj| obj.object_id().unwrap()) + .contains(&asset_id); + assert!(!alice_owns_asset); + + Ok(()) +} + +#[tokio::test] +async fn hosting_vc_works() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let newly_created_identity = identity_client + .create_identity(IotaDocument::new(identity_client.network())) + .finish() + .execute_with_gas(TEST_GAS_BUDGET, &identity_client) + .await? + .output; + let object_id = newly_created_identity.id(); + let did = { IotaDID::parse(format!("did:iota:{object_id}"))? }; + + test_client + .store_key_id_for_verification_method(identity_client.clone(), did.clone()) + .await?; + let did_doc = CoreDocument::builder(Object::default()) + .id(did.clone().into()) + .verification_method(VerificationMethod::new_from_jwk( + did.clone(), + identity_client.signer().public_key().clone(), + Some(identity_client.signer().key_id().as_str()), + )?) + .build()?; + let credential = CredentialBuilder::new(Object::default()) + .id(Url::parse("http://example.com/credentials/42")?) + .issuance_date(Timestamp::now_utc()) + .issuer(Url::parse(did.to_string())?) + .subject(serde_json::from_value(serde_json::json!({ + "id": did, + "type": ["VerifiableCredential", "ExampleCredential"], + "value": 3 + }))?) + .build()?; + let credential_jwt = did_doc + .create_credential_jwt( + &credential, + identity_client.signer().storage(), + identity_client.signer().key_id().as_str(), + &JwsSignatureOptions::default(), + None, + ) + .await?; + + let vc = PublicAvailableVC::new(credential_jwt.clone(), Some(TEST_GAS_BUDGET), &identity_client).await?; + assert_eq!(credential_jwt, vc.jwt()); + + let validator = JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); + validator.validate::<_, Object>( + &credential_jwt, + &did_doc, + &JwtCredentialValidationOptions::default(), + FailFast::FirstError, + )?; + + Ok(()) +} diff --git a/identity_iota_core/tests/e2e/client.rs b/identity_iota_core/tests/e2e/client.rs new file mode 100644 index 0000000000..e7a548cf80 --- /dev/null +++ b/identity_iota_core/tests/e2e/client.rs @@ -0,0 +1,98 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::common::get_funded_test_client; +use crate::common::TestClient; +use identity_iota_core::rebased::migration; +use identity_iota_core::rebased::transaction::Transaction; +use identity_iota_core::IotaDocument; +use iota_sdk::types::crypto::SignatureScheme; + +#[tokio::test] +async fn can_create_an_identity() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let identity = identity_client + .create_identity(IotaDocument::new(identity_client.network())) + .finish() + .execute(&identity_client) + .await? + .output; + + let did = identity.did_document().id(); + assert_eq!(did.network_str(), identity_client.network().as_ref()); + + Ok(()) +} + +#[tokio::test] +async fn can_resolve_a_new_identity() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let new_identity = identity_client + .create_identity(IotaDocument::new(identity_client.network())) + .finish() + .execute(&identity_client) + .await? + .output; + + let identity = migration::get_identity(&identity_client, new_identity.id()).await?; + + assert!(identity.is_some()); + + Ok(()) +} + +#[tokio::test] +async fn client_with_keytool_signer_active_address_works() -> anyhow::Result<()> { + let test_client = TestClient::new().await?; + let _identity = test_client + .create_identity(IotaDocument::new(test_client.network())) + .finish() + .execute(&test_client) + .await? + .output; + + Ok(()) +} + +#[tokio::test] +async fn client_with_new_ed25519_keytool_signer_works() -> anyhow::Result<()> { + let test_client = TestClient::new_with_key_type(SignatureScheme::ED25519).await?; + let _identity = test_client + .create_identity(IotaDocument::new(test_client.network())) + .finish() + .execute(&test_client) + .await? + .output; + + Ok(()) +} + +#[tokio::test] +async fn client_with_new_secp256r1_keytool_signer_works() -> anyhow::Result<()> { + let test_client = TestClient::new_with_key_type(SignatureScheme::Secp256r1).await?; + let _identity = test_client + .create_identity(IotaDocument::new(test_client.network())) + .finish() + .execute(&test_client) + .await? + .output; + + Ok(()) +} + +#[tokio::test] +async fn client_with_new_secp256k1_keytool_signer_works() -> anyhow::Result<()> { + let test_client = TestClient::new_with_key_type(SignatureScheme::Secp256k1).await?; + let _identity = test_client + .create_identity(IotaDocument::new(test_client.network())) + .finish() + .execute(&test_client) + .await? + .output; + + Ok(()) +} diff --git a/identity_iota_core/tests/e2e/common.rs b/identity_iota_core/tests/e2e/common.rs new file mode 100644 index 0000000000..00aa0f3a88 --- /dev/null +++ b/identity_iota_core/tests/e2e/common.rs @@ -0,0 +1,369 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +#![allow(dead_code)] + +use anyhow::anyhow; +use anyhow::Context; +use identity_iota_core::rebased::client::IdentityClient; +use identity_iota_core::rebased::client::IdentityClientReadOnly; +use identity_iota_core::rebased::transaction::Transaction; +use identity_iota_core::rebased::utils::request_funds; +use identity_iota_core::rebased::KeytoolSigner; +use identity_iota_core::IotaDID; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::OptionalSync; +use identity_jose::jwk::Jwk; +use identity_jose::jws::JwsAlgorithm; +use identity_storage::JwkMemStore; +use identity_storage::JwkStorage; +use identity_storage::KeyId; +use identity_storage::KeyIdMemstore; +use identity_storage::KeyIdStorage; +use identity_storage::KeyType; +use identity_storage::MethodDigest; +use identity_storage::Storage; +use identity_storage::StorageSigner; +use identity_verification::VerificationMethod; +use iota_sdk::rpc_types::IotaTransactionBlockEffectsAPI; +use iota_sdk::types::base_types::IotaAddress; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::crypto::SignatureScheme; +use iota_sdk::types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use iota_sdk::types::TypeTag; +use iota_sdk::types::IOTA_FRAMEWORK_PACKAGE_ID; +use iota_sdk::IotaClient; +use iota_sdk::IotaClientBuilder; +use iota_sdk::IOTA_LOCAL_NETWORK_URL; +use lazy_static::lazy_static; +use move_core_types::ident_str; +use move_core_types::language_storage::StructTag; +use secret_storage::Signer; +use serde::Deserialize; +use serde_json::Value; +use std::io::Write; +use std::ops::Deref; +use std::str::FromStr; +use std::sync::Arc; +use tokio::process::Command; +use tokio::sync::OnceCell; + +pub type MemStorage = Storage; +pub type MemSigner<'s> = StorageSigner<'s, JwkMemStore, KeyIdMemstore>; + +static PACKAGE_ID: OnceCell = OnceCell::const_new(); +static CLIENT: OnceCell = OnceCell::const_new(); +const SCRIPT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/scripts/"); +const CACHED_PKG_ID: &str = "/tmp/iota_identity_pkg_id.txt"; + +pub const TEST_GAS_BUDGET: u64 = 50_000_000; +pub const TEST_DOC: &[u8] = &[ + 68, 73, 68, 1, 0, 131, 1, 123, 34, 100, 111, 99, 34, 58, 123, 34, 105, 100, 34, 58, 34, 100, 105, 100, 58, 48, 58, + 48, 34, 44, 34, 118, 101, 114, 105, 102, 105, 99, 97, 116, 105, 111, 110, 77, 101, 116, 104, 111, 100, 34, 58, 91, + 123, 34, 105, 100, 34, 58, 34, 100, 105, 100, 58, 48, 58, 48, 35, 79, 115, 55, 95, 66, 100, 74, 120, 113, 86, 119, + 101, 76, 107, 56, 73, 87, 45, 76, 71, 83, 111, 52, 95, 65, 115, 52, 106, 70, 70, 86, 113, 100, 108, 74, 73, 99, 48, + 45, 100, 50, 49, 73, 34, 44, 34, 99, 111, 110, 116, 114, 111, 108, 108, 101, 114, 34, 58, 34, 100, 105, 100, 58, 48, + 58, 48, 34, 44, 34, 116, 121, 112, 101, 34, 58, 34, 74, 115, 111, 110, 87, 101, 98, 75, 101, 121, 34, 44, 34, 112, + 117, 98, 108, 105, 99, 75, 101, 121, 74, 119, 107, 34, 58, 123, 34, 107, 116, 121, 34, 58, 34, 79, 75, 80, 34, 44, + 34, 97, 108, 103, 34, 58, 34, 69, 100, 68, 83, 65, 34, 44, 34, 107, 105, 100, 34, 58, 34, 79, 115, 55, 95, 66, 100, + 74, 120, 113, 86, 119, 101, 76, 107, 56, 73, 87, 45, 76, 71, 83, 111, 52, 95, 65, 115, 52, 106, 70, 70, 86, 113, 100, + 108, 74, 73, 99, 48, 45, 100, 50, 49, 73, 34, 44, 34, 99, 114, 118, 34, 58, 34, 69, 100, 50, 53, 53, 49, 57, 34, 44, + 34, 120, 34, 58, 34, 75, 119, 99, 54, 89, 105, 121, 121, 65, 71, 79, 103, 95, 80, 116, 118, 50, 95, 49, 67, 80, 71, + 52, 98, 86, 87, 54, 102, 89, 76, 80, 83, 108, 115, 57, 112, 122, 122, 99, 78, 67, 67, 77, 34, 125, 125, 93, 125, 44, + 34, 109, 101, 116, 97, 34, 58, 123, 34, 99, 114, 101, 97, 116, 101, 100, 34, 58, 34, 50, 48, 50, 52, 45, 48, 53, 45, + 50, 50, 84, 49, 50, 58, 49, 52, 58, 51, 50, 90, 34, 44, 34, 117, 112, 100, 97, 116, 101, 100, 34, 58, 34, 50, 48, 50, + 52, 45, 48, 53, 45, 50, 50, 84, 49, 50, 58, 49, 52, 58, 51, 50, 90, 34, 125, 125, +]; + +lazy_static! { + pub static ref TEST_COIN_TYPE: StructTag = "0x2::coin::Coin".parse().unwrap(); +} + +pub async fn get_funded_test_client() -> anyhow::Result { + TestClient::new().await +} + +async fn init(iota_client: &IotaClient) -> anyhow::Result { + let network_id = iota_client.read_api().get_chain_identifier().await?; + let address = get_active_address().await?; + + if let Ok(id) = std::env::var("IOTA_IDENTITY_PKG_ID").or(get_cached_id(&network_id).await) { + std::env::set_var("IOTA_IDENTITY_PKG_ID", id.clone()); + id.parse().context("failed to parse object id from str") + } else { + publish_package(address).await + } +} + +async fn get_cached_id(network_id: &str) -> anyhow::Result { + let cache = tokio::fs::read_to_string(CACHED_PKG_ID).await?; + let (cached_id, cached_network_id) = cache.split_once(';').ok_or(anyhow!("Invalid or empty cached data"))?; + + if cached_network_id == network_id { + Ok(cached_id.to_owned()) + } else { + Err(anyhow!("A network change has invalidated the cached data")) + } +} + +async fn get_active_address() -> anyhow::Result { + Command::new("iota") + .arg("client") + .arg("active-address") + .arg("--json") + .output() + .await + .context("Failed to execute command") + .and_then(|output| Ok(serde_json::from_slice::(&output.stdout)?)) +} + +async fn publish_package(active_address: IotaAddress) -> anyhow::Result { + let output = Command::new("sh") + .current_dir(SCRIPT_DIR) + .arg("publish_identity_package.sh") + .output() + .await?; + let stdout = std::str::from_utf8(&output.stdout).unwrap(); + + if !output.status.success() { + let stderr = std::str::from_utf8(&output.stderr).unwrap(); + anyhow::bail!("Failed to publish move package: \n\n{stdout}\n\n{stderr}"); + } + + let package_id: ObjectID = { + let stdout_trimmed = stdout.trim(); + ObjectID::from_str(stdout_trimmed).with_context(|| { + let stderr = std::str::from_utf8(&output.stderr).unwrap(); + format!("failed to find IDENTITY_IOTA_PKG_ID in response from: '{stdout_trimmed}'; {stderr}") + })? + }; + + // Persist package ID in order to avoid publishing the package for every test. + let package_id_str = package_id.to_string(); + std::env::set_var("IDENTITY_IOTA_PKG_ID", package_id_str.as_str()); + let mut file = std::fs::File::create(CACHED_PKG_ID)?; + write!(&mut file, "{};{}", package_id_str, active_address)?; + + Ok(package_id) +} + +pub async fn get_key_data() -> Result<(Storage, KeyId, Jwk, Vec), anyhow::Error> { + let storage = Storage::::new(JwkMemStore::new(), KeyIdMemstore::new()); + let generate = storage + .key_storage() + .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) + .await?; + let public_key_jwk = generate.jwk.to_public().expect("public components should be derivable"); + let public_key_bytes = get_public_key_bytes(&public_key_jwk)?; + // let sender_address = convert_to_address(&public_key_bytes)?; + + Ok((storage, generate.key_id, public_key_jwk, public_key_bytes)) +} + +fn get_public_key_bytes(sender_public_jwk: &Jwk) -> Result, anyhow::Error> { + let public_key_base_64 = &sender_public_jwk + .try_okp_params() + .map_err(|err| anyhow!("key not of type `Okp`; {err}"))? + .x; + + identity_jose::jwu::decode_b64(public_key_base_64).map_err(|err| anyhow!("could not decode base64 public key; {err}")) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct GasObjectHelper { + nanos_balance: u64, +} + +async fn get_balance(address: IotaAddress) -> anyhow::Result { + let output = Command::new("iota") + .arg("client") + .arg("gas") + .arg("--json") + .arg(address.to_string()) + .output() + .await?; + + if !output.status.success() { + let error_msg = String::from_utf8(output.stderr)?; + anyhow::bail!("failed to get balance: {error_msg}"); + } + + let balance = serde_json::from_slice::>(&output.stdout)? + .into_iter() + .map(|gas_info| gas_info.nanos_balance) + .sum(); + + Ok(balance) +} + +#[derive(Clone)] +pub struct TestClient { + client: Arc>, + storage: Arc, +} + +impl Deref for TestClient { + type Target = IdentityClient; + fn deref(&self) -> &Self::Target { + &self.client + } +} + +impl TestClient { + pub async fn new() -> anyhow::Result { + let active_address = get_active_address().await?; + Self::new_from_address(active_address).await + } + + pub async fn new_from_address(address: IotaAddress) -> anyhow::Result { + let api_endpoint = std::env::var("API_ENDPOINT").unwrap_or_else(|_| IOTA_LOCAL_NETWORK_URL.to_string()); + let client = IotaClientBuilder::default().build(&api_endpoint).await?; + let package_id = PACKAGE_ID.get_or_try_init(|| init(&client)).await.copied()?; + + let balance = get_balance(address).await?; + if balance < TEST_GAS_BUDGET { + request_funds(&address).await?; + } + + let storage = Arc::new(Storage::new(JwkMemStore::new(), KeyIdMemstore::new())); + let identity_client = IdentityClientReadOnly::new_with_pkg_id(client, package_id).await?; + let signer = KeytoolSigner::builder().build().await?; + let client = IdentityClient::new(identity_client, signer).await?; + + Ok(TestClient { + client: Arc::new(client), + storage, + }) + } + + pub async fn new_with_key_type(key_type: SignatureScheme) -> anyhow::Result { + let address = make_address(key_type).await?; + Self::new_from_address(address).await + } + // Sets the current address to the address controller by this client. + async fn switch_address(&self) -> anyhow::Result<()> { + let output = Command::new("iota") + .arg("client") + .arg("switch") + .arg("--address") + .arg(self.client.sender_address().to_string()) + .output() + .await?; + + if !output.status.success() { + anyhow::bail!( + "Failed to switch address: {}", + std::str::from_utf8(&output.stderr).unwrap() + ); + } + + Ok(()) + } + + pub fn package_id(&self) -> ObjectID { + self.client.package_id() + } + + pub fn signer(&self) -> &KeytoolSigner { + self.client.signer() + } + + pub async fn store_key_id_for_verification_method( + &self, + identity_client: IdentityClient>, + did: IotaDID, + ) -> anyhow::Result<()> { + let public_key = identity_client.signer().public_key(); + let key_id = identity_client.signer().key_id(); + let fragment = key_id.as_str(); + let method = VerificationMethod::new_from_jwk(did, public_key.clone(), Some(fragment))?; + let method_digest: MethodDigest = MethodDigest::new(&method)?; + + self + .storage + .key_id_storage() + .insert_key_id(method_digest, key_id.clone()) + .await?; + + Ok(()) + } + + pub async fn new_user_client(&self) -> anyhow::Result> { + let generate = self + .storage + .key_storage() + .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) + .await?; + let public_key_jwk = generate.jwk.to_public().expect("public components should be derivable"); + let signer = StorageSigner::new(&self.storage, generate.key_id, public_key_jwk); + + let user_client = IdentityClient::new((*self.client).clone(), signer).await?; + + request_funds(&user_client.sender_address()).await?; + + Ok(user_client) + } +} + +pub async fn get_test_coin(recipient: IotaAddress, client: &IdentityClient) -> anyhow::Result +where + S: Signer + OptionalSync, +{ + let mut ptb = ProgrammableTransactionBuilder::new(); + let coin = ptb.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("coin").into(), + ident_str!("zero").into(), + vec![TypeTag::Bool], + vec![], + ); + ptb.transfer_args(recipient, vec![coin]); + ptb + .finish() + .execute(client) + .await? + .response + .effects + .expect("tx should have had effects") + .created() + .first() + .map(|obj| obj.object_id()) + .context("no coins were created") +} + +pub async fn make_address(key_type: SignatureScheme) -> anyhow::Result { + if !matches!( + key_type, + SignatureScheme::ED25519 | SignatureScheme::Secp256k1 | SignatureScheme::Secp256r1 + ) { + anyhow::bail!("key type {key_type} is not supported"); + } + + let output = Command::new("iota") + .arg("client") + .arg("new-address") + .arg("--key-scheme") + .arg(key_type.to_string()) + .arg("--json") + .output() + .await?; + let new_address = { + let stdout = std::str::from_utf8(&output.stdout).unwrap(); + let start_of_json = stdout.find('{').ok_or_else(|| { + let stderr = std::str::from_utf8(&output.stderr).unwrap(); + anyhow!("No json in output: '{stdout}'; {stderr}",) + })?; + let json_result = serde_json::from_str::(stdout[start_of_json..].trim())?; + let address_str = json_result + .get("address") + .context("no address in JSON output")? + .as_str() + .context("address is not a JSON string")?; + + address_str.parse()? + }; + + request_funds(&new_address).await?; + + Ok(new_address) +} diff --git a/identity_iota_core/tests/e2e/identity.rs b/identity_iota_core/tests/e2e/identity.rs new file mode 100644 index 0000000000..b0df15e2a8 --- /dev/null +++ b/identity_iota_core/tests/e2e/identity.rs @@ -0,0 +1,418 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use crate::common; +use crate::common::get_funded_test_client; +use crate::common::get_key_data; +use crate::common::TEST_COIN_TYPE; +use crate::common::TEST_GAS_BUDGET; +use identity_iota_core::rebased::client::get_object_id_from_did; +use identity_iota_core::rebased::migration::has_previous_version; +use identity_iota_core::rebased::migration::Identity; +use identity_iota_core::rebased::proposals::ProposalResult; +use identity_iota_core::rebased::transaction::Transaction; +use identity_iota_core::IotaDID; +use identity_iota_core::IotaDocument; +use identity_verification::MethodScope; +use identity_verification::VerificationMethod; +use iota_sdk::rpc_types::IotaObjectData; +use iota_sdk::types::base_types::ObjectID; +use iota_sdk::types::base_types::SequenceNumber; +use iota_sdk::types::object::Owner; +use iota_sdk::types::transaction::ObjectArg; +use iota_sdk::types::TypeTag; +use iota_sdk::types::IOTA_FRAMEWORK_PACKAGE_ID; +use move_core_types::ident_str; +use move_core_types::language_storage::StructTag; + +#[tokio::test] +async fn identity_deactivation_works() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let mut identity = identity_client + .create_identity(IotaDocument::new(identity_client.network())) + .finish() + .execute(&identity_client) + .await? + .output; + + identity + .deactivate_did() + .finish(&identity_client) + .await? + .execute(&identity_client) + .await?; + + assert!(identity.did_document().metadata.deactivated == Some(true)); + + Ok(()) +} + +#[tokio::test] +async fn updating_onchain_identity_did_doc_with_single_controller_works() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let mut newly_created_identity = identity_client + .create_identity(IotaDocument::new(identity_client.network())) + .finish() + .execute_with_gas(TEST_GAS_BUDGET, &identity_client) + .await? + .output; + + let updated_did_doc = { + let did = IotaDID::parse(format!("did:iota:{}", newly_created_identity.id()))?; + let mut doc = IotaDocument::new_with_id(did.clone()); + doc.insert_method( + VerificationMethod::new_from_jwk( + did, + identity_client.signer().public_key().clone(), + Some(identity_client.signer().key_id().as_str()), + )?, + MethodScope::VerificationMethod, + )?; + doc + }; + + newly_created_identity + .update_did_document(updated_did_doc) + .finish(&identity_client) + .await? + .execute(&identity_client) + .await?; + + Ok(()) +} + +#[tokio::test] +async fn approving_proposal_works() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let alice_client = test_client.new_user_client().await?; + let bob_client = test_client.new_user_client().await?; + + let mut identity = alice_client + .create_identity(IotaDocument::new(alice_client.network())) + .controller(alice_client.sender_address(), 1) + .controller(bob_client.sender_address(), 1) + .threshold(2) + .finish() + .execute(&alice_client) + .await? + .output; + let did_doc = { + let did = IotaDID::parse(format!("did:iota:{}", identity.id()))?; + let mut doc = IotaDocument::new_with_id(did.clone()); + doc.insert_method( + VerificationMethod::new_from_jwk( + did, + alice_client.signer().public_key().clone(), + Some(alice_client.signer().key_id().as_str()), + )?, + MethodScope::VerificationMethod, + )?; + doc + }; + let ProposalResult::Pending(mut proposal) = identity + .update_did_document(did_doc) + .finish(&alice_client) + .await? + .execute(&alice_client) + .await? + .output + else { + anyhow::bail!("the proposal is executed"); + }; + proposal.approve(&identity).execute(&bob_client).await?; + + assert_eq!(proposal.votes(), 2); + + Ok(()) +} + +#[tokio::test] +async fn adding_controller_works() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let alice_client = test_client.new_user_client().await?; + let bob_client = test_client.new_user_client().await?; + + let mut identity = alice_client + .create_identity(IotaDocument::new(alice_client.network())) + .finish() + .execute(&alice_client) + .await? + .output; + + // Alice proposes to add Bob as a controller. Since Alice has enough voting power the proposal + // is executed directly after creation. + identity + .update_config() + .add_controller(bob_client.sender_address(), 1) + .finish(&alice_client) + .await? + .execute(&alice_client) + .await?; + + let cap = bob_client + .find_owned_ref( + StructTag::from_str(&format!("{}::controller::ControllerCap", test_client.package_id())).unwrap(), + |_| true, + ) + .await?; + + assert!(cap.is_some()); + + Ok(()) +} + +#[tokio::test] +async fn can_get_historical_identity_data() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let mut newly_created_identity = identity_client + .create_identity(IotaDocument::new(identity_client.network())) + .finish() + .execute_with_gas(TEST_GAS_BUDGET, &identity_client) + .await? + .output; + + let did = IotaDID::parse(format!("did:iota:{}", newly_created_identity.id()))?; + let updated_did_doc = { + let mut doc = IotaDocument::new_with_id(did.clone()); + let (_, key_id, public_key_jwk, _) = get_key_data().await?; + doc.insert_method( + VerificationMethod::new_from_jwk(did.clone(), public_key_jwk, Some(key_id.as_str()))?, + MethodScope::VerificationMethod, + )?; + doc + }; + + newly_created_identity + .update_did_document(updated_did_doc) + .finish(&identity_client) + .await? + .execute_with_gas(TEST_GAS_BUDGET, &identity_client) + .await?; + + let Identity::FullFledged(updated_identity) = identity_client.get_identity(get_object_id_from_did(&did)?).await? + else { + anyhow::bail!("resolved identity should be an onchain identity"); + }; + + let history = updated_identity.get_history(&identity_client, None, None).await?; + + // test check for previous version + let has_previous_version_responses: Vec = history + .iter() + .map(has_previous_version) + .collect::, identity_iota_core::rebased::Error>>()?; + assert_eq!(has_previous_version_responses, vec![true, false]); + + let versions: Vec = history.iter().map(|elem| elem.version).collect(); + let version_numbers: Vec = versions.iter().map(|v| (*v).into()).collect(); + + // Check that we have 2 versions (original and updated) + assert_eq!(version_numbers.len(), 2); + // Check that versions are in descending order (newest to oldest) + assert!( + version_numbers[0] > version_numbers[1], + "versions should be in descending order" + ); + + // paging: + // you can either loop until no result is returned + let mut result_index = 0; + let mut current_item: Option<&IotaObjectData> = None; + let mut history: Vec; + loop { + history = updated_identity + .get_history(&identity_client, current_item, Some(1)) + .await?; + if history.is_empty() { + break; + } + current_item = history.first(); + assert_eq!(current_item.unwrap().version, *versions.get(result_index).unwrap()); + result_index += 1; + } + + // or check before fetching next page + let mut result_index = 0; + let mut current_item: Option<&IotaObjectData> = None; + let mut history: Vec; + loop { + history = updated_identity + .get_history(&identity_client, current_item, Some(1)) + .await?; + + current_item = history.first(); + assert_eq!(current_item.unwrap().version, *versions.get(result_index).unwrap()); + result_index += 1; + + if !has_previous_version(current_item.unwrap())? { + break; + } + } + + Ok(()) +} + +#[tokio::test] +async fn send_proposal_works() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let mut identity = identity_client + .create_identity(IotaDocument::new(identity_client.network())) + .finish() + .execute_with_gas(TEST_GAS_BUDGET, &identity_client) + .await? + .output; + let identity_address = identity.id().into(); + + // Let's give the identity 2 coins in order to have something to move. + let coin1 = common::get_test_coin(identity_address, &identity_client).await?; + let coin2 = common::get_test_coin(identity_address, &identity_client).await?; + + // Let's propose the send of those two coins to the identity_client's address. + let ProposalResult::Executed(_) = identity + .send_assets() + .object(coin1, identity_client.sender_address()) + .object(coin2, identity_client.sender_address()) + .finish(&identity_client) + .await? + .execute(&identity_client) + .await? + .output + else { + panic!("the controller has enough voting power and the proposal should have been executed"); + }; + + // Assert that identity_client's address now owns those coins. + identity_client + .find_owned_ref(TEST_COIN_TYPE.clone(), |obj| obj.object_id == coin1) + .await? + .expect("coin1 was transfered to this address"); + + identity_client + .find_owned_ref(TEST_COIN_TYPE.clone(), |obj| obj.object_id == coin2) + .await? + .expect("coin2 was transfered to this address"); + + Ok(()) +} + +#[tokio::test] +async fn borrow_proposal_works() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let mut identity = identity_client + .create_identity(IotaDocument::new(identity_client.network())) + .finish() + .execute(&identity_client) + .await? + .output; + let identity_address = identity.id().into(); + + let coin1 = common::get_test_coin(identity_address, &identity_client).await?; + let coin2 = common::get_test_coin(identity_address, &identity_client).await?; + + // Let's propose the borrow of those two coins to the identity_client's address. + let ProposalResult::Executed(_) = identity + .borrow_assets() + .borrow(coin1) + .borrow(coin2) + .with_intent(move |ptb, objs| { + ptb.programmable_move_call( + IOTA_FRAMEWORK_PACKAGE_ID, + ident_str!("coin").into(), + ident_str!("value").into(), + vec![TypeTag::Bool], + vec![objs.get(&coin1).expect("coin1 data is borrowed").0], + ); + }) + .finish(&identity_client) + .await? + .execute(&identity_client) + .await? + .output + else { + panic!("controller has enough voting power and proposal should have been executed"); + }; + + Ok(()) +} + +#[tokio::test] +async fn controller_execution_works() -> anyhow::Result<()> { + let test_client = get_funded_test_client().await?; + let identity_client = test_client.new_user_client().await?; + + let mut identity = identity_client + .create_identity(IotaDocument::new(identity_client.network())) + .finish() + .execute(&identity_client) + .await? + .output; + let identity_address = identity.id().into(); + + // Create a second identity owned by the first. + let identity2 = identity_client + .create_identity(IotaDocument::new(identity_client.network())) + .controller(identity_address, 1) + .threshold(1) + .finish() + .execute(&identity_client) + .await? + .output; + + // Let's find identity's controller cap for identity2. + let controller_cap = identity_client + .find_owned_ref_for_address( + identity_address, + format!("{}::controller::ControllerCap", identity_client.package_id()).parse()?, + |_| true, + ) + .await? + .expect("identity is a controller of identity2"); + + let identity2_ref = identity_client.get_object_ref_by_id(identity2.id()).await?.unwrap(); + let Owner::Shared { initial_shared_version } = identity2_ref.owner else { + panic!("identity2 is shared") + }; + // Perform an action on `identity2` as a controller of `identity`. + let result = identity + .controller_execution(controller_cap.0) + .with_intent(|ptb, controller_cap| { + let identity2 = ptb + .obj(ObjectArg::SharedObject { + id: identity2_ref.object_id(), + initial_shared_version, + mutable: true, + }) + .unwrap(); + + let token_to_revoke = ptb.pure(ObjectID::ZERO).unwrap(); + + ptb.programmable_move_call( + identity_client.package_id(), + ident_str!("identity").into(), + ident_str!("revoke_token").into(), + vec![], + vec![identity2, *controller_cap, token_to_revoke], + ); + }) + .finish(&identity_client) + .await? + .execute(&identity_client) + .await?; + + assert!(result.response.status_ok().unwrap()); + assert!(matches!(result.output, ProposalResult::Executed(_))); + + Ok(()) +} diff --git a/identity_iota_core/tests/e2e/main.rs b/identity_iota_core/tests/e2e/main.rs new file mode 100644 index 0000000000..47cff143e4 --- /dev/null +++ b/identity_iota_core/tests/e2e/main.rs @@ -0,0 +1,8 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod asset; +mod client; +pub mod common; +mod identity; +mod migration; diff --git a/identity_iota_core/tests/e2e/migration.rs b/identity_iota_core/tests/e2e/migration.rs new file mode 100644 index 0000000000..39597203e5 --- /dev/null +++ b/identity_iota_core/tests/e2e/migration.rs @@ -0,0 +1,17 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::common::get_funded_test_client; +use identity_iota_core::rebased::migration; +use iota_sdk::types::base_types::ObjectID; + +#[tokio::test] +async fn migration_registry_is_found() -> anyhow::Result<()> { + let client = get_funded_test_client().await?; + let random_alias_id = ObjectID::random(); + + let doc = migration::lookup(&client, random_alias_id).await?; + assert!(doc.is_none()); + + Ok(()) +} diff --git a/identity_iota_interaction/Cargo.toml b/identity_iota_interaction/Cargo.toml new file mode 100644 index 0000000000..6f6c016736 --- /dev/null +++ b/identity_iota_interaction/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "identity_iota_interaction" +version = "1.5.0" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords = ["iota", "tangle", "identity"] +license.workspace = true +readme = "./README.md" +repository.workspace = true +rust-version.workspace = true +description = "Trait definitions and a wasm32 compatible subset of code, copied from the IOTA Rust SDK, used to replace the IOTA Rust SDK for wasm32 builds." + +[dependencies] +anyhow = "1.0.75" +async-trait = { version = "0.1.81", default-features = false } +cfg-if = "1.0.0" +fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "5f2c63266a065996d53f98156f0412782b468597", package = "fastcrypto" } +jsonpath-rust = { version = "0.5.1", optional = true } +secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", default-features = false, tag = "v0.2.0" } +serde.workspace = true +serde_json.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v0.9.2-rc" } +move-core-types = { git = "https://github.com/iotaledger/iota.git", package = "move-core-types", tag = "v0.9.2-rc" } +tokio = { version = "1", optional = true, default-features = false, features = ["process"] } + +shared-crypto = { git = "https://github.com/iotaledger/iota.git", package = "shared-crypto", tag = "v0.9.2-rc" } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +bcs = "0.1.4" +eyre = { version = "0.6" } +fastcrypto-zkp = { git = "https://github.com/MystenLabs/fastcrypto", rev = "5f2c63266a065996d53f98156f0412782b468597", package = "fastcrypto-zkp" } +getrandom = { version = "0.2", default-features = false, features = ["js"] } +hex = { version = "0.4" } +itertools = "0.13" +jsonrpsee = { version = "0.24", default-features = false, features = ["wasm-client"] } +leb128 = { version = "0.2" } +num-bigint = { version = "0.4" } +primitive-types = { version = "0.12", features = ["impl-serde"] } +rand = "0.8.5" +ref-cast = { version = "1.0" } +serde_repr = { version = "0.1" } +serde_with = { version = "3.8", features = ["hex"] } +strum.workspace = true +thiserror.workspace = true +tracing = { version = "0.1" } +uint = { version = "0.9" } +derive_more = "0.99.18" +enum_dispatch = "0.3.13" +schemars = "0.8.21" + +[package.metadata.docs.rs] +# To build locally: +# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps --workspace --open +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[features] +default = ["send-sync-transaction", "secret-storage/default"] +send-sync-transaction = ["secret-storage/send-sync-storage"] +keytool-signer = ["dep:tokio", "dep:jsonpath-rust"] + +[lints.clippy] +result_large_err = "allow" + +[lints.rust] +# from local sdk types +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(msim)'] } diff --git a/identity_iota_interaction/README.md b/identity_iota_interaction/README.md new file mode 100644 index 0000000000..abee8ad9e1 --- /dev/null +++ b/identity_iota_interaction/README.md @@ -0,0 +1,51 @@ +# Platform Agnostic Iota Interaction + +This crate gathers types needed to interact with IOTA nodes in a platform-agnostic way +to allow building the Identity library for WASM32 architectures. + +The folder `sdk_types`, contained in this crate, provides a selection of +code copied from the iotaledger/iota.git repository: + +| Folder Name | Original Source in iotaledger/iota.git | +|------------------------------------|------------------------------------------------------| +| sdk_types/iota_json_rpc_types | crates/iota-json-rpc-types | +| sdk_types/iota_types | crates/iota-types | +| sdk_types/move_command_line_common | external-crates/move/crates/move-command-line-common | +| sdk_types/move_core_types | external-crates/move/crates/move-core-types | +| sdk_types/shared_crypto | crates/shared-crypto/Cargo.toml | + +The folder structure in `sdk_types` matches the way the original IOTA Client Rust SDK +provides the above listed crates via `pub use`. + +This crate (file 'lib.rs' contained in this folder) provides several +`build target` specific `pub use` and `type` expressions: + +* For **NON wasm32 targets**, the original _IOTA Client Rust SDK_ sources are provided +* For **WASM32 targets** the code contained in the `sdk_types` folder is used + +Please make sure always to import the SDK dependencies via `use identity_iota::iota_interaction::...` +instead of `use iota_sdk::...` in your code. This way the dependencies needed for your +code are automatically switched according to the currently used build target. + +The Advantage of this target specific dependency switching is, +that for NON wasm32 targets no type marshalling is needed because +the original Rust SDK types are used. + +The drawback of target specific dependency switching is, that code of +the original Rust SDK could be used, that is not contained in the +`sdk_types` folder. The following todos result from this drawback: + +TODOs: + +* Always build your code additionally for the wasm32-unknown-unknown target + before committing your code:
+ `cargo build --package identity_iota_.... --lib --target wasm32-unknown-unknown` +* We need to add tests for the wasm32-unknown-unknown target in the CI toolchain + to make sure the code is always buildable for wasm32 targets. + +All cross-platform usable types and traits (cross-platform-traits) +are contained in this crate. +Platform specific adapters (implementing the cross-platform-traits) are contained in +the crate [bindings/wasm/iota_interaction_ts](../../bindings/wasm/iota_interaction_ts) +and in the folder +[identity_iota_core/src/iota_interaction_rust](../../identity_iota_core/src/iota_interaction_rust). \ No newline at end of file diff --git a/identity_iota_interaction/src/iota_client_trait.rs b/identity_iota_interaction/src/iota_client_trait.rs new file mode 100644 index 0000000000..700a6432b1 --- /dev/null +++ b/identity_iota_interaction/src/iota_client_trait.rs @@ -0,0 +1,272 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::IotaRpcResult; +use crate::rpc_types::CoinPage; +use crate::rpc_types::EventFilter; +use crate::rpc_types::EventPage; +use crate::rpc_types::IotaExecutionStatus; +use crate::rpc_types::IotaObjectData; +use crate::rpc_types::IotaObjectDataOptions; +use crate::rpc_types::IotaObjectResponse; +use crate::rpc_types::IotaObjectResponseQuery; +use crate::rpc_types::IotaPastObjectResponse; +use crate::rpc_types::IotaTransactionBlockResponseOptions; +use crate::rpc_types::ObjectsPage; +use crate::rpc_types::OwnedObjectRef; +use crate::types::base_types::IotaAddress; +use crate::types::base_types::ObjectID; +use crate::types::base_types::SequenceNumber; +use crate::types::crypto::PublicKey; +use crate::types::crypto::Signature; +use crate::types::digests::TransactionDigest; +use crate::types::dynamic_field::DynamicFieldName; +use crate::types::event::EventID; +use crate::types::quorum_driver_types::ExecuteTransactionRequestType; +use crate::OptionalSend; +use crate::ProgrammableTransactionBcs; +use crate::SignatureBcs; +use crate::TransactionDataBcs; +use async_trait::async_trait; +use secret_storage::SignatureScheme as SignatureSchemeSecretStorage; +use secret_storage::Signer; +use std::boxed::Box; +use std::option::Option; +use std::result::Result; + +#[cfg(not(target_arch = "wasm32"))] +use std::marker::Send; + +#[cfg(feature = "send-sync-transaction")] +use crate::OptionalSync; + +pub struct IotaKeySignature { + pub public_key: PublicKey, + pub signature: Signature, +} + +impl SignatureSchemeSecretStorage for IotaKeySignature { + type PublicKey = PublicKey; + type Signature = Signature; + type Input = TransactionDataBcs; +} + +//******************************************************************** +// TODO: rename the following traits to have a consistent relation +// between platform specific trait specializations +// and the platform agnostic traits specified in this file: +// * QuorumDriverTrait -> QuorumDriverApiT +// * ReadTrait -> ReadApiT +// * CoinReadTrait -> CoinReadApiT +// * EventTrait -> EventApiT +// +// Platform specific trait specializations are defined +// in modules identity_iota_core::iota_interaction_rust and +// iota_interaction_ts with the following names: +// * QuorumDriverApiAdaptedT +// * ReadApiAdaptedT +// * CoinReadApiAdaptedT +// * EventApiAdaptedT +// * IotaClientAdaptedT +//******************************************************************** + +/// Adapter Allowing to query information from an IotaTransactionBlockResponse instance. +/// As IotaTransactionBlockResponse pulls too many dependencies we need to +/// hide it behind a trait. +#[cfg_attr(not(feature = "send-sync-transaction"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-transaction", async_trait)] +pub trait IotaTransactionBlockResponseT: OptionalSend { + /// Error type used + type Error; + /// The response type used in the platform specific client sdk + type NativeResponse; + + /// Indicates if IotaTransactionBlockResponse::effects is None + fn effects_is_none(&self) -> bool; + /// Indicates if there are Some(effects) + fn effects_is_some(&self) -> bool; + + /// Returns Debug representation of the IotaTransactionBlockResponse + fn to_string(&self) -> String; + + /// If effects_is_some(), returns a clone of the IotaTransactionBlockEffectsAPI::status() + /// Otherwise, returns None + fn effects_execution_status(&self) -> Option; + + /// If effects_is_some(), returns IotaTransactionBlockEffectsAPI::created() + /// as owned Vec. + /// Otherwise, returns None + fn effects_created(&self) -> Option>; + + /// Returns a reference to the platform specific client sdk response instance wrapped by this adapter + fn as_native_response(&self) -> &Self::NativeResponse; + + /// Returns a mutable reference to the platform specific client sdk response instance wrapped by this adapter + fn as_mut_native_response(&mut self) -> &mut Self::NativeResponse; + + /// Returns a clone of the wrapped platform specific client sdk response + fn clone_native_response(&self) -> Self::NativeResponse; + + // Returns digest for transaction block. + fn digest(&self) -> Result; +} + +#[cfg_attr(not(feature = "send-sync-transaction"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-transaction", async_trait)] +pub trait QuorumDriverTrait { + /// Error type used + type Error; + /// The response type used in the platform specific client sdk + type NativeResponse; + + async fn execute_transaction_block( + &self, + tx_data_bcs: &TransactionDataBcs, + signatures: &[SignatureBcs], + options: Option, + request_type: Option, + ) -> IotaRpcResult>>; +} + +#[cfg_attr(not(feature = "send-sync-transaction"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-transaction", async_trait)] +pub trait ReadTrait { + /// Error type used + type Error; + /// The response type used in the platform specific client sdk + type NativeResponse; + + async fn get_chain_identifier(&self) -> Result; + + async fn get_dynamic_field_object( + &self, + parent_object_id: ObjectID, + name: DynamicFieldName, + ) -> IotaRpcResult; + + async fn get_object_with_options( + &self, + object_id: ObjectID, + options: IotaObjectDataOptions, + ) -> IotaRpcResult; + + async fn get_owned_objects( + &self, + address: IotaAddress, + query: Option, + cursor: Option, + limit: Option, + ) -> IotaRpcResult; + + async fn get_reference_gas_price(&self) -> IotaRpcResult; + + async fn get_transaction_with_options( + &self, + digest: TransactionDigest, + options: IotaTransactionBlockResponseOptions, + ) -> IotaRpcResult>>; + + async fn try_get_parsed_past_object( + &self, + object_id: ObjectID, + version: SequenceNumber, + options: IotaObjectDataOptions, + ) -> IotaRpcResult; +} + +#[cfg_attr(not(feature = "send-sync-transaction"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-transaction", async_trait)] +pub trait CoinReadTrait { + type Error; + + async fn get_coins( + &self, + owner: IotaAddress, + coin_type: Option, + cursor: Option, + limit: Option, + ) -> IotaRpcResult; +} + +#[cfg_attr(not(feature = "send-sync-transaction"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-transaction", async_trait)] +pub trait EventTrait { + /// Error type used + type Error; + + async fn query_events( + &self, + query: EventFilter, + cursor: Option, + limit: Option, + descending_order: bool, + ) -> IotaRpcResult; +} + +#[cfg_attr(not(feature = "send-sync-transaction"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-transaction", async_trait)] +pub trait IotaClientTrait { + /// Error type used + type Error; + /// The response type used in the platform specific client sdk + type NativeResponse; + + #[cfg(not(feature = "send-sync-transaction"))] + fn quorum_driver_api( + &self, + ) -> Box + '_>; + #[cfg(feature = "send-sync-transaction")] + fn quorum_driver_api( + &self, + ) -> Box + Send + '_>; + + #[cfg(not(feature = "send-sync-transaction"))] + fn read_api(&self) -> Box + '_>; + #[cfg(feature = "send-sync-transaction")] + fn read_api(&self) -> Box + Send + '_>; + + #[cfg(not(feature = "send-sync-transaction"))] + fn coin_read_api(&self) -> Box + '_>; + #[cfg(feature = "send-sync-transaction")] + fn coin_read_api(&self) -> Box + Send + '_>; + + #[cfg(not(feature = "send-sync-transaction"))] + fn event_api(&self) -> Box + '_>; + #[cfg(feature = "send-sync-transaction")] + fn event_api(&self) -> Box + Send + '_>; + + #[cfg(not(feature = "send-sync-transaction"))] + async fn execute_transaction>( + &self, + tx_bcs: ProgrammableTransactionBcs, + gas_budget: Option, + signer: &S, + ) -> Result< + Box>, + Self::Error, + >; + #[cfg(feature = "send-sync-transaction")] + async fn execute_transaction + OptionalSync>( + &self, + tx_bcs: ProgrammableTransactionBcs, + gas_budget: Option, + signer: &S, + ) -> Result< + Box>, + Self::Error, + >; + + async fn default_gas_budget( + &self, + sender_address: IotaAddress, + tx_bcs: &ProgrammableTransactionBcs, + ) -> Result; + + async fn get_previous_version(&self, iod: IotaObjectData) -> Result, Self::Error>; + + async fn get_past_object( + &self, + object_id: ObjectID, + version: SequenceNumber, + ) -> Result; +} diff --git a/identity_iota_interaction/src/iota_verifiable_credential.rs b/identity_iota_interaction/src/iota_verifiable_credential.rs new file mode 100644 index 0000000000..f28f178a6c --- /dev/null +++ b/identity_iota_interaction/src/iota_verifiable_credential.rs @@ -0,0 +1,39 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::move_types::language_storage::TypeTag; +use crate::types::base_types::ObjectID; +use crate::MoveType; +use crate::TypedValue; +use serde::Deserialize; +use serde::Serialize; +use std::str::FromStr; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IotaVerifiableCredential { + data: Vec, +} + +impl IotaVerifiableCredential { + pub fn new(data: Vec) -> IotaVerifiableCredential { + IotaVerifiableCredential { data } + } + + pub fn data(&self) -> &Vec { + &self.data + } +} + +impl MoveType for IotaVerifiableCredential { + fn move_type(package: ObjectID) -> TypeTag { + TypeTag::from_str(&format!("{package}::public_vc::PublicVc")).expect("valid utf8") + } + + fn get_typed_value(&self, _package: ObjectID) -> TypedValue + where + Self: MoveType, + Self: Sized, + { + TypedValue::IotaVerifiableCredential(self) + } +} diff --git a/identity_iota_interaction/src/keytool_signer.rs b/identity_iota_interaction/src/keytool_signer.rs new file mode 100644 index 0000000000..716606b766 --- /dev/null +++ b/identity_iota_interaction/src/keytool_signer.rs @@ -0,0 +1,205 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::path::Path; +use std::path::PathBuf; + +use crate::types::base_types::IotaAddress; +use crate::types::crypto::PublicKey; +use crate::types::crypto::Signature; +use crate::types::crypto::SignatureScheme; +use crate::IotaKeySignature; +use crate::TransactionDataBcs; +use anyhow::anyhow; +use anyhow::Context as _; +use async_trait::async_trait; +use fastcrypto::encoding::Base64; +use fastcrypto::encoding::Encoding; +use jsonpath_rust::JsonPathQuery as _; +use secret_storage::Error as SecretStorageError; +use secret_storage::Signer; +use serde::Deserialize; +use serde_json::Value; + +/// Builder structure to ease the creation of a [KeytoolSigner]. +#[derive(Debug, Default)] +pub struct KeytoolSignerBuilder { + address: Option, + iota_bin: Option, +} + +impl KeytoolSignerBuilder { + /// Returns a new [KeytoolSignerBuilder] with default configuration: + /// - use current active address; + /// - assumes `iota` binary to be in PATH; + pub fn new() -> Self { + Self::default() + } + + /// Sets the address the signer will use. + /// Defaults to current active address if no address is provided. + pub fn with_address(mut self, address: IotaAddress) -> Self { + self.address = Some(address); + self + } + + /// Sets the path to the `iota` binary to use. + /// Assumes `iota` to be in PATH if no value is provided. + pub fn iota_bin_location(mut self, path: impl AsRef) -> Self { + let path = path.as_ref().to_path_buf(); + self.iota_bin = Some(path); + + self + } + + /// Builds a new [KeytoolSigner] using the provided configuration. + pub async fn build(self) -> anyhow::Result { + let KeytoolSignerBuilder { address, iota_bin } = self; + let iota_bin = iota_bin.unwrap_or_else(|| "iota".into()); + let address = if let Some(address) = address { + address + } else { + get_active_address(&iota_bin).await? + }; + + let public_key = get_key(&iota_bin, address).await.context("cannot find key")?; + + Ok(KeytoolSigner { + public_key, + iota_bin, + address, + }) + } +} + +/// IOTA Keytool [Signer] implementation. +#[derive(Debug)] +pub struct KeytoolSigner { + public_key: PublicKey, + iota_bin: PathBuf, + address: IotaAddress, +} + +impl KeytoolSigner { + /// Returns a [KeytoolSignerBuilder]. + pub fn builder() -> KeytoolSignerBuilder { + KeytoolSignerBuilder::default() + } + + /// Returns the [IotaAddress] used by this [KeytoolSigner]. + pub fn address(&self) -> IotaAddress { + self.address + } + + /// Returns the [PublicKey] used by this [KeytoolSigner]. + pub fn public_key(&self) -> &PublicKey { + &self.public_key + } + + async fn run_iota_cli_command(&self, args: &str) -> anyhow::Result { + run_iota_cli_command_with_bin(&self.iota_bin, args).await + } +} + +#[cfg_attr(feature = "send-sync-transaction", async_trait)] +#[cfg_attr(not(feature = "send-sync-transaction"), async_trait(?Send))] +impl Signer for KeytoolSigner { + type KeyId = IotaAddress; + + fn key_id(&self) -> &Self::KeyId { + &self.address + } + + async fn public_key(&self) -> Result { + Ok(self.public_key.clone()) + } + + async fn sign(&self, data: &TransactionDataBcs) -> Result { + let base64_data = Base64::encode(data); + let command = format!("keytool sign --address {} --data {base64_data}", self.address); + + self + .run_iota_cli_command(&command) + .await + .and_then(|json| { + json + .get("iotaSignature") + .context("invalid JSON output: missing iotaSignature")? + .as_str() + .context("not a JSON string")? + .parse() + .map_err(|e| anyhow!("invalid IOTA signature: {e}")) + }) + .map_err(SecretStorageError::Other) + } +} + +async fn run_iota_cli_command_with_bin(iota_bin: impl AsRef, args: &str) -> anyhow::Result { + let iota_bin = iota_bin.as_ref(); + + cfg_if::cfg_if! { + if #[cfg(not(target_arch = "wasm32"))] { + let output = tokio::process::Command::new(iota_bin) + .args(args.split_ascii_whitespace()) + .arg("--json") + .output() + .await + .map_err(|e| anyhow!("failed to run command: {e}"))?; + + if !output.status.success() { + let err_msg = + String::from_utf8(output.stderr).map_err(|e| anyhow!("command failed with non-utf8 error message: {e}"))?; + return Err(anyhow!("failed to run \"iota client active-address\": {err_msg}")); + } + + serde_json::from_slice(&output.stdout).context("invalid JSON object in command output") + } else { + extern "Rust" { + fn __wasm_exec_iota_cmd(cmd: &str) -> anyhow::Result; + } + let iota_bin = iota_bin.to_str().context("invalid IOTA bin path")?; + let cmd = format!("{iota_bin} {args} --json"); + unsafe { __wasm_exec_iota_cmd(&cmd) } + } + } +} + +async fn get_active_address(iota_bin: impl AsRef) -> anyhow::Result { + run_iota_cli_command_with_bin(iota_bin, "client active-address") + .await + .and_then(|value| serde_json::from_value(value).context("failed to parse IotaAddress from output")) +} + +async fn get_key(iota_bin: impl AsRef, address: IotaAddress) -> anyhow::Result { + let query = format!("$[?(@.iotaAddress==\"{}\")]", address); + + let pk_json_data = run_iota_cli_command_with_bin(iota_bin, "keytool list") + .await + .and_then(|json_value| { + json_value + .path(&query) + .map_err(|e| anyhow!("failed to query JSON output: {e}"))? + .get_mut(0) + .context("key not found") + .map(Value::take) + })?; + let Ok(KeytoolPublicKeyHelper { + public_base64_key, + flag, + }) = serde_json::from_value(pk_json_data) + else { + return Err(anyhow!("invalid key format")); + }; + + let signature_scheme = SignatureScheme::from_flag_byte(&flag).context(format!("invalid signature flag `{flag}`"))?; + let pk_bytes = Base64::decode(&public_base64_key).context("invalid base64 encoding for key")?; + + PublicKey::try_from_bytes(signature_scheme, &pk_bytes).map_err(|e| anyhow!("{e:?}")) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct KeytoolPublicKeyHelper { + public_base64_key: String, + flag: u8, +} diff --git a/identity_iota_interaction/src/lib.rs b/identity_iota_interaction/src/lib.rs new file mode 100644 index 0000000000..e4e0b58319 --- /dev/null +++ b/identity_iota_interaction/src/lib.rs @@ -0,0 +1,130 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#![allow(missing_docs)] + +mod iota_client_trait; +mod iota_verifiable_credential; +#[cfg(feature = "keytool-signer")] +pub mod keytool_signer; +mod move_call_traits; +mod move_type; +mod transaction_builder_trait; + +pub use iota_client_trait::*; +pub use iota_verifiable_credential::*; +#[cfg(feature = "keytool-signer")] +pub use keytool_signer::*; +pub use move_call_traits::*; +pub use move_type::*; +pub use transaction_builder_trait::*; + +#[cfg(target_arch = "wasm32")] +mod sdk_types; +#[cfg(target_arch = "wasm32")] +pub use sdk_types::*; + +#[cfg(not(target_arch = "wasm32"))] +pub use iota_sdk::*; +#[cfg(not(target_arch = "wasm32"))] +pub use move_core_types as move_types; +#[cfg(not(target_arch = "wasm32"))] +pub use shared_crypto; + +/// BCS serialized Transaction, where a Transaction includes the TransactionData and a Vec +pub type TransactionBcs = Vec; +/// BCS serialized TransactionData +/// TransactionData usually contain the ProgrammableTransaction, sender, kind = ProgrammableTransaction, +/// gas_coin, gas_budget, gas_price, expiration, ... +/// Example usage: +/// * TS: ExecuteTransactionBlockParams::transactionBlock - Base64 encoded TransactionDataBcs +pub type TransactionDataBcs = Vec; +/// BCS serialized Signature +pub type SignatureBcs = Vec; +/// BCS serialized ProgrammableTransaction +/// A ProgrammableTransaction +/// * has `inputs` (or *CallArgs*) and `commands` (or *Transactions*) +/// * is the result of ProgrammableTransactionBuilder::finish() +pub type ProgrammableTransactionBcs = Vec; +/// BCS serialized IotaTransactionBlockResponse +pub type IotaTransactionBlockResponseBcs = Vec; + +// dummy types, have to be replaced with actual types later on +pub type DummySigner = str; +pub type Hashable = Vec; +pub type Identity = (); + +/// `ident_str!` is a compile-time validated macro that constructs a +/// `&'static IdentStr` from a const `&'static str`. +/// +/// ### Example +/// +/// Creating a valid static or const [`IdentStr`]: +/// +/// ```rust +/// use move_core_types::ident_str; +/// use move_core_types::identifier::IdentStr; +/// const VALID_IDENT: &'static IdentStr = ident_str!("MyCoolIdentifier"); +/// +/// const THING_NAME: &'static str = "thing_name"; +/// const THING_IDENT: &'static IdentStr = ident_str!(THING_NAME); +/// ``` +/// +/// In contrast, creating an invalid [`IdentStr`] will fail at compile time: +/// +/// ```rust,compile_fail +/// use move_core_types::{ident_str, identifier::IdentStr}; +/// const INVALID_IDENT: &'static IdentStr = ident_str!("123Foo"); // Fails to compile! +/// ``` +// TODO(philiphayes): this should really be an associated const fn like `IdentStr::new`; +// unfortunately, both unsafe-reborrow and unsafe-transmute don't currently work +// inside const fn's. Only unsafe-transmute works inside static const-blocks +// (but not const-fn's). +#[macro_export] +macro_rules! ident_str { + ($ident:expr) => {{ + // Only static strings allowed. + let s: &'static str = $ident; + + // Only valid identifier strings are allowed. + // Note: Work-around hack to print an error message in a const block. + let is_valid = $crate::move_types::identifier::is_valid(s); + ["String is not a valid Move identifier"][!is_valid as usize]; + + // SAFETY: the following transmute is safe because + // (1) it's equivalent to the unsafe-reborrow inside IdentStr::ref_cast() + // (which we can't use b/c it's not const). + // (2) we've just asserted that IdentStr impls RefCast, which + // already guarantees the transmute is safe (RefCast checks that + // IdentStr(str) is #[repr(transparent)]). + // (3) both in and out lifetimes are 'static, so we're not widening the + // lifetime. (4) we've just asserted that the IdentStr passes the + // is_valid check. + // + // Note: this lint is unjustified and no longer checked. See issue: + // https://github.com/rust-lang/rust-clippy/issues/6372 + #[allow(clippy::transmute_ptr_to_ptr)] + unsafe { + ::std::mem::transmute::<&'static str, &'static $crate::move_types::identifier::IdentStr>(s) + } + }}; +} + +// Alias name for the Send trait controlled by the "send-sync-transaction" feature +cfg_if::cfg_if! { + if #[cfg(feature = "send-sync-transaction")] { + pub trait OptionalSend: Send {} + impl OptionalSend for T where T: Send {} + + pub trait OptionalSync: Sync {} + impl OptionalSync for T where T: Sync {} + } else { + pub trait OptionalSend: {} + impl OptionalSend for T {} + + pub trait OptionalSync: {} + impl OptionalSync for T where T: {} + } +} diff --git a/identity_iota_interaction/src/move_call_traits.rs b/identity_iota_interaction/src/move_call_traits.rs new file mode 100644 index 0000000000..4d46bd0935 --- /dev/null +++ b/identity_iota_interaction/src/move_call_traits.rs @@ -0,0 +1,250 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::collections::HashSet; +use std::iter::IntoIterator; + +use async_trait::async_trait; +use serde::Serialize; + +use crate::rpc_types::IotaObjectData; +use crate::rpc_types::OwnedObjectRef; +use crate::types::base_types::IotaAddress; +use crate::types::base_types::ObjectID; +use crate::types::base_types::ObjectRef; +use crate::types::base_types::SequenceNumber; +use crate::types::transaction::Argument; +use crate::types::TypeTag; +use crate::MoveType; +use crate::ProgrammableTransactionBcs; + +pub trait AssetMoveCalls { + type Error; + + fn new_asset( + inner: T, + mutable: bool, + transferable: bool, + deletable: bool, + package: ObjectID, + ) -> Result; + + fn delete(asset: ObjectRef, package: ObjectID) -> Result; + + fn transfer( + asset: ObjectRef, + recipient: IotaAddress, + package: ObjectID, + ) -> Result; + + fn make_tx( + proposal: (ObjectID, SequenceNumber), + cap: ObjectRef, + asset: ObjectRef, + asset_type_param: TypeTag, + package: ObjectID, + function_name: &'static str, + ) -> Result; + + fn accept_proposal( + proposal: (ObjectID, SequenceNumber), + recipient_cap: ObjectRef, + asset: ObjectRef, + asset_type_param: TypeTag, + package: ObjectID, + ) -> Result; + + fn conclude_or_cancel( + proposal: (ObjectID, SequenceNumber), + sender_cap: ObjectRef, + asset: ObjectRef, + asset_type_param: TypeTag, + package: ObjectID, + ) -> Result; + + fn update( + asset: ObjectRef, + new_content: T, + package: ObjectID, + ) -> Result; +} + +pub trait MigrationMoveCalls { + type Error; + + fn migrate_did_output( + did_output: ObjectRef, + creation_timestamp: Option, + migration_registry: OwnedObjectRef, + package: ObjectID, + ) -> anyhow::Result; +} + +pub trait BorrowIntentFnInternalT: FnOnce(&mut B, &HashMap) {} +impl BorrowIntentFnInternalT for T where T: FnOnce(&mut B, &HashMap) {} + +pub trait ControllerIntentFnInternalT: FnOnce(&mut B, &Argument) {} +impl ControllerIntentFnInternalT for T where T: FnOnce(&mut B, &Argument) {} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait IdentityMoveCalls { + type Error; + type NativeTxBuilder; + + fn propose_borrow( + identity: OwnedObjectRef, + capability: ObjectRef, + objects: Vec, + expiration: Option, + package_id: ObjectID, + ) -> Result; + + fn execute_borrow>( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + objects: Vec, + intent_fn: F, + package: ObjectID, + ) -> Result; + + fn create_and_execute_borrow( + identity: OwnedObjectRef, + capability: ObjectRef, + objects: Vec, + intent_fn: F, + expiration: Option, + package_id: ObjectID, + ) -> anyhow::Result + where + F: BorrowIntentFnInternalT; + + // We allow clippy::too_many_arguments here because splitting this trait function into multiple + // other functions or creating an options struct gathering multiple function arguments has lower + // priority at the moment. + // TODO: remove clippy::too_many_arguments allowance here + #[allow(clippy::too_many_arguments)] + fn propose_config_change( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + expiration: Option, + threshold: Option, + controllers_to_add: I1, + controllers_to_remove: HashSet, + controllers_to_update: I2, + package: ObjectID, + ) -> Result + where + I1: IntoIterator, + I2: IntoIterator; + + fn execute_config_change( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + proposal_id: ObjectID, + package: ObjectID, + ) -> Result; + + fn propose_controller_execution( + identity: OwnedObjectRef, + capability: ObjectRef, + controller_cap_id: ObjectID, + expiration: Option, + package_id: ObjectID, + ) -> Result; + + fn execute_controller_execution>( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + borrowing_controller_cap_ref: ObjectRef, + intent_fn: F, + package: ObjectID, + ) -> Result; + + fn create_and_execute_controller_execution( + identity: OwnedObjectRef, + capability: ObjectRef, + expiration: Option, + borrowing_controller_cap_ref: ObjectRef, + intent_fn: F, + package_id: ObjectID, + ) -> Result + where + F: ControllerIntentFnInternalT; + + async fn new_identity( + did_doc: Option<&[u8]>, + package_id: ObjectID, + ) -> Result; + + fn new_with_controllers>( + did_doc: Option<&[u8]>, + controllers: C, + threshold: u64, + package_id: ObjectID, + ) -> Result; + + fn approve_proposal( + identity: OwnedObjectRef, + controller_cap: ObjectRef, + proposal_id: ObjectID, + package: ObjectID, + ) -> Result; + + fn propose_send( + identity: OwnedObjectRef, + capability: ObjectRef, + transfer_map: Vec<(ObjectID, IotaAddress)>, + expiration: Option, + package_id: ObjectID, + ) -> Result; + + fn execute_send( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + objects: Vec<(ObjectRef, TypeTag)>, + package: ObjectID, + ) -> Result; + + async fn propose_update( + identity: OwnedObjectRef, + capability: ObjectRef, + did_doc: Option<&[u8]>, + expiration: Option, + package_id: ObjectID, + ) -> Result; + + fn execute_update( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + package_id: ObjectID, + ) -> Result; + + fn create_and_execute_send( + identity: OwnedObjectRef, + capability: ObjectRef, + transfer_map: Vec<(ObjectID, IotaAddress)>, + expiration: Option, + objects: Vec<(ObjectRef, TypeTag)>, + package: ObjectID, + ) -> anyhow::Result; + + fn propose_upgrade( + identity: OwnedObjectRef, + capability: ObjectRef, + expiration: Option, + package_id: ObjectID, + ) -> Result; + + fn execute_upgrade( + identity: OwnedObjectRef, + capability: ObjectRef, + proposal_id: ObjectID, + package_id: ObjectID, + ) -> Result; +} diff --git a/identity_iota_interaction/src/move_type.rs b/identity_iota_interaction/src/move_type.rs new file mode 100644 index 0000000000..ac49e6ee40 --- /dev/null +++ b/identity_iota_interaction/src/move_type.rs @@ -0,0 +1,75 @@ +// Copyright 2020-2025 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::base_types::IotaAddress; +use crate::types::base_types::ObjectID; +use crate::types::TypeTag; +use crate::IotaVerifiableCredential; +use serde::Serialize; + +pub enum TypedValue<'a, T: MoveType> { + IotaVerifiableCredential(&'a IotaVerifiableCredential), + Other(&'a T), +} + +/// Trait for types that can be converted to a Move type. +pub trait MoveType: Serialize { + /// Returns the Move type for this type. + fn move_type(package: ObjectID) -> TypeTag; + + fn get_typed_value(&self, _package: ObjectID) -> TypedValue + where + Self: MoveType, + Self: Sized, + { + TypedValue::Other(self) + } +} + +impl MoveType for u8 { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::U8 + } +} + +impl MoveType for u16 { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::U16 + } +} + +impl MoveType for u32 { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::U32 + } +} + +impl MoveType for u64 { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::U64 + } +} + +impl MoveType for u128 { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::U128 + } +} + +impl MoveType for bool { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::Bool + } +} + +impl MoveType for IotaAddress { + fn move_type(_package: ObjectID) -> TypeTag { + TypeTag::Address + } +} + +impl MoveType for Vec { + fn move_type(package: ObjectID) -> TypeTag { + TypeTag::Vector(Box::new(T::move_type(package))) + } +} diff --git a/identity_iota_interaction/src/sdk_types/error.rs b/identity_iota_interaction/src/sdk_types/error.rs new file mode 100644 index 0000000000..1448df9a33 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/error.rs @@ -0,0 +1,37 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use super::iota_types::base_types::{IotaAddress, TransactionDigest}; +use thiserror::Error; + +//pub use crate::json_rpc_error::Error as JsonRpcError; + +pub type IotaRpcResult = Result; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + Rpc(#[from] jsonrpsee::core::ClientError), + #[error(transparent)] + BcsSerialization(#[from] bcs::Error), + #[error("Subscription error: {0}")] + Subscription(String), + #[error("Failed to confirm tx status for {0:?} within {1} seconds.")] + FailToConfirmTransactionStatus(TransactionDigest, u64), + #[error("Data error: {0}")] + Data(String), + #[error( + "Client/Server api version mismatch, client api version: {client_version}, server api version: {server_version}" + )] + ServerVersionMismatch { + client_version: String, + server_version: String, + }, + #[error("Insufficient funds for address [{address}], requested amount: {amount}")] + InsufficientFunds { address: IotaAddress, amount: u128 }, + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error("Error caused by a foreign function interface call: {0}")] + FfiError(String), // Added for IOTA interaction +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/generated_types.rs b/identity_iota_interaction/src/sdk_types/generated_types.rs new file mode 100644 index 0000000000..2478fa4768 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/generated_types.rs @@ -0,0 +1,265 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use fastcrypto::encoding::Base64; +use serde::Deserialize; +use serde::Serialize; + +use super::iota_json_rpc_types::iota_transaction::IotaTransactionBlockResponseOptions; +use super::iota_types::quorum_driver_types::ExecuteTransactionRequestType; + +use crate::rpc_types::EventFilter; +use crate::rpc_types::IotaObjectDataFilter; +use crate::rpc_types::IotaObjectDataOptions; +use crate::types::dynamic_field::DynamicFieldName; +use crate::types::event::EventID; +use crate::types::iota_serde::SequenceNumber; +use crate::SignatureBcs; +use crate::TransactionDataBcs; + +// The types defined in this file: +// * do not exist in the iota rust sdk +// * have an equivalent type in the iota typescript sdk +// * are needed for wasm-bindings +// * have been generated by @iota/sdk/typescript/scripts/generate.ts +// +// As there is no equivalent rust type in the iota rust sdk, we need to +// define equivalent rust types here. + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExecuteTransactionBlockParams { + /// BCS serialized transaction data bytes without its type tag, as base-64 encoded string. + transaction_block: Base64, + /// A list of signatures (`flag || signature || pubkey` bytes, as base-64 encoded string). Signature is committed to + /// the intent message of the transaction data, as base-64 encoded string. + signature: Vec, + /// options for specifying the content to be returned + options: Option, + /// The request type, derived from `IotaTransactionBlockResponseOptions` if None + request_type: Option, +} + +impl ExecuteTransactionBlockParams { + pub fn new( + tx_bytes: &TransactionDataBcs, + signatures: &[SignatureBcs], + options: Option, + request_type: Option, + ) -> Self { + ExecuteTransactionBlockParams { + transaction_block: Base64::from_bytes(&tx_bytes), + signature: signatures.into_iter().map(|sig| Base64::from_bytes(&sig)).collect(), + options, + request_type, + } + } +} + +/// Return the dynamic field object information for a specified object +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetDynamicFieldObjectParams { + /// The ID of the queried parent object + parent_id: String, + /// The Name of the dynamic field + name: DynamicFieldName, +} + +impl GetDynamicFieldObjectParams { + pub fn new(parent_id: String, name: DynamicFieldName) -> Self { + GetDynamicFieldObjectParams { parent_id, name } + } +} + +/// Return the object information for a specified object +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetObjectParams { + /// the ID of the queried object + id: String, + /// options for specifying the content to be returned + options: Option, +} + +impl GetObjectParams { + pub fn new(id: String, options: Option) -> Self { + GetObjectParams { id, options } + } +} + +/// Return the list of objects owned by an address. Note that if the address owns more than +/// `QUERY_MAX_RESULT_LIMIT` objects, the pagination is not accurate, because previous page may have +/// been updated when the next page is fetched. Please use iotax_queryObjects if this is a concern. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetOwnedObjectsParams { + /// the owner's Iota address + owner: String, + /// An optional paging cursor. If provided, the query will start from the next item after the specified + /// cursor. Default to start from the first item if not specified. + cursor: Option, + /// Max number of items returned per page, default to [QUERY_MAX_RESULT_LIMIT] if not specified. + limit: Option, + /// If None, no filter will be applied + filter: Option, + /// config which fields to include in the response, by default only digest is included + options: Option, +} + +impl GetOwnedObjectsParams { + pub fn new( + owner: String, + cursor: Option, + limit: Option, + filter: Option, + options: Option, + ) -> Self { + GetOwnedObjectsParams { + owner, + cursor, + limit, + filter, + options, + } + } +} + +/// Return the transaction response object. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetTransactionBlockParams { + /// the digest of the queried transaction + digest: String, + /// options for specifying the content to be returned + #[serde(skip_serializing_if = "Option::is_none")] + options: Option, +} + +impl GetTransactionBlockParams { + pub fn new(digest: String, options: Option) -> Self { + GetTransactionBlockParams { digest, options } + } +} + +/// Note there is no software-level guarantee/SLA that objects with past versions can be retrieved by +/// this API, even if the object and version exists/existed. The result may vary across nodes depending +/// on their pruning policies. Return the object information for a specified version +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TryGetPastObjectParams { + /// the ID of the queried object + id: String, + /// the version of the queried object. If None, default to the latest known version + version: SequenceNumber, + //// options for specifying the content to be returned + options: Option, +} + +impl TryGetPastObjectParams { + pub fn new(id: String, version: SequenceNumber, options: Option) -> Self { + TryGetPastObjectParams { id, version, options } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum SortOrder { + Ascending, + Descending, +} + +impl SortOrder { + pub fn new(descending_order: bool) -> Self { + return if descending_order { + SortOrder::Descending + } else { + SortOrder::Ascending + }; + } +} + +/// Return list of events for a specified query criteria. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct QueryEventsParams { + /// The event query criteria. See [Event filter](https://docs.iota.io/build/event_api#event-filters) + /// documentation for examples. + query: EventFilter, + /// optional paging cursor + cursor: Option, + /// maximum number of items per page, default to [QUERY_MAX_RESULT_LIMIT] if not specified. + limit: Option, + /// query result ordering, default to false (ascending order), oldest record first. + order: Option, +} + +impl QueryEventsParams { + pub fn new(query: EventFilter, cursor: Option, limit: Option, order: Option) -> Self { + QueryEventsParams { + query, + cursor, + limit, + order, + } + } +} + +/// Return all Coin<`coin_type`> objects owned by an address. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetCoinsParams { + /// the owner's Iota address + owner: String, + /// optional type name for the coin (e.g., 0x168da5bf1f48dafc111b0a488fa454aca95e0b5e::usdc::USDC), + /// default to 0x2::iota::IOTA if not specified. + coin_type: Option, + /// optional paging cursor + cursor: Option, + /// maximum number of items per page + limit: Option, +} + +impl GetCoinsParams { + pub fn new(owner: String, coin_type: Option, cursor: Option, limit: Option) -> Self { + GetCoinsParams { + owner, + coin_type, + cursor, + limit, + } + } +} + +/// Params for `wait_for_transaction` / `wait_for_transaction`. +/// +/// Be careful when serializing with `serde_wasm_bindgen::to_value`, as `#[serde(flatten)]` +/// will turn the object into a `Map` instead of a plain object in Js. +/// Prefer serializing with `serde_wasm_bindgen::Serializer::json_compatible` or perform custom serialization. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WaitForTransactionParams { + /// Block digest and options for content that should be returned. + #[serde(flatten)] + get_transaction_block_params: GetTransactionBlockParams, + /// The amount of time to wait for a transaction block. Defaults to one minute. + #[serde(skip_serializing_if = "Option::is_none")] + timeout: Option, + /// The amount of time to wait between checks for the transaction block. Defaults to 2 seconds. + #[serde(skip_serializing_if = "Option::is_none")] + poll_interval: Option, +} + +impl WaitForTransactionParams { + pub fn new( + digest: String, + options: Option, + timeout: Option, + poll_interval: Option, + ) -> Self { + WaitForTransactionParams { + get_transaction_block_params: GetTransactionBlockParams::new(digest, options), + timeout, + poll_interval, + } + } +} diff --git a/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_coin.rs b/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_coin.rs new file mode 100644 index 0000000000..818dbb4fd7 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_coin.rs @@ -0,0 +1,36 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as}; + +use super::super::iota_types::{ + base_types::{ObjectID, ObjectRef, TransactionDigest, SequenceNumber}, + digests::ObjectDigest, + iota_serde::{BigInt, SequenceNumber as AsSequenceNumber} +}; + +use super::Page; + +pub type CoinPage = Page; + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Coin { + pub coin_type: String, + pub coin_object_id: ObjectID, + #[serde_as(as = "AsSequenceNumber")] + pub version: SequenceNumber, + pub digest: ObjectDigest, + #[serde_as(as = "BigInt")] + pub balance: u64, + pub previous_transaction: TransactionDigest, +} + +impl Coin { + pub fn object_ref(&self) -> ObjectRef { + (self.coin_object_id, self.version, self.digest) + } +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_event.rs b/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_event.rs new file mode 100644 index 0000000000..f4cc209c86 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_event.rs @@ -0,0 +1,120 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; +use serde_json::Value; + +use fastcrypto::encoding::Base58; + +use super::super::iota_types::{ + base_types::{ObjectID, IotaAddress, TransactionDigest}, + event::EventID, + iota_serde::{BigInt, IotaStructTag} +}; +use super::super::move_core_types::{ + identifier::Identifier, + language_storage::{StructTag}, +}; + +use super::{Page}; + +pub type EventPage = Page; + +#[serde_as] +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] +#[serde(rename = "Event", rename_all = "camelCase")] +pub struct IotaEvent { + /// Sequential event ID, ie (transaction seq number, event seq number). + /// 1) Serves as a unique event ID for each fullnode + /// 2) Also serves to sequence events for the purposes of pagination and + /// querying. A higher id is an event seen later by that fullnode. + /// This ID is the "cursor" for event querying. + pub id: EventID, + /// Move package where this event was emitted. + pub package_id: ObjectID, + #[serde_as(as = "DisplayFromStr")] + /// Move module where this event was emitted. + pub transaction_module: Identifier, + /// Sender's IOTA address. + pub sender: IotaAddress, + #[serde_as(as = "IotaStructTag")] + /// Move event type. + pub type_: StructTag, + /// Parsed json value of the event + pub parsed_json: Value, + #[serde_as(as = "Base58")] + /// Base 58 encoded bcs bytes of the move event + pub bcs: Vec, + /// UTC timestamp in milliseconds since epoch (1/1/1970) + #[serde(skip_serializing_if = "Option::is_none")] + #[serde_as(as = "Option>")] + pub timestamp_ms: Option, +} + +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum EventFilter { + /// Query by sender address. + Sender(IotaAddress), + /// Return events emitted by the given transaction. + Transaction( + /// digest of the transaction, as base-64 encoded string + TransactionDigest, + ), + /// Return events emitted in a specified Package. + Package(ObjectID), + /// Return events emitted in a specified Move module. + /// If the event is defined in Module A but emitted in a tx with Module B, + /// query `MoveModule` by module B returns the event. + /// Query `MoveEventModule` by module A returns the event too. + MoveModule { + /// the Move package ID + package: ObjectID, + /// the module name + #[serde_as(as = "DisplayFromStr")] + module: Identifier, + }, + /// Return events with the given Move event struct name (struct tag). + /// For example, if the event is defined in `0xabcd::MyModule`, and named + /// `Foo`, then the struct tag is `0xabcd::MyModule::Foo`. + MoveEventType( + #[serde_as(as = "IotaStructTag")] + StructTag, + ), + /// Return events with the given Move module name where the event struct is + /// defined. If the event is defined in Module A but emitted in a tx + /// with Module B, query `MoveEventModule` by module A returns the + /// event. Query `MoveModule` by module B returns the event too. + MoveEventModule { + /// the Move package ID + package: ObjectID, + /// the module name + #[serde_as(as = "DisplayFromStr")] + module: Identifier, + }, + MoveEventField { + path: String, + value: Value, + }, + /// Return events emitted in [start_time, end_time] interval + #[serde(rename_all = "camelCase")] + TimeRange { + /// left endpoint of time interval, milliseconds since epoch, inclusive + #[serde_as(as = "BigInt")] + start_time: u64, + /// right endpoint of time interval, milliseconds since epoch, exclusive + #[serde_as(as = "BigInt")] + end_time: u64, + }, + + All(Vec), + Any(Vec), + And(Box, Box), + Or(Box, Box), +} + +pub trait Filter { + fn matches(&self, item: &T) -> bool; +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_move.rs b/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_move.rs new file mode 100644 index 0000000000..fa5e6b9969 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_move.rs @@ -0,0 +1,346 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::BTreeMap; +use std::boxed::Box; +use std::fmt::{self, Display, Formatter, Write}; + +use itertools::Itertools; + +use serde::Deserialize; +use serde::Serialize; +use serde_with::{serde_as}; +use serde_json::{json, Value}; + +use tracing::warn; + +use crate::types::{ + base_types::{IotaAddress, ObjectID}, + iota_serde::IotaStructTag, +}; + +use super::super::move_core_types::{ + language_storage::StructTag, + annotated_value::{MoveStruct, MoveValue, MoveVariant}, + identifier::Identifier, +}; + +#[serde_as] +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +#[serde(untagged, rename = "MoveValue")] +pub enum IotaMoveValue { + // u64 and u128 are converted to String to avoid overflow + Number(u32), + Bool(bool), + Address(IotaAddress), + Vector(Vec), + String(String), + UID { id: ObjectID }, + Struct(IotaMoveStruct), + Option(Box>), + Variant(IotaMoveVariant), +} + +impl IotaMoveValue { + /// Extract values from MoveValue without type information in json format + pub fn to_json_value(self) -> Value { + match self { + IotaMoveValue::Struct(move_struct) => move_struct.to_json_value(), + IotaMoveValue::Vector(values) => IotaMoveStruct::Runtime(values).to_json_value(), + IotaMoveValue::Number(v) => json!(v), + IotaMoveValue::Bool(v) => json!(v), + IotaMoveValue::Address(v) => json!(v), + IotaMoveValue::String(v) => json!(v), + IotaMoveValue::UID { id } => json!({ "id": id }), + IotaMoveValue::Option(v) => json!(v), + IotaMoveValue::Variant(v) => v.to_json_value(), + } + } +} + +impl Display for IotaMoveValue { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut writer = String::new(); + match self { + IotaMoveValue::Number(value) => write!(writer, "{value}")?, + IotaMoveValue::Bool(value) => write!(writer, "{value}")?, + IotaMoveValue::Address(value) => write!(writer, "{value}")?, + IotaMoveValue::String(value) => write!(writer, "{value}")?, + IotaMoveValue::UID { id } => write!(writer, "{id}")?, + IotaMoveValue::Struct(value) => write!(writer, "{value}")?, + IotaMoveValue::Option(value) => write!(writer, "{value:?}")?, + IotaMoveValue::Vector(vec) => { + write!( + writer, + "{}", + vec.iter().map(|value| format!("{value}")).join(",\n") + )?; + } + IotaMoveValue::Variant(value) => write!(writer, "{value}")?, + } + write!(f, "{}", writer.trim_end_matches('\n')) + } +} + +impl From for IotaMoveValue { + fn from(value: MoveValue) -> Self { + match value { + MoveValue::U8(value) => IotaMoveValue::Number(value.into()), + MoveValue::U16(value) => IotaMoveValue::Number(value.into()), + MoveValue::U32(value) => IotaMoveValue::Number(value), + MoveValue::U64(value) => IotaMoveValue::String(format!("{value}")), + MoveValue::U128(value) => IotaMoveValue::String(format!("{value}")), + MoveValue::U256(value) => IotaMoveValue::String(format!("{value}")), + MoveValue::Bool(value) => IotaMoveValue::Bool(value), + MoveValue::Vector(values) => { + IotaMoveValue::Vector(values.into_iter().map(|value| value.into()).collect()) + } + MoveValue::Struct(value) => { + // Best effort IOTA core type conversion + let MoveStruct { type_, fields } = &value; + if let Some(value) = try_convert_type(type_, fields) { + return value; + } + IotaMoveValue::Struct(value.into()) + } + MoveValue::Signer(value) | MoveValue::Address(value) => { + IotaMoveValue::Address(IotaAddress::from(ObjectID::from(value))) + } + MoveValue::Variant(MoveVariant { + type_, + variant_name, + tag: _, + fields, + }) => IotaMoveValue::Variant(IotaMoveVariant { + type_: type_.clone(), + variant: variant_name.to_string(), + fields: fields + .into_iter() + .map(|(id, value)| (id.into_string(), value.into())) + .collect::>(), + }), + } + } +} + +fn to_bytearray(value: &[MoveValue]) -> Option> { + if value.iter().all(|value| matches!(value, MoveValue::U8(_))) { + let bytearray = value + .iter() + .flat_map(|value| { + if let MoveValue::U8(u8) = value { + Some(*u8) + } else { + None + } + }) + .collect::>(); + Some(bytearray) + } else { + None + } +} + +#[serde_as] +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +#[serde(rename = "MoveVariant")] +pub struct IotaMoveVariant { + #[serde(rename = "type")] + #[serde_as(as = "IotaStructTag")] + pub type_: StructTag, + pub variant: String, + pub fields: BTreeMap, +} + +impl IotaMoveVariant { + pub fn to_json_value(self) -> Value { + // We only care about values here, assuming type information is known at the + // client side. + let fields = self + .fields + .into_iter() + .map(|(key, value)| (key, value.to_json_value())) + .collect::>(); + json!({ + "variant": self.variant, + "fields": fields, + }) + } +} + +impl Display for IotaMoveVariant { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut writer = String::new(); + let IotaMoveVariant { + type_, + variant, + fields, + } = self; + writeln!(writer)?; + writeln!(writer, " {}: {type_}", "type")?; + writeln!(writer, " {}: {variant}", "variant")?; + for (name, value) in fields { + let value = format!("{}", value); + let value = if value.starts_with('\n') { + indent(&value, 2) + } else { + value + }; + writeln!(writer, " {}: {value}", name)?; + } + + write!(f, "{}", writer.trim_end_matches('\n')) + } +} + +#[serde_as] +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +#[serde(untagged, rename = "MoveStruct")] +pub enum IotaMoveStruct { + Runtime(Vec), + WithTypes { + #[serde(rename = "type")] + #[serde_as(as = "IotaStructTag")] + type_: StructTag, + fields: BTreeMap, + }, + WithFields(BTreeMap), +} + +impl IotaMoveStruct { + /// Extract values from MoveStruct without type information in json format + pub fn to_json_value(self) -> Value { + // Unwrap MoveStructs + match self { + IotaMoveStruct::Runtime(values) => { + let values = values + .into_iter() + .map(|value| value.to_json_value()) + .collect::>(); + json!(values) + } + // We only care about values here, assuming struct type information is known at the + // client side. + IotaMoveStruct::WithTypes { type_: _, fields } | IotaMoveStruct::WithFields(fields) => { + let fields = fields + .into_iter() + .map(|(key, value)| (key, value.to_json_value())) + .collect::>(); + json!(fields) + } + } + } + + pub fn read_dynamic_field_value(&self, field_name: &str) -> Option { + match self { + IotaMoveStruct::WithFields(fields) => fields.get(field_name).cloned(), + IotaMoveStruct::WithTypes { type_: _, fields } => fields.get(field_name).cloned(), + _ => None, + } + } +} + +impl Display for IotaMoveStruct { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut writer = String::new(); + match self { + IotaMoveStruct::Runtime(_) => {} + IotaMoveStruct::WithFields(fields) => { + for (name, value) in fields { + writeln!(writer, "{}: {value}", name)?; + } + } + IotaMoveStruct::WithTypes { type_, fields } => { + writeln!(writer)?; + writeln!(writer, " {}: {type_}", "type")?; + for (name, value) in fields { + let value = format!("{value}"); + let value = if value.starts_with('\n') { + indent(&value, 2) + } else { + value + }; + writeln!(writer, " {}: {value}", name)?; + } + } + } + write!(f, "{}", writer.trim_end_matches('\n')) + } +} + +fn indent(d: &T, indent: usize) -> String { + d.to_string() + .lines() + .map(|line| format!("{:indent$}{line}", "")) + .join("\n") +} + +fn try_convert_type( + type_: &StructTag, + fields: &[(Identifier, MoveValue)], +) -> Option { + let struct_name = format!( + "0x{}::{}::{}", + type_.address.short_str_lossless(), + type_.module, + type_.name + ); + let mut values = fields + .iter() + .map(|(id, value)| (id.to_string(), value)) + .collect::>(); + match struct_name.as_str() { + "0x1::string::String" | "0x1::ascii::String" => { + if let Some(MoveValue::Vector(bytes)) = values.remove("bytes") { + return to_bytearray(bytes) + .and_then(|bytes| String::from_utf8(bytes).ok()) + .map(IotaMoveValue::String); + } + } + "0x2::url::Url" => { + return values.remove("url").cloned().map(IotaMoveValue::from); + } + "0x2::object::ID" => { + return values.remove("bytes").cloned().map(IotaMoveValue::from); + } + "0x2::object::UID" => { + let id = values.remove("id").cloned().map(IotaMoveValue::from); + if let Some(IotaMoveValue::Address(address)) = id { + return Some(IotaMoveValue::UID { + id: ObjectID::from(address), + }); + } + } + "0x2::balance::Balance" => { + return values.remove("value").cloned().map(IotaMoveValue::from); + } + "0x1::option::Option" => { + if let Some(MoveValue::Vector(values)) = values.remove("vec") { + return Some(IotaMoveValue::Option(Box::new( + // in Move option is modeled as vec of 1 element + values.first().cloned().map(IotaMoveValue::from), + ))); + } + } + _ => return None, + } + warn!( + fields =? fields, + "Failed to convert {struct_name} to IotaMoveValue" + ); + None +} + +impl From for IotaMoveStruct { + fn from(move_struct: MoveStruct) -> Self { + IotaMoveStruct::WithTypes { + type_: move_struct.type_, + fields: move_struct + .fields + .into_iter() + .map(|(id, value)| (id.into_string(), value.into())) + .collect(), + } + } +} diff --git a/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_object.rs b/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_object.rs new file mode 100644 index 0000000000..b55488221b --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_object.rs @@ -0,0 +1,807 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::BTreeMap; +use std::string::String; +use std::fmt::{self, Display, Formatter, Write}; +use std::cmp::Ordering; + +use serde::Deserialize; +use serde::Serialize; +use serde_with::{DisplayFromStr, serde_as}; +use serde_json::Value; + +use fastcrypto::encoding::{Base64}; + +use anyhow::anyhow; + +use crate::move_core_types::{ + identifier::Identifier, + language_storage::StructTag +}; +use crate::types::{ + base_types::{ObjectID, SequenceNumber, ObjectType, ObjectRef, ObjectInfo, IotaAddress}, + move_package::{TypeOrigin, UpgradeInfo, MovePackage}, + iota_serde::{IotaStructTag, BigInt, SequenceNumber as AsSequenceNumber}, + digests::{ObjectDigest,TransactionDigest}, + object::Owner, + error::{IotaObjectResponseError, UserInputResult, UserInputError}, + gas_coin::GasCoin, +}; + +use super::{ + Page, + iota_move::{IotaMoveStruct, IotaMoveValue}, +}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct IotaObjectResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl IotaObjectResponse { + pub fn new(data: Option, error: Option) -> Self { + Self { data, error } + } + + pub fn new_with_data(data: IotaObjectData) -> Self { + Self { + data: Some(data), + error: None, + } + } + + pub fn new_with_error(error: IotaObjectResponseError) -> Self { + Self { + data: None, + error: Some(error), + } + } +} + +impl Ord for IotaObjectResponse { + fn cmp(&self, other: &Self) -> Ordering { + match (&self.data, &other.data) { + (Some(data), Some(data_2)) => { + if data.object_id.cmp(&data_2.object_id).eq(&Ordering::Greater) { + return Ordering::Greater; + } else if data.object_id.cmp(&data_2.object_id).eq(&Ordering::Less) { + return Ordering::Less; + } + Ordering::Equal + } + // In this ordering those with data will come before IotaObjectResponses that are + // errors. + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + // IotaObjectResponses that are errors are just considered equal. + _ => Ordering::Equal, + } + } +} + +impl PartialOrd for IotaObjectResponse { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl IotaObjectResponse { + pub fn move_object_bcs(&self) -> Option<&Vec> { + match &self.data { + Some(IotaObjectData { + bcs: Some(IotaRawData::MoveObject(obj)), + .. + }) => Some(&obj.bcs_bytes), + _ => None, + } + } + + pub fn owner(&self) -> Option { + if let Some(data) = &self.data { + return data.owner; + } + None + } + + pub fn object_id(&self) -> Result { + match (&self.data, &self.error) { + (Some(obj_data), None) => Ok(obj_data.object_id), + (None, Some(IotaObjectResponseError::NotExists { object_id })) => Ok(*object_id), + ( + None, + Some(IotaObjectResponseError::Deleted { + object_id, + version: _, + digest: _, + }), + ) => Ok(*object_id), + _ => Err(anyhow!( + "Could not get object_id, something went wrong with IotaObjectResponse construction." + )), + } + } + + pub fn object_ref_if_exists(&self) -> Option { + match (&self.data, &self.error) { + (Some(obj_data), None) => Some(obj_data.object_ref()), + _ => None, + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] +pub struct DisplayFieldsResponse { + pub data: Option>, + pub error: Option, +} + +#[serde_as] +#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase", rename = "ObjectData")] +pub struct IotaObjectData { + pub object_id: ObjectID, + /// Object version. + #[serde_as(as = "AsSequenceNumber")] + pub version: SequenceNumber, + /// Base64 string representing the object digest + pub digest: ObjectDigest, + /// The type of the object. Default to be None unless + /// IotaObjectDataOptions.showType is set to true + #[serde_as(as = "Option")] + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub type_: Option, + // Default to be None because otherwise it will be repeated for the getOwnedObjects endpoint + /// The owner of this object. Default to be None unless + /// IotaObjectDataOptions.showOwner is set to true + #[serde(skip_serializing_if = "Option::is_none")] + pub owner: Option, + /// The digest of the transaction that created or last mutated this object. + /// Default to be None unless IotaObjectDataOptions. + /// showPreviousTransaction is set to true + #[serde(skip_serializing_if = "Option::is_none")] + pub previous_transaction: Option, + /// The amount of IOTA we would rebate if this object gets deleted. + /// This number is re-calculated each time the object is mutated based on + /// the present storage gas price. + #[serde_as(as = "Option>")] + #[serde(skip_serializing_if = "Option::is_none")] + pub storage_rebate: Option, + /// The Display metadata for frontend UI rendering, default to be None + /// unless IotaObjectDataOptions.showContent is set to true This can also + /// be None if the struct type does not have Display defined + #[serde(skip_serializing_if = "Option::is_none")] + pub display: Option, + /// Move object content or package content, default to be None unless + /// IotaObjectDataOptions.showContent is set to true + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + /// Move object content or package content in BCS, default to be None unless + /// IotaObjectDataOptions.showBcs is set to true + #[serde(skip_serializing_if = "Option::is_none")] + pub bcs: Option, +} + +impl IotaObjectData { + pub fn object_ref(&self) -> ObjectRef { + (self.object_id, self.version, self.digest) + } + + pub fn object_type(&self) -> anyhow::Result { + self.type_ + .as_ref() + .ok_or_else(|| anyhow!("type is missing for object {:?}", self.object_id)) + .cloned() + } + + pub fn is_gas_coin(&self) -> bool { + match self.type_.as_ref() { + Some(ObjectType::Struct(ty)) if ty.is_gas_coin() => true, + Some(_) => false, + None => false, + } + } +} + +impl Display for IotaObjectData { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let type_ = if let Some(type_) = &self.type_ { + type_.to_string() + } else { + "Unknown Type".into() + }; + let mut writer = String::new(); + writeln!( + writer, + "{}", + format!("----- {type_} ({}[{}]) -----", self.object_id, self.version) + )?; + if let Some(owner) = self.owner { + writeln!(writer, "{}: {owner}", "Owner")?; + } + + writeln!( + writer, + "{}: {}", + "Version", + self.version + )?; + if let Some(storage_rebate) = self.storage_rebate { + writeln!( + writer, + "{}: {storage_rebate}", + "Storage Rebate", + )?; + } + + if let Some(previous_transaction) = self.previous_transaction { + writeln!( + writer, + "{}: {previous_transaction:?}", + "Previous Transaction", + )?; + } + if let Some(content) = self.content.as_ref() { + writeln!(writer, "{}", "----- Data -----")?; + write!(writer, "{content}")?; + } + + write!(f, "{writer}") + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)] +#[serde(rename_all = "camelCase", rename = "ObjectDataOptions", default)] +pub struct IotaObjectDataOptions { + /// Whether to show the type of the object. Default to be False + pub show_type: bool, + /// Whether to show the owner of the object. Default to be False + pub show_owner: bool, + /// Whether to show the previous transaction digest of the object. Default + /// to be False + pub show_previous_transaction: bool, + /// Whether to show the Display metadata of the object for frontend + /// rendering. Default to be False + pub show_display: bool, + /// Whether to show the content(i.e., package content or Move struct + /// content) of the object. Default to be False + pub show_content: bool, + /// Whether to show the content in BCS format. Default to be False + pub show_bcs: bool, + /// Whether to show the storage rebate of the object. Default to be False + pub show_storage_rebate: bool, +} + +impl IotaObjectDataOptions { + pub fn new() -> Self { + Self::default() + } + + /// return BCS data and all other metadata such as storage rebate + pub fn bcs_lossless() -> Self { + Self { + show_bcs: true, + show_type: true, + show_owner: true, + show_previous_transaction: true, + show_display: false, + show_content: false, + show_storage_rebate: true, + } + } + + /// return full content except bcs + pub fn full_content() -> Self { + Self { + show_bcs: false, + show_type: true, + show_owner: true, + show_previous_transaction: true, + show_display: false, + show_content: true, + show_storage_rebate: true, + } + } + + pub fn with_content(mut self) -> Self { + self.show_content = true; + self + } + + pub fn with_owner(mut self) -> Self { + self.show_owner = true; + self + } + + pub fn with_type(mut self) -> Self { + self.show_type = true; + self + } + + pub fn with_display(mut self) -> Self { + self.show_display = true; + self + } + + pub fn with_bcs(mut self) -> Self { + self.show_bcs = true; + self + } + + pub fn with_previous_transaction(mut self) -> Self { + self.show_previous_transaction = true; + self + } + + pub fn is_not_in_object_info(&self) -> bool { + self.show_bcs || self.show_content || self.show_display || self.show_storage_rebate + } +} + +impl IotaObjectResponse { + /// Returns a reference to the object if there is any, otherwise an Err if + /// the object does not exist or is deleted. + pub fn object(&self) -> Result<&IotaObjectData, IotaObjectResponseError> { + if let Some(data) = &self.data { + Ok(data) + } else if let Some(error) = &self.error { + Err(error.clone()) + } else { + // We really shouldn't reach this code block since either data, or error field + // should always be filled. + Err(IotaObjectResponseError::Unknown) + } + } + + /// Returns the object value if there is any, otherwise an Err if + /// the object does not exist or is deleted. + pub fn into_object(self) -> Result { + match self.object() { + Ok(data) => Ok(data.clone()), + Err(error) => Err(error), + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Ord, PartialOrd)] +#[serde(rename_all = "camelCase", rename = "ObjectRef")] +pub struct IotaObjectRef { + /// Hex code as string representing the object id + pub object_id: ObjectID, + /// Object version. + pub version: SequenceNumber, + /// Base64 string representing the object digest + pub digest: ObjectDigest, +} + +impl IotaObjectRef { + pub fn to_object_ref(&self) -> ObjectRef { + (self.object_id, self.version, self.digest) + } +} + +impl Display for IotaObjectRef { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "Object ID: {}, version: {}, digest: {}", + self.object_id, self.version, self.digest + ) + } +} + +impl From for IotaObjectRef { + fn from(oref: ObjectRef) -> Self { + Self { + object_id: oref.0, + version: oref.1, + digest: oref.2, + } + } +} + +pub trait IotaData: Sized { + type ObjectType; + type PackageType; + // Code is commented out because MoveObject and MoveStructLayout + // introduce too many dependencies + // fn try_from_object(object: MoveObject, layout: MoveStructLayout) + // -> Result; + // fn try_from_package(package: MovePackage) -> Result; + fn try_as_move(&self) -> Option<&Self::ObjectType>; + fn try_into_move(self) -> Option; + fn try_as_package(&self) -> Option<&Self::PackageType>; + fn type_(&self) -> Option<&StructTag>; +} + +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +#[serde(tag = "dataType", rename_all = "camelCase", rename = "RawData")] +pub enum IotaRawData { + // Manually handle generic schema generation + MoveObject(IotaRawMoveObject), + Package(IotaRawMovePackage), +} + +impl IotaData for IotaRawData { + type ObjectType = IotaRawMoveObject; + type PackageType = IotaRawMovePackage; + + // try_from_object() and try_from_package() are not defined here because + // MoveObject and MoveStructLayout introduce too many dependencies + + fn try_as_move(&self) -> Option<&Self::ObjectType> { + match self { + Self::MoveObject(o) => Some(o), + Self::Package(_) => None, + } + } + + fn try_into_move(self) -> Option { + match self { + Self::MoveObject(o) => Some(o), + Self::Package(_) => None, + } + } + + fn try_as_package(&self) -> Option<&Self::PackageType> { + match self { + Self::MoveObject(_) => None, + Self::Package(p) => Some(p), + } + } + + fn type_(&self) -> Option<&StructTag> { + match self { + Self::MoveObject(o) => Some(&o.type_), + Self::Package(_) => None, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +#[serde(tag = "dataType", rename_all = "camelCase", rename = "Data")] +pub enum IotaParsedData { + // Manually handle generic schema generation + MoveObject(IotaParsedMoveObject), + Package(IotaMovePackage), +} + +impl IotaData for IotaParsedData { + type ObjectType = IotaParsedMoveObject; + type PackageType = IotaMovePackage; + + // try_from_object() and try_from_package() are not defined here because + // MoveObject and MoveStructLayout introduce too many dependencies + + fn try_as_move(&self) -> Option<&Self::ObjectType> { + match self { + Self::MoveObject(o) => Some(o), + Self::Package(_) => None, + } + } + + fn try_into_move(self) -> Option { + match self { + Self::MoveObject(o) => Some(o), + Self::Package(_) => None, + } + } + + fn try_as_package(&self) -> Option<&Self::PackageType> { + match self { + Self::MoveObject(_) => None, + Self::Package(p) => Some(p), + } + } + + fn type_(&self) -> Option<&StructTag> { + match self { + Self::MoveObject(o) => Some(&o.type_), + Self::Package(_) => None, + } + } +} + +impl Display for IotaParsedData { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut writer = String::new(); + match self { + IotaParsedData::MoveObject(o) => { + writeln!(writer, "{}: {}", "type", o.type_)?; + write!(writer, "{}", &o.fields)?; + } + IotaParsedData::Package(p) => { + write!( + writer, + "{}: {:?}", + "Modules", + p.disassembled.keys() + )?; + } + } + write!(f, "{writer}") + } +} + +#[serde_as] +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +#[serde(rename = "MoveObject", rename_all = "camelCase")] +pub struct IotaParsedMoveObject { + #[serde(rename = "type")] + #[serde_as(as = "IotaStructTag")] + pub type_: StructTag, + pub fields: IotaMoveStruct, +} + +impl IotaParsedMoveObject { + // try_from_object_read()is not defined here because + // MoveObject introduces too many dependencies + + pub fn read_dynamic_field_value(&self, field_name: &str) -> Option { + match &self.fields { + IotaMoveStruct::WithFields(fields) => fields.get(field_name).cloned(), + IotaMoveStruct::WithTypes { fields, .. } => fields.get(field_name).cloned(), + _ => None, + } + } +} + +#[serde_as] +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +#[serde(rename = "RawMoveObject", rename_all = "camelCase")] +pub struct IotaRawMoveObject { + #[serde(rename = "type")] + #[serde_as(as = "IotaStructTag")] + pub type_: StructTag, + pub version: SequenceNumber, + #[serde_as(as = "Base64")] + pub bcs_bytes: Vec, +} + +impl IotaRawMoveObject { + pub fn deserialize<'a, T: Deserialize<'a>>(&'a self) -> Result { + Ok(bcs::from_bytes(self.bcs_bytes.as_slice())?) + } +} + +#[serde_as] +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +#[serde(rename = "RawMovePackage", rename_all = "camelCase")] +pub struct IotaRawMovePackage { + pub id: ObjectID, + pub version: SequenceNumber, + #[serde_as(as = "BTreeMap<_, Base64>")] + pub module_map: BTreeMap>, + pub type_origin_table: Vec, + pub linkage_table: BTreeMap, +} + +impl From for IotaRawMovePackage { + fn from(p: MovePackage) -> Self { + Self { + id: p.id(), + version: p.version(), + module_map: p.serialized_module_map().clone(), + type_origin_table: p.type_origin_table().clone(), + linkage_table: p.linkage_table().clone(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(tag = "status", content = "details", rename = "ObjectRead")] +pub enum IotaPastObjectResponse { + /// The object exists and is found with this version + VersionFound(IotaObjectData), + /// The object does not exist + ObjectNotExists(ObjectID), + /// The object is found to be deleted with this version + ObjectDeleted(IotaObjectRef), + /// The object exists but not found with this version + VersionNotFound(ObjectID, SequenceNumber), + /// The asked object version is higher than the latest + VersionTooHigh { + object_id: ObjectID, + asked_version: SequenceNumber, + latest_version: SequenceNumber, + }, +} + +impl IotaPastObjectResponse { + /// Returns a reference to the object if there is any, otherwise an Err + pub fn object(&self) -> UserInputResult<&IotaObjectData> { + match &self { + Self::ObjectDeleted(oref) => Err(UserInputError::ObjectDeleted { + object_ref: oref.to_object_ref(), + }), + Self::ObjectNotExists(id) => Err(UserInputError::ObjectNotFound { + object_id: *id, + version: None, + }), + Self::VersionFound(o) => Ok(o), + Self::VersionNotFound(id, seq_num) => Err(UserInputError::ObjectNotFound { + object_id: *id, + version: Some(*seq_num), + }), + Self::VersionTooHigh { + object_id, + asked_version, + latest_version, + } => Err(UserInputError::ObjectSequenceNumberTooHigh { + object_id: *object_id, + asked_version: *asked_version, + latest_version: *latest_version, + }), + } + } + + /// Returns the object value if there is any, otherwise an Err + pub fn into_object(self) -> UserInputResult { + match self { + Self::ObjectDeleted(oref) => Err(UserInputError::ObjectDeleted { + object_ref: oref.to_object_ref(), + }), + Self::ObjectNotExists(id) => Err(UserInputError::ObjectNotFound { + object_id: id, + version: None, + }), + Self::VersionFound(o) => Ok(o), + Self::VersionNotFound(object_id, version) => Err(UserInputError::ObjectNotFound { + object_id, + version: Some(version), + }), + Self::VersionTooHigh { + object_id, + asked_version, + latest_version, + } => Err(UserInputError::ObjectSequenceNumberTooHigh { + object_id, + asked_version, + latest_version, + }), + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +#[serde(rename = "MovePackage", rename_all = "camelCase")] +pub struct IotaMovePackage { + pub disassembled: BTreeMap, +} + +pub type ObjectsPage = Page; + +#[serde_as] +#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] +#[serde(rename = "GetPastObjectRequest", rename_all = "camelCase")] +pub struct IotaGetPastObjectRequest { + /// the ID of the queried object + pub object_id: ObjectID, + /// the version of the queried object. + #[serde_as(as = "AsSequenceNumber")] + pub version: SequenceNumber, +} + +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum IotaObjectDataFilter { + MatchAll(Vec), + MatchAny(Vec), + MatchNone(Vec), + /// Query by type a specified Package. + Package(ObjectID), + /// Query by type a specified Move module. + MoveModule { + /// the Move package ID + package: ObjectID, + /// the module name + #[serde_as(as = "DisplayFromStr")] + module: Identifier, + }, + /// Query by type + StructType( + #[serde_as(as = "IotaStructTag")] + StructTag, + ), + AddressOwner(IotaAddress), + ObjectOwner(ObjectID), + ObjectId(ObjectID), + // allow querying for multiple object ids + ObjectIds(Vec), + Version( + #[serde_as(as = "BigInt")] + u64, + ), +} + +impl IotaObjectDataFilter { + pub fn gas_coin() -> Self { + Self::StructType(GasCoin::type_()) + } + + pub fn and(self, other: Self) -> Self { + Self::MatchAll(vec![self, other]) + } + pub fn or(self, other: Self) -> Self { + Self::MatchAny(vec![self, other]) + } + pub fn not(self, other: Self) -> Self { + Self::MatchNone(vec![self, other]) + } + + pub fn matches(&self, object: &ObjectInfo) -> bool { + match self { + IotaObjectDataFilter::MatchAll(filters) => !filters.iter().any(|f| !f.matches(object)), + IotaObjectDataFilter::MatchAny(filters) => filters.iter().any(|f| f.matches(object)), + IotaObjectDataFilter::MatchNone(filters) => !filters.iter().any(|f| f.matches(object)), + IotaObjectDataFilter::StructType(s) => { + let obj_tag: StructTag = match &object.type_ { + ObjectType::Package => return false, + ObjectType::Struct(s) => s.clone().into(), + }; + // If people do not provide type_params, we will match all type_params + // e.g. `0x2::coin::Coin` can match `0x2::coin::Coin<0x2::iota::IOTA>` + if !s.type_params.is_empty() && s.type_params != obj_tag.type_params { + false + } else { + obj_tag.address == s.address + && obj_tag.module == s.module + && obj_tag.name == s.name + } + } + IotaObjectDataFilter::MoveModule { package, module } => { + matches!(&object.type_, ObjectType::Struct(s) if &ObjectID::from(s.address()) == package + && s.module() == module.as_ident_str()) + } + IotaObjectDataFilter::Package(p) => { + matches!(&object.type_, ObjectType::Struct(s) if &ObjectID::from(s.address()) == p) + } + IotaObjectDataFilter::AddressOwner(a) => { + matches!(object.owner, Owner::AddressOwner(addr) if &addr == a) + } + IotaObjectDataFilter::ObjectOwner(o) => { + matches!(object.owner, Owner::ObjectOwner(addr) if addr == IotaAddress::from(*o)) + } + IotaObjectDataFilter::ObjectId(id) => &object.object_id == id, + IotaObjectDataFilter::ObjectIds(ids) => ids.contains(&object.object_id), + IotaObjectDataFilter::Version(v) => object.version.value() == *v, + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "camelCase", rename = "ObjectResponseQuery", default)] +pub struct IotaObjectResponseQuery { + /// If None, no filter will be applied + pub filter: Option, + /// config which fields to include in the response, by default only digest + /// is included + pub options: Option, +} + +impl IotaObjectResponseQuery { + pub fn new( + filter: Option, + options: Option, + ) -> Self { + Self { filter, options } + } + + pub fn new_with_filter(filter: IotaObjectDataFilter) -> Self { + Self { + filter: Some(filter), + options: None, + } + } + + pub fn new_with_options(options: IotaObjectDataOptions) -> Self { + Self { + filter: None, + options: Some(options), + } + } +} diff --git a/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_transaction.rs b/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_transaction.rs new file mode 100644 index 0000000000..2927199e41 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/iota_transaction.rs @@ -0,0 +1,206 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + vec::Vec, + fmt::{self, Display, Formatter}, +}; + +use serde::Deserialize; +use serde::Serialize; + +use crate::types::{ + base_types::{SequenceNumber, ObjectID}, + object::Owner, + quorum_driver_types::ExecuteTransactionRequestType, + execution_status::ExecutionStatus, +}; + +use super::iota_object::IotaObjectRef; + +/// BCS serialized IotaTransactionBlockEffects +pub type IotaTransactionBlockEffectsBcs = Vec; + +/// BCS serialized IotaTransactionBlockEvents +pub type IotaTransactionBlockEventsBcs = Vec; + +/// BCS serialized ObjectChange +pub type ObjectChangeBcs = Vec; + +/// BCS serialized BalanceChange +pub type BalanceChangeBcs = Vec; + +/// BCS serialized IotaTransactionBlockKind +pub type IotaTransactionBlockKindBcs = Vec; + +pub type CheckpointSequenceNumber = u64; + +#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq, Default)] +#[serde( + rename_all = "camelCase", + rename = "TransactionBlockResponseOptions", + default +)] +pub struct IotaTransactionBlockResponseOptions { + /// Whether to show transaction input data. Default to be False + pub show_input: bool, + /// Whether to show bcs-encoded transaction input data + pub show_raw_input: bool, + /// Whether to show transaction effects. Default to be False + pub show_effects: bool, + /// Whether to show transaction events. Default to be False + pub show_events: bool, + /// Whether to show object_changes. Default to be False + pub show_object_changes: bool, + /// Whether to show balance_changes. Default to be False + pub show_balance_changes: bool, + /// Whether to show raw transaction effects. Default to be False + pub show_raw_effects: bool, +} + +impl IotaTransactionBlockResponseOptions { + pub fn new() -> Self { + Self::default() + } + + pub fn full_content() -> Self { + Self { + show_effects: true, + show_input: true, + show_raw_input: true, + show_events: true, + show_object_changes: true, + show_balance_changes: true, + // This field is added for graphql execution. We keep it false here + // so current users of `full_content` will not get raw effects unexpectedly. + show_raw_effects: false, + } + } + + pub fn with_input(mut self) -> Self { + self.show_input = true; + self + } + + pub fn with_raw_input(mut self) -> Self { + self.show_raw_input = true; + self + } + + pub fn with_effects(mut self) -> Self { + self.show_effects = true; + self + } + + pub fn with_events(mut self) -> Self { + self.show_events = true; + self + } + + pub fn with_balance_changes(mut self) -> Self { + self.show_balance_changes = true; + self + } + + pub fn with_object_changes(mut self) -> Self { + self.show_object_changes = true; + self + } + + pub fn with_raw_effects(mut self) -> Self { + self.show_raw_effects = true; + self + } + + /// default to return `WaitForEffectsCert` unless some options require + /// local execution + pub fn default_execution_request_type(&self) -> ExecuteTransactionRequestType { + // if people want effects or events, they typically want to wait for local + // execution + if self.require_effects() { + ExecuteTransactionRequestType::WaitForLocalExecution + } else { + ExecuteTransactionRequestType::WaitForEffectsCert + } + } + + pub fn require_input(&self) -> bool { + self.show_input || self.show_raw_input || self.show_object_changes + } + + pub fn require_effects(&self) -> bool { + self.show_effects + || self.show_events + || self.show_balance_changes + || self.show_object_changes + || self.show_raw_effects + } + + pub fn only_digest(&self) -> bool { + self == &Self::default() + } +} + +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] +#[serde(rename = "ExecutionStatus", rename_all = "camelCase", tag = "status")] +pub enum IotaExecutionStatus { + // Gas used in the success case. + Success, + // Gas used in the failed case, and the error. + Failure { error: String }, +} + +impl Display for IotaExecutionStatus { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Success => write!(f, "success"), + Self::Failure { error } => write!(f, "failure due to {error}"), + } + } +} + +impl IotaExecutionStatus { + pub fn is_ok(&self) -> bool { + matches!(self, IotaExecutionStatus::Success { .. }) + } + pub fn is_err(&self) -> bool { + matches!(self, IotaExecutionStatus::Failure { .. }) + } +} + +impl From for IotaExecutionStatus { + fn from(status: ExecutionStatus) -> Self { + match status { + ExecutionStatus::Success => Self::Success, + ExecutionStatus::Failure { + error, + command: None, + } => Self::Failure { + error: format!("{error:?}"), + }, + ExecutionStatus::Failure { + error, + command: Some(idx), + } => Self::Failure { + error: format!("{error:?} in command {idx}"), + }, + } + } +} + +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] +#[serde(rename = "OwnedObjectRef")] +pub struct OwnedObjectRef { + pub owner: Owner, + pub reference: IotaObjectRef, +} + +impl OwnedObjectRef { + pub fn object_id(&self) -> ObjectID { + self.reference.object_id + } + pub fn version(&self) -> SequenceNumber { + self.reference.version + } +} diff --git a/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/mod.rs b/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/mod.rs new file mode 100644 index 0000000000..47aa47d99e --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_json_rpc_types/mod.rs @@ -0,0 +1,27 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub mod iota_transaction; +pub mod iota_object; +pub mod iota_coin; +pub mod iota_event; +pub mod iota_move; + +pub use iota_transaction::*; +pub use iota_object::*; +pub use iota_coin::*; +pub use iota_event::*; + +use serde::{Deserialize, Serialize}; + +/// `next_cursor` points to the last item in the page; +/// Reading with `next_cursor` will start from the next item after `next_cursor` +/// if `next_cursor` is `Some`, otherwise it will start from the first item. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Page { + pub data: Vec, + pub next_cursor: Option, + pub has_next_page: bool, +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/balance.rs b/identity_iota_interaction/src/sdk_types/iota_types/balance.rs new file mode 100644 index 0000000000..c4867a4a10 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/balance.rs @@ -0,0 +1,95 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ident_str, fp_ensure}; + +use super::super::move_core_types::{ + identifier::{IdentStr}, + language_storage::{StructTag, TypeTag}, + annotated_value::{MoveStructLayout, MoveFieldLayout, MoveTypeLayout} +}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use super::{ + error::{ExecutionError, ExecutionErrorKind}, + iota_serde::{BigInt, Readable}, + IOTA_FRAMEWORK_ADDRESS, +}; + +pub const BALANCE_MODULE_NAME: &IdentStr = ident_str!("balance"); +pub const BALANCE_STRUCT_NAME: &IdentStr = ident_str!("Balance"); +pub const BALANCE_CREATE_REWARDS_FUNCTION_NAME: &IdentStr = ident_str!("create_staking_rewards"); +pub const BALANCE_DESTROY_REBATES_FUNCTION_NAME: &IdentStr = ident_str!("destroy_storage_rebates"); + +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct Supply { + #[serde_as(as = "Readable, _>")] + pub value: u64, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct Balance { + value: u64, +} + +impl Balance { + pub fn new(value: u64) -> Self { + Self { value } + } + + pub fn type_(type_param: TypeTag) -> StructTag { + StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + module: BALANCE_MODULE_NAME.to_owned(), + name: BALANCE_STRUCT_NAME.to_owned(), + type_params: vec![type_param], + } + } + + pub fn type_tag(inner_type_param: TypeTag) -> TypeTag { + TypeTag::Struct(Box::new(Self::type_(inner_type_param))) + } + + pub fn is_balance(s: &StructTag) -> bool { + s.address == IOTA_FRAMEWORK_ADDRESS + && s.module.as_ident_str() == BALANCE_MODULE_NAME + && s.name.as_ident_str() == BALANCE_STRUCT_NAME + } + + pub fn withdraw(&mut self, amount: u64) -> Result<(), ExecutionError> { + fp_ensure!( + self.value >= amount, + ExecutionError::new_with_source( + ExecutionErrorKind::InsufficientCoinBalance, + format!("balance: {} required: {}", self.value, amount) + ) + ); + self.value -= amount; + Ok(()) + } + + pub fn deposit_for_safe_mode(&mut self, amount: u64) { + self.value += amount; + } + + pub fn value(&self) -> u64 { + self.value + } + + pub fn to_bcs_bytes(&self) -> Vec { + bcs::to_bytes(&self).unwrap() + } + + pub fn layout(type_param: TypeTag) -> MoveStructLayout { + MoveStructLayout { + type_: Self::type_(type_param), + fields: vec![MoveFieldLayout::new( + ident_str!("value").to_owned(), + MoveTypeLayout::U64, + )], + } + } +} diff --git a/identity_iota_interaction/src/sdk_types/iota_types/base_types.rs b/identity_iota_interaction/src/sdk_types/iota_types/base_types.rs new file mode 100644 index 0000000000..f520c9f4ab --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/base_types.rs @@ -0,0 +1,810 @@ +// Copyright (c) 2021, Facebook, Inc. and its affiliates +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt; +use std::vec::Vec; +use std::option::Option; +use std::convert::{AsRef, TryFrom}; +use std::result::Result::Ok; +use std::option::Option::Some; +use std::str::FromStr; +use std::string::String; + +use Result; + +use rand::Rng; +use anyhow::anyhow; + +use serde::{ser::Error, Deserialize, Serialize}; +use serde_with::serde_as; + +use fastcrypto::encoding::{Hex, Encoding, decode_bytes_hex}; +use fastcrypto::hash::HashFunction; + +use crate::ident_str; + +use super::super::move_core_types::language_storage::{StructTag, TypeTag, ModuleId}; +use super::super::move_core_types::identifier::IdentStr; +use super::super::move_core_types::account_address::AccountAddress; + +use super::{IOTA_FRAMEWORK_ADDRESS, IOTA_CLOCK_OBJECT_ID, IOTA_SYSTEM_ADDRESS, MOVE_STDLIB_ADDRESS}; +use super::balance::Balance; +use super::coin::{Coin, CoinMetadata, TreasuryCap, COIN_MODULE_NAME, COIN_STRUCT_NAME}; +use super::crypto::{AuthorityPublicKeyBytes, IotaPublicKey, DefaultHash, PublicKey}; +use super::dynamic_field::DynamicFieldInfo; +use super::error::{IotaError, IotaResult}; +use super::gas_coin::GAS; +use super::governance::{StakedIota, STAKING_POOL_MODULE_NAME, STAKED_IOTA_STRUCT_NAME}; +use super::iota_serde::{Readable, HexAccountAddress, to_iota_struct_tag_string}; +use super::timelock::timelock::{self, TimeLock}; +use super::timelock::timelocked_staked_iota::TimelockedStakedIota; +use super::stardust::output::Nft; +use super::gas_coin::GasCoin; +use super::object::{Owner}; +use super::parse_iota_struct_tag; + +pub use super::digests::{ObjectDigest, TransactionDigest}; + +// ----------------------------------------------------------------- +// Originally defined in crates/iota-types/src/committee.rs +// ----------------------------------------------------------------- +pub type EpochId = u64; +// TODO: the stake and voting power of a validator can be different so +// in some places when we are actually referring to the voting power, we +// should use a different type alias, field name, etc. +pub type StakeUnit = u64; +// ----------------------------------------------------------------- +// Originally defined in crates/iota-types/src/execution_status.rs +// ----------------------------------------------------------------- +pub type CommandIndex = usize; +// ----------------------------------------------------------------- +// Originally defined in external-crates/move/crates/move-binary-format/src/file_format.rs +// ----------------------------------------------------------------- +/// Index into the code stream for a jump. The offset is relative to the +/// beginning of the instruction stream. +pub type CodeOffset = u16; +/// Type parameters are encoded as indices. This index can also be used to +/// lookup the kind of a type parameter in the `FunctionHandle` and +/// `StructHandle`. +pub type TypeParameterIndex = u16; +// ----------------------------------------------------------------- + +#[derive( + Eq, + PartialEq, + Ord, + PartialOrd, + Copy, + Clone, + Hash, + Default, + Debug, + Serialize, + Deserialize, +)] +pub struct SequenceNumber(u64); + +impl fmt::Display for SequenceNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:#x}", self.0) + } +} + +pub type AuthorityName = AuthorityPublicKeyBytes; + +#[serde_as] +#[derive(Eq, PartialEq, Clone, Copy, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct ObjectID( + #[serde_as(as = "Readable")] + AccountAddress, +); + +pub type ObjectRef = (ObjectID, SequenceNumber, ObjectDigest); + +/// Wrapper around StructTag with a space-efficient representation for common +/// types like coins The StructTag for a gas coin is 84 bytes, so using 1 byte +/// instead is a win. The inner representation is private to prevent incorrectly +/// constructing an `Other` instead of one of the specialized variants, e.g. +/// `Other(GasCoin::type_())` instead of `GasCoin` +#[derive(Eq, PartialEq, PartialOrd, Ord, Debug, Clone, Deserialize, Serialize, Hash)] +pub struct MoveObjectType(MoveObjectType_); + +/// Even though it is declared public, it is the "private", internal +/// representation for `MoveObjectType` +#[derive(Eq, PartialEq, PartialOrd, Ord, Debug, Clone, Deserialize, Serialize, Hash)] +pub enum MoveObjectType_ { + /// A type that is not `0x2::coin::Coin` + Other(StructTag), + /// An IOTA coin (i.e., `0x2::coin::Coin<0x2::iota::IOTA>`) + GasCoin, + /// A record of a staked IOTA coin (i.e., `0x3::staking_pool::StakedIota`) + StakedIota, + /// A non-IOTA coin type (i.e., `0x2::coin::Coin where T != + /// 0x2::iota::IOTA`) + Coin(TypeTag), + // NOTE: if adding a new type here, and there are existing on-chain objects of that + // type with Other(_), that is ok, but you must hand-roll PartialEq/Eq/Ord/maybe Hash + // to make sure the new type and Other(_) are interpreted consistently. +} + +impl MoveObjectType { + pub fn gas_coin() -> Self { + Self(MoveObjectType_::GasCoin) + } + + pub fn staked_iota() -> Self { + Self(MoveObjectType_::StakedIota) + } + + pub fn timelocked_iota_balance() -> Self { + Self(MoveObjectType_::Other(TimeLock::::type_( + Balance::type_(GAS::type_().into()).into(), + ))) + } + + pub fn timelocked_staked_iota() -> Self { + Self(MoveObjectType_::Other(TimelockedStakedIota::type_())) + } + + pub fn stardust_nft() -> Self { + Self(MoveObjectType_::Other(Nft::tag())) + } + + pub fn address(&self) -> AccountAddress { + match &self.0 { + MoveObjectType_::GasCoin | MoveObjectType_::Coin(_) => IOTA_FRAMEWORK_ADDRESS, + MoveObjectType_::StakedIota => IOTA_SYSTEM_ADDRESS, + MoveObjectType_::Other(s) => s.address, + } + } + + pub fn module(&self) -> &IdentStr { + match &self.0 { + MoveObjectType_::GasCoin | MoveObjectType_::Coin(_) => COIN_MODULE_NAME, + MoveObjectType_::StakedIota => STAKING_POOL_MODULE_NAME, + MoveObjectType_::Other(s) => &s.module, + } + } + + pub fn name(&self) -> &IdentStr { + match &self.0 { + MoveObjectType_::GasCoin | MoveObjectType_::Coin(_) => COIN_STRUCT_NAME, + MoveObjectType_::StakedIota => STAKED_IOTA_STRUCT_NAME, + MoveObjectType_::Other(s) => &s.name, + } + } + + pub fn type_params(&self) -> Vec { + match &self.0 { + MoveObjectType_::GasCoin => vec![GAS::type_tag()], + MoveObjectType_::StakedIota => vec![], + MoveObjectType_::Coin(inner) => vec![inner.clone()], + MoveObjectType_::Other(s) => s.type_params.clone(), + } + } + + pub fn into_type_params(self) -> Vec { + match self.0 { + MoveObjectType_::GasCoin => vec![GAS::type_tag()], + MoveObjectType_::StakedIota => vec![], + MoveObjectType_::Coin(inner) => vec![inner], + MoveObjectType_::Other(s) => s.type_params, + } + } + + pub fn coin_type_maybe(&self) -> Option { + match &self.0 { + MoveObjectType_::GasCoin => Some(GAS::type_tag()), + MoveObjectType_::Coin(inner) => Some(inner.clone()), + MoveObjectType_::StakedIota => None, + MoveObjectType_::Other(_) => None, + } + } + + pub fn module_id(&self) -> ModuleId { + ModuleId::new(self.address(), self.module().to_owned()) + } + + pub fn size_for_gas_metering(&self) -> usize { + // unwraps safe because a `StructTag` cannot fail to serialize + match &self.0 { + MoveObjectType_::GasCoin => 1, + MoveObjectType_::StakedIota => 1, + MoveObjectType_::Coin(inner) => bcs::serialized_size(inner).unwrap() + 1, + MoveObjectType_::Other(s) => bcs::serialized_size(s).unwrap() + 1, + } + } + + /// Return true if `self` is `0x2::coin::Coin` for some T (note: T can be + /// IOTA) + pub fn is_coin(&self) -> bool { + match &self.0 { + MoveObjectType_::GasCoin | MoveObjectType_::Coin(_) => true, + MoveObjectType_::StakedIota | MoveObjectType_::Other(_) => false, + } + } + + /// Return true if `self` is 0x2::coin::Coin<0x2::iota::IOTA> + pub fn is_gas_coin(&self) -> bool { + match &self.0 { + MoveObjectType_::GasCoin => true, + MoveObjectType_::StakedIota | MoveObjectType_::Coin(_) | MoveObjectType_::Other(_) => { + false + } + } + } + + /// Return true if `self` is `0x2::coin::Coin` + pub fn is_coin_t(&self, t: &TypeTag) -> bool { + match &self.0 { + MoveObjectType_::GasCoin => GAS::is_gas_type(t), + MoveObjectType_::Coin(c) => t == c, + MoveObjectType_::StakedIota | MoveObjectType_::Other(_) => false, + } + } + + pub fn is_staked_iota(&self) -> bool { + match &self.0 { + MoveObjectType_::StakedIota => true, + MoveObjectType_::GasCoin | MoveObjectType_::Coin(_) | MoveObjectType_::Other(_) => { + false + } + } + } + + pub fn is_coin_metadata(&self) -> bool { + match &self.0 { + MoveObjectType_::GasCoin | MoveObjectType_::StakedIota | MoveObjectType_::Coin(_) => { + false + } + MoveObjectType_::Other(s) => CoinMetadata::is_coin_metadata(s), + } + } + + pub fn is_treasury_cap(&self) -> bool { + match &self.0 { + MoveObjectType_::GasCoin | MoveObjectType_::StakedIota | MoveObjectType_::Coin(_) => { + false + } + MoveObjectType_::Other(s) => TreasuryCap::is_treasury_type(s), + } + } + + pub fn is_upgrade_cap(&self) -> bool { + self.address() == IOTA_FRAMEWORK_ADDRESS + && self.module().as_str() == "package" + && self.name().as_str() == "UpgradeCap" + } + + pub fn is_regulated_coin_metadata(&self) -> bool { + self.address() == IOTA_FRAMEWORK_ADDRESS + && self.module().as_str() == "coin" + && self.name().as_str() == "RegulatedCoinMetadata" + } + + pub fn is_coin_deny_cap_v1(&self) -> bool { + self.address() == IOTA_FRAMEWORK_ADDRESS + && self.module().as_str() == "coin" + && self.name().as_str() == "DenyCapV1" + } + + pub fn is_dynamic_field(&self) -> bool { + match &self.0 { + MoveObjectType_::GasCoin | MoveObjectType_::StakedIota | MoveObjectType_::Coin(_) => { + false + } + MoveObjectType_::Other(s) => DynamicFieldInfo::is_dynamic_field(s), + } + } + + pub fn is_timelock(&self) -> bool { + match &self.0 { + MoveObjectType_::GasCoin | MoveObjectType_::StakedIota | MoveObjectType_::Coin(_) => { + false + } + MoveObjectType_::Other(s) => timelock::is_timelock(s), + } + } + + pub fn is_timelocked_balance(&self) -> bool { + match &self.0 { + MoveObjectType_::GasCoin | MoveObjectType_::StakedIota | MoveObjectType_::Coin(_) => { + false + } + MoveObjectType_::Other(s) => timelock::is_timelocked_balance(s), + } + } + + pub fn is_timelocked_staked_iota(&self) -> bool { + match &self.0 { + MoveObjectType_::GasCoin | MoveObjectType_::StakedIota | MoveObjectType_::Coin(_) => { + false + } + MoveObjectType_::Other(s) => TimelockedStakedIota::is_timelocked_staked_iota(s), + } + } + + pub fn try_extract_field_value(&self) -> IotaResult { + match &self.0 { + MoveObjectType_::GasCoin | MoveObjectType_::StakedIota | MoveObjectType_::Coin(_) => { + Err(IotaError::ObjectDeserialization { + error: "Error extracting dynamic object value from Coin object".to_string(), + }) + } + MoveObjectType_::Other(s) => DynamicFieldInfo::try_extract_field_value(s), + } + } +} + +impl From for MoveObjectType { + fn from(mut s: StructTag) -> Self { + Self(if GasCoin::is_gas_coin(&s) { + MoveObjectType_::GasCoin + } else if Coin::is_coin(&s) { + // unwrap safe because a coin has exactly one type parameter + MoveObjectType_::Coin(s.type_params.pop().unwrap()) + } else if StakedIota::is_staked_iota(&s) { + MoveObjectType_::StakedIota + } else { + MoveObjectType_::Other(s) + }) + } +} + +impl From for StructTag { + fn from(t: MoveObjectType) -> Self { + match t.0 { + MoveObjectType_::GasCoin => GasCoin::type_(), + MoveObjectType_::StakedIota => StakedIota::type_(), + MoveObjectType_::Coin(inner) => Coin::type_(inner), + MoveObjectType_::Other(s) => s, + } + } +} + +impl From for TypeTag { + fn from(o: MoveObjectType) -> TypeTag { + let s: StructTag = o.into(); + TypeTag::Struct(Box::new(s)) + } +} + +/// Type of an IOTA object +#[derive(Clone, Serialize, Deserialize, Ord, PartialOrd, Eq, PartialEq, Debug)] +pub enum ObjectType { + /// Move package containing one or more bytecode modules + Package, + /// A Move struct of the given type + Struct(MoveObjectType), +} + +impl TryFrom for StructTag { + type Error = anyhow::Error; + + fn try_from(o: ObjectType) -> Result { + match o { + ObjectType::Package => Err(anyhow!("Cannot create StructTag from Package")), + ObjectType::Struct(move_object_type) => Ok(move_object_type.into()), + } + } +} + +impl FromStr for ObjectType { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + if s.to_lowercase() == PACKAGE { + Ok(ObjectType::Package) + } else { + let tag = parse_iota_struct_tag(s)?; + Ok(ObjectType::Struct(MoveObjectType::from(tag))) + } + } +} + +#[derive(Clone, Serialize, Deserialize, Ord, PartialOrd, Eq, PartialEq, Debug)] +pub struct ObjectInfo { + pub object_id: ObjectID, + pub version: SequenceNumber, + pub digest: ObjectDigest, + pub type_: ObjectType, + pub owner: Owner, + pub previous_transaction: TransactionDigest, +} + +const PACKAGE: &str = "package"; +impl ObjectType { + pub fn is_gas_coin(&self) -> bool { + matches!(self, ObjectType::Struct(s) if s.is_gas_coin()) + } + + pub fn is_coin(&self) -> bool { + matches!(self, ObjectType::Struct(s) if s.is_coin()) + } + + /// Return true if `self` is `0x2::coin::Coin` + pub fn is_coin_t(&self, t: &TypeTag) -> bool { + matches!(self, ObjectType::Struct(s) if s.is_coin_t(t)) + } + + pub fn is_package(&self) -> bool { + matches!(self, ObjectType::Package) + } +} + +impl From for ObjectRef { + fn from(info: ObjectInfo) -> Self { + (info.object_id, info.version, info.digest) + } +} + +impl From<&ObjectInfo> for ObjectRef { + fn from(info: &ObjectInfo) -> Self { + (info.object_id, info.version, info.digest) + } +} + +pub const IOTA_ADDRESS_LENGTH: usize = ObjectID::LENGTH; + +#[serde_as] +#[derive(Eq, Default, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize)] +pub struct IotaAddress( + #[serde_as(as = "Readable")] + [u8; IOTA_ADDRESS_LENGTH], +); + +impl IotaAddress { + pub const ZERO: Self = Self([0u8; IOTA_ADDRESS_LENGTH]); + + /// Convert the address to a byte buffer. + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + + pub fn generate(mut rng: R) -> Self { + let buf: [u8; IOTA_ADDRESS_LENGTH] = rng.gen(); + Self(buf) + } + + /// Serialize an `Option` in Hex. + pub fn optional_address_as_hex( + key: &Option, + serializer: S, + ) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(&key.map(Hex::encode).unwrap_or_default()) + } + + /// Deserialize into an `Option`. + pub fn optional_address_from_hex<'de, D>( + deserializer: D, + ) -> Result, D::Error> + where + D: serde::de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let value = decode_bytes_hex(&s).map_err(serde::de::Error::custom)?; + Ok(Some(value)) + } + + /// Return the underlying byte array of a IotaAddress. + pub fn to_inner(self) -> [u8; IOTA_ADDRESS_LENGTH] { + self.0 + } + + /// Parse a IotaAddress from a byte array or buffer. + pub fn from_bytes>(bytes: T) -> Result { + <[u8; IOTA_ADDRESS_LENGTH]>::try_from(bytes.as_ref()) + .map_err(|_| IotaError::InvalidAddress) + .map(IotaAddress) + } +} + +impl From for IotaAddress { + fn from(object_id: ObjectID) -> IotaAddress { + Self(object_id.into_bytes()) + } +} + +impl From for IotaAddress { + fn from(address: AccountAddress) -> IotaAddress { + Self(address.into_bytes()) + } +} + +impl TryFrom<&[u8]> for IotaAddress { + type Error = IotaError; + + /// Tries to convert the provided byte array into a IotaAddress. + fn try_from(bytes: &[u8]) -> Result { + Self::from_bytes(bytes) + } +} + +impl TryFrom> for IotaAddress { + type Error = IotaError; + + /// Tries to convert the provided byte buffer into a IotaAddress. + fn try_from(bytes: Vec) -> Result { + Self::from_bytes(bytes) + } +} + +impl AsRef<[u8]> for IotaAddress { + fn as_ref(&self) -> &[u8] { + &self.0[..] + } +} + +impl FromStr for IotaAddress { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + decode_bytes_hex(s).map_err(|e| anyhow!(e)) + } +} + +impl From<&T> for IotaAddress { + fn from(pk: &T) -> Self { + let mut hasher = DefaultHash::default(); + T::SIGNATURE_SCHEME.update_hasher_with_flag(&mut hasher); + hasher.update(pk); + let g_arr = hasher.finalize(); + IotaAddress(g_arr.digest) + } +} + +impl From<&PublicKey> for IotaAddress { + fn from(pk: &PublicKey) -> Self { + let mut hasher = DefaultHash::default(); + pk.scheme().update_hasher_with_flag(&mut hasher); + hasher.update(pk); + let g_arr = hasher.finalize(); + IotaAddress(g_arr.digest) + } +} + +impl fmt::Display for IotaAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x{}", Hex::encode(self.0)) + } +} + +impl fmt::Debug for IotaAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "0x{}", Hex::encode(self.0)) + } +} + +pub const STD_OPTION_MODULE_NAME: &IdentStr = ident_str!("option"); +pub const STD_OPTION_STRUCT_NAME: &IdentStr = ident_str!("Option"); +pub const RESOLVED_STD_OPTION: (&AccountAddress, &IdentStr, &IdentStr) = ( + &MOVE_STDLIB_ADDRESS, + STD_OPTION_MODULE_NAME, + STD_OPTION_STRUCT_NAME, +); + +pub const STD_ASCII_MODULE_NAME: &IdentStr = ident_str!("ascii"); +pub const STD_ASCII_STRUCT_NAME: &IdentStr = ident_str!("String"); +pub const RESOLVED_ASCII_STR: (&AccountAddress, &IdentStr, &IdentStr) = ( + &MOVE_STDLIB_ADDRESS, + STD_ASCII_MODULE_NAME, + STD_ASCII_STRUCT_NAME, +); + +pub const STD_UTF8_MODULE_NAME: &IdentStr = ident_str!("string"); +pub const STD_UTF8_STRUCT_NAME: &IdentStr = ident_str!("String"); +pub const RESOLVED_UTF8_STR: (&AccountAddress, &IdentStr, &IdentStr) = ( + &MOVE_STDLIB_ADDRESS, + STD_UTF8_MODULE_NAME, + STD_UTF8_STRUCT_NAME, +); + +// TODO: rename to version +impl SequenceNumber { + pub const MIN: SequenceNumber = SequenceNumber(u64::MIN); + pub const MAX: SequenceNumber = SequenceNumber(0x7fff_ffff_ffff_ffff); + + pub const fn new() -> Self { + SequenceNumber(0) + } + + pub const fn value(&self) -> u64 { + self.0 + } + + pub const fn from_u64(u: u64) -> Self { + SequenceNumber(u) + } +} + +impl ObjectID { + /// The number of bytes in an address. + pub const LENGTH: usize = AccountAddress::LENGTH; + /// Hex address: 0x0 + pub const ZERO: Self = Self::new([0u8; Self::LENGTH]); + pub const MAX: Self = Self::new([0xff; Self::LENGTH]); + /// Create a new ObjectID + pub const fn new(obj_id: [u8; Self::LENGTH]) -> Self { + Self(AccountAddress::new(obj_id)) + } + + /// Const fn variant of `>::from` + pub const fn from_address(addr: AccountAddress) -> Self { + Self(addr) + } + + /// Return a random ObjectID. + pub fn random() -> Self { + Self::from(AccountAddress::random()) + } + + /// Return the underlying bytes buffer of the ObjectID. + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + + /// Parse the ObjectID from byte array or buffer. + pub fn from_bytes>(bytes: T) -> Result { + <[u8; Self::LENGTH]>::try_from(bytes.as_ref()) + .map_err(|_| ObjectIDParseError::TryFromSlice) + .map(ObjectID::new) + } + + /// Return the underlying bytes array of the ObjectID. + pub fn into_bytes(self) -> [u8; Self::LENGTH] { + self.0.into_bytes() + } + + /// Make an ObjectID with padding 0s before the single byte. + pub const fn from_single_byte(byte: u8) -> ObjectID { + let mut bytes = [0u8; Self::LENGTH]; + bytes[Self::LENGTH - 1] = byte; + ObjectID::new(bytes) + } + + /// Convert from hex string to ObjectID where the string is prefixed with 0x + /// Padding 0s if the string is too short. + pub fn from_hex_literal(literal: &str) -> Result { + if !literal.starts_with("0x") { + return Err(ObjectIDParseError::HexLiteralPrefixMissing); + } + + let hex_len = literal.len() - 2; + + // If the string is too short, pad it + if hex_len < Self::LENGTH * 2 { + let mut hex_str = String::with_capacity(Self::LENGTH * 2); + for _ in 0..Self::LENGTH * 2 - hex_len { + hex_str.push('0'); + } + hex_str.push_str(&literal[2..]); + Self::from_str(&hex_str) + } else { + Self::from_str(&literal[2..]) + } + } + + + /// Return the full hex string with 0x prefix without removing trailing 0s. + /// Prefer this over [fn to_hex_literal] if the string needs to be fully + /// preserved. + pub fn to_hex_uncompressed(&self) -> String { + format!("{self}") + } + + pub fn is_clock(&self) -> bool { + *self == IOTA_CLOCK_OBJECT_ID + } +} + +impl From for ObjectID { + fn from(address: IotaAddress) -> ObjectID { + let tmp: AccountAddress = address.into(); + tmp.into() + } +} + +impl From for ObjectID { + fn from(address: AccountAddress) -> Self { + Self(address) + } +} + +impl fmt::Display for ObjectID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "0x{}", Hex::encode(self.0)) + } +} + +impl fmt::Debug for ObjectID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "0x{}", Hex::encode(self.0)) + } +} + +impl AsRef<[u8]> for ObjectID { + fn as_ref(&self) -> &[u8] { + self.0.as_slice() + } +} + +impl TryFrom<&[u8]> for ObjectID { + type Error = ObjectIDParseError; + + /// Tries to convert the provided byte array into ObjectID. + fn try_from(bytes: &[u8]) -> Result { + Self::from_bytes(bytes) + } +} + +impl TryFrom> for ObjectID { + type Error = ObjectIDParseError; + + /// Tries to convert the provided byte buffer into ObjectID. + fn try_from(bytes: Vec) -> Result { + Self::from_bytes(bytes) + } +} + +impl FromStr for ObjectID { + type Err = ObjectIDParseError; + + /// Parse ObjectID from hex string with or without 0x prefix, pad with 0s if + /// needed. + fn from_str(s: &str) -> Result { + decode_bytes_hex(s).or_else(|_| Self::from_hex_literal(s)) + } +} + +impl std::ops::Deref for ObjectID { + type Target = AccountAddress; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(PartialEq, Eq, Clone, Debug, thiserror::Error)] +pub enum ObjectIDParseError { + #[error("ObjectID hex literal must start with 0x")] + HexLiteralPrefixMissing, + + #[error("Could not convert from bytes slice")] + TryFromSlice, +} + +impl From for AccountAddress { + fn from(obj_id: ObjectID) -> Self { + obj_id.0 + } +} + +impl From for AccountAddress { + fn from(address: IotaAddress) -> Self { + Self::new(address.0) + } +} + +impl fmt::Display for MoveObjectType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + let s: StructTag = self.clone().into(); + write!( + f, + "{}", + to_iota_struct_tag_string(&s).map_err(fmt::Error::custom)? + ) + } +} + +impl fmt::Display for ObjectType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ObjectType::Package => write!(f, "{}", PACKAGE), + ObjectType::Struct(t) => write!(f, "{}", t), + } + } +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/coin.rs b/identity_iota_interaction/src/sdk_types/iota_types/coin.rs new file mode 100644 index 0000000000..2377ee80a3 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/coin.rs @@ -0,0 +1,186 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::Deserialize; +use serde::Serialize; + +use crate::ident_str; + +use super::super::move_core_types::language_storage::{StructTag, TypeTag}; +use super::super::move_core_types::identifier::IdentStr; +use super::super::move_core_types::annotated_value::{MoveStructLayout, MoveFieldLayout, MoveTypeLayout}; + +use super::id::UID; +use super::IOTA_FRAMEWORK_ADDRESS; +use super::error::{IotaError, ExecutionError, ExecutionErrorKind}; +use super::balance::{Supply, Balance}; +use super::base_types::ObjectID; + +pub const COIN_MODULE_NAME: &IdentStr = ident_str!("coin"); +pub const COIN_STRUCT_NAME: &IdentStr = ident_str!("Coin"); +pub const COIN_METADATA_STRUCT_NAME: &IdentStr = ident_str!("CoinMetadata"); +pub const COIN_TREASURE_CAP_NAME: &IdentStr = ident_str!("TreasuryCap"); +pub const COIN_JOIN_FUNC_NAME: &IdentStr = ident_str!("join"); + +pub const PAY_MODULE_NAME: &IdentStr = ident_str!("pay"); +pub const PAY_SPLIT_N_FUNC_NAME: &IdentStr = ident_str!("divide_and_keep"); +pub const PAY_SPLIT_VEC_FUNC_NAME: &IdentStr = ident_str!("split_vec"); + +// Rust version of the Move iota::coin::Coin type +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct Coin { + pub id: UID, + pub balance: Balance, +} + +impl Coin { + pub fn new(id: UID, value: u64) -> Self { + Self { + id, + balance: Balance::new(value), + } + } + + pub fn type_(type_param: TypeTag) -> StructTag { + StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + name: COIN_STRUCT_NAME.to_owned(), + module: COIN_MODULE_NAME.to_owned(), + type_params: vec![type_param], + } + } + + /// Is this other StructTag representing a Coin? + pub fn is_coin(other: &StructTag) -> bool { + other.address == IOTA_FRAMEWORK_ADDRESS + && other.module.as_ident_str() == COIN_MODULE_NAME + && other.name.as_ident_str() == COIN_STRUCT_NAME + } + + /// Create a coin from BCS bytes + pub fn from_bcs_bytes(content: &[u8]) -> Result { + bcs::from_bytes(content) + } + + pub fn id(&self) -> &ObjectID { + self.id.object_id() + } + + pub fn value(&self) -> u64 { + self.balance.value() + } + + pub fn to_bcs_bytes(&self) -> Vec { + bcs::to_bytes(&self).unwrap() + } + + pub fn layout(type_param: TypeTag) -> MoveStructLayout { + MoveStructLayout { + type_: Self::type_(type_param.clone()), + fields: vec![ + MoveFieldLayout::new( + ident_str!("id").to_owned(), + MoveTypeLayout::Struct(UID::layout()), + ), + MoveFieldLayout::new( + ident_str!("balance").to_owned(), + MoveTypeLayout::Struct(Balance::layout(type_param)), + ), + ], + } + } + + /// Add balance to this coin, erroring if the new total balance exceeds the + /// maximum + pub fn add(&mut self, balance: Balance) -> Result<(), ExecutionError> { + let Some(new_value) = self.value().checked_add(balance.value()) else { + return Err(ExecutionError::from_kind( + ExecutionErrorKind::CoinBalanceOverflow, + )); + }; + self.balance = Balance::new(new_value); + Ok(()) + } + + // Split amount out of this coin to a new coin. + // Related coin objects need to be updated in temporary_store to persist the + // changes, including creating the coin object related to the newly created + // coin. + pub fn split(&mut self, amount: u64, new_coin_id: UID) -> Result { + self.balance.withdraw(amount)?; + Ok(Coin::new(new_coin_id, amount)) + } +} + +// Rust version of the Move iota::coin::TreasuryCap type +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct TreasuryCap { + pub id: UID, + pub total_supply: Supply, +} + +impl TreasuryCap { + pub fn is_treasury_type(other: &StructTag) -> bool { + other.address == IOTA_FRAMEWORK_ADDRESS + && other.module.as_ident_str() == COIN_MODULE_NAME + && other.name.as_ident_str() == COIN_TREASURE_CAP_NAME + } + + /// Create a TreasuryCap from BCS bytes + pub fn from_bcs_bytes(content: &[u8]) -> Result { + bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization { + error: format!("Unable to deserialize TreasuryCap object: {}", err), + }) + } + + pub fn type_(type_param: StructTag) -> StructTag { + StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + name: COIN_TREASURE_CAP_NAME.to_owned(), + module: COIN_MODULE_NAME.to_owned(), + type_params: vec![TypeTag::Struct(Box::new(type_param))], + } + } +} + +// Rust version of the Move iota::coin::CoinMetadata type +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct CoinMetadata { + pub id: UID, + /// Number of decimal places the coin uses. + pub decimals: u8, + /// Name for the token + pub name: String, + /// Symbol for the token + pub symbol: String, + /// Description of the token + pub description: String, + /// URL for the token logo + pub icon_url: Option, +} + +impl CoinMetadata { + /// Is this other StructTag representing a CoinMetadata? + pub fn is_coin_metadata(other: &StructTag) -> bool { + other.address == IOTA_FRAMEWORK_ADDRESS + && other.module.as_ident_str() == COIN_MODULE_NAME + && other.name.as_ident_str() == COIN_METADATA_STRUCT_NAME + } + + /// Create a coin from BCS bytes + pub fn from_bcs_bytes(content: &[u8]) -> Result { + bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization { + error: format!("Unable to deserialize CoinMetadata object: {}", err), + }) + } + + pub fn type_(type_param: StructTag) -> StructTag { + StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + name: COIN_METADATA_STRUCT_NAME.to_owned(), + module: COIN_MODULE_NAME.to_owned(), + type_params: vec![TypeTag::Struct(Box::new(type_param))], + } + } +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/collection_types.rs b/identity_iota_interaction/src/sdk_types/iota_types/collection_types.rs new file mode 100644 index 0000000000..28d851db4a --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/collection_types.rs @@ -0,0 +1,103 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use super::{base_types::ObjectID, id::UID}; + +/// Rust version of the Move iota::vec_map::VecMap type +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct VecMap { + pub contents: Vec>, +} + +/// Rust version of the Move iota::vec_map::Entry type +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct Entry { + pub key: K, + pub value: V, +} + +/// Rust version of the Move iota::vec_set::VecSet type +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct VecSet { + pub contents: Vec, +} + +/// Rust version of the Move iota::table::Table type. +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct TableVec { + pub contents: Table, +} + +impl Default for TableVec { + fn default() -> Self { + TableVec { + contents: Table { + id: ObjectID::ZERO, + size: 0, + }, + } + } +} + +/// Rust version of the Move iota::table::Table type. +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct Table { + pub id: ObjectID, + pub size: u64, +} + +impl Default for Table { + fn default() -> Self { + Table { + id: ObjectID::ZERO, + size: 0, + } + } +} + +/// Rust version of the Move iota::linked_table::LinkedTable type. +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct LinkedTable { + pub id: ObjectID, + pub size: u64, + pub head: Option, + pub tail: Option, +} + +impl Default for LinkedTable { + fn default() -> Self { + LinkedTable { + id: ObjectID::ZERO, + size: 0, + head: None, + tail: None, + } + } +} + +/// Rust version of the Move iota::linked_table::Node type. +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct LinkedTableNode { + pub prev: Option, + pub next: Option, + pub value: V, +} + +/// Rust version of the Move iota::bag::Bag type. +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct Bag { + pub id: UID, + pub size: u64, +} + +impl Default for Bag { + fn default() -> Self { + Self { + id: UID::new(ObjectID::ZERO), + size: 0, + } + } +} diff --git a/identity_iota_interaction/src/sdk_types/iota_types/crypto.rs b/identity_iota_interaction/src/sdk_types/iota_types/crypto.rs new file mode 100644 index 0000000000..537d743f8c --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/crypto.rs @@ -0,0 +1,557 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +use std::hash::Hash; +use std::str::FromStr; + +use enum_dispatch::enum_dispatch; +use strum::EnumString; +use schemars::JsonSchema; +use derive_more::{AsRef, AsMut}; + +use fastcrypto::{ + error::FastCryptoResult, + bls12381::min_sig::{ + BLS12381AggregateSignature, BLS12381AggregateSignatureAsBytes, BLS12381KeyPair, + BLS12381PrivateKey, BLS12381PublicKey, BLS12381Signature, + }, ed25519::{ + Ed25519KeyPair, Ed25519PrivateKey, Ed25519PublicKey, Ed25519PublicKeyAsBytes, + Ed25519Signature + }, encoding::{Base64, Encoding}, error::FastCryptoError, hash::{Blake2b256, HashFunction}, secp256k1::{Secp256k1KeyPair, Secp256k1PublicKey, Secp256k1PublicKeyAsBytes, Secp256k1Signature}, secp256r1::{Secp256r1KeyPair, Secp256r1PublicKey, Secp256r1PublicKeyAsBytes, Secp256r1Signature}, traits::{Authenticator, KeyPair as KeypairTraits, Signer, ToFromBytes, VerifyingKey, EncodeDecodeBase64} +}; +use fastcrypto_zkp::zk_login_utils::Bn254FrElement; + +use serde::{Deserialize, Deserializer, Serializer}; +use serde::Serialize; +use serde_with::{serde_as, Bytes}; + +use crate::shared_crypto::intent::IntentMessage; + +use super::{ + base_types::IotaAddress, error::{IotaError, IotaResult}, iota_serde::Readable +}; + +// Authority Objects +pub type AuthorityKeyPair = BLS12381KeyPair; +pub type AuthorityPublicKey = BLS12381PublicKey; +pub type AuthorityPrivateKey = BLS12381PrivateKey; +pub type AuthoritySignature = BLS12381Signature; +pub type AggregateAuthoritySignature = BLS12381AggregateSignature; +pub type AggregateAuthoritySignatureAsBytes = BLS12381AggregateSignatureAsBytes; + +// TODO(joyqvq): prefix these types with Default, DefaultAccountKeyPair etc +pub type AccountKeyPair = Ed25519KeyPair; +pub type AccountPublicKey = Ed25519PublicKey; +pub type AccountPrivateKey = Ed25519PrivateKey; + +pub type NetworkKeyPair = Ed25519KeyPair; +pub type NetworkPublicKey = Ed25519PublicKey; +pub type NetworkPrivateKey = Ed25519PrivateKey; + +pub type DefaultHash = Blake2b256; + + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum PublicKey { + Ed25519(Ed25519PublicKeyAsBytes), + Secp256k1(Secp256k1PublicKeyAsBytes), + Secp256r1(Secp256r1PublicKeyAsBytes), + ZkLogin(ZkLoginPublicIdentifier), + Passkey(Secp256r1PublicKeyAsBytes), +} + +/// A wrapper struct to retrofit in [enum PublicKey] for zkLogin. +/// Useful to construct [struct MultiSigPublicKey]. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ZkLoginPublicIdentifier(pub Vec); // #[schemars(with = "Base64")] + +impl ZkLoginPublicIdentifier { + /// Consists of iss_bytes_len || iss_bytes || padded_32_byte_address_seed. + pub fn new(iss: &str, address_seed: &Bn254FrElement) -> IotaResult { + let mut bytes = Vec::new(); + let iss_bytes = iss.as_bytes(); + bytes.extend([iss_bytes.len() as u8]); + bytes.extend(iss_bytes); + bytes.extend(address_seed.padded()); + + Ok(Self(bytes)) + } +} +impl AsRef<[u8]> for PublicKey { + fn as_ref(&self) -> &[u8] { + match self { + PublicKey::Ed25519(pk) => &pk.0, + PublicKey::Secp256k1(pk) => &pk.0, + PublicKey::Secp256r1(pk) => &pk.0, + PublicKey::ZkLogin(z) => &z.0, + PublicKey::Passkey(pk) => &pk.0, + } + } +} + +impl EncodeDecodeBase64 for PublicKey { + fn encode_base64(&self) -> String { + let mut bytes: Vec = Vec::new(); + bytes.extend_from_slice(&[self.flag()]); + bytes.extend_from_slice(self.as_ref()); + Base64::encode(&bytes[..]) + } + + fn decode_base64(value: &str) -> FastCryptoResult { + let bytes = Base64::decode(value)?; + match bytes.first() { + Some(x) => { + if x == &SignatureScheme::ED25519.flag() { + let pk: Ed25519PublicKey = + Ed25519PublicKey::from_bytes(bytes.get(1..).ok_or( + FastCryptoError::InputLengthWrong(Ed25519PublicKey::LENGTH + 1), + )?)?; + Ok(PublicKey::Ed25519((&pk).into())) + } else if x == &SignatureScheme::Secp256k1.flag() { + let pk = Secp256k1PublicKey::from_bytes(bytes.get(1..).ok_or( + FastCryptoError::InputLengthWrong(Secp256k1PublicKey::LENGTH + 1), + )?)?; + Ok(PublicKey::Secp256k1((&pk).into())) + } else if x == &SignatureScheme::Secp256r1.flag() { + let pk = Secp256r1PublicKey::from_bytes(bytes.get(1..).ok_or( + FastCryptoError::InputLengthWrong(Secp256r1PublicKey::LENGTH + 1), + )?)?; + Ok(PublicKey::Secp256r1((&pk).into())) + } else if x == &SignatureScheme::PasskeyAuthenticator.flag() { + let pk = Secp256r1PublicKey::from_bytes(bytes.get(1..).ok_or( + FastCryptoError::InputLengthWrong(Secp256r1PublicKey::LENGTH + 1), + )?)?; + Ok(PublicKey::Passkey((&pk).into())) + } else { + Err(FastCryptoError::InvalidInput) + } + } + _ => Err(FastCryptoError::InvalidInput), + } + } +} + +impl PublicKey { + pub fn flag(&self) -> u8 { + self.scheme().flag() + } + + pub fn scheme(&self) -> SignatureScheme { + match self { + PublicKey::Ed25519(_) => SignatureScheme::ED25519, // Equals Ed25519IotaSignature::SCHEME + PublicKey::Secp256k1(_) => SignatureScheme::Secp256k1, // Equals Secp256k1IotaSignature::SCHEME + PublicKey::Secp256r1(_) => SignatureScheme::Secp256r1, // Equals Secp256r1IotaSignature::SCHEME + PublicKey::ZkLogin(_) => SignatureScheme::ZkLoginAuthenticator, + PublicKey::Passkey(_) => SignatureScheme::PasskeyAuthenticator, + } + } + + pub fn try_from_bytes( + curve: SignatureScheme, + key_bytes: &[u8], + ) -> Result { + match curve { + SignatureScheme::ED25519 => Ok(PublicKey::Ed25519( + (&Ed25519PublicKey::from_bytes(key_bytes)?).into(), + )), + SignatureScheme::Secp256k1 => Ok(PublicKey::Secp256k1( + (&Secp256k1PublicKey::from_bytes(key_bytes)?).into(), + )), + SignatureScheme::Secp256r1 => Ok(PublicKey::Secp256r1( + (&Secp256r1PublicKey::from_bytes(key_bytes)?).into(), + )), + SignatureScheme::PasskeyAuthenticator => Ok(PublicKey::Passkey( + (&Secp256r1PublicKey::from_bytes(key_bytes)?).into(), + )), + _ => Err(eyre::eyre!("Unsupported curve")), + } + } +} + +pub trait IotaPublicKey: VerifyingKey { + const SIGNATURE_SCHEME: SignatureScheme; +} + +// This struct exists due to the limitations of the `enum_dispatch` library. +// +pub trait IotaSignatureInner: Sized + ToFromBytes + PartialEq + Eq + Hash { + type Sig: Authenticator; + type PubKey: VerifyingKey + IotaPublicKey; + type KeyPair: KeypairTraits; + + const LENGTH: usize = Self::Sig::LENGTH + Self::PubKey::LENGTH + 1; + const SCHEME: SignatureScheme = Self::PubKey::SIGNATURE_SCHEME; + + /// Returns the deserialized signature and deserialized pubkey. + fn get_verification_inputs(&self) -> IotaResult<(Self::Sig, Self::PubKey)> { + let pk = Self::PubKey::from_bytes(self.public_key_bytes()) + .map_err(|_| IotaError::KeyConversion("Invalid public key".to_string()))?; + + // deserialize the signature + let signature = Self::Sig::from_bytes(self.signature_bytes()).map_err(|_| { + IotaError::InvalidSignature { + error: "Fail to get pubkey and sig".to_string(), + } + })?; + + Ok((signature, pk)) + } + + fn new(kp: &Self::KeyPair, message: &[u8]) -> Self { + let sig = Signer::sign(kp, message); + + let mut signature_bytes: Vec = Vec::new(); + signature_bytes + .extend_from_slice(&[::SIGNATURE_SCHEME.flag()]); + signature_bytes.extend_from_slice(sig.as_ref()); + signature_bytes.extend_from_slice(kp.public().as_ref()); + Self::from_bytes(&signature_bytes[..]) + .expect("Serialized signature did not have expected size") + } +} + +/// Defines the compressed version of the public key that we pass around +/// in IOTA. +#[serde_as] +#[derive( +Copy, +Clone, +PartialEq, +Eq, +Hash, +PartialOrd, +Ord, +Serialize, +Deserialize, +Debug // schemars::JsonSchema and AsRef are omitted here, having Debug instead +)] +pub struct AuthorityPublicKeyBytes( + #[serde_as(as = "Readable")] + pub [u8; AuthorityPublicKey::LENGTH], +); + + +#[derive(Clone, Copy, Deserialize, Serialize, Debug, EnumString, strum::Display)] +#[strum(serialize_all = "lowercase")] +pub enum SignatureScheme { + ED25519, + Secp256k1, + Secp256r1, + BLS12381, // This is currently not supported for user Iota Address. + MultiSig, + ZkLoginAuthenticator, + PasskeyAuthenticator, +} + +impl SignatureScheme { + pub fn flag(&self) -> u8 { + match self { + SignatureScheme::ED25519 => 0x00, + SignatureScheme::Secp256k1 => 0x01, + SignatureScheme::Secp256r1 => 0x02, + SignatureScheme::MultiSig => 0x03, + SignatureScheme::BLS12381 => 0x04, // This is currently not supported for user Iota + // Address. + SignatureScheme::ZkLoginAuthenticator => 0x05, + SignatureScheme::PasskeyAuthenticator => 0x06, + } + } + + /// Takes as input an hasher and updates it with a flag byte if the input + /// scheme is not ED25519; it does nothing otherwise. + pub fn update_hasher_with_flag(&self, hasher: &mut DefaultHash) { + match self { + SignatureScheme::ED25519 => (), + _ => hasher.update([self.flag()]), + }; + } + + pub fn from_flag(flag: &str) -> Result { + let byte_int = flag + .parse::() + .map_err(|_| IotaError::KeyConversion("Invalid key scheme".to_string()))?; + Self::from_flag_byte(&byte_int) + } + + pub fn from_flag_byte(byte_int: &u8) -> Result { + match byte_int { + 0x00 => Ok(SignatureScheme::ED25519), + 0x01 => Ok(SignatureScheme::Secp256k1), + 0x02 => Ok(SignatureScheme::Secp256r1), + 0x03 => Ok(SignatureScheme::MultiSig), + 0x04 => Ok(SignatureScheme::BLS12381), + 0x05 => Ok(SignatureScheme::ZkLoginAuthenticator), + 0x06 => Ok(SignatureScheme::PasskeyAuthenticator), + _ => Err(IotaError::KeyConversion("Invalid key scheme".to_string())), + } + } +} + +// Enums for signature scheme signatures +#[enum_dispatch] +#[derive(Clone, JsonSchema, Debug, PartialEq, Eq, Hash)] +pub enum Signature { + Ed25519IotaSignature, + Secp256k1IotaSignature, + Secp256r1IotaSignature, +} + +impl Serialize for Signature { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let bytes = self.as_ref(); + + if serializer.is_human_readable() { + let s = Base64::encode(bytes); + serializer.serialize_str(&s) + } else { + serializer.serialize_bytes(bytes) + } + } +} + +impl<'de> Deserialize<'de> for Signature { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use serde::de::Error; + + let bytes = if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + Base64::decode(&s).map_err(|e| Error::custom(e.to_string()))? + } else { + let data: Vec = Vec::deserialize(deserializer)?; + data + }; + + Self::from_bytes(&bytes).map_err(|e| Error::custom(e.to_string())) + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + match self { + Signature::Ed25519IotaSignature(sig) => sig.as_ref(), + Signature::Secp256k1IotaSignature(sig) => sig.as_ref(), + Signature::Secp256r1IotaSignature(sig) => sig.as_ref(), + } + } +} +impl AsMut<[u8]> for Signature { + fn as_mut(&mut self) -> &mut [u8] { + match self { + Signature::Ed25519IotaSignature(sig) => sig.as_mut(), + Signature::Secp256k1IotaSignature(sig) => sig.as_mut(), + Signature::Secp256r1IotaSignature(sig) => sig.as_mut(), + } + } +} + +impl ToFromBytes for Signature { + fn from_bytes(bytes: &[u8]) -> Result { + match bytes.first() { + Some(x) => { + if x == &Ed25519IotaSignature::SCHEME.flag() { + Ok(::from_bytes(bytes)?.into()) + } else if x == &Secp256k1IotaSignature::SCHEME.flag() { + Ok(::from_bytes(bytes)?.into()) + } else if x == &Secp256r1IotaSignature::SCHEME.flag() { + Ok(::from_bytes(bytes)?.into()) + } else { + Err(FastCryptoError::InvalidInput) + } + } + _ => Err(FastCryptoError::InvalidInput), + } + } +} + +impl FromStr for Signature { + type Err = eyre::Report; + fn from_str(s: &str) -> Result { + Self::decode_base64(s).map_err(|e| eyre::eyre!("Fail to decode base64 {}", e.to_string())) + } +} + +// Ed25519 Iota Signature port +// + +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, AsRef, AsMut)] +#[as_ref(forward)] +#[as_mut(forward)] +pub struct Ed25519IotaSignature( + #[schemars(with = "Base64")] + #[serde_as(as = "Readable")] + [u8; Ed25519PublicKey::LENGTH + Ed25519Signature::LENGTH + 1], +); + +// Implementation useful for simplify testing when mock signature is needed +impl Default for Ed25519IotaSignature { + fn default() -> Self { + Self([0; Ed25519PublicKey::LENGTH + Ed25519Signature::LENGTH + 1]) + } +} + +impl ToFromBytes for Ed25519IotaSignature { + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != Self::LENGTH { + return Err(FastCryptoError::InputLengthWrong(Self::LENGTH)); + } + let mut sig_bytes = [0; Self::LENGTH]; + sig_bytes.copy_from_slice(bytes); + Ok(Self(sig_bytes)) + } +} + +impl IotaSignatureInner for Ed25519IotaSignature { + type Sig = Ed25519Signature; + type PubKey = Ed25519PublicKey; + type KeyPair = Ed25519KeyPair; + const LENGTH: usize = Ed25519PublicKey::LENGTH + Ed25519Signature::LENGTH + 1; +} + +impl IotaPublicKey for Ed25519PublicKey { + const SIGNATURE_SCHEME: SignatureScheme = SignatureScheme::ED25519; +} + +// Secp256k1 Iota Signature port +// +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, AsRef, AsMut)] +#[as_ref(forward)] +#[as_mut(forward)] +pub struct Secp256k1IotaSignature( + #[schemars(with = "Base64")] + #[serde_as(as = "Readable")] + [u8; Secp256k1PublicKey::LENGTH + Secp256k1Signature::LENGTH + 1], +); + +impl ToFromBytes for Secp256k1IotaSignature { + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != Self::LENGTH { + return Err(FastCryptoError::InputLengthWrong(Self::LENGTH)); + } + let mut sig_bytes = [0; Self::LENGTH]; + sig_bytes.copy_from_slice(bytes); + Ok(Self(sig_bytes)) + } +} + +impl IotaSignatureInner for Secp256k1IotaSignature { + type Sig = Secp256k1Signature; + type PubKey = Secp256k1PublicKey; + type KeyPair = Secp256k1KeyPair; + const LENGTH: usize = Secp256k1PublicKey::LENGTH + Secp256k1Signature::LENGTH + 1; +} + +impl IotaPublicKey for Secp256k1PublicKey { + const SIGNATURE_SCHEME: SignatureScheme = SignatureScheme::Secp256k1; +} + +// Secp256r1 Iota Signature port +// +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, AsRef, AsMut)] +#[as_ref(forward)] +#[as_mut(forward)] +pub struct Secp256r1IotaSignature( + #[schemars(with = "Base64")] + #[serde_as(as = "Readable")] + [u8; Secp256r1PublicKey::LENGTH + Secp256r1Signature::LENGTH + 1], +); + +impl ToFromBytes for Secp256r1IotaSignature { + fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != Self::LENGTH { + return Err(FastCryptoError::InputLengthWrong(Self::LENGTH)); + } + let mut sig_bytes = [0; Self::LENGTH]; + sig_bytes.copy_from_slice(bytes); + Ok(Self(sig_bytes)) + } +} + +impl IotaSignatureInner for Secp256r1IotaSignature { + type Sig = Secp256r1Signature; + type PubKey = Secp256r1PublicKey; + type KeyPair = Secp256r1KeyPair; + const LENGTH: usize = Secp256r1PublicKey::LENGTH + Secp256r1Signature::LENGTH + 1; +} + +impl IotaPublicKey for Secp256r1PublicKey { + const SIGNATURE_SCHEME: SignatureScheme = SignatureScheme::Secp256r1; +} + +#[enum_dispatch(Signature)] +pub trait IotaSignature: Sized + ToFromBytes { + fn signature_bytes(&self) -> &[u8]; + fn public_key_bytes(&self) -> &[u8]; + fn scheme(&self) -> SignatureScheme; + + fn verify_secure( + &self, + value: &IntentMessage, + author: IotaAddress, + scheme: SignatureScheme, + ) -> IotaResult<()> + where + T: Serialize; +} + +impl IotaSignature for S { + fn signature_bytes(&self) -> &[u8] { + // Access array slice is safe because the array bytes is initialized as + // flag || signature || pubkey with its defined length. + &self.as_ref()[1..1 + S::Sig::LENGTH] + } + + fn public_key_bytes(&self) -> &[u8] { + // Access array slice is safe because the array bytes is initialized as + // flag || signature || pubkey with its defined length. + &self.as_ref()[S::Sig::LENGTH + 1..] + } + + fn scheme(&self) -> SignatureScheme { + S::PubKey::SIGNATURE_SCHEME + } + + fn verify_secure( + &self, + value: &IntentMessage, + author: IotaAddress, + scheme: SignatureScheme, + ) -> Result<(), IotaError> + where + T: Serialize, + { + let mut hasher = DefaultHash::default(); + hasher.update(bcs::to_bytes(&value).expect("Message serialization should not fail")); + let digest = hasher.finalize().digest; + + let (sig, pk) = &self.get_verification_inputs()?; + match scheme { + SignatureScheme::ZkLoginAuthenticator => {} // Pass this check because zk login does + // not derive address from pubkey. + _ => { + let address = IotaAddress::from(pk); + if author != address { + return Err(IotaError::IncorrectSigner { + error: format!( + "Incorrect signer, expected {:?}, got {:?}", + author, address + ), + }); + } + } + } + + pk.verify(&digest, sig) + .map_err(|e| IotaError::InvalidSignature { + error: format!("Fail to verify user sig {}", e), + }) + } +} diff --git a/identity_iota_interaction/src/sdk_types/iota_types/digests.rs b/identity_iota_interaction/src/sdk_types/iota_types/digests.rs new file mode 100644 index 0000000000..9e81e71762 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/digests.rs @@ -0,0 +1,468 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt; + +use serde::Deserialize; +use serde::Serialize; +use serde_with::{serde_as, Bytes}; + +use fastcrypto::encoding::{Base58, Encoding}; + +use super::iota_serde::Readable; + +/// A representation of a 32 byte digest +#[serde_as] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct Digest( + #[serde_as(as = "Readable")] + [u8; 32], +); + +impl Digest { + pub const ZERO: Self = Digest([0; 32]); + + pub const fn new(digest: [u8; 32]) -> Self { + Self(digest) + } + + pub fn generate(mut rng: R) -> Self { + let mut bytes = [0; 32]; + rng.fill_bytes(&mut bytes); + Self(bytes) + } + + pub fn random() -> Self { + Self::generate(rand::thread_rng()) + } + + pub const fn inner(&self) -> &[u8; 32] { + &self.0 + } + + pub const fn into_inner(self) -> [u8; 32] { + self.0 + } + + pub fn next_lexicographical(&self) -> Option { + let mut next_digest = *self; + let pos = next_digest.0.iter().rposition(|&byte| byte != 255)?; + next_digest.0[pos] += 1; + next_digest + .0 + .iter_mut() + .skip(pos + 1) + .for_each(|byte| *byte = 0); + Some(next_digest) + } +} + +impl AsRef<[u8]> for Digest { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl AsRef<[u8; 32]> for Digest { + fn as_ref(&self) -> &[u8; 32] { + &self.0 + } +} + +impl fmt::Display for Digest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // TODO avoid the allocation + f.write_str(&Base58::encode(self.0)) + } +} + +impl fmt::Debug for Digest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl fmt::LowerHex for Digest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + write!(f, "0x")?; + } + + for byte in self.0 { + write!(f, "{:02x}", byte)?; + } + + Ok(()) + } +} + +impl fmt::UpperHex for Digest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + write!(f, "0x")?; + } + + for byte in self.0 { + write!(f, "{:02X}", byte)?; + } + + Ok(()) + } +} + + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct CheckpointContentsDigest(Digest); + +impl CheckpointContentsDigest { + pub const fn new(digest: [u8; 32]) -> Self { + Self(Digest::new(digest)) + } + + pub fn generate(rng: R) -> Self { + Self(Digest::generate(rng)) + } + + pub fn random() -> Self { + Self(Digest::random()) + } + + pub const fn inner(&self) -> &[u8; 32] { + self.0.inner() + } + + pub const fn into_inner(self) -> [u8; 32] { + self.0.into_inner() + } + + pub fn base58_encode(&self) -> String { + Base58::encode(self.0) + } + + pub fn next_lexicographical(&self) -> Option { + self.0.next_lexicographical().map(Self) + } +} + +impl AsRef<[u8]> for CheckpointContentsDigest { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl AsRef<[u8; 32]> for CheckpointContentsDigest { + fn as_ref(&self) -> &[u8; 32] { + self.0.as_ref() + } +} + +impl From for [u8; 32] { + fn from(digest: CheckpointContentsDigest) -> Self { + digest.into_inner() + } +} + +impl From<[u8; 32]> for CheckpointContentsDigest { + fn from(digest: [u8; 32]) -> Self { + Self::new(digest) + } +} + +impl fmt::Display for CheckpointContentsDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl fmt::Debug for CheckpointContentsDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("CheckpointContentsDigest") + .field(&self.0) + .finish() + } +} + +impl std::str::FromStr for CheckpointContentsDigest { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut result = [0; 32]; + let buffer = Base58::decode(s).map_err(|e| anyhow::anyhow!(e))?; + if buffer.len() != 32 { + return Err(anyhow::anyhow!("Invalid digest length. Expected 32 bytes")); + } + result.copy_from_slice(&buffer); + Ok(CheckpointContentsDigest::new(result)) + } +} + +impl fmt::LowerHex for CheckpointContentsDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + +impl fmt::UpperHex for CheckpointContentsDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::UpperHex::fmt(&self.0, f) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct TransactionDigest(Digest); + +impl Default for TransactionDigest { + fn default() -> Self { + Self::ZERO + } +} + +impl TransactionDigest { + pub const ZERO: Self = Self(Digest::ZERO); + + pub const fn new(digest: [u8; 32]) -> Self { + Self(Digest::new(digest)) + } + + /// A digest we use to signify the parent transaction was the genesis, + /// ie. for an object there is no parent digest. + /// Note that this is not the same as the digest of the genesis transaction, + /// which cannot be known ahead of time. + // TODO(https://github.com/iotaledger/iota/issues/65): we can pick anything here + pub const fn genesis_marker() -> Self { + Self::ZERO + } + + pub fn generate(rng: R) -> Self { + Self(Digest::generate(rng)) + } + + pub fn random() -> Self { + Self(Digest::random()) + } + + pub fn inner(&self) -> &[u8; 32] { + self.0.inner() + } + + pub fn into_inner(self) -> [u8; 32] { + self.0.into_inner() + } + + pub fn base58_encode(&self) -> String { + Base58::encode(self.0) + } + + pub fn next_lexicographical(&self) -> Option { + self.0.next_lexicographical().map(Self) + } +} + +impl fmt::Display for TransactionDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl fmt::Debug for TransactionDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("TransactionDigest").field(&self.0).finish() + } +} + +impl fmt::LowerHex for TransactionDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + +impl fmt::UpperHex for TransactionDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::UpperHex::fmt(&self.0, f) + } +} + +impl TryFrom<&[u8]> for TransactionDigest { + type Error = super::error::IotaError; + + fn try_from(bytes: &[u8]) -> Result { + let arr: [u8; 32] = bytes + .try_into() + .map_err(|_| super::error::IotaError::InvalidTransactionDigest)?; + Ok(Self::new(arr)) + } +} + +impl std::str::FromStr for TransactionDigest { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut result = [0; 32]; + let buffer = Base58::decode(s).map_err(|e| anyhow::anyhow!(e))?; + if buffer.len() != 32 { + return Err(anyhow::anyhow!("Invalid digest length. Expected 32 bytes")); + } + result.copy_from_slice(&buffer); + Ok(TransactionDigest::new(result)) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct TransactionEffectsDigest(Digest); + +impl TransactionEffectsDigest { + pub const ZERO: Self = Self(Digest::ZERO); + + pub const fn new(digest: [u8; 32]) -> Self { + Self(Digest::new(digest)) + } + + pub fn generate(rng: R) -> Self { + Self(Digest::generate(rng)) + } + + pub fn random() -> Self { + Self(Digest::random()) + } + + pub const fn inner(&self) -> &[u8; 32] { + self.0.inner() + } + + pub const fn into_inner(self) -> [u8; 32] { + self.0.into_inner() + } + + pub fn base58_encode(&self) -> String { + Base58::encode(self.0) + } + + pub fn next_lexicographical(&self) -> Option { + self.0.next_lexicographical().map(Self) + } +} + +impl AsRef<[u8]> for TransactionEffectsDigest { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl AsRef<[u8; 32]> for TransactionEffectsDigest { + fn as_ref(&self) -> &[u8; 32] { + self.0.as_ref() + } +} + +impl From for [u8; 32] { + fn from(digest: TransactionEffectsDigest) -> Self { + digest.into_inner() + } +} + +impl From<[u8; 32]> for TransactionEffectsDigest { + fn from(digest: [u8; 32]) -> Self { + Self::new(digest) + } +} + +impl fmt::Display for TransactionEffectsDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl fmt::Debug for TransactionEffectsDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("TransactionEffectsDigest") + .field(&self.0) + .finish() + } +} + +impl fmt::LowerHex for TransactionEffectsDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + +impl fmt::UpperHex for TransactionEffectsDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::UpperHex::fmt(&self.0, f) + } +} + +#[serde_as] +#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize)] +pub struct TransactionEventsDigest(Digest); + +impl TransactionEventsDigest { + pub const ZERO: Self = Self(Digest::ZERO); + + pub const fn new(digest: [u8; 32]) -> Self { + Self(Digest::new(digest)) + } + + pub fn random() -> Self { + Self(Digest::random()) + } + + pub fn next_lexicographical(&self) -> Option { + self.0.next_lexicographical().map(Self) + } + + pub fn into_inner(self) -> [u8; 32] { + self.0.into_inner() + } +} + +impl fmt::Debug for TransactionEventsDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("TransactionEventsDigest") + .field(&self.0) + .finish() + } +} + +impl AsRef<[u8]> for TransactionEventsDigest { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl AsRef<[u8; 32]> for TransactionEventsDigest { + fn as_ref(&self) -> &[u8; 32] { + self.0.as_ref() + } +} + +impl std::str::FromStr for TransactionEventsDigest { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut result = [0; 32]; + let buffer = Base58::decode(s).map_err(|e| anyhow::anyhow!(e))?; + if buffer.len() != 32 { + return Err(anyhow::anyhow!("Invalid digest length. Expected 32 bytes")); + } + result.copy_from_slice(&buffer); + Ok(Self::new(result)) + } +} + +// Each object has a unique digest +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct ObjectDigest(Digest); + +impl fmt::Display for ObjectDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl fmt::Debug for ObjectDigest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "o#{}", self.0) + } +} diff --git a/identity_iota_interaction/src/sdk_types/iota_types/dynamic_field.rs b/identity_iota_interaction/src/sdk_types/iota_types/dynamic_field.rs new file mode 100644 index 0000000000..f8827f1b4e --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/dynamic_field.rs @@ -0,0 +1,156 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + fmt, + fmt::{Display, Formatter}, +}; + +use fastcrypto::{encoding::Base58}; + +use crate::ident_str; + +use super::super::move_core_types::{ + // annotated_value::{MoveStruct, MoveValue}, + identifier::IdentStr, + language_storage::{StructTag, TypeTag}, +}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use serde_with::{serde_as, DisplayFromStr}; + +use super::{ + base_types::{ObjectID, SequenceNumber}, + digests::{ObjectDigest}, + error::{IotaError, IotaResult}, + id::UID, + iota_serde::{IotaTypeTag, Readable}, + //object::Object, + //storage::ObjectStore, + IOTA_FRAMEWORK_ADDRESS, +}; + +const DYNAMIC_FIELD_MODULE_NAME: &IdentStr = ident_str!("dynamic_field"); +const DYNAMIC_FIELD_FIELD_STRUCT_NAME: &IdentStr = ident_str!("Field"); + +const DYNAMIC_OBJECT_FIELD_MODULE_NAME: &IdentStr = ident_str!("dynamic_object_field"); +const DYNAMIC_OBJECT_FIELD_WRAPPER_STRUCT_NAME: &IdentStr = ident_str!("Wrapper"); + +/// Rust version of the Move iota::dynamic_field::Field type +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Field { + pub id: UID, + pub name: N, + pub value: V, +} + +#[serde_as] +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DynamicFieldInfo { + pub name: DynamicFieldName, + #[serde_as(as = "Readable")] + pub bcs_name: Vec, + pub type_: DynamicFieldType, + pub object_type: String, + pub object_id: ObjectID, + pub version: SequenceNumber, + pub digest: ObjectDigest, +} + +#[serde_as] +#[derive(Clone, Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DynamicFieldName { + #[serde_as(as = "Readable")] + pub type_: TypeTag, + // Bincode does not like serde_json::Value, rocksdb will not insert the value without + // serializing value as string. TODO: investigate if this can be removed after switch to + // BCS. + #[serde_as(as = "Readable<_, DisplayFromStr>")] + pub value: Value, +} + +impl Display for DynamicFieldName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.type_, self.value) + } +} + +#[derive(Clone, Serialize, Deserialize, Ord, PartialOrd, Eq, PartialEq, Debug)] +pub enum DynamicFieldType { + #[serde(rename_all = "camelCase")] + DynamicField, + DynamicObject, +} + +impl Display for DynamicFieldType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DynamicFieldType::DynamicField => write!(f, "DynamicField"), + DynamicFieldType::DynamicObject => write!(f, "DynamicObject"), + } + } +} + +impl DynamicFieldInfo { + pub fn is_dynamic_field(tag: &StructTag) -> bool { + tag.address == IOTA_FRAMEWORK_ADDRESS + && tag.module.as_ident_str() == DYNAMIC_FIELD_MODULE_NAME + && tag.name.as_ident_str() == DYNAMIC_FIELD_FIELD_STRUCT_NAME + } + + pub fn is_dynamic_object_field_wrapper(tag: &StructTag) -> bool { + tag.address == IOTA_FRAMEWORK_ADDRESS + && tag.module.as_ident_str() == DYNAMIC_OBJECT_FIELD_MODULE_NAME + && tag.name.as_ident_str() == DYNAMIC_OBJECT_FIELD_WRAPPER_STRUCT_NAME + } + + pub fn dynamic_field_type(key: TypeTag, value: TypeTag) -> StructTag { + StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + name: DYNAMIC_FIELD_FIELD_STRUCT_NAME.to_owned(), + module: DYNAMIC_FIELD_MODULE_NAME.to_owned(), + type_params: vec![key, value], + } + } + + pub fn dynamic_object_field_wrapper(key: TypeTag) -> StructTag { + StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + module: DYNAMIC_OBJECT_FIELD_MODULE_NAME.to_owned(), + name: DYNAMIC_OBJECT_FIELD_WRAPPER_STRUCT_NAME.to_owned(), + type_params: vec![key], + } + } + + pub fn try_extract_field_name( + tag: &StructTag, + type_: &DynamicFieldType, + ) -> IotaResult { + match (type_, tag.type_params.first()) { + (DynamicFieldType::DynamicField, Some(name_type)) => Ok(name_type.clone()), + (DynamicFieldType::DynamicObject, Some(TypeTag::Struct(s))) => Ok(s + .type_params + .first() + .ok_or_else(|| IotaError::ObjectDeserialization { + error: format!("Error extracting dynamic object name from object: {tag}"), + })? + .clone()), + _ => Err(IotaError::ObjectDeserialization { + error: format!("Error extracting dynamic object name from object: {tag}"), + }), + } + } + + pub fn try_extract_field_value(tag: &StructTag) -> IotaResult { + match tag.type_params.last() { + Some(value_type) => Ok(value_type.clone()), + None => Err(IotaError::ObjectDeserialization { + error: format!("Error extracting dynamic object value from object: {tag}"), + }), + } + } +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/error.rs b/identity_iota_interaction/src/sdk_types/iota_types/error.rs new file mode 100644 index 0000000000..afcb184015 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/error.rs @@ -0,0 +1,943 @@ +// Copyright (c) 2021, Facebook, Inc. and its affiliates +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{collections::BTreeMap, fmt::Debug}; + +use serde::{Deserialize, Serialize}; +use strum::{AsRefStr, IntoStaticStr}; +use thiserror::Error; + +use super::super::{ + rpc_types::CheckpointSequenceNumber, +}; +use super::{ + base_types::*, + digests::{ObjectDigest, TransactionDigest, TransactionEffectsDigest, TransactionEventsDigest, + CheckpointContentsDigest}, + execution_status::{CommandArgumentError, ExecutionFailureStatus}, + object::Owner, +}; + +pub const TRANSACTION_NOT_FOUND_MSG_PREFIX: &str = "Could not find the referenced transaction"; +pub const TRANSACTIONS_NOT_FOUND_MSG_PREFIX: &str = "Could not find the referenced transactions"; + +#[macro_export] +macro_rules! fp_bail { + ($e:expr) => { + return Err($e) + }; +} + +#[macro_export(local_inner_macros)] +macro_rules! fp_ensure { + ($cond:expr, $e:expr) => { + if !($cond) { + fp_bail!($e); + } + }; +} + +#[macro_export] +macro_rules! exit_main { + ($result:expr) => { + match $result { + Ok(_) => (), + Err(err) => { + let err = format!("{:?}", err); + println!("{}", err.bold().red()); + std::process::exit(1); + } + } + }; +} + +#[macro_export] +macro_rules! make_invariant_violation { + ($($args:expr),* $(,)?) => {{ + if cfg!(debug_assertions) { + panic!($($args),*) + } + ExecutionError::invariant_violation(format!($($args),*)) + }} +} + +#[macro_export] +macro_rules! invariant_violation { + ($($args:expr),* $(,)?) => { + return Err(make_invariant_violation!($($args),*).into()) + }; +} + +#[macro_export] +macro_rules! assert_invariant { + ($cond:expr, $($args:expr),* $(,)?) => {{ + if !$cond { + invariant_violation!($($args),*) + } + }}; +} + +#[derive( + Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Error, Hash, AsRefStr, IntoStaticStr, +)] +pub enum UserInputError { + #[error("Mutable object {object_id} cannot appear more than one in one transaction")] + MutableObjectUsedMoreThanOnce { object_id: ObjectID }, + #[error("Wrong number of parameters for the transaction")] + ObjectInputArityViolation, + #[error( + "Could not find the referenced object {:?} at version {:?}", + object_id, + version + )] + ObjectNotFound { + object_id: ObjectID, + version: Option, + }, + #[error( + "Object {provided_obj_ref:?} is not available for consumption, its current version: {current_version:?}" + )] + ObjectVersionUnavailableForConsumption { + provided_obj_ref: ObjectRef, + current_version: SequenceNumber, + }, + #[error("Package verification failed: {err:?}")] + PackageVerificationTimedout { err: String }, + #[error("Dependent package not found on-chain: {package_id:?}")] + DependentPackageNotFound { package_id: ObjectID }, + #[error("Mutable parameter provided, immutable parameter expected")] + ImmutableParameterExpected { object_id: ObjectID }, + #[error("Size limit exceeded: {limit} is {value}")] + SizeLimitExceeded { limit: String, value: String }, + #[error( + "Object {child_id:?} is owned by object {parent_id:?}. \ + Objects owned by other objects cannot be used as input arguments" + )] + InvalidChildObjectArgument { + child_id: ObjectID, + parent_id: ObjectID, + }, + #[error( + "Invalid Object digest for object {object_id:?}. Expected digest : {expected_digest:?}" + )] + InvalidObjectDigest { + object_id: ObjectID, + expected_digest: ObjectDigest, + }, + #[error("Sequence numbers above the maximal value are not usable for transfers")] + InvalidSequenceNumber, + #[error("A move object is expected, instead a move package is passed: {object_id}")] + MovePackageAsObject { object_id: ObjectID }, + #[error("A move package is expected, instead a move object is passed: {object_id}")] + MoveObjectAsPackage { object_id: ObjectID }, + #[error("Transaction was not signed by the correct sender: {}", error)] + IncorrectUserSignature { error: String }, + + #[error("Object used as shared is not shared")] + NotSharedObject, + #[error("The transaction inputs contain duplicated ObjectRef's")] + DuplicateObjectRefInput, + + // Gas related errors + #[error("Transaction gas payment missing")] + MissingGasPayment, + #[error("Gas object is not an owned object with owner: {:?}", owner)] + GasObjectNotOwnedObject { owner: Owner }, + #[error("Gas budget: {:?} is higher than max: {:?}", gas_budget, max_budget)] + GasBudgetTooHigh { gas_budget: u64, max_budget: u64 }, + #[error("Gas budget: {:?} is lower than min: {:?}", gas_budget, min_budget)] + GasBudgetTooLow { gas_budget: u64, min_budget: u64 }, + #[error( + "Balance of gas object {:?} is lower than the needed amount: {:?}", + gas_balance, + needed_gas_amount + )] + GasBalanceTooLow { + gas_balance: u128, + needed_gas_amount: u128, + }, + #[error("Transaction kind does not support Sponsored Transaction")] + UnsupportedSponsoredTransactionKind, + #[error( + "Gas price {:?} under reference gas price (RGP) {:?}", + gas_price, + reference_gas_price + )] + GasPriceUnderRGP { + gas_price: u64, + reference_gas_price: u64, + }, + #[error("Gas price cannot exceed {:?} nanos", max_gas_price)] + GasPriceTooHigh { max_gas_price: u64 }, + #[error("Object {object_id} is not a gas object")] + InvalidGasObject { object_id: ObjectID }, + #[error("Gas object does not have enough balance to cover minimal gas spend")] + InsufficientBalanceToCoverMinimalGas, + + #[error( + "Could not find the referenced object {:?} as the asked version {:?} is higher than the latest {:?}", + object_id, + asked_version, + latest_version + )] + ObjectSequenceNumberTooHigh { + object_id: ObjectID, + asked_version: SequenceNumber, + latest_version: SequenceNumber, + }, + #[error("Object deleted at reference {:?}", object_ref)] + ObjectDeleted { object_ref: ObjectRef }, + #[error("Invalid Batch Transaction: {}", error)] + InvalidBatchTransaction { error: String }, + #[error("This Move function is currently disabled and not available for call")] + BlockedMoveFunction, + #[error("Empty input coins for Pay related transaction")] + EmptyInputCoins, + + #[error( + "IOTA payment transactions use first input coin for gas payment, but found a different gas object" + )] + UnexpectedGasPaymentObject, + + #[error("Wrong initial version given for shared object")] + SharedObjectStartingVersionMismatch, + + #[error( + "Attempt to transfer object {object_id} that does not have public transfer. Object transfer must be done instead using a distinct Move function call" + )] + TransferObjectWithoutPublicTransfer { object_id: ObjectID }, + + #[error( + "TransferObjects, MergeCoin, and Publish cannot have empty arguments. \ + If MakeMoveVec has empty arguments, it must have a type specified" + )] + EmptyCommandInput, + + #[error("Transaction is denied: {}", error)] + TransactionDenied { error: String }, + + #[error("Feature is not supported: {0}")] + Unsupported(String), + + #[error("Query transactions with move function input error: {0}")] + MoveFunctionInput(String), + + #[error("Verified checkpoint not found for sequence number: {0}")] + VerifiedCheckpointNotFound(CheckpointSequenceNumber), + + #[error("Verified checkpoint not found for digest: {0}")] + VerifiedCheckpointDigestNotFound(String), + + #[error("Latest checkpoint sequence number not found")] + LatestCheckpointSequenceNumberNotFound, + + #[error("Checkpoint contents not found for digest: {0}")] + CheckpointContentsNotFound(CheckpointContentsDigest), + + #[error("Genesis transaction not found")] + GenesisTransactionNotFound, + + #[error("Transaction {0} not found")] + TransactionCursorNotFound(u64), + + #[error( + "Object {:?} is a system object and cannot be accessed by user transactions", + object_id + )] + InaccessibleSystemObject { object_id: ObjectID }, + #[error( + "{max_publish_commands} max publish/upgrade commands allowed, {publish_count} provided" + )] + MaxPublishCountExceeded { + max_publish_commands: u64, + publish_count: u64, + }, + + #[error("Immutable parameter provided, mutable parameter expected")] + MutableParameterExpected { object_id: ObjectID }, + + #[error("Address {address:?} is denied for coin {coin_type}")] + AddressDeniedForCoin { + address: IotaAddress, + coin_type: String, + }, + + #[error("Commands following a command with Random can only be TransferObjects or MergeCoins")] + PostRandomCommandRestrictions, + + // Soft Bundle related errors + #[error( + "Number of transactions exceeds the maximum allowed ({:?}) in a Soft Bundle", + limit + )] + TooManyTransactionsInSoftBundle { limit: u64 }, + #[error("Transaction {:?} in Soft Bundle contains no shared objects", digest)] + NoSharedObject { digest: TransactionDigest }, + #[error("Transaction {:?} in Soft Bundle has already been executed", digest)] + AlreadyExecuted { digest: TransactionDigest }, + #[error("At least one certificate in Soft Bundle has already been processed")] + CertificateAlreadyProcessed, + #[error( + "Gas price for transaction {:?} in Soft Bundle mismatch: want {:?}, have {:?}", + digest, + expected, + actual + )] + GasPriceMismatch { + digest: TransactionDigest, + expected: u64, + actual: u64, + }, + + #[error("Coin type is globally paused for use: {coin_type}")] + CoinTypeGlobalPause { coin_type: String }, +} + +#[derive( + Eq, + PartialEq, + Clone, + Debug, + Serialize, + Deserialize, + Hash, + AsRefStr, + IntoStaticStr, + Error, +)] +#[serde(tag = "code", rename = "ObjectResponseError", rename_all = "camelCase")] +pub enum IotaObjectResponseError { + #[error("Object {:?} does not exist", object_id)] + NotExists { object_id: ObjectID }, + #[error("Cannot find dynamic field for parent object {:?}", parent_object_id)] + DynamicFieldNotFound { parent_object_id: ObjectID }, + #[error( + "Object has been deleted object_id: {:?} at version: {:?} in digest {:?}", + object_id, + version, + digest + )] + Deleted { + object_id: ObjectID, + /// Object version. + version: SequenceNumber, + /// Base64 string representing the object digest + digest: ObjectDigest, + }, + #[error("Unknown Error")] + Unknown, + #[error("Display Error: {:?}", error)] + Display { error: String }, + // TODO: also integrate IotaPastObjectResponse (VersionNotFound, VersionTooHigh) +} + +/// Custom error type for Iota. +#[derive( + Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Error, Hash, AsRefStr, IntoStaticStr, +)] +pub enum IotaError { + #[error("Error checking transaction input objects: {:?}", error)] + UserInput { error: UserInputError }, + + #[error("Error checking transaction object: {:?}", error)] + IotaObjectResponse { error: IotaObjectResponseError }, + + #[error("Expecting a single owner, shared ownership found")] + UnexpectedOwnerType, + + #[error("There are already {queue_len} transactions pending, above threshold of {threshold}")] + TooManyTransactionsPendingExecution { queue_len: usize, threshold: usize }, + + #[error("There are too many transactions pending in consensus")] + TooManyTransactionsPendingConsensus, + + #[error( + "Input {object_id} already has {queue_len} transactions pending, above threshold of {threshold}" + )] + TooManyTransactionsPendingOnObject { + object_id: ObjectID, + queue_len: usize, + threshold: usize, + }, + + #[error( + "Input {object_id} has a transaction {txn_age_sec} seconds old pending, above threshold of {threshold} seconds" + )] + TooOldTransactionPendingOnObject { + object_id: ObjectID, + txn_age_sec: u64, + threshold: u64, + }, + + #[error("Soft bundle must only contain transactions of UserTransaction kind")] + InvalidTxKindInSoftBundle, + + // Signature verification + #[error("Signature is not valid: {}", error)] + InvalidSignature { error: String }, + #[error("Required Signature from {expected} is absent {:?}", actual)] + SignerSignatureAbsent { + expected: String, + actual: Vec, + }, + #[error("Expect {expected} signer signatures but got {actual}")] + SignerSignatureNumberMismatch { expected: usize, actual: usize }, + #[error("Value was not signed by the correct sender: {}", error)] + IncorrectSigner { error: String }, + #[error( + "Value was not signed by a known authority. signer: {:?}, index: {:?}, committee: {committee}", + signer, + index + )] + UnknownSigner { + signer: Option, + index: Option, + committee: Box, // Committee is not available for wasm32 + }, + #[error( + "Validator {:?} responded multiple signatures for the same message, conflicting: {:?}", + signer, + conflicting_sig + )] + StakeAggregatorRepeatedSigner { + signer: AuthorityName, + conflicting_sig: bool, + }, + // TODO: Used for distinguishing between different occurrences of invalid signatures, to allow + // retries in some cases. + #[error( + "Signature is not valid, but a retry may result in a valid one: {}", + error + )] + PotentiallyTemporarilyInvalidSignature { error: String }, + + // Certificate verification and execution + #[error( + "Signature or certificate from wrong epoch, expected {expected_epoch}, got {actual_epoch}" + )] + WrongEpoch { + expected_epoch: EpochId, + actual_epoch: EpochId, + }, + #[error("Signatures in a certificate must form a quorum")] + CertificateRequiresQuorum, + #[error("Transaction certificate processing failed: {err}")] + ErrorWhileProcessingCertificate { err: String }, + #[error( + "Failed to get a quorum of signed effects when processing transaction: {effects_map:?}" + )] + QuorumFailedToGetEffectsQuorumWhenProcessingTransaction { + effects_map: BTreeMap, StakeUnit)>, + }, + #[error( + "Failed to verify Tx certificate with executed effects, error: {error:?}, validator: {validator_name:?}" + )] + FailedToVerifyTxCertWithExecutedEffects { + validator_name: AuthorityName, + error: String, + }, + #[error("Transaction is already finalized but with different user signatures")] + TxAlreadyFinalizedWithDifferentUserSigs, + + // Account access + #[error("Invalid authenticator")] + InvalidAuthenticator, + #[error("Invalid address")] + InvalidAddress, + #[error("Invalid transaction digest.")] + InvalidTransactionDigest, + + #[error("Invalid digest length. Expected {expected}, got {actual}")] + InvalidDigestLength { expected: usize, actual: usize }, + #[error("Invalid DKG message size")] + InvalidDkgMessageSize, + + #[error("Unexpected message.")] + UnexpectedMessage, + + // Move module publishing related errors + #[error("Failed to verify the Move module, reason: {error:?}.")] + ModuleVerificationFailure { error: String }, + #[error("Failed to deserialize the Move module, reason: {error:?}.")] + ModuleDeserializationFailure { error: String }, + #[error("Failed to publish the Move module(s), reason: {error}")] + ModulePublishFailure { error: String }, + #[error("Failed to build Move modules: {error}.")] + ModuleBuildFailure { error: String }, + + // Move call related errors + #[error("Function resolution failure: {error:?}.")] + FunctionNotFound { error: String }, + #[error("Module not found in package: {module_name:?}.")] + ModuleNotFound { module_name: String }, + #[error("Type error while binding function arguments: {error:?}.")] + Type { error: String }, + #[error("Circular object ownership detected")] + CircularObjectOwnership, + + // Internal state errors + #[error("Attempt to re-initialize a transaction lock for objects {:?}.", refs)] + ObjectLockAlreadyInitialized { refs: Vec }, + #[error( + "Object {obj_ref:?} already locked by a different transaction: {pending_transaction:?}" + )] + ObjectLockConflict { + obj_ref: ObjectRef, + pending_transaction: TransactionDigest, + }, + #[error( + "Objects {obj_refs:?} are already locked by a transaction from a future epoch {locked_epoch:?}), attempt to override with a transaction from epoch {new_epoch:?}" + )] + ObjectLockedAtFutureEpoch { + obj_refs: Vec, + locked_epoch: EpochId, + new_epoch: EpochId, + locked_by_tx: TransactionDigest, + }, + #[error("{TRANSACTION_NOT_FOUND_MSG_PREFIX} [{:?}].", digest)] + TransactionNotFound { digest: TransactionDigest }, + #[error("{TRANSACTIONS_NOT_FOUND_MSG_PREFIX} [{:?}].", digests)] + TransactionsNotFound { digests: Vec }, + #[error("Could not find the referenced transaction events [{digest:?}].")] + TransactionEventsNotFound { digest: TransactionEventsDigest }, + #[error( + "Attempt to move to `Executed` state an transaction that has already been executed: {:?}.", + digest + )] + TransactionAlreadyExecuted { digest: TransactionDigest }, + #[error("Object ID did not have the expected type")] + BadObjectType { error: String }, + #[error("Fail to retrieve Object layout for {st}")] + FailObjectLayout { st: String }, + + #[error("Execution invariant violated")] + ExecutionInvariantViolation, + #[error("Validator {authority:?} is faulty in a Byzantine manner: {reason:?}")] + ByzantineAuthoritySuspicion { + authority: AuthorityName, + reason: String, + }, + #[error( + "Attempted to access {object} through parent {given_parent}, \ + but it's actual parent is {actual_owner}" + )] + InvalidChildObjectAccess { + object: ObjectID, + given_parent: ObjectID, + actual_owner: Owner, + }, + + #[error("Authority Error: {error:?}")] + GenericAuthority { error: String }, + + // GenericBridge is not available + + #[error("Failed to dispatch subscription: {error:?}")] + FailedToDispatchSubscription { error: String }, + + #[error("Failed to serialize Owner: {error:?}")] + OwnerFailedToSerialize { error: String }, + + #[error("Failed to deserialize fields into JSON: {error:?}")] + ExtraFieldFailedToDeserialize { error: String }, + + #[error("Failed to execute transaction locally by Orchestrator: {error:?}")] + TransactionOrchestratorLocalExecution { error: String }, + + // Errors returned by authority and client read API's + #[error("Failure serializing transaction in the requested format: {:?}", error)] + TransactionSerialization { error: String }, + #[error("Failure serializing object in the requested format: {:?}", error)] + ObjectSerialization { error: String }, + #[error("Failure deserializing object in the requested format: {:?}", error)] + ObjectDeserialization { error: String }, + #[error("Event store component is not active on this node")] + NoEventStore, + + // Client side error + #[error("Too many authority errors were detected for {}: {:?}", action, errors)] + TooManyIncorrectAuthorities { + errors: Vec<(AuthorityName, IotaError)>, + action: String, + }, + #[error("Invalid transaction range query to the fullnode: {:?}", error)] + FullNodeInvalidTxRangeQuery { error: String }, + + // Errors related to the authority-consensus interface. + #[error("Failed to submit transaction to consensus: {0}")] + FailedToSubmitToConsensus(String), + #[error("Failed to connect with consensus node: {0}")] + ConsensusConnectionBroken(String), + #[error("Failed to execute handle_consensus_transaction on Iota: {0}")] + HandleConsensusTransactionFailure(String), + + // Cryptography errors. + #[error("Signature key generation error: {0}")] + SignatureKeyGen(String), + #[error("Key Conversion Error: {0}")] + KeyConversion(String), + #[error("Invalid Private Key provided")] + InvalidPrivateKey, + + // Unsupported Operations on Fullnode + #[error("Fullnode does not support handle_certificate")] + FullNodeCantHandleCertificate, + + // Epoch related errors. + #[error("Validator temporarily stopped processing transactions due to epoch change")] + ValidatorHaltedAtEpochEnd, + #[error("Operations for epoch {0} have ended")] + EpochEnded(EpochId), + #[error("Error when advancing epoch: {:?}", error)] + AdvanceEpoch { error: String }, + + #[error("Transaction Expired")] + TransactionExpired, + + // These are errors that occur when an RPC fails and is simply the utf8 message sent in a + // Tonic::Status + #[error("{1} - {0}")] + Rpc(String, String), + + #[error("Method not allowed")] + InvalidRpcMethod, + + // TODO: We should fold this into UserInputError::Unsupported. + #[error("Use of disabled feature: {:?}", error)] + UnsupportedFeature { error: String }, + + #[error("Unable to communicate with the Quorum Driver channel: {:?}", error)] + QuorumDriverCommunication { error: String }, + + #[error("Operation timed out")] + Timeout, + + #[error("Error executing {0}")] + Execution(String), + + #[error("Invalid committee composition")] + InvalidCommittee(String), + + #[error("Missing committee information for epoch {0}")] + MissingCommitteeAtEpoch(EpochId), + + #[error("Index store not available on this Fullnode.")] + IndexStoreNotAvailable, + + #[error("Failed to read dynamic field from table in the object store: {0}")] + DynamicFieldRead(String), + + #[error("Failed to read or deserialize system state related data structures on-chain: {0}")] + IotaSystemStateRead(String), + + #[error("Failed to read or deserialize bridge related data structures on-chain: {0}")] + IotaBridgeRead(String), + + #[error("Unexpected version error: {0}")] + UnexpectedVersion(String), + + #[error("Message version is not supported at the current protocol version: {error}")] + WrongMessageVersion { error: String }, + + #[error("unknown error: {0}")] + Unknown(String), + + #[error("Failed to perform file operation: {0}")] + FileIO(String), + + #[error("Failed to get JWK")] + JWKRetrieval, + + #[error("Storage error: {0}")] + Storage(String), + + #[error( + "Validator cannot handle the request at the moment. Please retry after at least {retry_after_secs} seconds." + )] + ValidatorOverloadedRetryAfter { retry_after_secs: u64 }, + + #[error("Too many requests")] + TooManyRequests, + + #[error("The request did not contain a certificate")] + NoCertificateProvided, +} + +#[repr(u64)] +#[expect(non_camel_case_types)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +/// Sub-status codes for the `UNKNOWN_VERIFICATION_ERROR` VM Status Code which +/// provides more context TODO: add more Vm Status errors. We use +/// `UNKNOWN_VERIFICATION_ERROR` as a catchall for now. +pub enum VMMVerifierErrorSubStatusCode { + MULTIPLE_RETURN_VALUES_NOT_ALLOWED = 0, + INVALID_OBJECT_CREATION = 1, +} + +#[repr(u64)] +#[expect(non_camel_case_types)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +/// Sub-status codes for the `MEMORY_LIMIT_EXCEEDED` VM Status Code which +/// provides more context +pub enum VMMemoryLimitExceededSubStatusCode { + EVENT_COUNT_LIMIT_EXCEEDED = 0, + EVENT_SIZE_LIMIT_EXCEEDED = 1, + NEW_ID_COUNT_LIMIT_EXCEEDED = 2, + DELETED_ID_COUNT_LIMIT_EXCEEDED = 3, + TRANSFER_ID_COUNT_LIMIT_EXCEEDED = 4, + OBJECT_RUNTIME_CACHE_LIMIT_EXCEEDED = 5, + OBJECT_RUNTIME_STORE_LIMIT_EXCEEDED = 6, + TOTAL_EVENT_SIZE_LIMIT_EXCEEDED = 7, +} + +pub type IotaResult = Result; +pub type UserInputResult = Result; + +// iota_protocol_config::Error is not available + +impl From for IotaError { + fn from(error: ExecutionError) -> Self { + IotaError::Execution(error.to_string()) + } +} + +// Status, TypedStoreError, crate::storage::error::Error are not available + +impl From for IotaError { + fn from(kind: ExecutionErrorKind) -> Self { + ExecutionError::from_kind(kind).into() + } +} + +impl From<&str> for IotaError { + fn from(error: &str) -> Self { + IotaError::GenericAuthority { + error: error.to_string(), + } + } +} + +impl From for IotaError { + fn from(error: String) -> Self { + IotaError::GenericAuthority { error } + } +} + +impl TryFrom for UserInputError { + type Error = anyhow::Error; + + fn try_from(err: IotaError) -> Result { + match err { + IotaError::UserInput { error } => Ok(error), + other => anyhow::bail!("error {:?} is not UserInput", other), + } + } +} + +impl From for IotaError { + fn from(error: UserInputError) -> Self { + IotaError::UserInput { error } + } +} + +impl From for IotaError { + fn from(error: IotaObjectResponseError) -> Self { + IotaError::IotaObjectResponse { error } + } +} + +impl IotaError { + pub fn individual_error_indicates_epoch_change(&self) -> bool { + matches!( + self, + IotaError::ValidatorHaltedAtEpochEnd | IotaError::MissingCommitteeAtEpoch(_) + ) + } + + /// Returns if the error is retryable and if the error's retryability is + /// explicitly categorized. + /// There should be only a handful of retryable errors. For now we list + /// common non-retryable error below to help us find more retryable + /// errors in logs. + pub fn is_retryable(&self) -> (bool, bool) { + let retryable = match self { + IotaError::Rpc { .. } => true, + + // Reconfig error + IotaError::ValidatorHaltedAtEpochEnd => true, + IotaError::MissingCommitteeAtEpoch(..) => true, + IotaError::WrongEpoch { .. } => true, + IotaError::EpochEnded { .. } => true, + + IotaError::UserInput { error } => { + match error { + // Only ObjectNotFound and DependentPackageNotFound is potentially retryable + UserInputError::ObjectNotFound { .. } => true, + UserInputError::DependentPackageNotFound { .. } => true, + _ => false, + } + } + + IotaError::PotentiallyTemporarilyInvalidSignature { .. } => true, + + // Overload errors + IotaError::TooManyTransactionsPendingExecution { .. } => true, + IotaError::TooManyTransactionsPendingOnObject { .. } => true, + IotaError::TooOldTransactionPendingOnObject { .. } => true, + IotaError::TooManyTransactionsPendingConsensus => true, + IotaError::ValidatorOverloadedRetryAfter { .. } => true, + + // Non retryable error + IotaError::Execution(..) => false, + IotaError::ByzantineAuthoritySuspicion { .. } => false, + IotaError::QuorumFailedToGetEffectsQuorumWhenProcessingTransaction { .. } => false, + IotaError::TxAlreadyFinalizedWithDifferentUserSigs => false, + IotaError::FailedToVerifyTxCertWithExecutedEffects { .. } => false, + IotaError::ObjectLockConflict { .. } => false, + + // NB: This is not an internal overload, but instead an imposed rate + // limit / blocking of a client. It must be non-retryable otherwise + // we will make the threat worse through automatic retries. + IotaError::TooManyRequests => false, + + // For all un-categorized errors, return here with categorized = false. + _ => return (false, false), + }; + + (retryable, true) + } + + pub fn is_object_or_package_not_found(&self) -> bool { + match self { + IotaError::UserInput { error } => { + matches!( + error, + UserInputError::ObjectNotFound { .. } + | UserInputError::DependentPackageNotFound { .. } + ) + } + _ => false, + } + } + + pub fn is_overload(&self) -> bool { + matches!( + self, + IotaError::TooManyTransactionsPendingExecution { .. } + | IotaError::TooManyTransactionsPendingOnObject { .. } + | IotaError::TooOldTransactionPendingOnObject { .. } + | IotaError::TooManyTransactionsPendingConsensus + ) + } + + pub fn is_retryable_overload(&self) -> bool { + matches!(self, IotaError::ValidatorOverloadedRetryAfter { .. }) + } + + pub fn retry_after_secs(&self) -> u64 { + match self { + IotaError::ValidatorOverloadedRetryAfter { retry_after_secs } => *retry_after_secs, + _ => 0, + } + } +} + +impl Ord for IotaError { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + Ord::cmp(self.as_ref(), other.as_ref()) + } +} + +impl PartialOrd for IotaError { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +type BoxError = Box; + +pub type ExecutionErrorKind = ExecutionFailureStatus; + +#[derive(Debug)] +pub struct ExecutionError { + inner: Box, +} + +#[derive(Debug)] +struct ExecutionErrorInner { + kind: ExecutionErrorKind, + source: Option, + command: Option, +} + +impl ExecutionError { + pub fn new(kind: ExecutionErrorKind, source: Option) -> Self { + Self { + inner: Box::new(ExecutionErrorInner { + kind, + source, + command: None, + }), + } + } + + pub fn new_with_source>(kind: ExecutionErrorKind, source: E) -> Self { + Self::new(kind, Some(source.into())) + } + + pub fn invariant_violation>(source: E) -> Self { + Self::new_with_source(ExecutionFailureStatus::InvariantViolation, source) + } + + pub fn with_command_index(mut self, command: CommandIndex) -> Self { + self.inner.command = Some(command); + self + } + + pub fn from_kind(kind: ExecutionErrorKind) -> Self { + Self::new(kind, None) + } + + pub fn kind(&self) -> &ExecutionErrorKind { + &self.inner.kind + } + + pub fn command(&self) -> Option { + self.inner.command + } + + pub fn source(&self) -> &Option { + &self.inner.source + } + + pub fn to_execution_status(&self) -> (ExecutionFailureStatus, Option) { + (self.kind().clone(), self.command()) + } +} + +impl std::fmt::Display for ExecutionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ExecutionError: {:?}", self) + } +} + +impl std::error::Error for ExecutionError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.inner.source.as_ref().map(|e| &**e as _) + } +} + +impl From for ExecutionError { + fn from(kind: ExecutionErrorKind) -> Self { + Self::from_kind(kind) + } +} + +pub fn command_argument_error(e: CommandArgumentError, arg_idx: usize) -> ExecutionError { + ExecutionError::from_kind(ExecutionErrorKind::command_argument_error( + e, + arg_idx as u16, + )) +} diff --git a/identity_iota_interaction/src/sdk_types/iota_types/event.rs b/identity_iota_interaction/src/sdk_types/iota_types/event.rs new file mode 100644 index 0000000000..ddf02a23cf --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/event.rs @@ -0,0 +1,55 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use anyhow::ensure; + +use super::{ + digests::TransactionDigest, + iota_serde::{Readable, BigInt}, +}; + +/// Unique ID of an IOTA Event, the ID is a combination of tx seq number and +/// event seq number, the ID is local to this particular fullnode and will be +/// different from other fullnode. +#[serde_as] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] +pub struct EventID { + pub tx_digest: TransactionDigest, + #[serde_as(as = "Readable, _>")] + pub event_seq: u64, +} + +impl From<(TransactionDigest, u64)> for EventID { + fn from((tx_digest_num, event_seq_number): (TransactionDigest, u64)) -> Self { + Self { + tx_digest: tx_digest_num as TransactionDigest, + event_seq: event_seq_number, + } + } +} + +impl From for String { + fn from(id: EventID) -> Self { + format!("{:?}:{}", id.tx_digest, id.event_seq) + } +} + +impl TryFrom for EventID { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + let values = value.split(':').collect::>(); + ensure!(values.len() == 2, "Malformed EventID : {value}"); + Ok(( + TransactionDigest::from_str(values[0])?, + u64::from_str(values[1])?, + ) + .into()) + } +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/execution_status.rs b/identity_iota_interaction/src/sdk_types/iota_types/execution_status.rs new file mode 100644 index 0000000000..9471ec477f --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/execution_status.rs @@ -0,0 +1,368 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::{self, Display, Formatter}; + +use super::super::move_core_types::language_storage::ModuleId; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use super::base_types::{ObjectID, IotaAddress, TypeParameterIndex, CodeOffset}; + +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] // Needed for QuorumDriverTrait implementation in IotaClientTsSdk +pub enum ExecutionStatus { + Success, + /// Gas used in the failed case, and the error. + Failure { + /// The error + error: ExecutionFailureStatus, + /// Which command the error occurred + command: Option, + }, +} + +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] +pub struct CongestedObjects(pub Vec); + +impl fmt::Display for CongestedObjects { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + for obj in &self.0 { + write!(f, "{}, ", obj)?; + } + Ok(()) + } +} + +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Error)] //, EnumVariantOrder)] +pub enum ExecutionFailureStatus { + // General transaction errors + #[error("Insufficient Gas.")] + InsufficientGas, + #[error("Invalid Gas Object. Possibly not address-owned or possibly not an IOTA coin.")] + InvalidGasObject, + #[error("INVARIANT VIOLATION.")] + InvariantViolation, + #[error("Attempted to used feature that is not supported yet")] + FeatureNotYetSupported, + #[error( + "Move object with size {object_size} is larger \ + than the maximum object size {max_object_size}" + )] + MoveObjectTooBig { + object_size: u64, + max_object_size: u64, + }, + #[error( + "Move package with size {object_size} is larger than the \ + maximum object size {max_object_size}" + )] + MovePackageTooBig { + object_size: u64, + max_object_size: u64, + }, + #[error("Circular Object Ownership, including object {object}.")] + CircularObjectOwnership { object: ObjectID }, + + // Coin errors + #[error("Insufficient coin balance for operation.")] + InsufficientCoinBalance, + #[error("The coin balance overflows u64")] + CoinBalanceOverflow, + + // Publish/Upgrade errors + #[error( + "Publish Error, Non-zero Address. \ + The modules in the package must have their self-addresses set to zero." + )] + PublishErrorNonZeroAddress, + + #[error( + "IOTA Move Bytecode Verification Error. \ + Please run the IOTA Move Verifier for more information." + )] + IotaMoveVerificationError, + + // Errors from the Move VM + // + // Indicates an error from a non-abort instruction + #[error( + "Move Primitive Runtime Error. Location: {0}. \ + Arithmetic error, stack overflow, max value depth, etc." + )] + MovePrimitiveRuntimeError(MoveLocationOpt), + #[error("Move Runtime Abort. Location: {0}, Abort Code: {1}")] + MoveAbort(MoveLocation, u64), + #[error( + "Move Bytecode Verification Error. \ + Please run the Bytecode Verifier for more information." + )] + VMVerificationOrDeserializationError, + #[error("MOVE VM INVARIANT VIOLATION.")] + VMInvariantViolation, + + // Programmable Transaction Errors + #[error("Function Not Found.")] + FunctionNotFound, + #[error( + "Arity mismatch for Move function. \ + The number of arguments does not match the number of parameters" + )] + ArityMismatch, + #[error( + "Type arity mismatch for Move function. \ + Mismatch between the number of actual versus expected type arguments." + )] + TypeArityMismatch, + #[error("Non Entry Function Invoked. Move Call must start with an entry function")] + NonEntryFunctionInvoked, + #[error("Invalid command argument at {arg_idx}. {kind}")] + CommandArgumentError { + arg_idx: u16, + kind: CommandArgumentError, + }, + #[error("Error for type argument at index {argument_idx}: {kind}")] + TypeArgumentError { + argument_idx: TypeParameterIndex, + kind: TypeArgumentError, + }, + #[error( + "Unused result without the drop ability. \ + Command result {result_idx}, return value {secondary_idx}" + )] + UnusedValueWithoutDrop { result_idx: u16, secondary_idx: u16 }, + #[error( + "Invalid public Move function signature. \ + Unsupported return type for return value {idx}" + )] + InvalidPublicFunctionReturnType { idx: u16 }, + #[error("Invalid Transfer Object, object does not have public transfer.")] + InvalidTransferObject, + + // Post-execution errors + // + // Indicates the effects from the transaction are too large + #[error( + "Effects of size {current_size} bytes too large. \ + Limit is {max_size} bytes" + )] + EffectsTooLarge { current_size: u64, max_size: u64 }, + + #[error( + "Publish/Upgrade Error, Missing dependency. \ + A dependency of a published or upgraded package has not been assigned an on-chain \ + address." + )] + PublishUpgradeMissingDependency, + + #[error( + "Publish/Upgrade Error, Dependency downgrade. \ + Indirect (transitive) dependency of published or upgraded package has been assigned an \ + on-chain version that is less than the version required by one of the package's \ + transitive dependencies." + )] + PublishUpgradeDependencyDowngrade, + + #[error("Invalid package upgrade. {upgrade_error}")] + PackageUpgradeError { upgrade_error: PackageUpgradeError }, + + // Indicates the transaction tried to write objects too large to storage + #[error( + "Written objects of {current_size} bytes too large. \ + Limit is {max_size} bytes" + )] + WrittenObjectsTooLarge { current_size: u64, max_size: u64 }, + + #[error("Certificate is on the deny list")] + CertificateDenied, + + #[error( + "IOTA Move Bytecode Verification Timeout. \ + Please run the IOTA Move Verifier for more information." + )] + IotaMoveVerificationTimeout, + + #[error("The shared object operation is not allowed.")] + SharedObjectOperationNotAllowed, + + #[error("Certificate cannot be executed due to a dependency on a deleted shared object")] + InputObjectDeleted, + + #[error("Certificate is cancelled due to congestion on shared objects: {congested_objects}")] + ExecutionCancelledDueToSharedObjectCongestion { congested_objects: CongestedObjects }, + + #[error("Address {address:?} is denied for coin {coin_type}")] + AddressDeniedForCoin { + address: IotaAddress, + coin_type: String, + }, + + #[error("Coin type is globally paused for use: {coin_type}")] + CoinTypeGlobalPause { coin_type: String }, + + #[error("Certificate is cancelled because randomness could not be generated this epoch")] + ExecutionCancelledDueToRandomnessUnavailable, + // NOTE: if you want to add a new enum, + // please add it at the end for Rust SDK backward compatibility. +} + +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Hash)] +pub struct MoveLocation { + pub module: ModuleId, + pub function: u16, + pub instruction: CodeOffset, + pub function_name: Option, +} + +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Hash)] +pub struct MoveLocationOpt(pub Option); + +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Hash, Error)] +pub enum CommandArgumentError { + #[error("The type of the value does not match the expected type")] + TypeMismatch, + #[error("The argument cannot be deserialized into a value of the specified type")] + InvalidBCSBytes, + #[error("The argument cannot be instantiated from raw bytes")] + InvalidUsageOfPureArg, + #[error( + "Invalid argument to private entry function. \ + These functions cannot take arguments from other Move functions" + )] + InvalidArgumentToPrivateEntryFunction, + #[error("Out of bounds access to input or result vector {idx}")] + IndexOutOfBounds { idx: u16 }, + #[error( + "Out of bounds secondary access to result vector \ + {result_idx} at secondary index {secondary_idx}" + )] + SecondaryIndexOutOfBounds { result_idx: u16, secondary_idx: u16 }, + #[error( + "Invalid usage of result {result_idx}, \ + expected a single result but found either no return values or multiple." + )] + InvalidResultArity { result_idx: u16 }, + #[error( + "Invalid taking of the Gas coin. \ + It can only be used by-value with TransferObjects" + )] + InvalidGasCoinUsage, + #[error( + "Invalid usage of value. \ + Mutably borrowed values require unique usage. \ + Immutably borrowed values cannot be taken or borrowed mutably. \ + Taken values cannot be used again." + )] + InvalidValueUsage, + #[error("Immutable objects cannot be passed by-value.")] + InvalidObjectByValue, + #[error("Immutable objects cannot be passed by mutable reference, &mut.")] + InvalidObjectByMutRef, + #[error( + "Shared object operations such a wrapping, freezing, or converting to owned are not \ + allowed." + )] + SharedObjectOperationNotAllowed, +} + +#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Hash, Error)] +pub enum PackageUpgradeError { + #[error("Unable to fetch package at {package_id}")] + UnableToFetchPackage { package_id: ObjectID }, + #[error("Object {object_id} is not a package")] + NotAPackage { object_id: ObjectID }, + #[error("New package is incompatible with previous version")] + IncompatibleUpgrade, + #[error("Digest in upgrade ticket and computed digest disagree")] + DigestDoesNotMatch { digest: Vec }, + #[error("Upgrade policy {policy} is not a valid upgrade policy")] + UnknownUpgradePolicy { policy: u8 }, + #[error("Package ID {package_id} does not match package ID in upgrade ticket {ticket_id}")] + PackageIDDoesNotMatch { + package_id: ObjectID, + ticket_id: ObjectID, + }, +} + +#[derive(Eq, PartialEq, Clone, Copy, Debug, Serialize, Deserialize, Hash, Error)] +pub enum TypeArgumentError { + #[error("A type was not found in the module specified.")] + TypeNotFound, + #[error("A type provided did not match the specified constraints.")] + ConstraintNotSatisfied, +} + +impl ExecutionFailureStatus { + pub fn command_argument_error(kind: CommandArgumentError, arg_idx: u16) -> Self { + Self::CommandArgumentError { arg_idx, kind } + } +} + +impl Display for MoveLocationOpt { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.0 { + None => write!(f, "UNKNOWN"), + Some(l) => write!(f, "{l}"), + } + } +} + +impl Display for MoveLocation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let Self { + module, + function, + instruction, + function_name, + } = self; + if let Some(fname) = function_name { + write!( + f, + "{module}::{fname} (function index {function}) at offset {instruction}" + ) + } else { + write!( + f, + "{module} in function definition {function} at offset {instruction}" + ) + } + } +} + +impl ExecutionStatus { + pub fn new_failure( + error: ExecutionFailureStatus, + command: Option, + ) -> ExecutionStatus { + ExecutionStatus::Failure { error, command } + } + + pub fn is_ok(&self) -> bool { + matches!(self, ExecutionStatus::Success { .. }) + } + + pub fn is_err(&self) -> bool { + matches!(self, ExecutionStatus::Failure { .. }) + } + + pub fn unwrap(&self) { + match self { + ExecutionStatus::Success => {} + ExecutionStatus::Failure { .. } => { + panic!("Unable to unwrap() on {:?}", self); + } + } + } + + pub fn unwrap_err(self) -> (ExecutionFailureStatus, Option) { + match self { + ExecutionStatus::Success { .. } => { + panic!("Unable to unwrap() on {:?}", self); + } + ExecutionStatus::Failure { error, command } => (error, command), + } + } +} + +pub type CommandIndex = usize; diff --git a/identity_iota_interaction/src/sdk_types/iota_types/gas_coin.rs b/identity_iota_interaction/src/sdk_types/iota_types/gas_coin.rs new file mode 100644 index 0000000000..b8cafc9720 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/gas_coin.rs @@ -0,0 +1,147 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::Deserialize; +use serde::Serialize; + +use crate::ident_str; + +use super::super::move_core_types::language_storage::{StructTag, TypeTag}; +use super::super::move_core_types::identifier::IdentStr; +use super::super::move_core_types::annotated_value::MoveStructLayout; + +use super::super::types::IOTA_FRAMEWORK_ADDRESS; + +use super::coin::{Coin, TreasuryCap}; +use super::base_types::{ObjectID}; +use super::id::UID; +use super::balance::{Balance, Supply}; +use std::fmt::{Display, Formatter}; + +/// The number of Nanos per IOTA token +pub const NANOS_PER_IOTA: u64 = 1_000_000_000; + +/// Total supply in IOTA at genesis, after the migration from a Stardust ledger, +/// before any inflation mechanism +pub const STARDUST_TOTAL_SUPPLY_IOTA: u64 = 4_600_000_000; + +// Note: cannot use checked arithmetic here since `const unwrap` is still +// unstable. +/// Total supply at genesis denominated in Nanos, after the migration from a +/// Stardust ledger, before any inflation mechanism +pub const STARDUST_TOTAL_SUPPLY_NANOS: u64 = STARDUST_TOTAL_SUPPLY_IOTA * NANOS_PER_IOTA; + +pub const GAS_MODULE_NAME: &IdentStr = ident_str!("iota"); +pub const GAS_STRUCT_NAME: &IdentStr = ident_str!("IOTA"); +pub const GAS_TREASURY_CAP_STRUCT_NAME: &IdentStr = ident_str!("IotaTreasuryCap"); + +pub struct GAS {} +impl GAS { + pub fn type_() -> StructTag { + StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + name: GAS_STRUCT_NAME.to_owned(), + module: GAS_MODULE_NAME.to_owned(), + type_params: Vec::new(), + } + } + + pub fn type_tag() -> TypeTag { + TypeTag::Struct(Box::new(Self::type_())) + } + + pub fn is_gas(other: &StructTag) -> bool { + &Self::type_() == other + } + + pub fn is_gas_type(other: &TypeTag) -> bool { + match other { + TypeTag::Struct(s) => Self::is_gas(s), + _ => false, + } + } +} + +/// Rust version of the Move iota::coin::Coin type +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct GasCoin(pub Coin); + +impl GasCoin { + pub fn new(id: ObjectID, value: u64) -> Self { + Self(Coin::new(UID::new(id), value)) + } + + pub fn value(&self) -> u64 { + self.0.value() + } + + pub fn type_() -> StructTag { + Coin::type_(TypeTag::Struct(Box::new(GAS::type_()))) + } + + /// Return `true` if `s` is the type of a gas coin (i.e., + /// 0x2::coin::Coin<0x2::iota::IOTA>) + pub fn is_gas_coin(s: &StructTag) -> bool { + Coin::is_coin(s) && s.type_params.len() == 1 && GAS::is_gas_type(&s.type_params[0]) + } + + /// Return `true` if `s` is the type of a gas balance (i.e., + /// 0x2::balance::Balance<0x2::iota::IOTA>) + pub fn is_gas_balance(s: &StructTag) -> bool { + Balance::is_balance(s) + && s.type_params.len() == 1 + && GAS::is_gas_type(&s.type_params[0]) + } + + pub fn id(&self) -> &ObjectID { + self.0.id() + } + + pub fn to_bcs_bytes(&self) -> Vec { + bcs::to_bytes(&self).unwrap() + } + + // MoveObject is not available for wasm32 + // + // pub fn to_object(&self, version: SequenceNumber) -> MoveObject { + // MoveObject::new_gas_coin(version, *self.id(), self.value()) + // } + + pub fn layout() -> MoveStructLayout { + Coin::layout(TypeTag::Struct(Box::new(GAS::type_()))) + } +} + +impl Display for GasCoin { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Coin {{ id: {}, value: {} }}", self.id(), self.value()) + } +} + +// Rust version of the IotaTreasuryCap type +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct IotaTreasuryCap { + pub inner: TreasuryCap, +} + +impl IotaTreasuryCap { + pub fn type_() -> StructTag { + StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + module: GAS_MODULE_NAME.to_owned(), + name: GAS_TREASURY_CAP_STRUCT_NAME.to_owned(), + type_params: Vec::new(), + } + } + + /// Returns the `TreasuryCap` object ID. + pub fn id(&self) -> &ObjectID { + self.inner.id.object_id() + } + + /// Returns the total `Supply` of `Coin`. + pub fn total_supply(&self) -> &Supply { + &self.inner.total_supply + } +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/governance.rs b/identity_iota_interaction/src/sdk_types/iota_types/governance.rs new file mode 100644 index 0000000000..e9ed0fe3e8 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/governance.rs @@ -0,0 +1,98 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::Deserialize; +use serde::Serialize; + +use crate::ident_str; + +use super::super::move_core_types::language_storage::StructTag; +use super::super::move_core_types::identifier::IdentStr; + +use super::gas_coin::NANOS_PER_IOTA; +use super::balance::Balance; +use super::IOTA_SYSTEM_ADDRESS; +use super::base_types::{ObjectID, EpochId}; +use super::id::{UID, ID}; + +/// Maximum number of active validators at any moment. +/// We do not allow the number of validators in any epoch to go above this. +pub const MAX_VALIDATOR_COUNT: u64 = 150; + +/// Lower-bound on the amount of stake required to become a validator. +/// +/// 2 million IOTA +pub const MIN_VALIDATOR_JOINING_STAKE_NANOS: u64 = 2_000_000 * NANOS_PER_IOTA; + +/// Validators with stake amount below `validator_low_stake_threshold` are +/// considered to have low stake and will be escorted out of the validator set +/// after being below this threshold for more than +/// `validator_low_stake_grace_period` number of epochs. +/// +/// 1.5 million IOTA +pub const VALIDATOR_LOW_STAKE_THRESHOLD_NANOS: u64 = 1_500_000 * NANOS_PER_IOTA; + +/// Validators with stake below `validator_very_low_stake_threshold` will be +/// removed immediately at epoch change, no grace period. +/// +/// 1 million IOTA +pub const VALIDATOR_VERY_LOW_STAKE_THRESHOLD_NANOS: u64 = 1_000_000 * NANOS_PER_IOTA; + +/// A validator can have stake below `validator_low_stake_threshold` +/// for this many epochs before being kicked out. +pub const VALIDATOR_LOW_STAKE_GRACE_PERIOD: u64 = 7; + +pub const STAKING_POOL_MODULE_NAME: &IdentStr = ident_str!("staking_pool"); +pub const STAKED_IOTA_STRUCT_NAME: &IdentStr = ident_str!("StakedIota"); + +pub const ADD_STAKE_MUL_COIN_FUN_NAME: &IdentStr = ident_str!("request_add_stake_mul_coin"); +pub const ADD_STAKE_FUN_NAME: &IdentStr = ident_str!("request_add_stake"); +pub const WITHDRAW_STAKE_FUN_NAME: &IdentStr = ident_str!("request_withdraw_stake"); + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct StakedIota { + id: UID, + pool_id: ID, + stake_activation_epoch: u64, + principal: Balance, +} + +impl StakedIota { + pub fn type_() -> StructTag { + StructTag { + address: IOTA_SYSTEM_ADDRESS, + module: STAKING_POOL_MODULE_NAME.to_owned(), + name: STAKED_IOTA_STRUCT_NAME.to_owned(), + type_params: vec![], + } + } + + pub fn is_staked_iota(s: &StructTag) -> bool { + s.address == IOTA_SYSTEM_ADDRESS + && s.module.as_ident_str() == STAKING_POOL_MODULE_NAME + && s.name.as_ident_str() == STAKED_IOTA_STRUCT_NAME + && s.type_params.is_empty() + } + + pub fn id(&self) -> ObjectID { + self.id.id.bytes + } + + pub fn pool_id(&self) -> ObjectID { + self.pool_id.bytes + } + + pub fn activation_epoch(&self) -> EpochId { + self.stake_activation_epoch + } + + pub fn request_epoch(&self) -> EpochId { + // TODO: this might change when we implement warm up period. + self.stake_activation_epoch.saturating_sub(1) + } + + pub fn principal(&self) -> u64 { + self.principal.value() + } +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/id.rs b/identity_iota_interaction/src/sdk_types/iota_types/id.rs new file mode 100644 index 0000000000..d674c48093 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/id.rs @@ -0,0 +1,108 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::ident_str; + +use super::super::{ + move_core_types::{ + account_address::AccountAddress, + identifier::IdentStr, + language_storage::{StructTag, TypeTag}, + annotated_value::{MoveStructLayout, MoveFieldLayout, MoveTypeLayout}, + }, +}; + +use super::{ + base_types::ObjectID, + MoveTypeTagTrait, + IOTA_FRAMEWORK_ADDRESS, +}; + +pub const OBJECT_MODULE_NAME_STR: &str = "object"; +pub const OBJECT_MODULE_NAME: &IdentStr = ident_str!(OBJECT_MODULE_NAME_STR); +pub const UID_STRUCT_NAME: &IdentStr = ident_str!("UID"); +pub const ID_STRUCT_NAME: &IdentStr = ident_str!("ID"); +pub const RESOLVED_IOTA_ID: (&AccountAddress, &IdentStr, &IdentStr) = + (&IOTA_FRAMEWORK_ADDRESS, OBJECT_MODULE_NAME, ID_STRUCT_NAME); + +/// Rust version of the Move iota::object::Info type +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct UID { + pub id: ID, +} + +/// Rust version of the Move iota::object::ID type +#[derive(Debug, Hash, Serialize, Deserialize, Clone, Eq, PartialEq)] +#[serde(transparent)] +pub struct ID { + pub bytes: ObjectID, +} + +impl UID { + pub fn new(bytes: ObjectID) -> Self { + Self { + id: { ID::new(bytes) }, + } + } + + pub fn type_() -> StructTag { + StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + module: OBJECT_MODULE_NAME.to_owned(), + name: UID_STRUCT_NAME.to_owned(), + type_params: Vec::new(), + } + } + + pub fn object_id(&self) -> &ObjectID { + &self.id.bytes + } + + pub fn to_bcs_bytes(&self) -> Vec { + bcs::to_bytes(&self).unwrap() + } + + pub fn layout() -> MoveStructLayout { + MoveStructLayout { + type_: Self::type_(), + fields: vec![MoveFieldLayout::new( + ident_str!("id").to_owned(), + MoveTypeLayout::Struct(ID::layout()), + )], + } + } +} + +impl ID { + pub fn new(object_id: ObjectID) -> Self { + Self { bytes: object_id } + } + + pub fn type_() -> StructTag { + StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + module: OBJECT_MODULE_NAME.to_owned(), + name: ID_STRUCT_NAME.to_owned(), + type_params: Vec::new(), + } + } + + pub fn layout() -> MoveStructLayout { + MoveStructLayout { + type_: Self::type_(), + fields: vec![MoveFieldLayout::new( + ident_str!("bytes").to_owned(), + MoveTypeLayout::Address, + )], + } + } +} + +impl MoveTypeTagTrait for ID { + fn get_type_tag() -> TypeTag { + TypeTag::Struct(Box::new(Self::type_())) + } +} diff --git a/identity_iota_interaction/src/sdk_types/iota_types/iota_serde.rs b/identity_iota_interaction/src/sdk_types/iota_types/iota_serde.rs new file mode 100644 index 0000000000..88bdef0efb --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/iota_serde.rs @@ -0,0 +1,406 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + fmt, + fmt::{Debug, Display, Formatter, Write}, + marker::PhantomData, + ops::Deref, + str::FromStr, +}; +use std::marker::Sized; +use std::string::{String, ToString}; +use std::result::Result::Ok; +#[allow(unused)] // Kept in sync with original source, so keep as is. +use std::option::Option; +use std::option::Option::Some; + +use fastcrypto::encoding::Hex; +use serde::{ + self, + de::{Deserializer, Error}, + ser::{Error as SerError, Serializer}, + Deserialize, Serialize, +}; +use serde_with::{serde_as, DeserializeAs, DisplayFromStr, SerializeAs}; + +use Result; + +use super::super::move_core_types::{ + account_address::AccountAddress, + language_storage::{StructTag, TypeTag} +}; + +#[allow(unused)] // Kept in sync with original source, so keep as is. +use super::{IOTA_FRAMEWORK_ADDRESS, MOVE_STDLIB_ADDRESS, IOTA_SYSTEM_ADDRESS, + STARDUST_ADDRESS, IOTA_SYSTEM_STATE_ADDRESS, IOTA_CLOCK_ADDRESS }; +use super::parse_iota_struct_tag; +use super::parse_iota_type_tag; + +/// The minimum and maximum protocol versions supported by this build. +const MIN_PROTOCOL_VERSION: u64 = 1; // Originally defined in crates/iota-protocol-config/src/lib.rs +pub const MAX_PROTOCOL_VERSION: u64 = 1; // Originally defined in crates/iota-protocol-config/src/lib.rs + +// ----------------------------------------------------------------------------------------- +// Originally contained in crates/iota-protocol-config/src/lib.rs +// ----------------------------------------------------------------------------------------- + +// Record history of protocol version allocations here: +// +// Version 1: Original version. +// Version 2: Don't redistribute slashed staking rewards, fix computation of +// SystemEpochInfoEventV1. +#[derive(Copy, Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct ProtocolVersion(u64); + +impl ProtocolVersion { + // The minimum and maximum protocol version supported by this binary. + // Counterintuitively, this constant may change over time as support for old + // protocol versions is removed from the source. This ensures that when a + // new network (such as a testnet) is created, its genesis committee will + // use a protocol version that is actually supported by the binary. + pub const MIN: Self = Self(MIN_PROTOCOL_VERSION); + + pub const MAX: Self = Self(MAX_PROTOCOL_VERSION); + + #[allow(unused)] // Kept in sync with original source, so keep as is. + #[cfg(not(msim))] + const MAX_ALLOWED: Self = Self::MAX; + + // We create 3 additional "fake" versions in simulator builds so that we can + // test upgrades. + #[cfg(msim)] + pub const MAX_ALLOWED: Self = Self(MAX_PROTOCOL_VERSION + 3); + + pub fn new(v: u64) -> Self { + Self(v) + } + + pub const fn as_u64(&self) -> u64 { + self.0 + } + + // For serde deserialization - we don't define a Default impl because there + // isn't a single universally appropriate default value. + pub fn max() -> Self { + Self::MAX + } +} + +impl From for ProtocolVersion { + fn from(v: u64) -> Self { + Self::new(v) + } +} + +// ----------------------------------------------------------------------------------------- +// End of originally contained in crates/iota-protocol-config/src/lib.rs section +// ----------------------------------------------------------------------------------------- + +#[inline] +fn to_custom_error<'de, D, E>(e: E) -> D::Error + where + E: Debug, + D: Deserializer<'de>, +{ + Error::custom(format!("byte deserialization failed, cause by: {:?}", e)) +} + +/// Use with serde_as to control serde for human-readable serialization and +/// deserialization `H` : serde_as SerializeAs/DeserializeAs delegation for +/// human readable in/output `R` : serde_as SerializeAs/DeserializeAs delegation +/// for non-human readable in/output +/// +/// # Example: +/// +/// ```text +/// #[serde_as] +/// #[derive(Deserialize, Serialize)] +/// struct Example(#[serde_as(as = "Readable")] [u8; 20]); +/// ``` +/// +/// The above example will delegate human-readable serde to `DisplayFromStr` +/// and array tuple (default) for non-human-readable serializer. +pub struct Readable { + human_readable: PhantomData, + non_human_readable: PhantomData, +} + +impl SerializeAs for Readable + where + H: SerializeAs, + R: SerializeAs, +{ + fn serialize_as(value: &T, serializer: S) -> Result + where + S: Serializer, + { + if serializer.is_human_readable() { + H::serialize_as(value, serializer) + } else { + R::serialize_as(value, serializer) + } + } +} + +impl<'de, R, H, T> DeserializeAs<'de, T> for Readable + where + H: DeserializeAs<'de, T>, + R: DeserializeAs<'de, T>, +{ + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + H::deserialize_as(deserializer) + } else { + R::deserialize_as(deserializer) + } + } +} + +/// custom serde for AccountAddress +pub struct HexAccountAddress; + +impl SerializeAs for HexAccountAddress { + fn serialize_as(value: &AccountAddress, serializer: S) -> Result + where + S: Serializer, + { + Hex::serialize_as(value, serializer) + } +} + +impl<'de> DeserializeAs<'de, AccountAddress> for HexAccountAddress { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + if s.starts_with("0x") { + AccountAddress::from_hex_literal(&s) + } else { + AccountAddress::from_hex(&s) + } + .map_err(to_custom_error::<'de, D, _>) + } +} + +/// Serializes a bitmap according to the roaring bitmap on-disk standard. +/// +pub struct IotaBitmap; + +pub struct IotaStructTag; + +impl SerializeAs for IotaStructTag { + fn serialize_as(value: &StructTag, serializer: S) -> Result + where + S: Serializer, + { + let f = to_iota_struct_tag_string(value).map_err(S::Error::custom)?; + f.serialize(serializer) + } +} + +const IOTA_ADDRESSES: [AccountAddress; 7] = [ + AccountAddress::ZERO, + AccountAddress::ONE, + IOTA_FRAMEWORK_ADDRESS, + IOTA_SYSTEM_ADDRESS, + STARDUST_ADDRESS, + IOTA_SYSTEM_STATE_ADDRESS, + IOTA_CLOCK_ADDRESS, +]; +/// Serialize StructTag as a string, retaining the leading zeros in the address. +pub fn to_iota_struct_tag_string(value: &StructTag) -> Result { + let mut f = String::new(); + // trim leading zeros if address is in IOTA_ADDRESSES + let address = if IOTA_ADDRESSES.contains(&value.address) { + value.address.short_str_lossless() + } else { + value.address.to_canonical_string(/* with_prefix */ false) + }; + + write!(f, "0x{}::{}::{}", address, value.module, value.name)?; + if let Some(first_ty) = value.type_params.first() { + write!(f, "<")?; + write!(f, "{}", to_iota_type_tag_string(first_ty)?)?; + for ty in value.type_params.iter().skip(1) { + write!(f, ", {}", to_iota_type_tag_string(ty)?)?; + } + write!(f, ">")?; + } + Ok(f) +} + +fn to_iota_type_tag_string(value: &TypeTag) -> Result { + match value { + TypeTag::Vector(t) => Ok(format!("vector<{}>", to_iota_type_tag_string(t)?)), + TypeTag::Struct(s) => to_iota_struct_tag_string(s), + _ => Ok(value.to_string()), + } +} + +impl<'de> DeserializeAs<'de, StructTag> for IotaStructTag { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + parse_iota_struct_tag(&s).map_err(D::Error::custom) + } +} + +pub struct IotaTypeTag; + +impl SerializeAs for IotaTypeTag { + fn serialize_as(value: &TypeTag, serializer: S) -> Result + where + S: Serializer, + { + let s = to_iota_type_tag_string(value).map_err(S::Error::custom)?; + s.serialize(serializer) + } +} + +impl<'de> DeserializeAs<'de, TypeTag> for IotaTypeTag { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + parse_iota_type_tag(&s).map_err(D::Error::custom) + } +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Copy)] +pub struct BigInt( + #[serde_as(as = "DisplayFromStr")] + T, +) + where + T: Display + FromStr, + ::Err: Display; + +impl BigInt + where + T: Display + FromStr, + ::Err: Display, +{ + pub fn into_inner(self) -> T { + self.0 + } +} + +impl SerializeAs for BigInt + where + T: Display + FromStr + Copy, + ::Err: Display, +{ + fn serialize_as(value: &T, serializer: S) -> Result + where + S: Serializer, + { + BigInt(*value).serialize(serializer) + } +} + +impl<'de, T> DeserializeAs<'de, T> for BigInt + where + T: Display + FromStr + Copy, + ::Err: Display, +{ + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(*BigInt::deserialize(deserializer)?) + } +} + +impl From for BigInt + where + T: Display + FromStr, + ::Err: Display, +{ + fn from(v: T) -> BigInt { + BigInt(v) + } +} + +impl Deref for BigInt + where + T: Display + FromStr, + ::Err: Display, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for BigInt + where + T: Display + FromStr, + ::Err: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Copy)] +pub struct SequenceNumber(u64); + +impl SerializeAs for SequenceNumber { + fn serialize_as( + value: &super::base_types::SequenceNumber, + serializer: S, + ) -> Result + where + S: Serializer, + { + let s = value.value().to_string(); + s.serialize(serializer) + } +} + +impl<'de> DeserializeAs<'de, super::base_types::SequenceNumber> for SequenceNumber { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let b = BigInt::deserialize(deserializer)?; + Ok(super::base_types::SequenceNumber::from_u64(*b)) + } +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Copy)] +#[serde(rename = "ProtocolVersion")] +pub struct AsProtocolVersion(u64); + +impl SerializeAs for AsProtocolVersion { + fn serialize_as(value: &ProtocolVersion, serializer: S) -> Result + where + S: Serializer, + { + let s = value.as_u64().to_string(); + s.serialize(serializer) + } +} + +impl<'de> DeserializeAs<'de, ProtocolVersion> for AsProtocolVersion { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let b = BigInt::::deserialize(deserializer)?; + Ok(ProtocolVersion::from(*b)) + } +} diff --git a/identity_iota_interaction/src/sdk_types/iota_types/iota_types_lib.rs b/identity_iota_interaction/src/sdk_types/iota_types/iota_types_lib.rs new file mode 100644 index 0000000000..4742ecc9b9 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/iota_types_lib.rs @@ -0,0 +1,125 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused)] // Kept in sync with original source, so keep as is. +use serde::{Deserialize, Serialize}; +use super::super::move_core_types::{ + account_address::AccountAddress, + language_storage::{TypeTag, StructTag}, +}; +use super::base_types::{ObjectID, SequenceNumber, IotaAddress}; +use super::object::OBJECT_START_VERSION; + +macro_rules! built_in_ids { + ($($addr:ident / $id:ident = $init:expr);* $(;)?) => { + $( + pub const $addr: AccountAddress = builtin_address($init); + pub const $id: ObjectID = ObjectID::from_address($addr); + )* + } +} + +macro_rules! built_in_pkgs { + ($($addr:ident / $id:ident = $init:expr);* $(;)?) => { + built_in_ids! { $($addr / $id = $init;)* } + pub const SYSTEM_PACKAGE_ADDRESSES: &[AccountAddress] = &[$($addr),*]; + pub fn is_system_package(addr: impl Into) -> bool { + matches!(addr.into(), $($addr)|*) + } + } +} + +built_in_pkgs! { + MOVE_STDLIB_ADDRESS / MOVE_STDLIB_PACKAGE_ID = 0x1; + IOTA_FRAMEWORK_ADDRESS / IOTA_FRAMEWORK_PACKAGE_ID = 0x2; + IOTA_SYSTEM_ADDRESS / IOTA_SYSTEM_PACKAGE_ID = 0x3; + BRIDGE_ADDRESS / BRIDGE_PACKAGE_ID = 0xb; + STARDUST_ADDRESS / STARDUST_PACKAGE_ID = 0x107a; +} + +built_in_ids! { + IOTA_SYSTEM_STATE_ADDRESS / IOTA_SYSTEM_STATE_OBJECT_ID = 0x5; + IOTA_CLOCK_ADDRESS / IOTA_CLOCK_OBJECT_ID = 0x6; + IOTA_AUTHENTICATOR_STATE_ADDRESS / IOTA_AUTHENTICATOR_STATE_OBJECT_ID = 0x7; + IOTA_RANDOMNESS_STATE_ADDRESS / IOTA_RANDOMNESS_STATE_OBJECT_ID = 0x8; + IOTA_BRIDGE_ADDRESS / IOTA_BRIDGE_OBJECT_ID = 0x9; + IOTA_DENY_LIST_ADDRESS / IOTA_DENY_LIST_OBJECT_ID = 0x403; +} + +pub const IOTA_SYSTEM_STATE_OBJECT_SHARED_VERSION: SequenceNumber = OBJECT_START_VERSION; +pub const IOTA_CLOCK_OBJECT_SHARED_VERSION: SequenceNumber = OBJECT_START_VERSION; +pub const IOTA_AUTHENTICATOR_STATE_OBJECT_SHARED_VERSION: SequenceNumber = OBJECT_START_VERSION; + +const fn builtin_address(suffix: u16) -> AccountAddress { + let mut addr = [0u8; AccountAddress::LENGTH]; + let [hi, lo] = suffix.to_be_bytes(); + addr[AccountAddress::LENGTH - 2] = hi; + addr[AccountAddress::LENGTH - 1] = lo; + AccountAddress::new(addr) +} + +/// Parse `s` as a struct type: A fully-qualified name, optionally followed by a +/// list of type parameters (types -- see `parse_iota_type_tag`, separated by +/// commas, surrounded by angle brackets). Parsing succeeds if and only if `s` +/// matches this format exactly, with no remaining input. This function is +/// intended for use within the authority codebase. +pub fn parse_iota_struct_tag(s: &str) -> anyhow::Result { + use super::super::move_command_line_common::types::ParsedStructType; + ParsedStructType::parse(s)?.into_struct_tag(&resolve_address) +} + +/// Parse `s` as a type: Either a struct type (see `parse_iota_struct_tag`), a +/// primitive type, or a vector with a type parameter. Parsing succeeds if and +/// only if `s` matches this format exactly, with no remaining input. This +/// function is intended for use within the authority codebase. +pub fn parse_iota_type_tag(s: &str) -> anyhow::Result { + use super::super::move_command_line_common::types::ParsedType; + ParsedType::parse(s)?.into_type_tag(&resolve_address) +} + +/// Resolve well-known named addresses into numeric addresses. +pub fn resolve_address(addr: &str) -> Option { + match addr { + "std" => Some(MOVE_STDLIB_ADDRESS), + "iota" => Some(IOTA_FRAMEWORK_ADDRESS), + "iota_system" => Some(IOTA_SYSTEM_ADDRESS), + "stardust" => Some(STARDUST_ADDRESS), + "bridge" => Some(BRIDGE_ADDRESS), + _ => None, + } +} + +pub trait MoveTypeTagTrait { + fn get_type_tag() -> TypeTag; +} + +impl MoveTypeTagTrait for u8 { + fn get_type_tag() -> TypeTag { + TypeTag::U8 + } +} + +impl MoveTypeTagTrait for u64 { + fn get_type_tag() -> TypeTag { + TypeTag::U64 + } +} + +impl MoveTypeTagTrait for ObjectID { + fn get_type_tag() -> TypeTag { + TypeTag::Address + } +} + +impl MoveTypeTagTrait for IotaAddress { + fn get_type_tag() -> TypeTag { + TypeTag::Address + } +} + +impl MoveTypeTagTrait for Vec { + fn get_type_tag() -> TypeTag { + TypeTag::Vector(Box::new(T::get_type_tag())) + } +} diff --git a/identity_iota_interaction/src/sdk_types/iota_types/mod.rs b/identity_iota_interaction/src/sdk_types/iota_types/mod.rs new file mode 100644 index 0000000000..5dc4a92343 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/mod.rs @@ -0,0 +1,27 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub mod balance; +pub mod base_types; +pub mod coin; +pub mod collection_types; +pub mod crypto; +pub mod digests; +pub mod dynamic_field; +pub mod error; +pub mod event; +pub mod execution_status; +pub mod gas_coin; +pub mod governance; +pub mod id; +pub mod iota_serde; +pub mod iota_types_lib; +pub mod move_package; +pub mod object; +pub mod quorum_driver_types; +pub mod stardust; +pub mod timelock; +pub mod transaction; + +pub use iota_types_lib::*; +pub use super::move_core_types::{identifier::Identifier, language_storage::TypeTag}; \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/move_package.rs b/identity_iota_interaction/src/sdk_types/iota_types/move_package.rs new file mode 100644 index 0000000000..e7f63eb303 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/move_package.rs @@ -0,0 +1,84 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::BTreeMap; + +use serde::Deserialize; +use serde::Serialize; +use serde_with::{serde_as, Bytes}; + +use super::base_types::{ObjectID, SequenceNumber}; + +/// Identifies a struct and the module it was defined in +#[derive( +Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize, Hash)] +pub struct TypeOrigin { + pub module_name: String, + // `struct_name` alias to support backwards compatibility with the old name + #[serde(alias = "struct_name")] + pub datatype_name: String, + pub package: ObjectID, +} + +/// Upgraded package info for the linkage table +#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash)] +pub struct UpgradeInfo { + /// ID of the upgraded packages + pub upgraded_id: ObjectID, + /// Version of the upgraded package + pub upgraded_version: SequenceNumber, +} + +// serde_bytes::ByteBuf is an analog of Vec with built-in fast +// serialization. +#[serde_as] +#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash)] +pub struct MovePackage { + pub(crate) id: ObjectID, + /// Most move packages are uniquely identified by their ID (i.e. there is + /// only one version per ID), but the version is still stored because + /// one package may be an upgrade of another (at a different ID), in + /// which case its version will be one greater than the version of the + /// upgraded package. + /// + /// Framework packages are an exception to this rule -- all versions of the + /// framework packages exist at the same ID, at increasing versions. + /// + /// In all cases, packages are referred to by move calls using just their + /// ID, and they are always loaded at their latest version. + pub(crate) version: SequenceNumber, + // TODO use session cache + #[serde_as(as = "BTreeMap<_, Bytes>")] + pub(crate) module_map: BTreeMap>, + + /// Maps struct/module to a package version where it was first defined, + /// stored as a vector for simple serialization and deserialization. + pub(crate) type_origin_table: Vec, + + // For each dependency, maps original package ID to the info about the (upgraded) dependency + // version that this package is using + pub(crate) linkage_table: BTreeMap, +} + +impl MovePackage { + pub fn id(&self) -> ObjectID { + self.id + } + + pub fn version(&self) -> SequenceNumber { + self.version + } + + pub fn serialized_module_map(&self) -> &BTreeMap> { + &self.module_map + } + + pub fn type_origin_table(&self) -> &Vec { + &self.type_origin_table + } + + pub fn linkage_table(&self) -> &BTreeMap { + &self.linkage_table + } +} diff --git a/identity_iota_interaction/src/sdk_types/iota_types/object.rs b/identity_iota_interaction/src/sdk_types/iota_types/object.rs new file mode 100644 index 0000000000..4d74782e14 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/object.rs @@ -0,0 +1,109 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::{Display, Formatter}; + +use serde::Deserialize; +use serde::Serialize; + +use super::base_types::{IotaAddress, SequenceNumber, ObjectID}; +use super::error::{IotaResult, IotaError}; + +pub const OBJECT_START_VERSION: SequenceNumber = SequenceNumber::from_u64(1); + +#[derive( +Eq, PartialEq, Debug, Clone, Copy, Deserialize, Serialize, Hash, Ord, PartialOrd,)] +pub enum Owner { + /// Object is exclusively owned by a single address, and is mutable. + AddressOwner(IotaAddress), + /// Object is exclusively owned by a single object, and is mutable. + /// The object ID is converted to IotaAddress as IotaAddress is universal. + ObjectOwner(IotaAddress), + /// Object is shared, can be used by any address, and is mutable. + Shared { + /// The version at which the object became shared + initial_shared_version: SequenceNumber, + }, + /// Object is immutable, and hence ownership doesn't matter. + Immutable, +} + +impl Owner { + // NOTE: only return address of AddressOwner, otherwise return error, + // ObjectOwner's address is converted from object id, thus we will skip it. + pub fn get_address_owner_address(&self) -> IotaResult { + match self { + Self::AddressOwner(address) => Ok(*address), + Self::Shared { .. } | Self::Immutable | Self::ObjectOwner(_) => { + Err(IotaError::UnexpectedOwnerType) + } + } + } + + // NOTE: this function will return address of both AddressOwner and ObjectOwner, + // address of ObjectOwner is converted from object id, even though the type is + // IotaAddress. + pub fn get_owner_address(&self) -> IotaResult { + match self { + Self::AddressOwner(address) | Self::ObjectOwner(address) => Ok(*address), + Self::Shared { .. } | Self::Immutable => Err(IotaError::UnexpectedOwnerType), + } + } + + pub fn is_immutable(&self) -> bool { + matches!(self, Owner::Immutable) + } + + pub fn is_address_owned(&self) -> bool { + matches!(self, Owner::AddressOwner(_)) + } + + pub fn is_child_object(&self) -> bool { + matches!(self, Owner::ObjectOwner(_)) + } + + pub fn is_shared(&self) -> bool { + matches!(self, Owner::Shared { .. }) + } +} + +impl PartialEq for Owner { + fn eq(&self, other: &IotaAddress) -> bool { + match self { + Self::AddressOwner(address) => address == other, + Self::ObjectOwner(_) | Self::Shared { .. } | Self::Immutable => false, + } + } +} + +impl PartialEq for Owner { + fn eq(&self, other: &ObjectID) -> bool { + let other_id: IotaAddress = (*other).into(); + match self { + Self::ObjectOwner(id) => id == &other_id, + Self::AddressOwner(_) | Self::Shared { .. } | Self::Immutable => false, + } + } +} + +impl Display for Owner { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::AddressOwner(address) => { + write!(f, "Account Address ( {} )", address) + } + Self::ObjectOwner(address) => { + write!(f, "Object ID: ( {} )", address) + } + Self::Immutable => { + write!(f, "Immutable") + } + Self::Shared { + initial_shared_version, + } => { + write!(f, "Shared( {} )", initial_shared_version.value()) + } + } + } +} diff --git a/identity_iota_interaction/src/sdk_types/iota_types/quorum_driver_types.rs b/identity_iota_interaction/src/sdk_types/iota_types/quorum_driver_types.rs new file mode 100644 index 0000000000..3eb37fcbbe --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/quorum_driver_types.rs @@ -0,0 +1,12 @@ +// Copyright (c) 2021, Facebook, Inc. and its affiliates +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum ExecuteTransactionRequestType { + WaitForEffectsCert, + WaitForLocalExecution, +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/stardust/mod.rs b/identity_iota_interaction/src/sdk_types/iota_types/stardust/mod.rs new file mode 100644 index 0000000000..f6606f3f82 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/stardust/mod.rs @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub mod output; diff --git a/identity_iota_interaction/src/sdk_types/iota_types/stardust/output/mod.rs b/identity_iota_interaction/src/sdk_types/iota_types/stardust/output/mod.rs new file mode 100644 index 0000000000..28d547dcf2 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/stardust/output/mod.rs @@ -0,0 +1,6 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub mod nft; + +pub use nft::*; \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/stardust/output/nft.rs b/identity_iota_interaction/src/sdk_types/iota_types/stardust/output/nft.rs new file mode 100644 index 0000000000..b53b83d546 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/stardust/output/nft.rs @@ -0,0 +1,33 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::ident_str; + +use crate::sdk_types::move_types::language_storage::StructTag; +use crate::sdk_types::move_types::identifier::IdentStr; + +use super::super::super::STARDUST_ADDRESS; + +pub const IRC27_MODULE_NAME: &IdentStr = ident_str!("irc27"); +pub const NFT_MODULE_NAME: &IdentStr = ident_str!("nft"); +pub const NFT_OUTPUT_MODULE_NAME: &IdentStr = ident_str!("nft_output"); +pub const NFT_OUTPUT_STRUCT_NAME: &IdentStr = ident_str!("NftOutput"); +pub const NFT_STRUCT_NAME: &IdentStr = ident_str!("Nft"); +pub const IRC27_STRUCT_NAME: &IdentStr = ident_str!("Irc27Metadata"); +pub const NFT_DYNAMIC_OBJECT_FIELD_KEY: &[u8] = b"nft"; +pub const NFT_DYNAMIC_OBJECT_FIELD_KEY_TYPE: &str = "vector"; + +pub struct Nft {} + +impl Nft { + /// Returns the struct tag that represents the fully qualified path of an + /// [`Nft`] in its move package. + pub fn tag() -> StructTag { + StructTag { + address: STARDUST_ADDRESS.into(), + module: NFT_MODULE_NAME.to_owned(), + name: NFT_STRUCT_NAME.to_owned(), + type_params: Vec::new(), + } + } +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/timelock/mod.rs b/identity_iota_interaction/src/sdk_types/iota_types/timelock/mod.rs new file mode 100644 index 0000000000..9e825bb774 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/timelock/mod.rs @@ -0,0 +1,5 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub mod timelock; +pub mod timelocked_staked_iota; \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/timelock/timelock.rs b/identity_iota_interaction/src/sdk_types/iota_types/timelock/timelock.rs new file mode 100644 index 0000000000..65772e9e91 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/timelock/timelock.rs @@ -0,0 +1,124 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::Deserialize; +use serde::Serialize; + +use crate::ident_str; + +#[allow(unused)] // Kept in sync with original source, so keep as is. +use crate::sdk_types::move_types::{ + language_storage::{TypeTag, StructTag}, + identifier::{IdentStr}, +}; + +#[allow(unused)] // Kept in sync with original source, so keep as is. +use super::super::{ + IOTA_FRAMEWORK_ADDRESS, + IOTA_SYSTEM_ADDRESS, + base_types::{ObjectID, EpochId}, + balance::Balance, + governance::StakedIota, + id::UID, + error::IotaError, +}; + +#[allow(unused)] // Kept in sync with original source, so keep as is. +use super::timelocked_staked_iota::{TIMELOCKED_STAKED_IOTA_MODULE_NAME, TIMELOCKED_STAKED_IOTA_STRUCT_NAME}; + +pub const TIMELOCK_MODULE_NAME: &IdentStr = ident_str!("timelock"); +pub const TIMELOCK_STRUCT_NAME: &IdentStr = ident_str!("TimeLock"); + +/// Rust version of the Move stardust::TimeLock type. +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct TimeLock { + id: UID, + /// The locked object. + locked: T, + /// This is the epoch time stamp of when the lock expires. + expiration_timestamp_ms: u64, + /// Timelock related label. + label: Option, +} + +impl TimeLock { + /// Constructor. + pub fn new(id: UID, locked: T, expiration_timestamp_ms: u64, label: Option) -> Self { + Self { + id, + locked, + expiration_timestamp_ms, + label, + } + } + + /// Get the TimeLock's `type`. + pub fn type_(type_param: TypeTag) -> StructTag { + StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + module: TIMELOCK_MODULE_NAME.to_owned(), + name: TIMELOCK_STRUCT_NAME.to_owned(), + type_params: vec![type_param], + } + } + + /// Get the TimeLock's `id`. + pub fn id(&self) -> &ObjectID { + self.id.object_id() + } + + /// Get the TimeLock's `locked` object. + pub fn locked(&self) -> &T { + &self.locked + } + + /// Get the TimeLock's `expiration_timestamp_ms`. + pub fn expiration_timestamp_ms(&self) -> u64 { + self.expiration_timestamp_ms + } + + /// Get the TimeLock's `label``. + pub fn label(&self) -> &Option { + &self.label + } +} + +impl<'de, T> TimeLock + where + T: Serialize + Deserialize<'de>, +{ + /// Create a `TimeLock` from BCS bytes. + pub fn from_bcs_bytes(content: &'de [u8]) -> Result { + bcs::from_bytes(content).map_err(|err| IotaError::ObjectDeserialization { + error: format!("Unable to deserialize TimeLock object: {:?}", err), + }) + } + + /// Serialize a `TimeLock` as a `Vec` of BCS. + pub fn to_bcs_bytes(&self) -> Vec { + bcs::to_bytes(&self).unwrap() + } +} + +/// Is this other StructTag representing a TimeLock? +pub fn is_timelock(other: &StructTag) -> bool { + other.address == IOTA_FRAMEWORK_ADDRESS + && other.module.as_ident_str() == TIMELOCK_MODULE_NAME + && other.name.as_ident_str() == TIMELOCK_STRUCT_NAME +} + +/// Is this other StructTag representing a `TimeLock>`? +pub fn is_timelocked_balance(other: &StructTag) -> bool { + if !is_timelock(other) { + return false; + } + + if other.type_params.len() != 1 { + return false; + } + + match &other.type_params[0] { + TypeTag::Struct(tag) => Balance::is_balance(tag), + _ => false, + } +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/timelock/timelocked_staked_iota.rs b/identity_iota_interaction/src/sdk_types/iota_types/timelock/timelocked_staked_iota.rs new file mode 100644 index 0000000000..2c052b9f6b --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/timelock/timelocked_staked_iota.rs @@ -0,0 +1,85 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use crate::ident_str; +use crate::sdk_types::move_types::identifier::IdentStr; +use crate::sdk_types::move_types::language_storage::StructTag; +use super::super::{ + IOTA_SYSTEM_ADDRESS, + base_types::{ObjectID, EpochId}, + governance::StakedIota, + id::UID, +}; + +pub const TIMELOCKED_STAKED_IOTA_MODULE_NAME: &IdentStr = ident_str!("timelocked_staking"); +pub const TIMELOCKED_STAKED_IOTA_STRUCT_NAME: &IdentStr = ident_str!("TimelockedStakedIota"); + +/// Rust version of the Move +/// stardust::timelocked_staked_iota::TimelockedStakedIota type. +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct TimelockedStakedIota { + id: UID, + /// A self-custodial object holding the staked IOTA tokens. + staked_iota: StakedIota, + /// This is the epoch time stamp of when the lock expires. + expiration_timestamp_ms: u64, + /// Timelock related label. + label: Option, +} + +impl TimelockedStakedIota { + /// Get the TimeLock's `type`. + pub fn type_() -> StructTag { + StructTag { + address: IOTA_SYSTEM_ADDRESS, + module: TIMELOCKED_STAKED_IOTA_MODULE_NAME.to_owned(), + name: TIMELOCKED_STAKED_IOTA_STRUCT_NAME.to_owned(), + type_params: vec![], + } + } + + /// Is this other StructTag representing a TimelockedStakedIota? + pub fn is_timelocked_staked_iota(s: &StructTag) -> bool { + s.address == IOTA_SYSTEM_ADDRESS + && s.module.as_ident_str() == TIMELOCKED_STAKED_IOTA_MODULE_NAME + && s.name.as_ident_str() == TIMELOCKED_STAKED_IOTA_STRUCT_NAME + && s.type_params.is_empty() + } + + /// Get the TimelockedStakedIota's `id`. + pub fn id(&self) -> ObjectID { + self.id.id.bytes + } + + /// Get the wrapped StakedIota's `pool_id`. + pub fn pool_id(&self) -> ObjectID { + self.staked_iota.pool_id() + } + + /// Get the wrapped StakedIota's `activation_epoch`. + pub fn activation_epoch(&self) -> EpochId { + self.staked_iota.activation_epoch() + } + + /// Get the wrapped StakedIota's `request_epoch`. + pub fn request_epoch(&self) -> EpochId { + // TODO: this might change when we implement warm up period. + self.staked_iota.activation_epoch().saturating_sub(1) + } + + /// Get the wrapped StakedIota's `principal`. + pub fn principal(&self) -> u64 { + self.staked_iota.principal() + } + + /// Get the TimelockedStakedIota's `expiration_timestamp_ms`. + pub fn expiration_timestamp_ms(&self) -> u64 { + self.expiration_timestamp_ms + } + + /// Get the TimelockedStakedIota's `label``. + pub fn label(&self) -> &Option { + &self.label + } +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/iota_types/transaction.rs b/identity_iota_interaction/src/sdk_types/iota_types/transaction.rs new file mode 100644 index 0000000000..6eac4bfd6c --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/iota_types/transaction.rs @@ -0,0 +1,166 @@ +// Copyright (c) 2021, Facebook, Inc. and its affiliates +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::{Display, Formatter}; + +use serde::{Deserialize, Serialize}; + +use super::super::move_core_types::identifier::Identifier; +use super::super::move_core_types::language_storage::TypeTag; + +use super::base_types::{ObjectID, ObjectRef, SequenceNumber}; +use super::{ + IOTA_CLOCK_OBJECT_ID, + IOTA_CLOCK_OBJECT_SHARED_VERSION, + IOTA_AUTHENTICATOR_STATE_OBJECT_ID, + IOTA_AUTHENTICATOR_STATE_OBJECT_SHARED_VERSION, + IOTA_SYSTEM_STATE_OBJECT_ID, + IOTA_SYSTEM_STATE_OBJECT_SHARED_VERSION, +}; + +pub const TEST_ONLY_GAS_UNIT_FOR_TRANSFER: u64 = 10_000; +pub const TEST_ONLY_GAS_UNIT_FOR_OBJECT_BASICS: u64 = 50_000; +pub const TEST_ONLY_GAS_UNIT_FOR_PUBLISH: u64 = 70_000; +pub const TEST_ONLY_GAS_UNIT_FOR_STAKING: u64 = 50_000; +pub const TEST_ONLY_GAS_UNIT_FOR_GENERIC: u64 = 50_000; +pub const TEST_ONLY_GAS_UNIT_FOR_SPLIT_COIN: u64 = 10_000; +// For some transactions we may either perform heavy operations or touch +// objects that are storage expensive. That may happen (and often is the case) +// because the object touched are set up in genesis and carry no storage cost +// (and thus rebate) on first usage. +pub const TEST_ONLY_GAS_UNIT_FOR_HEAVY_COMPUTATION_STORAGE: u64 = 5_000_000; + +pub const GAS_PRICE_FOR_SYSTEM_TX: u64 = 1; + +pub const DEFAULT_VALIDATOR_GAS_PRICE: u64 = 1000; + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] +pub enum CallArg { + // contains no structs or objects + Pure(Vec), + // an object + Object(ObjectArg), +} + +impl CallArg { + pub const IOTA_SYSTEM_MUT: Self = Self::Object(ObjectArg::IOTA_SYSTEM_MUT); + pub const CLOCK_IMM: Self = Self::Object(ObjectArg::SharedObject { + id: IOTA_CLOCK_OBJECT_ID, + initial_shared_version: IOTA_CLOCK_OBJECT_SHARED_VERSION, + mutable: false, + }); + pub const CLOCK_MUT: Self = Self::Object(ObjectArg::SharedObject { + id: IOTA_CLOCK_OBJECT_ID, + initial_shared_version: IOTA_CLOCK_OBJECT_SHARED_VERSION, + mutable: true, + }); + pub const AUTHENTICATOR_MUT: Self = Self::Object(ObjectArg::SharedObject { + id: IOTA_AUTHENTICATOR_STATE_OBJECT_ID, + initial_shared_version: IOTA_AUTHENTICATOR_STATE_OBJECT_SHARED_VERSION, + mutable: true, + }); +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] +pub enum ObjectArg { + // A Move object, either immutable, or owned mutable. + ImmOrOwnedObject(ObjectRef), + // A Move object that's shared. + // SharedObject::mutable controls whether caller asks for a mutable reference to shared + // object. + SharedObject { + id: ObjectID, + initial_shared_version: SequenceNumber, + mutable: bool, + }, + // A Move object that can be received in this transaction. + Receiving(ObjectRef), +} + +impl ObjectArg { + pub const IOTA_SYSTEM_MUT: Self = Self::SharedObject { + id: IOTA_SYSTEM_STATE_OBJECT_ID, + initial_shared_version: IOTA_SYSTEM_STATE_OBJECT_SHARED_VERSION, + mutable: true, + }; +} + +/// A single command in a programmable transaction. +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] +pub enum Command { + /// A call to either an entry or a public Move function + MoveCall(Box), + /// `(Vec, address)` + /// It sends n-objects to the specified address. These objects must have + /// store (public transfer) and either the previous owner must be an + /// address or the object must be newly created. + TransferObjects(Vec, Argument), + /// `(&mut Coin, Vec)` -> `Vec>` + /// It splits off some amounts into a new coins with those amounts + SplitCoins(Argument, Vec), + /// `(&mut Coin, Vec>)` + /// It merges n-coins into the first coin + MergeCoins(Argument, Vec), + /// Publishes a Move package. It takes the package bytes and a list of the + /// package's transitive dependencies to link against on-chain. + Publish(Vec>, Vec), + /// `forall T: Vec -> vector` + /// Given n-values of the same type, it constructs a vector. For non objects + /// or an empty vector, the type tag must be specified. + MakeMoveVec(Option, Vec), + /// Upgrades a Move package + /// Takes (in order): + /// 1. A vector of serialized modules for the package. + /// 2. A vector of object ids for the transitive dependencies of the new + /// package. + /// 3. The object ID of the package being upgraded. + /// 4. An argument holding the `UpgradeTicket` that must have been produced + /// from an earlier command in the same programmable transaction. + Upgrade(Vec>, Vec, ObjectID, Argument), +} + +/// An argument to a programmable transaction command +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] +pub enum Argument { + /// The gas coin. The gas coin can only be used by-ref, except for with + /// `TransferObjects`, which can use it by-value. + GasCoin, + /// One of the input objects or primitive values (from + /// `ProgrammableTransaction` inputs) + Input(u16), + /// The result of another command (from `ProgrammableTransaction` commands) + Result(u16), + /// Like a `Result` but it accesses a nested result. Currently, the only + /// usage of this is to access a value from a Move call with multiple + /// return values. + NestedResult(u16, u16), +} + +/// The command for calling a Move function, either an entry function or a +/// public function (which cannot return references). +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] +pub struct ProgrammableMoveCall { + /// The package containing the module and function. + pub package: ObjectID, + /// The specific module in the package containing the function. + pub module: Identifier, + /// The function to be called. + pub function: Identifier, + /// The type arguments to the function. + pub type_arguments: Vec, + /// The arguments to the function. + pub arguments: Vec, +} + +impl Display for Argument { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Argument::GasCoin => write!(f, "GasCoin"), + Argument::Input(i) => write!(f, "Input({i})"), + Argument::Result(i) => write!(f, "Result({i})"), + Argument::NestedResult(i, j) => write!(f, "NestedResult({i},{j})"), + } + } +} diff --git a/identity_iota_interaction/src/sdk_types/mod.rs b/identity_iota_interaction/src/sdk_types/mod.rs new file mode 100644 index 0000000000..57d30805bc --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/mod.rs @@ -0,0 +1,18 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#[path = "iota_json_rpc_types/mod.rs"] +pub mod rpc_types; +#[path = "iota_types/mod.rs"] +pub mod types; +#[path = "move_core_types/mod.rs"] +pub mod move_types; + +pub mod move_command_line_common; +pub mod shared_crypto; +pub mod error; +pub mod generated_types; + +pub(crate) use types as iota_types; +pub(crate) use move_types as move_core_types; +pub(crate) use rpc_types as iota_json_rpc_types; \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/move_command_line_common/address.rs b/identity_iota_interaction/src/sdk_types/move_command_line_common/address.rs new file mode 100644 index 0000000000..9816b018e2 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_command_line_common/address.rs @@ -0,0 +1,195 @@ +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{fmt, hash::Hash}; +use std::option::Option::{self, Some, None}; +use std::string::String; + +use num_bigint::BigUint; +use anyhow::anyhow; + +use super::super::move_core_types::account_address::AccountAddress; +use super::parser::{NumberFormat, parse_address_number}; + +// Parsed Address, either a name or a numerical address +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum ParsedAddress { + Named(String), + Numerical(NumericalAddress), +} + +/// Numerical address represents non-named address values +/// or the assigned value of a named address +#[derive(Clone, Copy)] +pub struct NumericalAddress { + /// the number for the address + bytes: AccountAddress, + /// The format (e.g. decimal or hex) for displaying the number + format: NumberFormat, +} + +impl ParsedAddress { + pub fn into_account_address( + self, + mapping: &impl Fn(&str) -> Option, + ) -> anyhow::Result { + match self { + Self::Named(n) => { + mapping(n.as_str()).ok_or_else(|| anyhow!("Unbound named address: '{}'", n)) + } + Self::Numerical(a) => Ok(a.into_inner()), + } + } +} + +impl NumericalAddress { + // bytes used for errors when an address is not known but is needed + pub const DEFAULT_ERROR_ADDRESS: Self = NumericalAddress { + bytes: AccountAddress::ONE, + format: NumberFormat::Hex, + }; + + pub const fn new(bytes: [u8; AccountAddress::LENGTH], format: NumberFormat) -> Self { + Self { + bytes: AccountAddress::new(bytes), + format, + } + } + + pub fn into_inner(self) -> AccountAddress { + self.bytes + } + + pub fn into_bytes(self) -> [u8; AccountAddress::LENGTH] { + self.bytes.into_bytes() + } + + pub fn parse_str(s: &str) -> Result { + match parse_address_number(s) { + Some((n, format)) => Ok(NumericalAddress { + bytes: AccountAddress::new(n), + format, + }), + None => + // TODO the kind of error is in an unstable nightly API + // But currently the only way this should fail is if the number is too long + { + Err(format!( + "Invalid address literal. The numeric value is too large. \ + The maximum size is {} bytes", + AccountAddress::LENGTH, + )) + } + } + } +} + +impl AsRef<[u8]> for NumericalAddress { + fn as_ref(&self) -> &[u8] { + self.bytes.as_ref() + } +} + +impl fmt::Display for NumericalAddress { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.format { + NumberFormat::Decimal => { + let n = BigUint::from_bytes_be(self.bytes.as_ref()); + write!(f, "{}", n) + } + NumberFormat::Hex => write!(f, "{:#X}", self), + } + } +} + +impl fmt::Debug for NumericalAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl fmt::UpperHex for NumericalAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let encoded = hex::encode_upper(self.as_ref()); + let dropped = encoded + .chars() + .skip_while(|c| c == &'0') + .collect::(); + let prefix = if f.alternate() { "0x" } else { "" }; + if dropped.is_empty() { + write!(f, "{}0", prefix) + } else { + write!(f, "{}{}", prefix, dropped) + } + } +} + +impl fmt::LowerHex for NumericalAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let encoded = hex::encode(self.as_ref()); + let dropped = encoded + .chars() + .skip_while(|c| c == &'0') + .collect::(); + let prefix = if f.alternate() { "0x" } else { "" }; + if dropped.is_empty() { + write!(f, "{}0", prefix) + } else { + write!(f, "{}{}", prefix, dropped) + } + } +} + +impl fmt::Display for ParsedAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Named(n) => write!(f, "{n}"), + Self::Numerical(a) => write!(f, "{a}"), + } + } +} + +impl PartialOrd for NumericalAddress { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for NumericalAddress { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let Self { + bytes: self_bytes, + format: _, + } = self; + let Self { + bytes: other_bytes, + format: _, + } = other; + self_bytes.cmp(other_bytes) + } +} + +impl PartialEq for NumericalAddress { + fn eq(&self, other: &Self) -> bool { + let Self { + bytes: self_bytes, + format: _, + } = self; + let Self { + bytes: other_bytes, + format: _, + } = other; + self_bytes == other_bytes + } +} +impl Eq for NumericalAddress {} + +impl Hash for NumericalAddress { + fn hash(&self, state: &mut H) { + let Self { + bytes: self_bytes, + format: _, + } = self; + self_bytes.hash(state) + } +} diff --git a/identity_iota_interaction/src/sdk_types/move_command_line_common/mod.rs b/identity_iota_interaction/src/sdk_types/move_command_line_common/mod.rs new file mode 100644 index 0000000000..c1c903c737 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_command_line_common/mod.rs @@ -0,0 +1,9 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#![forbid(unsafe_code)] + +pub mod address; +pub mod parser; +pub mod values; +pub mod types; \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/move_command_line_common/parser.rs b/identity_iota_interaction/src/sdk_types/move_command_line_common/parser.rs new file mode 100644 index 0000000000..956b833039 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_command_line_common/parser.rs @@ -0,0 +1,750 @@ +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{fmt::Display, iter::Peekable, num::ParseIntError}; + +use anyhow::{anyhow, bail, Result}; +use super::super::{ + move_core_types::{ + account_address::AccountAddress, + u256::{U256FromStrError, U256}, + }, +}; +use num_bigint::BigUint; + +use super::{ + address::{NumericalAddress, ParsedAddress}, + types::{ParsedFqName, ParsedModuleId, ParsedStructType, ParsedType, TypeToken}, + values::{ParsableValue, ParsedValue, ValueToken}, +}; + +const MAX_TYPE_DEPTH: u64 = 128; +const MAX_TYPE_NODE_COUNT: u64 = 256; + +pub trait Token: Display + Copy + Eq { + fn is_whitespace(&self) -> bool; + fn next_token(s: &str) -> Result>; + fn tokenize(mut s: &str) -> Result> { + let mut v = vec![]; + while let Some((tok, n)) = Self::next_token(s)? { + v.push((tok, &s[..n])); + s = &s[n..]; + } + Ok(v) + } +} + +pub struct Parser<'a, Tok: Token, I: Iterator> { + count: u64, + it: Peekable, +} + +impl ParsedType { + pub fn parse(s: &str) -> Result { + parse(s, |parser| parser.parse_type()) + } +} + +impl ParsedModuleId { + pub fn parse(s: &str) -> Result { + parse(s, |parser| parser.parse_module_id()) + } +} + +impl ParsedFqName { + pub fn parse(s: &str) -> Result { + parse(s, |parser| parser.parse_fq_name()) + } +} + +impl ParsedStructType { + pub fn parse(s: &str) -> Result { + let ty = parse(s, |parser| parser.parse_type()) + .map_err(|e| anyhow!("Invalid struct type: {}. Got error: {}", s, e))?; + match ty { + ParsedType::Struct(s) => Ok(s), + _ => bail!("Invalid struct type: {}", s), + } + } +} + +impl ParsedAddress { + pub fn parse(s: &str) -> Result { + parse(s, |parser| parser.parse_address()) + } +} + +impl ParsedValue { + pub fn parse(s: &str) -> Result> { + parse(s, |parser| parser.parse_value()) + } +} + +fn parse<'a, Tok: Token, R>( + s: &'a str, + f: impl FnOnce(&mut Parser<'a, Tok, std::vec::IntoIter<(Tok, &'a str)>>) -> Result, +) -> Result { + let tokens: Vec<_> = Tok::tokenize(s)? + .into_iter() + .filter(|(tok, _)| !tok.is_whitespace()) + .collect(); + let mut parser = Parser::new(tokens); + let res = f(&mut parser)?; + if let Ok((_, contents)) = parser.advance_any() { + bail!("Expected end of token stream. Got: {}", contents) + } + Ok(res) +} + +impl<'a, Tok: Token, I: Iterator> Parser<'a, Tok, I> { + pub fn new>(v: T) -> Self { + Self { + count: 0, + it: v.into_iter().peekable(), + } + } + + pub fn advance_any(&mut self) -> Result<(Tok, &'a str)> { + match self.it.next() { + Some(tok) => Ok(tok), + None => bail!("unexpected end of tokens"), + } + } + + pub fn advance(&mut self, expected_token: Tok) -> Result<&'a str> { + let (t, contents) = self.advance_any()?; + if t != expected_token { + bail!("expected token {}, got {}", expected_token, t) + } + Ok(contents) + } + + pub fn peek(&mut self) -> Option<(Tok, &'a str)> { + self.it.peek().copied() + } + + pub fn peek_tok(&mut self) -> Option { + self.it.peek().map(|(tok, _)| *tok) + } + + pub fn parse_list( + &mut self, + parse_list_item: impl Fn(&mut Self) -> Result, + delim: Tok, + end_token: Tok, + allow_trailing_delim: bool, + ) -> Result> { + let is_end = + |tok_opt: Option| -> bool { tok_opt.map(|tok| tok == end_token).unwrap_or(true) }; + let mut v = vec![]; + while !is_end(self.peek_tok()) { + v.push(parse_list_item(self)?); + if is_end(self.peek_tok()) { + break; + } + self.advance(delim)?; + if is_end(self.peek_tok()) && allow_trailing_delim { + break; + } + } + Ok(v) + } +} + +impl<'a, I: Iterator> Parser<'a, TypeToken, I> { + pub fn parse_module_id(&mut self) -> Result { + let (tok, contents) = self.advance_any()?; + self.parse_module_id_impl(tok, contents) + } + + pub fn parse_fq_name(&mut self) -> Result { + let (tok, contents) = self.advance_any()?; + self.parse_fq_name_impl(tok, contents) + } + + pub fn parse_type(&mut self) -> Result { + self.parse_type_impl(0) + } + + pub fn parse_module_id_impl( + &mut self, + tok: TypeToken, + contents: &'a str, + ) -> Result { + let tok = match tok { + TypeToken::Ident => ValueToken::Ident, + TypeToken::AddressIdent => ValueToken::Number, + tok => bail!("unexpected token {tok}, expected address"), + }; + let address = parse_address_impl(tok, contents)?; + self.advance(TypeToken::ColonColon)?; + let name = self.advance(TypeToken::Ident)?.to_owned(); + Ok(ParsedModuleId { address, name }) + } + + pub fn parse_fq_name_impl( + &mut self, + tok: TypeToken, + contents: &'a str, + ) -> Result { + let module = self.parse_module_id_impl(tok, contents)?; + self.advance(TypeToken::ColonColon)?; + let name = self.advance(TypeToken::Ident)?.to_owned(); + Ok(ParsedFqName { module, name }) + } + + fn parse_type_impl(&mut self, depth: u64) -> Result { + self.count += 1; + + if depth > MAX_TYPE_DEPTH || self.count > MAX_TYPE_NODE_COUNT { + bail!("Type exceeds maximum nesting depth or node count") + } + + Ok(match self.advance_any()? { + (TypeToken::Ident, "u8") => ParsedType::U8, + (TypeToken::Ident, "u16") => ParsedType::U16, + (TypeToken::Ident, "u32") => ParsedType::U32, + (TypeToken::Ident, "u64") => ParsedType::U64, + (TypeToken::Ident, "u128") => ParsedType::U128, + (TypeToken::Ident, "u256") => ParsedType::U256, + (TypeToken::Ident, "bool") => ParsedType::Bool, + (TypeToken::Ident, "address") => ParsedType::Address, + (TypeToken::Ident, "signer") => ParsedType::Signer, + (TypeToken::Ident, "vector") => { + self.advance(TypeToken::Lt)?; + let ty = self.parse_type_impl(depth + 1)?; + self.advance(TypeToken::Gt)?; + ParsedType::Vector(Box::new(ty)) + } + + (tok @ (TypeToken::Ident | TypeToken::AddressIdent), contents) => { + let fq_name = self.parse_fq_name_impl(tok, contents)?; + let type_args = match self.peek_tok() { + Some(TypeToken::Lt) => { + self.advance(TypeToken::Lt)?; + let type_args = self.parse_list( + |parser| parser.parse_type_impl(depth + 1), + TypeToken::Comma, + TypeToken::Gt, + true, + )?; + self.advance(TypeToken::Gt)?; + type_args + } + _ => vec![], + }; + ParsedType::Struct(ParsedStructType { fq_name, type_args }) + } + (tok, _) => bail!("unexpected token {tok}, expected type"), + }) + } +} + +impl<'a, I: Iterator> Parser<'a, ValueToken, I> { + pub fn parse_value(&mut self) -> Result> { + if let Some(extra) = Extra::parse_value(self) { + return Ok(ParsedValue::Custom(extra?)); + } + let (tok, contents) = self.advance_any()?; + Ok(match tok { + ValueToken::Number if !matches!(self.peek_tok(), Some(ValueToken::ColonColon)) => { + let (u, _) = parse_u256(contents)?; + ParsedValue::InferredNum(u) + } + ValueToken::NumberTyped => { + if let Some(s) = contents.strip_suffix("u8") { + let (u, _) = parse_u8(s)?; + ParsedValue::U8(u) + } else if let Some(s) = contents.strip_suffix("u16") { + let (u, _) = parse_u16(s)?; + ParsedValue::U16(u) + } else if let Some(s) = contents.strip_suffix("u32") { + let (u, _) = parse_u32(s)?; + ParsedValue::U32(u) + } else if let Some(s) = contents.strip_suffix("u64") { + let (u, _) = parse_u64(s)?; + ParsedValue::U64(u) + } else if let Some(s) = contents.strip_suffix("u128") { + let (u, _) = parse_u128(s)?; + ParsedValue::U128(u) + } else { + let (u, _) = parse_u256(contents.strip_suffix("u256").unwrap())?; + ParsedValue::U256(u) + } + } + ValueToken::True => ParsedValue::Bool(true), + ValueToken::False => ParsedValue::Bool(false), + + ValueToken::ByteString => { + let contents = contents + .strip_prefix("b\"") + .unwrap() + .strip_suffix('\"') + .unwrap(); + ParsedValue::Vector( + contents + .as_bytes() + .iter() + .copied() + .map(ParsedValue::U8) + .collect(), + ) + } + ValueToken::HexString => { + let contents = contents + .strip_prefix("x\"") + .unwrap() + .strip_suffix('\"') + .unwrap() + .to_ascii_lowercase(); + ParsedValue::Vector( + hex::decode(contents) + .unwrap() + .into_iter() + .map(ParsedValue::U8) + .collect(), + ) + } + ValueToken::Utf8String => { + let contents = contents + .strip_prefix('\"') + .unwrap() + .strip_suffix('\"') + .unwrap(); + ParsedValue::Vector( + contents + .as_bytes() + .iter() + .copied() + .map(ParsedValue::U8) + .collect(), + ) + } + + ValueToken::AtSign => ParsedValue::Address(self.parse_address()?), + + ValueToken::Ident if contents == "vector" => { + self.advance(ValueToken::LBracket)?; + let values = self.parse_list( + |parser| parser.parse_value(), + ValueToken::Comma, + ValueToken::RBracket, + true, + )?; + self.advance(ValueToken::RBracket)?; + ParsedValue::Vector(values) + } + + ValueToken::Ident if contents == "struct" => { + self.advance(ValueToken::LParen)?; + let values = self.parse_list( + |parser| parser.parse_value(), + ValueToken::Comma, + ValueToken::RParen, + true, + )?; + self.advance(ValueToken::RParen)?; + ParsedValue::Struct(values) + } + + _ => bail!("unexpected token {}, expected type", tok), + }) + } + + pub fn parse_address(&mut self) -> Result { + let (tok, contents) = self.advance_any()?; + parse_address_impl(tok, contents) + } +} + +pub fn parse_address_impl(tok: ValueToken, contents: &str) -> Result { + Ok(match tok { + ValueToken::Number => { + ParsedAddress::Numerical(NumericalAddress::parse_str(contents).map_err(|s| { + anyhow!( + "Failed to parse numerical address '{}'. Got error: {}", + contents, + s + ) + })?) + } + ValueToken::Ident => ParsedAddress::Named(contents.to_owned()), + _ => bail!("unexpected token {}, expected identifier or number", tok), + }) +} + +#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] +#[repr(u32)] +/// Number format enum, the u32 value represents the base +pub enum NumberFormat { + Decimal = 10, + Hex = 16, +} + +// Determines the base of the number literal, depending on the prefix +pub(crate) fn determine_num_text_and_base(s: &str) -> (&str, NumberFormat) { + match s.strip_prefix("0x") { + Some(s_hex) => (s_hex, NumberFormat::Hex), + None => (s, NumberFormat::Decimal), + } +} + +// Parse a u8 from a decimal or hex encoding +pub fn parse_u8(s: &str) -> Result<(u8, NumberFormat), ParseIntError> { + let (txt, base) = determine_num_text_and_base(s); + Ok(( + u8::from_str_radix(&txt.replace('_', ""), base as u32)?, + base, + )) +} + +// Parse a u16 from a decimal or hex encoding +pub fn parse_u16(s: &str) -> Result<(u16, NumberFormat), ParseIntError> { + let (txt, base) = determine_num_text_and_base(s); + Ok(( + u16::from_str_radix(&txt.replace('_', ""), base as u32)?, + base, + )) +} + +// Parse a u32 from a decimal or hex encoding +pub fn parse_u32(s: &str) -> Result<(u32, NumberFormat), ParseIntError> { + let (txt, base) = determine_num_text_and_base(s); + Ok(( + u32::from_str_radix(&txt.replace('_', ""), base as u32)?, + base, + )) +} + +// Parse a u64 from a decimal or hex encoding +pub fn parse_u64(s: &str) -> Result<(u64, NumberFormat), ParseIntError> { + let (txt, base) = determine_num_text_and_base(s); + Ok(( + u64::from_str_radix(&txt.replace('_', ""), base as u32)?, + base, + )) +} + +// Parse a u128 from a decimal or hex encoding +pub fn parse_u128(s: &str) -> Result<(u128, NumberFormat), ParseIntError> { + let (txt, base) = determine_num_text_and_base(s); + Ok(( + u128::from_str_radix(&txt.replace('_', ""), base as u32)?, + base, + )) +} + +// Parse a u256 from a decimal or hex encoding +pub fn parse_u256(s: &str) -> Result<(U256, NumberFormat), U256FromStrError> { + let (txt, base) = determine_num_text_and_base(s); + Ok(( + U256::from_str_radix(&txt.replace('_', ""), base as u32)?, + base, + )) +} + +// Parse an address from a decimal or hex encoding +pub fn parse_address_number(s: &str) -> Option<([u8; AccountAddress::LENGTH], NumberFormat)> { + let (txt, base) = determine_num_text_and_base(s); + let parsed = BigUint::parse_bytes( + txt.as_bytes(), + match base { + NumberFormat::Hex => 16, + NumberFormat::Decimal => 10, + }, + )?; + let bytes = parsed.to_bytes_be(); + if bytes.len() > AccountAddress::LENGTH { + return None; + } + let mut result = [0u8; AccountAddress::LENGTH]; + result[(AccountAddress::LENGTH - bytes.len())..].clone_from_slice(&bytes); + Some((result, base)) +} + +#[cfg(test)] +mod tests { + use move_core_types::{account_address::AccountAddress, identifier::Identifier, u256::U256}; + use proptest::{prelude::*, proptest}; + + use crate::{ + address::{NumericalAddress, ParsedAddress}, + types::{ParsedStructType, ParsedType}, + values::ParsedValue, + }; + + #[allow(clippy::unreadable_literal)] + #[test] + fn tests_parse_value_positive() { + use ParsedValue as V; + let cases: &[(&str, V)] = &[ + (" 0u8", V::U8(0)), + ("0u8", V::U8(0)), + ("0xF_Fu8", V::U8(255)), + ("0xF__FF__Eu16", V::U16(u16::MAX - 1)), + ("0xFFF_FF__FF_Cu32", V::U32(u32::MAX - 3)), + ("255u8", V::U8(255)), + ("255u256", V::U256(U256::from(255u64))), + ("0", V::InferredNum(U256::from(0u64))), + ("0123", V::InferredNum(U256::from(123u64))), + ("0xFF", V::InferredNum(U256::from(0xFFu64))), + ("0xF_F", V::InferredNum(U256::from(0xFFu64))), + ("0xFF__", V::InferredNum(U256::from(0xFFu64))), + ( + "0x12_34__ABCD_FF", + V::InferredNum(U256::from(0x1234ABCDFFu64)), + ), + ("0u64", V::U64(0)), + ("0x0u64", V::U64(0)), + ( + "18446744073709551615", + V::InferredNum(U256::from(18446744073709551615u128)), + ), + ("18446744073709551615u64", V::U64(18446744073709551615)), + ("0u128", V::U128(0)), + ("1_0u8", V::U8(1_0)), + ("10_u8", V::U8(10)), + ("1_000u64", V::U64(1_000)), + ("1_000", V::InferredNum(U256::from(1_000u32))), + ("1_0_0_0u64", V::U64(1_000)), + ("1_000_000u128", V::U128(1_000_000)), + ( + "340282366920938463463374607431768211455u128", + V::U128(340282366920938463463374607431768211455), + ), + ("true", V::Bool(true)), + ("false", V::Bool(false)), + ( + "@0x0", + V::Address(ParsedAddress::Numerical(NumericalAddress::new( + AccountAddress::from_hex_literal("0x0") + .unwrap() + .into_bytes(), + crate::parser::NumberFormat::Hex, + ))), + ), + ( + "@0", + V::Address(ParsedAddress::Numerical(NumericalAddress::new( + AccountAddress::from_hex_literal("0x0") + .unwrap() + .into_bytes(), + crate::parser::NumberFormat::Hex, + ))), + ), + ( + "@0x54afa3526", + V::Address(ParsedAddress::Numerical(NumericalAddress::new( + AccountAddress::from_hex_literal("0x54afa3526") + .unwrap() + .into_bytes(), + crate::parser::NumberFormat::Hex, + ))), + ), + ( + "b\"hello\"", + V::Vector("hello".as_bytes().iter().copied().map(V::U8).collect()), + ), + ("x\"7fff\"", V::Vector(vec![V::U8(0x7f), V::U8(0xff)])), + ("x\"\"", V::Vector(vec![])), + ("x\"00\"", V::Vector(vec![V::U8(0x00)])), + ( + "x\"deadbeef\"", + V::Vector(vec![V::U8(0xde), V::U8(0xad), V::U8(0xbe), V::U8(0xef)]), + ), + ]; + + for (s, expected) in cases { + assert_eq!(&ParsedValue::parse(s).unwrap(), expected) + } + } + + #[test] + fn tests_parse_value_negative() { + /// Test cases for the parser that should always fail. + const PARSE_VALUE_NEGATIVE_TEST_CASES: &[&str] = &[ + "-3", + "0u42", + "0u645", + "0u64x", + "0u6 4", + "0u", + "_10", + "_10_u8", + "_10__u8", + "10_u8__", + "0xFF_u8_", + "0xF_u8__", + "0x_F_u8__", + "_", + "__", + "__4", + "_u8", + "5_bool", + "256u8", + "4294967296u32", + "65536u16", + "18446744073709551616u64", + "340282366920938463463374607431768211456u128", + "340282366920938463463374607431768211456340282366920938463463374607431768211456340282366920938463463374607431768211456340282366920938463463374607431768211456u256", + "0xg", + "0x00g0", + "0x", + "0x_", + "", + "@@", + "()", + "x\"ffff", + "x\"a \"", + "x\" \"", + "x\"0g\"", + "x\"0\"", + "garbage", + "true3", + "3false", + "3 false", + "", + "0XFF", + "0X0", + ]; + + for s in PARSE_VALUE_NEGATIVE_TEST_CASES { + assert!( + ParsedValue::<()>::parse(s).is_err(), + "Unexpectedly succeeded in parsing: {}", + s + ) + } + } + + #[test] + fn test_parse_type_negative() { + for s in &[ + "_", + "_::_::_", + "0x1::_", + "0x1::__::_", + "0x1::_::__", + "0x1::_::foo", + "0x1::foo::_", + "0x1::_::_", + "0x1::bar::foo<0x1::_::foo>", + ] { + assert!( + ParsedType::parse(s).is_err(), + "Parsed type {s} but should have failed" + ); + } + } + + #[test] + fn test_parse_struct_negative() { + for s in &[ + "_", + "_::_::_", + "0x1::_", + "0x1::__::_", + "0x1::_::__", + "0x1::_::foo", + "0x1::foo::_", + "0x1::_::_", + "0x1::bar::foo<0x1::_::foo>", + ] { + assert!( + ParsedStructType::parse(s).is_err(), + "Parsed type {s} but should have failed" + ); + } + } + + #[test] + fn test_type_type() { + for s in &[ + "u64", + "bool", + "vector", + "vector>", + "address", + "signer", + "0x1::M::S", + "0x2::M::S_", + "0x3::M_::S", + "0x4::M_::S_", + "0x00000000004::M::S", + "0x1::M::S", + "0x1::M::S<0x2::P::Q>", + "vector<0x1::M::S>", + "vector<0x1::M_::S_>", + "vector>", + "0x1::M::S>", + "0x1::_bar::_BAR", + "0x1::__::__", + "0x1::_bar::_BAR<0x2::_____::______fooo______>", + "0x1::__::__<0x2::_____::______fooo______, 0xff::Bar____::_______foo>", + ] { + assert!(ParsedType::parse(s).is_ok(), "Failed to parse type {}", s); + } + } + + #[test] + fn test_parse_valid_struct_type() { + let valid = vec![ + "0x1::Foo::Foo", + "0x1::Foo_Type::Foo", + "0x1::Foo_::Foo", + "0x1::X_123::X32_", + "0x1::Foo::Foo_Type", + "0x1::Foo::Foo<0x1::ABC::ABC>", + "0x1::Foo::Foo<0x1::ABC::ABC_Type>", + "0x1::Foo::Foo", + "0x1::Foo::Foo", + "0x1::Foo::Foo", + "0x1::Foo::Foo", + "0x1::Foo::Foo", + "0x1::Foo::Foo", + "0x1::Foo::Foo", + "0x1::Foo::Foo

", + "0x1::Foo::Foo", + "0x1::Foo::Foo>", + "0x1::Foo::Foo", + "0x1::Foo::Foo", + "0x1::Foo::Foo", + "0x1::Foo::Foo,address,signer>", + "0x1::Foo::Foo>>", + "0x1::Foo::Foo<0x1::Foo::Struct, 0x1::Foo::Foo>>>>", + "0x1::_bar::_BAR", + "0x1::__::__", + "0x1::_bar::_BAR<0x2::_____::______fooo______>", + "0x1::__::__<0x2::_____::______fooo______, 0xff::Bar____::_______foo>", + ]; + for s in valid { + assert!( + ParsedStructType::parse(s).is_ok(), + "Failed to parse struct {}", + s + ); + } + } + + fn struct_type_gen() -> impl Strategy { + ( + any::(), + any::(), + any::(), + ) + .prop_map(|(address, module, name)| format!("0x{}::{}::{}", address, module, name)) + } + + proptest! { + #[test] + fn test_parse_valid_struct_type_proptest(s in struct_type_gen()) { + prop_assert!(ParsedStructType::parse(&s).is_ok()); + } + + #[test] + fn test_parse_valid_type_struct_only_proptest(s in struct_type_gen()) { + prop_assert!(ParsedStructType::parse(&s).is_ok()); + } +} +} diff --git a/identity_iota_interaction/src/sdk_types/move_command_line_common/types.rs b/identity_iota_interaction/src/sdk_types/move_command_line_common/types.rs new file mode 100644 index 0000000000..7a3a81ed68 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_command_line_common/types.rs @@ -0,0 +1,193 @@ +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::{self, Display}; + +use anyhow::{bail}; +use super::super::move_core_types::{ + account_address::AccountAddress, + identifier::{self, Identifier}, + language_storage::{StructTag, TypeTag, ModuleId} +}; + +use super::{address::ParsedAddress, parser::Token}; + +#[derive(Eq, PartialEq, Debug, Clone, Copy)] +pub enum TypeToken { + Whitespace, + Ident, + AddressIdent, + ColonColon, + Lt, + Gt, + Comma, +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct ParsedModuleId { + pub address: ParsedAddress, + pub name: String, +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct ParsedFqName { + pub module: ParsedModuleId, + pub name: String, +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct ParsedStructType { + pub fq_name: ParsedFqName, + pub type_args: Vec, +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum ParsedType { + U8, + U16, + U32, + U64, + U128, + U256, + Bool, + Address, + Signer, + Vector(Box), + Struct(ParsedStructType), +} + +impl Display for TypeToken { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + let s = match *self { + TypeToken::Whitespace => "[whitespace]", + TypeToken::Ident => "[identifier]", + TypeToken::AddressIdent => "[address]", + TypeToken::ColonColon => "::", + TypeToken::Lt => "<", + TypeToken::Gt => ">", + TypeToken::Comma => ",", + }; + fmt::Display::fmt(s, formatter) + } +} + +impl Token for TypeToken { + fn is_whitespace(&self) -> bool { + matches!(self, Self::Whitespace) + } + + fn next_token(s: &str) -> anyhow::Result> { + let mut chars = s.chars().peekable(); + + let c = match chars.next() { + None => return Ok(None), + Some(c) => c, + }; + Ok(Some(match c { + '<' => (Self::Lt, 1), + '>' => (Self::Gt, 1), + ',' => (Self::Comma, 1), + ':' => match chars.next() { + Some(':') => (Self::ColonColon, 2), + _ => bail!("unrecognized token: {}", s), + }, + '0' if matches!(chars.peek(), Some('x') | Some('X')) => { + chars.next().unwrap(); + match chars.next() { + Some(c) if c.is_ascii_hexdigit() || c == '_' => { + // 0x + c + remaining + let len = 3 + chars + .take_while(|q| char::is_ascii_hexdigit(q) || *q == '_') + .count(); + (Self::AddressIdent, len) + } + _ => bail!("unrecognized token: {}", s), + } + } + c if c.is_ascii_digit() => { + // c + remaining + let len = 1 + chars.take_while(char::is_ascii_digit).count(); + (Self::AddressIdent, len) + } + c if c.is_ascii_whitespace() => { + // c + remaining + let len = 1 + chars.take_while(char::is_ascii_whitespace).count(); + (Self::Whitespace, len) + } + c if c.is_ascii_alphabetic() + || (c == '_' + && chars + .peek() + .is_some_and(|c| identifier::is_valid_identifier_char(*c))) => + { + // c + remaining + let len = 1 + chars + .take_while(|c| identifier::is_valid_identifier_char(*c)) + .count(); + (Self::Ident, len) + } + _ => bail!("unrecognized token: {}", s), + })) + } +} + +impl ParsedModuleId { + pub fn into_module_id( + self, + mapping: &impl Fn(&str) -> Option, + ) -> anyhow::Result { + Ok(ModuleId::new( + self.address.into_account_address(mapping)?, + Identifier::new(self.name)?, + )) + } +} + +impl ParsedFqName { + pub fn into_fq_name( + self, + mapping: &impl Fn(&str) -> Option, + ) -> anyhow::Result<(ModuleId, String)> { + Ok((self.module.into_module_id(mapping)?, self.name)) + } +} + +impl ParsedStructType { + pub fn into_struct_tag( + self, + mapping: &impl Fn(&str) -> Option, + ) -> anyhow::Result { + let Self { fq_name, type_args } = self; + Ok(StructTag { + address: fq_name.module.address.into_account_address(mapping)?, + module: Identifier::new(fq_name.module.name)?, + name: Identifier::new(fq_name.name)?, + type_params: type_args + .into_iter() + .map(|t| t.into_type_tag(mapping)) + .collect::>()?, + }) + } +} + +impl ParsedType { + pub fn into_type_tag( + self, + mapping: &impl Fn(&str) -> Option, + ) -> anyhow::Result { + Ok(match self { + ParsedType::U8 => TypeTag::U8, + ParsedType::U16 => TypeTag::U16, + ParsedType::U32 => TypeTag::U32, + ParsedType::U64 => TypeTag::U64, + ParsedType::U128 => TypeTag::U128, + ParsedType::U256 => TypeTag::U256, + ParsedType::Bool => TypeTag::Bool, + ParsedType::Address => TypeTag::Address, + ParsedType::Signer => TypeTag::Signer, + ParsedType::Vector(inner) => TypeTag::Vector(Box::new(inner.into_type_tag(mapping)?)), + ParsedType::Struct(s) => TypeTag::Struct(Box::new(s.into_struct_tag(mapping)?)), + }) + } +} diff --git a/identity_iota_interaction/src/sdk_types/move_command_line_common/values.rs b/identity_iota_interaction/src/sdk_types/move_command_line_common/values.rs new file mode 100644 index 0000000000..25d9873e7b --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_command_line_common/values.rs @@ -0,0 +1,320 @@ +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::{self, Display}; + +use anyhow::bail; + +use super::super::move_core_types::{ + account_address::AccountAddress, + identifier, + runtime_value::{MoveStruct, MoveValue}, + u256::U256, +}; + +use super::{ + address::ParsedAddress, + parser::{Parser, Token}, +}; + +#[derive(Eq, PartialEq, Debug, Clone, Copy)] +pub enum ValueToken { + Number, + NumberTyped, + True, + False, + ByteString, + HexString, + Utf8String, + Ident, + AtSign, + LBrace, + RBrace, + LBracket, + RBracket, + LParen, + RParen, + Comma, + Colon, + ColonColon, + Whitespace, +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum ParsedValue { + Address(ParsedAddress), + InferredNum(U256), + U8(u8), + U16(u16), + U32(u32), + U64(u64), + U128(u128), + U256(U256), + Bool(bool), + Vector(Vec>), + Struct(Vec>), + Custom(Extra), +} + +pub trait ParsableValue: Sized + Send + Sync + Clone + 'static { + type ConcreteValue: Send; + fn parse_value<'a, I: Iterator>( + parser: &mut Parser<'a, ValueToken, I>, + ) -> Option>; + + fn move_value_into_concrete(v: MoveValue) -> anyhow::Result; + fn concrete_vector(elems: Vec) -> anyhow::Result; + fn concrete_struct(values: Vec) -> anyhow::Result; + fn into_concrete_value( + self, + mapping: &impl Fn(&str) -> Option, + ) -> anyhow::Result; +} + +impl ParsableValue for () { + type ConcreteValue = MoveValue; + fn parse_value<'a, I: Iterator>( + _: &mut Parser<'a, ValueToken, I>, + ) -> Option> { + None + } + fn move_value_into_concrete(v: MoveValue) -> anyhow::Result { + Ok(v) + } + + fn concrete_vector(elems: Vec) -> anyhow::Result { + Ok(MoveValue::Vector(elems)) + } + + fn concrete_struct(values: Vec) -> anyhow::Result { + Ok(MoveValue::Struct(MoveStruct(values))) + } + fn into_concrete_value( + self, + _mapping: &impl Fn(&str) -> Option, + ) -> anyhow::Result { + unreachable!() + } +} + +impl Display for ValueToken { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + let s = match self { + ValueToken::Number => "[num]", + ValueToken::NumberTyped => "[num typed]", + ValueToken::True => "true", + ValueToken::False => "false", + ValueToken::ByteString => "[byte string]", + ValueToken::Utf8String => "[utf8 string]", + ValueToken::HexString => "[hex string]", + ValueToken::Whitespace => "[whitespace]", + ValueToken::Ident => "[identifier]", + ValueToken::AtSign => "@", + ValueToken::LBrace => "{", + ValueToken::RBrace => "}", + ValueToken::LBracket => "[", + ValueToken::RBracket => "]", + ValueToken::LParen => "(", + ValueToken::RParen => ")", + ValueToken::Comma => ",", + ValueToken::Colon => ":", + ValueToken::ColonColon => "::", + }; + fmt::Display::fmt(s, formatter) + } +} + +impl Token for ValueToken { + fn is_whitespace(&self) -> bool { + matches!(self, Self::Whitespace) + } + + fn next_token(s: &str) -> anyhow::Result> { + fn number_maybe_with_suffix(text: &str, num_text_len: usize) -> (ValueToken, usize) { + let rest = &text[num_text_len..]; + if rest.starts_with("u8") { + (ValueToken::NumberTyped, num_text_len + 2) + } else if rest.starts_with("u64") || rest.starts_with("u16") || rest.starts_with("u32") + { + (ValueToken::NumberTyped, num_text_len + 3) + } else if rest.starts_with("u128") || rest.starts_with("u256") { + (ValueToken::NumberTyped, num_text_len + 4) + } else { + // No typed suffix + (ValueToken::Number, num_text_len) + } + } + if s.starts_with("true") { + return Ok(Some((Self::True, 4))); + } + if s.starts_with("false") { + return Ok(Some((Self::False, 5))); + } + + let mut chars = s.chars().peekable(); + let c = match chars.next() { + None => return Ok(None), + Some(c) => c, + }; + Ok(Some(match c { + '@' => (Self::AtSign, 1), + '{' => (Self::LBrace, 1), + '}' => (Self::RBrace, 1), + '[' => (Self::LBracket, 1), + ']' => (Self::RBracket, 1), + '(' => (Self::LParen, 1), + ')' => (Self::RParen, 1), + ',' => (Self::Comma, 1), + ':' if matches!(chars.peek(), Some(':')) => (Self::ColonColon, 2), + ':' => (Self::Colon, 1), + '0' if matches!(chars.peek(), Some('x')) => { + chars.next().unwrap(); + match chars.next() { + Some(c) if c.is_ascii_hexdigit() => { + let len = 3 + chars + .take_while(|c| char::is_ascii_hexdigit(c) || *c == '_') + .count(); + number_maybe_with_suffix(s, len) + } + _ => bail!("unrecognized token: {}", s), + } + } + 'b' if matches!(chars.peek(), Some('"')) => { + chars.next().unwrap(); + // b" + let mut len = 2; + loop { + len += 1; + match chars.next() { + Some('"') => break, + Some(c) if c.is_ascii() => (), + Some(c) => bail!( + "Unexpected non-ascii character '{}' in byte string: {}", + c.escape_default(), + s + ), + None => bail!("Unexpected end of string before end quote: {}", s), + } + } + if s[..len].chars().any(|c| c == '\\') { + bail!( + "Escape characters not yet supported in byte string: {}", + &s[..len] + ) + } + (ValueToken::ByteString, len) + } + 'x' if matches!(chars.peek(), Some('"')) => { + chars.next().unwrap(); + // x" + let mut len = 2; + loop { + len += 1; + match chars.next() { + Some('"') => break, + Some(c) if c.is_ascii_hexdigit() => (), + Some(c) => bail!( + "Unexpected non-hexdigit '{}' in hex string: {}", + c.escape_default(), + s + ), + None => bail!("Unexpected end of string before end quote: {}", s), + } + } + assert!(len >= 3); + let num_digits = len - 3; + if num_digits % 2 != 0 { + bail!( + "Expected an even number of hex digits in hex string: {}", + &s[..len] + ) + } + (ValueToken::HexString, len) + } + '"' => { + // there is no need to check if a given char is valid UTF8 as it is already + // guaranteed; from the Rust docs + // (https://doc.rust-lang.org/std/primitive.char.html): "char values are USVs and + // str values are valid UTF-8, it is safe to store any char in a str or read any + // character from a str as a char"; this means that while not every char is + // valid UTF8, those stored in &str are + let end_quote_byte_offset = match s[1..].find('"') { + Some(o) => o, + None => bail!("Unexpected end of string before end quote: {}", s), + }; + // the length of the token (which we need in bytes rather than chars as s is + // sliced in parser and slicing str uses byte indexes) is the + // same as position of the ending double quote (in the whole + // string) plus 1 + let len = s[..1].len() + end_quote_byte_offset + 1; + if s[..len].chars().any(|c| c == '\\') { + bail!( + "Escape characters not yet supported in utf8 string: {}", + &s[..len] + ) + } + (ValueToken::Utf8String, len) + } + c if c.is_ascii_digit() => { + // c + remaining + let len = 1 + chars + .take_while(|c| char::is_ascii_digit(c) || *c == '_') + .count(); + number_maybe_with_suffix(s, len) + } + c if c.is_ascii_whitespace() => { + // c + remaining + let len = 1 + chars.take_while(char::is_ascii_whitespace).count(); + (Self::Whitespace, len) + } + c if c.is_ascii_alphabetic() => { + // c + remaining + // TODO be more permissive + let len = 1 + chars + .take_while(|c| identifier::is_valid_identifier_char(*c)) + .count(); + (Self::Ident, len) + } + _ => bail!("unrecognized token: {}", s), + })) + } +} + +impl ParsedValue { + pub fn into_concrete_value( + self, + mapping: &impl Fn(&str) -> Option, + ) -> anyhow::Result { + match self { + ParsedValue::Address(a) => Extra::move_value_into_concrete(MoveValue::Address( + a.into_account_address(mapping)?, + )), + ParsedValue::U8(u) => Extra::move_value_into_concrete(MoveValue::U8(u)), + ParsedValue::U16(u) => Extra::move_value_into_concrete(MoveValue::U16(u)), + ParsedValue::U32(u) => Extra::move_value_into_concrete(MoveValue::U32(u)), + ParsedValue::U64(u) => Extra::move_value_into_concrete(MoveValue::U64(u)), + ParsedValue::InferredNum(u) if u <= (u64::MAX.into()) => { + Extra::move_value_into_concrete(MoveValue::U64(u.try_into()?)) + } + ParsedValue::U128(u) => Extra::move_value_into_concrete(MoveValue::U128(u)), + ParsedValue::InferredNum(u) | ParsedValue::U256(u) => { + Extra::move_value_into_concrete(MoveValue::U256(u)) + } + ParsedValue::Bool(b) => Extra::move_value_into_concrete(MoveValue::Bool(b)), + ParsedValue::Vector(values) => Extra::concrete_vector( + values + .into_iter() + .map(|value| value.into_concrete_value(mapping)) + .collect::>()?, + ), + ParsedValue::Struct(values) => Extra::concrete_struct( + values + .into_iter() + .map(|value| value.into_concrete_value(mapping)) + .collect::>()?, + ), + ParsedValue::Custom(c) => Extra::into_concrete_value(c, mapping), + } + } +} diff --git a/identity_iota_interaction/src/sdk_types/move_core_types/account_address.rs b/identity_iota_interaction/src/sdk_types/move_core_types/account_address.rs new file mode 100644 index 0000000000..534fb5fd5b --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_core_types/account_address.rs @@ -0,0 +1,329 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{convert::TryFrom, fmt, str::FromStr}; + +use hex::FromHex; +use rand::{rngs::OsRng, Rng}; +use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; + +/// A struct that represents an account address. +#[derive(Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] +pub struct AccountAddress([u8; AccountAddress::LENGTH]); + +impl AccountAddress { + pub const fn new(address: [u8; Self::LENGTH]) -> Self { + Self(address) + } + + /// The number of bytes in an address. + pub const LENGTH: usize = 32; + + /// Hex address: 0x0 + pub const ZERO: Self = Self([0u8; Self::LENGTH]); + + /// Hex address: 0x1 + pub const ONE: Self = Self::get_hex_address_one(); + + /// Hex address: 0x2 + pub const TWO: Self = Self::get_hex_address_two(); + + const fn get_hex_address_one() -> Self { + let mut addr = [0u8; AccountAddress::LENGTH]; + addr[AccountAddress::LENGTH - 1] = 1u8; + Self(addr) + } + + const fn get_hex_address_two() -> Self { + let mut addr = [0u8; AccountAddress::LENGTH]; + addr[AccountAddress::LENGTH - 1] = 2u8; + Self(addr) + } + + pub fn random() -> Self { + let mut rng = OsRng; + let buf: [u8; Self::LENGTH] = rng.gen(); + Self(buf) + } + + /// Return a canonical string representation of the address + /// Addresses are hex-encoded lowercase values of length ADDRESS_LENGTH (16, + /// 20, or 32 depending on the Move platform) + /// e.g., 0000000000000000000000000000000a, *not* + /// 0x0000000000000000000000000000000a, 0xa, or 0xA Note: this function + /// is guaranteed to be stable, and this is suitable for use inside Move + /// native functions or the VM. However, one can pass with_prefix=true + /// to get its representation with the 0x prefix. + pub fn to_canonical_string(&self, with_prefix: bool) -> String { + self.to_canonical_display(with_prefix).to_string() + } + + /// Implements Display for the address, with the prefix 0x if with_prefix is + /// true. + pub fn to_canonical_display(&self, with_prefix: bool) -> impl fmt::Display + '_ { + struct HexDisplay<'a> { + data: &'a [u8], + with_prefix: bool, + } + + impl<'a> fmt::Display for HexDisplay<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.with_prefix { + write!(f, "0x{}", hex::encode(self.data)) + } else { + write!(f, "{}", hex::encode(self.data)) + } + } + } + HexDisplay { + data: &self.0, + with_prefix, + } + } + + pub fn short_str_lossless(&self) -> String { + let hex_str = hex::encode(self.0).trim_start_matches('0').to_string(); + if hex_str.is_empty() { + "0".to_string() + } else { + hex_str + } + } + + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + + pub fn into_bytes(self) -> [u8; Self::LENGTH] { + self.0 + } + + pub fn from_hex_literal(literal: &str) -> Result { + if !literal.starts_with("0x") { + return Err(AccountAddressParseError); + } + + let hex_len = literal.len() - 2; + + // If the string is too short, pad it + if hex_len < Self::LENGTH * 2 { + let mut hex_str = String::with_capacity(Self::LENGTH * 2); + for _ in 0..Self::LENGTH * 2 - hex_len { + hex_str.push('0'); + } + hex_str.push_str(&literal[2..]); + AccountAddress::from_hex(hex_str) + } else { + AccountAddress::from_hex(&literal[2..]) + } + } + + pub fn to_hex_literal(&self) -> String { + format!("0x{}", self.short_str_lossless()) + } + + pub fn from_hex>(hex: T) -> Result { + <[u8; Self::LENGTH]>::from_hex(hex) + .map_err(|_| AccountAddressParseError) + .map(Self) + } + + pub fn to_hex(&self) -> String { + format!("{:x}", self) + } + + pub fn from_bytes>(bytes: T) -> Result { + <[u8; Self::LENGTH]>::try_from(bytes.as_ref()) + .map_err(|_| AccountAddressParseError) + .map(Self) + } + + // AbstractMemorySize is not available for wasm32 + // + // /// TODO (ade): use macro to enfornce determinism + // pub fn abstract_size_for_gas_metering(&self) -> AbstractMemorySize { + // AbstractMemorySize::new(Self::LENGTH as u64) + // } +} + +impl AsRef<[u8]> for AccountAddress { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl std::ops::Deref for AccountAddress { + type Target = [u8; Self::LENGTH]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for AccountAddress { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + write!(f, "{:x}", self) + } +} + +impl fmt::Debug for AccountAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:x}", self) + } +} + +impl fmt::LowerHex for AccountAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + write!(f, "0x")?; + } + + for byte in &self.0 { + write!(f, "{:02x}", byte)?; + } + + Ok(()) + } +} + +impl fmt::UpperHex for AccountAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + write!(f, "0x")?; + } + + for byte in &self.0 { + write!(f, "{:02X}", byte)?; + } + + Ok(()) + } +} + +impl From<[u8; AccountAddress::LENGTH]> for AccountAddress { + fn from(bytes: [u8; AccountAddress::LENGTH]) -> Self { + Self::new(bytes) + } +} + +impl TryFrom<&[u8]> for AccountAddress { + type Error = AccountAddressParseError; + + /// Tries to convert the provided byte array into Address. + fn try_from(bytes: &[u8]) -> Result { + Self::from_bytes(bytes) + } +} + +impl TryFrom> for AccountAddress { + type Error = AccountAddressParseError; + + /// Tries to convert the provided byte buffer into Address. + fn try_from(bytes: Vec) -> Result { + Self::from_bytes(bytes) + } +} + +impl From for Vec { + fn from(addr: AccountAddress) -> Vec { + addr.0.to_vec() + } +} + +impl From<&AccountAddress> for Vec { + fn from(addr: &AccountAddress) -> Vec { + addr.0.to_vec() + } +} + +impl From for [u8; AccountAddress::LENGTH] { + fn from(addr: AccountAddress) -> Self { + addr.0 + } +} + +impl From<&AccountAddress> for [u8; AccountAddress::LENGTH] { + fn from(addr: &AccountAddress) -> Self { + addr.0 + } +} + +impl From<&AccountAddress> for String { + fn from(addr: &AccountAddress) -> String { + ::hex::encode(addr.as_ref()) + } +} + +impl TryFrom for AccountAddress { + type Error = AccountAddressParseError; + + fn try_from(s: String) -> Result { + Self::from_hex(s) + } +} + +impl FromStr for AccountAddress { + type Err = AccountAddressParseError; + + fn from_str(s: &str) -> Result { + // Accept 0xADDRESS or ADDRESS + if let Ok(address) = AccountAddress::from_hex_literal(s) { + Ok(address) + } else { + Self::from_hex(s) + } + } +} + +impl<'de> Deserialize<'de> for AccountAddress { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + let s = ::deserialize(deserializer)?; + AccountAddress::from_str(&s).map_err(D::Error::custom) + } else { + // In order to preserve the Serde data model and help analysis tools, + // make sure to wrap our value in a container with the same name + // as the original type. + #[derive(::serde::Deserialize)] + #[serde(rename = "AccountAddress")] + struct Value([u8; AccountAddress::LENGTH]); + + let value = Value::deserialize(deserializer)?; + Ok(AccountAddress::new(value.0)) + } + } +} + +impl Serialize for AccountAddress { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + if serializer.is_human_readable() { + self.to_hex().serialize(serializer) + } else { + // See comment in deserialize. + serializer.serialize_newtype_struct("AccountAddress", &self.0) + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct AccountAddressParseError; + +impl fmt::Display for AccountAddressParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + write!( + f, + "Unable to parse AccountAddress (must be hex string of length {})", + AccountAddress::LENGTH + ) + } +} + +impl std::error::Error for AccountAddressParseError {} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/move_core_types/annotated_value.rs b/identity_iota_interaction/src/sdk_types/move_core_types/annotated_value.rs new file mode 100644 index 0000000000..13550e264f --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_core_types/annotated_value.rs @@ -0,0 +1,731 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + collections::BTreeMap, + convert::From, + fmt::{self, Debug}, +}; + +use anyhow::Result as AResult; +use serde::{ + de::Error as DeError, + ser::{SerializeMap, SerializeSeq, SerializeStruct}, + Deserialize, Serialize, +}; + +use super::{ + account_address::AccountAddress, + annotated_visitor::{visit_struct, visit_value, Error as VError, Visitor}, + identifier::Identifier, + language_storage::{StructTag, TypeTag}, + runtime_value::{self as R, MOVE_STRUCT_FIELDS, MOVE_STRUCT_TYPE}, + u256, VARIANT_COUNT_MAX, +}; + +/// In the `WithTypes` configuration, a Move struct gets serialized into a Serde +/// struct with this name +pub const MOVE_STRUCT_NAME: &str = "struct"; + +/// In the `WithTypes` configuration, a Move enum/struct gets serialized into a +/// Serde struct with this as the first field +pub const MOVE_DATA_TYPE: &str = "type"; + +/// In the `WithTypes` configuration, a Move struct gets serialized into a Serde +/// struct with this as the second field +pub const MOVE_DATA_FIELDS: &str = "fields"; + +/// In the `WithTypes` configuration, a Move enum gets serialized into a Serde +/// struct with this as the second field In the `WithFields` configuration, this +/// is the first field of the serialized enum +pub const MOVE_VARIANT_NAME: &str = "variant_name"; + +/// Field name for the tag of the variant +pub const MOVE_VARIANT_TAG_NAME: &str = "variant_tag"; + +/// In the `WithTypes` configuration, a Move enum gets serialized into a Serde +/// struct with this name +pub const MOVE_ENUM_NAME: &str = "enum"; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct MoveStruct { + pub type_: StructTag, + pub fields: Vec<(Identifier, MoveValue)>, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct MoveVariant { + pub type_: StructTag, + pub variant_name: Identifier, + pub tag: u16, + pub fields: Vec<(Identifier, MoveValue)>, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum MoveValue { + U8(u8), + U64(u64), + U128(u128), + Bool(bool), + Address(AccountAddress), + Vector(Vec), + Struct(MoveStruct), + Signer(AccountAddress), + // NOTE: Added in bytecode version v6, do not reorder! + U16(u16), + U32(u32), + U256(u256::U256), + Variant(MoveVariant), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MoveFieldLayout { + pub name: Identifier, + pub layout: MoveTypeLayout, +} + +impl MoveFieldLayout { + pub fn new(name: Identifier, layout: MoveTypeLayout) -> Self { + Self { name, layout } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MoveStructLayout { + /// An decorated representation with both types and human-readable field + /// names + pub type_: StructTag, + pub fields: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MoveEnumLayout { + pub type_: StructTag, + pub variants: BTreeMap<(Identifier, u16), Vec>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MoveDatatypeLayout { + Struct(MoveStructLayout), + Enum(MoveEnumLayout), +} + +impl MoveDatatypeLayout { + pub fn into_layout(self) -> MoveTypeLayout { + match self { + Self::Struct(s) => MoveTypeLayout::Struct(s), + Self::Enum(e) => MoveTypeLayout::Enum(e), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MoveTypeLayout { + #[serde(rename(serialize = "bool", deserialize = "bool"))] + Bool, + #[serde(rename(serialize = "u8", deserialize = "u8"))] + U8, + #[serde(rename(serialize = "u64", deserialize = "u64"))] + U64, + #[serde(rename(serialize = "u128", deserialize = "u128"))] + U128, + #[serde(rename(serialize = "address", deserialize = "address"))] + Address, + #[serde(rename(serialize = "vector", deserialize = "vector"))] + Vector(Box), + #[serde(rename(serialize = "struct", deserialize = "struct"))] + Struct(MoveStructLayout), + #[serde(rename(serialize = "signer", deserialize = "signer"))] + Signer, + + // NOTE: Added in bytecode version v6, do not reorder! + #[serde(rename(serialize = "u16", deserialize = "u16"))] + U16, + #[serde(rename(serialize = "u32", deserialize = "u32"))] + U32, + #[serde(rename(serialize = "u256", deserialize = "u256"))] + U256, + #[serde(rename(serialize = "enum", deserialize = "enum"))] + Enum(MoveEnumLayout), +} + +impl MoveValue { + /// TODO (annotated-visitor): Port legacy uses of this method to + /// `BoundedVisitor`. + pub fn simple_deserialize(blob: &[u8], ty: &MoveTypeLayout) -> AResult { + Ok(bcs::from_bytes_seed(ty, blob)?) + } + + /// Deserialize `blob` as a Move value with the given `ty`-pe layout, and + /// visit its sub-structure with the given `visitor`. The visitor + /// dictates the return value that is built up during deserialization. + /// + /// # Nested deserialization + /// + /// Vectors and structs are nested structures that can be met during + /// deserialization. Visitors are passed a driver (`VecDriver` or + /// `StructDriver` correspondingly) which controls how nested elements + /// or fields are visited including whether a given nested element/field is + /// explored, which visitor to use (the visitor can pass `self` to + /// recursively explore them) and whether a given element is visited or + /// skipped. + /// + /// The visitor may leave elements unvisited at the end of the vector or + /// struct, which implicitly skips them. + /// + /// # Errors + /// + /// Deserialization can fail because of an issue in the serialized format + /// (data doesn't match layout, unexpected bytes or trailing bytes), or + /// a custom error expressed by the visitor. + pub fn visit_deserialize( + mut blob: &[u8], + ty: &MoveTypeLayout, + visitor: &mut V, + ) -> AResult + where + V::Error: std::error::Error + Send + Sync + 'static, + { + let res = visit_value(&mut blob, ty, visitor)?; + if blob.is_empty() { + Ok(res) + } else { + Err(VError::TrailingBytes(blob.len()).into()) + } + } + + pub fn simple_serialize(&self) -> Option> { + bcs::to_bytes(self).ok() + } + + pub fn undecorate(self) -> R::MoveValue { + match self { + Self::Struct(s) => R::MoveValue::Struct(s.undecorate()), + Self::Variant(v) => R::MoveValue::Variant(v.undecorate()), + Self::Vector(vals) => { + R::MoveValue::Vector(vals.into_iter().map(MoveValue::undecorate).collect()) + } + MoveValue::U8(u) => R::MoveValue::U8(u), + MoveValue::U64(u) => R::MoveValue::U64(u), + MoveValue::U128(u) => R::MoveValue::U128(u), + MoveValue::Bool(b) => R::MoveValue::Bool(b), + MoveValue::Address(a) => R::MoveValue::Address(a), + MoveValue::Signer(s) => R::MoveValue::Signer(s), + MoveValue::U16(u) => R::MoveValue::U16(u), + MoveValue::U32(u) => R::MoveValue::U32(u), + MoveValue::U256(u) => R::MoveValue::U256(u), + } + } +} + +pub fn serialize_values<'a, I>(vals: I) -> Vec> +where + I: IntoIterator, +{ + vals.into_iter() + .map(|val| { + val.simple_serialize() + .expect("serialization should succeed") + }) + .collect() +} + +impl MoveStruct { + pub fn new(type_: StructTag, fields: Vec<(Identifier, MoveValue)>) -> Self { + Self { type_, fields } + } + + /// TODO (annotated-visitor): Port legacy uses of this method to + /// `BoundedVisitor`. + pub fn simple_deserialize(blob: &[u8], ty: &MoveStructLayout) -> AResult { + Ok(bcs::from_bytes_seed(ty, blob)?) + } + + /// Like `MoveValue::visit_deserialize` (see for details), but specialized + /// to visiting a struct (the `blob` is known to be a serialized Move + /// struct, and the layout is a `MoveStructLayout`). + pub fn visit_deserialize( + mut blob: &[u8], + ty: &MoveStructLayout, + visitor: &mut V, + ) -> AResult + where + V::Error: std::error::Error + Send + Sync + 'static, + { + let res = visit_struct(&mut blob, ty, visitor)?; + if blob.is_empty() { + Ok(res) + } else { + Err(VError::TrailingBytes(blob.len()).into()) + } + } + + pub fn into_fields(self) -> Vec { + self.fields.into_iter().map(|(_, v)| v).collect() + } + + pub fn undecorate(self) -> R::MoveStruct { + R::MoveStruct( + self.into_fields() + .into_iter() + .map(MoveValue::undecorate) + .collect(), + ) + } +} + +impl MoveVariant { + pub fn new( + type_: StructTag, + variant_name: Identifier, + tag: u16, + fields: Vec<(Identifier, MoveValue)>, + ) -> Self { + Self { + type_, + variant_name, + tag, + fields, + } + } + + pub fn simple_deserialize(blob: &[u8], ty: &MoveEnumLayout) -> AResult { + Ok(bcs::from_bytes_seed(ty, blob)?) + } + + pub fn into_fields(self) -> Vec { + self.fields.into_iter().map(|(_, v)| v).collect() + } + + pub fn undecorate(self) -> R::MoveVariant { + R::MoveVariant { + tag: self.tag, + fields: self + .into_fields() + .into_iter() + .map(MoveValue::undecorate) + .collect(), + } + } +} + +impl MoveStructLayout { + pub fn new(type_: StructTag, fields: Vec) -> Self { + Self { type_, fields } + } + + pub fn into_fields(self) -> Vec { + self.fields.into_iter().map(|f| f.layout).collect() + } +} + +impl<'d> serde::de::DeserializeSeed<'d> for &MoveTypeLayout { + type Value = MoveValue; + fn deserialize>( + self, + deserializer: D, + ) -> Result { + match self { + MoveTypeLayout::Bool => bool::deserialize(deserializer).map(MoveValue::Bool), + MoveTypeLayout::U8 => u8::deserialize(deserializer).map(MoveValue::U8), + MoveTypeLayout::U16 => u16::deserialize(deserializer).map(MoveValue::U16), + MoveTypeLayout::U32 => u32::deserialize(deserializer).map(MoveValue::U32), + MoveTypeLayout::U64 => u64::deserialize(deserializer).map(MoveValue::U64), + MoveTypeLayout::U128 => u128::deserialize(deserializer).map(MoveValue::U128), + MoveTypeLayout::U256 => u256::U256::deserialize(deserializer).map(MoveValue::U256), + MoveTypeLayout::Address => { + AccountAddress::deserialize(deserializer).map(MoveValue::Address) + } + MoveTypeLayout::Signer => { + AccountAddress::deserialize(deserializer).map(MoveValue::Signer) + } + MoveTypeLayout::Struct(ty) => Ok(MoveValue::Struct(ty.deserialize(deserializer)?)), + MoveTypeLayout::Enum(ty) => Ok(MoveValue::Variant(ty.deserialize(deserializer)?)), + MoveTypeLayout::Vector(layout) => Ok(MoveValue::Vector( + deserializer.deserialize_seq(VectorElementVisitor(layout))?, + )), + } + } +} + +struct VectorElementVisitor<'a>(&'a MoveTypeLayout); + +impl<'d, 'a> serde::de::Visitor<'d> for VectorElementVisitor<'a> { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Vector") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'d>, + { + let mut vals = Vec::new(); + while let Some(elem) = seq.next_element_seed(self.0)? { + vals.push(elem) + } + Ok(vals) + } +} + +struct DecoratedStructFieldVisitor<'a>(&'a [MoveFieldLayout]); + +impl<'d, 'a> serde::de::Visitor<'d> for DecoratedStructFieldVisitor<'a> { + type Value = Vec<(Identifier, MoveValue)>; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Struct") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'d>, + { + let mut vals = Vec::new(); + for (i, layout) in self.0.iter().enumerate() { + match seq.next_element_seed(layout)? { + Some(elem) => vals.push(elem), + None => return Err(A::Error::invalid_length(i, &self)), + } + } + Ok(vals) + } +} + +impl<'d> serde::de::DeserializeSeed<'d> for &MoveFieldLayout { + type Value = (Identifier, MoveValue); + + fn deserialize>( + self, + deserializer: D, + ) -> Result { + Ok((self.name.clone(), self.layout.deserialize(deserializer)?)) + } +} + +impl<'d> serde::de::DeserializeSeed<'d> for &MoveStructLayout { + type Value = MoveStruct; + + fn deserialize>( + self, + deserializer: D, + ) -> Result { + let fields = deserializer + .deserialize_tuple(self.fields.len(), DecoratedStructFieldVisitor(&self.fields))?; + Ok(MoveStruct { + type_: self.type_.clone(), + fields, + }) + } +} + +impl<'d> serde::de::DeserializeSeed<'d> for &MoveEnumLayout { + type Value = MoveVariant; + fn deserialize>( + self, + deserializer: D, + ) -> Result { + let (variant_name, tag, fields) = + deserializer.deserialize_tuple(2, DecoratedEnumFieldVisitor(&self.variants))?; + Ok(MoveVariant { + type_: self.type_.clone(), + variant_name, + tag, + fields, + }) + } +} + +struct DecoratedEnumFieldVisitor<'a>(&'a BTreeMap<(Identifier, u16), Vec>); + +impl<'d, 'a> serde::de::Visitor<'d> for DecoratedEnumFieldVisitor<'a> { + type Value = (Identifier, u16, Vec<(Identifier, MoveValue)>); + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Enum") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'d>, + { + let tag = match seq.next_element_seed(&MoveTypeLayout::U8)? { + Some(MoveValue::U8(tag)) if tag as u64 <= VARIANT_COUNT_MAX => tag as u16, + Some(MoveValue::U8(tag)) => return Err(A::Error::invalid_length(tag as usize, &self)), + Some(val) => { + return Err(A::Error::invalid_type( + serde::de::Unexpected::Other(&format!("{val:?}")), + &self, + )); + } + None => return Err(A::Error::invalid_length(0, &self)), + }; + + let Some(((variant_name, _), variant_layout)) = + self.0.iter().find(|((_, v_tag), _)| *v_tag == tag) + else { + return Err(A::Error::invalid_length(tag as usize, &self)); + }; + + let Some(fields) = seq.next_element_seed(&DecoratedVariantFieldLayout(variant_layout))? + else { + return Err(A::Error::invalid_length(1, &self)); + }; + + Ok((variant_name.clone(), tag, fields)) + } +} + +struct DecoratedVariantFieldLayout<'a>(&'a Vec); + +impl<'d, 'a> serde::de::DeserializeSeed<'d> for &DecoratedVariantFieldLayout<'a> { + type Value = Vec<(Identifier, MoveValue)>; + + fn deserialize>( + self, + deserializer: D, + ) -> Result { + deserializer.deserialize_tuple(self.0.len(), DecoratedStructFieldVisitor(self.0)) + } +} + +impl serde::Serialize for MoveValue { + fn serialize(&self, serializer: S) -> Result { + match self { + MoveValue::Struct(s) => s.serialize(serializer), + MoveValue::Variant(v) => v.serialize(serializer), + MoveValue::Bool(b) => serializer.serialize_bool(*b), + MoveValue::U8(i) => serializer.serialize_u8(*i), + MoveValue::U16(i) => serializer.serialize_u16(*i), + MoveValue::U32(i) => serializer.serialize_u32(*i), + MoveValue::U64(i) => serializer.serialize_u64(*i), + MoveValue::U128(i) => serializer.serialize_u128(*i), + MoveValue::U256(i) => i.serialize(serializer), + MoveValue::Address(a) => a.serialize(serializer), + MoveValue::Signer(a) => a.serialize(serializer), + MoveValue::Vector(v) => { + let mut t = serializer.serialize_seq(Some(v.len()))?; + for val in v { + t.serialize_element(val)?; + } + t.end() + } + } + } +} + +struct MoveFields<'a>(&'a [(Identifier, MoveValue)]); + +impl<'a> serde::Serialize for MoveFields<'a> { + fn serialize(&self, serializer: S) -> Result { + let mut t = serializer.serialize_map(Some(self.0.len()))?; + for (f, v) in self.0.iter() { + t.serialize_entry(f, v)?; + } + t.end() + } +} + +impl serde::Serialize for MoveStruct { + fn serialize(&self, serializer: S) -> Result { + // Serialize a Move struct as Serde struct type named `struct `with two fields + // named `type` and `fields`. `fields` will get serialized as a Serde + // map. Unfortunately, we can't serialize this in the logical way: as a + // Serde struct named `type` with a field for each of `fields` because + // serde insists that struct and field names be `'static &str`'s + let mut t = serializer.serialize_struct(MOVE_STRUCT_NAME, 2)?; + // serialize type as string (e.g., + // 0x0::ModuleName::StructName) instead of (e.g. + // { address: 0x0...0, module: ModuleName, name: StructName, type_args: + // [TypeArg1, TypeArg2]}) + t.serialize_field(MOVE_STRUCT_TYPE, &self.type_.to_string())?; + t.serialize_field(MOVE_STRUCT_FIELDS, &MoveFields(&self.fields))?; + t.end() + } +} + +impl serde::Serialize for MoveVariant { + fn serialize(&self, serializer: S) -> Result { + // Serialize an enum as: + // enum { "type": 0xC::module::enum_type, "variant_name": name, "variant_tag": + // tag, "fields": { ... } } + let mut t = serializer.serialize_struct(MOVE_ENUM_NAME, 4)?; + t.serialize_field(MOVE_DATA_TYPE, &self.type_.to_string())?; + t.serialize_field(MOVE_VARIANT_NAME, &self.variant_name.to_string())?; + t.serialize_field(MOVE_VARIANT_TAG_NAME, &MoveValue::U16(self.tag))?; + t.serialize_field(MOVE_DATA_FIELDS, &MoveFields(&self.fields))?; + t.end() + } +} + +impl fmt::Display for MoveTypeLayout { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + use MoveTypeLayout::*; + match self { + Bool => write!(f, "bool"), + U8 => write!(f, "u8"), + U16 => write!(f, "u16"), + U32 => write!(f, "u32"), + U64 => write!(f, "u64"), + U128 => write!(f, "u128"), + U256 => write!(f, "u256"), + Address => write!(f, "address"), + Signer => write!(f, "signer"), + Vector(typ) if f.alternate() => write!(f, "vector<{typ:#}>"), + Vector(typ) => write!(f, "vector<{typ}>"), + Struct(s) if f.alternate() => write!(f, "{s:#}"), + Struct(s) => write!(f, "{s}"), + Enum(e) if f.alternate() => write!(f, "{e:#}"), + Enum(e) => write!(f, "enum {}", e), + } + } +} + +/// Helper type that uses `T`'s `Display` implementation as its own `Debug` +/// implementation, to allow other `Display` implementations in this module to +/// take advantage of the structured formatting helpers that Rust uses for its +/// own debug types. +struct DebugAsDisplay<'a, T>(&'a T); +impl<'a, T: fmt::Display> fmt::Debug for DebugAsDisplay<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + write!(f, "{:#}", self.0) + } else { + write!(f, "{}", self.0) + } + } +} + +impl fmt::Display for MoveStructLayout { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + use DebugAsDisplay as DD; + write!(f, "struct ")?; + write!(f, "{} ", self.type_)?; + let mut map = f.debug_map(); + for field in &self.fields { + map.entry(&DD(&field.name), &DD(&field.layout)); + } + map.finish() + } +} + +impl fmt::Display for MoveEnumLayout { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + use DebugAsDisplay as DD; + write!(f, "enum {} ", self.type_)?; + let mut vmap = f.debug_set(); + for ((variant_name, _), fields) in self.variants.iter() { + vmap.entry(&DD(&MoveVariantDisplay(variant_name.as_str(), fields))); + } + vmap.finish() + } +} + +struct MoveVariantDisplay<'a>(&'a str, &'a [MoveFieldLayout]); + +impl<'a> fmt::Display for MoveVariantDisplay<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + use DebugAsDisplay as DD; + let mut map = f.debug_struct(self.0); + for field in self.1 { + map.field(field.name.as_str(), &DD(&field.layout)); + } + map.finish() + } +} + +impl From<&MoveTypeLayout> for TypeTag { + fn from(val: &MoveTypeLayout) -> TypeTag { + match val { + MoveTypeLayout::Address => TypeTag::Address, + MoveTypeLayout::Bool => TypeTag::Bool, + MoveTypeLayout::U8 => TypeTag::U8, + MoveTypeLayout::U16 => TypeTag::U16, + MoveTypeLayout::U32 => TypeTag::U32, + MoveTypeLayout::U64 => TypeTag::U64, + MoveTypeLayout::U128 => TypeTag::U128, + MoveTypeLayout::U256 => TypeTag::U256, + MoveTypeLayout::Signer => TypeTag::Signer, + MoveTypeLayout::Vector(v) => { + let inner_type = &**v; + TypeTag::Vector(Box::new(inner_type.into())) + } + MoveTypeLayout::Struct(v) => TypeTag::Struct(Box::new(v.into())), + MoveTypeLayout::Enum(e) => TypeTag::Struct(Box::new(e.into())), + } + } +} + +impl From<&MoveStructLayout> for StructTag { + fn from(val: &MoveStructLayout) -> StructTag { + val.type_.clone() + } +} + +impl From<&MoveEnumLayout> for StructTag { + fn from(val: &MoveEnumLayout) -> StructTag { + val.type_.clone() + } +} + +impl fmt::Display for MoveValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MoveValue::U8(u) => write!(f, "{}u8", u), + MoveValue::U16(u) => write!(f, "{}u16", u), + MoveValue::U32(u) => write!(f, "{}u32", u), + MoveValue::U64(u) => write!(f, "{}u64", u), + MoveValue::U128(u) => write!(f, "{}u128", u), + MoveValue::U256(u) => write!(f, "{}u256", u), + MoveValue::Bool(false) => write!(f, "false"), + MoveValue::Bool(true) => write!(f, "true"), + MoveValue::Address(a) => write!(f, "{}", a.to_hex_literal()), + MoveValue::Signer(a) => write!(f, "signer({})", a.to_hex_literal()), + MoveValue::Vector(v) => { + use DebugAsDisplay as DD; + write!(f, "vector")?; + let mut list = f.debug_list(); + for val in v { + list.entry(&DD(val)); + } + list.finish() + } + MoveValue::Struct(s) => fmt::Display::fmt(s, f), + MoveValue::Variant(v) => fmt::Display::fmt(v, f), + } + } +} + +impl fmt::Display for MoveStruct { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use DebugAsDisplay as DD; + fmt::Display::fmt(&self.type_, f)?; + write!(f, " ")?; + let mut map = f.debug_map(); + for (field, value) in &self.fields { + map.entry(&DD(field), &DD(value)); + } + map.finish() + } +} + +impl fmt::Display for MoveVariant { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use DebugAsDisplay as DD; + let MoveVariant { + type_, + variant_name, + tag: _, + fields, + } = self; + write!(f, "{}::{}", type_, variant_name)?; + let mut map = f.debug_map(); + for (field, value) in fields { + map.entry(&DD(field), &DD(value)); + } + map.finish() + } +} diff --git a/identity_iota_interaction/src/sdk_types/move_core_types/annotated_visitor.rs b/identity_iota_interaction/src/sdk_types/move_core_types/annotated_visitor.rs new file mode 100644 index 0000000000..4961305a16 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_core_types/annotated_visitor.rs @@ -0,0 +1,524 @@ +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::io::Read; + +use super::{ + account_address::AccountAddress, + annotated_value::{MoveEnumLayout, MoveFieldLayout, MoveStructLayout, MoveTypeLayout}, + identifier::IdentStr, + u256::U256, + VARIANT_COUNT_MAX, +}; + +/// Visitors can be used for building values out of a serialized Move struct or +/// value. +pub trait Visitor { + type Value; + + /// Visitors can return any error as long as it can represent an error from + /// the visitor itself. The easiest way to achieve this is to use + /// `thiserror`: + /// + /// ```rust,no_doc + /// #[derive(thiserror::Error)] + /// enum Error { + /// #[error(transparent)] + /// Visitor(#[from] annotated_visitor::Error) + /// + /// // Custom error variants ... + /// } + /// ``` + type Error: From; + + fn visit_u8(&mut self, value: u8) -> Result; + fn visit_u16(&mut self, value: u16) -> Result; + fn visit_u32(&mut self, value: u32) -> Result; + fn visit_u64(&mut self, value: u64) -> Result; + fn visit_u128(&mut self, value: u128) -> Result; + fn visit_u256(&mut self, value: U256) -> Result; + fn visit_bool(&mut self, value: bool) -> Result; + fn visit_address(&mut self, value: AccountAddress) -> Result; + fn visit_signer(&mut self, value: AccountAddress) -> Result; + + fn visit_vector( + &mut self, + driver: &mut VecDriver<'_, '_, '_>, + ) -> Result; + + fn visit_struct( + &mut self, + driver: &mut StructDriver<'_, '_, '_>, + ) -> Result; + + fn visit_variant( + &mut self, + driver: &mut VariantDriver<'_, '_, '_>, + ) -> Result; +} + +/// A traversal is a special kind of visitor that doesn't return any values. The +/// trait comes with default implementations for every variant that do nothing, +/// allowing an implementor to focus on only the cases they care about. +/// +/// Note that the default implementation for structs and vectors recurse down +/// into their elements. A traversal that doesn't want to look inside structs +/// and vectors needs to provide a custom implementation with an empty body: +/// +/// ```rust,no_run +/// fn traverse_vector(&mut self, _: &mut VecDriver) -> Result<(), Self::Error> { +/// Ok(()) +/// } +/// ``` +pub trait Traversal { + type Error: From; + + fn traverse_u8(&mut self, _value: u8) -> Result<(), Self::Error> { + Ok(()) + } + + fn traverse_u16(&mut self, _value: u16) -> Result<(), Self::Error> { + Ok(()) + } + + fn traverse_u32(&mut self, _value: u32) -> Result<(), Self::Error> { + Ok(()) + } + + fn traverse_u64(&mut self, _value: u64) -> Result<(), Self::Error> { + Ok(()) + } + + fn traverse_u128(&mut self, _value: u128) -> Result<(), Self::Error> { + Ok(()) + } + + fn traverse_u256(&mut self, _value: U256) -> Result<(), Self::Error> { + Ok(()) + } + + fn traverse_bool(&mut self, _value: bool) -> Result<(), Self::Error> { + Ok(()) + } + + fn traverse_address(&mut self, _value: AccountAddress) -> Result<(), Self::Error> { + Ok(()) + } + + fn traverse_signer(&mut self, _value: AccountAddress) -> Result<(), Self::Error> { + Ok(()) + } + + fn traverse_vector(&mut self, driver: &mut VecDriver<'_, '_, '_>) -> Result<(), Self::Error> { + while driver.next_element(self)?.is_some() {} + Ok(()) + } + + fn traverse_struct( + &mut self, + driver: &mut StructDriver<'_, '_, '_>, + ) -> Result<(), Self::Error> { + while driver.next_field(self)?.is_some() {} + Ok(()) + } + + fn traverse_variant( + &mut self, + driver: &mut VariantDriver<'_, '_, '_>, + ) -> Result<(), Self::Error> { + while driver.next_field(self)?.is_some() {} + Ok(()) + } +} + +/// Default implementation converting any traversal into a visitor. +impl Visitor for T { + type Value = (); + type Error = T::Error; + + fn visit_u8(&mut self, value: u8) -> Result { + self.traverse_u8(value) + } + + fn visit_u16(&mut self, value: u16) -> Result { + self.traverse_u16(value) + } + + fn visit_u32(&mut self, value: u32) -> Result { + self.traverse_u32(value) + } + + fn visit_u64(&mut self, value: u64) -> Result { + self.traverse_u64(value) + } + + fn visit_u128(&mut self, value: u128) -> Result { + self.traverse_u128(value) + } + + fn visit_u256(&mut self, value: U256) -> Result { + self.traverse_u256(value) + } + + fn visit_bool(&mut self, value: bool) -> Result { + self.traverse_bool(value) + } + + fn visit_address(&mut self, value: AccountAddress) -> Result { + self.traverse_address(value) + } + + fn visit_signer(&mut self, value: AccountAddress) -> Result { + self.traverse_signer(value) + } + + fn visit_vector( + &mut self, + driver: &mut VecDriver<'_, '_, '_>, + ) -> Result { + self.traverse_vector(driver) + } + + fn visit_struct( + &mut self, + driver: &mut StructDriver<'_, '_, '_>, + ) -> Result { + self.traverse_struct(driver) + } + + fn visit_variant( + &mut self, + driver: &mut VariantDriver<'_, '_, '_>, + ) -> Result { + self.traverse_variant(driver) + } +} + +/// Exposes information about a vector being visited (the element layout) to a +/// visitor implementation, and allows that visitor to progress the traversal +/// (by visiting or skipping elements). +pub struct VecDriver<'r, 'b, 'l> { + bytes: &'r mut &'b [u8], + layout: &'l MoveTypeLayout, + len: u64, + off: u64, +} + +/// Exposes information about a struct being visited (its layout, details about +/// the next field to be visited) to a visitor implementation, and allows that +/// visitor to progress the traversal (by visiting or skipping fields). +pub struct StructDriver<'r, 'b, 'l> { + bytes: &'r mut &'b [u8], + layout: &'l MoveStructLayout, + off: usize, +} + +/// Exposes information about a variant being visited (its layout, details about +/// the next field to be visited, the variant's tag, and name) to a visitor +/// implementation, and allows that visitor to progress the traversal (by +/// visiting or skipping fields). +pub struct VariantDriver<'r, 'b, 'l> { + bytes: &'r mut &'b [u8], + layout: &'l MoveEnumLayout, + tag: u16, + variant_name: &'l IdentStr, + variant_layout: &'l [MoveFieldLayout], + off: usize, +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("unexpected end of input")] + UnexpectedEof, + + #[error("unexpected byte: {0}")] + UnexpectedByte(u8), + + #[error("trailing {0} byte(s) at the end of input")] + TrailingBytes(usize), + + #[error("invalid variant tag: {0}")] + UnexpectedVariantTag(usize), +} + +/// The null traversal implements `Traversal` and `Visitor` but without doing +/// anything (does not return a value, and does not modify any state). This is +/// useful for skipping over parts of the value structure. +pub struct NullTraversal; + +impl Traversal for NullTraversal { + type Error = Error; +} + +#[allow(clippy::len_without_is_empty)] +impl<'r, 'b, 'l> VecDriver<'r, 'b, 'l> { + fn new(bytes: &'r mut &'b [u8], layout: &'l MoveTypeLayout, len: u64) -> Self { + Self { + bytes, + layout, + len, + off: 0, + } + } + + /// Type layout for the vector's inner type. + pub fn element_layout(&self) -> &'l MoveTypeLayout { + self.layout + } + + /// The number of elements in this vector + pub fn len(&self) -> u64 { + self.len + } + + /// Returns whether or not there are more elements to visit in this vector. + pub fn has_element(&self) -> bool { + self.off < self.len + } + + /// Visit the next element in the vector. The driver accepts a visitor to + /// use for this element, allowing the visitor to be changed on + /// recursive calls or even between elements in the same vector. + /// + /// Returns `Ok(None)` if there are no more elements in the vector, `Ok(v)` + /// if there was an element and it was successfully visited (where `v` + /// is the value returned by the visitor) or an error if there was an + /// underlying deserialization error, or an error during visitation. + pub fn next_element( + &mut self, + visitor: &mut V, + ) -> Result, V::Error> { + Ok(if self.off >= self.len { + None + } else { + let res = visit_value(self.bytes, self.layout, visitor)?; + self.off += 1; + Some(res) + }) + } + + /// Skip the next element in this vector. Returns whether there was an + /// element to skip or not on success, or an error if there was an + /// underlying deserialization error. + pub fn skip_element(&mut self) -> Result { + self.next_element(&mut NullTraversal).map(|v| v.is_some()) + } +} + +impl<'r, 'b, 'l> StructDriver<'r, 'b, 'l> { + fn new(bytes: &'r mut &'b [u8], layout: &'l MoveStructLayout) -> Self { + Self { + bytes, + layout, + off: 0, + } + } + + /// The layout of the struct being visited. + pub fn struct_layout(&self) -> &'l MoveStructLayout { + self.layout + } + + /// The layout of the next field to be visited (if there is one), or `None` + /// otherwise. + pub fn peek_field(&self) -> Option<&'l MoveFieldLayout> { + self.layout.fields.get(self.off) + } + + /// Visit the next field in the struct. The driver accepts a visitor to use + /// for this field, allowing the visitor to be changed on recursive + /// calls or even between fields in the same struct. + /// + /// Returns `Ok(None)` if there are no more fields in the struct, `Ok((f, + /// v))` if there was an field and it was successfully visited (where + /// `v` is the value returned by the visitor, and `f` is the layout of + /// the field that was visited) or an error if there was an underlying + /// deserialization error, or an error during visitation. + pub fn next_field( + &mut self, + visitor: &mut V, + ) -> Result, V::Error> { + let Some(field) = self.peek_field() else { + return Ok(None); + }; + + let res = visit_value(self.bytes, &field.layout, visitor)?; + self.off += 1; + Ok(Some((field, res))) + } + + /// Skip the next field. Returns the layout of the field that was visited if + /// there was one, or `None` if there was none. Can return an error if + /// there was a deserialization error. + pub fn skip_field(&mut self) -> Result, Error> { + self.next_field(&mut NullTraversal) + .map(|res| res.map(|(f, _)| f)) + } +} + +impl<'r, 'b, 'l> VariantDriver<'r, 'b, 'l> { + fn new( + bytes: &'r mut &'b [u8], + layout: &'l MoveEnumLayout, + variant_layout: &'l [MoveFieldLayout], + variant_name: &'l IdentStr, + tag: u16, + ) -> Self { + Self { + bytes, + layout, + tag, + variant_name, + variant_layout, + off: 0, + } + } + + /// The layout of the enum being visited. + pub fn enum_layout(&self) -> &'l MoveEnumLayout { + self.layout + } + + /// The layout of the variant being visited. + pub fn variant_layout(&self) -> &'l [MoveFieldLayout] { + self.variant_layout + } + + /// The tag of the variant being visited. + pub fn tag(&self) -> u16 { + self.tag + } + + /// The name of the enum variant being visited. + pub fn variant_name(&self) -> &'l IdentStr { + self.variant_name + } + + /// The layout of the next field to be visited (if there is one), or `None` + /// otherwise. + pub fn peek_field(&self) -> Option<&'l MoveFieldLayout> { + self.variant_layout.get(self.off) + } + + /// Visit the next field in the variant. The driver accepts a visitor to use + /// for this field, allowing the visitor to be changed on recursive + /// calls or even between fields in the same variant. + /// + /// Returns `Ok(None)` if there are no more fields in the variant, `Ok((f, + /// v))` if there was an field and it was successfully visited (where + /// `v` is the value returned by the visitor, and `f` is the layout of + /// the field that was visited) or an error if there was an underlying + /// deserialization error, or an error during visitation. + pub fn next_field( + &mut self, + visitor: &mut V, + ) -> Result, V::Error> { + let Some(field) = self.peek_field() else { + return Ok(None); + }; + + let res = visit_value(self.bytes, &field.layout, visitor)?; + self.off += 1; + Ok(Some((field, res))) + } + + /// Skip the next field. Returns the layout of the field that was visited if + /// there was one, or `None` if there was none. Can return an error if + /// there was a deserialization error. + pub fn skip_field(&mut self) -> Result, Error> { + self.next_field(&mut NullTraversal) + .map(|res| res.map(|(f, _)| f)) + } +} + +/// Visit a serialized Move value with the provided `layout`, held in `bytes`, +/// using the provided visitor to build a value out of it. See +/// `annotated_value::MoveValue::visit_deserialize` for details. +pub(crate) fn visit_value( + bytes: &mut &[u8], + layout: &MoveTypeLayout, + visitor: &mut V, +) -> Result { + use MoveTypeLayout as L; + + match layout { + L::Bool => match read_exact::<1>(bytes)? { + [0] => visitor.visit_bool(false), + [1] => visitor.visit_bool(true), + [b] => Err(Error::UnexpectedByte(b).into()), + }, + + L::U8 => visitor.visit_u8(u8::from_le_bytes(read_exact::<1>(bytes)?)), + L::U16 => visitor.visit_u16(u16::from_le_bytes(read_exact::<2>(bytes)?)), + L::U32 => visitor.visit_u32(u32::from_le_bytes(read_exact::<4>(bytes)?)), + L::U64 => visitor.visit_u64(u64::from_le_bytes(read_exact::<8>(bytes)?)), + L::U128 => visitor.visit_u128(u128::from_le_bytes(read_exact::<16>(bytes)?)), + L::U256 => visitor.visit_u256(U256::from_le_bytes(&read_exact::<32>(bytes)?)), + L::Address => visitor.visit_address(AccountAddress::new(read_exact::<32>(bytes)?)), + L::Signer => visitor.visit_signer(AccountAddress::new(read_exact::<32>(bytes)?)), + + L::Vector(l) => { + let len = leb128::read::unsigned(bytes).map_err(|_| Error::UnexpectedEof)?; + let mut driver = VecDriver::new(bytes, l.as_ref(), len); + let res = visitor.visit_vector(&mut driver)?; + while driver.skip_element()? {} + Ok(res) + } + L::Enum(e) => visit_variant(bytes, e, visitor), + L::Struct(l) => visit_struct(bytes, l, visitor), + } +} + +/// Like `visit_value` but specialized to visiting a struct (where the `bytes` +/// is known to be a serialized move struct), and the layout is a struct layout. +pub(crate) fn visit_struct( + bytes: &mut &[u8], + layout: &MoveStructLayout, + visitor: &mut V, +) -> Result { + let mut driver = StructDriver::new(bytes, layout); + let res = visitor.visit_struct(&mut driver)?; + while driver.skip_field()?.is_some() {} + Ok(res) +} + +/// Like `visit_struct` but specialized to visiting a variant (where the `bytes` +/// is known to be a serialized move variant), and the layout is an enum layout. +pub(crate) fn visit_variant( + bytes: &mut &[u8], + layout: &MoveEnumLayout, + visitor: &mut V, +) -> Result { + // Since variants are bounded at 127, we can read the tag as a single byte. + // When we add true ULEB encoding for enum variants switch to this: + // let tag = leb128::read::unsigned(bytes).map_err(|_| Error::UnexpectedEof)?; + let [tag] = read_exact::<1>(bytes)?; + if tag >= VARIANT_COUNT_MAX as u8 { + return Err(Error::UnexpectedVariantTag(tag as usize).into()); + } + let variant_layout = layout + .variants + .iter() + .find(|((_, vtag), _)| *vtag == tag as u16) + .ok_or(Error::UnexpectedVariantTag(tag as usize))?; + + let mut driver = VariantDriver::new( + bytes, + layout, + variant_layout.1, + &variant_layout.0.0, + tag as u16, + ); + let res = visitor.visit_variant(&mut driver)?; + while driver.skip_field()?.is_some() {} + Ok(res) +} + +fn read_exact(bytes: &mut &[u8]) -> Result<[u8; N], Error> { + let mut buf = [0u8; N]; + bytes + .read_exact(&mut buf) + .map_err(|_| Error::UnexpectedEof)?; + Ok(buf) +} diff --git a/identity_iota_interaction/src/sdk_types/move_core_types/identifier.rs b/identity_iota_interaction/src/sdk_types/move_core_types/identifier.rs new file mode 100644 index 0000000000..af0a2549ad --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_core_types/identifier.rs @@ -0,0 +1,225 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use core::str::FromStr; +use core::ops::Deref; +use core::fmt; +use std::borrow::Borrow; + +use ref_cast::RefCast; + +use serde::Deserialize; +use serde::Serialize; + +use anyhow::{bail, Result}; + + +/// Return true if this character can appear in a Move identifier. +/// +/// Note: there are stricter restrictions on whether a character can begin a +/// Move identifier--only alphabetic characters are allowed here. +#[inline] +pub const fn is_valid_identifier_char(c: char) -> bool { + matches!(c, '_' | 'a'..='z' | 'A'..='Z' | '0'..='9') +} + +/// Returns `true` if all bytes in `b` after the offset `start_offset` are valid +/// ASCII identifier characters. +const fn all_bytes_valid(b: &[u8], start_offset: usize) -> bool { + let mut i = start_offset; + // TODO(philiphayes): use for loop instead of while loop when it's stable in + // const fn's. + while i < b.len() { + if !is_valid_identifier_char(b[i] as char) { + return false; + } + i += 1; + } + true +} + +/// Describes what identifiers are allowed. +/// +/// For now this is deliberately restrictive -- we would like to evolve this in +/// the future. +// TODO: "" is coded as an exception. It should be removed once +// CompiledScript goes away. Note: needs to be pub as it's used in the +// `ident_str!` macro. +pub const fn is_valid(s: &str) -> bool { + // Rust const fn's don't currently support slicing or indexing &str's, so we + // have to operate on the underlying byte slice. This is not a problem as + // valid identifiers are (currently) ASCII-only. + let b = s.as_bytes(); + match b { + b"" => true, + [b'a'..=b'z', ..] | [b'A'..=b'Z', ..] => all_bytes_valid(b, 1), + [b'_', ..] if b.len() > 1 => all_bytes_valid(b, 1), + _ => false, + } +} + +/// An owned identifier. +/// +/// For more details, see the module level documentation. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)] +pub struct Identifier(Box); +// An identifier cannot be mutated so use Box instead of String -- it is 1 +// word smaller. + +impl Identifier { + /// Creates a new `Identifier` instance. + pub fn new(s: impl Into>) -> Result { + let s = s.into(); + if Self::is_valid(&s) { + Ok(Self(s)) + } else { + bail!("Invalid identifier '{}'", s); + } + } + + /// Returns true if this string is a valid identifier. + pub fn is_valid(s: impl AsRef) -> bool { + is_valid(s.as_ref()) + } + + /// Returns if this identifier is ``. + /// TODO: remove once we fully separate CompiledScript & CompiledModule. + pub fn is_self(&self) -> bool { + &*self.0 == "" + } + + /// Converts a vector of bytes to an `Identifier`. + pub fn from_utf8(vec: Vec) -> Result { + let s = String::from_utf8(vec)?; + Self::new(s) + } + + /// Creates a borrowed version of `self`. + pub fn as_ident_str(&self) -> &IdentStr { + self + } + + /// Converts this `Identifier` into a `String`. + /// + /// This is not implemented as a `From` trait to discourage automatic + /// conversions -- these conversions should not typically happen. + pub fn into_string(self) -> String { + self.0.into() + } + + /// Converts this `Identifier` into a UTF-8-encoded byte sequence. + pub fn into_bytes(self) -> Vec { + self.into_string().into_bytes() + } +} + +impl FromStr for Identifier { + type Err = anyhow::Error; + + fn from_str(data: &str) -> Result { + Self::new(data) + } +} + +impl From<&IdentStr> for Identifier { + fn from(ident_str: &IdentStr) -> Self { + ident_str.to_owned() + } +} + +impl AsRef for Identifier { + fn as_ref(&self) -> &IdentStr { + self + } +} + +impl Deref for Identifier { + type Target = IdentStr; + + fn deref(&self) -> &IdentStr { + // Identifier and IdentStr maintain the same invariants, so it is safe to + // convert. + IdentStr::ref_cast(&self.0) + } +} + +impl fmt::Display for Identifier { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", &self.0) + } +} + +/// A borrowed identifier. +/// +/// For more details, see the module level documentation. +#[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd, RefCast)] +#[repr(transparent)] +pub struct IdentStr(str); + +impl IdentStr { + pub fn new(s: &str) -> Result<&IdentStr> { + if Self::is_valid(s) { + Ok(IdentStr::ref_cast(s)) + } else { + bail!("Invalid identifier '{}'", s); + } + } + + /// Returns true if this string is a valid identifier. + pub fn is_valid(s: impl AsRef) -> bool { + is_valid(s.as_ref()) + } + + /// Returns the length of `self` in bytes. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if `self` has a length of zero bytes. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Converts `self` to a `&str`. + /// + /// This is not implemented as a `From` trait to discourage automatic + /// conversions -- these conversions should not typically happen. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Converts `self` to a byte slice. + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + // AbstractMemorySize is not available for wasm32 + // + // /// Returns the abstract size of the struct + // /// TODO (ade): use macro to enfornce determinism + // pub fn abstract_size_for_gas_metering(&self) -> AbstractMemorySize { + // AbstractMemorySize::new((self.len()) as u64) + // } +} + +impl Borrow for Identifier { + fn borrow(&self) -> &IdentStr { + self + } +} + +impl ToOwned for IdentStr { + type Owned = Identifier; + + fn to_owned(&self) -> Identifier { + Identifier(self.0.into()) + } +} + +impl fmt::Display for IdentStr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", &self.0) + } +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/move_core_types/language_storage.rs b/identity_iota_interaction/src/sdk_types/move_core_types/language_storage.rs new file mode 100644 index 0000000000..e0e5de89e7 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_core_types/language_storage.rs @@ -0,0 +1,381 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +use serde::{Deserialize, Serialize}; + +use super::{ + account_address::AccountAddress, +}; + +use super::identifier::{IdentStr, Identifier}; +use super::parser::{parse_type_tag, parse_struct_tag}; + +pub const CODE_TAG: u8 = 0; +pub const RESOURCE_TAG: u8 = 1; + +/// Hex address: 0x1 +pub const CORE_CODE_ADDRESS: AccountAddress = AccountAddress::ONE; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Hash, Eq, Clone, PartialOrd, Ord)] +pub enum TypeTag { + // alias for compatibility with old json serialized data. + #[serde(rename = "bool", alias = "Bool")] + Bool, + #[serde(rename = "u8", alias = "U8")] + U8, + #[serde(rename = "u64", alias = "U64")] + U64, + #[serde(rename = "u128", alias = "U128")] + U128, + #[serde(rename = "address", alias = "Address")] + Address, + #[serde(rename = "signer", alias = "Signer")] + Signer, + #[serde(rename = "vector", alias = "Vector")] + Vector(Box), + #[serde(rename = "struct", alias = "Struct")] + Struct(Box), + + // NOTE: Added in bytecode version v6, do not reorder! + #[serde(rename = "u16", alias = "U16")] + U16, + #[serde(rename = "u32", alias = "U32")] + U32, + #[serde(rename = "u256", alias = "U256")] + U256, +} + +impl TypeTag { + /// Return a canonical string representation of the type. All types are + /// represented using their source syntax: + /// "u8", "u64", "u128", "bool", "address", "vector", "signer" for ground + /// types. Struct types are represented as fully qualified type names; + /// e.g. `00000000000000000000000000000001::string::String` or + /// `0000000000000000000000000000000a::module_name1::type_name1<0000000000000000000000000000000a::module_name2::type_name2>` + /// With or without the prefix 0x depending on the `with_prefix` flag. + /// Addresses are hex-encoded lowercase values of length ADDRESS_LENGTH (16, + /// 20, or 32 depending on the Move platform) Note: this function is + /// guaranteed to be stable, and this is suitable for use inside + /// Move native functions or the VM. By contrast, the `Display` + /// implementation is subject to change and should not be used inside + /// stable code. + pub fn to_canonical_string(&self, with_prefix: bool) -> String { + self.to_canonical_display(with_prefix).to_string() + } + + /// Return the canonical string representation of the TypeTag conditionally + /// with prefix 0x + pub fn to_canonical_display(&self, with_prefix: bool) -> impl std::fmt::Display + '_ { + struct CanonicalDisplay<'a> { + data: &'a TypeTag, + with_prefix: bool, + } + + impl std::fmt::Display for CanonicalDisplay<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self.data { + TypeTag::Bool => write!(f, "bool"), + TypeTag::U8 => write!(f, "u8"), + TypeTag::U16 => write!(f, "u16"), + TypeTag::U32 => write!(f, "u32"), + TypeTag::U64 => write!(f, "u64"), + TypeTag::U128 => write!(f, "u128"), + TypeTag::U256 => write!(f, "u256"), + TypeTag::Address => write!(f, "address"), + TypeTag::Signer => write!(f, "signer"), + TypeTag::Vector(t) => { + write!(f, "vector<{}>", t.to_canonical_display(self.with_prefix)) + } + TypeTag::Struct(s) => write!(f, "{}", s.to_canonical_display(self.with_prefix)), + } + } + } + + CanonicalDisplay { + data: self, + with_prefix, + } + } +} + +impl FromStr for TypeTag { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + parse_type_tag(s) + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Hash, Eq, Clone, PartialOrd, Ord)] +pub struct StructTag { + pub address: AccountAddress, + pub module: Identifier, + pub name: Identifier, + // alias for compatibility with old json serialized data. + #[serde(rename = "type_args", alias = "type_params")] + pub type_params: Vec, +} + +impl StructTag { + pub fn access_vector(&self) -> Vec { + let mut key = vec![RESOURCE_TAG]; + key.append(&mut bcs::to_bytes(self).unwrap()); + key + } + + /// Returns true if this is a `StructTag` for an `std::ascii::String` struct + /// defined in the standard library at address `move_std_addr`. + pub fn is_ascii_string(&self, move_std_addr: &AccountAddress) -> bool { + self.address == *move_std_addr + && self.module.as_str().eq("ascii") + && self.name.as_str().eq("String") + } + + /// Returns true if this is a `StructTag` for an `std::string::String` + /// struct defined in the standard library at address `move_std_addr`. + pub fn is_std_string(&self, move_std_addr: &AccountAddress) -> bool { + self.address == *move_std_addr + && self.module.as_str().eq("string") + && self.name.as_str().eq("String") + } + + pub fn module_id(&self) -> ModuleId { + ModuleId::new(self.address, self.module.to_owned()) + } + + /// Return a canonical string representation of the struct. + /// Struct types are represented as fully qualified type names; e.g. + /// `00000000000000000000000000000001::string::String`, + /// `0000000000000000000000000000000a::module_name1::type_name1<0000000000000000000000000000000a::module_name2::type_name2>`, + /// or `0000000000000000000000000000000a::module_name2::type_name2. With or without the prefix 0x depending on the `with_prefix` + /// flag. Addresses are hex-encoded lowercase values of length + /// ADDRESS_LENGTH (16, 20, or 32 depending on the Move platform) + /// Note: this function is guaranteed to be stable, and this is suitable for + /// use inside Move native functions or the VM. By contrast, the + /// `Display` implementation is subject to change and should not be used + /// inside stable code. + pub fn to_canonical_string(&self, with_prefix: bool) -> String { + self.to_canonical_display(with_prefix).to_string() + } + + /// Implements the canonical string representation of the StructTag with the + /// prefix 0x + pub fn to_canonical_display(&self, with_prefix: bool) -> impl std::fmt::Display + '_ { + struct CanonicalDisplay<'a> { + data: &'a StructTag, + with_prefix: bool, + } + + impl std::fmt::Display for CanonicalDisplay<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}::{}::{}", + self.data.address.to_canonical_display(self.with_prefix), + self.data.module, + self.data.name + )?; + + if let Some(first_ty) = self.data.type_params.first() { + write!(f, "<")?; + write!(f, "{}", first_ty.to_canonical_display(self.with_prefix))?; + for ty in self.data.type_params.iter().skip(1) { + // Note that unlike Display for StructTag, there is no space between the + // comma and canonical display. This follows the + // original to_canonical_string() implementation. + write!(f, ",{}", ty.to_canonical_display(self.with_prefix))?; + } + write!(f, ">")?; + } + Ok(()) + } + } + + CanonicalDisplay { + data: self, + with_prefix, + } + } +} + +impl FromStr for StructTag { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + parse_struct_tag(s) + } +} + +/// Represents the initial key into global storage where we first index by the +/// address, and then the struct tag +#[derive(Serialize, Deserialize, Debug, PartialEq, Hash, Eq, Clone, PartialOrd, Ord)] +pub struct ModuleId { + address: AccountAddress, + name: Identifier, +} + +impl From for (AccountAddress, Identifier) { + fn from(module_id: ModuleId) -> Self { + (module_id.address, module_id.name) + } +} + +impl ModuleId { + pub fn new(address: AccountAddress, name: Identifier) -> Self { + ModuleId { address, name } + } + + pub fn name(&self) -> &IdentStr { + &self.name + } + + pub fn address(&self) -> &AccountAddress { + &self.address + } + + pub fn access_vector(&self) -> Vec { + let mut key = vec![CODE_TAG]; + key.append(&mut bcs::to_bytes(self).unwrap()); + key + } + + pub fn to_canonical_string(&self, with_prefix: bool) -> String { + self.to_canonical_display(with_prefix).to_string() + } + + /// Proxy type for overriding `ModuleId`'s display implementation, to use a + /// canonical form (full-width addresses), with an optional "0x" prefix + /// (controlled by the `with_prefix` flag). + pub fn to_canonical_display(&self, with_prefix: bool) -> impl Display + '_ { + struct IdDisplay<'a> { + id: &'a ModuleId, + with_prefix: bool, + } + + impl<'a> Display for IdDisplay<'a> { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!( + f, + "{}::{}", + self.id.address.to_canonical_display(self.with_prefix), + self.id.name, + ) + } + } + + IdDisplay { + id: self, + with_prefix, + } + } +} + +impl Display for ModuleId { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "{}", self.to_canonical_display(/* with_prefix */ false)) + } +} + +impl ModuleId { + pub fn short_str_lossless(&self) -> String { + format!("0x{}::{}", self.address.short_str_lossless(), self.name) + } +} + +impl Display for StructTag { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!( + f, + "0x{}::{}::{}", + self.address.short_str_lossless(), + self.module, + self.name + )?; + if let Some(first_ty) = self.type_params.first() { + write!(f, "<")?; + write!(f, "{}", first_ty)?; + for ty in self.type_params.iter().skip(1) { + write!(f, ", {}", ty)?; + } + write!(f, ">")?; + } + Ok(()) + } +} + +impl Display for TypeTag { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + match self { + TypeTag::Struct(s) => write!(f, "{}", s), + TypeTag::Vector(ty) => write!(f, "vector<{}>", ty), + TypeTag::U8 => write!(f, "u8"), + TypeTag::U16 => write!(f, "u16"), + TypeTag::U32 => write!(f, "u32"), + TypeTag::U64 => write!(f, "u64"), + TypeTag::U128 => write!(f, "u128"), + TypeTag::U256 => write!(f, "u256"), + TypeTag::Address => write!(f, "address"), + TypeTag::Signer => write!(f, "signer"), + TypeTag::Bool => write!(f, "bool"), + } + } +} + +impl From for TypeTag { + fn from(t: StructTag) -> TypeTag { + TypeTag::Struct(Box::new(t)) + } +} + +#[cfg(test)] +mod tests { + use std::mem; + + use super::{ModuleId, TypeTag}; + use crate::{ + account_address::AccountAddress, ident_str, identifier::Identifier, + language_storage::StructTag, + }; + + #[test] + fn test_type_tag_serde() { + let a = TypeTag::Struct(Box::new(StructTag { + address: AccountAddress::ONE, + module: Identifier::from_utf8(("abc".as_bytes()).to_vec()).unwrap(), + name: Identifier::from_utf8(("abc".as_bytes()).to_vec()).unwrap(), + type_params: vec![TypeTag::U8], + })); + let b = serde_json::to_string(&a).unwrap(); + let c: TypeTag = serde_json::from_str(&b).unwrap(); + assert!(a.eq(&c), "Typetag serde error"); + assert_eq!(mem::size_of::(), 16); + } + + #[test] + fn test_module_id_display() { + let id = ModuleId::new(AccountAddress::ONE, ident_str!("foo").to_owned()); + + assert_eq!( + format!("{id}"), + "0000000000000000000000000000000000000000000000000000000000000001::foo", + ); + + assert_eq!( + format!("{}", id.to_canonical_display(/* with_prefix */ false)), + "0000000000000000000000000000000000000000000000000000000000000001::foo", + ); + + assert_eq!( + format!("{}", id.to_canonical_display(/* with_prefix */ true)), + "0x0000000000000000000000000000000000000000000000000000000000000001::foo", + ); + } +} diff --git a/identity_iota_interaction/src/sdk_types/move_core_types/mod.rs b/identity_iota_interaction/src/sdk_types/move_core_types/mod.rs new file mode 100644 index 0000000000..1e62fdbb03 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_core_types/mod.rs @@ -0,0 +1,35 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub mod account_address; +pub mod annotated_value; +pub mod annotated_visitor; +pub mod identifier; +pub mod language_storage; +pub mod parser; +pub mod runtime_value; +pub mod u256; + +use std::fmt; + +pub const VARIANT_COUNT_MAX: u64 = 127; + +pub(crate) fn fmt_list( + f: &mut fmt::Formatter<'_>, + begin: &str, + items: impl IntoIterator, + end: &str, +) -> fmt::Result { + write!(f, "{}", begin)?; + let mut items = items.into_iter(); + if let Some(x) = items.next() { + write!(f, "{}", x)?; + for x in items { + write!(f, ", {}", x)?; + } + } + write!(f, "{}", end)?; + Ok(()) +} diff --git a/identity_iota_interaction/src/sdk_types/move_core_types/parser.rs b/identity_iota_interaction/src/sdk_types/move_core_types/parser.rs new file mode 100644 index 0000000000..949533cae6 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_core_types/parser.rs @@ -0,0 +1,358 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::iter::Peekable; + +use anyhow::{bail, format_err, Result}; + +use super::super::move_types::identifier; +use super::super::move_types::account_address::AccountAddress; +use super::super::move_types::identifier::Identifier; + +use super::language_storage::{TypeTag, StructTag}; + +#[derive(Eq, PartialEq, Debug)] +enum Token { + U8Type, + U16Type, + U32Type, + U64Type, + U128Type, + U256Type, + BoolType, + AddressType, + VectorType, + SignerType, + Whitespace(String), + Name(String), + Address(String), + U8(String), + U16(String), + U32(String), + U64(String), + U128(String), + U256(String), + + Bytes(String), + True, + False, + ColonColon, + Lt, + Gt, + Comma, + EOF, +} + +impl Token { + fn is_whitespace(&self) -> bool { + matches!(self, Self::Whitespace(_)) + } +} + +fn token_as_name(tok: Token) -> Result { + use Token::*; + Ok(match tok { + U8Type => "u8".to_string(), + U16Type => "u16".to_string(), + U32Type => "u32".to_string(), + U64Type => "u64".to_string(), + U128Type => "u128".to_string(), + U256Type => "u256".to_string(), + BoolType => "bool".to_string(), + AddressType => "address".to_string(), + VectorType => "vector".to_string(), + True => "true".to_string(), + False => "false".to_string(), + SignerType => "signer".to_string(), + Name(s) => s, + Whitespace(_) | Address(_) | U8(_) | U16(_) | U32(_) | U64(_) | U128(_) | U256(_) + | Bytes(_) | ColonColon | Lt | Gt | Comma | EOF => { + bail!("Invalid token. Expected a name but got {:?}", tok) + } + }) +} + +fn name_token(s: String) -> Token { + match s.as_str() { + "u8" => Token::U8Type, + "u16" => Token::U16Type, + "u32" => Token::U32Type, + "u64" => Token::U64Type, + "u128" => Token::U128Type, + "u256" => Token::U256Type, + "bool" => Token::BoolType, + "address" => Token::AddressType, + "vector" => Token::VectorType, + "true" => Token::True, + "false" => Token::False, + "signer" => Token::SignerType, + _ => Token::Name(s), + } +} + +fn next_number(initial: char, mut it: impl Iterator) -> Result<(Token, usize)> { + let mut num = String::new(); + num.push(initial); + loop { + match it.next() { + Some(c) if c.is_ascii_digit() || c == '_' => num.push(c), + Some(c) if c.is_alphanumeric() => { + let mut suffix = String::new(); + suffix.push(c); + loop { + match it.next() { + Some(c) if c.is_ascii_alphanumeric() => suffix.push(c), + _ => { + let len = num.len() + suffix.len(); + let tok = match suffix.as_str() { + "u8" => Token::U8(num), + "u16" => Token::U16(num), + "u32" => Token::U32(num), + "u64" => Token::U64(num), + "u128" => Token::U128(num), + "u256" => Token::U256(num), + _ => bail!("invalid suffix"), + }; + return Ok((tok, len)); + } + } + } + } + _ => { + let len = num.len(); + return Ok((Token::U64(num), len)); + } + } + } +} + +#[allow(clippy::many_single_char_names)] +fn next_token(s: &str) -> Result> { + let mut it = s.chars().peekable(); + match it.next() { + None => Ok(None), + Some(c) => Ok(Some(match c { + '<' => (Token::Lt, 1), + '>' => (Token::Gt, 1), + ',' => (Token::Comma, 1), + ':' => match it.next() { + Some(':') => (Token::ColonColon, 2), + _ => bail!("unrecognized token"), + }, + '0' if it.peek() == Some(&'x') || it.peek() == Some(&'X') => { + it.next().unwrap(); + match it.next() { + Some(c) if c.is_ascii_hexdigit() => { + let mut r = String::new(); + r.push('0'); + r.push('x'); + r.push(c); + for c in it { + if c.is_ascii_hexdigit() { + r.push(c); + } else { + break; + } + } + let len = r.len(); + (Token::Address(r), len) + } + _ => bail!("unrecognized token"), + } + } + c if c.is_ascii_digit() => next_number(c, it)?, + 'b' if it.peek() == Some(&'"') => { + it.next().unwrap(); + let mut r = String::new(); + loop { + match it.next() { + Some('"') => break, + Some(c) if c.is_ascii() => r.push(c), + _ => bail!("unrecognized token"), + } + } + let len = r.len() + 3; + (Token::Bytes(hex::encode(r)), len) + } + 'x' if it.peek() == Some(&'"') => { + it.next().unwrap(); + let mut r = String::new(); + loop { + match it.next() { + Some('"') => break, + Some(c) if c.is_ascii_hexdigit() => r.push(c), + _ => bail!("unrecognized token"), + } + } + let len = r.len() + 3; + (Token::Bytes(r), len) + } + c if c.is_ascii_whitespace() => { + let mut r = String::new(); + r.push(c); + for c in it { + if c.is_ascii_whitespace() { + r.push(c); + } else { + break; + } + } + let len = r.len(); + (Token::Whitespace(r), len) + } + c if c.is_ascii_alphabetic() => { + let mut r = String::new(); + r.push(c); + for c in it { + if identifier::is_valid_identifier_char(c) { + r.push(c); + } else { + break; + } + } + let len = r.len(); + (name_token(r), len) + } + _ => bail!("unrecognized token"), + })), + } +} + +fn tokenize(mut s: &str) -> Result> { + let mut v = vec![]; + while let Some((tok, n)) = next_token(s)? { + v.push(tok); + s = &s[n..]; + } + Ok(v) +} + +struct Parser> { + it: Peekable, +} + +impl> Parser { + fn new>(v: T) -> Self { + Self { + it: v.into_iter().peekable(), + } + } + + fn next(&mut self) -> Result { + match self.it.next() { + Some(tok) => Ok(tok), + None => bail!("out of tokens, this should not happen"), + } + } + + fn peek(&mut self) -> Option<&Token> { + self.it.peek() + } + + fn consume(&mut self, tok: Token) -> Result<()> { + let t = self.next()?; + if t != tok { + bail!("expected token {:?}, got {:?}", tok, t) + } + Ok(()) + } + + fn parse_comma_list( + &mut self, + parse_list_item: F, + end_token: Token, + allow_trailing_comma: bool, + ) -> Result> + where + F: Fn(&mut Self) -> Result, + R: std::fmt::Debug, + { + let mut v = vec![]; + if !(self.peek() == Some(&end_token)) { + loop { + v.push(parse_list_item(self)?); + if self.peek() == Some(&end_token) { + break; + } + self.consume(Token::Comma)?; + if self.peek() == Some(&end_token) && allow_trailing_comma { + break; + } + } + } + Ok(v) + } + + fn parse_type_tag(&mut self) -> Result { + Ok(match self.next()? { + Token::U8Type => TypeTag::U8, + Token::U16Type => TypeTag::U16, + Token::U32Type => TypeTag::U32, + Token::U64Type => TypeTag::U64, + Token::U128Type => TypeTag::U128, + Token::U256Type => TypeTag::U256, + Token::BoolType => TypeTag::Bool, + Token::AddressType => TypeTag::Address, + Token::SignerType => TypeTag::Signer, + Token::VectorType => { + self.consume(Token::Lt)?; + let ty = self.parse_type_tag()?; + self.consume(Token::Gt)?; + TypeTag::Vector(Box::new(ty)) + } + Token::Address(addr) => { + self.consume(Token::ColonColon)?; + let module = self.next().and_then(token_as_name)?; + self.consume(Token::ColonColon)?; + let name = self.next().and_then(token_as_name)?; + let ty_args = if self.peek() == Some(&Token::Lt) { + self.next()?; + let ty_args = + self.parse_comma_list(|parser| parser.parse_type_tag(), Token::Gt, true)?; + self.consume(Token::Gt)?; + ty_args + } else { + vec![] + }; + TypeTag::Struct(Box::new(StructTag { + address: AccountAddress::from_hex_literal(&addr)?, + module: Identifier::new(module)?, + name: Identifier::new(name)?, + type_params: ty_args, + })) + } + tok => bail!("unexpected token {:?}, expected type tag", tok), + }) + } +} + +fn parse(s: &str, f: F) -> Result + where + F: Fn(&mut Parser>) -> Result, +{ + let mut tokens: Vec<_> = tokenize(s)? + .into_iter() + .filter(|tok| !tok.is_whitespace()) + .collect(); + tokens.push(Token::EOF); + let mut parser = Parser::new(tokens); + let res = f(&mut parser)?; + parser.consume(Token::EOF)?; + Ok(res) +} + +pub fn parse_type_tag(s: &str) -> Result { + parse(s, |parser| parser.parse_type_tag()) +} + +pub fn parse_struct_tag(s: &str) -> Result { + let type_tag = parse(s, |parser| parser.parse_type_tag()) + .map_err(|e| format_err!("invalid struct tag: {}, {}", s, e))?; + if let TypeTag::Struct(struct_tag) = type_tag { + Ok(*struct_tag) + } else { + bail!("invalid struct tag: {}", s) + } +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/move_core_types/runtime_value.rs b/identity_iota_interaction/src/sdk_types/move_core_types/runtime_value.rs new file mode 100644 index 0000000000..00d6800367 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_core_types/runtime_value.rs @@ -0,0 +1,590 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::{self, Debug}; + +use serde::{ + de::Error as DeError, + ser::{SerializeSeq, SerializeTuple}, + Deserialize, Serialize, +}; + +use anyhow::{anyhow, Result as AResult}; +//use move_proc_macros::test_variant_order; +// use crate::{ +// de::Error as DeError, +// ser::{SerializeSeq, SerializeTuple}, +// Deserialize, Serialize, +// }; + +use super::{account_address::AccountAddress, annotated_value as A, fmt_list, u256, VARIANT_COUNT_MAX}; + +/// In the `WithTypes` configuration, a Move struct gets serialized into a Serde +/// struct with this name +pub const MOVE_STRUCT_NAME: &str = "struct"; + +/// In the `WithTypes` configuration, a Move struct gets serialized into a Serde +/// struct with this as the first field +pub const MOVE_STRUCT_TYPE: &str = "type"; + +/// In the `WithTypes` configuration, a Move struct gets serialized into a Serde +/// struct with this as the second field +pub const MOVE_STRUCT_FIELDS: &str = "fields"; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct MoveStruct(pub Vec); + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct MoveVariant { + pub tag: u16, + pub fields: Vec, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum MoveValue { + U8(u8), + U64(u64), + U128(u128), + Bool(bool), + Address(AccountAddress), + Vector(Vec), + Struct(MoveStruct), + Signer(AccountAddress), + // NOTE: Added in bytecode version v6, do not reorder! + U16(u16), + U32(u32), + U256(u256::U256), + Variant(MoveVariant), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MoveStructLayout(pub Vec); + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MoveEnumLayout(pub Vec>); + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MoveDatatypeLayout { + Struct(MoveStructLayout), + Enum(MoveEnumLayout), +} + +impl MoveDatatypeLayout { + pub fn into_layout(self) -> MoveTypeLayout { + match self { + MoveDatatypeLayout::Struct(layout) => MoveTypeLayout::Struct(layout), + MoveDatatypeLayout::Enum(layout) => MoveTypeLayout::Enum(layout), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MoveTypeLayout { + #[serde(rename(serialize = "bool", deserialize = "bool"))] + Bool, + #[serde(rename(serialize = "u8", deserialize = "u8"))] + U8, + #[serde(rename(serialize = "u64", deserialize = "u64"))] + U64, + #[serde(rename(serialize = "u128", deserialize = "u128"))] + U128, + #[serde(rename(serialize = "address", deserialize = "address"))] + Address, + #[serde(rename(serialize = "vector", deserialize = "vector"))] + Vector(Box), + #[serde(rename(serialize = "struct", deserialize = "struct"))] + Struct(MoveStructLayout), + #[serde(rename(serialize = "signer", deserialize = "signer"))] + Signer, + + // NOTE: Added in bytecode version v6, do not reorder! + #[serde(rename(serialize = "u16", deserialize = "u16"))] + U16, + #[serde(rename(serialize = "u32", deserialize = "u32"))] + U32, + #[serde(rename(serialize = "u256", deserialize = "u256"))] + U256, + #[serde(rename(serialize = "enum", deserialize = "enum"))] + Enum(MoveEnumLayout), +} + +impl MoveValue { + pub fn simple_deserialize(blob: &[u8], ty: &MoveTypeLayout) -> AResult { + Ok(bcs::from_bytes_seed(ty, blob)?) + } + + pub fn simple_serialize(&self) -> Option> { + bcs::to_bytes(self).ok() + } + + pub fn vector_u8(v: Vec) -> Self { + MoveValue::Vector(v.into_iter().map(MoveValue::U8).collect()) + } + + /// Converts the `Vec` to a `Vec` if the inner `MoveValue` is + /// a `MoveValue::U8`, or returns an error otherwise. + pub fn vec_to_vec_u8(vec: Vec) -> AResult> { + let mut vec_u8 = Vec::with_capacity(vec.len()); + + for byte in vec { + match byte { + MoveValue::U8(u8) => { + vec_u8.push(u8); + } + _ => { + return Err(anyhow!( + "Expected inner MoveValue in Vec to be a MoveValue::U8" + .to_string(), + )); + } + } + } + Ok(vec_u8) + } + + pub fn vector_address(v: Vec) -> Self { + MoveValue::Vector(v.into_iter().map(MoveValue::Address).collect()) + } + + pub fn decorate(self, layout: &A::MoveTypeLayout) -> A::MoveValue { + match (self, layout) { + (MoveValue::Struct(s), A::MoveTypeLayout::Struct(l)) => { + A::MoveValue::Struct(s.decorate(l)) + } + (MoveValue::Variant(s), A::MoveTypeLayout::Enum(l)) => { + A::MoveValue::Variant(s.decorate(l)) + } + (MoveValue::Vector(vals), A::MoveTypeLayout::Vector(t)) => { + A::MoveValue::Vector(vals.into_iter().map(|v| v.decorate(t)).collect()) + } + (MoveValue::U8(a), _) => A::MoveValue::U8(a), + (MoveValue::U64(u), _) => A::MoveValue::U64(u), + (MoveValue::U128(u), _) => A::MoveValue::U128(u), + (MoveValue::Bool(b), _) => A::MoveValue::Bool(b), + (MoveValue::Address(a), _) => A::MoveValue::Address(a), + (MoveValue::Signer(a), _) => A::MoveValue::Signer(a), + (MoveValue::U16(u), _) => A::MoveValue::U16(u), + (MoveValue::U32(u), _) => A::MoveValue::U32(u), + (MoveValue::U256(u), _) => A::MoveValue::U256(u), + _ => panic!("Invalid decoration"), + } + } +} + +pub fn serialize_values<'a, I>(vals: I) -> Vec> +where + I: IntoIterator, +{ + vals.into_iter() + .map(|val| { + val.simple_serialize() + .expect("serialization should succeed") + }) + .collect() +} + +impl MoveStruct { + pub fn new(value: Vec) -> Self { + Self(value) + } + + pub fn simple_deserialize(blob: &[u8], ty: &MoveStructLayout) -> AResult { + Ok(bcs::from_bytes_seed(ty, blob)?) + } + + pub fn decorate(self, layout: &A::MoveStructLayout) -> A::MoveStruct { + let MoveStruct(vals) = self; + let A::MoveStructLayout { type_, fields } = layout; + A::MoveStruct { + type_: type_.clone(), + fields: vals + .into_iter() + .zip(fields) + .map(|(v, l)| (l.name.clone(), v.decorate(&l.layout))) + .collect(), + } + } + + pub fn fields(&self) -> &[MoveValue] { + &self.0 + } + + pub fn into_fields(self) -> Vec { + self.0 + } +} + +impl MoveVariant { + pub fn new(tag: u16, fields: Vec) -> Self { + Self { tag, fields } + } + + pub fn simple_deserialize(blob: &[u8], ty: &MoveEnumLayout) -> AResult { + Ok(bcs::from_bytes_seed(ty, blob)?) + } + + pub fn decorate(self, layout: &A::MoveEnumLayout) -> A::MoveVariant { + let MoveVariant { tag, fields } = self; + let A::MoveEnumLayout { type_, variants } = layout; + let ((v_name, _), v_layout) = variants + .iter() + .find(|((_, v_tag), _)| *v_tag == tag) + .unwrap(); + A::MoveVariant { + type_: type_.clone(), + tag, + fields: fields + .into_iter() + .zip(v_layout) + .map(|(v, l)| (l.name.clone(), v.decorate(&l.layout))) + .collect(), + variant_name: v_name.clone(), + } + } + + pub fn fields(&self) -> &[MoveValue] { + &self.fields + } + + pub fn into_fields(self) -> Vec { + self.fields + } +} + +impl MoveStructLayout { + pub fn new(types: Vec) -> Self { + Self(types) + } + + pub fn fields(&self) -> &[MoveTypeLayout] { + &self.0 + } + + pub fn into_fields(self) -> Vec { + self.0 + } +} + +impl<'d> serde::de::DeserializeSeed<'d> for &MoveTypeLayout { + type Value = MoveValue; + fn deserialize>( + self, + deserializer: D, + ) -> Result { + match self { + MoveTypeLayout::Bool => bool::deserialize(deserializer).map(MoveValue::Bool), + MoveTypeLayout::U8 => u8::deserialize(deserializer).map(MoveValue::U8), + MoveTypeLayout::U16 => u16::deserialize(deserializer).map(MoveValue::U16), + MoveTypeLayout::U32 => u32::deserialize(deserializer).map(MoveValue::U32), + MoveTypeLayout::U64 => u64::deserialize(deserializer).map(MoveValue::U64), + MoveTypeLayout::U128 => u128::deserialize(deserializer).map(MoveValue::U128), + MoveTypeLayout::U256 => u256::U256::deserialize(deserializer).map(MoveValue::U256), + MoveTypeLayout::Address => { + AccountAddress::deserialize(deserializer).map(MoveValue::Address) + } + MoveTypeLayout::Signer => { + AccountAddress::deserialize(deserializer).map(MoveValue::Signer) + } + MoveTypeLayout::Struct(ty) => Ok(MoveValue::Struct(ty.deserialize(deserializer)?)), + MoveTypeLayout::Enum(ty) => Ok(MoveValue::Variant(ty.deserialize(deserializer)?)), + MoveTypeLayout::Vector(layout) => Ok(MoveValue::Vector( + deserializer.deserialize_seq(VectorElementVisitor(layout))?, + )), + } + } +} + +struct VectorElementVisitor<'a>(&'a MoveTypeLayout); + +impl<'d, 'a> serde::de::Visitor<'d> for VectorElementVisitor<'a> { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Vector") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'d>, + { + let mut vals = Vec::new(); + while let Some(elem) = seq.next_element_seed(self.0)? { + vals.push(elem) + } + Ok(vals) + } +} + +struct StructFieldVisitor<'a>(&'a [MoveTypeLayout]); + +impl<'d, 'a> serde::de::Visitor<'d> for StructFieldVisitor<'a> { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Struct") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'d>, + { + let mut val = Vec::new(); + for (i, field_type) in self.0.iter().enumerate() { + match seq.next_element_seed(field_type)? { + Some(elem) => val.push(elem), + None => return Err(A::Error::invalid_length(i, &self)), + } + } + Ok(val) + } +} + +impl<'d> serde::de::DeserializeSeed<'d> for &MoveStructLayout { + type Value = MoveStruct; + + fn deserialize>( + self, + deserializer: D, + ) -> Result { + Ok(MoveStruct(deserializer.deserialize_tuple( + self.0.len(), + StructFieldVisitor(&self.0), + )?)) + } +} + +impl<'d> serde::de::DeserializeSeed<'d> for &MoveEnumLayout { + type Value = MoveVariant; + fn deserialize>( + self, + deserializer: D, + ) -> Result { + deserializer.deserialize_tuple(2, EnumFieldVisitor(&self.0)) + } +} + +struct EnumFieldVisitor<'a>(&'a Vec>); + +impl<'d, 'a> serde::de::Visitor<'d> for EnumFieldVisitor<'a> { + type Value = MoveVariant; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Enum") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'d>, + { + let tag = match seq.next_element_seed(&MoveTypeLayout::U8)? { + Some(MoveValue::U8(tag)) if tag as u64 <= VARIANT_COUNT_MAX => tag as u16, + Some(MoveValue::U8(tag)) => return Err(A::Error::invalid_length(tag as usize, &self)), + Some(val) => { + return Err(A::Error::invalid_type( + serde::de::Unexpected::Other(&format!("{val:?}")), + &self, + )); + } + None => return Err(A::Error::invalid_length(0, &self)), + }; + + let Some(variant_layout) = self.0.get(tag as usize) else { + return Err(A::Error::invalid_length(tag as usize, &self)); + }; + + let Some(fields) = seq.next_element_seed(&MoveVariantFieldLayout(variant_layout))? else { + return Err(A::Error::invalid_length(1, &self)); + }; + + Ok(MoveVariant { tag, fields }) + } +} + +struct MoveVariantFieldLayout<'a>(&'a [MoveTypeLayout]); + +impl<'d, 'a> serde::de::DeserializeSeed<'d> for &MoveVariantFieldLayout<'a> { + type Value = Vec; + + fn deserialize>( + self, + deserializer: D, + ) -> Result { + deserializer.deserialize_tuple(self.0.len(), StructFieldVisitor(self.0)) + } +} + +impl serde::Serialize for MoveValue { + fn serialize(&self, serializer: S) -> Result { + match self { + MoveValue::Struct(s) => s.serialize(serializer), + MoveValue::Variant(v) => v.serialize(serializer), + MoveValue::Bool(b) => serializer.serialize_bool(*b), + MoveValue::U8(i) => serializer.serialize_u8(*i), + MoveValue::U16(i) => serializer.serialize_u16(*i), + MoveValue::U32(i) => serializer.serialize_u32(*i), + MoveValue::U64(i) => serializer.serialize_u64(*i), + MoveValue::U128(i) => serializer.serialize_u128(*i), + MoveValue::U256(i) => i.serialize(serializer), + MoveValue::Address(a) => a.serialize(serializer), + MoveValue::Signer(a) => a.serialize(serializer), + MoveValue::Vector(v) => { + let mut t = serializer.serialize_seq(Some(v.len()))?; + for val in v { + t.serialize_element(val)?; + } + t.end() + } + } + } +} + +impl serde::Serialize for MoveStruct { + fn serialize(&self, serializer: S) -> Result { + let mut t = serializer.serialize_tuple(self.0.len())?; + for v in self.0.iter() { + t.serialize_element(v)?; + } + t.end() + } +} + +impl serde::Serialize for MoveVariant { + // Serialize a variant as: (tag, [fields...]) + // Since we restrict tags to be less than or equal to 127, the tag will always + // be a single byte in uleb encoding and we don't actually need to uleb + // encode it, but we can at a later date if we want/need to. + fn serialize(&self, serializer: S) -> Result { + let tag = if self.tag as u64 > VARIANT_COUNT_MAX { + return Err(serde::ser::Error::custom(format!( + "Variant tag {} is greater than the maximum allowed value of {}", + self.tag, VARIANT_COUNT_MAX + ))); + } else { + self.tag as u8 + }; + + let mut t = serializer.serialize_tuple(2)?; + + t.serialize_element(&tag)?; + t.serialize_element(&MoveFields(&self.fields))?; + + t.end() + } +} + +struct MoveFields<'a>(&'a [MoveValue]); + +impl<'a> serde::Serialize for MoveFields<'a> { + fn serialize(&self, serializer: S) -> Result { + let mut t = serializer.serialize_tuple(self.0.len())?; + for v in self.0.iter() { + t.serialize_element(v)?; + } + t.end() + } +} + +impl fmt::Display for MoveTypeLayout { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + use MoveTypeLayout::*; + match self { + Bool => write!(f, "bool"), + U8 => write!(f, "u8"), + U16 => write!(f, "u16"), + U32 => write!(f, "u32"), + U64 => write!(f, "u64"), + U128 => write!(f, "u128"), + U256 => write!(f, "u256"), + Address => write!(f, "address"), + Signer => write!(f, "signer"), + Vector(typ) if f.alternate() => write!(f, "vector<{typ:#}>"), + Vector(typ) => write!(f, "vector<{typ}>"), + Struct(s) if f.alternate() => write!(f, "{s:#}"), + Struct(s) => write!(f, "{s}"), + Enum(e) if f.alternate() => write!(f, "{e:#}"), + Enum(e) => write!(f, "{e}"), + } + } +} + +/// Helper type that uses `T`'s `Display` implementation as its own `Debug` +/// implementation, to allow other `Display` implementations in this module to +/// take advantage of the structured formatting helpers that Rust uses for its +/// own debug types. +struct DebugAsDisplay<'a, T>(&'a T); +impl<'a, T: fmt::Display> fmt::Debug for DebugAsDisplay<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + write!(f, "{:#}", self.0) + } else { + write!(f, "{}", self.0) + } + } +} + +impl fmt::Display for MoveStructLayout { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + use DebugAsDisplay as DD; + + write!(f, "struct ")?; + let mut map = f.debug_map(); + for (i, l) in self.0.iter().enumerate() { + map.entry(&i, &DD(&l)); + } + + map.finish() + } +} + +impl fmt::Display for MoveEnumLayout { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + write!(f, "enum ")?; + for (tag, variant) in self.0.iter().enumerate() { + write!(f, "variant_tag: {} {{ ", tag)?; + for (i, l) in variant.iter().enumerate() { + write!(f, "{}: {}, ", i, l)? + } + write!(f, " }} ")?; + } + Ok(()) + } +} + +impl fmt::Display for MoveValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MoveValue::U8(u) => write!(f, "{}u8", u), + MoveValue::U16(u) => write!(f, "{}u16", u), + MoveValue::U32(u) => write!(f, "{}u32", u), + MoveValue::U64(u) => write!(f, "{}u64", u), + MoveValue::U128(u) => write!(f, "{}u128", u), + MoveValue::U256(u) => write!(f, "{}u256", u), + MoveValue::Bool(false) => write!(f, "false"), + MoveValue::Bool(true) => write!(f, "true"), + MoveValue::Address(a) => write!(f, "{}", a.to_hex_literal()), + MoveValue::Signer(a) => write!(f, "signer({})", a.to_hex_literal()), + MoveValue::Vector(v) => fmt_list(f, "vector[", v, "]"), + MoveValue::Struct(s) => fmt::Display::fmt(s, f), + MoveValue::Variant(v) => fmt::Display::fmt(v, f), + } + } +} + +impl fmt::Display for MoveStruct { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt_list(f, "struct[", &self.0, "]") + } +} + +impl fmt::Display for MoveVariant { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt_list( + f, + &format!("variant(tag = {})[", self.tag), + &self.fields, + "]", + ) + } +} diff --git a/identity_iota_interaction/src/sdk_types/move_core_types/u256.rs b/identity_iota_interaction/src/sdk_types/move_core_types/u256.rs new file mode 100644 index 0000000000..51f91391d1 --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/move_core_types/u256.rs @@ -0,0 +1,379 @@ +// Copyright (c) The Move Contributors +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + fmt, + mem::size_of, + ops::{ + Shl, Shr, + }, +}; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use uint::FromStrRadixErr; + +// This U256 impl was chosen for now but we are open to changing it as needed +use primitive_types::U256 as PrimitiveU256; + +const NUM_BITS_PER_BYTE: usize = 8; +const U256_NUM_BITS: usize = 256; +pub const U256_NUM_BYTES: usize = U256_NUM_BITS / NUM_BITS_PER_BYTE; + +#[derive(Debug)] +pub struct U256FromStrError(FromStrRadixErr); + +/// A list of error categories encountered when parsing numbers. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum U256CastErrorKind { + /// Value too large to fit in U8. + TooLargeForU8, + + /// Value too large to fit in U16. + TooLargeForU16, + + /// Value too large to fit in U32. + TooLargeForU32, + + /// Value too large to fit in U64. + TooLargeForU64, + + /// Value too large to fit in U128. + TooLargeForU128, +} + +#[derive(Debug)] +pub struct U256CastError { + kind: U256CastErrorKind, + val: U256, +} + +impl U256CastError { + pub fn new>(val: T, kind: U256CastErrorKind) -> Self { + Self { + kind, + val: val.into(), + } + } +} + +impl std::error::Error for U256CastError {} + +impl fmt::Display for U256CastError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let type_str = match self.kind { + U256CastErrorKind::TooLargeForU8 => "u8", + U256CastErrorKind::TooLargeForU16 => "u16", + U256CastErrorKind::TooLargeForU32 => "u32", + U256CastErrorKind::TooLargeForU64 => "u64", + U256CastErrorKind::TooLargeForU128 => "u128", + }; + let err_str = format!("Cast failed. {} too large for {}.", self.val, type_str); + write!(f, "{err_str}") + } +} + +impl std::error::Error for U256FromStrError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.0.source() + } +} + +impl fmt::Display for U256FromStrError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Copy, PartialOrd, Ord, Default)] +pub struct U256(PrimitiveU256); + +impl fmt::Display for U256 { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::UpperHex for U256 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::UpperHex::fmt(&self.0, f) + } +} + +impl fmt::LowerHex for U256 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + +impl std::str::FromStr for U256 { + type Err = U256FromStrError; + + fn from_str(s: &str) -> Result { + Self::from_str_radix(s, 10) + } +} + +impl<'de> Deserialize<'de> for U256 { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + Ok(U256::from_le_bytes( + &(<[u8; U256_NUM_BYTES]>::deserialize(deserializer)?), + )) + } +} + +impl Serialize for U256 { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + self.to_le_bytes().serialize(serializer) + } +} + +impl U256 { + /// Zero value as U256 + pub const fn zero() -> Self { + Self(PrimitiveU256::zero()) + } + + /// One value as U256 + pub const fn one() -> Self { + Self(PrimitiveU256::one()) + } + + /// Max value of U256: + /// 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + pub const fn max_value() -> Self { + Self(PrimitiveU256::max_value()) + } + + /// U256 from string with radix 10 or 16 + pub fn from_str_radix(src: &str, radix: u32) -> Result { + PrimitiveU256::from_str_radix(src.trim_start_matches('0'), radix) + .map(Self) + .map_err(U256FromStrError) + } + + /// U256 from 32 little endian bytes + pub fn from_le_bytes(slice: &[u8; U256_NUM_BYTES]) -> Self { + Self(PrimitiveU256::from_little_endian(slice)) + } + + /// U256 to 32 little endian bytes + pub fn to_le_bytes(self) -> [u8; U256_NUM_BYTES] { + let mut bytes = [0u8; U256_NUM_BYTES]; + self.0.to_little_endian(&mut bytes); + bytes + } + + /// Leading zeros of the number + pub fn leading_zeros(&self) -> u32 { + self.0.leading_zeros() + } + + // Unchecked downcasting. Values as truncated if larger than target max + pub fn unchecked_as_u8(&self) -> u8 { + self.0.low_u128() as u8 + } + + pub fn unchecked_as_u16(&self) -> u16 { + self.0.low_u128() as u16 + } + + pub fn unchecked_as_u32(&self) -> u32 { + self.0.low_u128() as u32 + } + + pub fn unchecked_as_u64(&self) -> u64 { + self.0.low_u128() as u64 + } + + pub fn unchecked_as_u128(&self) -> u128 { + self.0.low_u128() + } + + // Check arithmetic + /// Checked integer addition. Computes self + rhs, returning None if + /// overflow occurred. + pub fn checked_add(self, rhs: Self) -> Option { + self.0.checked_add(rhs.0).map(Self) + } + + /// Checked integer subtraction. Computes self - rhs, returning None if + /// overflow occurred. + pub fn checked_sub(self, rhs: Self) -> Option { + self.0.checked_sub(rhs.0).map(Self) + } + + /// Checked integer multiplication. Computes self * rhs, returning None if + /// overflow occurred. + pub fn checked_mul(self, rhs: Self) -> Option { + self.0.checked_mul(rhs.0).map(Self) + } + + /// Checked integer division. Computes self / rhs, returning None if rhs == + /// 0. + pub fn checked_div(self, rhs: Self) -> Option { + self.0.checked_div(rhs.0).map(Self) + } + + /// Checked integer remainder. Computes self % rhs, returning None if rhs == + /// 0. + pub fn checked_rem(self, rhs: Self) -> Option { + self.0.checked_rem(rhs.0).map(Self) + } + + /// Checked integer remainder. Computes self % rhs, returning None if rhs == + /// 0. + pub fn checked_shl(self, rhs: u32) -> Option { + if rhs >= U256_NUM_BITS as u32 { + return None; + } + Some(Self(self.0.shl(rhs))) + } + + /// Checked shift right. Computes self >> rhs, returning None if rhs is + /// larger than or equal to the number of bits in self. + pub fn checked_shr(self, rhs: u32) -> Option { + if rhs >= U256_NUM_BITS as u32 { + return None; + } + Some(Self(self.0.shr(rhs))) + } + + /// Downcast to a an unsigned value of type T + /// T must be at most u128 + pub fn down_cast_lossy>(self) -> T { + // Size of this type + let type_size = size_of::(); + // Maximum value for this type + let max_val: u128 = if type_size < 16 { + (1u128 << (NUM_BITS_PER_BYTE * type_size)) - 1u128 + } else { + u128::MAX + }; + // This should never fail + match T::try_from(self.0.low_u128() & max_val) { + Ok(w) => w, + Err(_) => panic!("Fatal! Downcast failed"), + } + } + + /// Wrapping integer addition. Computes self + rhs, wrapping around at the + /// boundary of the type. By definition in std::instrinsics, + /// a.wrapping_add(b) = (a + b) % (2^N), where N is bit width + pub fn wrapping_add(self, rhs: Self) -> Self { + Self(self.0.overflowing_add(rhs.0).0) + } + + /// Wrapping integer subtraction. Computes self - rhs, wrapping around at + /// the boundary of the type. By definition in std::instrinsics, + /// a.wrapping_add(b) = (a - b) % (2^N), where N is bit width + pub fn wrapping_sub(self, rhs: Self) -> Self { + Self(self.0.overflowing_sub(rhs.0).0) + } + + /// Wrapping integer multiplication. Computes self * rhs, wrapping around + /// at the boundary of the type. By definition in std::instrinsics, + /// a.wrapping_mul(b) = (a * b) % (2^N), where N is bit width + pub fn wrapping_mul(self, rhs: Self) -> Self { + Self(self.0.overflowing_mul(rhs.0).0) + } +} + +impl From for U256 { + fn from(n: u8) -> Self { + U256(PrimitiveU256::from(n)) + } +} + +impl From for U256 { + fn from(n: u16) -> Self { + U256(PrimitiveU256::from(n)) + } +} + +impl From for U256 { + fn from(n: u32) -> Self { + U256(PrimitiveU256::from(n)) + } +} + +impl From for U256 { + fn from(n: u64) -> Self { + U256(PrimitiveU256::from(n)) + } +} + +impl From for U256 { + fn from(n: u128) -> Self { + U256(PrimitiveU256::from(n)) + } +} + +impl TryFrom for u8 { + type Error = U256CastError; + fn try_from(n: U256) -> Result { + let n = n.0.low_u64(); + if n > u8::MAX as u64 { + Err(U256CastError::new(n, U256CastErrorKind::TooLargeForU8)) + } else { + Ok(n as u8) + } + } +} + +impl TryFrom for u16 { + type Error = U256CastError; + + fn try_from(n: U256) -> Result { + let n = n.0.low_u64(); + if n > u16::MAX as u64 { + Err(U256CastError::new(n, U256CastErrorKind::TooLargeForU16)) + } else { + Ok(n as u16) + } + } +} + +impl TryFrom for u32 { + type Error = U256CastError; + + fn try_from(n: U256) -> Result { + let n = n.0.low_u64(); + if n > u32::MAX as u64 { + Err(U256CastError::new(n, U256CastErrorKind::TooLargeForU32)) + } else { + Ok(n as u32) + } + } +} + +impl TryFrom for u64 { + type Error = U256CastError; + + fn try_from(n: U256) -> Result { + let n = n.0.low_u128(); + if n > u64::MAX as u128 { + Err(U256CastError::new(n, U256CastErrorKind::TooLargeForU64)) + } else { + Ok(n as u64) + } + } +} + +impl TryFrom for u128 { + type Error = U256CastError; + + fn try_from(n: U256) -> Result { + if n > U256::from(u128::MAX) { + Err(U256CastError::new(n, U256CastErrorKind::TooLargeForU128)) + } else { + Ok(n.0.low_u128()) + } + } +} \ No newline at end of file diff --git a/identity_iota_interaction/src/sdk_types/shared_crypto/intent.rs b/identity_iota_interaction/src/sdk_types/shared_crypto/intent.rs new file mode 100644 index 0000000000..03ff73711d --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/shared_crypto/intent.rs @@ -0,0 +1,204 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; +use std::vec::Vec; +use std::default::Default; +use std::convert::{TryFrom, TryInto}; +use std::result::Result::{Ok, Err}; + +use eyre::eyre; +use fastcrypto::encoding::decode_bytes_hex; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use Result; + +pub const INTENT_PREFIX_LENGTH: usize = 3; + +/// The version here is to distinguish between signing different versions of the +/// struct or enum. Serialized output between two different versions of the same +/// struct/enum might accidentally (or maliciously on purpose) match. +#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, PartialEq, Eq, Debug, Hash)] +#[repr(u8)] +pub enum IntentVersion { + V0 = 0, +} + +impl TryFrom for IntentVersion { + type Error = eyre::Report; + fn try_from(value: u8) -> Result { + bcs::from_bytes(&[value]).map_err(|_| eyre!("Invalid IntentVersion")) + } +} + +/// This enums specifies the application ID. Two intents in two different +/// applications (i.e., IOTA, Ethereum etc) should never collide, so +/// that even when a signing key is reused, nobody can take a signature +/// designated for app_1 and present it as a valid signature for an (any) intent +/// in app_2. +#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, PartialEq, Eq, Debug, Hash)] +#[repr(u8)] +pub enum AppId { + Iota = 0, + Consensus = 1, +} + +// TODO(joyqvq): Use num_derive +impl TryFrom for AppId { + type Error = eyre::Report; + fn try_from(value: u8) -> Result { + bcs::from_bytes(&[value]).map_err(|_| eyre!("Invalid AppId")) + } +} + +impl Default for AppId { + fn default() -> Self { + Self::Iota + } +} + +/// This enums specifies the intent scope. Two intents for different scope +/// should never collide, so no signature provided for one intent scope can be +/// used for another, even when the serialized data itself may be the same. +#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, PartialEq, Eq, Debug, Hash)] +#[repr(u8)] +pub enum IntentScope { + TransactionData = 0, // Used for a user signature on a transaction data. + TransactionEffects = 1, // Used for an authority signature on transaction effects. + CheckpointSummary = 2, // Used for an authority signature on a checkpoint summary. + PersonalMessage = 3, // Used for a user signature on a personal message. + SenderSignedTransaction = 4, // Used for an authority signature on a user signed transaction. + ProofOfPossession = 5, /* Used as a signature representing an authority's proof of + * possession of its authority key. */ + BridgeEventUnused = 6, // for bridge purposes but it's currently not included in messages. + ConsensusBlock = 7, // Used for consensus authority signature on block's digest +} + +impl TryFrom for IntentScope { + type Error = eyre::Report; + fn try_from(value: u8) -> Result { + bcs::from_bytes(&[value]).map_err(|_| eyre!("Invalid IntentScope")) + } +} + +/// An intent is a compact struct serves as the domain separator for a message +/// that a signature commits to. It consists of three parts: [enum IntentScope] +/// (what the type of the message is), [enum IntentVersion], [enum AppId] (what +/// application that the signature refers to). It is used to construct [struct +/// IntentMessage] that what a signature commits to. +/// +/// The serialization of an Intent is a 3-byte array where each field is +/// represented by a byte. +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Hash)] +pub struct Intent { + pub scope: IntentScope, + pub version: IntentVersion, + pub app_id: AppId, +} + +impl Intent { + pub fn to_bytes(&self) -> [u8; INTENT_PREFIX_LENGTH] { + [self.scope as u8, self.version as u8, self.app_id as u8] + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != INTENT_PREFIX_LENGTH { + return Err(eyre!("Invalid Intent")); + } + Ok(Self { + scope: bytes[0].try_into()?, + version: bytes[1].try_into()?, + app_id: bytes[2].try_into()?, + }) + } +} + +impl FromStr for Intent { + type Err = eyre::Report; + fn from_str(s: &str) -> Result { + let bytes: Vec = decode_bytes_hex(s).map_err(|_| eyre!("Invalid Intent"))?; + Self::from_bytes(bytes.as_slice()) + } +} + +impl Intent { + pub fn iota_app(scope: IntentScope) -> Self { + Self { + version: IntentVersion::V0, + scope, + app_id: AppId::Iota, + } + } + + pub fn iota_transaction() -> Self { + Self { + scope: IntentScope::TransactionData, + version: IntentVersion::V0, + app_id: AppId::Iota, + } + } + + pub fn personal_message() -> Self { + Self { + scope: IntentScope::PersonalMessage, + version: IntentVersion::V0, + app_id: AppId::Iota, + } + } + + pub fn consensus_app(scope: IntentScope) -> Self { + Self { + scope, + version: IntentVersion::V0, + app_id: AppId::Consensus, + } + } +} + +/// Intent Message is a wrapper around a message with its intent. The message +/// can be any type that implements [trait Serialize]. *ALL* signatures in IOTA +/// must commits to the intent message, not the message itself. This guarantees +/// any intent message signed in the system cannot collide with another since +/// they are domain separated by intent. +/// +/// The serialization of an IntentMessage is compact: it only appends three +/// bytes to the message itself. +#[derive(Debug, PartialEq, Eq, Serialize, Clone, Hash, Deserialize)] +pub struct IntentMessage { + pub intent: Intent, + pub value: T, +} + +impl IntentMessage { + pub fn new(intent: Intent, value: T) -> Self { + Self { intent, value } + } +} + +/// A person message that wraps around a byte array. +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct PersonalMessage { + pub message: Vec, +} + +pub trait SecureIntent: Serialize + private::SealedIntent {} + +pub(crate) mod private { + use super::IntentMessage; + + pub trait SealedIntent {} + impl SealedIntent for IntentMessage {} +} + +/// A 1-byte domain separator for hashing Object ID in IOTA. It is starting from +/// 0xf0 to ensure no hashing collision for any ObjectID vs IotaAddress which is +/// derived as the hash of `flag || pubkey`. See +/// `iota_types::crypto::SignatureScheme::flag()`. +#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, PartialEq, Eq, Debug, Hash)] +#[repr(u8)] +pub enum HashingIntentScope { + ChildObjectId = 0xf0, + RegularObjectId = 0xf1, +} diff --git a/identity_iota_interaction/src/sdk_types/shared_crypto/mod.rs b/identity_iota_interaction/src/sdk_types/shared_crypto/mod.rs new file mode 100644 index 0000000000..db49c84edf --- /dev/null +++ b/identity_iota_interaction/src/sdk_types/shared_crypto/mod.rs @@ -0,0 +1,4 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub mod intent; \ No newline at end of file diff --git a/identity_iota_interaction/src/transaction_builder_trait.rs b/identity_iota_interaction/src/transaction_builder_trait.rs new file mode 100644 index 0000000000..e6bdcc9f2e --- /dev/null +++ b/identity_iota_interaction/src/transaction_builder_trait.rs @@ -0,0 +1,15 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::ProgrammableTransactionBcs; + +pub trait TransactionBuilderT { + type Error; + type NativeTxBuilder; + + fn finish(self) -> Result; + + fn as_native_tx_builder(&mut self) -> &mut Self::NativeTxBuilder; + + fn into_native_tx_builder(self) -> Self::NativeTxBuilder; +} diff --git a/identity_jose/Cargo.toml b/identity_jose/Cargo.toml index 71e941434d..89ef5cf3af 100644 --- a/identity_jose/Cargo.toml +++ b/identity_jose/Cargo.toml @@ -17,14 +17,12 @@ iota-crypto = { version = "0.23.2", default-features = false, features = ["std", json-proof-token.workspace = true serde.workspace = true serde_json = { version = "1.0", default-features = false, features = ["std"] } -subtle = { version = "2.5", default-features = false } thiserror.workspace = true zeroize = { version = "1.6", default-features = false, features = ["std", "zeroize_derive"] } [dev-dependencies] -anyhow = "1" -iota-crypto = { version = "0.23.2", features = ["ed25519", "random", "hmac"] } -p256 = { version = "0.12.0", default-features = false, features = ["std", "ecdsa", "ecdsa-core"] } +iota-crypto = { version = "0.23", features = ["ed25519", "random", "hmac"] } +p256 = { version = "0.13.0", default-features = false, features = ["std", "ecdsa", "ecdsa-core"] } signature = { version = "2", default-features = false } [[example]] diff --git a/identity_jose/src/error.rs b/identity_jose/src/error.rs index 9a4afd4cb1..5d8a77367f 100644 --- a/identity_jose/src/error.rs +++ b/identity_jose/src/error.rs @@ -41,7 +41,7 @@ pub enum Error { #[error("attempt to parse an unregistered jws algorithm")] JwsAlgorithmParsingError, /// Caused by an error during signature verification. - #[error("signature verification error")] + #[error("signature verification error; {0}")] SignatureVerificationError(#[source] crate::jws::SignatureVerificationError), /// Caused by a mising header. #[error("missing header")] diff --git a/identity_jose/src/tests/es256.rs b/identity_jose/src/tests/es256.rs index 7b8e343728..b6a2c68339 100644 --- a/identity_jose/src/tests/es256.rs +++ b/identity_jose/src/tests/es256.rs @@ -24,7 +24,7 @@ pub(crate) fn expand_p256_jwk(jwk: &Jwk) -> (SecretKey, PublicKey) { } let sk_bytes = params.d.as_ref().map(jwu::decode_b64).unwrap().unwrap(); - let sk = SecretKey::from_be_bytes(&sk_bytes).unwrap(); + let sk = SecretKey::from_slice(&sk_bytes).unwrap(); // Transformation according to section 2.3.3 from http://www.secg.org/sec1-v2.pdf. let pk_bytes: Vec = [0x04] diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index fe763ddb6c..083b4caa63 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -26,19 +26,18 @@ thiserror = { version = "1.0", default-features = false } version = "=1.5.0" path = "../identity_iota_core" default-features = false -features = ["send-sync-client-ext", "iota-client"] +features = ["iota-client"] optional = true [dev-dependencies] -identity_iota_core = { path = "../identity_iota_core", features = ["test"] } -iota-sdk = { version = "1.1.5" } tokio = { version = "1.29.0", default-features = false, features = ["rt-multi-thread", "macros"] } [features] -default = ["revocation-bitmap", "iota"] +default = ["revocation-bitmap", "iota", "send-sync-client"] revocation-bitmap = ["identity_credential/revocation-bitmap", "identity_iota_core?/revocation-bitmap"] # Enables the IOTA integration for the resolver. iota = ["dep:identity_iota_core"] +send-sync-client = ["identity_iota_core?/send-sync-client-ext"] [lints] workspace = true diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs index 228a65582b..4519c45d39 100644 --- a/identity_resolver/src/resolution/resolver.rs +++ b/identity_resolver/src/resolution/resolver.rs @@ -20,7 +20,7 @@ use super::commands::Command; use super::commands::SendSyncCommand; use super::commands::SingleThreadedCommand; -/// Convenience type for resolving DID documents from different DID methods. +/// Convenience type for resolving DID documents from different DID methods. /// /// # Configuration /// @@ -264,7 +264,7 @@ impl + 'static> Resolver> { } } -#[cfg(feature = "iota")] +#[cfg(all(feature = "iota", not(target_arch = "wasm32")))] mod iota_handler { use crate::ErrorCause; @@ -272,81 +272,86 @@ mod iota_handler { use identity_document::document::CoreDocument; use identity_iota_core::IotaDID; use identity_iota_core::IotaDocument; - use identity_iota_core::IotaIdentityClientExt; - use std::collections::HashMap; use std::sync::Arc; - impl Resolver - where - DOC: From + AsRef + 'static, - { - /// Convenience method for attaching a new handler responsible for resolving IOTA DIDs. - /// - /// See also [`attach_handler`](Self::attach_handler). - pub fn attach_iota_handler(&mut self, client: CLI) - where - CLI: IotaIdentityClientExt + Send + Sync + 'static, - { - let arc_client: Arc = Arc::new(client); + mod iota_specific { + use identity_iota_core::DidResolutionHandler; + use std::collections::HashMap; - let handler = move |did: IotaDID| { - let future_client = arc_client.clone(); - async move { future_client.resolve_did(&did).await } - }; + use super::*; - self.attach_handler(IotaDID::METHOD.to_owned(), handler); - } - - /// Convenience method for attaching multiple handlers responsible for resolving IOTA DIDs - /// on multiple networks. - /// - /// - /// # Arguments - /// - /// * `clients` - A collection of tuples where each tuple contains the name of the network name and its - /// corresponding client. - /// - /// # Examples - /// - /// ```ignore - /// // Assume `smr_client` and `iota_client` are instances IOTA clients `iota_sdk::client::Client`. - /// attach_multiple_iota_handlers(vec![("smr", smr_client), ("iota", iota_client)]); - /// ``` - /// - /// # See Also - /// - [`attach_handler`](Self::attach_handler). - /// - /// # Note - /// - /// - Using `attach_iota_handler` or `attach_handler` for the IOTA method would override all previously added - /// clients. - /// - This function does not validate the provided configuration. Ensure that the provided network name corresponds - /// with the client, possibly by using `client.network_name()`. - pub fn attach_multiple_iota_handlers(&mut self, clients: I) + impl Resolver where - CLI: IotaIdentityClientExt + Send + Sync + 'static, - I: IntoIterator, + DOC: From + AsRef + 'static, { - let arc_clients = Arc::new(clients.into_iter().collect::>()); - - let handler = move |did: IotaDID| { - let future_client = arc_clients.clone(); - async move { - let did_network = did.network_str(); - let client: &CLI = - future_client - .get(did_network) - .ok_or(crate::Error::new(ErrorCause::UnsupportedNetwork( - did_network.to_string(), - )))?; - client - .resolve_did(&did) - .await - .map_err(|err| crate::Error::new(ErrorCause::HandlerError { source: Box::new(err) })) - } - }; - - self.attach_handler(IotaDID::METHOD.to_owned(), handler); + /// Convenience method for attaching a new handler responsible for resolving IOTA DIDs. + /// + /// See also [`attach_handler`](Self::attach_handler). + pub fn attach_iota_handler(&mut self, client: CLI) + where + CLI: DidResolutionHandler + Send + Sync + 'static, + { + let arc_client: Arc = Arc::new(client); + + let handler = move |did: IotaDID| { + let future_client = arc_client.clone(); + async move { future_client.resolve_did(&did).await } + }; + + self.attach_handler(IotaDID::METHOD.to_owned(), handler); + } + + /// Convenience method for attaching multiple handlers responsible for resolving IOTA DIDs + /// on multiple networks. + /// + /// + /// # Arguments + /// + /// * `clients` - A collection of tuples where each tuple contains the name of the network name and its + /// corresponding client. + /// + /// # Examples + /// + /// ```ignore + /// // Assume `client1` and `client2` are instances of identity clients `IdentityClientReadOnly`. + /// attach_multiple_iota_handlers(vec![("client1", client1), ("client2", client2)]); + /// ``` + /// + /// # See Also + /// - [`attach_handler`](Self::attach_handler). + /// + /// # Note + /// + /// - Using `attach_iota_handler` or `attach_handler` for the IOTA method would override all previously added + /// clients. + /// - This function does not validate the provided configuration. Ensure that the provided network name + /// corresponds with the client, possibly by using `client.network_name()`. + pub fn attach_multiple_iota_handlers(&mut self, clients: I) + where + CLI: DidResolutionHandler + Send + Sync + 'static, + I: IntoIterator, + { + let arc_clients = Arc::new(clients.into_iter().collect::>()); + + let handler = move |did: IotaDID| { + let future_client = arc_clients.clone(); + async move { + let did_network = did.network_str(); + let client: &CLI = + future_client + .get(did_network) + .ok_or(crate::Error::new(ErrorCause::UnsupportedNetwork( + did_network.to_string(), + )))?; + client + .resolve_did(&did) + .await + .map_err(|err| crate::Error::new(ErrorCause::HandlerError { source: Box::new(err) })) + } + }; + + self.attach_handler(IotaDID::METHOD.to_owned(), handler); + } } } } @@ -375,42 +380,28 @@ where #[cfg(test)] mod tests { - use identity_iota_core::block::output::AliasId; - use identity_iota_core::block::output::AliasOutput; - use identity_iota_core::block::output::OutputId; - use identity_iota_core::block::protocol::ProtocolParameters; + use identity_iota_core::DidResolutionHandler; use identity_iota_core::IotaDID; use identity_iota_core::IotaDocument; - use identity_iota_core::IotaIdentityClient; - use identity_iota_core::IotaIdentityClientExt; use super::*; struct DummyClient(IotaDocument); #[async_trait::async_trait] - impl IotaIdentityClient for DummyClient { - async fn get_alias_output(&self, _id: AliasId) -> identity_iota_core::Result<(OutputId, AliasOutput)> { - unreachable!() - } - async fn get_protocol_parameters(&self) -> identity_iota_core::Result { - unreachable!() - } - } - - #[async_trait::async_trait] - impl IotaIdentityClientExt for DummyClient { + impl DidResolutionHandler for DummyClient { async fn resolve_did(&self, did: &IotaDID) -> identity_iota_core::Result { if self.0.id().as_str() == did.as_str() { Ok(self.0.clone()) } else { Err(identity_iota_core::Error::DIDResolutionError( - iota_sdk::client::error::Error::NoOutput(did.to_string()), + "DID not found".to_string(), )) } } } + #[cfg(feature = "iota")] #[tokio::test] async fn test_multiple_handlers() { let did1 = diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index 1295430510..61acebf74a 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -11,9 +11,11 @@ repository.workspace = true description = "Abstractions over storage for cryptographic keys used in DID Documents" [dependencies] -anyhow = "1.0.82" +anyhow = { version = "1.0.82" } async-trait = { version = "0.1.64", default-features = false } +bcs = { version = "0.1.4", optional = true } bls12_381_plus = { workspace = true, optional = true } +fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "5f2c63266a065996d53f98156f0412782b468597", package = "fastcrypto", optional = true } futures = { version = "0.3.27", default-features = false, features = ["async-await"] } identity_core = { version = "=1.5.0", path = "../identity_core", default-features = false } identity_credential = { version = "=1.5.0", path = "../identity_credential", default-features = false, features = ["credential", "presentation", "revocation-bitmap"] } @@ -25,12 +27,19 @@ iota-crypto = { version = "0.23.2", default-features = false, features = ["ed255 json-proof-token = { workspace = true, optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } seahash = { version = "4.1.0", default-features = false } +secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", default-features = false, tag = "v0.2.0", optional = true } serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"], optional = true } zkryptium = { workspace = true, optional = true } +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +identity_iota_interaction = { version = "=1.5.0", path = "../identity_iota_interaction", optional = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +identity_iota_interaction = { version = "=1.5.0", path = "../identity_iota_interaction", default-features = false, optional = true } + [dev-dependencies] identity_credential = { version = "=1.5.0", path = "../identity_credential", features = ["revocation-bitmap"] } identity_eddsa_verifier = { version = "=1.5.0", path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } @@ -42,9 +51,17 @@ default = ["iota-document", "memstore"] # Exposes in-memory implementations of the storage traits intended exclusively for testing. memstore = ["dep:tokio", "dep:rand", "dep:iota-crypto"] # Enables `Send` + `Sync` bounds for the storage traits. -send-sync-storage = [] +send-sync-storage = ["identity_iota_core?/send-sync-client-ext", "secret-storage?/send-sync-storage"] # Implements the JwkStorageDocumentExt trait for IotaDocument iota-document = ["dep:identity_iota_core"] +# enables support to sign via storage +storage-signer = [ + "identity_iota_core?/iota-client", + "dep:secret-storage", + "dep:identity_iota_interaction", + "dep:fastcrypto", + "dep:bcs", +] # Enables JSON Proof Token & BBS+ related features jpt-bbs-plus = [ "identity_credential/jpt-bbs-plus", diff --git a/identity_storage/src/storage/error.rs b/identity_storage/src/storage/error.rs index 51af20ead0..307ecf24fb 100644 --- a/identity_storage/src/storage/error.rs +++ b/identity_storage/src/storage/error.rs @@ -30,13 +30,12 @@ pub enum JwkStorageDocumentError { /// Caused by an invalid JWP algorithm. #[error("invalid JWP algorithm")] InvalidJwpAlgorithm, - /// Cannot cunstruct a valid Jwp (issued or presented form) + /// Cannot construct a valid Jwp (issued or presented form) #[error("Not able to construct a valid Jwp")] JwpBuildingError, /// Credential's proof update internal error #[error("Credential's proof internal error")] ProofUpdateError(String), - /// Caused by a failure to construct a verification method. #[error("method generation failed: unable to create a valid verification method")] VerificationMethodConstructionError(#[source] identity_verification::Error), diff --git a/identity_storage/src/storage/mod.rs b/identity_storage/src/storage/mod.rs index 7643c41a95..a1fff2c089 100644 --- a/identity_storage/src/storage/mod.rs +++ b/identity_storage/src/storage/mod.rs @@ -12,6 +12,8 @@ mod signature_options; #[cfg(feature = "jpt-bbs-plus")] mod timeframe_revocation_ext; +#[cfg(feature = "storage-signer")] +mod storage_signer; #[cfg(all(test, feature = "memstore"))] pub(crate) mod tests; @@ -21,6 +23,8 @@ pub use jwk_document_ext::*; #[cfg(feature = "jpt-bbs-plus")] pub use jwp_document_ext::*; pub use signature_options::*; +#[cfg(feature = "storage-signer")] +pub use storage_signer::*; #[cfg(feature = "jpt-bbs-plus")] pub use timeframe_revocation_ext::*; diff --git a/identity_storage/src/storage/storage_signer.rs b/identity_storage/src/storage/storage_signer.rs new file mode 100644 index 0000000000..0c7c1707d6 --- /dev/null +++ b/identity_storage/src/storage/storage_signer.rs @@ -0,0 +1,154 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::anyhow; +use anyhow::Context as _; +use async_trait::async_trait; +use fastcrypto::hash::Blake2b256; +use fastcrypto::traits::ToFromBytes; +use identity_iota_interaction::shared_crypto::intent::Intent; +use identity_iota_interaction::types::crypto::PublicKey; +use identity_iota_interaction::types::crypto::Signature; +use identity_iota_interaction::types::crypto::SignatureScheme as IotaSignatureScheme; +use identity_iota_interaction::IotaKeySignature; +use identity_iota_interaction::OptionalSync; +use identity_iota_interaction::TransactionDataBcs; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkParams; +use identity_verification::jwk::JwkParamsEc; +use identity_verification::jwu; +use secret_storage::Error as SecretStorageError; +use secret_storage::Signer; + +use crate::JwkStorage; +use crate::KeyId; +use crate::KeyIdStorage; +use crate::KeyStorageErrorKind; +use crate::Storage; + +/// Signer that offers signing capabilities for `Signer` trait from `secret_storage`. +/// `Storage` is used to sign. +pub struct StorageSigner<'a, K, I> { + key_id: KeyId, + public_key: Jwk, + storage: &'a Storage, +} + +impl Clone for StorageSigner<'_, K, I> { + fn clone(&self) -> Self { + StorageSigner { + key_id: self.key_id.clone(), + public_key: self.public_key.clone(), + storage: self.storage, + } + } +} + +impl<'a, K, I> StorageSigner<'a, K, I> { + /// Creates new `StorageSigner` with reference to a `Storage` instance. + pub fn new(storage: &'a Storage, key_id: KeyId, public_key: Jwk) -> Self { + Self { + key_id, + public_key, + storage, + } + } + + /// Returns a reference to the [`KeyId`] of the key used by this [`Signer`]. + pub fn key_id(&self) -> &KeyId { + &self.key_id + } + + /// Returns this [`Signer`]'s public key as [`Jwk`]. + pub fn public_key(&self) -> &Jwk { + &self.public_key + } + + /// Returns a reference to this [`Signer`]'s [`Storage`]. + pub fn storage(&self) -> &Storage { + self.storage + } +} + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl Signer for StorageSigner<'_, K, I> +where + K: JwkStorage + OptionalSync, + I: KeyIdStorage + OptionalSync, +{ + type KeyId = KeyId; + + fn key_id(&self) -> &KeyId { + &self.key_id + } + + async fn public_key(&self) -> Result { + match self.public_key.params() { + JwkParams::Okp(params) => { + if params.crv != "Ed25519" { + return Err(SecretStorageError::Other(anyhow!( + "unsupported key type {}", + params.crv + ))); + } + + jwu::decode_b64(¶ms.x) + .context("failed to base64 decode key") + .and_then(|pk_bytes| { + PublicKey::try_from_bytes(IotaSignatureScheme::ED25519, &pk_bytes).map_err(|e| anyhow!("{e}")) + }) + .map_err(SecretStorageError::Other) + } + JwkParams::Ec(JwkParamsEc { crv, x, y, .. }) => { + let pk_bytes = { + let mut decoded_x_bytes = jwu::decode_b64(x) + .map_err(|e| SecretStorageError::Other(anyhow!("failed to decode b64 x parameter: {e}")))?; + let mut decoded_y_bytes = jwu::decode_b64(y) + .map_err(|e| SecretStorageError::Other(anyhow!("failed to decode b64 y parameter: {e}")))?; + decoded_x_bytes.append(&mut decoded_y_bytes); + + decoded_x_bytes + }; + + if self.public_key.alg() == Some("ES256") || crv == "P-256" { + PublicKey::try_from_bytes(IotaSignatureScheme::Secp256r1, &pk_bytes) + .map_err(|e| SecretStorageError::Other(anyhow!("not a secp256r1 key: {e}"))) + } else if self.public_key.alg() == Some("ES256K") || crv == "K-256" { + PublicKey::try_from_bytes(IotaSignatureScheme::Secp256k1, &pk_bytes) + .map_err(|e| SecretStorageError::Other(anyhow!("not a secp256k1 key: {e}"))) + } else { + Err(SecretStorageError::Other(anyhow!("invalid EC key"))) + } + } + _ => Err(SecretStorageError::Other(anyhow!("unsupported key"))), + } + } + async fn sign(&self, data: &TransactionDataBcs) -> Result { + use fastcrypto::hash::HashFunction; + + let intent_bytes = Intent::iota_transaction().to_bytes(); + let mut hasher = Blake2b256::default(); + hasher.update(intent_bytes); + hasher.update(data); + let digest = hasher.finalize().digest; + + let signature_bytes = self + .storage + .key_storage() + .sign(&self.key_id, &digest, &self.public_key) + .await + .map_err(|e| match e.kind() { + KeyStorageErrorKind::KeyNotFound => SecretStorageError::KeyNotFound(e.to_string()), + KeyStorageErrorKind::RetryableIOFailure => SecretStorageError::StoreDisconnected(e.to_string()), + _ => SecretStorageError::Other(anyhow::anyhow!(e)), + })?; + + let public_key = Signer::public_key(self).await?; + + let iota_signature_bytes = [[public_key.flag()].as_slice(), &signature_bytes, public_key.as_ref()].concat(); + + Signature::from_bytes(&iota_signature_bytes) + .map_err(|e| SecretStorageError::Other(anyhow!("failed to create valid IOTA signature: {e}"))) + } +} diff --git a/rustfmt.toml b/rustfmt.toml index c0842c2114..103ed48b33 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -6,3 +6,6 @@ normalize_doc_attributes = false tab_spaces = 2 wrap_comments = true imports_granularity = "Item" +ignore = [ + "identity_iota_interaction/src/sdk_types", +]