From 41f0f88ca6f2035a462966be1e33821e7c39c5a0 Mon Sep 17 00:00:00 2001 From: Rigidity Date: Tue, 15 Oct 2024 13:40:22 -0400 Subject: [PATCH] Full review --- .cargo/config.toml | 2 + .github/workflows/napi.yml | 271 ++ .github/workflows/rust.yml | 59 + .gitignore | 3 + .prettierrc | 3 + .rustfmt.toml | 1 + Cargo.lock | 2696 ++++++++++++++++- Cargo.toml | 139 +- LICENSE | 201 ++ README.md | 38 +- crates/chia-sdk-client/Cargo.toml | 34 + crates/chia-sdk-client/src/client.rs | 131 + crates/chia-sdk-client/src/connect.rs | 61 + crates/chia-sdk-client/src/error.rs | 58 + crates/chia-sdk-client/src/lib.rs | 23 + crates/chia-sdk-client/src/network.rs | 79 + crates/chia-sdk-client/src/peer.rs | 339 +++ crates/chia-sdk-client/src/request_map.rs | 61 + crates/chia-sdk-client/src/tls.rs | 127 + crates/chia-sdk-derive/Cargo.toml | 23 + crates/chia-sdk-derive/src/impl_conditions.rs | 396 +++ crates/chia-sdk-derive/src/lib.rs | 10 + crates/chia-sdk-driver/Cargo.toml | 42 + crates/chia-sdk-driver/docs.md | 42 + crates/chia-sdk-driver/src/driver_error.rs | 50 + crates/chia-sdk-driver/src/hashed_ptr.rs | 185 ++ crates/chia-sdk-driver/src/layer.rs | 94 + crates/chia-sdk-driver/src/layers.rs | 44 + .../chia-sdk-driver/src/layers/cat_layer.rs | 176 ++ .../chia-sdk-driver/src/layers/datalayer.rs | 66 + .../src/layers/datalayer/delegation_layer.rs | 171 ++ .../src/layers/datalayer/oracle_layer.rs | 87 + .../src/layers/datalayer/writer_layer.rs | 157 + .../chia-sdk-driver/src/layers/did_layer.rs | 155 + .../src/layers/nft_ownership_layer.rs | 121 + .../src/layers/nft_state_layer.rs | 159 + .../layers/p2_delegated_conditions_layer.rs | 100 + .../layers/p2_delegated_singleton_layer.rs | 194 ++ .../src/layers/p2_one_of_many.rs | 106 + .../src/layers/p2_singleton.rs | 236 ++ .../src/layers/royalty_transfer_layer.rs | 105 + .../src/layers/settlement_layer.rs | 39 + .../src/layers/singleton_layer.rs | 142 + .../src/layers/standard_layer.rs | 154 + crates/chia-sdk-driver/src/lib.rs | 23 + crates/chia-sdk-driver/src/merkle_tree.rs | 171 ++ crates/chia-sdk-driver/src/primitives.rs | 17 + crates/chia-sdk-driver/src/primitives/cat.rs | 543 ++++ .../src/primitives/cat/cat_spend.rs | 28 + .../src/primitives/cat/single_cat_spend.rs | 29 + .../src/primitives/datalayer.rs | 6 + .../src/primitives/datalayer/datastore.rs | 2196 ++++++++++++++ .../primitives/datalayer/datastore_info.rs | 287 ++ .../datalayer/datastore_launcher.rs | 198 ++ crates/chia-sdk-driver/src/primitives/did.rs | 384 +++ .../src/primitives/did/did_info.rs | 148 + .../src/primitives/did/did_launcher.rs | 82 + .../src/primitives/intermediate_launcher.rs | 97 + .../src/primitives/launcher.rs | 221 ++ crates/chia-sdk-driver/src/primitives/nft.rs | 522 ++++ .../src/primitives/nft/did_owner.rs | 29 + .../src/primitives/nft/metadata_update.rs | 19 + .../src/primitives/nft/nft_info.rs | 208 ++ .../src/primitives/nft/nft_launcher.rs | 296 ++ .../src/primitives/nft/nft_mint.rs | 48 + crates/chia-sdk-driver/src/puzzle.rs | 137 + crates/chia-sdk-driver/src/spend.rs | 14 + crates/chia-sdk-driver/src/spend_context.rs | 255 ++ .../src/spend_with_conditions.rs | 11 + crates/chia-sdk-offers/Cargo.toml | 37 + crates/chia-sdk-offers/src/compress.rs | 113 + crates/chia-sdk-offers/src/encode.rs | 25 + crates/chia-sdk-offers/src/error.rs | 49 + crates/chia-sdk-offers/src/lib.rs | 13 + crates/chia-sdk-offers/src/offer.rs | 118 + crates/chia-sdk-offers/src/offer_builder.rs | 157 + crates/chia-sdk-offers/src/parsed_offer.rs | 20 + .../test_data/compressed.offer | 1 + .../test_data/decompressed.offer | 1 + crates/chia-sdk-signer/Cargo.toml | 29 + .../chia-sdk-signer/src/agg_sig_constants.rs | 83 + crates/chia-sdk-signer/src/error.rs | 18 + crates/chia-sdk-signer/src/lib.rs | 7 + .../chia-sdk-signer/src/required_signature.rs | 261 ++ crates/chia-sdk-test/Cargo.toml | 41 + crates/chia-sdk-test/src/announcements.rs | 105 + crates/chia-sdk-test/src/error.rs | 20 + crates/chia-sdk-test/src/events.rs | 14 + crates/chia-sdk-test/src/keys.rs | 26 + crates/chia-sdk-test/src/lib.rs | 34 + crates/chia-sdk-test/src/peer_simulator.rs | 885 ++++++ .../chia-sdk-test/src/peer_simulator/error.rs | 40 + .../src/peer_simulator/peer_map.rs | 30 + .../src/peer_simulator/simulator_config.rs | 21 + .../src/peer_simulator/subscriptions.rs | 54 + .../src/peer_simulator/ws_connection.rs | 536 ++++ crates/chia-sdk-test/src/simulator.rs | 278 ++ crates/chia-sdk-test/src/transaction.rs | 66 + crates/chia-sdk-types/Cargo.toml | 29 + crates/chia-sdk-types/src/condition.rs | 209 ++ .../chia-sdk-types/src/condition/agg_sig.rs | 105 + crates/chia-sdk-types/src/conditions.rs | 61 + crates/chia-sdk-types/src/constants.rs | 98 + crates/chia-sdk-types/src/lib.rs | 9 + crates/chia-sdk-types/src/run_puzzle.rs | 19 + examples/address_conversion.rs | 23 + examples/cat_spends.rs | 42 + examples/custom_p2_puzzle.rs | 145 + examples/puzzles/custom_p2_puzzle.clsp | 17 + examples/puzzles/custom_p2_puzzle.clsp.hex | 4 + examples/spend_simulator.rs | 40 + napi/.gitignore | 2 + napi/.npmignore | 13 + napi/Cargo.toml | 31 + napi/__test__/index.spec.ts | 283 ++ napi/build.rs | 6 + napi/index.d.ts | 168 + napi/index.js | 326 ++ napi/npm/darwin-arm64/README.md | 3 + napi/npm/darwin-arm64/package.json | 22 + napi/npm/darwin-universal/README.md | 3 + napi/npm/darwin-universal/package.json | 19 + napi/npm/darwin-x64/README.md | 3 + napi/npm/darwin-x64/package.json | 22 + napi/npm/linux-x64-gnu/README.md | 3 + napi/npm/linux-x64-gnu/package.json | 25 + napi/npm/win32-x64-msvc/README.md | 3 + napi/npm/win32-x64-msvc/package.json | 22 + napi/package.json | 53 + napi/pnpm-lock.yaml | 1457 +++++++++ napi/scripts/update-declarations.js | 22 + napi/src/clvm.rs | 559 ++++ napi/src/clvm_value.rs | 126 + napi/src/coin.rs | 36 + napi/src/coin_spend.rs | 40 + napi/src/lib.rs | 28 + napi/src/lineage_proof.rs | 45 + napi/src/nft.rs | 141 + napi/src/program.rs | 183 ++ napi/src/simulator.rs | 73 + napi/src/traits.rs | 235 ++ napi/src/utils.rs | 39 + napi/tsconfig.json | 9 + rust-toolchain.toml | 4 + src/address.rs | 170 ++ src/coin_selection.rs | 241 ++ src/lib.rs | 13 + 147 files changed, 21656 insertions(+), 4 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 .github/workflows/napi.yml create mode 100644 .github/workflows/rust.yml create mode 100644 .prettierrc create mode 100644 .rustfmt.toml create mode 100644 LICENSE create mode 100644 crates/chia-sdk-client/Cargo.toml create mode 100644 crates/chia-sdk-client/src/client.rs create mode 100644 crates/chia-sdk-client/src/connect.rs create mode 100644 crates/chia-sdk-client/src/error.rs create mode 100644 crates/chia-sdk-client/src/lib.rs create mode 100644 crates/chia-sdk-client/src/network.rs create mode 100644 crates/chia-sdk-client/src/peer.rs create mode 100644 crates/chia-sdk-client/src/request_map.rs create mode 100644 crates/chia-sdk-client/src/tls.rs create mode 100644 crates/chia-sdk-derive/Cargo.toml create mode 100644 crates/chia-sdk-derive/src/impl_conditions.rs create mode 100644 crates/chia-sdk-derive/src/lib.rs create mode 100644 crates/chia-sdk-driver/Cargo.toml create mode 100644 crates/chia-sdk-driver/docs.md create mode 100644 crates/chia-sdk-driver/src/driver_error.rs create mode 100644 crates/chia-sdk-driver/src/hashed_ptr.rs create mode 100644 crates/chia-sdk-driver/src/layer.rs create mode 100644 crates/chia-sdk-driver/src/layers.rs create mode 100644 crates/chia-sdk-driver/src/layers/cat_layer.rs create mode 100644 crates/chia-sdk-driver/src/layers/datalayer.rs create mode 100644 crates/chia-sdk-driver/src/layers/datalayer/delegation_layer.rs create mode 100644 crates/chia-sdk-driver/src/layers/datalayer/oracle_layer.rs create mode 100644 crates/chia-sdk-driver/src/layers/datalayer/writer_layer.rs create mode 100644 crates/chia-sdk-driver/src/layers/did_layer.rs create mode 100644 crates/chia-sdk-driver/src/layers/nft_ownership_layer.rs create mode 100644 crates/chia-sdk-driver/src/layers/nft_state_layer.rs create mode 100644 crates/chia-sdk-driver/src/layers/p2_delegated_conditions_layer.rs create mode 100644 crates/chia-sdk-driver/src/layers/p2_delegated_singleton_layer.rs create mode 100644 crates/chia-sdk-driver/src/layers/p2_one_of_many.rs create mode 100644 crates/chia-sdk-driver/src/layers/p2_singleton.rs create mode 100644 crates/chia-sdk-driver/src/layers/royalty_transfer_layer.rs create mode 100644 crates/chia-sdk-driver/src/layers/settlement_layer.rs create mode 100644 crates/chia-sdk-driver/src/layers/singleton_layer.rs create mode 100644 crates/chia-sdk-driver/src/layers/standard_layer.rs create mode 100644 crates/chia-sdk-driver/src/lib.rs create mode 100644 crates/chia-sdk-driver/src/merkle_tree.rs create mode 100644 crates/chia-sdk-driver/src/primitives.rs create mode 100644 crates/chia-sdk-driver/src/primitives/cat.rs create mode 100644 crates/chia-sdk-driver/src/primitives/cat/cat_spend.rs create mode 100644 crates/chia-sdk-driver/src/primitives/cat/single_cat_spend.rs create mode 100644 crates/chia-sdk-driver/src/primitives/datalayer.rs create mode 100644 crates/chia-sdk-driver/src/primitives/datalayer/datastore.rs create mode 100644 crates/chia-sdk-driver/src/primitives/datalayer/datastore_info.rs create mode 100644 crates/chia-sdk-driver/src/primitives/datalayer/datastore_launcher.rs create mode 100644 crates/chia-sdk-driver/src/primitives/did.rs create mode 100644 crates/chia-sdk-driver/src/primitives/did/did_info.rs create mode 100644 crates/chia-sdk-driver/src/primitives/did/did_launcher.rs create mode 100644 crates/chia-sdk-driver/src/primitives/intermediate_launcher.rs create mode 100644 crates/chia-sdk-driver/src/primitives/launcher.rs create mode 100644 crates/chia-sdk-driver/src/primitives/nft.rs create mode 100644 crates/chia-sdk-driver/src/primitives/nft/did_owner.rs create mode 100644 crates/chia-sdk-driver/src/primitives/nft/metadata_update.rs create mode 100644 crates/chia-sdk-driver/src/primitives/nft/nft_info.rs create mode 100644 crates/chia-sdk-driver/src/primitives/nft/nft_launcher.rs create mode 100644 crates/chia-sdk-driver/src/primitives/nft/nft_mint.rs create mode 100644 crates/chia-sdk-driver/src/puzzle.rs create mode 100644 crates/chia-sdk-driver/src/spend.rs create mode 100644 crates/chia-sdk-driver/src/spend_context.rs create mode 100644 crates/chia-sdk-driver/src/spend_with_conditions.rs create mode 100644 crates/chia-sdk-offers/Cargo.toml create mode 100644 crates/chia-sdk-offers/src/compress.rs create mode 100644 crates/chia-sdk-offers/src/encode.rs create mode 100644 crates/chia-sdk-offers/src/error.rs create mode 100644 crates/chia-sdk-offers/src/lib.rs create mode 100644 crates/chia-sdk-offers/src/offer.rs create mode 100644 crates/chia-sdk-offers/src/offer_builder.rs create mode 100644 crates/chia-sdk-offers/src/parsed_offer.rs create mode 100644 crates/chia-sdk-offers/test_data/compressed.offer create mode 100644 crates/chia-sdk-offers/test_data/decompressed.offer create mode 100644 crates/chia-sdk-signer/Cargo.toml create mode 100644 crates/chia-sdk-signer/src/agg_sig_constants.rs create mode 100644 crates/chia-sdk-signer/src/error.rs create mode 100644 crates/chia-sdk-signer/src/lib.rs create mode 100644 crates/chia-sdk-signer/src/required_signature.rs create mode 100644 crates/chia-sdk-test/Cargo.toml create mode 100644 crates/chia-sdk-test/src/announcements.rs create mode 100644 crates/chia-sdk-test/src/error.rs create mode 100644 crates/chia-sdk-test/src/events.rs create mode 100644 crates/chia-sdk-test/src/keys.rs create mode 100644 crates/chia-sdk-test/src/lib.rs create mode 100644 crates/chia-sdk-test/src/peer_simulator.rs create mode 100644 crates/chia-sdk-test/src/peer_simulator/error.rs create mode 100644 crates/chia-sdk-test/src/peer_simulator/peer_map.rs create mode 100644 crates/chia-sdk-test/src/peer_simulator/simulator_config.rs create mode 100644 crates/chia-sdk-test/src/peer_simulator/subscriptions.rs create mode 100644 crates/chia-sdk-test/src/peer_simulator/ws_connection.rs create mode 100644 crates/chia-sdk-test/src/simulator.rs create mode 100644 crates/chia-sdk-test/src/transaction.rs create mode 100644 crates/chia-sdk-types/Cargo.toml create mode 100644 crates/chia-sdk-types/src/condition.rs create mode 100644 crates/chia-sdk-types/src/condition/agg_sig.rs create mode 100644 crates/chia-sdk-types/src/conditions.rs create mode 100644 crates/chia-sdk-types/src/constants.rs create mode 100644 crates/chia-sdk-types/src/lib.rs create mode 100644 crates/chia-sdk-types/src/run_puzzle.rs create mode 100644 examples/address_conversion.rs create mode 100644 examples/cat_spends.rs create mode 100644 examples/custom_p2_puzzle.rs create mode 100644 examples/puzzles/custom_p2_puzzle.clsp create mode 100644 examples/puzzles/custom_p2_puzzle.clsp.hex create mode 100644 examples/spend_simulator.rs create mode 100644 napi/.gitignore create mode 100644 napi/.npmignore create mode 100644 napi/Cargo.toml create mode 100644 napi/__test__/index.spec.ts create mode 100644 napi/build.rs create mode 100644 napi/index.d.ts create mode 100644 napi/index.js create mode 100644 napi/npm/darwin-arm64/README.md create mode 100644 napi/npm/darwin-arm64/package.json create mode 100644 napi/npm/darwin-universal/README.md create mode 100644 napi/npm/darwin-universal/package.json create mode 100644 napi/npm/darwin-x64/README.md create mode 100644 napi/npm/darwin-x64/package.json create mode 100644 napi/npm/linux-x64-gnu/README.md create mode 100644 napi/npm/linux-x64-gnu/package.json create mode 100644 napi/npm/win32-x64-msvc/README.md create mode 100644 napi/npm/win32-x64-msvc/package.json create mode 100644 napi/package.json create mode 100644 napi/pnpm-lock.yaml create mode 100644 napi/scripts/update-declarations.js create mode 100644 napi/src/clvm.rs create mode 100644 napi/src/clvm_value.rs create mode 100644 napi/src/coin.rs create mode 100644 napi/src/coin_spend.rs create mode 100644 napi/src/lib.rs create mode 100644 napi/src/lineage_proof.rs create mode 100644 napi/src/nft.rs create mode 100644 napi/src/program.rs create mode 100644 napi/src/simulator.rs create mode 100644 napi/src/traits.rs create mode 100644 napi/src/utils.rs create mode 100644 napi/tsconfig.json create mode 100644 rust-toolchain.toml create mode 100644 src/address.rs create mode 100644 src/coin_selection.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..ac2b23f8 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/.github/workflows/napi.yml b/.github/workflows/napi.yml new file mode 100644 index 00000000..64c21bf9 --- /dev/null +++ b/.github/workflows/napi.yml @@ -0,0 +1,271 @@ +name: Node.js +on: + push: + branches: + - main + tags: + - "**" + + pull_request: + branches: + - "**" + +env: + DEBUG: napi:* + APP_NAME: chia-wallet-sdk + MACOSX_DEPLOYMENT_TARGET: "10.13" + +permissions: + contents: write + id-token: write + +jobs: + build: + defaults: + run: + working-directory: ./napi + strategy: + fail-fast: false + matrix: + settings: + - host: macos-latest + target: x86_64-apple-darwin + build: pnpm build:macos-x64 + - host: windows-latest + build: pnpm build:windows-x64 + target: x86_64-pc-windows-msvc + - host: ubuntu-latest + target: x86_64-unknown-linux-gnu + docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian + build: pnpm build:linux-x64 + - host: macos-latest + target: aarch64-apple-darwin + build: pnpm build:macos-arm64 + name: stable - ${{ matrix.settings.target }} - node@20 + runs-on: ${{ matrix.settings.host }} + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup node + uses: actions/setup-node@v4 + if: ${{ !matrix.settings.docker }} + with: + node-version: 20 + cache: pnpm + cache-dependency-path: napi/pnpm-lock.yaml + + - name: Install + uses: dtolnay/rust-toolchain@stable + if: ${{ !matrix.settings.docker }} + with: + toolchain: stable + targets: ${{ matrix.settings.target }} + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + .cargo-cache + target/ + key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }} + + - uses: goto-bus-stop/setup-zig@v2 + if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' || matrix.settings.target == 'armv7-unknown-linux-musleabihf' }} + with: + version: 0.13.0 + + - name: Setup toolchain + run: ${{ matrix.settings.setup }} + if: ${{ matrix.settings.setup }} + shell: bash + + - name: Setup node x86 + if: matrix.settings.target == 'i686-pc-windows-msvc' + run: pnpm config set supportedArchitectures.cpu "ia32" + shell: bash + + - name: Install dependencies + run: pnpm install + + - name: Setup node x86 + uses: actions/setup-node@v4 + if: matrix.settings.target == 'i686-pc-windows-msvc' + with: + node-version: 20 + cache: pnpm + cache-dependency-path: napi/pnpm-lock.yaml + architecture: x86 + + - name: Build in docker + uses: addnab/docker-run-action@v3 + if: ${{ matrix.settings.docker }} + with: + image: ${{ matrix.settings.docker }} + options: "--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build/napi" + run: ${{ matrix.settings.build }} + + - name: Build + run: ${{ matrix.settings.build }} + if: ${{ !matrix.settings.docker }} + shell: bash + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: bindings-${{ matrix.settings.target }} + path: napi/${{ env.APP_NAME }}.*.node + if-no-files-found: error + + test-bindings: + name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} + needs: + - build + defaults: + run: + working-directory: ./napi + strategy: + fail-fast: false + matrix: + settings: + - host: macos-latest + target: x86_64-apple-darwin + - host: windows-latest + target: x86_64-pc-windows-msvc + - host: ubuntu-latest + target: x86_64-unknown-linux-gnu + node: + - "18" + - "20" + runs-on: ${{ matrix.settings.host }} + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: pnpm + cache-dependency-path: napi/pnpm-lock.yaml + architecture: x64 + + - name: Install dependencies + run: pnpm install + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindings-${{ matrix.settings.target }} + path: napi + + - name: List packages + run: ls -R . + shell: bash + + - name: Test bindings + run: pnpm test + + universal-macOS: + name: Build universal macOS binary + needs: + - build + defaults: + run: + working-directory: ./napi + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + cache-dependency-path: napi/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install + + - name: Download macOS x64 artifact + uses: actions/download-artifact@v4 + with: + name: bindings-x86_64-apple-darwin + path: napi/artifacts + + - name: Download macOS arm64 artifact + uses: actions/download-artifact@v4 + with: + name: bindings-aarch64-apple-darwin + path: napi/artifacts + + - name: Combine binaries + run: pnpm universal + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: bindings-universal-apple-darwin + path: napi/${{ env.APP_NAME }}.*.node + if-no-files-found: error + + publish: + name: Publish + runs-on: ubuntu-latest + needs: + - test-bindings + - universal-macOS + defaults: + run: + working-directory: ./napi + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + cache-dependency-path: napi/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: napi/artifacts + + - name: Move artifacts + run: pnpm artifacts + + - name: List packages + run: ls -R ./npm + shell: bash + + - name: Publish + if: startsWith(github.event.ref, 'refs/tags') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + npm config set provenance true + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc + npm publish --access public diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 00000000..cf8d1302 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,59 @@ +name: Rust +on: + push: + branches: + - main + tags: + - "**" + + pull_request: + branches: + - "**" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Cargo binstall + run: curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + + - name: Instal cargo-workspaces + run: cargo binstall cargo-workspaces --locked -y + + - name: Install cargo-tarpaulin + run: cargo binstall cargo-tarpaulin --locked -y + + - name: Run tests + run: cargo tarpaulin --release --workspace --exclude chia-wallet-sdk-napi --all-features --out xml + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Cleanup coverage reports + run: rm -f cobertura.xml + + - name: Clippy + run: cargo clippy --workspace --all-features --all-targets + + - name: Unused dependencies + run: | + cargo binstall cargo-machete --locked -y + cargo machete + + - name: Fmt + run: cargo fmt --all -- --files-with-diff --check + + - name: Publish + if: startsWith(github.event.ref, 'refs/tags') + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.cargo_registry_token }} + run: cargo ws publish --publish-as-is --allow-dirty diff --git a/.gitignore b/.gitignore index ea8c4bf7..67555b51 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ +/.vscode /target +*.DS_Store +cobertura.xml diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..75fa1341 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "tabWidth": 2 +} diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 00000000..3a26366d --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +edition = "2021" diff --git a/Cargo.lock b/Cargo.lock index c31fb5a0..01a6e39c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,5 +3,2699 @@ version = 3 [[package]] -name = "wallet-sdk" +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "aws-lc-rs" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae74d9bd0a7530e8afd1770739ad34b36838829d6ad61818f9230f683f5ad77" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0e249228c6ad2d240c2dc94b714d711629d52bad946075d8e9b2f5391f0703" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bip39" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" +dependencies = [ + "bitcoin_hashes", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blst" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4378725facc195f1a538864863f6de233b500a8862747e7f165078a419d5e874" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cc" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68064e60dbf1f17005c2fde4d07c16d8baa506fd7ffed8ccab702d93617975c7" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chia" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b730bab32685f98520fff23181dd17965ef62eb37b2cf4ec749614d2e5a10be4" +dependencies = [ + "chia-bls 0.13.0", + "chia-client", + "chia-consensus", + "chia-protocol", + "chia-puzzles", + "chia-ssl", + "chia-traits 0.11.0", + "clvm-traits", + "clvm-utils", + "clvmr", +] + +[[package]] +name = "chia-bls" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "725b43b268cb81f014559eed11a74d0e5b9dd55282cae272ff509796099ab0b9" +dependencies = [ + "anyhow", + "blst", + "chia-traits 0.10.0", + "hex", + "hkdf", + "lru", + "sha2", + "thiserror", +] + +[[package]] +name = "chia-bls" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "281de3426c8abee22bb0aac614ced4ca143f148071dfe10c11f78b5da918ed0b" +dependencies = [ + "anyhow", + "blst", + "chia-traits 0.11.0", + "clvmr", + "hex", + "hkdf", + "lru", + "sha2", + "thiserror", +] + +[[package]] +name = "chia-client" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f5234e4b1d3c34a00a2764f7779279ef3c362c0610be8fda708b1903065f6c6" +dependencies = [ + "chia-protocol", + "chia-traits 0.11.0", + "futures-util", + "thiserror", + "tokio", + "tokio-tungstenite", + "tungstenite", +] + +[[package]] +name = "chia-consensus" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc7b4621512c9d00d9199b42431eface49efbbf9b0f7d576c771a359c5684ec" +dependencies = [ + "chia-bls 0.13.0", + "chia-protocol", + "chia-puzzles", + "chia-traits 0.11.0", + "chia_streamable_macro 0.11.0", + "clvm-derive", + "clvm-traits", + "clvm-utils", + "clvmr", + "hex", + "hex-literal", + "thiserror", +] + +[[package]] +name = "chia-protocol" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88e5dd3701f0655b50d14b9a85012d29bc35398ec017e54721c8ac13255ad65a" +dependencies = [ + "chia-bls 0.13.0", + "chia-traits 0.11.0", + "chia_streamable_macro 0.11.0", + "clvm-traits", + "clvm-utils", + "clvmr", + "hex", +] + +[[package]] +name = "chia-puzzles" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3237073f8bd068bc0ea78399725bb09ac198b25664baf9e6a93a3858108a3ff3" +dependencies = [ + "chia-bls 0.13.0", + "chia-protocol", + "clvm-traits", + "clvm-utils", + "clvmr", + "hex-literal", + "num-bigint", +] + +[[package]] +name = "chia-sdk-client" +version = "0.16.0" +dependencies = [ + "chia-protocol", + "chia-sdk-types", + "chia-ssl", + "chia-traits 0.11.0", + "futures-util", + "native-tls", + "rustls", + "rustls-pemfile", + "thiserror", + "tokio", + "tokio-tungstenite", + "tracing", + "tungstenite", +] + +[[package]] +name = "chia-sdk-derive" +version = "0.16.0" +dependencies = [ + "convert_case", + "quote", + "syn", +] + +[[package]] +name = "chia-sdk-driver" +version = "0.16.0" +dependencies = [ + "anyhow", + "chia-bls 0.13.0", + "chia-consensus", + "chia-protocol", + "chia-puzzles", + "chia-sdk-test", + "chia-sdk-types", + "clvm-traits", + "clvm-utils", + "clvmr", + "hex", + "hex-literal", + "num-bigint", + "rstest", + "thiserror", +] + +[[package]] +name = "chia-sdk-offers" +version = "0.16.0" +dependencies = [ + "anyhow", + "bech32", + "chia-bls 0.13.0", + "chia-protocol", + "chia-puzzles", + "chia-sdk-driver", + "chia-sdk-test", + "chia-sdk-types", + "chia-traits 0.11.0", + "clvm-traits", + "clvm-utils", + "clvmr", + "flate2", + "hex", + "hex-literal", + "indexmap", + "once_cell", + "thiserror", +] + +[[package]] +name = "chia-sdk-signer" +version = "0.16.0" +dependencies = [ + "chia-bls 0.13.0", + "chia-consensus", + "chia-protocol", + "chia-puzzles", + "chia-sdk-types", + "clvm-traits", + "clvmr", + "hex", + "hex-literal", + "thiserror", +] + +[[package]] +name = "chia-sdk-test" +version = "0.16.0" +dependencies = [ + "anyhow", + "bip39", + "chia-bls 0.13.0", + "chia-consensus", + "chia-protocol", + "chia-puzzles", + "chia-sdk-client", + "chia-sdk-signer", + "chia-sdk-types", + "chia-traits 0.11.0", + "clvm-traits", + "clvm-utils", + "clvmr", + "fastrand", + "futures-channel", + "futures-util", + "indexmap", + "itertools 0.13.0", + "log", + "rand", + "rand_chacha", + "thiserror", + "tokio", + "tokio-tungstenite", +] + +[[package]] +name = "chia-sdk-types" +version = "0.16.0" +dependencies = [ + "anyhow", + "chia-bls 0.13.0", + "chia-consensus", + "chia-protocol", + "chia-sdk-derive", + "clvm-traits", + "clvmr", + "hex", + "hex-literal", + "once_cell", +] + +[[package]] +name = "chia-ssl" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c4a23cb186c6d5d7c6cc993a0d11725bd8a74a59cb605ace4523bc40fcd010" +dependencies = [ + "lazy_static", + "rand", + "rcgen", + "rsa", + "thiserror", + "time", +] + +[[package]] +name = "chia-traits" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb786114e5c748fe0af3ba1e95840fa1910b28f7300c05184506045aff60bb6" +dependencies = [ + "chia_streamable_macro 0.10.0", + "sha2", + "thiserror", +] + +[[package]] +name = "chia-traits" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d677fcdf166a855ddc1f5e6a0e3dfa0bd7e823d24a82198e79400dbacb1a503" +dependencies = [ + "chia_streamable_macro 0.11.0", + "clvmr", + "thiserror", +] + +[[package]] +name = "chia-wallet-sdk" +version = "0.16.0" +dependencies = [ + "anyhow", + "bech32", + "chia-bls 0.13.0", + "chia-protocol", + "chia-puzzles", + "chia-sdk-client", + "chia-sdk-driver", + "chia-sdk-offers", + "chia-sdk-signer", + "chia-sdk-test", + "chia-sdk-types", + "clvm-traits", + "clvm-utils", + "clvmr", + "hex", + "hex-literal", + "indexmap", + "rand", + "rand_chacha", + "thiserror", +] + +[[package]] +name = "chia-wallet-sdk-napi" +version = "0.0.0" +dependencies = [ + "chia", + "chia-wallet-sdk", + "clvmr", + "hex", + "napi", + "napi-build", + "napi-derive", + "num-bigint", +] + +[[package]] +name = "chia_streamable_macro" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43975e01fb4293021af4950366a6f80c45d7301d499622359c7533bd7af19592" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "chia_streamable_macro" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e4fde91c96e99445cca016635bc4830a302e8366c238d87b8a4213c16abb270" +dependencies = [ + "clvmr", + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clvm-derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67bf4a5d8f6991d385c78c2371ea603de5c2dbf9aebf4e6e801d0dece0e8485d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clvm-traits" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e46a5ec963484ea2cb2a450691b80cf7db1d3fca4192120a2606026a261e73e" +dependencies = [ + "chia-bls 0.13.0", + "clvm-derive", + "clvmr", + "num-bigint", + "thiserror", +] + +[[package]] +name = "clvm-utils" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dfb6b76affa69bdf036fef6da3dbefb4feedc2db37cc6341b02c935c94c0d0a" +dependencies = [ + "clvm-traits", + "clvmr", + "hex", +] + +[[package]] +name = "clvmr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903233f3bc9392b44a589f0cea46a53a67fedc3afb4b1303f636977074a505c5" +dependencies = [ + "chia-bls 0.10.0", + "hex-literal", + "k256", + "lazy_static", + "num-bigint", + "num-integer", + "num-traits", + "p256", + "sha2", +] + +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "flate2" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "k256" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.156" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libz-sys" +version = "1.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" +dependencies = [ + "cc", + "cmake", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + +[[package]] +name = "napi" +version = "2.16.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53575dfa17f208dd1ce3a2da2da4659aae393b256a472f2738a8586a6c4107fd" +dependencies = [ + "bitflags", + "ctor", + "napi-derive", + "napi-sys", + "once_cell", +] + +[[package]] +name = "napi-build" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" + +[[package]] +name = "napi-derive" +version = "2.16.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17435f7a00bfdab20b0c27d9c56f58f6499e418252253081bfff448099da31d1" +dependencies = [ + "cfg-if", + "convert_case", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "967c485e00f0bf3b1bdbe510a38a4606919cf1d34d9a37ad41f25a81aa077abe" +dependencies = [ + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "regex", + "semver", + "syn", +] + +[[package]] +name = "napi-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +dependencies = [ + "libloading", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c958dd45046245b9c3c2547369bb634eb461670b2e7e0de552905801a648d1d" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rcgen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54077e1872c46788540de1ea3d7f4ccb1983d12f9aa909b234468676c1a36779" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rstest" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate 3.1.0", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "aws-lc-rs", + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" + +[[package]] +name = "rustls-webpki" +version = "0.102.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "native-tls", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tungstenite", + "webpki-roots", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "webpki-roots" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 2266e941..14637277 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,141 @@ [package] -name = "wallet-sdk" -version = "0.1.0" +name = "chia-wallet-sdk" +version = "0.16.0" edition = "2021" +license = "Apache-2.0" +description = "An unofficial SDK for building Chia wallets." +authors = ["Brandon Haggstrom "] +homepage = "https://github.com/Rigidity/chia-wallet-sdk" +repository = "https://github.com/Rigidity/chia-wallet-sdk" +readme = { workspace = true } +keywords = { workspace = true } +categories = { workspace = true } + +[package.metadata.docs.rs] +all-features = true + +[workspace] +resolver = "2" +members = ["crates/*", "napi"] + +[workspace.package] +readme = "README.md" +keywords = ["chia", "wallet", "blockchain", "crypto"] +categories = ["cryptography::cryptocurrencies", "development-tools"] + +[workspace.lints.rust] +rust_2018_idioms = { level = "deny", priority = -1 } +rust_2021_compatibility = { level = "deny", priority = -1 } +future_incompatible = { level = "deny", priority = -1 } +nonstandard_style = { level = "deny", priority = -1 } +unsafe_code = "deny" +non_ascii_idents = "deny" +unused_extern_crates = "deny" +trivial_casts = "deny" +trivial_numeric_casts = "deny" +unreachable_pub = "warn" +unreachable_code = "warn" +unreachable_patterns = "deny" +dead_code = "deny" +deprecated = "deny" +deprecated_in_future = "deny" +missing_debug_implementations = "warn" +missing_copy_implementations = "warn" + +[workspace.lints.rustdoc] +all = { level = "deny", priority = -1 } +missing_crate_level_docs = "allow" + +[workspace.lints.clippy] +all = { level = "deny", priority = -1 } +cargo = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +too_many_lines = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +module_name_repetitions = "allow" +multiple_crate_versions = "allow" +must_use_candidate = "allow" + +[features] +chip-0035 = ["chia-sdk-driver/chip-0035"] +native-tls = ["chia-sdk-client/native-tls"] +rustls = ["chia-sdk-client/rustls"] [dependencies] +thiserror = { workspace = true } +chia-protocol = { workspace = true } +hex = { workspace = true } +bech32 = { workspace = true } +rand = { workspace = true } +rand_chacha = { workspace = true } +indexmap = { workspace = true } +chia-sdk-client = { workspace = true } +chia-sdk-driver = { workspace = true } +chia-sdk-offers = { workspace = true } +chia-sdk-signer = { workspace = true } +chia-sdk-test = { workspace = true } +chia-sdk-types = { workspace = true } + +[dev-dependencies] +anyhow = { workspace = true } +hex-literal = { workspace = true } +chia-puzzles = { workspace = true } +chia-bls = { workspace = true } +clvm-utils = { workspace = true } +clvm-traits = { workspace = true, features = ["derive"] } +clvmr = { workspace = true } + +[workspace.dependencies] +chia-wallet-sdk = { version = "0.16.0", path = "." } +chia-sdk-client = { version = "0.16.0", path = "./crates/chia-sdk-client" } +chia-sdk-derive = { version = "0.16.0", path = "./crates/chia-sdk-derive" } +chia-sdk-driver = { version = "0.16.0", path = "./crates/chia-sdk-driver" } +chia-sdk-offers = { version = "0.16.0", path = "./crates/chia-sdk-offers" } +chia-sdk-signer = { version = "0.16.0", path = "./crates/chia-sdk-signer" } +chia-sdk-test = { version = "0.16.0", path = "./crates/chia-sdk-test" } +chia-sdk-types = { version = "0.16.0", path = "./crates/chia-sdk-types" } +chia = "0.13.0" +chia-ssl = "0.11.0" +chia-protocol = "0.13.0" +chia-consensus = "0.13.0" +chia-traits = "0.11.0" +chia-bls = "0.13.0" +chia-puzzles = "0.13.0" +clvm-traits = "0.13.0" +clvm-utils = "0.13.0" +clvmr = "0.8.0" +thiserror = "1.0.61" +hex = "0.4.3" +bech32 = "0.9.1" +rand = "0.8.5" +rand_chacha = "0.3.1" +hex-literal = "0.4.1" +indexmap = "2.2.6" +bip39 = "2.0.0" +futures-util = "0.3.30" +futures-channel = "0.3.30" +anyhow = "1.0.86" +tokio = "1.37.0" +itertools = "0.13.0" +tokio-tungstenite = "0.21.0" +tungstenite = "0.21.0" +native-tls = "0.2.11" +rustls = "0.22.0" +rustls-pemfile = "2.1.3" +log = "0.4.21" +flate2 = "1.0.30" +once_cell = "1.19.0" +num-bigint = "0.4.6" +rstest = "0.22.0" +tracing = "0.1.40" +syn = "2.0.76" +quote = "1.0.37" +convert_case = "0.6.0" +fastrand = "2.1.1" +napi-derive = "2.12.2" +napi = { version = "2.12.2", default-features = false } + +[profile.release] +lto = true +strip = "symbols" diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..e4a1a8b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2024] [Brandon Haggstrom] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 9fd3c702..54be72f2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,39 @@ # Chia Wallet SDK -An unofficial SDK for developing wallet applications for the Chia blockchain. +[![crate](https://img.shields.io/crates/v/chia-wallet-sdk.svg)](https://crates.io/crates/chia-wallet-sdk) +[![documentation](https://docs.rs/chia-wallet-sdk/badge.svg)](https://docs.rs/chia-wallet-sdk) +[![minimum rustc 1.75](https://img.shields.io/badge/rustc-1.75+-red.svg)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) +[![codecov](https://codecov.io/github/Rigidity/chia-wallet-sdk/graph/badge.svg?token=M2MPMFGCCA)](https://codecov.io/github/Rigidity/chia-wallet-sdk) + +This is an unofficial wallet SDK for the [Chia blockchain](https://chia.net), enabling the development of high-performance wallet applications built with Chia's [light wallet protocol](https://docs.chia.net/wallet-protocol). + +![image](https://github.com/Rigidity/chia-wallet-sdk/assets/35380458/06dd1f97-1f0e-4f6d-98cb-cbcb2b47ee70) + +## Why do you need an SDK? + +If you intend on writing an application that uses the Chia blockchain, be it a dApp, a wallet, or even just tooling on top of it, you will most likely need some code to interact with a Chia wallet. The worst case scenario is that you need to write an entire wallet and all of its driver code from scratch every time. This is very challenging to do, takes a lot of time, and can be error prone if anything is done wrong. + +To build driver code, you need libraries like [chia-bls](https://docs.rs/chia-bls) and [clvmr](https://docs.rs/clvm), for interacting with Chia's native BLS signatures and CLVM runtime. You compose puzzles by currying them, and spend them by constructing solutions. Even with libraries in place to do this (assuming they are tested properly), it can be very tedious and hard to get right. That's what this wallet sdk is aiming to solve. + +It's essentially a higher level wrapper over the core primitives that the Chia blockchain provides, and aims to make various things in the lifecycle of wallet development simpler such as state management and signing. + +## chia_rs and clvm_rs + +This SDK is built on top of the primitives developed in the [chia_rs](https://github.com/Chia-Network/chia_rs) and [clvm_rs](https://github.com/Chia-Network/clvm_rs) libraries. I help maintain chia_rs to add new core functionality necessary for wallet development as needed. And clvm_rs is a great implementation of the CLVM runtime, especially when combined with the [clvm-traits](https://docs.rs/clvm-traits/latest/clvm_traits/) helper library for translating Rust types to CLVM and vice versa. + +## Supported primitives + +Currently, the following Chia primitives are supported: + +- [Standard Transactions](https://chialisp.com/standard-transactions), either as an inner puzzle or standalone +- [CATs](https://chialisp.com/cats) (Chia Asset Tokens), with creation, parsing, and spending capabilities +- [DIDs](https://chialisp.com/dids) (Decentralized Identities), with creation, parsing, and (limited) spending capabilities +- [NFTs](https://chialisp.com/nfts) (Non-Fungible Tokens), with minting and (limited) spending capabilities + +Additionally, the wallet sdk is designed to be modular, so you can extend it with your own primitives and driver code if needed! Contributions are welcome for adding things to the wallet sdk itself as well. + +## Credits + +Special thanks to [SumSet Tech, LLC](https://sumset.tech) for sponsoring the initial development of various parts of the wallet sdk. + +Banner image produced by [Midjourney](https://www.midjourney.com). diff --git a/crates/chia-sdk-client/Cargo.toml b/crates/chia-sdk-client/Cargo.toml new file mode 100644 index 00000000..148d0ef0 --- /dev/null +++ b/crates/chia-sdk-client/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "chia-sdk-client" +version = "0.16.0" +edition = "2021" +license = "Apache-2.0" +description = "Utilities for connecting to Chia full node peers via the light wallet protocol." +authors = ["Brandon Haggstrom "] +homepage = "https://github.com/Rigidity/chia-wallet-sdk" +repository = "https://github.com/Rigidity/chia-wallet-sdk" +readme = { workspace = true } +keywords = { workspace = true } +categories = { workspace = true } + +[lints] +workspace = true + +[features] +native-tls = ["dep:native-tls", "tokio-tungstenite/native-tls"] +rustls = ["dep:rustls", "dep:rustls-pemfile", "tokio-tungstenite/rustls-tls-webpki-roots"] + +[dependencies] +chia-sdk-types = { workspace = true } +chia-protocol = { workspace = true } +chia-traits = { workspace = true } +chia-ssl = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["sync", "time", "rt"] } +tungstenite = { workspace = true } +native-tls = { workspace = true, optional = true } +rustls = { workspace = true, optional = true, features = ["aws_lc_rs"] } +rustls-pemfile = { workspace = true, optional = true } +tracing = { workspace = true } +futures-util = { workspace = true } +tokio-tungstenite = { workspace = true } diff --git a/crates/chia-sdk-client/src/client.rs b/crates/chia-sdk-client/src/client.rs new file mode 100644 index 00000000..09b0e7b3 --- /dev/null +++ b/crates/chia-sdk-client/src/client.rs @@ -0,0 +1,131 @@ +use std::{ + collections::{HashMap, HashSet}, + fmt, + net::{IpAddr, SocketAddr}, + ops::Deref, + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; + +use chia_protocol::Message; +use tokio::sync::{mpsc, Mutex}; +use tokio_tungstenite::Connector; + +use crate::{connect_peer, ClientError, Network, Peer}; + +#[derive(Clone)] +pub struct Client { + network_id: String, + network: Network, + connector: Connector, + state: Arc>, +} + +#[allow(clippy::missing_fields_in_debug)] +impl fmt::Debug for Client { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Client") + .field("network_id", &self.network_id) + .field("network", &self.network) + .finish() + } +} + +impl Deref for Client { + type Target = Mutex; + + fn deref(&self) -> &Self::Target { + &self.state + } +} + +#[derive(Debug, Default, Clone)] +pub struct ClientState { + peers: HashMap, + banned_peers: HashMap, + trusted_peers: HashSet, +} + +impl Client { + pub fn new(network_id: String, network: Network, connector: Connector) -> Self { + Self { + network_id, + network, + connector, + state: Arc::new(Mutex::new(ClientState::default())), + } + } + + pub fn network_id(&self) -> &str { + &self.network_id + } + + pub fn network(&self) -> &Network { + &self.network + } + + pub async fn connect( + &self, + socket_addr: SocketAddr, + ) -> Result, ClientError> { + let (peer, receiver) = + connect_peer(self.network_id.clone(), self.connector.clone(), socket_addr).await?; + + let mut state = self.state.lock().await; + let ip_addr = peer.socket_addr().ip(); + + if state.is_banned(&ip_addr) { + return Err(ClientError::BannedPeer); + } + + state.peers.insert(peer.socket_addr().ip(), peer); + + Ok(receiver) + } +} + +impl ClientState { + pub fn peers(&self) -> impl Iterator { + self.peers.values() + } + + pub fn disconnect(&mut self, ip_addr: &IpAddr) -> bool { + self.peers.remove(ip_addr).is_some() + } + + pub fn is_banned(&self, ip_addr: &IpAddr) -> bool { + self.banned_peers.contains_key(ip_addr) + } + + pub fn is_trusted(&self, ip_addr: &IpAddr) -> bool { + self.trusted_peers.contains(ip_addr) + } + + pub fn ban(&mut self, ip_addr: IpAddr) -> bool { + if self.is_trusted(&ip_addr) { + return false; + } + + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + + self.disconnect(&ip_addr); + self.banned_peers.insert(ip_addr, timestamp).is_none() + } + + pub fn unban(&mut self, ip_addr: IpAddr) -> bool { + self.banned_peers.remove(&ip_addr).is_some() + } + + pub fn trust(&mut self, ip_addr: IpAddr) -> bool { + let result = self.trusted_peers.insert(ip_addr); + self.banned_peers.remove(&ip_addr); + result + } + + pub fn untrust(&mut self, ip_addr: IpAddr) -> bool { + self.trusted_peers.remove(&ip_addr) + } +} diff --git a/crates/chia-sdk-client/src/connect.rs b/crates/chia-sdk-client/src/connect.rs new file mode 100644 index 00000000..630265d5 --- /dev/null +++ b/crates/chia-sdk-client/src/connect.rs @@ -0,0 +1,61 @@ +use std::net::SocketAddr; + +use chia_protocol::{Handshake, Message, NodeType, ProtocolMessageTypes}; +use chia_traits::Streamable; +use tokio::sync::mpsc; +use tokio_tungstenite::Connector; +use tracing::instrument; + +use crate::{ClientError, Peer}; + +#[instrument(skip(connector))] +pub async fn connect_peer( + network_id: String, + connector: Connector, + socket_addr: SocketAddr, +) -> Result<(Peer, mpsc::Receiver), ClientError> { + let (peer, mut receiver) = Peer::connect(socket_addr, connector).await?; + + peer.send(Handshake { + network_id: network_id.clone(), + protocol_version: "0.0.37".to_string(), + software_version: "0.0.0".to_string(), + server_port: 0, + node_type: NodeType::Wallet, + capabilities: vec![ + (1, "1".to_string()), + (2, "1".to_string()), + (3, "1".to_string()), + ], + }) + .await?; + + let Some(message) = receiver.recv().await else { + return Err(ClientError::MissingHandshake); + }; + + if message.msg_type != ProtocolMessageTypes::Handshake { + return Err(ClientError::InvalidResponse( + vec![ProtocolMessageTypes::Handshake], + message.msg_type, + )); + } + + let handshake = Handshake::from_bytes(&message.data)?; + + if handshake.node_type != NodeType::FullNode { + return Err(ClientError::WrongNodeType( + NodeType::FullNode, + handshake.node_type, + )); + } + + if handshake.network_id != network_id { + return Err(ClientError::WrongNetwork( + network_id.to_string(), + handshake.network_id, + )); + } + + Ok((peer, receiver)) +} diff --git a/crates/chia-sdk-client/src/error.rs b/crates/chia-sdk-client/src/error.rs new file mode 100644 index 00000000..a778c4df --- /dev/null +++ b/crates/chia-sdk-client/src/error.rs @@ -0,0 +1,58 @@ +use chia_protocol::{NodeType, ProtocolMessageTypes}; +use thiserror::Error; +use tokio::sync::oneshot::error::RecvError; + +#[derive(Debug, Error)] +pub enum ClientError { + #[error("SSL error: {0}")] + Ssl(#[from] chia_ssl::Error), + + #[error("TLS method is not supported")] + UnsupportedTls, + + #[error("Streamable error: {0}")] + Streamable(#[from] chia_traits::Error), + + #[error("WebSocket error: {0}")] + WebSocket(#[from] tungstenite::Error), + + #[cfg(feature = "native-tls")] + #[error("Native TLS error: {0}")] + NativeTls(#[from] native_tls::Error), + + #[cfg(feature = "rustls")] + #[error("Rustls error: {0}")] + Rustls(#[from] rustls::Error), + + #[cfg(feature = "rustls")] + #[error("Missing pkcs8 private key")] + MissingPkcs8Key, + + #[cfg(feature = "rustls")] + #[error("Missing CA cert")] + MissingCa, + + #[error("Unexpected message received with type {0:?}")] + UnexpectedMessage(ProtocolMessageTypes), + + #[error("Expected response with type {0:?}, found {1:?}")] + InvalidResponse(Vec, ProtocolMessageTypes), + + #[error("Failed to receive message")] + Recv(#[from] RecvError), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Missing response during handshake")] + MissingHandshake, + + #[error("Expected node type {0:?}, but found {1:?}")] + WrongNodeType(NodeType, NodeType), + + #[error("Expected network {0}, but found {1}")] + WrongNetwork(String, String), + + #[error("The peer is banned")] + BannedPeer, +} diff --git a/crates/chia-sdk-client/src/lib.rs b/crates/chia-sdk-client/src/lib.rs new file mode 100644 index 00000000..751521a1 --- /dev/null +++ b/crates/chia-sdk-client/src/lib.rs @@ -0,0 +1,23 @@ +mod error; +mod network; +mod peer; +mod request_map; +mod tls; + +pub use error::*; +pub use network::*; +pub use peer::*; +pub use tls::*; + +#[cfg(any(feature = "native-tls", feature = "rustls"))] +mod client; +#[cfg(any(feature = "native-tls", feature = "rustls"))] +mod connect; + +#[cfg(any(feature = "native-tls", feature = "rustls"))] +pub use client::*; +#[cfg(any(feature = "native-tls", feature = "rustls"))] +pub use connect::*; + +#[cfg(any(feature = "native-tls", feature = "rustls"))] +pub use tokio_tungstenite::Connector; diff --git a/crates/chia-sdk-client/src/network.rs b/crates/chia-sdk-client/src/network.rs new file mode 100644 index 00000000..57302034 --- /dev/null +++ b/crates/chia-sdk-client/src/network.rs @@ -0,0 +1,79 @@ +use std::{net::SocketAddr, time::Duration}; + +use chia_protocol::Bytes32; +use chia_sdk_types::{MAINNET_CONSTANTS, TESTNET11_CONSTANTS}; +use futures_util::{stream::FuturesUnordered, StreamExt}; +use tracing::{info, instrument, warn}; + +use crate::ClientError; + +#[derive(Debug, Clone)] +pub struct Network { + pub default_port: u16, + pub genesis_challenge: Bytes32, + pub dns_introducers: Vec, +} + +impl Network { + pub fn default_mainnet() -> Self { + Self { + default_port: 8444, + genesis_challenge: MAINNET_CONSTANTS.genesis_challenge, + dns_introducers: vec![ + "dns-introducer.chia.net".to_string(), + "chia.ctrlaltdel.ch".to_string(), + "seeder.dexie.space".to_string(), + "chia.hoffmang.com".to_string(), + ], + } + } + + pub fn default_testnet11() -> Self { + Self { + default_port: 58444, + genesis_challenge: TESTNET11_CONSTANTS.genesis_challenge, + dns_introducers: vec!["dns-introducer-testnet11.chia.net".to_string()], + } + } + + #[instrument] + pub async fn lookup_all(&self, timeout: Duration, batch_size: usize) -> Vec { + let mut result = Vec::new(); + + for batch in self.dns_introducers.chunks(batch_size) { + let mut futures = FuturesUnordered::new(); + + for dns_introducer in batch { + futures.push(async move { + match tokio::time::timeout(timeout, self.lookup_host(dns_introducer)).await { + Ok(Ok(addrs)) => addrs, + Ok(Err(error)) => { + warn!("Failed to lookup DNS introducer {dns_introducer}: {error}"); + Vec::new() + } + Err(_timeout) => { + warn!("Timeout looking up DNS introducer {dns_introducer}"); + Vec::new() + } + } + }); + } + + while let Some(addrs) = futures.next().await { + result.extend(addrs); + } + } + + result + } + + #[instrument] + pub async fn lookup_host(&self, dns_introducer: &str) -> Result, ClientError> { + info!("Looking up DNS introducer {dns_introducer}"); + let mut result = Vec::new(); + for addr in tokio::net::lookup_host(format!("{dns_introducer}:80")).await? { + result.push(SocketAddr::new(addr.ip(), self.default_port)); + } + Ok(result) + } +} diff --git a/crates/chia-sdk-client/src/peer.rs b/crates/chia-sdk-client/src/peer.rs new file mode 100644 index 00000000..69c8bae9 --- /dev/null +++ b/crates/chia-sdk-client/src/peer.rs @@ -0,0 +1,339 @@ +use std::{net::SocketAddr, sync::Arc}; + +use chia_protocol::{ + Bytes32, ChiaProtocolMessage, CoinStateFilters, Message, PuzzleSolutionResponse, + RegisterForCoinUpdates, RegisterForPhUpdates, RejectCoinState, RejectPuzzleSolution, + RejectPuzzleState, RequestChildren, RequestCoinState, RequestPeers, RequestPuzzleSolution, + RequestPuzzleState, RequestRemoveCoinSubscriptions, RequestRemovePuzzleSubscriptions, + RequestTransaction, RespondChildren, RespondCoinState, RespondPeers, RespondPuzzleSolution, + RespondPuzzleState, RespondRemoveCoinSubscriptions, RespondRemovePuzzleSubscriptions, + RespondToCoinUpdates, RespondToPhUpdates, RespondTransaction, SendTransaction, SpendBundle, + TransactionAck, +}; +use chia_traits::Streamable; +use futures_util::{ + stream::{SplitSink, SplitStream}, + SinkExt, StreamExt, +}; +use tokio::{ + net::TcpStream, + sync::{mpsc, oneshot, Mutex}, + task::JoinHandle, +}; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; +use tracing::{debug, warn}; + +use crate::{request_map::RequestMap, ClientError}; + +#[cfg(any(feature = "native-tls", feature = "rustls"))] +use tokio_tungstenite::Connector; + +type WebSocket = WebSocketStream>; +type Sink = SplitSink; +type Stream = SplitStream; +type Response = std::result::Result; + +#[derive(Debug, Clone)] +pub struct Peer(Arc); + +#[derive(Debug)] +struct PeerInner { + sink: Mutex, + inbound_handle: JoinHandle<()>, + requests: Arc, + socket_addr: SocketAddr, +} + +impl Peer { + /// Connects to a peer using its IP address and port. + #[cfg(any(feature = "native-tls", feature = "rustls"))] + pub async fn connect( + socket_addr: SocketAddr, + connector: Connector, + ) -> Result<(Self, mpsc::Receiver), ClientError> { + Self::connect_full_uri(&format!("wss://{socket_addr}/ws"), connector).await + } + + /// Connects to a peer using its full websocket URI. + /// For example, `wss://127.0.0.1:8444/ws`. + #[cfg(any(feature = "native-tls", feature = "rustls"))] + pub async fn connect_full_uri( + uri: &str, + connector: Connector, + ) -> Result<(Self, mpsc::Receiver), ClientError> { + let (ws, _) = + tokio_tungstenite::connect_async_tls_with_config(uri, None, false, Some(connector)) + .await?; + Self::from_websocket(ws) + } + + /// Creates a peer from an existing websocket connection. + /// The connection must be secured with TLS, so that the certificate can be hashed in a peer id. + pub fn from_websocket(ws: WebSocket) -> Result<(Self, mpsc::Receiver), ClientError> { + let socket_addr = match ws.get_ref() { + #[cfg(feature = "native-tls")] + MaybeTlsStream::NativeTls(tls) => { + let tls_stream = tls.get_ref(); + let tcp_stream = tls_stream.get_ref().get_ref(); + tcp_stream.peer_addr()? + } + #[cfg(feature = "rustls")] + MaybeTlsStream::Rustls(tls) => { + let (tcp_stream, _) = tls.get_ref(); + tcp_stream.peer_addr()? + } + MaybeTlsStream::Plain(plain) => plain.peer_addr()?, + _ => return Err(ClientError::UnsupportedTls), + }; + + let (sink, stream) = ws.split(); + let (sender, receiver) = mpsc::channel(32); + + let requests = Arc::new(RequestMap::new()); + let requests_clone = requests.clone(); + + let inbound_handle = tokio::spawn(async move { + if let Err(error) = handle_inbound_messages(stream, sender, requests_clone).await { + debug!("Error handling message: {error}"); + } + }); + + let peer = Self(Arc::new(PeerInner { + sink: Mutex::new(sink), + inbound_handle, + requests, + socket_addr, + })); + + Ok((peer, receiver)) + } + + /// The IP address and port of the peer connection. + pub fn socket_addr(&self) -> SocketAddr { + self.0.socket_addr + } + + pub async fn send_transaction( + &self, + spend_bundle: SpendBundle, + ) -> Result { + self.request_infallible(SendTransaction::new(spend_bundle)) + .await + } + + pub async fn request_puzzle_state( + &self, + puzzle_hashes: Vec, + previous_height: Option, + header_hash: Bytes32, + filters: CoinStateFilters, + subscribe_when_finished: bool, + ) -> Result, ClientError> { + self.request_fallible(RequestPuzzleState::new( + puzzle_hashes, + previous_height, + header_hash, + filters, + subscribe_when_finished, + )) + .await + } + + pub async fn request_coin_state( + &self, + coin_ids: Vec, + previous_height: Option, + header_hash: Bytes32, + subscribe: bool, + ) -> Result, ClientError> { + self.request_fallible(RequestCoinState::new( + coin_ids, + previous_height, + header_hash, + subscribe, + )) + .await + } + + pub async fn register_for_ph_updates( + &self, + puzzle_hashes: Vec, + min_height: u32, + ) -> Result { + self.request_infallible(RegisterForPhUpdates::new(puzzle_hashes, min_height)) + .await + } + + pub async fn register_for_coin_updates( + &self, + coin_ids: Vec, + min_height: u32, + ) -> Result { + self.request_infallible(RegisterForCoinUpdates::new(coin_ids, min_height)) + .await + } + + pub async fn remove_puzzle_subscriptions( + &self, + puzzle_hashes: Option>, + ) -> Result { + self.request_infallible(RequestRemovePuzzleSubscriptions::new(puzzle_hashes)) + .await + } + + pub async fn remove_coin_subscriptions( + &self, + coin_ids: Option>, + ) -> Result { + self.request_infallible(RequestRemoveCoinSubscriptions::new(coin_ids)) + .await + } + + pub async fn request_transaction( + &self, + transaction_id: Bytes32, + ) -> Result { + self.request_infallible(RequestTransaction::new(transaction_id)) + .await + } + + pub async fn request_puzzle_and_solution( + &self, + coin_id: Bytes32, + height: u32, + ) -> Result, ClientError> { + match self + .request_fallible::(RequestPuzzleSolution::new( + coin_id, height, + )) + .await? + { + Ok(response) => Ok(Ok(response.response)), + Err(rejection) => Ok(Err(rejection)), + } + } + + pub async fn request_children(&self, coin_id: Bytes32) -> Result { + self.request_infallible(RequestChildren::new(coin_id)).await + } + + pub async fn request_peers(&self) -> Result { + self.request_infallible(RequestPeers::new()).await + } + + /// Sends a message to the peer, but does not expect any response. + pub async fn send(&self, body: T) -> Result<(), ClientError> + where + T: Streamable + ChiaProtocolMessage, + { + let message = Message::new(T::msg_type(), None, body.to_bytes()?.into()) + .to_bytes()? + .into(); + + self.0.sink.lock().await.send(message).await?; + + Ok(()) + } + + /// Sends a message to the peer and expects a message that's either a response or a rejection. + pub async fn request_fallible(&self, body: B) -> Result, ClientError> + where + T: Streamable + ChiaProtocolMessage, + E: Streamable + ChiaProtocolMessage, + B: Streamable + ChiaProtocolMessage, + { + let message = self.request_raw(body).await?; + if message.msg_type != T::msg_type() && message.msg_type != E::msg_type() { + return Err(ClientError::InvalidResponse( + vec![T::msg_type(), E::msg_type()], + message.msg_type, + )); + } + if message.msg_type == T::msg_type() { + Ok(Ok(T::from_bytes(&message.data)?)) + } else { + Ok(Err(E::from_bytes(&message.data)?)) + } + } + + /// Sends a message to the peer and expects a specific response message. + pub async fn request_infallible(&self, body: B) -> Result + where + T: Streamable + ChiaProtocolMessage, + B: Streamable + ChiaProtocolMessage, + { + let message = self.request_raw(body).await?; + if message.msg_type != T::msg_type() { + return Err(ClientError::InvalidResponse( + vec![T::msg_type()], + message.msg_type, + )); + } + Ok(T::from_bytes(&message.data)?) + } + + /// Sends a message to the peer and expects any arbitrary protocol message without parsing it. + pub async fn request_raw(&self, body: T) -> Result + where + T: Streamable + ChiaProtocolMessage, + { + let (sender, receiver) = oneshot::channel(); + + let message = Message { + msg_type: T::msg_type(), + id: Some(self.0.requests.insert(sender).await), + data: body.to_bytes()?.into(), + } + .to_bytes()? + .into(); + + self.0.sink.lock().await.send(message).await?; + Ok(receiver.await?) + } +} + +impl Drop for PeerInner { + fn drop(&mut self) { + self.inbound_handle.abort(); + } +} + +async fn handle_inbound_messages( + mut stream: Stream, + sender: mpsc::Sender, + requests: Arc, +) -> Result<(), ClientError> { + use tungstenite::Message::{Binary, Close, Frame, Ping, Pong, Text}; + + while let Some(message) = stream.next().await { + let message = message?; + + match message { + Frame(..) => unreachable!(), + Close(..) => break, + Ping(..) | Pong(..) => {} + Text(text) => { + warn!("Received unexpected text message: {text}"); + } + Binary(binary) => { + let message = Message::from_bytes(&binary)?; + + let Some(id) = message.id else { + sender.send(message).await.ok(); + continue; + }; + + let Some(request) = requests.remove(id).await else { + warn!( + "Received {:?} message with untracked id {id}", + message.msg_type + ); + return Err(ClientError::UnexpectedMessage(message.msg_type)); + }; + + request.send(message); + } + } + } + Ok(()) +} diff --git a/crates/chia-sdk-client/src/request_map.rs b/crates/chia-sdk-client/src/request_map.rs new file mode 100644 index 00000000..697b9b59 --- /dev/null +++ b/crates/chia-sdk-client/src/request_map.rs @@ -0,0 +1,61 @@ +use std::{collections::HashMap, sync::Arc}; + +use chia_protocol::Message; +use tokio::sync::{oneshot, Mutex, OwnedSemaphorePermit, Semaphore}; + +#[derive(Debug)] +pub(crate) struct Request { + sender: oneshot::Sender, + _permit: OwnedSemaphorePermit, +} + +impl Request { + pub(crate) fn send(self, message: Message) { + self.sender.send(message).ok(); + } +} + +#[derive(Debug)] +pub(crate) struct RequestMap { + items: Mutex>, + semaphore: Arc, +} + +impl RequestMap { + pub(crate) fn new() -> Self { + Self { + items: Mutex::new(HashMap::new()), + semaphore: Arc::new(Semaphore::new(u16::MAX as usize)), + } + } + + pub(crate) async fn insert(&self, sender: oneshot::Sender) -> u16 { + let permit = self + .semaphore + .clone() + .acquire_owned() + .await + .expect("semaphore closed"); + + let mut items = self.items.lock().await; + + items.retain(|_, v| !v.sender.is_closed()); + + let index = (0..=u16::MAX) + .find(|i| !items.contains_key(i)) + .expect("exceeded expected number of requests"); + + items.insert( + index, + Request { + sender, + _permit: permit, + }, + ); + index + } + + pub(crate) async fn remove(&self, id: u16) -> Option { + self.items.lock().await.remove(&id) + } +} diff --git a/crates/chia-sdk-client/src/tls.rs b/crates/chia-sdk-client/src/tls.rs new file mode 100644 index 00000000..a600b1f2 --- /dev/null +++ b/crates/chia-sdk-client/src/tls.rs @@ -0,0 +1,127 @@ +use std::fs; + +use chia_ssl::ChiaCertificate; + +#[cfg(any(feature = "native-tls", feature = "rustls"))] +use tokio_tungstenite::Connector; + +use crate::ClientError; + +/// Loads an SSL certificate, or creates it if it doesn't exist already. +pub fn load_ssl_cert(cert_path: &str, key_path: &str) -> Result { + fs::read_to_string(cert_path) + .and_then(|cert| { + fs::read_to_string(key_path).map(|key| ChiaCertificate { + cert_pem: cert, + key_pem: key, + }) + }) + .or_else(|_| { + let cert = ChiaCertificate::generate()?; + fs::write(cert_path, &cert.cert_pem)?; + fs::write(key_path, &cert.key_pem)?; + Ok(cert) + }) +} + +/// Creates a native-tls connector from a certificate. +#[cfg(feature = "native-tls")] +pub fn create_native_tls_connector(cert: &ChiaCertificate) -> Result { + use native_tls::{Identity, TlsConnector}; + + let identity = Identity::from_pkcs8(cert.cert_pem.as_bytes(), cert.key_pem.as_bytes())?; + let tls_connector = TlsConnector::builder() + .identity(identity) + .danger_accept_invalid_certs(true) + .build()?; + + Ok(Connector::NativeTls(tls_connector)) +} + +/// Creates a rustls connector from a certificate. +#[cfg(feature = "rustls")] +pub fn create_rustls_connector(cert: &ChiaCertificate) -> Result { + use std::sync::Arc; + + use chia_ssl::CHIA_CA_CRT; + use rustls::{ + client::danger::HandshakeSignatureValid, + crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider}, + pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime}, + ClientConfig, DigitallySignedStruct, RootCertStore, + }; + + #[derive(Debug)] + struct NoCertificateVerification(CryptoProvider); + + impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp: &[u8], + _now: UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls12_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls13_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0.signature_verification_algorithms.supported_schemes() + } + } + + let mut root_cert_store = RootCertStore::empty(); + + let ca: Vec> = + rustls_pemfile::certs(&mut CHIA_CA_CRT.as_bytes()).collect::>()?; + + root_cert_store.add(ca.into_iter().next().ok_or(ClientError::MissingCa)?)?; + + let cert_chain: Vec> = + rustls_pemfile::certs(&mut cert.cert_pem.as_bytes()).collect::>()?; + + let key = rustls_pemfile::pkcs8_private_keys(&mut cert.key_pem.as_bytes()) + .next() + .ok_or(ClientError::MissingPkcs8Key)??; + + let mut config = ClientConfig::builder() + .with_root_certificates(root_cert_store) + .with_client_auth_cert(cert_chain, PrivateKeyDer::Pkcs8(key))?; + + config + .dangerous() + .set_certificate_verifier(Arc::new(NoCertificateVerification( + rustls::crypto::aws_lc_rs::default_provider(), + ))); + + Ok(Connector::Rustls(Arc::new(config))) +} diff --git a/crates/chia-sdk-derive/Cargo.toml b/crates/chia-sdk-derive/Cargo.toml new file mode 100644 index 00000000..9702e308 --- /dev/null +++ b/crates/chia-sdk-derive/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "chia-sdk-derive" +version = "0.16.0" +edition = "2021" +license = "Apache-2.0" +description = "Derive macros for chia-wallet-sdk." +authors = ["Brandon Haggstrom "] +homepage = "https://github.com/Rigidity/chia-wallet-sdk" +repository = "https://github.com/Rigidity/chia-wallet-sdk" +readme = { workspace = true } +keywords = { workspace = true } +categories = { workspace = true } + +[lints] +workspace = true + +[lib] +proc-macro = true + +[dependencies] +syn = { workspace = true, features = ["visit-mut", "extra-traits"] } +quote = { workspace = true } +convert_case = { workspace = true } diff --git a/crates/chia-sdk-derive/src/impl_conditions.rs b/crates/chia-sdk-derive/src/impl_conditions.rs new file mode 100644 index 00000000..9c9591a6 --- /dev/null +++ b/crates/chia-sdk-derive/src/impl_conditions.rs @@ -0,0 +1,396 @@ +use convert_case::{Case, Casing}; +use proc_macro::{Span, TokenStream}; +use quote::quote; +use syn::{ + braced, + parse::{Parse, ParseStream}, + parse_macro_input, + punctuated::{Pair, Punctuated}, + visit_mut::VisitMut, + Expr, Ident, Token, Type, +}; + +#[derive(Debug, Clone)] +struct Conditions { + name: Ident, + generic: Ident, + items: Punctuated, +} + +#[derive(Debug, Clone)] +struct Condition { + name: Ident, + generics: Option>, + additional_derives: Option, + fields: Punctuated, +} + +#[derive(Debug, Clone)] +struct AdditionalDerives { + derives: Punctuated, +} + +#[derive(Debug, Clone)] +struct ConditionField { + modifier: Modifier, + name: Ident, + ty: Type, + constant: Option, +} + +#[derive(Debug, Clone)] +enum Modifier { + None, + Spread, + Optional, +} + +impl Parse for Conditions { + fn parse(input: ParseStream<'_>) -> syn::Result { + input.parse::()?; + input.parse::()?; + let name = input.parse()?; + input.parse::()?; + let generic = input.parse()?; + input.parse::]>()?; + + let content; + braced!(content in input); + let items = content.parse_terminated(Condition::parse, Token![,])?; + + Ok(Self { + name, + generic, + items, + }) + } +} + +impl Parse for Condition { + fn parse(input: ParseStream<'_>) -> syn::Result { + let name = input.parse()?; + let generics = if input.peek(Token![<]) { + input.parse::()?; + let mut generics = Punctuated::new(); + loop { + generics.push_value(input.parse()?); + if input.peek(Token![>]) { + break; + } + generics.push_punct(input.parse()?); + } + input.parse::]>()?; + Some(generics) + } else { + None + }; + let additional_derives = if input.peek(Token![as]) { + Some(input.parse()?) + } else { + None + }; + + let content; + braced!(content in input); + let fields = content.parse_terminated(ConditionField::parse, Token![,])?; + + Ok(Self { + name, + generics, + additional_derives, + fields, + }) + } +} + +impl Parse for AdditionalDerives { + fn parse(input: ParseStream<'_>) -> syn::Result { + input.parse::()?; + + let mut derives = Punctuated::new(); + + loop { + derives.push_value(input.parse()?); + + if input.peek(Token![+]) { + derives.push_punct(input.parse()?); + } else { + break; + } + } + + Ok(Self { derives }) + } +} + +impl Parse for ConditionField { + fn parse(input: ParseStream<'_>) -> syn::Result { + let mut modifier = Modifier::None; + + if input.peek(Token![...]) { + input.parse::()?; + modifier = Modifier::Spread; + } + + let name = input.parse()?; + + if matches!(modifier, Modifier::None) && input.peek(Token![?]) { + input.parse::()?; + modifier = Modifier::Optional; + } + + input.parse::()?; + + let ty = input.parse()?; + + let constant = if input.peek(Token![if]) { + input.parse::()?; + Some(input.parse()?) + } else { + None + }; + + Ok(Self { + modifier, + name, + ty, + constant, + }) + } +} + +struct IdentReplacer { + from: Ident, + to: Ident, +} + +impl VisitMut for IdentReplacer { + fn visit_type_mut(&mut self, ty: &mut Type) { + if let Type::Path(type_path) = ty { + if let Some(first_segment) = type_path.path.segments.first_mut() { + if first_segment.ident == self.from { + first_segment.ident = self.to.clone(); + } + } + } + // Continue recursively visiting all types + syn::visit_mut::visit_type_mut(self, ty); + } +} + +fn replace_ident_in_type(ty: &mut Type, from: Ident, to: Ident) { + let mut replacer = IdentReplacer { from, to }; + replacer.visit_type_mut(ty); +} + +pub(crate) fn impl_conditions(input: TokenStream) -> TokenStream { + let Conditions { + name, + generic: enum_generic, + items, + .. + } = parse_macro_input!(input as Conditions); + + let mut variants = Vec::new(); + let mut structs = Vec::new(); + let mut main_impls = Vec::new(); + + for Condition { + name: condition, + generics: generic_list, + additional_derives, + fields, + .. + } in items + { + let generics_original = generic_list.as_ref().map(|idents| quote!( < #idents > )); + let mut generics_remapped = Punctuated::new(); + for pair in generic_list.clone().unwrap_or_default().into_pairs() { + match pair { + Pair::Punctuated(_ident, comma) => { + generics_remapped.push_value(enum_generic.clone()); + generics_remapped.push_punct(comma); + } + Pair::End(_ident) => { + generics_remapped.push_value(enum_generic.clone()); + } + } + } + let generics_remapped = if generics_original.is_some() { + Some(quote!( < #generics_remapped > )) + } else { + None + }; + + variants.push(quote! { + #condition(#condition #generics_remapped), + }); + + let additional_derives = additional_derives.map(|AdditionalDerives { derives }| { + let derives = derives.into_iter(); + quote! { + #( #[derive( #derives )] )* + } + }); + + let definitions = fields.clone().into_iter().map(|field| { + let ConditionField { + modifier, + name, + ty, + constant, + } = field; + + match modifier { + Modifier::None if constant.is_some() => quote! { + #[clvm(constant = #constant)] + pub #name: #ty, + }, + Modifier::None => quote! { + pub #name: #ty, + }, + Modifier::Spread => quote! { + #[clvm(rest)] + pub #name: #ty, + }, + Modifier::Optional => quote! { + #[clvm(default)] + pub #name: #ty, + }, + } + }); + + let mut parameters_original = Vec::new(); + let mut parameters_remapped = Vec::new(); + + fields.clone().into_iter().for_each(|field| { + let ConditionField { + name, + mut ty, + constant, + .. + } = field; + + if constant.is_some() { + return; + } + + parameters_original.push(quote! { #name: #ty }); + + for generic in generic_list.clone().unwrap_or_default() { + replace_ident_in_type(&mut ty, generic, enum_generic.clone()); + } + + parameters_remapped.push(quote! { #name: #ty }); + }); + + let names = fields.into_iter().filter_map(|field| { + let ConditionField { name, constant, .. } = field; + if constant.is_some() { + return None; + } + Some(quote! { #name }) + }); + + let new_parameters = parameters_original.clone(); + let new_names = names.clone(); + + structs.push(quote! { + #[derive(::clvm_traits::ToClvm, ::clvm_traits::FromClvm)] + #[::clvm_traits::apply_constants] + #[derive(Debug, Clone, PartialEq, Eq)] + #additional_derives + #[clvm(list)] + pub struct #condition #generics_original { + #( #definitions )* + } + + impl #generics_original #condition #generics_original { + pub fn new( #( #new_parameters, )* ) -> Self { + Self { #( #new_names, )* } + } + } + }); + + let snake_case = Ident::new( + &condition.to_string().to_case(Case::Snake), + Span::call_site().into(), + ); + + let into_name = Ident::new(&format!("into_{snake_case}"), Span::call_site().into()); + let as_name = Ident::new(&format!("as_{snake_case}"), Span::call_site().into()); + let is_name = Ident::new(&format!("is_{snake_case}"), Span::call_site().into()); + + let condition_parameters = parameters_remapped.clone(); + let condition_names = names.clone(); + + let generics_remapped_turbofish = generics_remapped + .clone() + .map(|generics| quote!( ::#generics )); + + main_impls.push(quote! { + pub fn #snake_case( #( #condition_parameters, )* ) -> Self { + Self::#condition( #condition #generics_remapped_turbofish { #( #condition_names, )* } ) + } + }); + + main_impls.push(quote! { + pub fn #into_name(self) -> Option<#condition #generics_remapped> { + if let Self::#condition(inner) = self { + Some(inner) + } else { + None + } + } + }); + + main_impls.push(quote! { + pub fn #as_name(&self) -> Option<&#condition #generics_remapped> { + if let Self::#condition(inner) = self { + Some(inner) + } else { + None + } + } + }); + + main_impls.push(quote! { + pub fn #is_name(&self) -> bool { + matches!(self, Self::#condition(..)) + } + }); + + let condition_names = names.clone(); + + structs.push(quote! { + impl<#enum_generic> crate::Conditions<#enum_generic> { + pub fn #snake_case(self, #( #parameters_remapped, )* ) -> Self { + self.with( #condition #generics_remapped_turbofish { #( #condition_names, )* } ) + } + } + + impl<#enum_generic> From<#condition #generics_remapped> for Condition<#enum_generic> { + fn from(inner: #condition #generics_remapped) -> Self { + Self::#condition(inner) + } + } + }); + } + + quote! { + #[non_exhaustive] + #[derive(Debug, Clone, PartialEq, Eq, ::clvm_traits::ToClvm, ::clvm_traits::FromClvm)] + #[clvm(transparent)] + pub enum #name<#enum_generic = ::clvmr::NodePtr> { + #( #variants )* + Other(T), + } + + impl<#enum_generic> #name<#enum_generic> { + #( #main_impls )* + } + + #( #structs )* + } + .into() +} diff --git a/crates/chia-sdk-derive/src/lib.rs b/crates/chia-sdk-derive/src/lib.rs new file mode 100644 index 00000000..efbe1f1b --- /dev/null +++ b/crates/chia-sdk-derive/src/lib.rs @@ -0,0 +1,10 @@ +use proc_macro::TokenStream; + +mod impl_conditions; + +use impl_conditions::impl_conditions; + +#[proc_macro] +pub fn conditions(input: TokenStream) -> TokenStream { + impl_conditions(input) +} diff --git a/crates/chia-sdk-driver/Cargo.toml b/crates/chia-sdk-driver/Cargo.toml new file mode 100644 index 00000000..f9cf383b --- /dev/null +++ b/crates/chia-sdk-driver/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "chia-sdk-driver" +version = "0.16.0" +edition = "2021" +license = "Apache-2.0" +description = "Driver code for interacting with standard puzzles on the Chia blockchain." +authors = ["Brandon Haggstrom "] +homepage = "https://github.com/Rigidity/chia-wallet-sdk" +repository = "https://github.com/Rigidity/chia-wallet-sdk" +readme = { workspace = true } +keywords = { workspace = true } +categories = { workspace = true } + +[package.metadata.docs.rs] +all-features = true + +[lints] +workspace = true + +[features] +chip-0035 = [] + +[dependencies] +chia-bls = { workspace = true } +chia-protocol = { workspace = true } +chia-puzzles = { workspace = true } +clvm-traits = { workspace = true } +clvm-utils = { workspace = true } +clvmr = { workspace = true } +thiserror = { workspace = true } +chia-sdk-types = { workspace = true } +hex-literal = { workspace = true } +num-bigint = { workspace = true} +hex = { workspace = true } + +[dev-dependencies] +chia-sdk-test = { workspace = true } +anyhow = { workspace = true } +chia-consensus = { workspace = true } +hex = { workspace = true } +hex-literal = { workspace = true } +rstest = { workspace = true } diff --git a/crates/chia-sdk-driver/docs.md b/crates/chia-sdk-driver/docs.md new file mode 100644 index 00000000..57eed2ed --- /dev/null +++ b/crates/chia-sdk-driver/docs.md @@ -0,0 +1,42 @@ +## Puzzles + +Chia coins have a puzzle, which controls how it can be spent. +The solution is used as the arguments to the puzzle, and the +output is a list of [`Conditions`]. + +A puzzle consists of multiple layers composed together. + +## Layers + +A [`Layer`] is a subset of the logic that makes up a smart coin in Chia. +They are also referred to as "inner puzzles", and the solution can be broken +up into "inner solutions" as well. + +Generally, you can parse and construct the individual layers separately. +This allows them to be composed together freely. However, there are sometimes +additional restraints which limit the ways they can be mixed. For example, +the [`CatLayer`] cannot have another [`CatLayer`] as its inner puzzle, due to the +way it's written. This would create an error when validating the announcements. + +### P2 Puzzles + +A p2 puzzle (meaning "pay to") controls the ownership of the coin. +The simplest example of this is [`p2_conditions.clsp`], which requires a signature +from a single public key and outputs a list of conditions from the solution. + +The "standard transaction" (which is [`p2_delegated_puzzle_or_hidden_puzzle.clsp`]) +is a kind of p2 puzzle that adds additional flexibility. Specifically, support +for an inner puzzle, and usage of a delegated puzzle instead of directly conditions. + +Generally, the p2 puzzle is the base layer in a coin's puzzle, and everything +else builds on top of it to restrict the way it can be spent or attach state. + +## Primitives + +A [`Primitive`] uses one or more [`Layer`] to parse info from a parent's coin spend. +Generally, [`Layer`] has the ability to parse and construct individual puzzles and solutions, +and the composed [`Primitive`] struct can parse all of the information required to spend a coin. +The [`Primitive`] should also provide a way to spend the coin, and other utilities necessary. + +[`p2_conditions.clsp`]: https://github.com/Chia-Network/chia-blockchain/blob/bd022b0c9b0d3e0bc13a0efebba9f22417ca64b5/chia/wallet/puzzles/p2_conditions.clsp +[`p2_delegated_puzzle_or_hidden_puzzle.clsp`]: https://github.com/Chia-Network/chia-blockchain/blob/bd022b0c9b0d3e0bc13a0efebba9f22417ca64b5/chia/wallet/puzzles/p2_delegated_puzzle_or_hidden_puzzle.clsp diff --git a/crates/chia-sdk-driver/src/driver_error.rs b/crates/chia-sdk-driver/src/driver_error.rs new file mode 100644 index 00000000..bcdcbe85 --- /dev/null +++ b/crates/chia-sdk-driver/src/driver_error.rs @@ -0,0 +1,50 @@ +use std::num::TryFromIntError; + +use clvm_traits::{FromClvmError, ToClvmError}; +use clvmr::reduction::EvalErr; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DriverError { + #[error("io error: {0}")] + Io(#[from] std::io::Error), + + #[error("try from int error")] + TryFromInt(#[from] TryFromIntError), + + #[error("failed to serialize clvm value: {0}")] + ToClvm(#[from] ToClvmError), + + #[error("failed to deserialize clvm value: {0}")] + FromClvm(#[from] FromClvmError), + + #[error("clvm eval error: {0}")] + Eval(#[from] EvalErr), + + #[error("invalid mod hash")] + InvalidModHash, + + #[error("non-standard inner puzzle layer")] + NonStandardLayer, + + #[error("missing child")] + MissingChild, + + #[error("missing hint")] + MissingHint, + + #[error("missing memo")] + MissingMemo, + + #[error("invalid memo")] + InvalidMemo, + + #[error("invalid singleton struct")] + InvalidSingletonStruct, + + #[error("expected even oracle fee, but it was odd")] + OddOracleFee, + + #[error("custom driver error: {0}")] + Custom(String), +} diff --git a/crates/chia-sdk-driver/src/hashed_ptr.rs b/crates/chia-sdk-driver/src/hashed_ptr.rs new file mode 100644 index 00000000..ca5fc89d --- /dev/null +++ b/crates/chia-sdk-driver/src/hashed_ptr.rs @@ -0,0 +1,185 @@ +use std::{cmp::Ordering, fmt}; + +use clvm_traits::{FromClvm, FromClvmError, ToClvm, ToClvmError}; +use clvm_utils::{tree_hash, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; +use hex_literal::hex; + +#[derive(Clone, Copy, Eq)] +pub struct HashedPtr { + ptr: NodePtr, + tree_hash: TreeHash, +} + +impl HashedPtr { + pub const NIL: Self = Self { + ptr: NodePtr::NIL, + tree_hash: TreeHash::new(hex!( + "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a" + )), + }; + + pub fn new(ptr: NodePtr, tree_hash: TreeHash) -> Self { + Self { ptr, tree_hash } + } + + pub fn from_ptr(allocator: &Allocator, ptr: NodePtr) -> Self { + Self::new(ptr, tree_hash(allocator, ptr)) + } + + pub fn ptr(&self) -> NodePtr { + self.ptr + } + + pub fn tree_hash(&self) -> TreeHash { + self.tree_hash + } +} + +impl fmt::Debug for HashedPtr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "HashedPtr({})", self.tree_hash) + } +} + +impl fmt::Display for HashedPtr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.tree_hash) + } +} + +impl PartialEq for HashedPtr { + fn eq(&self, other: &Self) -> bool { + self.tree_hash == other.tree_hash + } +} + +impl PartialOrd for HashedPtr { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.tree_hash.cmp(&other.tree_hash)) + } +} + +impl Ord for HashedPtr { + fn cmp(&self, other: &Self) -> Ordering { + self.tree_hash.cmp(&other.tree_hash) + } +} + +impl ToClvm for HashedPtr { + fn to_clvm(&self, _encoder: &mut Allocator) -> Result { + Ok(self.ptr) + } +} + +impl FromClvm for HashedPtr { + fn from_clvm(decoder: &Allocator, node: NodePtr) -> Result { + Ok(Self::from_ptr(decoder, node)) + } +} + +impl ToTreeHash for HashedPtr { + fn tree_hash(&self) -> TreeHash { + self.tree_hash + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_nil_hashed_ptr() { + let allocator = Allocator::new(); + let atom = allocator.atom(HashedPtr::NIL.ptr); + assert!(atom.as_ref().is_empty()); + + assert_eq!(HashedPtr::NIL, HashedPtr::NIL); + assert_eq!(HashedPtr::NIL.ptr(), NodePtr::NIL); + assert_eq!( + HashedPtr::NIL.tree_hash(), + tree_hash(&allocator, NodePtr::NIL) + ); + } + + #[test] + fn test_hashed_ptr() -> anyhow::Result<()> { + let mut allocator = Allocator::new(); + + let ptr = ["Hello", " ", "world", "!"].to_clvm(&mut allocator)?; + let hashed_ptr = HashedPtr::from_ptr(&allocator, ptr); + assert_eq!(hashed_ptr.ptr(), ptr); + assert_eq!(hashed_ptr.tree_hash(), tree_hash(&allocator, ptr)); + assert_eq!(hashed_ptr, hashed_ptr); + assert_eq!(hashed_ptr, HashedPtr::new(ptr, hashed_ptr.tree_hash())); + + Ok(()) + } + + #[test] + fn test_hashed_ptr_roundtrip() -> anyhow::Result<()> { + let mut allocator = Allocator::new(); + + let ptr = "hello".to_clvm(&mut allocator)?; + let hashed_ptr = HashedPtr::from_ptr(&allocator, ptr); + + let new_ptr = hashed_ptr.to_clvm(&mut allocator)?; + assert_eq!(ptr, new_ptr); + + let new_hashed_ptr = HashedPtr::from_clvm(&allocator, new_ptr)?; + assert_eq!(hashed_ptr, new_hashed_ptr); + + Ok(()) + } + + #[test] + fn test_hashed_ptr_to_treehash() -> anyhow::Result<()> { + let mut allocator = Allocator::new(); + + let ptr = "hello".to_clvm(&mut allocator)?; + let hashed_ptr = HashedPtr::from_ptr(&allocator, ptr); + let tree_hash = ToTreeHash::tree_hash(&hashed_ptr); + assert_eq!(tree_hash, hashed_ptr.tree_hash()); + + Ok(()) + } + + #[test] + fn test_hashed_ptr_order() -> anyhow::Result<()> { + let mut allocator = Allocator::new(); + + let mut ptrs = Vec::new(); + + for i in 0..5 { + let ptr = i.to_clvm(&mut allocator)?; + ptrs.push(HashedPtr::from_ptr(&allocator, ptr)); + } + + ptrs.sort(); + + let hashes: Vec = ptrs.into_iter().map(|ptr| ptr.tree_hash()).collect(); + + assert_eq!( + hashes, + [ + TreeHash::new(hex!( + "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a" + )), + TreeHash::new(hex!( + "9dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2" + )), + TreeHash::new(hex!( + "a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222" + )), + TreeHash::new(hex!( + "a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c5" + )), + TreeHash::new(hex!( + "c79b932e1e1da3c0e098e5ad2c422937eb904a76cf61d83975a74a68fbb04b99" + )) + ] + ); + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/layer.rs b/crates/chia-sdk-driver/src/layer.rs new file mode 100644 index 00000000..09eb93c2 --- /dev/null +++ b/crates/chia-sdk-driver/src/layer.rs @@ -0,0 +1,94 @@ +use chia_protocol::{Coin, CoinSpend}; +use clvm_traits::{FromClvm, ToClvm}; +use clvmr::{Allocator, NodePtr}; + +use crate::{DriverError, Puzzle, Spend, SpendContext}; + +/// An individual layer in a puzzle's hierarchy. +pub trait Layer { + /// Most of the time, this is an actual CLVM type representing the solution. + /// However, you can also use a helper struct and customize [`Layer::construct_solution`] and [`Layer::parse_solution`]. + type Solution; + + /// Parses this layer from the given puzzle, returning [`None`] if the puzzle doesn't match. + /// An error is returned if the puzzle should have matched but couldn't be parsed. + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> + where + Self: Sized; + + /// Parses the [`Layer::Solution`] type from a CLVM solution pointer. + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result; + + /// Constructs the full curried puzzle for this layer. + /// Ideally, the puzzle itself should be cached in the [`SpendContext`]. + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result; + + /// Constructs the full solution for this layer. + /// Can be used to construct the solution from a helper struct, if it's not directly a CLVM type. + /// It's also possible to influence the solution based on the puzzle, if needed. + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result; + + /// Creates a spend for this layer. + fn construct_spend( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + let solution = self.construct_solution(ctx, solution)?; + let puzzle = self.construct_puzzle(ctx)?; + Ok(Spend::new(puzzle, solution)) + } + + /// Creates a coin spend for this layer. + fn construct_coin_spend( + &self, + ctx: &mut SpendContext, + coin: Coin, + solution: Self::Solution, + ) -> Result { + let solution = self.construct_solution(ctx, solution)?; + let puzzle = self.construct_puzzle(ctx)?; + Ok(CoinSpend::new( + coin, + ctx.serialize(&puzzle)?, + ctx.serialize(&solution)?, + )) + } +} + +impl Layer for T +where + T: ToClvm + FromClvm, +{ + type Solution = NodePtr; + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + Ok(Some(T::from_clvm(allocator, puzzle.ptr())?)) + } + + fn parse_solution( + _allocator: &Allocator, + solution: NodePtr, + ) -> Result { + Ok(solution) + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + ctx.alloc(self) + } + + fn construct_solution( + &self, + _ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + Ok(solution) + } +} diff --git a/crates/chia-sdk-driver/src/layers.rs b/crates/chia-sdk-driver/src/layers.rs new file mode 100644 index 00000000..12612475 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers.rs @@ -0,0 +1,44 @@ +mod cat_layer; +mod did_layer; +mod nft_ownership_layer; +mod nft_state_layer; +mod p2_delegated_conditions_layer; +mod p2_delegated_singleton_layer; +mod p2_one_of_many; +mod p2_singleton; +mod royalty_transfer_layer; +mod settlement_layer; +mod singleton_layer; +mod standard_layer; + +pub use cat_layer::*; +pub use did_layer::*; +pub use nft_ownership_layer::*; +pub use nft_state_layer::*; +pub use p2_delegated_conditions_layer::*; +pub use p2_delegated_singleton_layer::*; +pub use p2_one_of_many::*; +pub use p2_singleton::*; +pub use royalty_transfer_layer::*; +pub use settlement_layer::*; +pub use singleton_layer::*; +pub use standard_layer::*; + +#[cfg(feature = "chip-0035")] +mod datalayer; + +#[cfg(feature = "chip-0035")] +pub use datalayer::*; + +#[cfg(test)] +mod tests { + #[macro_export] + macro_rules! assert_puzzle_hash { + ($puzzle:ident => $puzzle_hash:ident) => { + let mut a = clvmr::Allocator::new(); + let ptr = clvmr::serde::node_from_bytes(&mut a, &$puzzle)?; + let hash = clvm_utils::tree_hash(&mut a, ptr); + assert_eq!($puzzle_hash, hash); + }; + } +} diff --git a/crates/chia-sdk-driver/src/layers/cat_layer.rs b/crates/chia-sdk-driver/src/layers/cat_layer.rs new file mode 100644 index 00000000..d1f7f425 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/cat_layer.rs @@ -0,0 +1,176 @@ +use chia_protocol::Bytes32; +use chia_puzzles::cat::{CatArgs, CatSolution, CAT_PUZZLE_HASH}; +use clvm_traits::FromClvm; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; + +use crate::{DriverError, Layer, Puzzle, SpendContext}; + +/// The CAT [`Layer`] enforces restrictions on the supply of a token. +/// Specifically, unless the TAIL program is run, the supply cannot change. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CatLayer { + /// The asset id of the CAT token. This is the tree hash of the TAIL program. + pub asset_id: Bytes32, + /// The inner puzzle layer, commonly used for determining ownership. + pub inner_puzzle: I, +} + +impl CatLayer { + pub fn new(asset_id: Bytes32, inner_puzzle: I) -> Self { + Self { + asset_id, + inner_puzzle, + } + } +} + +impl Layer for CatLayer +where + I: Layer, +{ + type Solution = CatSolution; + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != CAT_PUZZLE_HASH { + return Ok(None); + } + + let args = CatArgs::::from_clvm(allocator, puzzle.args)?; + + if args.mod_hash != CAT_PUZZLE_HASH.into() { + return Err(DriverError::InvalidModHash); + } + + let Some(inner_puzzle) = + I::parse_puzzle(allocator, Puzzle::parse(allocator, args.inner_puzzle))? + else { + return Ok(None); + }; + + Ok(Some(Self { + asset_id: args.asset_id, + inner_puzzle, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + let solution = CatSolution::::from_clvm(allocator, solution)?; + let inner_solution = I::parse_solution(allocator, solution.inner_puzzle_solution)?; + Ok(CatSolution { + inner_puzzle_solution: inner_solution, + lineage_proof: solution.lineage_proof, + prev_coin_id: solution.prev_coin_id, + this_coin_info: solution.this_coin_info, + next_coin_proof: solution.next_coin_proof, + prev_subtotal: solution.prev_subtotal, + extra_delta: solution.extra_delta, + }) + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.cat_puzzle()?, + args: CatArgs::new(self.asset_id, self.inner_puzzle.construct_puzzle(ctx)?), + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + let inner_solution = self + .inner_puzzle + .construct_solution(ctx, solution.inner_puzzle_solution)?; + ctx.alloc(&CatSolution { + inner_puzzle_solution: inner_solution, + lineage_proof: solution.lineage_proof, + prev_coin_id: solution.prev_coin_id, + this_coin_info: solution.this_coin_info, + next_coin_proof: solution.next_coin_proof, + prev_subtotal: solution.prev_subtotal, + extra_delta: solution.extra_delta, + }) + } +} + +impl ToTreeHash for CatLayer +where + I: ToTreeHash, +{ + fn tree_hash(&self) -> TreeHash { + let inner_puzzle_hash = self.inner_puzzle.tree_hash(); + CatArgs::curry_tree_hash(self.asset_id, inner_puzzle_hash) + } +} + +#[cfg(test)] +mod tests { + use chia_protocol::Coin; + use chia_puzzles::CoinProof; + + use super::*; + + #[test] + fn test_cat_layer() -> anyhow::Result<()> { + let mut ctx = SpendContext::new(); + let asset_id = Bytes32::new([1; 32]); + + let layer = CatLayer::new(asset_id, "Hello, world!".to_string()); + + let ptr = layer.construct_puzzle(&mut ctx)?; + let puzzle = Puzzle::parse(&ctx.allocator, ptr); + let roundtrip = + CatLayer::::parse_puzzle(&ctx.allocator, puzzle)?.expect("invalid CAT layer"); + + assert_eq!(roundtrip.asset_id, layer.asset_id); + assert_eq!(roundtrip.inner_puzzle, layer.inner_puzzle); + + let expected = CatArgs::curry_tree_hash(asset_id, layer.inner_puzzle.tree_hash()); + assert_eq!(hex::encode(ctx.tree_hash(ptr)), hex::encode(expected)); + + Ok(()) + } + + #[test] + fn test_cat_solution() -> anyhow::Result<()> { + let mut ctx = SpendContext::new(); + + let layer = CatLayer::new(Bytes32::default(), NodePtr::NIL); + + let solution = CatSolution { + inner_puzzle_solution: NodePtr::NIL, + lineage_proof: None, + prev_coin_id: Bytes32::default(), + this_coin_info: Coin::new(Bytes32::default(), Bytes32::default(), 42), + next_coin_proof: CoinProof { + parent_coin_info: Bytes32::default(), + inner_puzzle_hash: Bytes32::default(), + amount: 34, + }, + prev_subtotal: 0, + extra_delta: 0, + }; + let expected_ptr = ctx.alloc(&solution)?; + let expected_hash = ctx.tree_hash(expected_ptr); + + let actual_ptr = layer.construct_solution(&mut ctx, solution)?; + let actual_hash = ctx.tree_hash(actual_ptr); + + assert_eq!(hex::encode(actual_hash), hex::encode(expected_hash)); + + let roundtrip = CatLayer::::parse_solution(&ctx.allocator, actual_ptr)?; + assert_eq!(roundtrip, solution); + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/layers/datalayer.rs b/crates/chia-sdk-driver/src/layers/datalayer.rs new file mode 100644 index 00000000..98a27793 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/datalayer.rs @@ -0,0 +1,66 @@ +mod delegation_layer; +mod oracle_layer; +mod writer_layer; + +use clvm_utils::TreeHash; +use hex_literal::hex; + +pub use delegation_layer::*; +pub use oracle_layer::*; +pub use writer_layer::*; + +pub const DL_METADATA_UPDATER_PUZZLE: [u8; 1] = hex!( + " + 0b + " +); + +pub const DL_METADATA_UPDATER_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + " + 57bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9 + " +)); + +#[cfg(test)] +mod tests { + use clvm_traits::{clvm_list, ToClvm}; + use clvm_utils::tree_hash; + use clvmr::serde::node_from_bytes; + use rstest::rstest; + + use crate::{assert_puzzle_hash, SpendContext}; + + use super::*; + + #[test] + fn test_puzzle_hashes() -> anyhow::Result<()> { + assert_puzzle_hash!(DELEGATION_LAYER_PUZZLE => DELEGATION_LAYER_PUZZLE_HASH); + assert_puzzle_hash!(WRITER_FILTER_PUZZLE => WRITER_FILTER_PUZZLE_HASH); + assert_puzzle_hash!(DL_METADATA_UPDATER_PUZZLE => DL_METADATA_UPDATER_PUZZLE_HASH); + Ok(()) + } + + // tests that DL metadata updater indeed returns the third argument + #[rstest] + #[case::string(&hex!("8379616b"))] // run -d '"yak"' + #[case::atom(&hex!("ff018379616b"))] // run -d '(mod () "yak"))' + #[case::one_item_list(&hex!("ff01ff0180"))] // run -d '(mod () (list 1)))' + #[case::multiple_item_list(&hex!("ff01ff01ff02ff0380"))] // run -d '(mod () (list 1 2 3)))' + #[case::lists_within_list(&hex!("ff01ff01ffff02ff0380ffff04ff0580ffff060780"))] // run -d '(mod () (list 1 (list 2 3) (list 4 5) (c 6 7))))' + fn test_dl_metadata_updater_puzzle(#[case] third_arg: &'static [u8]) -> anyhow::Result<()> { + let mut ctx = SpendContext::new(); + + let third_arg_ptr = node_from_bytes(&mut ctx.allocator, third_arg)?; + let solution_ptr = clvm_list![(), (), third_arg_ptr].to_clvm(&mut ctx.allocator)?; + + let puzzle_ptr = node_from_bytes(&mut ctx.allocator, &DL_METADATA_UPDATER_PUZZLE)?; + let output = ctx.run(puzzle_ptr, solution_ptr)?; + + assert_eq!( + tree_hash(&ctx.allocator, output), + tree_hash(&ctx.allocator, third_arg_ptr), + ); + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/layers/datalayer/delegation_layer.rs b/crates/chia-sdk-driver/src/layers/datalayer/delegation_layer.rs new file mode 100644 index 00000000..5b5fce08 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/datalayer/delegation_layer.rs @@ -0,0 +1,171 @@ +use chia_protocol::Bytes32; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; +use hex_literal::hex; + +use crate::{DriverError, Layer, Puzzle, SpendContext}; + +#[allow(clippy::doc_markdown)] +/// The Delegation [`Layer`] is used to enable DataLayer delegation capabilities +/// For more information, see CHIP-0035. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DelegationLayer { + /// Launcher ID of the singleton outer layer. Used as the default hint when recreating this layer. + pub launcher_id: Bytes32, + /// Puzzle hash of the owner (usually a p2 puzzle like the standard puzzle). + pub owner_puzzle_hash: Bytes32, + /// Merkle root corresponding to the tree of delegated puzzles. + pub merkle_root: Bytes32, +} + +impl DelegationLayer { + pub fn new(launcher_id: Bytes32, owner_puzzle_hash: Bytes32, merkle_root: Bytes32) -> Self { + Self { + launcher_id, + owner_puzzle_hash, + merkle_root, + } + } +} + +impl Layer for DelegationLayer { + type Solution = DelegationLayerSolution; + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != DELEGATION_LAYER_PUZZLE_HASH { + return Ok(None); + } + + let args = DelegationLayerArgs::from_clvm(allocator, puzzle.args)?; + + Ok(Some(Self { + launcher_id: args.launcher_id, + owner_puzzle_hash: args.owner_puzzle_hash, + merkle_root: args.merkle_root, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + Ok(DelegationLayerSolution::::from_clvm( + allocator, solution, + )?) + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.delegation_layer_puzzle()?, + args: DelegationLayerArgs::new( + self.launcher_id, + self.owner_puzzle_hash, + self.merkle_root, + ), + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + ctx.alloc(&solution) + } +} + +pub const DELEGATION_LAYER_PUZZLE: [u8; 1027] = hex!( + " + ff02ffff01ff02ff12ffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff5fff + ff04ffff02ff81bfff82017f80ffff04ffff02ff16ffff04ff02ffff04ff81bfff80808080ff8080 + 8080808080808080ffff04ffff01ffffff3381f3ff02ffffa04bf5122f344554c53bde2ebb8cd2b7 + e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e8 + 78a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f680 + 6923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c5ff + ffff02ffff03ffff09ff82017fff1780ffff0181bfffff01ff02ffff03ffff09ff2fffff02ff1eff + ff04ff02ffff04ffff0bffff0101ff82017f80ffff04ff5fff808080808080ffff01ff02ff1affff + 04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff81bfffff04ffff04ff2fffff04ff0bff8080 + 80ff8080808080808080ffff01ff088080ff018080ff0180ff02ffff03ff2fffff01ff02ffff03ff + ff09ff818fff1880ffff01ff02ff1affff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff6f + ffff04ff81cfff8080808080808080ffff01ff04ffff02ffff03ffff02ffff03ffff09ff818fffff + 0181e880ffff01ff22ffff09ff820acfff8080ffff09ff8214cfffff01a057bfd1cb0adda3d94315 + 053fda723f2028320faa8338225d99f629e3d46d43a98080ffff01ff010180ff0180ffff014fffff + 01ff088080ff0180ffff02ff1affff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff6fffff + 04ff5fff80808080808080808080ff0180ffff01ff04ffff04ff10ffff04ffff0bff5cffff0bff14 + ffff0bff14ff6cff0580ffff0bff14ffff0bff7cffff0bff14ffff0bff14ff6cffff0bffff0101ff + 058080ffff0bff14ffff0bff7cffff0bff14ffff0bff14ff6cffff0bffff0101ff0b8080ffff0bff + 14ffff0bff7cffff0bff14ffff0bff14ff6cffff0bffff0101ff178080ffff0bff14ffff0bff7cff + ff0bff14ffff0bff14ff6cffff0bffff0101ff819f8080ffff0bff14ff6cff4c808080ff4c808080 + ff4c808080ff4c808080ff4c808080ffff04ffff0101ffff04ff81dfff8080808080ff808080ff01 + 80ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff16ffff04ff02ffff04ff09ff8080 + 8080ffff02ff16ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff + 02ffff03ff1bffff01ff02ff1effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01 + ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13 + ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff018080 + " +); + +pub const DELEGATION_LAYER_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + " + c3b249466cb15c51e5abb5c54ef5077c1624ae2e6a0f8f7a3fa197a943a5d62e + " +)); + +#[derive(ToClvm, FromClvm, Debug, Clone, Copy, PartialEq, Eq)] +#[clvm(curry)] +pub struct DelegationLayerArgs { + pub mod_hash: Bytes32, + pub launcher_id: Bytes32, + pub owner_puzzle_hash: Bytes32, + pub merkle_root: Bytes32, +} + +impl DelegationLayerArgs { + pub fn new(launcher_id: Bytes32, owner_puzzle_hash: Bytes32, merkle_root: Bytes32) -> Self { + Self { + mod_hash: DELEGATION_LAYER_PUZZLE_HASH.into(), + launcher_id, + owner_puzzle_hash, + merkle_root, + } + } +} + +impl DelegationLayerArgs { + pub fn curry_tree_hash( + launcher_id: Bytes32, + owner_puzzle_hash: Bytes32, + merkle_root: Bytes32, + ) -> TreeHash { + CurriedProgram { + program: DELEGATION_LAYER_PUZZLE_HASH, + args: DelegationLayerArgs { + mod_hash: DELEGATION_LAYER_PUZZLE_HASH.into(), + launcher_id, + owner_puzzle_hash, + merkle_root, + }, + } + .tree_hash() + } +} + +#[derive(ToClvm, FromClvm, Debug, Clone, PartialEq, Eq)] +#[clvm(list)] +pub struct DelegationLayerSolution { + pub merkle_proof: Option<(u32, Vec)>, + pub puzzle_reveal: P, + pub puzzle_solution: S, +} + +impl SpendContext { + pub fn delegation_layer_puzzle(&mut self) -> Result { + self.puzzle(DELEGATION_LAYER_PUZZLE_HASH, &DELEGATION_LAYER_PUZZLE) + } +} diff --git a/crates/chia-sdk-driver/src/layers/datalayer/oracle_layer.rs b/crates/chia-sdk-driver/src/layers/datalayer/oracle_layer.rs new file mode 100644 index 00000000..ac519750 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/datalayer/oracle_layer.rs @@ -0,0 +1,87 @@ +use chia_protocol::{Bytes, Bytes32}; +use chia_sdk_types::Condition; +use clvm_traits::{clvm_quote, match_quote, FromClvm, ToClvm}; +use clvmr::{Allocator, NodePtr}; + +use crate::{DriverError, Layer, Puzzle, Spend, SpendContext}; + +/// The Oracle [`Layer`] enables anyone to spend a coin provided they pay an XCH fee to an address. +/// It's typically used with [`DelegationLayer`](crate::DelegationLayer). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OracleLayer { + /// The puzzle hash corresponding to the address the fee should be paid to. + pub oracle_puzzle_hash: Bytes32, + /// The amount of XCH that should be paid to the oracle. + pub oracle_fee: u64, +} + +impl OracleLayer { + /// Creates a new [`OracleLayer`] if the fee is even. + /// Returns `None` if the fee is odd, which would make the puzzle invalid. + pub fn new(oracle_puzzle_hash: Bytes32, oracle_fee: u64) -> Option { + if oracle_fee % 2 != 0 { + return None; + } + + Some(Self { + oracle_puzzle_hash, + oracle_fee, + }) + } +} + +impl Layer for OracleLayer { + type Solution = (); + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_raw() else { + return Ok(None); + }; + + let (_q, conditions) = + >)>::from_clvm(allocator, puzzle.ptr)?; + if conditions.len() != 2 { + return Ok(None); + } + + if let Some(Condition::CreateCoin(create_coin)) = conditions.first() { + Ok(Self::new(create_coin.puzzle_hash, create_coin.amount)) + } else { + Ok(None) + } + } + + fn parse_solution(_: &Allocator, _: NodePtr) -> Result { + Ok(()) + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + if self.oracle_fee % 2 == 1 { + return Err(DriverError::Custom("oracle fee must be even".to_string())); + } + + let conditions: Vec> = vec![ + Condition::create_coin(self.oracle_puzzle_hash, self.oracle_fee, vec![]), + Condition::create_puzzle_announcement(Bytes::new("$".into())), + ]; + + Ok(clvm_quote!(conditions).to_clvm(&mut ctx.allocator)?) + } + + fn construct_solution( + &self, + _: &mut SpendContext, + (): Self::Solution, + ) -> Result { + Ok(NodePtr::NIL) + } +} + +impl OracleLayer { + pub fn spend(self, ctx: &mut SpendContext) -> Result { + let puzzle = self.construct_puzzle(ctx)?; + let solution = self.construct_solution(ctx, ())?; + + Ok(Spend { puzzle, solution }) + } +} diff --git a/crates/chia-sdk-driver/src/layers/datalayer/writer_layer.rs b/crates/chia-sdk-driver/src/layers/datalayer/writer_layer.rs new file mode 100644 index 00000000..66626ab6 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/datalayer/writer_layer.rs @@ -0,0 +1,157 @@ +use chia_puzzles::standard::StandardSolution; +use chia_sdk_types::Conditions; +use clvm_traits::{clvm_quote, FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; +use hex_literal::hex; + +use crate::{DriverError, Layer, Puzzle, Spend, SpendContext, StandardLayer}; + +/// The Writer [`Layer`] removes an authorized puzzle's ability to change the list of authorized puzzles. +/// It's typically used with [`DelegationLayer`](crate::DelegationLayer). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct WriterLayer { + /// The inner puzzle layer, commonly used for determining ownership. + pub inner_puzzle: I, +} + +impl WriterLayer { + pub fn new(inner_puzzle: I) -> Self { + Self { inner_puzzle } + } +} + +impl Layer for WriterLayer +where + I: Layer, +{ + type Solution = I::Solution; + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != WRITER_FILTER_PUZZLE_HASH { + return Ok(None); + } + + let args = WriterLayerArgs::::from_clvm(allocator, puzzle.args)?; + + let Some(inner_puzzle) = + I::parse_puzzle(allocator, Puzzle::parse(allocator, args.inner_puzzle))? + else { + return Ok(None); + }; + + Ok(Some(Self { inner_puzzle })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + let inner_solution = + WriterLayerSolution::::from_clvm(allocator, solution)?.inner_solution; + + I::parse_solution(allocator, inner_solution) + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.delegated_writer_filter()?, + args: WriterLayerArgs::new(self.inner_puzzle.construct_puzzle(ctx)?), + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + let inner_solution = self.inner_puzzle.construct_solution(ctx, solution)?; + + ctx.alloc(&WriterLayerSolution:: { inner_solution }) + } +} + +impl ToTreeHash for WriterLayer +where + I: ToTreeHash, +{ + fn tree_hash(&self) -> TreeHash { + let inner_puzzle_hash = self.inner_puzzle.tree_hash(); + + WriterLayerArgs::curry_tree_hash(inner_puzzle_hash) + } +} + +impl WriterLayer { + pub fn spend( + self, + ctx: &mut SpendContext, + output_conditions: Conditions, + ) -> Result { + let dp = ctx.alloc(&clvm_quote!(output_conditions))?; + let solution = self.construct_solution( + ctx, + StandardSolution { + original_public_key: None, + delegated_puzzle: dp, + solution: NodePtr::NIL, + }, + )?; + let puzzle = self.construct_puzzle(ctx)?; + + Ok(Spend { puzzle, solution }) + } +} + +pub const WRITER_FILTER_PUZZLE: [u8; 110] = hex!( + " + ff02ffff01ff02ff02ffff04ff02ffff04ffff02ff05ff0b80ff80808080ffff04ffff01ff02ffff + 03ff05ffff01ff02ffff03ffff09ff11ffff0181f380ffff01ff0880ffff01ff04ff09ffff02ff02 + ffff04ff02ffff04ff0dff808080808080ff0180ff8080ff0180ff018080 + " +); + +pub const WRITER_FILTER_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + " + 407f70ea751c25052708219ae148b45db2f61af2287da53d600b2486f12b3ca6 + " +)); + +#[derive(ToClvm, FromClvm, Debug, Clone, Copy, PartialEq, Eq)] +#[clvm(curry)] +pub struct WriterLayerArgs { + pub inner_puzzle: I, +} + +impl WriterLayerArgs { + pub fn new(inner_puzzle: I) -> Self { + Self { inner_puzzle } + } +} + +impl WriterLayerArgs { + pub fn curry_tree_hash(inner_puzzle: TreeHash) -> TreeHash { + CurriedProgram { + program: WRITER_FILTER_PUZZLE_HASH, + args: WriterLayerArgs { inner_puzzle }, + } + .tree_hash() + } +} + +#[derive(ToClvm, FromClvm, Debug, Clone, PartialEq, Eq)] +#[clvm(list)] +pub struct WriterLayerSolution { + pub inner_solution: I, +} + +impl SpendContext { + pub fn delegated_writer_filter(&mut self) -> Result { + self.puzzle(WRITER_FILTER_PUZZLE_HASH, &WRITER_FILTER_PUZZLE) + } +} diff --git a/crates/chia-sdk-driver/src/layers/did_layer.rs b/crates/chia-sdk-driver/src/layers/did_layer.rs new file mode 100644 index 00000000..7a66a83b --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/did_layer.rs @@ -0,0 +1,155 @@ +use chia_protocol::Bytes32; +use chia_puzzles::{ + did::{DidArgs, DidSolution, DID_INNER_PUZZLE_HASH}, + singleton::{SingletonStruct, SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_PUZZLE_HASH}, +}; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; + +use crate::{DriverError, Layer, Puzzle, SpendContext}; + +/// The DID [`Layer`] keeps track of metadata and handles recovery capabilities. +/// It's typically an inner layer of the [`SingletonLayer`](crate::SingletonLayer). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DidLayer { + /// The unique launcher id for the DID. Also referred to as the DID id. + pub launcher_id: Bytes32, + /// The tree hash of an optional list of recovery DIDs. + pub recovery_list_hash: Option, + /// The number of verifications required to recover the DID. + pub num_verifications_required: u64, + /// Metadata associated with the DID. This is often just `()` for DIDs without metadata. + pub metadata: M, + /// The inner puzzle layer, commonly used for determining ownership. + pub inner_puzzle: I, +} + +impl DidLayer { + pub fn new( + launcher_id: Bytes32, + recovery_list_hash: Option, + num_verifications_required: u64, + metadata: M, + inner_puzzle: I, + ) -> Self { + Self { + launcher_id, + recovery_list_hash, + num_verifications_required, + metadata, + inner_puzzle, + } + } + + pub fn with_metadata(self, metadata: N) -> DidLayer { + DidLayer { + launcher_id: self.launcher_id, + recovery_list_hash: self.recovery_list_hash, + num_verifications_required: self.num_verifications_required, + metadata, + inner_puzzle: self.inner_puzzle, + } + } +} + +impl Layer for DidLayer +where + I: Layer, + M: ToClvm + FromClvm, +{ + type Solution = DidSolution; + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != DID_INNER_PUZZLE_HASH { + return Ok(None); + } + + let args = DidArgs::::from_clvm(allocator, puzzle.args)?; + + if args.singleton_struct.mod_hash != SINGLETON_TOP_LAYER_PUZZLE_HASH.into() + || args.singleton_struct.launcher_puzzle_hash != SINGLETON_LAUNCHER_PUZZLE_HASH.into() + { + return Err(DriverError::InvalidSingletonStruct); + } + + let Some(inner_puzzle) = + I::parse_puzzle(allocator, Puzzle::parse(allocator, args.inner_puzzle))? + else { + return Ok(None); + }; + + Ok(Some(Self { + launcher_id: args.singleton_struct.launcher_id, + recovery_list_hash: args.recovery_list_hash, + num_verifications_required: args.num_verifications_required, + metadata: args.metadata, + inner_puzzle, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + match DidSolution::::from_clvm(allocator, solution)? { + DidSolution::Spend(inner_solution) => { + let inner_solution = I::parse_solution(allocator, inner_solution)?; + Ok(DidSolution::Spend(inner_solution)) + } + DidSolution::Recover(recovery) => Ok(DidSolution::Recover(recovery)), + } + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.did_inner_puzzle()?, + args: DidArgs::new( + self.inner_puzzle.construct_puzzle(ctx)?, + self.recovery_list_hash, + self.num_verifications_required, + SingletonStruct::new(self.launcher_id), + &self.metadata, + ), + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + match solution { + DidSolution::Spend(inner_solution) => { + let inner_solution = self.inner_puzzle.construct_solution(ctx, inner_solution)?; + Ok(ctx.alloc(&DidSolution::Spend(inner_solution))?) + } + DidSolution::Recover(recovery) => { + Ok(ctx.alloc(&DidSolution::::Recover(recovery))?) + } + } + } +} + +impl ToTreeHash for DidLayer +where + M: ToTreeHash, + I: ToTreeHash, +{ + fn tree_hash(&self) -> TreeHash { + let inner_puzzle_hash = self.inner_puzzle.tree_hash(); + let metadata_hash = self.metadata.tree_hash(); + DidArgs::curry_tree_hash( + inner_puzzle_hash, + self.recovery_list_hash, + self.num_verifications_required, + SingletonStruct::new(self.launcher_id), + metadata_hash, + ) + } +} diff --git a/crates/chia-sdk-driver/src/layers/nft_ownership_layer.rs b/crates/chia-sdk-driver/src/layers/nft_ownership_layer.rs new file mode 100644 index 00000000..8bf2ce08 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/nft_ownership_layer.rs @@ -0,0 +1,121 @@ +use chia_protocol::Bytes32; +use chia_puzzles::nft::{ + NftOwnershipLayerArgs, NftOwnershipLayerSolution, NFT_OWNERSHIP_LAYER_PUZZLE_HASH, +}; +use clvm_traits::FromClvm; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; + +use crate::{DriverError, Layer, Puzzle, SpendContext}; + +/// The NFT ownership [`Layer`] keeps track of the current DID that owns the NFT. +/// It also contains a transfer layer, which is used to transfer ownership of the NFT. +/// The inner puzzle layer is commonly used for determining ownership (in the key sense, not DID). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NftOwnershipLayer { + /// The DID owner of this NFT, if it's currently assigned to one. + pub current_owner: Option, + /// The transfer layer, which is used to transfer ownership of the NFT. + pub transfer_layer: T, + /// The inner puzzle layer, commonly used for determining ownership. + pub inner_puzzle: I, +} + +impl NftOwnershipLayer { + pub fn new(current_owner: Option, transfer_layer: T, inner_puzzle: I) -> Self { + Self { + current_owner, + transfer_layer, + inner_puzzle, + } + } +} + +impl Layer for NftOwnershipLayer +where + T: Layer, + I: Layer, +{ + type Solution = NftOwnershipLayerSolution; + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != NFT_OWNERSHIP_LAYER_PUZZLE_HASH { + return Ok(None); + } + + let args = NftOwnershipLayerArgs::::from_clvm(allocator, puzzle.args)?; + + if args.mod_hash != NFT_OWNERSHIP_LAYER_PUZZLE_HASH.into() { + return Err(DriverError::InvalidModHash); + } + + let Some(transfer_layer) = + T::parse_puzzle(allocator, Puzzle::parse(allocator, args.transfer_program))? + else { + return Err(DriverError::NonStandardLayer); + }; + + let Some(inner_puzzle) = + I::parse_puzzle(allocator, Puzzle::parse(allocator, args.inner_puzzle))? + else { + return Ok(None); + }; + + Ok(Some(Self { + current_owner: args.current_owner, + transfer_layer, + inner_puzzle, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + let solution = NftOwnershipLayerSolution::::from_clvm(allocator, solution)?; + Ok(NftOwnershipLayerSolution { + inner_solution: I::parse_solution(allocator, solution.inner_solution)?, + }) + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.nft_ownership_layer()?, + args: NftOwnershipLayerArgs::new( + self.current_owner, + self.transfer_layer.construct_puzzle(ctx)?, + self.inner_puzzle.construct_puzzle(ctx)?, + ), + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + let inner_solution = self + .inner_puzzle + .construct_solution(ctx, solution.inner_solution)?; + ctx.alloc(&NftOwnershipLayerSolution { inner_solution }) + } +} + +impl ToTreeHash for NftOwnershipLayer +where + T: ToTreeHash, + I: ToTreeHash, +{ + fn tree_hash(&self) -> TreeHash { + NftOwnershipLayerArgs::curry_tree_hash( + self.current_owner, + self.transfer_layer.tree_hash(), + self.inner_puzzle.tree_hash(), + ) + } +} diff --git a/crates/chia-sdk-driver/src/layers/nft_state_layer.rs b/crates/chia-sdk-driver/src/layers/nft_state_layer.rs new file mode 100644 index 00000000..3e0bf078 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/nft_state_layer.rs @@ -0,0 +1,159 @@ +use chia_protocol::Bytes32; +use chia_puzzles::nft::{NftStateLayerArgs, NftStateLayerSolution, NFT_STATE_LAYER_PUZZLE_HASH}; +use chia_sdk_types::{run_puzzle, NewMetadataOutput, UpdateNftMetadata}; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; + +use crate::{DriverError, Layer, Puzzle, SpendContext}; + +/// The NFT state [`Layer`] keeps track of the current metadata of the NFT and how to change it. +/// It's typically an inner layer of the [`SingletonLayer`](crate::SingletonLayer). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NftStateLayer { + /// The NFT metadata. The standard metadata type is [`NftMetadata`](chia_puzzles::nft::NftMetadata). + pub metadata: M, + /// The tree hash of the metadata updater puzzle. + pub metadata_updater_puzzle_hash: Bytes32, + /// The inner puzzle layer. Typically, this is the [`NftOwnershipLayer`](crate::NftOwnershipLayer). + /// However, for the NFT0 standard this can be the p2 layer itself. + pub inner_puzzle: I, +} + +impl NftStateLayer { + pub fn new(metadata: M, metadata_updater_puzzle_hash: Bytes32, inner_puzzle: I) -> Self { + Self { + metadata, + metadata_updater_puzzle_hash, + inner_puzzle, + } + } + + pub fn with_metadata(self, metadata: N) -> NftStateLayer { + NftStateLayer { + metadata, + metadata_updater_puzzle_hash: self.metadata_updater_puzzle_hash, + inner_puzzle: self.inner_puzzle, + } + } +} + +impl Layer for NftStateLayer +where + M: ToClvm + FromClvm, + I: Layer, +{ + type Solution = NftStateLayerSolution; + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != NFT_STATE_LAYER_PUZZLE_HASH { + return Ok(None); + } + + let args = NftStateLayerArgs::::from_clvm(allocator, puzzle.args)?; + + if args.mod_hash != NFT_STATE_LAYER_PUZZLE_HASH.into() { + return Err(DriverError::InvalidModHash); + } + + let Some(inner_puzzle) = + I::parse_puzzle(allocator, Puzzle::parse(allocator, args.inner_puzzle))? + else { + return Ok(None); + }; + + Ok(Some(Self { + metadata: args.metadata, + metadata_updater_puzzle_hash: args.metadata_updater_puzzle_hash, + inner_puzzle, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + let solution = NftStateLayerSolution::::from_clvm(allocator, solution)?; + Ok(NftStateLayerSolution { + inner_solution: I::parse_solution(allocator, solution.inner_solution)?, + }) + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.nft_state_layer()?, + args: NftStateLayerArgs { + mod_hash: NFT_STATE_LAYER_PUZZLE_HASH.into(), + metadata: &self.metadata, + metadata_updater_puzzle_hash: self.metadata_updater_puzzle_hash, + inner_puzzle: self.inner_puzzle.construct_puzzle(ctx)?, + }, + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + let inner_solution = self + .inner_puzzle + .construct_solution(ctx, solution.inner_solution)?; + ctx.alloc(&NftStateLayerSolution { inner_solution }) + } +} + +impl ToTreeHash for NftStateLayer +where + M: ToTreeHash, + I: ToTreeHash, +{ + fn tree_hash(&self) -> TreeHash { + let metadata_hash = self.metadata.tree_hash(); + let inner_puzzle_hash = self.inner_puzzle.tree_hash(); + CurriedProgram { + program: NFT_STATE_LAYER_PUZZLE_HASH, + args: NftStateLayerArgs { + mod_hash: NFT_STATE_LAYER_PUZZLE_HASH.into(), + metadata: metadata_hash, + metadata_updater_puzzle_hash: self.metadata_updater_puzzle_hash, + inner_puzzle: inner_puzzle_hash, + }, + } + .tree_hash() + } +} + +impl NftStateLayer { + pub fn get_next_metadata( + allocator: &mut Allocator, + current_metadata: &M, + curent_metadata_updater_puzzle_hash: Bytes32, + condition: UpdateNftMetadata, + ) -> Result + where + M: ToClvm + FromClvm, + { + let real_metadata_updater_solution: Vec = vec![ + current_metadata.to_clvm(allocator)?, + curent_metadata_updater_puzzle_hash.to_clvm(allocator)?, + condition.updater_solution, + ]; + let real_metadata_updater_solution = real_metadata_updater_solution.to_clvm(allocator)?; + + let output = run_puzzle( + allocator, + condition.updater_puzzle_reveal, + real_metadata_updater_solution, + )?; + + let parsed = NewMetadataOutput::::from_clvm(allocator, output)?; + + Ok(parsed.metadata_info.new_metadata) + } +} diff --git a/crates/chia-sdk-driver/src/layers/p2_delegated_conditions_layer.rs b/crates/chia-sdk-driver/src/layers/p2_delegated_conditions_layer.rs new file mode 100644 index 00000000..a530fd6c --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/p2_delegated_conditions_layer.rs @@ -0,0 +1,100 @@ +use chia_bls::PublicKey; +use chia_sdk_types::Condition; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, TreeHash}; +use clvmr::{Allocator, NodePtr}; +use hex_literal::hex; + +use crate::{DriverError, Layer, Puzzle, SpendContext}; + +/// The p2 delegated conditions [`Layer`] allows a certain key to spend the coin. +/// To do so, a list of additional conditions is signed and passed in the solution. +/// Typically, the [`StandardLayer`](crate::StandardLayer) is used instead, since it adds more flexibility. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct P2DelegatedConditionsLayer { + /// The public key that has the ability to spend the coin. + pub public_key: PublicKey, +} + +impl Layer for P2DelegatedConditionsLayer { + type Solution = P2DelegatedConditionsSolution; + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.p2_delegated_conditions_puzzle()?, + args: P2DelegatedConditionsArgs::new(self.public_key), + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + ctx.alloc(&solution) + } + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != P2_DELEGATED_CONDITIONS_PUZZLE_HASH { + return Ok(None); + } + + let args = P2DelegatedConditionsArgs::from_clvm(allocator, puzzle.args)?; + + Ok(Some(Self { + public_key: args.public_key, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + Ok(P2DelegatedConditionsSolution::from_clvm( + allocator, solution, + )?) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(curry)] +pub struct P2DelegatedConditionsArgs { + pub public_key: PublicKey, +} + +impl P2DelegatedConditionsArgs { + pub fn new(public_key: PublicKey) -> Self { + Self { public_key } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct P2DelegatedConditionsSolution { + pub conditions: Vec>, +} + +impl P2DelegatedConditionsSolution { + pub fn new(conditions: Vec) -> Self { + Self { conditions } + } +} + +pub const P2_DELEGATED_CONDITIONS_PUZZLE: [u8; 137] = hex!( + " + ff02ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff + 04ff0bff80808080ff80808080ff0b80ffff04ffff01ff32ff02ffff03ffff07 + ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080 + ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff05 + 8080ff0180ff018080 + " +); + +pub const P2_DELEGATED_CONDITIONS_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + "0ff94726f1a8dea5c3f70d3121945190778d3b2b3fcda3735a1f290977e98341" +)); diff --git a/crates/chia-sdk-driver/src/layers/p2_delegated_singleton_layer.rs b/crates/chia-sdk-driver/src/layers/p2_delegated_singleton_layer.rs new file mode 100644 index 00000000..206d6d8c --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/p2_delegated_singleton_layer.rs @@ -0,0 +1,194 @@ +use chia_protocol::{Bytes32, Coin}; +use chia_puzzles::singleton::{SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_PUZZLE_HASH}; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; +use hex_literal::hex; + +use crate::{DriverError, Layer, Puzzle, Spend, SpendContext}; + +/// The p2 delegated singleton [`Layer`] allows for requiring that a singleton +/// be spent alongside this coin to authorize it, while also outputting conditions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct P2DelegatedSingletonLayer { + pub launcher_id: Bytes32, +} + +impl P2DelegatedSingletonLayer { + pub fn new(launcher_id: Bytes32) -> Self { + Self { launcher_id } + } + + pub fn spend( + &self, + ctx: &mut SpendContext, + coin_id: Bytes32, + singleton_inner_puzzle_hash: Bytes32, + delegated_spend: Spend, + ) -> Result { + let puzzle = self.construct_puzzle(ctx)?; + let solution = self.construct_solution( + ctx, + P2DelegatedSingletonSolution { + singleton_inner_puzzle_hash, + coin_id, + delegated_puzzle: delegated_spend.puzzle, + delegated_solution: delegated_spend.solution, + }, + )?; + Ok(Spend { puzzle, solution }) + } + + pub fn spend_coin( + &self, + ctx: &mut SpendContext, + coin: Coin, + singleton_inner_puzzle_hash: Bytes32, + delegated_spend: Spend, + ) -> Result<(), DriverError> { + let coin_spend = self.construct_coin_spend( + ctx, + coin, + P2DelegatedSingletonSolution { + singleton_inner_puzzle_hash, + coin_id: coin.coin_id(), + delegated_puzzle: delegated_spend.puzzle, + delegated_solution: delegated_spend.solution, + }, + )?; + ctx.insert(coin_spend); + Ok(()) + } +} + +impl Layer for P2DelegatedSingletonLayer { + type Solution = P2DelegatedSingletonSolution; + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != P2_DELEGATED_SINGLETON_PUZZLE_HASH { + return Ok(None); + } + + let args = P2DelegatedSingletonArgs::from_clvm(allocator, puzzle.args)?; + + if args.singleton_mod_hash != SINGLETON_TOP_LAYER_PUZZLE_HASH.into() + || args.launcher_puzzle_hash != SINGLETON_LAUNCHER_PUZZLE_HASH.into() + { + return Err(DriverError::InvalidSingletonStruct); + } + + Ok(Some(Self { + launcher_id: args.launcher_id, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + Ok(P2DelegatedSingletonSolution::from_clvm( + allocator, solution, + )?) + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.p2_delegated_singleton_puzzle()?, + args: P2DelegatedSingletonArgs::new(self.launcher_id), + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + ctx.alloc(&solution) + } +} + +impl ToTreeHash for P2DelegatedSingletonLayer { + fn tree_hash(&self) -> TreeHash { + P2DelegatedSingletonArgs::curry_tree_hash(self.launcher_id) + } +} + +// (mod (SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH singleton_inner_puzzle_hash delegated_puzzle delegated_solution my_id) + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(curry)] +pub struct P2DelegatedSingletonArgs { + pub singleton_mod_hash: Bytes32, + pub launcher_id: Bytes32, + pub launcher_puzzle_hash: Bytes32, +} + +impl P2DelegatedSingletonArgs { + pub fn new(launcher_id: Bytes32) -> Self { + Self { + singleton_mod_hash: SINGLETON_TOP_LAYER_PUZZLE_HASH.into(), + launcher_id, + launcher_puzzle_hash: SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + } + } + + pub fn curry_tree_hash(launcher_id: Bytes32) -> TreeHash { + CurriedProgram { + program: P2_DELEGATED_SINGLETON_PUZZLE_HASH, + args: Self::new(launcher_id), + } + .tree_hash() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct P2DelegatedSingletonSolution { + pub singleton_inner_puzzle_hash: Bytes32, + pub delegated_puzzle: P, + pub delegated_solution: S, + pub coin_id: Bytes32, +} + +pub const P2_DELEGATED_SINGLETON_PUZZLE: [u8; 508] = hex!( + " + ff02ffff01ff02ff16ffff04ff02ffff04ffff04ffff04ff28ffff04ffff0bff + ff02ff2effff04ff02ffff04ff05ffff04ff2fffff04ffff02ff3effff04ff02 + ffff04ffff04ff05ffff04ff0bff178080ff80808080ff808080808080ff8201 + 7f80ff808080ffff04ffff04ff14ffff04ffff02ff3effff04ff02ffff04ff5f + ff80808080ff808080ffff04ffff04ff10ffff04ff82017fff808080ff808080 + 80ffff04ffff02ff5fff81bf80ff8080808080ffff04ffff01ffffff46ff3f02 + ff3cff0401ffff01ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04 + ff0dffff04ffff0bff2affff0bff3cff2c80ffff0bff2affff0bff2affff0bff + 3cff1280ff0980ffff0bff2aff0bffff0bff3cff8080808080ff8080808080ff + ff010b80ff0180ffff02ffff03ff05ffff01ff04ff09ffff02ff16ffff04ff02 + ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ffff0bff2affff + 0bff3cff3880ffff0bff2affff0bff2affff0bff3cff1280ff0580ffff0bff2a + ffff02ff3affff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080 + ffff0bff3cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ff + ff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04 + ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 + " +); + +pub const P2_DELEGATED_SINGLETON_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + "2cadfbf73f1ff120d708ad2fefad1c78eefb8d874231bc87eac7c2df5eeb904a" +)); + +#[cfg(test)] +mod tests { + use super::*; + + use crate::assert_puzzle_hash; + + #[test] + fn test_puzzle_hash() -> anyhow::Result<()> { + assert_puzzle_hash!(P2_DELEGATED_SINGLETON_PUZZLE => P2_DELEGATED_SINGLETON_PUZZLE_HASH); + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/layers/p2_one_of_many.rs b/crates/chia-sdk-driver/src/layers/p2_one_of_many.rs new file mode 100644 index 00000000..fd638432 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/p2_one_of_many.rs @@ -0,0 +1,106 @@ +use chia_protocol::Bytes32; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, TreeHash}; +use clvmr::{Allocator, NodePtr}; +use hex_literal::hex; + +use crate::{DriverError, Layer, Puzzle, SpendContext}; + +/// The p2 1 of n [`Layer`] allows for picking from several delegated puzzles at runtime without revealing up front. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct P2OneOfMany { + /// The merkle root used to lookup the delegated puzzle as part of the solution. + pub merkle_root: Bytes32, +} + +impl Layer for P2OneOfMany { + type Solution = P2OneOfManySolution; + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != P2_ONE_OF_MANY_PUZZLE_HASH { + return Ok(None); + } + + let args = P2OneOfManyArgs::from_clvm(allocator, puzzle.args)?; + + Ok(Some(Self { + merkle_root: args.merkle_root, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + Ok(P2OneOfManySolution::::from_clvm( + allocator, solution, + )?) + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.p2_one_of_many_puzzle()?, + args: P2OneOfManyArgs { + merkle_root: self.merkle_root, + }, + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + ctx.alloc(&solution) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(curry)] +pub struct P2OneOfManyArgs { + pub merkle_root: Bytes32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct P2OneOfManySolution { + pub merkle_proof: Bytes32, + pub puzzle: P, + pub solution: S, +} + +pub const P2_ONE_OF_MANY_PUZZLE: [u8; 280] = hex!( + " + ff02ffff01ff02ffff03ffff09ff05ffff02ff06ffff04ff02ffff04ffff0bff + ff0101ffff02ff04ffff04ff02ffff04ff17ff8080808080ffff04ff0bff8080 + 80808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff04ffff01ffff + 02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff04ffff04ff02ffff04 + ff09ff80808080ffff02ff04ffff04ff02ffff04ff0dff8080808080ffff01ff + 0bffff0101ff058080ff0180ff02ffff03ff1bffff01ff02ff06ffff04ff02ff + ff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580 + ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff01 + 81ff80ff3b80ff8080808080ffff010580ff0180ff018080 + " +); + +pub const P2_ONE_OF_MANY_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + "46b29fd87fbeb6737600c4543931222a6c1ed3db6fa5601a3ca284a9f4efe780" +)); + +#[cfg(test)] +mod tests { + use super::*; + + use crate::assert_puzzle_hash; + + #[test] + fn test_puzzle_hash() -> anyhow::Result<()> { + assert_puzzle_hash!(P2_ONE_OF_MANY_PUZZLE => P2_ONE_OF_MANY_PUZZLE_HASH); + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/layers/p2_singleton.rs b/crates/chia-sdk-driver/src/layers/p2_singleton.rs new file mode 100644 index 00000000..ac8da523 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/p2_singleton.rs @@ -0,0 +1,236 @@ +use chia_protocol::{Bytes32, Coin}; +use chia_puzzles::singleton::{SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_PUZZLE_HASH}; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; +use hex_literal::hex; + +use crate::{DriverError, Layer, Puzzle, Spend, SpendContext}; + +/// The p2 singleton [`Layer`] allows for requiring that a +/// singleton be spent alongside this coin to authorize it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct P2Singleton { + pub launcher_id: Bytes32, +} + +impl P2Singleton { + pub fn new(launcher_id: Bytes32) -> Self { + Self { launcher_id } + } + + pub fn spend( + &self, + ctx: &mut SpendContext, + coin_id: Bytes32, + singleton_inner_puzzle_hash: Bytes32, + ) -> Result { + let puzzle = self.construct_puzzle(ctx)?; + let solution = self.construct_solution( + ctx, + P2SingletonSolution { + singleton_inner_puzzle_hash, + my_id: coin_id, + }, + )?; + Ok(Spend { puzzle, solution }) + } + + pub fn spend_coin( + &self, + ctx: &mut SpendContext, + coin: Coin, + singleton_inner_puzzle_hash: Bytes32, + ) -> Result<(), DriverError> { + let coin_spend = self.construct_coin_spend( + ctx, + coin, + P2SingletonSolution { + singleton_inner_puzzle_hash, + my_id: coin.coin_id(), + }, + )?; + ctx.insert(coin_spend); + Ok(()) + } +} + +impl Layer for P2Singleton { + type Solution = P2SingletonSolution; + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != P2_SINGLETON_PUZZLE_HASH { + return Ok(None); + } + + let args = P2SingletonArgs::from_clvm(allocator, puzzle.args)?; + + if args.singleton_mod_hash != SINGLETON_TOP_LAYER_PUZZLE_HASH.into() + || args.launcher_puzzle_hash != SINGLETON_LAUNCHER_PUZZLE_HASH.into() + { + return Err(DriverError::InvalidSingletonStruct); + } + + Ok(Some(Self { + launcher_id: args.launcher_id, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + Ok(P2SingletonSolution::from_clvm(allocator, solution)?) + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.p2_singleton_puzzle()?, + args: P2SingletonArgs::new(self.launcher_id), + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + ctx.alloc(&solution) + } +} + +impl ToTreeHash for P2Singleton { + fn tree_hash(&self) -> TreeHash { + P2SingletonArgs::curry_tree_hash(self.launcher_id) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(curry)] +pub struct P2SingletonArgs { + pub singleton_mod_hash: Bytes32, + pub launcher_id: Bytes32, + pub launcher_puzzle_hash: Bytes32, +} + +impl P2SingletonArgs { + pub fn new(launcher_id: Bytes32) -> Self { + Self { + singleton_mod_hash: SINGLETON_TOP_LAYER_PUZZLE_HASH.into(), + launcher_id, + launcher_puzzle_hash: SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + } + } + + pub fn curry_tree_hash(launcher_id: Bytes32) -> TreeHash { + CurriedProgram { + program: P2_SINGLETON_PUZZLE_HASH, + args: Self::new(launcher_id), + } + .tree_hash() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct P2SingletonSolution { + pub singleton_inner_puzzle_hash: Bytes32, + pub my_id: Bytes32, +} + +pub const P2_SINGLETON_PUZZLE: [u8; 403] = hex!( + " + ff02ffff01ff04ffff04ff18ffff04ffff0bffff02ff2effff04ff02ffff04ff + 05ffff04ff2fffff04ffff02ff3effff04ff02ffff04ffff04ff05ffff04ff0b + ff178080ff80808080ff808080808080ff5f80ff808080ffff04ffff04ff2cff + ff01ff248080ffff04ffff04ff10ffff04ff5fff808080ff80808080ffff04ff + ff01ffffff463fff02ff3c04ffff01ff0102ffff02ffff03ff05ffff01ff02ff + 16ffff04ff02ffff04ff0dffff04ffff0bff3affff0bff12ff3c80ffff0bff3a + ffff0bff3affff0bff12ff2a80ff0980ffff0bff3aff0bffff0bff12ff808080 + 8080ff8080808080ffff010b80ff0180ffff0bff3affff0bff12ff1480ffff0b + ff3affff0bff3affff0bff12ff2a80ff0580ffff0bff3affff02ff16ffff04ff + 02ffff04ff07ffff04ffff0bff12ff1280ff8080808080ffff0bff12ff808080 + 8080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02 + ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ff + ff01ff0bffff0101ff058080ff0180ff018080 + " +); + +pub const P2_SINGLETON_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + "40f828d8dd55603f4ff9fbf6b73271e904e69406982f4fbefae2c8dcceaf9834" +)); + +#[cfg(test)] +mod tests { + use chia_protocol::Coin; + use chia_puzzles::{singleton::SingletonSolution, EveProof, Proof}; + use chia_sdk_test::Simulator; + use chia_sdk_types::Conditions; + + use super::*; + + use crate::{assert_puzzle_hash, Launcher, SingletonLayer, SpendWithConditions, StandardLayer}; + + #[test] + fn test_puzzle_hash() -> anyhow::Result<()> { + assert_puzzle_hash!(P2_SINGLETON_PUZZLE => P2_SINGLETON_PUZZLE_HASH); + Ok(()) + } + + #[test] + fn test_p2_singleton_layer() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + let (sk, pk, puzzle_hash, coin) = sim.new_p2(2)?; + let p2 = StandardLayer::new(pk); + + let launcher = Launcher::new(coin.coin_id(), 1); + let launcher_id = launcher.coin().coin_id(); + let (create_singleton, singleton) = launcher.spend(ctx, puzzle_hash, ())?; + + let p2_singleton = P2Singleton::new(launcher_id); + let p2_singleton_hash = p2_singleton.tree_hash().into(); + + p2.spend( + ctx, + coin, + create_singleton.create_coin(p2_singleton_hash, 1, vec![launcher_id.into()]), + )?; + + let p2_coin = Coin::new(coin.coin_id(), p2_singleton_hash, 1); + p2_singleton.spend_coin(ctx, p2_coin, puzzle_hash)?; + + let inner_solution = p2 + .spend_with_conditions( + ctx, + Conditions::new() + .create_coin(puzzle_hash, 1, vec![launcher_id.into()]) + .create_puzzle_announcement(p2_coin.coin_id().into()), + )? + .solution; + let singleton_spend = SingletonLayer::new(launcher_id, p2.construct_puzzle(ctx)?) + .construct_coin_spend( + ctx, + singleton, + SingletonSolution { + lineage_proof: Proof::Eve(EveProof { + parent_parent_coin_info: coin.coin_id(), + parent_amount: 1, + }), + amount: singleton.amount, + inner_solution, + }, + )?; + ctx.insert(singleton_spend); + + sim.spend_coins(ctx.take(), &[sk])?; + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/layers/royalty_transfer_layer.rs b/crates/chia-sdk-driver/src/layers/royalty_transfer_layer.rs new file mode 100644 index 00000000..77a0ffb1 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/royalty_transfer_layer.rs @@ -0,0 +1,105 @@ +use std::convert::Infallible; + +use chia_protocol::Bytes32; +use chia_puzzles::{ + nft::{NftRoyaltyTransferPuzzleArgs, NFT_ROYALTY_TRANSFER_PUZZLE_HASH}, + singleton::{SingletonStruct, SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_PUZZLE_HASH}, +}; +use clvm_traits::FromClvm; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; + +use crate::{DriverError, Layer, Puzzle, SpendContext}; + +/// The royalty transfer [`Layer`] is used to transfer NFTs with royalties. +/// When an NFT is transferred, a percentage of the transfer amount is paid to an address. +/// This address can for example be the creator, or a royalty split puzzle. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RoyaltyTransferLayer { + /// The launcher id of the NFT this transfer program belongs to. + pub launcher_id: Bytes32, + /// The puzzle hash that receives royalties paid when transferring this NFT. + pub royalty_puzzle_hash: Bytes32, + /// The percentage of the transfer amount that is paid as royalties. + /// This is represented in ten thousandths, so a value of 300 means 3%. + pub royalty_ten_thousandths: u16, +} + +impl RoyaltyTransferLayer { + pub fn new( + launcher_id: Bytes32, + royalty_puzzle_hash: Bytes32, + royalty_ten_thousandths: u16, + ) -> Self { + Self { + launcher_id, + royalty_puzzle_hash, + royalty_ten_thousandths, + } + } +} + +impl Layer for RoyaltyTransferLayer { + type Solution = Infallible; + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.nft_royalty_transfer()?, + args: NftRoyaltyTransferPuzzleArgs { + singleton_struct: SingletonStruct::new(self.launcher_id), + royalty_puzzle_hash: self.royalty_puzzle_hash, + royalty_ten_thousandths: self.royalty_ten_thousandths, + }, + }; + ctx.alloc(&curried) + } + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != NFT_ROYALTY_TRANSFER_PUZZLE_HASH { + return Ok(None); + } + + let args = NftRoyaltyTransferPuzzleArgs::from_clvm(allocator, puzzle.args)?; + + if args.singleton_struct.mod_hash != SINGLETON_TOP_LAYER_PUZZLE_HASH.into() + || args.singleton_struct.launcher_puzzle_hash != SINGLETON_LAUNCHER_PUZZLE_HASH.into() + { + return Err(DriverError::InvalidSingletonStruct); + } + + Ok(Some(Self { + launcher_id: args.singleton_struct.launcher_id, + royalty_puzzle_hash: args.royalty_puzzle_hash, + royalty_ten_thousandths: args.royalty_ten_thousandths, + })) + } + + fn construct_solution( + &self, + _ctx: &mut SpendContext, + _solution: Self::Solution, + ) -> Result { + panic!("RoyaltyTransferLayer does not have a solution"); + } + + fn parse_solution( + _allocator: &clvmr::Allocator, + _solution: NodePtr, + ) -> Result { + panic!("RoyaltyTransferLayer does not have a solution"); + } +} + +impl ToTreeHash for RoyaltyTransferLayer { + fn tree_hash(&self) -> TreeHash { + NftRoyaltyTransferPuzzleArgs::curry_tree_hash( + self.launcher_id, + self.royalty_puzzle_hash, + self.royalty_ten_thousandths, + ) + } +} diff --git a/crates/chia-sdk-driver/src/layers/settlement_layer.rs b/crates/chia-sdk-driver/src/layers/settlement_layer.rs new file mode 100644 index 00000000..3e361170 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/settlement_layer.rs @@ -0,0 +1,39 @@ +use chia_puzzles::offer::{SettlementPaymentsSolution, SETTLEMENT_PAYMENTS_PUZZLE_HASH}; +use clvm_traits::FromClvm; +use clvmr::{Allocator, NodePtr}; + +use crate::{DriverError, Layer, Puzzle, SpendContext}; + +/// The settlement [`Layer`] is used to spend coins that are part of an offer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SettlementLayer; + +impl Layer for SettlementLayer { + type Solution = SettlementPaymentsSolution; + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + ctx.settlement_payments_puzzle() + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + ctx.alloc(&solution) + } + + fn parse_puzzle(_allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + if puzzle.curried_puzzle_hash() != SETTLEMENT_PAYMENTS_PUZZLE_HASH { + return Ok(None); + } + Ok(Some(Self)) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + Ok(FromClvm::from_clvm(allocator, solution)?) + } +} diff --git a/crates/chia-sdk-driver/src/layers/singleton_layer.rs b/crates/chia-sdk-driver/src/layers/singleton_layer.rs new file mode 100644 index 00000000..d968c942 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/singleton_layer.rs @@ -0,0 +1,142 @@ +use chia_protocol::{Bytes32, Coin}; +use chia_puzzles::{ + singleton::{ + SingletonArgs, SingletonSolution, SingletonStruct, SINGLETON_LAUNCHER_PUZZLE_HASH, + SINGLETON_TOP_LAYER_PUZZLE_HASH, + }, + LineageProof, +}; +use clvm_traits::FromClvm; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; + +use crate::{DriverError, Layer, Puzzle, SpendContext}; + +/// The singleton [`Layer`] enforces uniqueness on a coin, which is identified by the launcher id. +/// It contains an inner puzzle layer, which determines the actual behavior of the coin. +/// Only one singleton can be created when the coin is spent, preserving the lineage of the asset. +/// +/// Examples of singletons include: +/// * [`DidLayer`](crate::DidLayer) for Decentralized Identifiers (DIDs). +/// * [`NftStateLayer`](crate::NftStateLayer) for Non-Fungible Tokens (NFTs). +/// +/// However, assets like CATs (Chia Asset Tokens) are not singletons, as they are fungible. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SingletonLayer { + /// The unique launcher id for the singleton. Also referred to as the singleton id. + pub launcher_id: Bytes32, + /// The inner puzzle layer. For singletons, this determines the actual behavior of the coin. + pub inner_puzzle: I, +} + +impl SingletonLayer { + pub fn new(launcher_id: Bytes32, inner_puzzle: I) -> Self { + Self { + launcher_id, + inner_puzzle, + } + } +} + +impl Layer for SingletonLayer +where + I: Layer, +{ + type Solution = SingletonSolution; + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != SINGLETON_TOP_LAYER_PUZZLE_HASH { + return Ok(None); + } + + let args = SingletonArgs::::from_clvm(allocator, puzzle.args)?; + + if args.singleton_struct.mod_hash != SINGLETON_TOP_LAYER_PUZZLE_HASH.into() + || args.singleton_struct.launcher_puzzle_hash != SINGLETON_LAUNCHER_PUZZLE_HASH.into() + { + return Err(DriverError::InvalidSingletonStruct); + } + + let Some(inner_puzzle) = + I::parse_puzzle(allocator, Puzzle::parse(allocator, args.inner_puzzle))? + else { + return Ok(None); + }; + + Ok(Some(Self { + launcher_id: args.singleton_struct.launcher_id, + inner_puzzle, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + let solution = SingletonSolution::::from_clvm(allocator, solution)?; + let inner_solution = I::parse_solution(allocator, solution.inner_solution)?; + Ok(SingletonSolution { + lineage_proof: solution.lineage_proof, + amount: solution.amount, + inner_solution, + }) + } + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.singleton_top_layer()?, + args: SingletonArgs { + singleton_struct: SingletonStruct::new(self.launcher_id), + inner_puzzle: self.inner_puzzle.construct_puzzle(ctx)?, + }, + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + let inner_solution = self + .inner_puzzle + .construct_solution(ctx, solution.inner_solution)?; + ctx.alloc(&SingletonSolution { + lineage_proof: solution.lineage_proof, + amount: solution.amount, + inner_solution, + }) + } +} + +impl ToTreeHash for SingletonLayer +where + I: ToTreeHash, +{ + fn tree_hash(&self) -> TreeHash { + let inner_puzzle = self.inner_puzzle.tree_hash(); + SingletonArgs { + singleton_struct: SingletonStruct::new(self.launcher_id), + inner_puzzle, + } + .tree_hash() + } +} + +impl SingletonLayer +where + I: ToTreeHash, +{ + /// Returns the [`LineageProof`] for this singleton's child. + pub fn lineage_proof(&self, this_coin: Coin) -> LineageProof { + LineageProof { + parent_parent_coin_info: this_coin.parent_coin_info, + parent_inner_puzzle_hash: self.inner_puzzle.tree_hash().into(), + parent_amount: this_coin.amount, + } + } +} diff --git a/crates/chia-sdk-driver/src/layers/standard_layer.rs b/crates/chia-sdk-driver/src/layers/standard_layer.rs new file mode 100644 index 00000000..b2c98c57 --- /dev/null +++ b/crates/chia-sdk-driver/src/layers/standard_layer.rs @@ -0,0 +1,154 @@ +use chia_bls::PublicKey; +use chia_protocol::Coin; +use chia_puzzles::standard::{StandardArgs, StandardSolution, STANDARD_PUZZLE_HASH}; +use chia_sdk_types::Conditions; +use clvm_traits::{clvm_quote, FromClvm}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; + +use crate::{DriverError, Layer, Puzzle, Spend, SpendContext, SpendWithConditions}; + +/// This is the actual puzzle name for the [`StandardLayer`]. +pub type P2DelegatedOrHiddenLayer = StandardLayer; + +/// The standard [`Layer`] is used for most coins on the Chia blockchain. It allows a single key +/// to spend the coin by providing a delegated puzzle (for example to output [`Conditions`]). +/// +/// There is also additional hidden puzzle functionality which can be encoded in the key. +/// To do this, you calculate a "synthetic key" from the original key and the hidden puzzle hash. +/// When spending the coin, you can reveal this hidden puzzle and provide the original key. +/// This functionality is seldom used in Chia, and usually the "default hidden puzzle" is used instead. +/// The default hidden puzzle is not spendable, so you can only spend XCH coins by signing with your key. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct StandardLayer { + pub synthetic_key: PublicKey, +} + +impl StandardLayer { + pub fn new(synthetic_key: PublicKey) -> Self { + Self { synthetic_key } + } + + pub fn spend( + &self, + ctx: &mut SpendContext, + coin: Coin, + conditions: Conditions, + ) -> Result<(), DriverError> { + let spend = self.spend_with_conditions(ctx, conditions)?; + ctx.spend(coin, spend) + } + + pub fn delegated_inner_spend( + &self, + ctx: &mut SpendContext, + spend: Spend, + ) -> Result { + self.construct_spend( + ctx, + StandardSolution { + original_public_key: None, + delegated_puzzle: spend.puzzle, + solution: spend.solution, + }, + ) + } +} + +impl Layer for StandardLayer { + type Solution = StandardSolution; + + fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result { + let curried = CurriedProgram { + program: ctx.standard_puzzle()?, + args: StandardArgs::new(self.synthetic_key), + }; + ctx.alloc(&curried) + } + + fn construct_solution( + &self, + ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + ctx.alloc(&solution) + } + + fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> { + let Some(puzzle) = puzzle.as_curried() else { + return Ok(None); + }; + + if puzzle.mod_hash != STANDARD_PUZZLE_HASH { + return Ok(None); + } + + let args = StandardArgs::from_clvm(allocator, puzzle.args)?; + + Ok(Some(Self { + synthetic_key: args.synthetic_key, + })) + } + + fn parse_solution( + allocator: &Allocator, + solution: NodePtr, + ) -> Result { + Ok(StandardSolution::from_clvm(allocator, solution)?) + } +} + +impl SpendWithConditions for StandardLayer { + fn spend_with_conditions( + &self, + ctx: &mut SpendContext, + conditions: Conditions, + ) -> Result { + let delegated_puzzle = ctx.alloc(&clvm_quote!(conditions))?; + self.construct_spend( + ctx, + StandardSolution { + original_public_key: None, + delegated_puzzle, + solution: NodePtr::NIL, + }, + ) + } +} + +impl ToTreeHash for StandardLayer { + fn tree_hash(&self) -> TreeHash { + StandardArgs::curry_tree_hash(self.synthetic_key) + } +} + +#[cfg(test)] +mod tests { + use chia_sdk_test::Simulator; + + use super::*; + + #[test] + fn test_flash_loan() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + let (sk, pk, puzzle_hash, coin) = sim.new_p2(1)?; + let p2 = StandardLayer::new(pk); + + p2.spend( + ctx, + coin, + Conditions::new().create_coin(puzzle_hash, u64::MAX, Vec::new()), + )?; + + p2.spend( + ctx, + Coin::new(coin.coin_id(), puzzle_hash, u64::MAX), + Conditions::new().create_coin(puzzle_hash, 1, Vec::new()), + )?; + + sim.spend_coins(ctx.take(), &[sk])?; + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/lib.rs b/crates/chia-sdk-driver/src/lib.rs new file mode 100644 index 00000000..ed35b73a --- /dev/null +++ b/crates/chia-sdk-driver/src/lib.rs @@ -0,0 +1,23 @@ +#![doc = include_str!("../docs.md")] + +mod driver_error; +mod hashed_ptr; +mod layer; +mod layers; +mod merkle_tree; +mod primitives; +mod puzzle; +mod spend; +mod spend_context; +mod spend_with_conditions; + +pub use driver_error::*; +pub use hashed_ptr::*; +pub use layer::*; +pub use layers::*; +pub use merkle_tree::*; +pub use primitives::*; +pub use puzzle::*; +pub use spend::*; +pub use spend_context::*; +pub use spend_with_conditions::*; diff --git a/crates/chia-sdk-driver/src/merkle_tree.rs b/crates/chia-sdk-driver/src/merkle_tree.rs new file mode 100644 index 00000000..951fadd0 --- /dev/null +++ b/crates/chia-sdk-driver/src/merkle_tree.rs @@ -0,0 +1,171 @@ +use std::collections::HashMap; + +use chia_protocol::Bytes32; +use clvmr::sha2::Sha256; + +const HASH_TREE_PREFIX: &[u8] = &[2]; +const HASH_LEAF_PREFIX: &[u8] = &[1]; + +#[derive(Debug, Clone)] +pub struct MerkleTree { + pub root: Bytes32, + pub proofs: HashMap)>, +} + +use std::fmt::Debug; + +#[derive(Debug, Clone)] +pub enum BinaryTree { + Leaf(T), + Node(Box>, Box>), +} + +impl MerkleTree { + pub fn new(leaves: &[Bytes32]) -> Self { + if leaves.is_empty() { + return Self { + root: Bytes32::default(), + proofs: HashMap::new(), + }; + } + + let (root, proofs) = MerkleTree::build_merkle_tree(leaves); + Self { root, proofs } + } + + fn build_merkle_tree(leaves: &[Bytes32]) -> (Bytes32, HashMap)>) { + let binary_tree = MerkleTree::list_to_binary_tree(leaves); + MerkleTree::build_merkle_tree_from_binary_tree(&binary_tree) + } + + fn sha256(args: &[&[u8]]) -> Bytes32 { + let mut hasher = Sha256::new(); + args.iter().for_each(|arg| hasher.update(arg)); + + Bytes32::from(hasher.finalize()) + } + + fn list_to_binary_tree(objects: &[T]) -> BinaryTree { + let size = objects.len(); + if size == 0 { + return BinaryTree::Leaf(T::default()); + } + if size == 1 { + return BinaryTree::Leaf(objects[0].clone()); + } + let midpoint = (size + 1) >> 1; + let first_half = &objects[..midpoint]; + let last_half = &objects[midpoint..]; + let left_tree = MerkleTree::list_to_binary_tree(first_half); + let right_tree = MerkleTree::list_to_binary_tree(last_half); + BinaryTree::Node(Box::new(left_tree), Box::new(right_tree)) + } + + fn build_merkle_tree_from_binary_tree( + tuples: &BinaryTree, + ) -> (Bytes32, HashMap)>) { + match tuples { + BinaryTree::Leaf(t) => { + let hash = MerkleTree::sha256(&[HASH_LEAF_PREFIX, t]); + let mut proof = HashMap::new(); + proof.insert(*t, (0, vec![])); + (hash, proof) + } + BinaryTree::Node(left, right) => { + let (left_root, left_proofs) = MerkleTree::build_merkle_tree_from_binary_tree(left); + let (right_root, right_proofs) = + MerkleTree::build_merkle_tree_from_binary_tree(right); + + let new_root = MerkleTree::sha256(&[HASH_TREE_PREFIX, &left_root, &right_root]); + let mut new_proofs = HashMap::new(); + + for (name, (path, mut proof)) in left_proofs { + proof.push(right_root); + new_proofs.insert(name, (path, proof)); + } + + for (name, (path, mut proof)) in right_proofs { + let path = path | (1 << proof.len()); + proof.push(left_root); + new_proofs.insert(name, (path, proof)); + } + + (new_root, new_proofs) + } + } + } + + pub fn get_proof(&self, leaf: Bytes32) -> Option<(u32, Vec)> { + self.proofs.get(&leaf).cloned() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + use rstest::rstest; + + #[rstest] + #[case::no_leaves(&[], + Bytes32::default(), + vec![] + )] + #[case::one_leaf(&[Bytes32::from([1; 32])], + Bytes32::from(hex!("ce041765675ad4d93378e20bd3a7d0d97ddcf3385fb6341581b21d4bc9e3e69e")), + vec![(Bytes32::from([1; 32]), 0, vec![])] + )] + #[case::two_leaves(&[Bytes32::from([1; 32]), Bytes32::from([2; 32])], + Bytes32::from(hex!("00f2e7e0bc3ee77f0b5aa330406f69bfbd5c2e3b8a4338dba49f64bb3f0247c4")), + vec![ + (Bytes32::from([1; 32]), 0, vec![hex!("f1386fff8b06ac98d347997ff5d0abad3b977514b1b7cfe0689f45f3f1393497").into()]), + (Bytes32::from([2; 32]), 1, vec![hex!("ce041765675ad4d93378e20bd3a7d0d97ddcf3385fb6341581b21d4bc9e3e69e").into()]) + ] + )] + #[case::three_leaves(&[Bytes32::from([1; 32]), Bytes32::from([2; 32]), Bytes32::from([3; 32])], + Bytes32::from(hex!("adb439d3868b9273de8753e20a62a8e6d9ff6cfb43b189337a23df0690c7f55b")), + vec![ + (Bytes32::from([1; 32]), 0, vec![hex!("f1386fff8b06ac98d347997ff5d0abad3b977514b1b7cfe0689f45f3f1393497").into(), hex!("131c41585fc6b26c2cf8ea6fc61be03c3c4e3facb3f7e70ec69ea094b17dc3e1").into()]), + (Bytes32::from([2; 32]), 1, vec![hex!("ce041765675ad4d93378e20bd3a7d0d97ddcf3385fb6341581b21d4bc9e3e69e").into(), hex!("131c41585fc6b26c2cf8ea6fc61be03c3c4e3facb3f7e70ec69ea094b17dc3e1").into()]), + (Bytes32::from([3; 32]), 1, vec![hex!("00f2e7e0bc3ee77f0b5aa330406f69bfbd5c2e3b8a4338dba49f64bb3f0247c4").into()]) + ] + )] + #[case::seven_leaves(&[Bytes32::from([1; 32]), Bytes32::from([2; 32]), Bytes32::from([3; 32]), Bytes32::from([4; 32]), Bytes32::from([5; 32]), Bytes32::from([6; 32]), Bytes32::from([7; 32])], + Bytes32::from(hex!("1c4b11429685dd0a516282981bb3e12c13596e846f67af1da080b9134cdea4c6")), + vec![ + (Bytes32::from([1; 32]), 0, vec![hex!("f1386fff8b06ac98d347997ff5d0abad3b977514b1b7cfe0689f45f3f1393497").into(), hex!("1d85c3d5d2a5f093b49c79b2686ff698fb58d3ef4959b939ed6925dc65325499").into(), hex!("c80c9f4f69abfa70474c4d27d076ab32e23ff9bd1215fe87c6a0e6899a126d10").into()]), + (Bytes32::from([2; 32]), 1, vec![hex!("ce041765675ad4d93378e20bd3a7d0d97ddcf3385fb6341581b21d4bc9e3e69e").into(), hex!("1d85c3d5d2a5f093b49c79b2686ff698fb58d3ef4959b939ed6925dc65325499").into(), hex!("c80c9f4f69abfa70474c4d27d076ab32e23ff9bd1215fe87c6a0e6899a126d10").into()]), + (Bytes32::from([3; 32]), 2, vec![hex!("db1a2656e1809de78fb29dddf24a1c75fbf7c6dc1f1341f485457c713ce49fa0").into(), hex!("00f2e7e0bc3ee77f0b5aa330406f69bfbd5c2e3b8a4338dba49f64bb3f0247c4").into(), hex!("c80c9f4f69abfa70474c4d27d076ab32e23ff9bd1215fe87c6a0e6899a126d10").into()]), + (Bytes32::from([4; 32]), 3, vec![hex!("131c41585fc6b26c2cf8ea6fc61be03c3c4e3facb3f7e70ec69ea094b17dc3e1").into(), hex!("00f2e7e0bc3ee77f0b5aa330406f69bfbd5c2e3b8a4338dba49f64bb3f0247c4").into(), hex!("c80c9f4f69abfa70474c4d27d076ab32e23ff9bd1215fe87c6a0e6899a126d10").into()]), + (Bytes32::from([5; 32]), 4, vec![hex!("0684e189ecc12eb7472925a5b16ec60d10a476a59545452f58fcca994433a4f7").into(), hex!("d3907c0247e7e98b72338a00d87244248df71eb313589da290d45adfba44e6d2").into(), hex!("7eb919730e38f305365791a43adddeea0fc275371aac8c7b08983937beeb956f").into()]), + (Bytes32::from([6; 32]), 5, vec![hex!("90cbc3c7c7634183ae482172520c1b8d85ee10f1ca0b4744fdbe7da2245141bb").into(), hex!("d3907c0247e7e98b72338a00d87244248df71eb313589da290d45adfba44e6d2").into(), hex!("7eb919730e38f305365791a43adddeea0fc275371aac8c7b08983937beeb956f").into()]), + (Bytes32::from([7; 32]), 3, vec![hex!("3831644ba5da8ec5f16d32ef7c0a318cfec302245fac118321a5da9f43efbf94").into(), hex!("7eb919730e38f305365791a43adddeea0fc275371aac8c7b08983937beeb956f").into()]) + ] + )] + #[case::eight_leaves(&[Bytes32::from([1; 32]), Bytes32::from([2; 32]), Bytes32::from([3; 32]), Bytes32::from([4; 32]), Bytes32::from([5; 32]), Bytes32::from([6; 32]), Bytes32::from([7; 32]), Bytes32::from([8; 32])], + Bytes32::from(hex!("3023a77c57dd4c0f84fe2d9b42252e483a9974482b6d4d5fbf0e3d405a46f436")), + vec![ + (Bytes32::from([1; 32]), 0, vec![hex!("f1386fff8b06ac98d347997ff5d0abad3b977514b1b7cfe0689f45f3f1393497").into(), hex!("1d85c3d5d2a5f093b49c79b2686ff698fb58d3ef4959b939ed6925dc65325499").into(), hex!("eb06e593af742e80db1c2bef77f23c85ad87a8048bb1228037cd18d6b50f9042").into()]), + (Bytes32::from([2; 32]), 1, vec![hex!("ce041765675ad4d93378e20bd3a7d0d97ddcf3385fb6341581b21d4bc9e3e69e").into(), hex!("1d85c3d5d2a5f093b49c79b2686ff698fb58d3ef4959b939ed6925dc65325499").into(), hex!("eb06e593af742e80db1c2bef77f23c85ad87a8048bb1228037cd18d6b50f9042").into()]), + (Bytes32::from([3; 32]), 2, vec![hex!("db1a2656e1809de78fb29dddf24a1c75fbf7c6dc1f1341f485457c713ce49fa0").into(), hex!("00f2e7e0bc3ee77f0b5aa330406f69bfbd5c2e3b8a4338dba49f64bb3f0247c4").into(), hex!("eb06e593af742e80db1c2bef77f23c85ad87a8048bb1228037cd18d6b50f9042").into()]), + (Bytes32::from([4; 32]), 3, vec![hex!("131c41585fc6b26c2cf8ea6fc61be03c3c4e3facb3f7e70ec69ea094b17dc3e1").into(), hex!("00f2e7e0bc3ee77f0b5aa330406f69bfbd5c2e3b8a4338dba49f64bb3f0247c4").into(), hex!("eb06e593af742e80db1c2bef77f23c85ad87a8048bb1228037cd18d6b50f9042").into()]), + (Bytes32::from([5; 32]), 4, vec![hex!("0684e189ecc12eb7472925a5b16ec60d10a476a59545452f58fcca994433a4f7").into(), hex!("f76c002f93a1ba959ebe50568ba888a5d1871e2f804977e996bb6932f7eadf06").into(), hex!("7eb919730e38f305365791a43adddeea0fc275371aac8c7b08983937beeb956f").into()]), + (Bytes32::from([6; 32]), 5, vec![hex!("90cbc3c7c7634183ae482172520c1b8d85ee10f1ca0b4744fdbe7da2245141bb").into(), hex!("f76c002f93a1ba959ebe50568ba888a5d1871e2f804977e996bb6932f7eadf06").into(), hex!("7eb919730e38f305365791a43adddeea0fc275371aac8c7b08983937beeb956f").into()]), + (Bytes32::from([7; 32]), 6, vec![hex!("467d8acd80729c1fe2c497db207e7861b0fd9aab3552da7a2abb828a45f288cc").into(), hex!("3831644ba5da8ec5f16d32ef7c0a318cfec302245fac118321a5da9f43efbf94").into(), hex!("7eb919730e38f305365791a43adddeea0fc275371aac8c7b08983937beeb956f").into()]), + (Bytes32::from([8; 32]), 7, vec![hex!("d3907c0247e7e98b72338a00d87244248df71eb313589da290d45adfba44e6d2").into(), hex!("3831644ba5da8ec5f16d32ef7c0a318cfec302245fac118321a5da9f43efbf94").into(), hex!("7eb919730e38f305365791a43adddeea0fc275371aac8c7b08983937beeb956f").into()]) + ] + )] + fn test_merkle_tree( + #[case] leaves: &[Bytes32], + #[case] expected_root: Bytes32, + #[case] expected_proofs: Vec<(Bytes32, u32, Vec)>, + ) { + let merkle_tree = MerkleTree::new(leaves); + + assert_eq!(merkle_tree.root, expected_root); + + for (leaf, path, proof) in expected_proofs { + assert_eq!(merkle_tree.get_proof(leaf), Some((path, proof))); + } + } +} diff --git a/crates/chia-sdk-driver/src/primitives.rs b/crates/chia-sdk-driver/src/primitives.rs new file mode 100644 index 00000000..84593409 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives.rs @@ -0,0 +1,17 @@ +mod cat; +mod did; +mod intermediate_launcher; +mod launcher; +mod nft; + +pub use cat::*; +pub use did::*; +pub use intermediate_launcher::*; +pub use launcher::*; +pub use nft::*; + +#[cfg(feature = "chip-0035")] +mod datalayer; + +#[cfg(feature = "chip-0035")] +pub use datalayer::*; diff --git a/crates/chia-sdk-driver/src/primitives/cat.rs b/crates/chia-sdk-driver/src/primitives/cat.rs new file mode 100644 index 00000000..3700780e --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/cat.rs @@ -0,0 +1,543 @@ +use chia_bls::PublicKey; +use chia_protocol::{Bytes32, Coin}; +use chia_puzzles::{ + cat::{CatArgs, CatSolution, EverythingWithSignatureTailArgs, GenesisByCoinIdTailArgs}, + CoinProof, LineageProof, +}; +use chia_sdk_types::{run_puzzle, Condition, Conditions, CreateCoin}; +use clvm_traits::{clvm_quote, FromClvm}; +use clvm_utils::CurriedProgram; +use clvmr::{Allocator, NodePtr}; + +use crate::{CatLayer, DriverError, Layer, Puzzle, Spend, SpendContext}; + +mod cat_spend; +mod single_cat_spend; + +pub use cat_spend::*; +pub use single_cat_spend::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Cat { + pub coin: Coin, + pub lineage_proof: Option, + pub asset_id: Bytes32, + pub p2_puzzle_hash: Bytes32, +} + +impl Cat { + pub fn new( + coin: Coin, + lineage_proof: Option, + asset_id: Bytes32, + p2_puzzle_hash: Bytes32, + ) -> Self { + Self { + coin, + lineage_proof, + asset_id, + p2_puzzle_hash, + } + } + + pub fn single_issuance_eve( + ctx: &mut SpendContext, + parent_coin_id: Bytes32, + amount: u64, + extra_conditions: Conditions, + ) -> Result<(Conditions, Cat), DriverError> { + let genesis_by_coin_id_ptr = ctx.genesis_by_coin_id_tail_puzzle()?; + + let tail = ctx.alloc(&CurriedProgram { + program: genesis_by_coin_id_ptr, + args: GenesisByCoinIdTailArgs::new(parent_coin_id), + })?; + + Self::create_and_spend_eve( + ctx, + parent_coin_id, + ctx.tree_hash(tail).into(), + amount, + extra_conditions.run_cat_tail(tail, NodePtr::NIL), + ) + } + + pub fn multi_issuance_eve( + ctx: &mut SpendContext, + parent_coin_id: Bytes32, + public_key: PublicKey, + amount: u64, + extra_conditions: Conditions, + ) -> Result<(Conditions, Cat), DriverError> { + let everything_with_signature_ptr = ctx.everything_with_signature_tail_puzzle()?; + + let tail = ctx.alloc(&CurriedProgram { + program: everything_with_signature_ptr, + args: EverythingWithSignatureTailArgs::new(public_key), + })?; + + Self::create_and_spend_eve( + ctx, + parent_coin_id, + ctx.tree_hash(tail).into(), + amount, + extra_conditions.run_cat_tail(tail, NodePtr::NIL), + ) + } + + /// Creates and spends an eve CAT with the provided conditions. + /// To issue the CAT, you will need to reveal the TAIL puzzle and solution. + /// This can be done with the [`RunCatTail`] condition. + pub fn create_and_spend_eve( + ctx: &mut SpendContext, + parent_coin_id: Bytes32, + asset_id: Bytes32, + amount: u64, + conditions: Conditions, + ) -> Result<(Conditions, Cat), DriverError> { + let inner_puzzle = ctx.alloc(&clvm_quote!(conditions))?; + let eve_layer = CatLayer::new(asset_id, inner_puzzle); + let inner_puzzle_hash = ctx.tree_hash(inner_puzzle).into(); + let puzzle_ptr = eve_layer.construct_puzzle(ctx)?; + let puzzle_hash = ctx.tree_hash(puzzle_ptr).into(); + + let eve = Cat::new( + Coin::new(parent_coin_id, puzzle_hash, amount), + None, + asset_id, + inner_puzzle_hash, + ); + + eve.spend( + ctx, + SingleCatSpend::eve( + eve.coin, + inner_puzzle_hash, + Spend::new(inner_puzzle, NodePtr::NIL), + ), + )?; + + Ok(( + Conditions::new().create_coin(puzzle_hash, amount, Vec::new()), + eve, + )) + } + + /// Creates coin spends for one or more CATs in a ring. + /// Without the ring announcements, CAT spends cannot share inputs and outputs. + /// + /// Each item is a CAT and the inner spend for that CAT. + pub fn spend_all(ctx: &mut SpendContext, cat_spends: &[CatSpend]) -> Result<(), DriverError> { + let len = cat_spends.len(); + + let mut total_delta = 0; + + for (index, cat_spend) in cat_spends.iter().enumerate() { + let CatSpend { + cat, + inner_spend, + extra_delta, + } = cat_spend; + + // Calculate the delta and add it to the subtotal. + let output = ctx.run(inner_spend.puzzle, inner_spend.solution)?; + let conditions: Vec = ctx.extract(output)?; + + let create_coins = conditions + .into_iter() + .filter_map(|ptr| ctx.extract::(ptr).ok()); + + let delta = create_coins.fold( + i128::from(cat.coin.amount) - i128::from(*extra_delta), + |delta, create_coin| delta - i128::from(create_coin.amount), + ); + + let prev_subtotal = total_delta; + total_delta += delta; + + // Find information of neighboring coins on the ring. + let prev = &cat_spends[if index == 0 { len - 1 } else { index - 1 }]; + let next = &cat_spends[if index == len - 1 { 0 } else { index + 1 }]; + + cat.spend( + ctx, + SingleCatSpend { + inner_spend: *inner_spend, + prev_coin_id: prev.cat.coin.coin_id(), + next_coin_proof: CoinProof { + parent_coin_info: next.cat.coin.parent_coin_info, + inner_puzzle_hash: ctx.tree_hash(next.inner_spend.puzzle).into(), + amount: next.cat.coin.amount, + }, + prev_subtotal: prev_subtotal.try_into()?, + extra_delta: *extra_delta, + }, + )?; + } + + Ok(()) + } + + /// Creates a coin spend for this CAT. + pub fn spend(&self, ctx: &mut SpendContext, spend: SingleCatSpend) -> Result<(), DriverError> { + let cat_layer = CatLayer::new(self.asset_id, spend.inner_spend.puzzle); + + let puzzle = cat_layer.construct_puzzle(ctx)?; + let solution = cat_layer.construct_solution( + ctx, + CatSolution { + lineage_proof: self.lineage_proof, + prev_coin_id: spend.prev_coin_id, + this_coin_info: self.coin, + next_coin_proof: spend.next_coin_proof, + prev_subtotal: spend.prev_subtotal, + extra_delta: spend.extra_delta, + inner_puzzle_solution: spend.inner_spend.solution, + }, + )?; + + ctx.spend(self.coin, Spend::new(puzzle, solution)) + } + + /// Returns the lineage proof that would be used by each child. + pub fn child_lineage_proof(&self) -> LineageProof { + LineageProof { + parent_parent_coin_info: self.coin.parent_coin_info, + parent_inner_puzzle_hash: self.p2_puzzle_hash, + parent_amount: self.coin.amount, + } + } + + /// Creates a wrapped spendable CAT for a given output. + #[must_use] + pub fn wrapped_child(&self, p2_puzzle_hash: Bytes32, amount: u64) -> Self { + let puzzle_hash = CatArgs::curry_tree_hash(self.asset_id, p2_puzzle_hash.into()); + Self { + coin: Coin::new(self.coin.coin_id(), puzzle_hash.into(), amount), + lineage_proof: Some(self.child_lineage_proof()), + asset_id: self.asset_id, + p2_puzzle_hash, + } + } +} + +impl Cat { + pub fn parse_children( + allocator: &mut Allocator, + parent_coin: Coin, + parent_puzzle: Puzzle, + parent_solution: NodePtr, + ) -> Result>, DriverError> + where + Self: Sized, + { + let Some(parent_layer) = CatLayer::::parse_puzzle(allocator, parent_puzzle)? else { + return Ok(None); + }; + let parent_solution = CatLayer::::parse_solution(allocator, parent_solution)?; + + let output = run_puzzle( + allocator, + parent_layer.inner_puzzle.ptr(), + parent_solution.inner_puzzle_solution, + )?; + let conditions = Vec::::from_clvm(allocator, output)?; + + let outputs = conditions + .into_iter() + .filter_map(Condition::into_create_coin) + .map(|create_coin| { + // Calculate what the wrapped puzzle hash would be for the created coin. + // This is because we're running the inner layer. + let wrapped_puzzle_hash = + CatArgs::curry_tree_hash(parent_layer.asset_id, create_coin.puzzle_hash.into()); + + Self { + coin: Coin::new( + parent_coin.coin_id(), + wrapped_puzzle_hash.into(), + create_coin.amount, + ), + lineage_proof: Some(LineageProof { + parent_parent_coin_info: parent_coin.parent_coin_info, + parent_inner_puzzle_hash: parent_layer + .inner_puzzle + .curried_puzzle_hash() + .into(), + parent_amount: parent_coin.amount, + }), + asset_id: parent_layer.asset_id, + p2_puzzle_hash: create_coin.puzzle_hash, + } + }) + .collect(); + + Ok(Some(outputs)) + } +} + +#[cfg(test)] +mod tests { + use chia_consensus::gen::validation_error::ErrorCode; + use chia_puzzles::cat::EverythingWithSignatureTailArgs; + use chia_sdk_test::{Simulator, SimulatorError}; + use rstest::rstest; + + use crate::{SpendWithConditions, StandardLayer}; + + use super::*; + + #[test] + fn test_single_issuance_cat() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + let (sk, pk, puzzle_hash, coin) = sim.new_p2(1)?; + let p2 = StandardLayer::new(pk); + + let (issue_cat, cat) = Cat::single_issuance_eve( + ctx, + coin.coin_id(), + 1, + Conditions::new().create_coin(puzzle_hash, 1, vec![puzzle_hash.into()]), + )?; + p2.spend(ctx, coin, issue_cat)?; + + sim.spend_coins(ctx.take(), &[sk])?; + + let cat = cat.wrapped_child(puzzle_hash, 1); + assert_eq!(cat.p2_puzzle_hash, puzzle_hash); + assert_eq!( + cat.asset_id, + GenesisByCoinIdTailArgs::curry_tree_hash(coin.coin_id()).into() + ); + assert!(sim.coin_state(cat.coin.coin_id()).is_some()); + + Ok(()) + } + + #[test] + fn test_multi_issuance_cat() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + let (sk, pk, puzzle_hash, coin) = sim.new_p2(1)?; + let p2 = StandardLayer::new(pk); + + let (issue_cat, cat) = Cat::multi_issuance_eve( + ctx, + coin.coin_id(), + pk, + 1, + Conditions::new().create_coin(puzzle_hash, 1, vec![puzzle_hash.into()]), + )?; + p2.spend(ctx, coin, issue_cat)?; + sim.spend_coins(ctx.take(), &[sk])?; + + let cat = cat.wrapped_child(puzzle_hash, 1); + assert_eq!(cat.p2_puzzle_hash, puzzle_hash); + assert_eq!( + cat.asset_id, + EverythingWithSignatureTailArgs::curry_tree_hash(pk).into() + ); + assert!(sim.coin_state(cat.coin.coin_id()).is_some()); + + Ok(()) + } + + #[test] + fn test_missing_cat_issuance_output() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + let (sk, pk, _puzzle_hash, coin) = sim.new_p2(1)?; + let p2 = StandardLayer::new(pk); + + let (issue_cat, _cat) = + Cat::single_issuance_eve(ctx, coin.coin_id(), 1, Conditions::new())?; + p2.spend(ctx, coin, issue_cat)?; + + assert!(matches!( + sim.spend_coins(ctx.take(), &[sk]).unwrap_err(), + SimulatorError::Validation(ErrorCode::AssertCoinAnnouncementFailed) + )); + + Ok(()) + } + + #[test] + fn test_exceeded_cat_issuance_output() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + let (sk, pk, puzzle_hash, coin) = sim.new_p2(2)?; + let p2 = StandardLayer::new(pk); + + let (issue_cat, _cat) = Cat::single_issuance_eve( + ctx, + coin.coin_id(), + 1, + Conditions::new().create_coin(puzzle_hash, 2, vec![puzzle_hash.into()]), + )?; + p2.spend(ctx, coin, issue_cat)?; + + assert!(matches!( + sim.spend_coins(ctx.take(), &[sk]).unwrap_err(), + SimulatorError::Validation(ErrorCode::AssertCoinAnnouncementFailed) + )); + + Ok(()) + } + + #[rstest] + #[case(1)] + #[case(2)] + #[case(3)] + #[case(10)] + fn test_cat_spends(#[case] coins: usize) -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + // All of the amounts are different to prevent coin id collisions. + let mut amounts = Vec::with_capacity(coins); + + for amount in 0..coins { + amounts.push(amount as u64); + } + + // Create the coin with the sum of all the amounts we need to issue. + let sum = amounts.iter().sum::(); + let (sk, pk, puzzle_hash, coin) = sim.new_p2(sum)?; + let p2 = StandardLayer::new(pk); + + // Issue the CAT coins with those amounts. + let mut conditions = Conditions::new(); + + for &amount in &amounts { + conditions = conditions.create_coin(puzzle_hash, amount, vec![puzzle_hash.into()]); + } + + let (issue_cat, cat) = Cat::single_issuance_eve(ctx, coin.coin_id(), sum, conditions)?; + p2.spend(ctx, coin, issue_cat)?; + + sim.spend_coins(ctx.take(), &[sk.clone()])?; + + let mut cats: Vec = amounts + .into_iter() + .map(|amount| cat.wrapped_child(puzzle_hash, amount)) + .collect(); + + // Spend the CAT coins a few times. + for _ in 0..3 { + let cat_spends: Vec = cats + .iter() + .map(|cat| { + Ok(CatSpend::new( + *cat, + p2.spend_with_conditions( + ctx, + Conditions::new().create_coin( + puzzle_hash, + cat.coin.amount, + vec![puzzle_hash.into()], + ), + )?, + )) + }) + .collect::>()?; + + Cat::spend_all(ctx, &cat_spends)?; + sim.spend_coins(ctx.take(), &[sk.clone()])?; + + // Update the cats to the children. + cats = cats + .into_iter() + .map(|cat| cat.wrapped_child(puzzle_hash, cat.coin.amount)) + .collect(); + } + + Ok(()) + } + + #[test] + fn test_different_cat_p2_puzzles() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + let (sk, pk, puzzle_hash, coin) = sim.new_p2(2)?; + let p2 = StandardLayer::new(pk); + + // This will just return the solution verbatim. + let custom_p2 = ctx.alloc(&1)?; + let custom_p2_puzzle_hash = ctx.tree_hash(custom_p2).into(); + + let (issue_cat, cat) = Cat::single_issuance_eve( + ctx, + coin.coin_id(), + 2, + Conditions::new() + .create_coin(puzzle_hash, 1, vec![puzzle_hash.into()]) + .create_coin(custom_p2_puzzle_hash, 1, vec![custom_p2_puzzle_hash.into()]), + )?; + p2.spend(ctx, coin, issue_cat)?; + sim.spend_coins(ctx.take(), &[sk.clone()])?; + + let spends = [ + CatSpend::new( + cat.wrapped_child(puzzle_hash, 1), + p2.spend_with_conditions( + ctx, + Conditions::new().create_coin(puzzle_hash, 1, vec![puzzle_hash.into()]), + )?, + ), + CatSpend::new( + cat.wrapped_child(custom_p2_puzzle_hash, 1), + Spend::new( + custom_p2, + ctx.alloc(&[CreateCoin::new( + custom_p2_puzzle_hash, + 1, + vec![custom_p2_puzzle_hash.into()], + )])?, + ), + ), + ]; + + Cat::spend_all(ctx, &spends)?; + sim.spend_coins(ctx.take(), &[sk])?; + + Ok(()) + } + + #[test] + fn test_cat_melt() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + let (sk, pk, puzzle_hash, coin) = sim.new_p2(10000)?; + let p2 = StandardLayer::new(pk); + + let conditions = + Conditions::new().create_coin(puzzle_hash, 10000, vec![puzzle_hash.into()]); + let (issue_cat, cat) = Cat::multi_issuance_eve(ctx, coin.coin_id(), pk, 10000, conditions)?; + p2.spend(ctx, coin, issue_cat)?; + + let everything_with_signature_ptr = ctx.everything_with_signature_tail_puzzle()?; + + let tail = ctx.alloc(&CurriedProgram { + program: everything_with_signature_ptr, + args: EverythingWithSignatureTailArgs::new(pk), + })?; + + let cat_spend = CatSpend::with_extra_delta( + cat.wrapped_child(puzzle_hash, 10000), + p2.spend_with_conditions( + ctx, + Conditions::new() + .create_coin(puzzle_hash, 7000, vec![puzzle_hash.into()]) + .run_cat_tail(tail, NodePtr::NIL), + )?, + -3000, + ); + + Cat::spend_all(ctx, &[cat_spend])?; + + sim.spend_coins(ctx.take(), &[sk])?; + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/cat/cat_spend.rs b/crates/chia-sdk-driver/src/primitives/cat/cat_spend.rs new file mode 100644 index 00000000..df54c682 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/cat/cat_spend.rs @@ -0,0 +1,28 @@ +use crate::Spend; + +use super::Cat; + +#[derive(Debug, Clone, Copy)] +pub struct CatSpend { + pub cat: Cat, + pub inner_spend: Spend, + pub extra_delta: i64, +} + +impl CatSpend { + pub fn new(cat: Cat, inner_spend: Spend) -> Self { + Self { + cat, + inner_spend, + extra_delta: 0, + } + } + + pub fn with_extra_delta(cat: Cat, inner_spend: Spend, extra_delta: i64) -> Self { + Self { + cat, + inner_spend, + extra_delta, + } + } +} diff --git a/crates/chia-sdk-driver/src/primitives/cat/single_cat_spend.rs b/crates/chia-sdk-driver/src/primitives/cat/single_cat_spend.rs new file mode 100644 index 00000000..4ada1bd6 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/cat/single_cat_spend.rs @@ -0,0 +1,29 @@ +use chia_protocol::{Bytes32, Coin}; +use chia_puzzles::CoinProof; + +use crate::Spend; + +#[derive(Debug, Clone, Copy)] +pub struct SingleCatSpend { + pub prev_coin_id: Bytes32, + pub next_coin_proof: CoinProof, + pub prev_subtotal: i64, + pub extra_delta: i64, + pub inner_spend: Spend, +} + +impl SingleCatSpend { + pub fn eve(coin: Coin, inner_puzzle_hash: Bytes32, inner_spend: Spend) -> Self { + Self { + prev_coin_id: coin.coin_id(), + next_coin_proof: CoinProof { + parent_coin_info: coin.parent_coin_info, + inner_puzzle_hash, + amount: coin.amount, + }, + prev_subtotal: 0, + extra_delta: 0, + inner_spend, + } + } +} diff --git a/crates/chia-sdk-driver/src/primitives/datalayer.rs b/crates/chia-sdk-driver/src/primitives/datalayer.rs new file mode 100644 index 00000000..d5d270ac --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/datalayer.rs @@ -0,0 +1,6 @@ +mod datastore; +mod datastore_info; +mod datastore_launcher; + +pub use datastore::*; +pub use datastore_info::*; diff --git a/crates/chia-sdk-driver/src/primitives/datalayer/datastore.rs b/crates/chia-sdk-driver/src/primitives/datalayer/datastore.rs new file mode 100644 index 00000000..6324b771 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/datalayer/datastore.rs @@ -0,0 +1,2196 @@ +use chia_protocol::{Bytes, Bytes32, Coin, CoinSpend}; +use chia_puzzles::{ + nft::{NftStateLayerArgs, NftStateLayerSolution, NFT_STATE_LAYER_PUZZLE_HASH}, + singleton::{ + LauncherSolution, SingletonArgs, SingletonSolution, SINGLETON_LAUNCHER_PUZZLE_HASH, + }, + EveProof, LineageProof, Proof, +}; +use chia_sdk_types::{run_puzzle, CreateCoin, NewMetadataInfo, NewMetadataOutput}; +use chia_sdk_types::{Condition, UpdateNftMetadata}; +use clvm_traits::{FromClvm, FromClvmError, ToClvm}; +use clvm_utils::{tree_hash, CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; +use num_bigint::BigInt; + +use crate::{ + DelegationLayerArgs, DelegationLayerSolution, DriverError, Layer, NftStateLayer, Puzzle, + SingletonLayer, Spend, SpendContext, DELEGATION_LAYER_PUZZLE_HASH, + DL_METADATA_UPDATER_PUZZLE_HASH, +}; + +use super::{ + get_merkle_tree, DataStoreInfo, DataStoreMetadata, DelegatedPuzzle, HintType, + MetadataWithRootHash, +}; + +/// Everything that is required to spend a [`DataStore`] coin. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DataStore { + /// The coin that holds this [`DataStore`]. + pub coin: Coin, + /// The lineage proof for the singletonlayer. + pub proof: Proof, + /// The info associated with the [`DataStore`], including the metadata. + pub info: DataStoreInfo, +} + +impl DataStore +where + M: ToClvm + FromClvm, +{ + pub fn new(coin: Coin, proof: Proof, info: DataStoreInfo) -> Self { + DataStore { coin, proof, info } + } + + /// Creates a coin spend for this [`DataStore`]. + pub fn spend(self, ctx: &mut SpendContext, inner_spend: Spend) -> Result + where + M: Clone, + { + let (puzzle_ptr, solution_ptr) = if self.info.delegated_puzzles.is_empty() { + let layers = self + .info + .clone() + .into_layers_without_delegation_layer(inner_spend.puzzle); + + let solution_ptr = layers.construct_solution( + ctx, + SingletonSolution { + lineage_proof: self.proof, + amount: self.coin.amount, + inner_solution: NftStateLayerSolution { + inner_solution: inner_spend.solution, + }, + }, + )?; + + (layers.construct_puzzle(ctx)?, solution_ptr) + } else { + let layers = self.info.clone().into_layers_with_delegation_layer(ctx)?; + let puzzle_ptr = layers.construct_puzzle(ctx)?; + + let delegated_puzzle_hash = tree_hash(&ctx.allocator, inner_spend.puzzle); + + let tree = get_merkle_tree(ctx, self.info.delegated_puzzles)?; + + let inner_solution = DelegationLayerSolution { + // if running owner puzzle, the line below will return 'None', thus ensuring correct puzzle behavior + merkle_proof: tree.get_proof(delegated_puzzle_hash.into()), + puzzle_reveal: inner_spend.puzzle, + puzzle_solution: inner_spend.solution, + }; + + let solution_ptr = layers.construct_solution( + ctx, + SingletonSolution { + lineage_proof: self.proof, + amount: self.coin.amount, + inner_solution: NftStateLayerSolution { inner_solution }, + }, + )?; + (puzzle_ptr, solution_ptr) + }; + + let puzzle = ctx.serialize(&puzzle_ptr)?; + let solution = ctx.serialize(&solution_ptr)?; + + Ok(CoinSpend::new(self.coin, puzzle, solution)) + } + + /// Returns the lineage proof that would be used by the child. + pub fn child_lineage_proof(&self, ctx: &mut SpendContext) -> Result { + Ok(LineageProof { + parent_parent_coin_info: self.coin.parent_coin_info, + parent_inner_puzzle_hash: self.info.inner_puzzle_hash(ctx)?.into(), + parent_amount: self.coin.amount, + }) + } +} + +#[derive(ToClvm, FromClvm, Debug, Clone, PartialEq, Eq)] +#[clvm(list)] +pub struct DlLauncherKvList { + pub metadata: M, + pub state_layer_inner_puzzle_hash: Bytes32, + #[clvm(rest)] + pub memos: Vec, +} + +#[derive(ToClvm, FromClvm, Debug, Clone, PartialEq, Eq)] +#[clvm(list)] +pub struct OldDlLauncherKvList { + pub root_hash: Bytes32, + pub state_layer_inner_puzzle_hash: Bytes32, + #[clvm(rest)] + pub memos: Vec, +} + +// Does not implement Primitive because it needs extra info. +impl DataStore +where + M: ToClvm + FromClvm + MetadataWithRootHash, +{ + pub fn build_datastore( + coin: Coin, + launcher_id: Bytes32, + proof: Proof, + metadata: M, + fallback_owner_ph: Bytes32, + memos: Vec, + ) -> Result { + let mut memos = memos; + + if memos.is_empty() { + // no hints; owner puzzle hash is the inner puzzle hash + return Ok(DataStore { + coin, + proof, + info: DataStoreInfo { + launcher_id, + metadata, + owner_puzzle_hash: fallback_owner_ph, + delegated_puzzles: vec![], + }, + }); + } + + if memos.drain(0..1).next().ok_or(DriverError::MissingMemo)? != launcher_id.into() { + return Err(DriverError::InvalidMemo); + } + + if memos.len() == 2 && memos[0] == metadata.root_hash().into() { + // vanilla store using old memo format + let owner_puzzle_hash = Bytes32::new( + memos[1] + .to_vec() + .try_into() + .map_err(|_| DriverError::InvalidMemo)?, + ); + return Ok(DataStore { + coin, + proof, + info: DataStoreInfo { + launcher_id, + metadata, + owner_puzzle_hash, + delegated_puzzles: vec![], + }, + }); + } + + let owner_puzzle_hash: Bytes32 = if memos.is_empty() { + fallback_owner_ph + } else { + Bytes32::new( + memos + .drain(0..1) + .next() + .ok_or(DriverError::MissingMemo)? + .to_vec() + .try_into() + .map_err(|_| DriverError::InvalidMemo)?, + ) + }; + + let mut delegated_puzzles = vec![]; + while memos.len() > 1 { + delegated_puzzles.push(DelegatedPuzzle::from_memos(&mut memos)?); + } + + Ok(DataStore { + coin, + proof, + info: DataStoreInfo { + launcher_id, + metadata, + owner_puzzle_hash, + delegated_puzzles, + }, + }) + } + + pub fn from_spend( + allocator: &mut Allocator, + cs: &CoinSpend, + parent_delegated_puzzles: &[DelegatedPuzzle], + ) -> Result, DriverError> + where + Self: Sized, + { + let solution_node_ptr = cs + .solution + .to_clvm(allocator) + .map_err(DriverError::ToClvm)?; + + if cs.coin.puzzle_hash == SINGLETON_LAUNCHER_PUZZLE_HASH.into() { + // we're just launching this singleton :) + // solution is (singleton_full_puzzle_hash amount key_value_list) + // kv_list is (metadata state_layer_hash) + let launcher_id = cs.coin.coin_id(); + + let proof = Proof::Eve(EveProof { + parent_parent_coin_info: cs.coin.parent_coin_info, + parent_amount: cs.coin.amount, + }); + + let solution = LauncherSolution::>::from_clvm( + allocator, + solution_node_ptr, + ); + + return match solution { + Ok(solution) => { + let metadata = solution.key_value_list.metadata; + + let new_coin = Coin { + parent_coin_info: launcher_id, + puzzle_hash: solution.singleton_puzzle_hash, + amount: solution.amount, + }; + + let mut memos: Vec = vec![launcher_id.into()]; + memos.extend(solution.key_value_list.memos); + + Ok(Some(Self::build_datastore( + new_coin, + launcher_id, + proof, + metadata, + solution.key_value_list.state_layer_inner_puzzle_hash, + memos, + )?)) + } + Err(err) => match err { + FromClvmError::ExpectedPair => { + // datastore launched using old memo format + let solution = LauncherSolution::>::from_clvm( + allocator, + solution_node_ptr, + )?; + + let coin = Coin { + parent_coin_info: launcher_id, + puzzle_hash: solution.singleton_puzzle_hash, + amount: solution.amount, + }; + + Ok(Some(Self::build_datastore( + coin, + launcher_id, + proof, + M::root_hash_only(solution.key_value_list.root_hash), + solution.key_value_list.state_layer_inner_puzzle_hash, + solution.key_value_list.memos, + )?)) + } + _ => Err(DriverError::FromClvm(err)), + }, + }; + } + + let parent_puzzle_ptr = cs + .puzzle_reveal + .to_clvm(allocator) + .map_err(DriverError::ToClvm)?; + let parent_puzzle = Puzzle::parse(allocator, parent_puzzle_ptr); + + let Some(singleton_layer) = + SingletonLayer::::parse_puzzle(allocator, parent_puzzle)? + else { + return Ok(None); + }; + + let Some(state_layer) = + NftStateLayer::::parse_puzzle(allocator, singleton_layer.inner_puzzle)? + else { + return Ok(None); + }; + + let parent_solution_ptr = cs.solution.to_clvm(allocator)?; + let parent_solution = SingletonLayer::>::parse_solution( + allocator, + parent_solution_ptr, + )?; + + // At this point, inner puzzle might be either a delegation layer or just an ownership layer. + let inner_puzzle = state_layer.inner_puzzle.ptr(); + let inner_solution = parent_solution.inner_solution.inner_solution; + + let inner_output = run_puzzle(allocator, inner_puzzle, inner_solution)?; + let inner_conditions = Vec::::from_clvm(allocator, inner_output)?; + + let mut inner_create_coin_condition = None; + let mut inner_new_metadata_condition = None; + + for condition in inner_conditions { + match condition { + Condition::CreateCoin(condition) if condition.amount % 2 == 1 => { + inner_create_coin_condition = Some(condition); + } + Condition::UpdateNftMetadata(condition) => { + inner_new_metadata_condition = Some(condition); + } + _ => {} + } + } + + let Some(inner_create_coin_condition) = inner_create_coin_condition else { + return Err(DriverError::MissingChild); + }; + + let new_metadata = if let Some(inner_new_metadata_condition) = inner_new_metadata_condition + { + NftStateLayer::::get_next_metadata( + allocator, + &state_layer.metadata, + state_layer.metadata_updater_puzzle_hash, + inner_new_metadata_condition, + )? + } else { + state_layer.metadata + }; + + // first, just compute new coin info - will be used in any case + + let new_metadata_ptr = new_metadata.to_clvm(allocator)?; + let new_puzzle_hash = SingletonArgs::curry_tree_hash( + singleton_layer.launcher_id, + CurriedProgram { + program: NFT_STATE_LAYER_PUZZLE_HASH, + args: NftStateLayerArgs:: { + mod_hash: NFT_STATE_LAYER_PUZZLE_HASH.into(), + metadata: tree_hash(allocator, new_metadata_ptr), + metadata_updater_puzzle_hash: state_layer.metadata_updater_puzzle_hash, + inner_puzzle: inner_create_coin_condition.puzzle_hash.into(), + }, + } + .tree_hash(), + ); + + let new_coin = Coin { + parent_coin_info: cs.coin.coin_id(), + puzzle_hash: new_puzzle_hash.into(), + amount: inner_create_coin_condition.amount, + }; + + // if the coin was re-created with memos, there is a delegation layer + // and delegated puzzles have been updated (we can rebuild the list from memos) + if inner_create_coin_condition.memos.len() > 1 { + // keep in mind that there's always the launcher id memo being added + return Ok(Some(Self::build_datastore( + new_coin, + singleton_layer.launcher_id, + Proof::Lineage(singleton_layer.lineage_proof(cs.coin)), + new_metadata, + state_layer.inner_puzzle.tree_hash().into(), + inner_create_coin_condition.memos, + )?)); + } + + let mut owner_puzzle_hash: Bytes32 = state_layer.inner_puzzle.tree_hash().into(); + + // does the parent coin currently have a delegation layer? + let delegation_layer_maybe = state_layer.inner_puzzle; + if delegation_layer_maybe.is_curried() + && delegation_layer_maybe.mod_hash() == DELEGATION_LAYER_PUZZLE_HASH + { + let deleg_puzzle_args = DelegationLayerArgs::from_clvm( + allocator, + delegation_layer_maybe + .as_curried() + .ok_or(DriverError::NonStandardLayer)? + .args, + ) + .map_err(DriverError::FromClvm)?; + owner_puzzle_hash = deleg_puzzle_args.owner_puzzle_hash; + + let delegation_layer_solution = + DelegationLayerSolution::::from_clvm(allocator, inner_solution)?; + + // to get more info, we'll need to run the delegated puzzle (delegation layer's "inner" puzzle) + let output = run_puzzle( + allocator, + delegation_layer_solution.puzzle_reveal, + delegation_layer_solution.puzzle_solution, + )?; + + let odd_create_coin = Vec::::from_clvm(allocator, output)? + .iter() + .map(|cond| Condition::::from_clvm(allocator, *cond)) + .find(|cond| match cond { + Ok(Condition::CreateCoin(create_coin)) => create_coin.amount % 2 == 1, + _ => false, + }); + + let Some(odd_create_coin) = odd_create_coin else { + // no CREATE_COIN was created by the innermost puzzle + // delegation layer therefore added one (assuming the spend is valid)] + return Ok(Some(DataStore { + coin: new_coin, + proof: Proof::Lineage(singleton_layer.lineage_proof(cs.coin)), + info: DataStoreInfo { + launcher_id: singleton_layer.launcher_id, + metadata: new_metadata, + owner_puzzle_hash, + delegated_puzzles: parent_delegated_puzzles.to_vec(), + }, + })); + }; + + let odd_create_coin = odd_create_coin?; + + // if there were any memos, the if above would have caught it since it processes + // output conditions of the state layer inner puzzle (i.e., it runs the delegation layer) + // therefore, this spend is either 'exiting' the delegation layer or re-creatign it + if let Condition::CreateCoin(create_coin) = odd_create_coin { + let prev_deleg_layer_ph = delegation_layer_maybe.tree_hash(); + + if create_coin.puzzle_hash == prev_deleg_layer_ph.into() { + // owner is re-creating the delegation layer with the same options + return Ok(Some(DataStore { + coin: new_coin, + proof: Proof::Lineage(singleton_layer.lineage_proof(cs.coin)), + info: DataStoreInfo { + launcher_id: singleton_layer.launcher_id, + metadata: new_metadata, + owner_puzzle_hash, // owner puzzle was ran + delegated_puzzles: parent_delegated_puzzles.to_vec(), + }, + })); + } + + // owner is exiting the delegation layer + owner_puzzle_hash = create_coin.puzzle_hash; + } + } + + // all methods exhausted; this coin doesn't seem to have a delegation layer + Ok(Some(DataStore { + coin: new_coin, + proof: Proof::Lineage(singleton_layer.lineage_proof(cs.coin)), + info: DataStoreInfo { + launcher_id: singleton_layer.launcher_id, + metadata: new_metadata, + owner_puzzle_hash, + delegated_puzzles: vec![], + }, + })) + } +} + +impl DataStore { + pub fn get_recreation_memos( + launcher_id: Bytes32, + owner_puzzle_hash: TreeHash, + delegated_puzzles: Vec, + ) -> Vec { + let owner_puzzle_hash: Bytes32 = owner_puzzle_hash.into(); + let mut memos: Vec = vec![launcher_id.into(), owner_puzzle_hash.into()]; + + for delegated_puzzle in delegated_puzzles { + match delegated_puzzle { + DelegatedPuzzle::Admin(inner_puzzle_hash) => { + memos.push(Bytes::new([HintType::AdminPuzzle as u8].into())); + memos.push(Bytes32::from(inner_puzzle_hash).into()); + } + DelegatedPuzzle::Writer(inner_puzzle_hash) => { + memos.push(Bytes::new([HintType::WriterPuzzle as u8].into())); + memos.push(Bytes32::from(inner_puzzle_hash).into()); + } + DelegatedPuzzle::Oracle(oracle_puzzle_hash, oracle_fee) => { + memos.push(Bytes::new([HintType::OraclePuzzle as u8].into())); + memos.push(oracle_puzzle_hash.into()); + + let fee_bytes = BigInt::from(oracle_fee).to_signed_bytes_be(); + let mut fee_bytes = fee_bytes.as_slice(); + + // https://github.com/Chia-Network/clvm_rs/blob/66a17f9576d26011321bb4c8c16eb1c63b169f1f/src/allocator.rs#L295 + while (!fee_bytes.is_empty()) && (fee_bytes[0] == 0) { + if fee_bytes.len() > 1 && (fee_bytes[1] & 0x80 == 0x80) { + break; + } + fee_bytes = &fee_bytes[1..]; + } + + memos.push(fee_bytes.into()); + } + } + } + + memos + } + + // As an owner use CREATE_COIN to: + // - just re-create store (no hints needed) + // - change delegated puzzles (hints needed) + pub fn owner_create_coin_condition( + ctx: &mut SpendContext, + launcher_id: Bytes32, + new_inner_puzzle_hash: Bytes32, + new_delegated_puzzles: Vec, + hint_delegated_puzzles: bool, + ) -> Result { + let new_puzzle_hash = if new_delegated_puzzles.is_empty() { + new_inner_puzzle_hash + } else { + let new_merkle_root = get_merkle_tree(ctx, new_delegated_puzzles.clone())?.root; + DelegationLayerArgs::curry_tree_hash( + launcher_id, + new_inner_puzzle_hash, + new_merkle_root, + ) + .into() + }; + + Ok(Condition::CreateCoin(CreateCoin { + amount: 1, + puzzle_hash: new_puzzle_hash, + memos: if hint_delegated_puzzles { + Self::get_recreation_memos( + launcher_id, + new_inner_puzzle_hash.into(), + new_delegated_puzzles, + ) + } else { + vec![launcher_id.into()] + }, + })) + } + + pub fn new_metadata_condition( + ctx: &mut SpendContext, + new_metadata: M, + ) -> Result + where + M: ToClvm, + { + let new_metadata_condition = UpdateNftMetadata::> { + updater_puzzle_reveal: 11, + // metadata updater will just return solution, so we can set the solution to NewMetadataOutput :) + updater_solution: NewMetadataOutput { + metadata_info: NewMetadataInfo:: { + new_metadata, + new_updater_puzzle_hash: DL_METADATA_UPDATER_PUZZLE_HASH.into(), + }, + conditions: (), + }, + } + .to_clvm(&mut ctx.allocator)?; + + Ok(Condition::Other(new_metadata_condition)) + } +} + +#[allow(clippy::type_complexity)] +#[allow(clippy::too_many_arguments)] +#[cfg(test)] +pub mod tests { + use core::panic; + + use chia_bls::{PublicKey, SecretKey}; + use chia_puzzles::standard::StandardArgs; + use chia_sdk_test::{test_secret_keys, Simulator}; + use chia_sdk_types::{Conditions, MeltSingleton, UpdateDataStoreMerkleRoot}; + use clvmr::sha2::Sha256; + use rstest::rstest; + + use crate::{ + DelegationLayer, Launcher, OracleLayer, SpendWithConditions, StandardLayer, WriterLayer, + }; + + use super::*; + + #[derive(Debug, PartialEq, Copy, Clone)] + pub enum Label { + None, + Some, + New, + } + + impl Label { + pub fn value(&self) -> Option { + match self { + Label::None => None, + Label::Some => Some(String::from("label")), + Label::New => Some(String::from("new_label")), + } + } + } + + #[derive(Debug, PartialEq, Copy, Clone)] + pub enum Description { + None, + Some, + New, + } + + impl Description { + pub fn value(&self) -> Option { + match self { + Description::None => None, + Description::Some => Some(String::from("description")), + Description::New => Some(String::from("new_description")), + } + } + } + + #[derive(Debug, PartialEq, Copy, Clone)] + pub enum RootHash { + Zero, + Some, + } + + impl RootHash { + pub fn value(&self) -> Bytes32 { + match self { + RootHash::Zero => Bytes32::from([0; 32]), + RootHash::Some => Bytes32::from([1; 32]), + } + } + } + + #[derive(Debug, PartialEq, Copy, Clone)] + pub enum ByteSize { + None, + Some, + New, + } + + impl ByteSize { + pub fn value(&self) -> Option { + match self { + ByteSize::None => None, + ByteSize::Some => Some(1337), + ByteSize::New => Some(42), + } + } + } + + pub fn metadata_from_tuple(t: (RootHash, Label, Description, ByteSize)) -> DataStoreMetadata { + DataStoreMetadata { + root_hash: t.0.value(), + label: t.1.value(), + description: t.2.value(), + bytes: t.3.value(), + } + } + + #[test] + fn test_simple_datastore() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + + let [sk]: [SecretKey; 1] = test_secret_keys(1)?.try_into().unwrap(); + let pk = sk.public_key(); + let p2 = StandardLayer::new(pk); + + let puzzle_hash = StandardArgs::curry_tree_hash(pk).into(); + let coin = sim.new_coin(puzzle_hash, 1); + + let ctx = &mut SpendContext::new(); + + let (launch_singleton, datastore) = Launcher::new(coin.coin_id(), 1).mint_datastore( + ctx, + DataStoreMetadata::root_hash_only(RootHash::Zero.value()), + puzzle_hash.into(), + vec![], + )?; + p2.spend(ctx, coin, launch_singleton)?; + + let spends = ctx.take(); + for spend in spends { + if spend.coin.coin_id() == datastore.info.launcher_id { + let new_datastore = + DataStore::from_spend(&mut ctx.allocator, &spend, &[])?.unwrap(); + + assert_eq!(datastore, new_datastore); + } + + ctx.insert(spend); + } + + let datastore_inner_spend = StandardLayer::new(pk) + .spend_with_conditions(ctx, Conditions::new().create_coin(puzzle_hash, 1, vec![]))?; + + let old_datastore_coin = datastore.coin; + let new_spend = datastore.spend(ctx, datastore_inner_spend)?; + + ctx.insert(new_spend); + + sim.spend_coins(ctx.take(), &[sk])?; + + // Make sure the datastore was created. + let coin_state = sim + .coin_state(old_datastore_coin.coin_id()) + .expect("expected datastore coin"); + assert_eq!(coin_state.coin, old_datastore_coin); + assert!(coin_state.spent_height.is_some()); + + Ok(()) + } + + #[allow(clippy::similar_names)] + #[test] + fn test_datastore_with_delegation_layer() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + + let [owner_sk, admin_sk, writer_sk]: [SecretKey; 3] = + test_secret_keys(3)?.try_into().unwrap(); + + let owner_pk = owner_sk.public_key(); + let admin_pk = admin_sk.public_key(); + let writer_pk = writer_sk.public_key(); + + let oracle_puzzle_hash: Bytes32 = [1; 32].into(); + let oracle_fee = 1000; + + let owner_puzzle_hash = StandardArgs::curry_tree_hash(owner_pk).into(); + let coin = sim.new_coin(owner_puzzle_hash, 1); + + let ctx = &mut SpendContext::new(); + + let admin_puzzle: NodePtr = CurriedProgram { + program: ctx.standard_puzzle()?, + args: StandardArgs::new(admin_pk), + } + .to_clvm(&mut ctx.allocator)?; + let admin_puzzle_hash = tree_hash(&ctx.allocator, admin_puzzle); + + let writer_inner_puzzle: NodePtr = CurriedProgram { + program: ctx.standard_puzzle()?, + args: StandardArgs::new(writer_pk), + } + .to_clvm(&mut ctx.allocator)?; + let writer_inner_puzzle_hash = tree_hash(&ctx.allocator, writer_inner_puzzle); + + let admin_delegated_puzzle = DelegatedPuzzle::Admin(admin_puzzle_hash); + let writer_delegated_puzzle = DelegatedPuzzle::Writer(writer_inner_puzzle_hash); + + let oracle_delegated_puzzle = DelegatedPuzzle::Oracle(oracle_puzzle_hash, oracle_fee); + + let (launch_singleton, datastore) = Launcher::new(coin.coin_id(), 1).mint_datastore( + ctx, + DataStoreMetadata::default(), + owner_puzzle_hash.into(), + vec![ + admin_delegated_puzzle, + writer_delegated_puzzle, + oracle_delegated_puzzle, + ], + )?; + StandardLayer::new(owner_pk).spend(ctx, coin, launch_singleton)?; + + let spends = ctx.take(); + for spend in spends { + if spend.coin.coin_id() == datastore.info.launcher_id { + let new_datastore = + DataStore::from_spend(&mut ctx.allocator, &spend, &[])?.unwrap(); + + assert_eq!(datastore, new_datastore); + } + + ctx.insert(spend); + } + + assert_eq!(datastore.info.metadata.root_hash, RootHash::Zero.value()); + + // writer: update metadata + let new_metadata = metadata_from_tuple(( + RootHash::Some, + Label::Some, + Description::Some, + ByteSize::Some, + )); + + let new_metadata_condition = DataStore::new_metadata_condition(ctx, new_metadata.clone())?; + + let inner_spend = WriterLayer::new(StandardLayer::new(writer_pk)) + .spend(ctx, Conditions::new().with(new_metadata_condition))?; + let new_spend = datastore.clone().spend(ctx, inner_spend)?; + + let datastore = DataStore::::from_spend( + &mut ctx.allocator, + &new_spend, + &datastore.info.delegated_puzzles, + )? + .unwrap(); + ctx.insert(new_spend); + + assert_eq!(datastore.info.metadata, new_metadata); + + // admin: remove writer from delegated puzzles + let delegated_puzzles = vec![admin_delegated_puzzle, oracle_delegated_puzzle]; + let new_merkle_tree = get_merkle_tree(ctx, delegated_puzzles.clone())?; + let new_merkle_root = new_merkle_tree.root; + + let new_merkle_root_condition = UpdateDataStoreMerkleRoot { + new_merkle_root, + memos: DataStore::::get_recreation_memos( + datastore.info.launcher_id, + owner_puzzle_hash.into(), + delegated_puzzles.clone(), + ), + } + .to_clvm(&mut ctx.allocator)?; + + let inner_spend = StandardLayer::new(admin_pk).spend_with_conditions( + ctx, + Conditions::new().with(Condition::Other(new_merkle_root_condition)), + )?; + let new_spend = datastore.clone().spend(ctx, inner_spend)?; + + let datastore = DataStore::::from_spend( + &mut ctx.allocator, + &new_spend, + &datastore.info.delegated_puzzles, + )? + .unwrap(); + ctx.insert(new_spend); + + assert!(!datastore.info.delegated_puzzles.is_empty()); + assert_eq!(datastore.info.delegated_puzzles, delegated_puzzles); + + // oracle: just spend :) + + let oracle_layer = OracleLayer::new(oracle_puzzle_hash, oracle_fee).unwrap(); + let inner_datastore_spend = oracle_layer.construct_spend(ctx, ())?; + + let new_spend = datastore.clone().spend(ctx, inner_datastore_spend)?; + + let new_datastore = DataStore::::from_spend( + &mut ctx.allocator, + &new_spend, + &datastore.info.delegated_puzzles, + )? + .unwrap(); + ctx.insert(new_spend); + + assert_eq!(new_datastore.info, new_datastore.info); + let datastore = new_datastore; + + // mint a coin that asserts the announcement and has enough value + let new_coin = sim.new_coin(owner_puzzle_hash, oracle_fee); + + let mut hasher = Sha256::new(); + hasher.update(datastore.coin.puzzle_hash); + hasher.update(Bytes::new("$".into()).to_vec()); + + StandardLayer::new(owner_pk).spend( + ctx, + new_coin, + Conditions::new().assert_puzzle_announcement(Bytes32::new(hasher.finalize())), + )?; + + // finally, remove delegation layer altogether + let owner_layer = StandardLayer::new(owner_pk); + let output_condition = DataStore::::owner_create_coin_condition( + ctx, + datastore.info.launcher_id, + owner_puzzle_hash, + vec![], + true, + )?; + let datastore_remove_delegation_layer_inner_spend = + owner_layer.spend_with_conditions(ctx, Conditions::new().with(output_condition))?; + let new_spend = datastore + .clone() + .spend(ctx, datastore_remove_delegation_layer_inner_spend)?; + + let new_datastore = + DataStore::::from_spend(&mut ctx.allocator, &new_spend, &[])? + .unwrap(); + ctx.insert(new_spend); + + assert!(new_datastore.info.delegated_puzzles.is_empty()); + assert_eq!(new_datastore.info.owner_puzzle_hash, owner_puzzle_hash); + + sim.spend_coins(ctx.take(), &[owner_sk, admin_sk, writer_sk])?; + + // Make sure the datastore was created. + let coin_state = sim + .coin_state(new_datastore.coin.parent_coin_info) + .expect("expected datastore coin"); + assert_eq!(coin_state.coin, datastore.coin); + assert!(coin_state.spent_height.is_some()); + + Ok(()) + } + + #[derive(PartialEq, Debug, Clone, Copy)] + pub enum DstAdminLayer { + None, + Same, + New, + } + + fn assert_delegated_puzzles_contain( + dps: &[DelegatedPuzzle], + values: &[DelegatedPuzzle], + contained: &[bool], + ) { + for (i, value) in values.iter().enumerate() { + assert_eq!(dps.iter().any(|dp| dp == value), contained[i]); + } + } + + #[rstest( + src_with_writer => [true, false], + src_with_oracle => [true, false], + dst_with_writer => [true, false], + dst_with_oracle => [true, false], + src_meta => [ + (RootHash::Zero, Label::None, Description::None, ByteSize::None), + (RootHash::Some, Label::Some, Description::Some, ByteSize::Some), + ], + dst_meta => [ + (RootHash::Zero, Label::None, Description::None, ByteSize::None), + (RootHash::Zero, Label::Some, Description::Some, ByteSize::Some), + (RootHash::Zero, Label::New, Description::New, ByteSize::New), + ], + dst_admin => [ + DstAdminLayer::None, + DstAdminLayer::Same, + DstAdminLayer::New, + ] + )] + #[test] + fn test_datastore_admin_transition( + src_meta: (RootHash, Label, Description, ByteSize), + src_with_writer: bool, + // src must have admin layer in this scenario + src_with_oracle: bool, + dst_with_writer: bool, + dst_with_oracle: bool, + dst_admin: DstAdminLayer, + dst_meta: (RootHash, Label, Description, ByteSize), + ) -> anyhow::Result<()> { + let mut sim = Simulator::new(); + + let [owner_sk, admin_sk, admin2_sk, writer_sk]: [SecretKey; 4] = + test_secret_keys(4)?.try_into().unwrap(); + + let owner_pk = owner_sk.public_key(); + let admin_pk = admin_sk.public_key(); + let admin2_pk = admin2_sk.public_key(); + let writer_pk = writer_sk.public_key(); + + let oracle_puzzle_hash: Bytes32 = [7; 32].into(); + let oracle_fee = 1000; + + let owner_puzzle_hash = StandardArgs::curry_tree_hash(owner_pk).into(); + let coin = sim.new_coin(owner_puzzle_hash, 1); + + let ctx = &mut SpendContext::new(); + + let admin_delegated_puzzle = + DelegatedPuzzle::Admin(StandardArgs::curry_tree_hash(admin_pk)); + let admin2_delegated_puzzle = + DelegatedPuzzle::Admin(StandardArgs::curry_tree_hash(admin2_pk)); + let writer_delegated_puzzle = + DelegatedPuzzle::Writer(StandardArgs::curry_tree_hash(writer_pk)); + let oracle_delegated_puzzle = DelegatedPuzzle::Oracle(oracle_puzzle_hash, oracle_fee); + + let mut src_delegated_puzzles: Vec = vec![]; + src_delegated_puzzles.push(admin_delegated_puzzle); + if src_with_writer { + src_delegated_puzzles.push(writer_delegated_puzzle); + } + if src_with_oracle { + src_delegated_puzzles.push(oracle_delegated_puzzle); + } + + let (launch_singleton, src_datastore) = Launcher::new(coin.coin_id(), 1).mint_datastore( + ctx, + metadata_from_tuple(src_meta), + owner_puzzle_hash.into(), + src_delegated_puzzles.clone(), + )?; + + StandardLayer::new(owner_pk).spend(ctx, coin, launch_singleton)?; + + // transition from src to dst + let mut admin_inner_output = Conditions::new(); + + let mut dst_delegated_puzzles: Vec = src_delegated_puzzles.clone(); + if src_with_writer != dst_with_writer + || src_with_oracle != dst_with_oracle + || dst_admin != DstAdminLayer::Same + { + dst_delegated_puzzles.clear(); + + if dst_with_writer { + dst_delegated_puzzles.push(writer_delegated_puzzle); + } + if dst_with_oracle { + dst_delegated_puzzles.push(oracle_delegated_puzzle); + } + + match dst_admin { + DstAdminLayer::None => {} + DstAdminLayer::Same => { + dst_delegated_puzzles.push(admin_delegated_puzzle); + } + DstAdminLayer::New => { + dst_delegated_puzzles.push(admin2_delegated_puzzle); + } + } + + let new_merkle_tree = get_merkle_tree(ctx, dst_delegated_puzzles.clone())?; + + let new_merkle_root_condition = UpdateDataStoreMerkleRoot { + new_merkle_root: new_merkle_tree.root, + memos: DataStore::::get_recreation_memos( + src_datastore.info.launcher_id, + owner_puzzle_hash.into(), + dst_delegated_puzzles.clone(), + ), + } + .to_clvm(&mut ctx.allocator)?; + + admin_inner_output = + admin_inner_output.with(Condition::Other(new_merkle_root_condition)); + } + + if src_meta != dst_meta { + let new_metadata = metadata_from_tuple(dst_meta); + + admin_inner_output = + admin_inner_output.with(DataStore::new_metadata_condition(ctx, new_metadata)?); + } + + // delegated puzzle info + inner puzzle reveal + solution + let inner_datastore_spend = + StandardLayer::new(admin_pk).spend_with_conditions(ctx, admin_inner_output)?; + let src_datastore_coin = src_datastore.coin; + let new_spend = src_datastore.clone().spend(ctx, inner_datastore_spend)?; + + let dst_datastore = DataStore::::from_spend( + &mut ctx.allocator, + &new_spend, + &src_datastore.info.delegated_puzzles, + )? + .unwrap(); + ctx.insert(new_spend); + + assert_eq!(src_datastore.info.delegated_puzzles, src_delegated_puzzles); + assert_eq!(src_datastore.info.owner_puzzle_hash, owner_puzzle_hash); + + assert_eq!(src_datastore.info.metadata, metadata_from_tuple(src_meta)); + + assert_delegated_puzzles_contain( + &src_datastore.info.delegated_puzzles, + &[ + admin2_delegated_puzzle, + admin_delegated_puzzle, + writer_delegated_puzzle, + oracle_delegated_puzzle, + ], + &[false, true, src_with_writer, src_with_oracle], + ); + + assert_eq!(dst_datastore.info.delegated_puzzles, dst_delegated_puzzles); + assert_eq!(dst_datastore.info.owner_puzzle_hash, owner_puzzle_hash); + + assert_eq!(dst_datastore.info.metadata, metadata_from_tuple(dst_meta)); + + assert_delegated_puzzles_contain( + &dst_datastore.info.delegated_puzzles, + &[ + admin2_delegated_puzzle, + admin_delegated_puzzle, + writer_delegated_puzzle, + oracle_delegated_puzzle, + ], + &[ + dst_admin == DstAdminLayer::New, + dst_admin == DstAdminLayer::Same, + dst_with_writer, + dst_with_oracle, + ], + ); + + sim.spend_coins(ctx.take(), &[owner_sk, admin_sk, writer_sk])?; + + let src_coin_state = sim + .coin_state(src_datastore_coin.coin_id()) + .expect("expected src datastore coin"); + assert_eq!(src_coin_state.coin, src_datastore_coin); + assert!(src_coin_state.spent_height.is_some()); + let dst_coin_state = sim + .coin_state(dst_datastore.coin.coin_id()) + .expect("expected dst datastore coin"); + assert_eq!(dst_coin_state.coin, dst_datastore.coin); + assert!(dst_coin_state.created_height.is_some()); + + Ok(()) + } + + #[rstest( + src_with_admin => [true, false], + src_with_writer => [true, false], + src_with_oracle => [true, false], + dst_with_admin => [true, false], + dst_with_writer => [true, false], + dst_with_oracle => [true, false], + src_meta => [ + (RootHash::Zero, Label::None, Description::None, ByteSize::None), + (RootHash::Some, Label::Some, Description::Some, ByteSize::Some), + ], + dst_meta => [ + (RootHash::Zero, Label::None, Description::None, ByteSize::None), + (RootHash::Some, Label::Some, Description::Some, ByteSize::Some), + (RootHash::Some, Label::New, Description::New, ByteSize::New), + ], + change_owner => [true, false], + )] + #[test] + fn test_datastore_owner_transition( + src_meta: (RootHash, Label, Description, ByteSize), + src_with_admin: bool, + src_with_writer: bool, + src_with_oracle: bool, + dst_with_admin: bool, + dst_with_writer: bool, + dst_with_oracle: bool, + dst_meta: (RootHash, Label, Description, ByteSize), + change_owner: bool, + ) -> anyhow::Result<()> { + let mut sim = Simulator::new(); + + let [owner_sk, owner2_sk, admin_sk, writer_sk]: [SecretKey; 4] = + test_secret_keys(4)?.try_into().unwrap(); + + let owner_pk = owner_sk.public_key(); + let owner2_pk = owner2_sk.public_key(); + let admin_pk = admin_sk.public_key(); + let writer_pk = writer_sk.public_key(); + + let oracle_puzzle_hash: Bytes32 = [7; 32].into(); + let oracle_fee = 1000; + + let owner_puzzle_hash = StandardArgs::curry_tree_hash(owner_pk).into(); + let coin = sim.new_coin(owner_puzzle_hash, 1); + + let owner2_puzzle_hash = StandardArgs::curry_tree_hash(owner2_pk).into(); + assert_ne!(owner_puzzle_hash, owner2_puzzle_hash); + + let ctx = &mut SpendContext::new(); + + let admin_delegated_puzzle = + DelegatedPuzzle::Admin(StandardArgs::curry_tree_hash(admin_pk)); + let writer_delegated_puzzle = + DelegatedPuzzle::Writer(StandardArgs::curry_tree_hash(writer_pk)); + let oracle_delegated_puzzle = DelegatedPuzzle::Oracle(oracle_puzzle_hash, oracle_fee); + + let mut src_delegated_puzzles: Vec = vec![]; + if src_with_admin { + src_delegated_puzzles.push(admin_delegated_puzzle); + } + if src_with_writer { + src_delegated_puzzles.push(writer_delegated_puzzle); + } + if src_with_oracle { + src_delegated_puzzles.push(oracle_delegated_puzzle); + } + + let (launch_singleton, src_datastore) = Launcher::new(coin.coin_id(), 1).mint_datastore( + ctx, + metadata_from_tuple(src_meta), + owner_puzzle_hash.into(), + src_delegated_puzzles.clone(), + )?; + StandardLayer::new(owner_pk).spend(ctx, coin, launch_singleton)?; + + // transition from src to dst using owner puzzle + let mut owner_output_conds = Conditions::new(); + + let mut dst_delegated_puzzles: Vec = src_delegated_puzzles.clone(); + let mut hint_new_delegated_puzzles = change_owner; + if src_with_admin != dst_with_admin + || src_with_writer != dst_with_writer + || src_with_oracle != dst_with_oracle + || dst_delegated_puzzles.is_empty() + { + dst_delegated_puzzles.clear(); + hint_new_delegated_puzzles = true; + + if dst_with_admin { + dst_delegated_puzzles.push(admin_delegated_puzzle); + } + if dst_with_writer { + dst_delegated_puzzles.push(writer_delegated_puzzle); + } + if dst_with_oracle { + dst_delegated_puzzles.push(oracle_delegated_puzzle); + } + } + + owner_output_conds = + owner_output_conds.with(DataStore::::owner_create_coin_condition( + ctx, + src_datastore.info.launcher_id, + if change_owner { + owner2_puzzle_hash + } else { + owner_puzzle_hash + }, + dst_delegated_puzzles.clone(), + hint_new_delegated_puzzles, + )?); + + if src_meta != dst_meta { + let new_metadata = metadata_from_tuple(dst_meta); + + owner_output_conds = + owner_output_conds.with(DataStore::new_metadata_condition(ctx, new_metadata)?); + } + + // delegated puzzle info + inner puzzle reveal + solution + let inner_datastore_spend = + StandardLayer::new(owner_pk).spend_with_conditions(ctx, owner_output_conds)?; + let new_spend = src_datastore.clone().spend(ctx, inner_datastore_spend)?; + + let dst_datastore = DataStore::::from_spend( + &mut ctx.allocator, + &new_spend, + &src_datastore.info.delegated_puzzles, + )? + .unwrap(); + + ctx.insert(new_spend); + + assert_eq!(src_datastore.info.delegated_puzzles, src_delegated_puzzles); + assert_eq!(src_datastore.info.owner_puzzle_hash, owner_puzzle_hash); + + assert_eq!(src_datastore.info.metadata, metadata_from_tuple(src_meta)); + + assert_delegated_puzzles_contain( + &src_datastore.info.delegated_puzzles, + &[ + admin_delegated_puzzle, + writer_delegated_puzzle, + oracle_delegated_puzzle, + ], + &[src_with_admin, src_with_writer, src_with_oracle], + ); + + assert_eq!(dst_datastore.info.delegated_puzzles, dst_delegated_puzzles); + assert_eq!( + dst_datastore.info.owner_puzzle_hash, + if change_owner { + owner2_puzzle_hash + } else { + owner_puzzle_hash + } + ); + + assert_eq!(dst_datastore.info.metadata, metadata_from_tuple(dst_meta)); + + assert_delegated_puzzles_contain( + &dst_datastore.info.delegated_puzzles, + &[ + admin_delegated_puzzle, + writer_delegated_puzzle, + oracle_delegated_puzzle, + ], + &[dst_with_admin, dst_with_writer, dst_with_oracle], + ); + + sim.spend_coins(ctx.take(), &[owner_sk, admin_sk, writer_sk])?; + + let src_coin_state = sim + .coin_state(src_datastore.coin.coin_id()) + .expect("expected src datastore coin"); + assert_eq!(src_coin_state.coin, src_datastore.coin); + assert!(src_coin_state.spent_height.is_some()); + + let dst_coin_state = sim + .coin_state(dst_datastore.coin.coin_id()) + .expect("expected dst datastore coin"); + assert_eq!(dst_coin_state.coin, dst_datastore.coin); + assert!(dst_coin_state.created_height.is_some()); + + Ok(()) + } + + #[rstest( + with_admin_layer => [true, false], + with_oracle_layer => [true, false], + meta_transition => [ + ( + (RootHash::Zero, Label::None, Description::None, ByteSize::None), + (RootHash::Zero, Label::Some, Description::Some, ByteSize::Some), + ), + ( + (RootHash::Zero, Label::None, Description::None, ByteSize::None), + (RootHash::Some, Label::None, Description::None, ByteSize::None), + ), + ( + (RootHash::Zero, Label::Some, Description::Some, ByteSize::Some), + (RootHash::Some, Label::Some, Description::Some, ByteSize::Some), + ), + ( + (RootHash::Zero, Label::Some, Description::Some, ByteSize::Some), + (RootHash::Zero, Label::New, Description::New, ByteSize::New), + ), + ( + (RootHash::Zero, Label::None, Description::None, ByteSize::None), + (RootHash::Zero, Label::None, Description::None, ByteSize::Some), + ), + ( + (RootHash::Zero, Label::None, Description::None, ByteSize::None), + (RootHash::Zero, Label::None, Description::Some, ByteSize::Some), + ), + ], + )] + #[test] + fn test_datastore_writer_transition( + with_admin_layer: bool, + with_oracle_layer: bool, + meta_transition: ( + (RootHash, Label, Description, ByteSize), + (RootHash, Label, Description, ByteSize), + ), + ) -> anyhow::Result<()> { + let mut sim = Simulator::new(); + + let [owner_sk, admin_sk, writer_sk]: [SecretKey; 3] = + test_secret_keys(3)?.try_into().unwrap(); + + let owner_pk = owner_sk.public_key(); + let admin_pk = admin_sk.public_key(); + let writer_pk = writer_sk.public_key(); + + let oracle_puzzle_hash: Bytes32 = [7; 32].into(); + let oracle_fee = 1000; + + let owner_puzzle_hash = StandardArgs::curry_tree_hash(owner_pk).into(); + let coin = sim.new_coin(owner_puzzle_hash, 1); + + let ctx = &mut SpendContext::new(); + + let admin_delegated_puzzle = + DelegatedPuzzle::Admin(StandardArgs::curry_tree_hash(admin_pk)); + let writer_delegated_puzzle = + DelegatedPuzzle::Writer(StandardArgs::curry_tree_hash(writer_pk)); + let oracle_delegated_puzzle = DelegatedPuzzle::Oracle(oracle_puzzle_hash, oracle_fee); + + let mut delegated_puzzles: Vec = vec![]; + delegated_puzzles.push(writer_delegated_puzzle); + if with_admin_layer { + delegated_puzzles.push(admin_delegated_puzzle); + } + if with_oracle_layer { + delegated_puzzles.push(oracle_delegated_puzzle); + } + + let (launch_singleton, src_datastore) = Launcher::new(coin.coin_id(), 1).mint_datastore( + ctx, + metadata_from_tuple(meta_transition.0), + owner_puzzle_hash.into(), + delegated_puzzles.clone(), + )?; + + StandardLayer::new(owner_pk).spend(ctx, coin, launch_singleton)?; + + // transition from src to dst using writer (update metadata) + let new_metadata = metadata_from_tuple(meta_transition.1); + let new_metadata_condition = DataStore::new_metadata_condition(ctx, new_metadata)?; + + let inner_spend = WriterLayer::new(StandardLayer::new(writer_pk)) + .spend(ctx, Conditions::new().with(new_metadata_condition))?; + + let new_spend = src_datastore.clone().spend(ctx, inner_spend)?; + + let dst_datastore = DataStore::::from_spend( + &mut ctx.allocator, + &new_spend, + &src_datastore.info.delegated_puzzles, + )? + .unwrap(); + ctx.insert(new_spend.clone()); + + assert_eq!(src_datastore.info.delegated_puzzles, delegated_puzzles); + assert_eq!(src_datastore.info.owner_puzzle_hash, owner_puzzle_hash); + + assert_eq!( + src_datastore.info.metadata, + metadata_from_tuple(meta_transition.0) + ); + + assert_delegated_puzzles_contain( + &src_datastore.info.delegated_puzzles, + &[ + admin_delegated_puzzle, + writer_delegated_puzzle, + oracle_delegated_puzzle, + ], + &[with_admin_layer, true, with_oracle_layer], + ); + + assert_eq!(dst_datastore.info.delegated_puzzles, delegated_puzzles); + assert_eq!(dst_datastore.info.owner_puzzle_hash, owner_puzzle_hash); + + assert_eq!( + dst_datastore.info.metadata, + metadata_from_tuple(meta_transition.1) + ); + + assert_delegated_puzzles_contain( + &dst_datastore.info.delegated_puzzles, + &[ + admin_delegated_puzzle, + writer_delegated_puzzle, + oracle_delegated_puzzle, + ], + &[with_admin_layer, true, with_oracle_layer], + ); + + sim.spend_coins(ctx.take(), &[owner_sk, admin_sk, writer_sk])?; + + let src_coin_state = sim + .coin_state(src_datastore.coin.coin_id()) + .expect("expected src datastore coin"); + assert_eq!(src_coin_state.coin, src_datastore.coin); + assert!(src_coin_state.spent_height.is_some()); + let dst_coin_state = sim + .coin_state(dst_datastore.coin.coin_id()) + .expect("expected dst datastore coin"); + assert_eq!(dst_coin_state.coin, dst_datastore.coin); + assert!(dst_coin_state.created_height.is_some()); + + Ok(()) + } + + #[rstest( + with_admin_layer => [true, false], + with_writer_layer => [true, false], + meta => [ + (RootHash::Zero, Label::None, Description::None, ByteSize::None), + (RootHash::Zero, Label::None, Description::None, ByteSize::Some), + (RootHash::Zero, Label::None, Description::Some, ByteSize::Some), + (RootHash::Zero, Label::Some, Description::Some, ByteSize::Some), + ], + )] + #[test] + fn test_datastore_oracle_transition( + with_admin_layer: bool, + with_writer_layer: bool, + meta: (RootHash, Label, Description, ByteSize), + ) -> anyhow::Result<()> { + let mut sim = Simulator::new(); + + let [owner_sk, admin_sk, writer_sk, dude_sk]: [SecretKey; 4] = + test_secret_keys(4)?.try_into().unwrap(); + + let owner_pk = owner_sk.public_key(); + let admin_pk = admin_sk.public_key(); + let writer_pk = writer_sk.public_key(); + let dude_pk = dude_sk.public_key(); + + let oracle_puzzle_hash: Bytes32 = [7; 32].into(); + let oracle_fee = 1000; + + let owner_puzzle_hash = StandardArgs::curry_tree_hash(owner_pk).into(); + let coin = sim.new_coin(owner_puzzle_hash, 1); + + let dude_puzzle_hash = StandardArgs::curry_tree_hash(dude_pk).into(); + + let ctx = &mut SpendContext::new(); + + let admin_delegated_puzzle = + DelegatedPuzzle::Admin(StandardArgs::curry_tree_hash(admin_pk)); + let writer_delegated_puzzle = + DelegatedPuzzle::Writer(StandardArgs::curry_tree_hash(writer_pk)); + let oracle_delegated_puzzle = DelegatedPuzzle::Oracle(oracle_puzzle_hash, oracle_fee); + + let mut delegated_puzzles: Vec = vec![]; + delegated_puzzles.push(oracle_delegated_puzzle); + + if with_admin_layer { + delegated_puzzles.push(admin_delegated_puzzle); + } + if with_writer_layer { + delegated_puzzles.push(writer_delegated_puzzle); + } + + let (launch_singleton, src_datastore) = Launcher::new(coin.coin_id(), 1).mint_datastore( + ctx, + metadata_from_tuple(meta), + owner_puzzle_hash.into(), + delegated_puzzles.clone(), + )?; + + StandardLayer::new(owner_pk).spend(ctx, coin, launch_singleton)?; + + // 'dude' spends oracle + let inner_datastore_spend = OracleLayer::new(oracle_puzzle_hash, oracle_fee) + .unwrap() + .spend(ctx)?; + let new_spend = src_datastore.clone().spend(ctx, inner_datastore_spend)?; + + let dst_datastore = DataStore::from_spend( + &mut ctx.allocator, + &new_spend, + &src_datastore.info.delegated_puzzles, + )? + .unwrap(); + ctx.insert(new_spend); + + assert_eq!(src_datastore.info, dst_datastore.info); + + // mint a coin that asserts the announcement and has enough value + let mut hasher = Sha256::new(); + hasher.update(src_datastore.coin.puzzle_hash); + hasher.update(Bytes::new("$".into()).to_vec()); + + let new_coin = sim.new_coin(dude_puzzle_hash, oracle_fee); + StandardLayer::new(dude_pk).spend( + ctx, + new_coin, + Conditions::new().assert_puzzle_announcement(Bytes32::new(hasher.finalize())), + )?; + + // asserts + + assert_eq!(src_datastore.info.delegated_puzzles, delegated_puzzles); + assert_eq!(src_datastore.info.owner_puzzle_hash, owner_puzzle_hash); + + assert_eq!(src_datastore.info.metadata, metadata_from_tuple(meta)); + + assert_delegated_puzzles_contain( + &src_datastore.info.delegated_puzzles, + &[ + admin_delegated_puzzle, + writer_delegated_puzzle, + oracle_delegated_puzzle, + ], + &[with_admin_layer, with_writer_layer, true], + ); + + assert_eq!(dst_datastore.info.delegated_puzzles, delegated_puzzles); + assert_eq!(dst_datastore.info.owner_puzzle_hash, owner_puzzle_hash); + + assert_eq!(dst_datastore.info.metadata, metadata_from_tuple(meta)); + + assert_delegated_puzzles_contain( + &dst_datastore.info.delegated_puzzles, + &[ + admin_delegated_puzzle, + writer_delegated_puzzle, + oracle_delegated_puzzle, + ], + &[with_admin_layer, with_writer_layer, true], + ); + + sim.spend_coins(ctx.take(), &[owner_sk, dude_sk])?; + + let src_datastore_coin_id = src_datastore.coin.coin_id(); + let src_coin_state = sim + .coin_state(src_datastore_coin_id) + .expect("expected src datastore coin"); + assert_eq!(src_coin_state.coin, src_datastore.coin); + assert!(src_coin_state.spent_height.is_some()); + let dst_coin_state = sim + .coin_state(dst_datastore.coin.coin_id()) + .expect("expected dst datastore coin"); + assert_eq!(dst_coin_state.coin, dst_datastore.coin); + assert!(dst_coin_state.created_height.is_some()); + + let oracle_coin = Coin::new(src_datastore_coin_id, oracle_puzzle_hash, oracle_fee); + let oracle_coin_state = sim + .coin_state(oracle_coin.coin_id()) + .expect("expected oracle coin"); + assert_eq!(oracle_coin_state.coin, oracle_coin); + assert!(oracle_coin_state.created_height.is_some()); + + Ok(()) + } + + #[rstest( + with_admin_layer => [true, false], + with_writer_layer => [true, false], + with_oracle_layer => [true, false], + meta => [ + (RootHash::Zero, Label::None, Description::None, ByteSize::None), + (RootHash::Zero, Label::Some, Description::Some, ByteSize::Some), + ], + )] + #[test] + fn test_melt( + with_admin_layer: bool, + with_writer_layer: bool, + with_oracle_layer: bool, + meta: (RootHash, Label, Description, ByteSize), + ) -> anyhow::Result<()> { + let mut sim = Simulator::new(); + + let [owner_sk, admin_sk, writer_sk]: [SecretKey; 3] = + test_secret_keys(3)?.try_into().unwrap(); + + let owner_pk = owner_sk.public_key(); + let admin_pk = admin_sk.public_key(); + let writer_pk = writer_sk.public_key(); + + let oracle_puzzle_hash: Bytes32 = [7; 32].into(); + let oracle_fee = 1000; + + let owner_puzzle_hash = StandardArgs::curry_tree_hash(owner_pk).into(); + let coin = sim.new_coin(owner_puzzle_hash, 1); + + let ctx = &mut SpendContext::new(); + + let admin_delegated_puzzle = + DelegatedPuzzle::Admin(StandardArgs::curry_tree_hash(admin_pk)); + let writer_delegated_puzzle = + DelegatedPuzzle::Writer(StandardArgs::curry_tree_hash(writer_pk)); + let oracle_delegated_puzzle = DelegatedPuzzle::Oracle(oracle_puzzle_hash, oracle_fee); + + let mut delegated_puzzles: Vec = vec![]; + if with_admin_layer { + delegated_puzzles.push(admin_delegated_puzzle); + } + if with_writer_layer { + delegated_puzzles.push(writer_delegated_puzzle); + } + if with_oracle_layer { + delegated_puzzles.push(oracle_delegated_puzzle); + } + + let (launch_singleton, src_datastore) = Launcher::new(coin.coin_id(), 1).mint_datastore( + ctx, + metadata_from_tuple(meta), + owner_puzzle_hash.into(), + delegated_puzzles.clone(), + )?; + + StandardLayer::new(owner_pk).spend(ctx, coin, launch_singleton)?; + + // owner melts + let output_conds = Conditions::new().with(Condition::Other( + MeltSingleton {}.to_clvm(&mut ctx.allocator)?, + )); + let inner_datastore_spend = + StandardLayer::new(owner_pk).spend_with_conditions(ctx, output_conds)?; + + let new_spend = src_datastore.clone().spend(ctx, inner_datastore_spend)?; + ctx.insert(new_spend); + + // asserts + + assert_eq!(src_datastore.info.owner_puzzle_hash, owner_puzzle_hash); + + assert_eq!(src_datastore.info.metadata, metadata_from_tuple(meta)); + + assert_delegated_puzzles_contain( + &src_datastore.info.delegated_puzzles, + &[ + admin_delegated_puzzle, + writer_delegated_puzzle, + oracle_delegated_puzzle, + ], + &[with_admin_layer, with_writer_layer, with_oracle_layer], + ); + + sim.spend_coins(ctx.take(), &[owner_sk])?; + + let src_coin_state = sim + .coin_state(src_datastore.coin.coin_id()) + .expect("expected src datastore coin"); + assert_eq!(src_coin_state.coin, src_datastore.coin); + assert!(src_coin_state.spent_height.is_some()); // tx happened + + Ok(()) + } + + enum AttackerPuzzle { + Admin, + Writer, + } + + impl AttackerPuzzle { + fn get_spend( + &self, + ctx: &mut SpendContext, + attacker_pk: PublicKey, + output_conds: Conditions, + ) -> Result { + Ok(match self { + AttackerPuzzle::Admin => { + StandardLayer::new(attacker_pk).spend_with_conditions(ctx, output_conds)? + } + + AttackerPuzzle::Writer => { + WriterLayer::new(StandardLayer::new(attacker_pk)).spend(ctx, output_conds)? + } + }) + } + } + + #[rstest( + puzzle => [AttackerPuzzle::Admin, AttackerPuzzle::Writer], + )] + #[test] + fn test_create_coin_filer(puzzle: AttackerPuzzle) -> anyhow::Result<()> { + let mut sim = Simulator::new(); + + let [owner_sk, attacker_sk]: [SecretKey; 2] = test_secret_keys(2)?.try_into().unwrap(); + + let owner_pk = owner_sk.public_key(); + let attacker_pk = attacker_sk.public_key(); + + let owner_puzzle_hash = StandardArgs::curry_tree_hash(owner_pk).into(); + let attacker_puzzle_hash = StandardArgs::curry_tree_hash(attacker_pk); + let coin = sim.new_coin(owner_puzzle_hash, 1); + + let ctx = &mut SpendContext::new(); + + let delegated_puzzle = match puzzle { + AttackerPuzzle::Admin => DelegatedPuzzle::Admin(attacker_puzzle_hash), + AttackerPuzzle::Writer => DelegatedPuzzle::Writer(attacker_puzzle_hash), + }; + + let (launch_singleton, src_datastore) = Launcher::new(coin.coin_id(), 1).mint_datastore( + ctx, + DataStoreMetadata::default(), + owner_puzzle_hash.into(), + vec![delegated_puzzle], + )?; + + StandardLayer::new(owner_pk).spend(ctx, coin, launch_singleton)?; + + // delegated puzzle tries to steal the coin + let inner_datastore_spend = puzzle.get_spend( + ctx, + attacker_pk, + Conditions::new().with(Condition::CreateCoin(CreateCoin { + puzzle_hash: attacker_puzzle_hash.into(), + amount: 1, + memos: vec![], + })), + )?; + + let new_spend = src_datastore.spend(ctx, inner_datastore_spend)?; + + let puzzle_reveal_ptr = ctx.alloc(&new_spend.puzzle_reveal)?; + let solution_ptr = ctx.alloc(&new_spend.solution)?; + match ctx.run(puzzle_reveal_ptr, solution_ptr) { + Ok(_) => panic!("expected error"), + Err(err) => match err { + DriverError::Eval(eval_err) => { + assert_eq!(eval_err.1, "clvm raise"); + } + _ => panic!("expected 'clvm raise' error"), + }, + } + + Ok(()) + } + + #[rstest( + puzzle => [AttackerPuzzle::Admin, AttackerPuzzle::Writer], + )] + #[test] + fn test_melt_filter(puzzle: AttackerPuzzle) -> anyhow::Result<()> { + let mut sim = Simulator::new(); + + let [owner_sk, attacker_sk]: [SecretKey; 2] = test_secret_keys(2)?.try_into().unwrap(); + + let owner_pk = owner_sk.public_key(); + let attacker_pk = attacker_sk.public_key(); + + let owner_puzzle_hash = StandardArgs::curry_tree_hash(owner_pk).into(); + let coin = sim.new_coin(owner_puzzle_hash, 1); + + let attacker_puzzle_hash = StandardArgs::curry_tree_hash(attacker_pk); + + let ctx = &mut SpendContext::new(); + + let delegated_puzzle = match puzzle { + AttackerPuzzle::Admin => DelegatedPuzzle::Admin(attacker_puzzle_hash), + AttackerPuzzle::Writer => DelegatedPuzzle::Writer(attacker_puzzle_hash), + }; + + let (launch_singleton, src_datastore) = Launcher::new(coin.coin_id(), 1).mint_datastore( + ctx, + DataStoreMetadata::default(), + owner_puzzle_hash.into(), + vec![delegated_puzzle], + )?; + + StandardLayer::new(owner_pk).spend(ctx, coin, launch_singleton)?; + + // attacker tries to melt the coin via delegated puzzle + let conds = Conditions::new().with(Condition::Other( + MeltSingleton {}.to_clvm(&mut ctx.allocator)?, + )); + let inner_datastore_spend = puzzle.get_spend(ctx, attacker_pk, conds)?; + + let new_spend = src_datastore.spend(ctx, inner_datastore_spend)?; + + let puzzle_reveal_ptr = ctx.alloc(&new_spend.puzzle_reveal)?; + let solution_ptr = ctx.alloc(&new_spend.solution)?; + match ctx.run(puzzle_reveal_ptr, solution_ptr) { + Ok(_) => panic!("expected error"), + Err(err) => match err { + DriverError::Eval(eval_err) => { + assert_eq!(eval_err.1, "clvm raise"); + Ok(()) + } + _ => panic!("expected 'clvm raise' error"), + }, + } + } + + #[rstest( + test_puzzle => [AttackerPuzzle::Admin, AttackerPuzzle::Writer], + new_merkle_root => [RootHash::Zero, RootHash::Some], + memos => [vec![], vec![RootHash::Zero], vec![RootHash::Some]], + )] + fn test_new_merkle_root_filter( + test_puzzle: AttackerPuzzle, + new_merkle_root: RootHash, + memos: Vec, + ) -> anyhow::Result<()> { + let [attacker_sk]: [SecretKey; 1] = test_secret_keys(1)?.try_into().unwrap(); + + let attacker_pk = attacker_sk.public_key(); + + let ctx = &mut SpendContext::new(); + + let condition_output = Conditions::new().with(Condition::Other( + UpdateDataStoreMerkleRoot { + new_merkle_root: new_merkle_root.value(), + memos: memos.into_iter().map(|m| m.value().into()).collect(), + } + .to_clvm(&mut ctx.allocator)?, + )); + + let spend = test_puzzle.get_spend(ctx, attacker_pk, condition_output)?; + + match ctx.run(spend.puzzle, spend.solution) { + Ok(_) => match test_puzzle { + AttackerPuzzle::Admin => Ok(()), + AttackerPuzzle::Writer => panic!("expected error from writer puzzle"), + }, + Err(err) => match err { + DriverError::Eval(eval_err) => match test_puzzle { + AttackerPuzzle::Admin => panic!("expected admin puzzle to run normally"), + AttackerPuzzle::Writer => { + assert_eq!(eval_err.1, "clvm raise"); + Ok(()) + } + }, + _ => panic!("other error encountered"), + }, + } + } + + #[rstest( + puzzle => [AttackerPuzzle::Admin, AttackerPuzzle::Writer], + new_root_hash => [RootHash::Zero, RootHash::Some], + new_updater_ph => [RootHash::Zero.value().into(), DL_METADATA_UPDATER_PUZZLE_HASH], + output_conditions => [false, true], + )] + fn test_metadata_filter( + puzzle: AttackerPuzzle, + new_root_hash: RootHash, + new_updater_ph: TreeHash, + output_conditions: bool, + ) -> anyhow::Result<()> { + let should_error_out = + output_conditions || new_updater_ph != DL_METADATA_UPDATER_PUZZLE_HASH; + + let [attacker_sk]: [SecretKey; 1] = test_secret_keys(1)?.try_into().unwrap(); + + let attacker_pk = attacker_sk.public_key(); + + let ctx = &mut SpendContext::new(); + + let new_metadata_condition = Condition::Other( + UpdateNftMetadata { + updater_puzzle_reveal: 11, + updater_solution: NewMetadataOutput { + metadata_info: NewMetadataInfo { + new_metadata: DataStoreMetadata::root_hash_only(new_root_hash.value()), + new_updater_puzzle_hash: new_updater_ph.into(), + }, + conditions: if output_conditions { + vec![CreateCoin { + puzzle_hash: [0; 32].into(), + amount: 1, + memos: vec![], + }] + } else { + vec![] + }, + }, + } + .to_clvm(&mut ctx.allocator)?, + ); + + let inner_spend = puzzle.get_spend( + ctx, + attacker_pk, + Conditions::new().with(new_metadata_condition), + )?; + + let delegated_puzzles = match puzzle { + AttackerPuzzle::Admin => { + vec![DelegatedPuzzle::Admin(StandardArgs::curry_tree_hash( + attacker_pk, + ))] + } + AttackerPuzzle::Writer => vec![DelegatedPuzzle::Writer(StandardArgs::curry_tree_hash( + attacker_pk, + ))], + }; + let merkle_tree = get_merkle_tree(ctx, delegated_puzzles.clone())?; + + let delegation_layer = + DelegationLayer::new(Bytes32::default(), Bytes32::default(), merkle_tree.root); + + let puzzle_ptr = delegation_layer.construct_puzzle(ctx)?; + + let delegated_puzzle_hash = ctx.tree_hash(inner_spend.puzzle); + let solution_ptr = delegation_layer.construct_solution( + ctx, + DelegationLayerSolution { + merkle_proof: merkle_tree.get_proof(delegated_puzzle_hash.into()), + puzzle_reveal: inner_spend.puzzle, + puzzle_solution: inner_spend.solution, + }, + )?; + + match ctx.run(puzzle_ptr, solution_ptr) { + Ok(_) => { + if should_error_out { + panic!("expected puzzle to error out"); + } else { + Ok(()) + } + } + Err(err) => match err { + DriverError::Eval(eval_err) => { + if should_error_out { + if output_conditions { + assert_eq!(eval_err.1, "= on list"); + } else { + assert_eq!(eval_err.1, "clvm raise"); + } + Ok(()) + } else { + panic!("expected puzzle to not error out"); + } + } + _ => panic!("unexpected error while evaluating puzzle"), + }, + } + } + + #[rstest( + transition => [ + (RootHash::Zero, RootHash::Zero, true), + (RootHash::Zero, RootHash::Some, false), + (RootHash::Zero, RootHash::Some, true), + (RootHash::Some, RootHash::Some, true), + (RootHash::Some, RootHash::Some, false), + (RootHash::Some, RootHash::Some, true), + ] + )] + #[test] + fn test_old_memo_format(transition: (RootHash, RootHash, bool)) -> anyhow::Result<()> { + let mut sim = Simulator::new(); + + let [owner_sk, owner2_sk]: [SecretKey; 2] = test_secret_keys(2)?.try_into().unwrap(); + + let owner_pk = owner_sk.public_key(); + let owner2_pk = owner2_sk.public_key(); + + let owner_puzzle_hash = StandardArgs::curry_tree_hash(owner_pk); + let coin = sim.new_coin(owner_puzzle_hash.into(), 1); + + let owner2_puzzle_hash = StandardArgs::curry_tree_hash(owner2_pk); + + let ctx = &mut SpendContext::new(); + + // launch using old memos scheme + let launcher = Launcher::new(coin.coin_id(), 1); + let inner_puzzle_hash: TreeHash = owner_puzzle_hash; + + let first_root_hash: RootHash = transition.0; + let metadata_ptr = ctx.alloc(&vec![first_root_hash.value()])?; + let metadata_hash = ctx.tree_hash(metadata_ptr); + let state_layer_hash = CurriedProgram { + program: NFT_STATE_LAYER_PUZZLE_HASH, + args: NftStateLayerArgs:: { + mod_hash: NFT_STATE_LAYER_PUZZLE_HASH.into(), + metadata: metadata_hash, + metadata_updater_puzzle_hash: DL_METADATA_UPDATER_PUZZLE_HASH.into(), + inner_puzzle: inner_puzzle_hash, + }, + } + .tree_hash(); + + // https://github.com/Chia-Network/chia-blockchain/blob/4ffb6dfa6f53f6cd1920bcc775e27377a771fbec/chia/wallet/db_wallet/db_wallet_puzzles.py#L59 + // kv_list = 'memos': (root_hash inner_puzzle_hash) + let kv_list = vec![first_root_hash.value(), owner_puzzle_hash.into()]; + + let launcher_coin = launcher.coin(); + let (launcher_conds, eve_coin) = launcher.spend(ctx, state_layer_hash.into(), kv_list)?; + + StandardLayer::new(owner_pk).spend(ctx, coin, launcher_conds)?; + + let spends = ctx.take(); + spends + .clone() + .into_iter() + .for_each(|spend| ctx.insert(spend)); + + let datastore_from_launcher = spends + .into_iter() + .find(|spend| spend.coin.coin_id() == eve_coin.parent_coin_info) + .map(|spend| { + DataStore::from_spend(&mut ctx.allocator, &spend, &[]) + .unwrap() + .unwrap() + }) + .expect("expected launcher spend"); + + assert_eq!( + datastore_from_launcher.info.metadata, + DataStoreMetadata::root_hash_only(first_root_hash.value()) + ); + assert_eq!( + datastore_from_launcher.info.owner_puzzle_hash, + owner_puzzle_hash.into() + ); + assert!(datastore_from_launcher.info.delegated_puzzles.is_empty()); + + assert_eq!( + datastore_from_launcher.info.launcher_id, + eve_coin.parent_coin_info + ); + assert_eq!(datastore_from_launcher.coin.coin_id(), eve_coin.coin_id()); + + match datastore_from_launcher.proof { + Proof::Eve(proof) => { + assert_eq!( + proof.parent_parent_coin_info, + launcher_coin.parent_coin_info + ); + assert_eq!(proof.parent_amount, launcher_coin.amount); + } + Proof::Lineage(_) => panic!("expected eve (not lineage) proof for info_from_launcher"), + } + + // now spend the signleton using old memo format and check that info is parsed correctly + + let mut inner_spend_conditions = Conditions::new(); + + let second_root_hash: RootHash = transition.1; + + let new_metadata = DataStoreMetadata::root_hash_only(second_root_hash.value()); + if second_root_hash != first_root_hash { + inner_spend_conditions = inner_spend_conditions.with( + DataStore::new_metadata_condition(ctx, new_metadata.clone())?, + ); + } + + let new_owner: bool = transition.2; + let new_inner_ph: Bytes32 = if new_owner { + owner2_puzzle_hash.into() + } else { + owner_puzzle_hash.into() + }; + + // https://github.com/Chia-Network/chia-blockchain/blob/4ffb6dfa6f53f6cd1920bcc775e27377a771fbec/chia/data_layer/data_layer_wallet.py#L526 + // memos are (launcher_id root_hash inner_puzzle_hash) + inner_spend_conditions = inner_spend_conditions.with(Condition::CreateCoin(CreateCoin { + puzzle_hash: new_inner_ph, + amount: 1, + memos: vec![ + launcher_coin.coin_id().into(), + second_root_hash.value().into(), + new_inner_ph.into(), + ], + })); + + let inner_spend = + StandardLayer::new(owner_pk).spend_with_conditions(ctx, inner_spend_conditions)?; + let spend = datastore_from_launcher.clone().spend(ctx, inner_spend)?; + + let new_datastore = DataStore::::from_spend( + &mut ctx.allocator, + &spend, + &datastore_from_launcher.info.delegated_puzzles, + )? + .unwrap(); + + assert_eq!( + new_datastore.info.metadata, + DataStoreMetadata::root_hash_only(second_root_hash.value()) + ); + + assert!(new_datastore.info.delegated_puzzles.is_empty()); + + assert_eq!(new_datastore.info.owner_puzzle_hash, new_inner_ph); + assert_eq!(new_datastore.info.launcher_id, eve_coin.parent_coin_info); + + assert_eq!( + new_datastore.coin.parent_coin_info, + datastore_from_launcher.coin.coin_id() + ); + assert_eq!( + new_datastore.coin.puzzle_hash, + SingletonArgs::curry_tree_hash( + datastore_from_launcher.info.launcher_id, + CurriedProgram { + program: NFT_STATE_LAYER_PUZZLE_HASH, + args: NftStateLayerArgs:: { + mod_hash: NFT_STATE_LAYER_PUZZLE_HASH.into(), + metadata: new_metadata, + metadata_updater_puzzle_hash: DL_METADATA_UPDATER_PUZZLE_HASH.into(), + inner_puzzle: new_inner_ph.into(), + }, + } + .tree_hash() + ) + .into() + ); + assert_eq!(new_datastore.coin.amount, 1); + + match new_datastore.proof { + Proof::Lineage(proof) => { + assert_eq!(proof.parent_parent_coin_info, eve_coin.parent_coin_info); + assert_eq!(proof.parent_amount, eve_coin.amount); + assert_eq!( + proof.parent_inner_puzzle_hash, + CurriedProgram { + program: NFT_STATE_LAYER_PUZZLE_HASH, + args: NftStateLayerArgs:: { + mod_hash: NFT_STATE_LAYER_PUZZLE_HASH.into(), + metadata: datastore_from_launcher.info.metadata, + metadata_updater_puzzle_hash: DL_METADATA_UPDATER_PUZZLE_HASH.into(), + inner_puzzle: owner_puzzle_hash, + }, + } + .tree_hash() + .into() + ); + } + Proof::Eve(_) => panic!("expected lineage (not eve) proof for new_info"), + } + + ctx.insert(spend); + + sim.spend_coins(ctx.take(), &[owner_sk, owner2_sk])?; + + let eve_coin_state = sim + .coin_state(eve_coin.coin_id()) + .expect("expected eve coin"); + assert!(eve_coin_state.created_height.is_some()); + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/datalayer/datastore_info.rs b/crates/chia-sdk-driver/src/primitives/datalayer/datastore_info.rs new file mode 100644 index 00000000..88224816 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/datalayer/datastore_info.rs @@ -0,0 +1,287 @@ +use crate::{ + DelegationLayer, DelegationLayerArgs, DriverError, Layer, MerkleTree, NftStateLayer, + OracleLayer, SingletonLayer, SpendContext, WriterLayerArgs, DELEGATION_LAYER_PUZZLE_HASH, + DL_METADATA_UPDATER_PUZZLE_HASH, +}; +use chia_protocol::{Bytes, Bytes32}; +use chia_puzzles::nft::NftStateLayerArgs; +use clvm_traits::{ClvmDecoder, ClvmEncoder, FromClvm, FromClvmError, Raw, ToClvm, ToClvmError}; +use clvm_utils::{tree_hash, CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::Allocator; +use num_bigint::BigInt; + +pub type StandardDataStoreLayers = + SingletonLayer>; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[repr(u8)] +#[clvm(atom)] +pub enum HintType { + // 0 skipped to prevent confusion with () which is also none (end of list) + AdminPuzzle = 1, + WriterPuzzle = 2, + OraclePuzzle = 3, +} + +impl HintType { + pub fn from_value(value: u8) -> Option { + match value { + 1 => Some(Self::AdminPuzzle), + 2 => Some(Self::WriterPuzzle), + 3 => Some(Self::OraclePuzzle), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Copy)] +pub enum DelegatedPuzzle { + Admin(TreeHash), // puzzle hash + Writer(TreeHash), // inner puzzle hash + Oracle(Bytes32, u64), // oracle fee puzzle hash, fee amount +} + +impl DelegatedPuzzle { + pub fn from_memos(remaining_memos: &mut Vec) -> Result { + if remaining_memos.len() < 2 { + return Err(DriverError::MissingMemo); + } + + let first_memo = remaining_memos.remove(0); + if first_memo.len() != 1 { + return Err(DriverError::InvalidMemo); + } + let puzzle_type = HintType::from_value(first_memo[0]); + + // under current specs, first value will always be a puzzle hash + let puzzle_hash: TreeHash = TreeHash::new( + remaining_memos + .remove(0) + .to_vec() + .try_into() + .map_err(|_| DriverError::InvalidMemo)?, + ); + + match puzzle_type { + Some(HintType::AdminPuzzle) => Ok(DelegatedPuzzle::Admin(puzzle_hash)), + Some(HintType::WriterPuzzle) => Ok(DelegatedPuzzle::Writer(puzzle_hash)), + Some(HintType::OraclePuzzle) => { + if remaining_memos.is_empty() { + return Err(DriverError::MissingMemo); + } + + // puzzle hash bech32m_decode(oracle_address), not puzzle hash of the whole oracle puzze! + let oracle_fee: u64 = BigInt::from_signed_bytes_be(&remaining_memos.remove(0)) + .to_u64_digits() + .1[0]; + + Ok(DelegatedPuzzle::Oracle(puzzle_hash.into(), oracle_fee)) + } + None => Err(DriverError::MissingMemo), + } + } +} + +pub trait MetadataWithRootHash { + fn root_hash(&self) -> Bytes32; + fn root_hash_only(root_hash: Bytes32) -> Self; +} + +impl MetadataWithRootHash for DataStoreMetadata { + fn root_hash(&self) -> Bytes32 { + self.root_hash + } + + fn root_hash_only(root_hash: Bytes32) -> Self { + Self { + root_hash, + label: None, + description: None, + bytes: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct DataStoreMetadata { + pub root_hash: Bytes32, + pub label: Option, + pub description: Option, + pub bytes: Option, +} + +impl> FromClvm for DataStoreMetadata { + fn from_clvm(decoder: &D, node: N) -> Result { + let (root_hash, items) = <(Bytes32, Vec<(String, Raw)>)>::from_clvm(decoder, node)?; + let mut metadata = Self::root_hash_only(root_hash); + + for (key, Raw(ptr)) in items { + match key.as_str() { + "l" => metadata.label = Some(String::from_clvm(decoder, ptr)?), + "d" => metadata.description = Some(String::from_clvm(decoder, ptr)?), + "b" => metadata.bytes = Some(u64::from_clvm(decoder, ptr)?), + _ => (), + } + } + + Ok(metadata) + } +} + +impl> ToClvm for DataStoreMetadata { + fn to_clvm(&self, encoder: &mut E) -> Result { + let mut items: Vec<(&str, Raw)> = Vec::new(); + + if let Some(label) = &self.label { + items.push(("l", Raw(label.to_clvm(encoder)?))); + } + + if let Some(description) = &self.description { + items.push(("d", Raw(description.to_clvm(encoder)?))); + } + + if let Some(bytes) = self.bytes { + items.push(("b", Raw(bytes.to_clvm(encoder)?))); + } + + (self.root_hash, items).to_clvm(encoder) + } +} + +#[must_use] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DataStoreInfo { + pub launcher_id: Bytes32, + pub metadata: M, + pub owner_puzzle_hash: Bytes32, + pub delegated_puzzles: Vec, +} + +impl DataStoreInfo { + pub fn new( + launcher_id: Bytes32, + metadata: M, + owner_puzzle_hash: Bytes32, + delegated_puzzles: Vec, + ) -> Self { + Self { + launcher_id, + metadata, + owner_puzzle_hash, + delegated_puzzles, + } + } + + pub fn from_layers_with_delegation_layer( + layers: StandardDataStoreLayers, + delegated_puzzles: Vec, + ) -> Self { + Self { + launcher_id: layers.launcher_id, + metadata: layers.inner_puzzle.metadata, + owner_puzzle_hash: layers.inner_puzzle.inner_puzzle.owner_puzzle_hash, + delegated_puzzles, + } + } + + pub fn from_layers_without_delegation_layer(layers: StandardDataStoreLayers) -> Self + where + I: ToTreeHash, + { + Self { + launcher_id: layers.launcher_id, + metadata: layers.inner_puzzle.metadata, + owner_puzzle_hash: layers.inner_puzzle.inner_puzzle.tree_hash().into(), + delegated_puzzles: vec![], + } + } + + pub fn into_layers_with_delegation_layer( + self, + ctx: &mut SpendContext, + ) -> Result, DriverError> { + Ok(SingletonLayer::new( + self.launcher_id, + NftStateLayer::new( + self.metadata, + DL_METADATA_UPDATER_PUZZLE_HASH.into(), + DelegationLayer::new( + self.launcher_id, + self.owner_puzzle_hash, + get_merkle_tree(ctx, self.delegated_puzzles)?.root, + ), + ), + )) + } + + #[must_use] + pub fn into_layers_without_delegation_layer( + self, + innermost_layer: I, + ) -> StandardDataStoreLayers { + SingletonLayer::new( + self.launcher_id, + NftStateLayer::new( + self.metadata, + DL_METADATA_UPDATER_PUZZLE_HASH.into(), + innermost_layer, + ), + ) + } + + pub fn inner_puzzle_hash(&self, ctx: &mut SpendContext) -> Result + where + M: ToClvm, + { + let metadata_ptr = ctx.alloc(&self.metadata)?; + + if !self.delegated_puzzles.is_empty() { + return Ok(NftStateLayerArgs::curry_tree_hash( + ctx.tree_hash(metadata_ptr), + CurriedProgram { + program: DELEGATION_LAYER_PUZZLE_HASH, + args: DelegationLayerArgs { + mod_hash: DELEGATION_LAYER_PUZZLE_HASH.into(), + launcher_id: self.launcher_id, + owner_puzzle_hash: self.owner_puzzle_hash, + merkle_root: get_merkle_tree(ctx, self.delegated_puzzles.clone())?.root, + }, + } + .tree_hash(), + )); + } + + let inner_ph_hash: TreeHash = self.owner_puzzle_hash.into(); + Ok(NftStateLayerArgs::curry_tree_hash( + ctx.tree_hash(metadata_ptr), + inner_ph_hash, + )) + } +} + +pub fn get_merkle_tree( + ctx: &mut SpendContext, + delegated_puzzles: Vec, +) -> Result { + let mut leaves = Vec::::with_capacity(delegated_puzzles.len()); + + for dp in delegated_puzzles { + match dp { + DelegatedPuzzle::Admin(puzzle_hash) => { + leaves.push(puzzle_hash.into()); + } + DelegatedPuzzle::Writer(inner_puzzle_hash) => { + leaves.push(WriterLayerArgs::curry_tree_hash(inner_puzzle_hash).into()); + } + DelegatedPuzzle::Oracle(oracle_puzzle_hash, oracle_fee) => { + let oracle_full_puzzle_ptr = OracleLayer::new(oracle_puzzle_hash, oracle_fee) + .ok_or(DriverError::OddOracleFee)? + .construct_puzzle(ctx)?; + + leaves.push(tree_hash(&ctx.allocator, oracle_full_puzzle_ptr).into()); + } + } + } + + Ok(MerkleTree::new(&leaves)) +} diff --git a/crates/chia-sdk-driver/src/primitives/datalayer/datastore_launcher.rs b/crates/chia-sdk-driver/src/primitives/datalayer/datastore_launcher.rs new file mode 100644 index 00000000..c2f0c3c4 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/datalayer/datastore_launcher.rs @@ -0,0 +1,198 @@ +use chia_protocol::Bytes32; +use chia_puzzles::{ + nft::{NftStateLayerArgs, NFT_STATE_LAYER_PUZZLE_HASH}, + EveProof, Proof, +}; +use chia_sdk_types::Conditions; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::Allocator; + +use crate::{ + DelegationLayerArgs, DriverError, Launcher, SpendContext, DL_METADATA_UPDATER_PUZZLE_HASH, +}; + +use super::{get_merkle_tree, DataStore, DataStoreInfo, DelegatedPuzzle, DlLauncherKvList}; + +impl Launcher { + pub fn mint_datastore( + self, + ctx: &mut SpendContext, + metadata: M, + owner_puzzle_hash: TreeHash, + delegated_puzzles: Vec, + ) -> Result<(Conditions, DataStore), DriverError> + where + M: ToClvm + FromClvm + Clone, + { + let launcher_coin = self.coin(); + let launcher_id = launcher_coin.coin_id(); + + let inner_puzzle_hash: TreeHash = if delegated_puzzles.is_empty() { + owner_puzzle_hash + } else { + DelegationLayerArgs::curry_tree_hash( + launcher_id, + owner_puzzle_hash.into(), + get_merkle_tree(ctx, delegated_puzzles.clone())?.root, + ) + }; + + let metadata_ptr = ctx.alloc(&metadata)?; + let metadata_hash = ctx.tree_hash(metadata_ptr); + let state_layer_hash = CurriedProgram { + program: NFT_STATE_LAYER_PUZZLE_HASH, + args: NftStateLayerArgs:: { + mod_hash: NFT_STATE_LAYER_PUZZLE_HASH.into(), + metadata: metadata_hash, + metadata_updater_puzzle_hash: DL_METADATA_UPDATER_PUZZLE_HASH.into(), + inner_puzzle: inner_puzzle_hash, + }, + } + .tree_hash(); + + let mut memos = DataStore::::get_recreation_memos( + Bytes32::default(), + owner_puzzle_hash, + delegated_puzzles.clone(), + ) + .into_iter() + .skip(1) + .collect(); + if delegated_puzzles.is_empty() { + memos = vec![]; + } + let kv_list = DlLauncherKvList { + metadata: metadata.clone(), + state_layer_inner_puzzle_hash: inner_puzzle_hash.into(), + memos, + }; + + let (chained_spend, eve_coin) = self.spend(ctx, state_layer_hash.into(), kv_list)?; + + let proof = Proof::Eve(EveProof { + parent_parent_coin_info: launcher_coin.parent_coin_info, + parent_amount: launcher_coin.amount, + }); + + let data_store = DataStore { + coin: eve_coin, + proof, + info: DataStoreInfo { + launcher_id, + metadata, + owner_puzzle_hash: owner_puzzle_hash.into(), + delegated_puzzles, + }, + }; + + Ok((chained_spend, data_store)) + } +} + +#[cfg(test)] +mod tests { + use chia_bls::SecretKey; + use chia_puzzles::standard::StandardArgs; + use chia_sdk_test::{test_secret_keys, Simulator}; + use rstest::rstest; + + use crate::{ + tests::{ByteSize, Description, Label, RootHash}, + DataStoreMetadata, StandardLayer, + }; + + use super::*; + + #[rstest] + fn test_datastore_launch( + #[values(true, false)] use_label: bool, + #[values(true, false)] use_description: bool, + #[values(true, false)] use_byte_size: bool, + #[values(true, false)] with_writer: bool, + #[values(true, false)] with_admin: bool, + #[values(true, false)] with_oracle: bool, + ) -> anyhow::Result<()> { + let mut sim = Simulator::new(); + + let [owner_sk, admin_sk, writer_sk]: [SecretKey; 3] = + test_secret_keys(3)?.try_into().unwrap(); + + let owner_pk = owner_sk.public_key(); + let admin_pk = admin_sk.public_key(); + let writer_pk = writer_sk.public_key(); + + let oracle_puzzle_hash: Bytes32 = [7; 32].into(); + let oracle_fee = 1000; + + let owner_puzzle_hash = StandardArgs::curry_tree_hash(owner_pk).into(); + let coin = sim.new_coin(owner_puzzle_hash, 1); + + let ctx = &mut SpendContext::new(); + + let admin_delegated_puzzle = + DelegatedPuzzle::Admin(StandardArgs::curry_tree_hash(admin_pk)); + let writer_delegated_puzzle = + DelegatedPuzzle::Writer(StandardArgs::curry_tree_hash(writer_pk)); + let oracle_delegated_puzzle = DelegatedPuzzle::Oracle(oracle_puzzle_hash, oracle_fee); + + let mut delegated_puzzles: Vec = vec![]; + if with_admin { + delegated_puzzles.push(admin_delegated_puzzle); + } + if with_writer { + delegated_puzzles.push(writer_delegated_puzzle); + } + if with_oracle { + delegated_puzzles.push(oracle_delegated_puzzle); + } + + let metadata = DataStoreMetadata { + root_hash: RootHash::Zero.value(), + label: if use_label { Label::Some.value() } else { None }, + description: if use_description { + Description::Some.value() + } else { + None + }, + bytes: if use_byte_size { + ByteSize::Some.value() + } else { + None + }, + }; + + let (launch_singleton, datastore) = Launcher::new(coin.coin_id(), 1).mint_datastore( + ctx, + metadata.clone(), + owner_puzzle_hash.into(), + delegated_puzzles, + )?; + StandardLayer::new(owner_pk).spend(ctx, coin, launch_singleton)?; + + let spends = ctx.take(); + for spend in spends.clone() { + if spend.coin.coin_id() == datastore.info.launcher_id { + let new_datastore = + DataStore::from_spend(&mut ctx.allocator, &spend, &[])?.unwrap(); + + assert_eq!(datastore, new_datastore); + } + + ctx.insert(spend); + } + + assert_eq!(datastore.info.metadata, metadata); + + sim.spend_coins(spends, &[owner_sk, admin_sk, writer_sk])?; + + // Make sure the datastore was created. + let coin_state = sim + .coin_state(datastore.coin.coin_id()) + .expect("expected datastore coin"); + assert_eq!(coin_state.coin, datastore.coin); + assert!(coin_state.created_height.is_some()); + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/did.rs b/crates/chia-sdk-driver/src/primitives/did.rs new file mode 100644 index 00000000..338d44f0 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/did.rs @@ -0,0 +1,384 @@ +use chia_protocol::Coin; +use chia_puzzles::{did::DidSolution, singleton::SingletonSolution, LineageProof, Proof}; +use chia_sdk_types::{run_puzzle, Condition, Conditions}; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{tree_hash, ToTreeHash}; +use clvmr::{Allocator, NodePtr}; + +use crate::{ + DidLayer, DriverError, Layer, Puzzle, SingletonLayer, Spend, SpendContext, SpendWithConditions, +}; + +mod did_info; +mod did_launcher; + +pub use did_info::*; + +#[must_use] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Did { + pub coin: Coin, + pub proof: Proof, + pub info: DidInfo, +} + +impl Did { + pub fn new(coin: Coin, proof: Proof, info: DidInfo) -> Self { + Self { coin, proof, info } + } + + pub fn with_metadata(self, metadata: N) -> Did { + Did { + coin: self.coin, + proof: self.proof, + info: self.info.with_metadata(metadata), + } + } +} + +impl Did +where + M: ToTreeHash, +{ + /// Returns the lineage proof that would be used by the child. + pub fn child_lineage_proof(&self) -> LineageProof { + LineageProof { + parent_parent_coin_info: self.coin.parent_coin_info, + parent_inner_puzzle_hash: self.info.inner_puzzle_hash().into(), + parent_amount: self.coin.amount, + } + } + + /// Creates a wrapped spendable DID for the child. + pub fn wrapped_child(self) -> Self { + Self { + coin: Coin::new(self.coin.coin_id(), self.coin.puzzle_hash, self.coin.amount), + proof: Proof::Lineage(self.child_lineage_proof()), + info: self.info, + } + } +} + +impl Did +where + M: ToClvm + FromClvm + Clone, +{ + /// Creates a coin spend for this DID. + pub fn spend(&self, ctx: &mut SpendContext, inner_spend: Spend) -> Result<(), DriverError> { + let layers = self.info.clone().into_layers(inner_spend.puzzle); + + let puzzle = layers.construct_puzzle(ctx)?; + let solution = layers.construct_solution( + ctx, + SingletonSolution { + lineage_proof: self.proof, + amount: self.coin.amount, + inner_solution: DidSolution::Spend(inner_spend.solution), + }, + )?; + + ctx.spend(self.coin, Spend::new(puzzle, solution))?; + + Ok(()) + } + + /// Spends this DID with an inner puzzle that supports being spent with conditions. + pub fn spend_with( + &self, + ctx: &mut SpendContext, + inner: &I, + conditions: Conditions, + ) -> Result<(), DriverError> + where + I: SpendWithConditions, + { + let inner_spend = inner.spend_with_conditions(ctx, conditions)?; + self.spend(ctx, inner_spend) + } + + /// Recreates this DID and outputs additional conditions via the inner puzzle. + pub fn update_with_metadata( + self, + ctx: &mut SpendContext, + inner: &I, + metadata: N, + extra_conditions: Conditions, + ) -> Result, DriverError> + where + I: SpendWithConditions, + M: ToTreeHash, + N: ToClvm + ToTreeHash + Clone, + { + let new_inner_puzzle_hash = self + .info + .clone() + .with_metadata(metadata.clone()) + .inner_puzzle_hash(); + + self.spend_with( + ctx, + inner, + extra_conditions.create_coin( + new_inner_puzzle_hash.into(), + self.coin.amount, + vec![self.info.p2_puzzle_hash.into()], + ), + )?; + + Ok(self.wrapped_child().with_metadata(metadata)) + } + + /// Creates a new DID coin with the given metadata. + pub fn update( + self, + ctx: &mut SpendContext, + inner: &I, + extra_conditions: Conditions, + ) -> Result, DriverError> + where + M: ToTreeHash, + I: SpendWithConditions, + { + let metadata = self.info.metadata.clone(); + self.update_with_metadata(ctx, inner, metadata, extra_conditions) + } +} + +impl Did +where + M: ToClvm + FromClvm + Clone, +{ + pub fn parse_child( + allocator: &mut Allocator, + parent_coin: Coin, + parent_puzzle: Puzzle, + parent_solution: NodePtr, + coin: Coin, + ) -> Result, DriverError> + where + Self: Sized, + { + let Some(singleton_layer) = + SingletonLayer::::parse_puzzle(allocator, parent_puzzle)? + else { + return Ok(None); + }; + + let Some(did_layer) = + DidLayer::::parse_puzzle(allocator, singleton_layer.inner_puzzle)? + else { + return Ok(None); + }; + + if singleton_layer.launcher_id != did_layer.launcher_id { + return Err(DriverError::InvalidSingletonStruct); + } + + let singleton_solution = + SingletonLayer::::parse_solution(allocator, parent_solution)?; + + let output = run_puzzle( + allocator, + singleton_layer.inner_puzzle.ptr(), + singleton_solution.inner_solution, + )?; + let conditions = Vec::::from_clvm(allocator, output)?; + + let Some(create_coin) = conditions + .into_iter() + .filter_map(Condition::into_create_coin) + .find(|create_coin| create_coin.amount % 2 == 1) + else { + return Err(DriverError::MissingChild); + }; + + let Some(hint) = create_coin + .memos + .into_iter() + .find_map(|memo| memo.try_into().ok()) + else { + return Err(DriverError::MissingHint); + }; + + let metadata_ptr = did_layer.metadata.to_clvm(allocator)?; + let metadata_hash = tree_hash(allocator, metadata_ptr); + let did_layer_hashed = did_layer.clone().with_metadata(metadata_hash); + + let parent_inner_puzzle_hash = did_layer_hashed.tree_hash().into(); + let layers = SingletonLayer::new(singleton_layer.launcher_id, did_layer); + + let mut info = DidInfo::from_layers(layers); + info.p2_puzzle_hash = hint; + + Ok(Some(Self { + coin, + proof: Proof::Lineage(LineageProof { + parent_parent_coin_info: parent_coin.parent_coin_info, + parent_inner_puzzle_hash, + parent_amount: parent_coin.amount, + }), + info, + })) + } +} + +#[cfg(test)] +mod tests { + use std::fmt; + + use chia_protocol::Bytes32; + use chia_sdk_test::Simulator; + use clvm_traits::clvm_list; + use rstest::rstest; + + use crate::{HashedPtr, Launcher, StandardLayer}; + + use super::*; + + #[test] + fn test_create_and_update_simple_did() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + let (sk, pk, puzzle_hash, coin) = sim.new_p2(1)?; + let p2 = StandardLayer::new(pk); + + let launcher = Launcher::new(coin.coin_id(), 1); + let (create_did, did) = launcher.create_simple_did(ctx, &p2)?; + p2.spend(ctx, coin, create_did)?; + sim.spend_coins(ctx.take(), &[sk])?; + + assert_eq!(did.info.recovery_list_hash, None); + assert_eq!(did.info.num_verifications_required, 1); + assert_eq!(did.info.p2_puzzle_hash, puzzle_hash); + + Ok(()) + } + + #[rstest] + fn test_create_and_update_did( + #[values(None, Some(Bytes32::default()))] recovery_list_hash: Option, + #[values(0, 1, 3)] num_verifications_required: u64, + #[values((), "Atom".to_string(), clvm_list!("Complex".to_string(), 42), 100)] + metadata: impl ToClvm + + FromClvm + + ToTreeHash + + Clone + + PartialEq + + fmt::Debug, + ) -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + let (sk, pk, puzzle_hash, coin) = sim.new_p2(1)?; + let p2 = StandardLayer::new(pk); + + let launcher = Launcher::new(coin.coin_id(), 1); + let (create_did, did) = launcher.create_did( + ctx, + recovery_list_hash, + num_verifications_required, + metadata.clone(), + &p2, + )?; + p2.spend(ctx, coin, create_did)?; + sim.spend_coins(ctx.take(), &[sk])?; + + assert_eq!(did.info.recovery_list_hash, recovery_list_hash); + assert_eq!( + did.info.num_verifications_required, + num_verifications_required + ); + assert_eq!(did.info.metadata, metadata); + assert_eq!(did.info.p2_puzzle_hash, puzzle_hash); + + Ok(()) + } + + #[test] + fn test_update_did_metadata() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + let (sk, pk, _puzzle_hash, coin) = sim.new_p2(1)?; + let p2 = StandardLayer::new(pk); + + let launcher = Launcher::new(coin.coin_id(), 1); + let (create_did, did) = launcher.create_simple_did(ctx, &p2)?; + p2.spend(ctx, coin, create_did)?; + sim.spend_coins(ctx.take(), &[sk])?; + + let new_metadata = "New Metadata".to_string(); + let updated_did = + did.update_with_metadata(ctx, &p2, new_metadata.clone(), Conditions::default())?; + + assert_eq!(updated_did.info.metadata, new_metadata); + + Ok(()) + } + + #[test] + fn test_nodeptr_metadata() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + let (sk, pk, _puzzle_hash, coin) = sim.new_p2(1)?; + let p2 = StandardLayer::new(pk); + + let launcher = Launcher::new(coin.coin_id(), 1); + let (create_did, did) = launcher.create_did(ctx, None, 1, HashedPtr::NIL, &p2)?; + p2.spend(ctx, coin, create_did)?; + sim.spend_coins(ctx.take(), &[sk])?; + + let new_metadata = HashedPtr::from_ptr(&ctx.allocator, ctx.allocator.one()); + let updated_did = + did.update_with_metadata(ctx, &p2, new_metadata, Conditions::default())?; + + assert_eq!(updated_did.info.metadata, new_metadata); + + Ok(()) + } + + #[test] + fn test_parse_did() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + let (sk, pk, _puzzle_hash, coin) = sim.new_p2(1)?; + let p2 = StandardLayer::new(pk); + + let (create_did, expected_did) = + Launcher::new(coin.coin_id(), 1).create_simple_did(ctx, &p2)?; + p2.spend(ctx, coin, create_did)?; + + sim.spend_coins(ctx.take(), &[sk])?; + + let mut allocator = Allocator::new(); + + let puzzle_reveal = sim + .puzzle_reveal(expected_did.coin.parent_coin_info) + .expect("missing puzzle") + .to_clvm(&mut allocator)?; + + let solution = sim + .solution(expected_did.coin.parent_coin_info) + .expect("missing solution") + .to_clvm(&mut allocator)?; + + let parent_coin = sim + .coin_state(expected_did.coin.parent_coin_info) + .expect("missing parent coin state") + .coin; + + let puzzle = Puzzle::parse(&allocator, puzzle_reveal); + + let did = Did::<()>::parse_child( + &mut allocator, + parent_coin, + puzzle, + solution, + expected_did.coin, + )? + .expect("could not parse did"); + + assert_eq!(did, expected_did); + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/did/did_info.rs b/crates/chia-sdk-driver/src/primitives/did/did_info.rs new file mode 100644 index 00000000..8913bef3 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/did/did_info.rs @@ -0,0 +1,148 @@ +use chia_protocol::Bytes32; +use chia_puzzles::{did::DidArgs, singleton::SingletonStruct}; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{ToTreeHash, TreeHash}; +use clvmr::Allocator; + +use crate::{DidLayer, DriverError, Layer, Puzzle, SingletonLayer}; + +pub type StandardDidLayers = SingletonLayer>; + +#[must_use] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DidInfo { + pub launcher_id: Bytes32, + pub recovery_list_hash: Option, + pub num_verifications_required: u64, + pub metadata: M, + pub p2_puzzle_hash: Bytes32, +} + +impl DidInfo { + pub fn new( + launcher_id: Bytes32, + recovery_list_hash: Option, + num_verifications_required: u64, + metadata: M, + p2_puzzle_hash: Bytes32, + ) -> Self { + Self { + launcher_id, + recovery_list_hash, + num_verifications_required, + metadata, + p2_puzzle_hash, + } + } + + /// Parses the DID info and p2 puzzle that corresponds to the p2 puzzle hash. + pub fn parse( + allocator: &Allocator, + puzzle: Puzzle, + ) -> Result, DriverError> + where + M: ToClvm + FromClvm, + { + let Some(layers) = StandardDidLayers::::parse_puzzle(allocator, puzzle)? else { + return Ok(None); + }; + + let p2_puzzle = layers.inner_puzzle.inner_puzzle; + + Ok(Some((Self::from_layers(layers), p2_puzzle))) + } + + pub fn from_layers(layers: StandardDidLayers) -> Self + where + I: ToTreeHash, + { + Self { + launcher_id: layers.launcher_id, + recovery_list_hash: layers.inner_puzzle.recovery_list_hash, + num_verifications_required: layers.inner_puzzle.num_verifications_required, + metadata: layers.inner_puzzle.metadata, + p2_puzzle_hash: layers.inner_puzzle.inner_puzzle.tree_hash().into(), + } + } + + #[must_use] + pub fn into_layers(self, p2_puzzle: I) -> StandardDidLayers { + SingletonLayer::new( + self.launcher_id, + DidLayer::new( + self.launcher_id, + self.recovery_list_hash, + self.num_verifications_required, + self.metadata, + p2_puzzle, + ), + ) + } + + pub fn with_metadata(self, metadata: N) -> DidInfo { + DidInfo { + launcher_id: self.launcher_id, + recovery_list_hash: self.recovery_list_hash, + num_verifications_required: self.num_verifications_required, + metadata, + p2_puzzle_hash: self.p2_puzzle_hash, + } + } + + pub fn inner_puzzle_hash(&self) -> TreeHash + where + M: ToTreeHash, + { + DidArgs::curry_tree_hash( + self.p2_puzzle_hash.into(), + self.recovery_list_hash, + self.num_verifications_required, + SingletonStruct::new(self.launcher_id), + self.metadata.tree_hash(), + ) + } +} + +#[cfg(test)] +mod tests { + use chia_sdk_test::Simulator; + use chia_sdk_types::Conditions; + + use crate::{Launcher, SpendContext, StandardLayer}; + + use super::*; + + #[test] + fn test_parse_did_info() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + let (sk, pk, puzzle_hash, coin) = sim.new_p2(1)?; + let p2 = StandardLayer::new(pk); + + let custom_metadata = ["Metadata".to_string(), "Example".to_string()]; + let (create_did, did) = + Launcher::new(coin.coin_id(), 1).create_did(ctx, None, 1, custom_metadata, &p2)?; + p2.spend(ctx, coin, create_did)?; + + let original_did = did.clone(); + let _did = did.update(ctx, &p2, Conditions::new())?; + + sim.spend_coins(ctx.take(), &[sk])?; + + let puzzle_reveal = sim + .puzzle_reveal(original_did.coin.coin_id()) + .expect("missing did puzzle"); + + let mut allocator = Allocator::new(); + let ptr = puzzle_reveal.to_clvm(&mut allocator)?; + let puzzle = Puzzle::parse(&allocator, ptr); + let (did_info, p2_puzzle) = + DidInfo::<[String; 2]>::parse(&allocator, puzzle)?.expect("not a did"); + + assert_eq!(did_info, original_did.info); + assert_eq!(p2_puzzle.curried_puzzle_hash(), puzzle_hash.into()); + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/did/did_launcher.rs b/crates/chia-sdk-driver/src/primitives/did/did_launcher.rs new file mode 100644 index 00000000..4b6a4511 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/did/did_launcher.rs @@ -0,0 +1,82 @@ +use chia_protocol::Bytes32; +use chia_puzzles::{EveProof, Proof}; +use chia_sdk_types::Conditions; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::ToTreeHash; +use clvmr::Allocator; + +use crate::{DriverError, Launcher, SpendContext, SpendWithConditions}; + +use super::{Did, DidInfo}; + +impl Launcher { + pub fn create_eve_did( + self, + ctx: &mut SpendContext, + p2_puzzle_hash: Bytes32, + recovery_list_hash: Option, + num_verifications_required: u64, + metadata: M, + ) -> Result<(Conditions, Did), DriverError> + where + M: ToClvm + FromClvm + ToTreeHash, + { + let launcher_coin = self.coin(); + + let did_info = DidInfo::new( + launcher_coin.coin_id(), + recovery_list_hash, + num_verifications_required, + metadata, + p2_puzzle_hash, + ); + + let inner_puzzle_hash = did_info.inner_puzzle_hash(); + let (launch_singleton, eve_coin) = self.spend(ctx, inner_puzzle_hash.into(), ())?; + + let proof = Proof::Eve(EveProof { + parent_parent_coin_info: launcher_coin.parent_coin_info, + parent_amount: launcher_coin.amount, + }); + + Ok((launch_singleton, Did::new(eve_coin, proof, did_info))) + } + + pub fn create_did( + self, + ctx: &mut SpendContext, + recovery_list_hash: Option, + num_verifications_required: u64, + metadata: M, + inner: &I, + ) -> Result<(Conditions, Did), DriverError> + where + M: ToClvm + FromClvm + ToTreeHash + Clone, + I: SpendWithConditions + ToTreeHash, + Self: Sized, + { + let (create_eve, eve) = self.create_eve_did( + ctx, + inner.tree_hash().into(), + recovery_list_hash, + num_verifications_required, + metadata, + )?; + + let did = eve.update(ctx, inner, Conditions::new())?; + + Ok((create_eve, did)) + } + + pub fn create_simple_did( + self, + ctx: &mut SpendContext, + inner: &I, + ) -> Result<(Conditions, Did<()>), DriverError> + where + I: SpendWithConditions + ToTreeHash, + Self: Sized, + { + self.create_did(ctx, None, 1, (), inner) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/intermediate_launcher.rs b/crates/chia-sdk-driver/src/primitives/intermediate_launcher.rs new file mode 100644 index 00000000..7818b44a --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/intermediate_launcher.rs @@ -0,0 +1,97 @@ +use chia_protocol::{Bytes32, Coin, CoinSpend}; +use chia_puzzles::{nft::NftIntermediateLauncherArgs, singleton::SINGLETON_LAUNCHER_PUZZLE_HASH}; +use chia_sdk_types::{announcement_id, Conditions}; +use clvm_utils::CurriedProgram; +use clvmr::{sha2::Sha256, Allocator}; + +use crate::{DriverError, SpendContext}; + +use super::Launcher; + +/// An intermediate launcher is a coin that is created prior to the actual launcher coin. +/// In this case, it automatically creates the launcher coin upon being spent. +/// +/// The purpose of this is to allow multiple launcher coins to be created from a single parent. +/// Without an intermediate launcher, they would all have the same coin id. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[must_use] +pub struct IntermediateLauncher { + mint_number: usize, + mint_total: usize, + intermediate_coin: Coin, + launcher_coin: Coin, +} + +impl IntermediateLauncher { + /// Create a new intermediate launcher with the given index. This makes the puzzle hash, and therefore coin id, unique. + pub fn new(parent_coin_id: Bytes32, mint_number: usize, mint_total: usize) -> Self { + let intermediate_puzzle_hash = + NftIntermediateLauncherArgs::curry_tree_hash(mint_number, mint_total).into(); + + let intermediate_coin = Coin::new(parent_coin_id, intermediate_puzzle_hash, 0); + + let launcher_coin = Coin::new( + intermediate_coin.coin_id(), + SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + 1, + ); + + Self { + mint_number, + mint_total, + intermediate_coin, + launcher_coin, + } + } + + /// The intermediate coin that will be created when the parent is spent. + pub fn intermediate_coin(&self) -> Coin { + self.intermediate_coin + } + + /// The singleton launcher coin that will be created when the intermediate coin is spent. + pub fn launcher_coin(&self) -> Coin { + self.launcher_coin + } + + /// Spends the intermediate coin to create the launcher coin. + pub fn create(self, ctx: &mut SpendContext) -> Result { + let mut parent = Conditions::new(); + + let intermediate_puzzle = ctx.nft_intermediate_launcher()?; + + let puzzle = ctx.alloc(&CurriedProgram { + program: intermediate_puzzle, + args: NftIntermediateLauncherArgs::new(self.mint_number, self.mint_total), + })?; + + parent = parent.create_coin(self.intermediate_coin.puzzle_hash, 0, Vec::new()); + + let puzzle_reveal = ctx.serialize(&puzzle)?; + let solution = ctx.serialize(&())?; + + ctx.insert(CoinSpend::new( + self.intermediate_coin, + puzzle_reveal, + solution, + )); + + let mut index_message = Sha256::new(); + index_message.update(usize_to_bytes(self.mint_number)); + index_message.update(usize_to_bytes(self.mint_total)); + + Ok(Launcher::from_coin( + self.launcher_coin, + parent.assert_coin_announcement(announcement_id( + self.intermediate_coin.coin_id(), + index_message.finalize(), + )), + )) + } +} + +fn usize_to_bytes(value: usize) -> Vec { + let mut allocator = Allocator::new(); + let atom = allocator.new_number(value.into()).unwrap(); + allocator.atom(atom).as_ref().to_vec() +} diff --git a/crates/chia-sdk-driver/src/primitives/launcher.rs b/crates/chia-sdk-driver/src/primitives/launcher.rs new file mode 100644 index 00000000..63175576 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/launcher.rs @@ -0,0 +1,221 @@ +#![allow(clippy::missing_const_for_fn)] + +use chia_protocol::{Bytes32, Coin, CoinSpend, Program}; +use chia_puzzles::singleton::{ + LauncherSolution, SingletonArgs, SINGLETON_LAUNCHER_PUZZLE, SINGLETON_LAUNCHER_PUZZLE_HASH, +}; +use chia_sdk_types::{announcement_id, Conditions}; +use clvm_traits::ToClvm; +use clvmr::Allocator; + +use crate::{DriverError, SpendContext}; + +/// A singleton launcher is a coin that is spent within the same block to create a singleton. +/// The first coin that is created is known as an "eve" singleton. +/// The [`Launcher`] type allows you to get the launcher id before committing to creating the singleton. +#[derive(Debug, Clone)] +#[must_use] +pub struct Launcher { + coin: Coin, + conditions: Conditions, + singleton_amount: u64, +} + +impl Launcher { + /// Creates a new [`Launcher`] with the specified launcher coin and parent spend conditions. + pub fn from_coin(coin: Coin, conditions: Conditions) -> Self { + Self { + coin, + conditions, + singleton_amount: coin.amount, + } + } + + /// The parent coin specified when constructing the [`Launcher`] will create the launcher coin. + /// By default, no hint is used when creating the launcher coin. To specify a hint, use [`Launcher::hinted`]. + pub fn new(parent_coin_id: Bytes32, amount: u64) -> Self { + Self::from_coin( + Coin::new( + parent_coin_id, + SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + amount, + ), + Conditions::new().create_coin( + SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + amount, + Vec::new(), + ), + ) + } + + /// The parent coin specified when constructing the [`Launcher`] will create the launcher coin. + /// The created launcher coin will be hinted to make identifying it easier later. + pub fn hinted(parent_coin_id: Bytes32, amount: u64, hint: Bytes32) -> Self { + Self::from_coin( + Coin::new( + parent_coin_id, + SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + amount, + ), + Conditions::new().create_coin( + SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + amount, + vec![hint.into()], + ), + ) + } + + /// The parent coin specified when constructing the [`Launcher`] will create the launcher coin. + /// By default, no hint is used when creating the coin. To specify a hint, use [`Launcher::create_early_hinted`]. + /// + /// This method is used to create the launcher coin immediately from the parent, then spend it later attached to any coin spend. + /// For example, this is useful for minting NFTs from intermediate coins created with an earlier instance of a DID. + pub fn create_early(parent_coin_id: Bytes32, amount: u64) -> (Conditions, Self) { + ( + Conditions::new().create_coin( + SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + amount, + Vec::new(), + ), + Self::from_coin( + Coin::new( + parent_coin_id, + SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + amount, + ), + Conditions::new(), + ), + ) + } + + /// The parent coin specified when constructing the [`Launcher`] will create the launcher coin. + /// The created launcher coin will be hinted to make identifying it easier later. + /// + /// This method is used to create the launcher coin immediately from the parent, then spend it later attached to any coin spend. + /// For example, this is useful for minting NFTs from intermediate coins created with an earlier instance of a DID. + pub fn create_early_hinted( + parent_coin_id: Bytes32, + amount: u64, + hint: Bytes32, + ) -> (Conditions, Self) { + ( + Conditions::new().create_coin( + SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + amount, + vec![hint.into()], + ), + Self::from_coin( + Coin::new( + parent_coin_id, + SINGLETON_LAUNCHER_PUZZLE_HASH.into(), + amount, + ), + Conditions::new(), + ), + ) + } + + /// Changes the singleton amount to differ from the launcher amount. + /// This is useful in situations where the launcher amount is 0 and the singleton amount is 1, for example. + pub fn with_singleton_amount(mut self, singleton_amount: u64) -> Self { + self.singleton_amount = singleton_amount; + self + } + + /// The singleton launcher coin that will be created when the parent is spent. + pub fn coin(&self) -> Coin { + self.coin + } + + /// Spends the launcher coin to create the eve singleton. + /// Includes an optional metadata value that is traditionally a list of key value pairs. + pub fn spend( + self, + ctx: &mut SpendContext, + singleton_inner_puzzle_hash: Bytes32, + key_value_list: T, + ) -> Result<(Conditions, Coin), DriverError> + where + T: ToClvm, + { + let singleton_puzzle_hash = + SingletonArgs::curry_tree_hash(self.coin.coin_id(), singleton_inner_puzzle_hash.into()) + .into(); + + let solution_ptr = ctx.alloc(&LauncherSolution { + singleton_puzzle_hash, + amount: self.singleton_amount, + key_value_list, + })?; + + let solution = ctx.serialize(&solution_ptr)?; + + ctx.insert(CoinSpend::new( + self.coin, + Program::from(SINGLETON_LAUNCHER_PUZZLE.to_vec()), + solution, + )); + + let singleton_coin = Coin::new( + self.coin.coin_id(), + singleton_puzzle_hash, + self.singleton_amount, + ); + + Ok(( + self.conditions.assert_coin_announcement(announcement_id( + self.coin.coin_id(), + ctx.tree_hash(solution_ptr), + )), + singleton_coin, + )) + } +} + +#[cfg(test)] +mod tests { + use crate::StandardLayer; + + use super::*; + + use chia_sdk_test::Simulator; + + #[test] + fn test_singleton_launcher() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let (sk, pk, _puzzle_hash, coin) = sim.new_p2(1)?; + + let ctx = &mut SpendContext::new(); + let launcher = Launcher::new(coin.coin_id(), 1); + assert_eq!(launcher.coin.amount, 1); + + let (conditions, singleton) = launcher.spend(ctx, Bytes32::default(), ())?; + StandardLayer::new(pk).spend(ctx, coin, conditions)?; + assert_eq!(singleton.amount, 1); + + sim.spend_coins(ctx.take(), &[sk])?; + + Ok(()) + } + + #[test] + fn test_singleton_launcher_custom_amount() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let (sk, pk, _puzzle_hash, coin) = sim.new_p2(1)?; + + let ctx = &mut SpendContext::new(); + let launcher = Launcher::new(coin.coin_id(), 0); + assert_eq!(launcher.coin.amount, 0); + + let (conditions, singleton) = + launcher + .with_singleton_amount(1) + .spend(ctx, Bytes32::default(), ())?; + StandardLayer::new(pk).spend(ctx, coin, conditions)?; + assert_eq!(singleton.amount, 1); + + sim.spend_coins(ctx.take(), &[sk])?; + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/nft.rs b/crates/chia-sdk-driver/src/primitives/nft.rs new file mode 100644 index 00000000..6bc0aae1 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/nft.rs @@ -0,0 +1,522 @@ +use chia_protocol::{Bytes32, Coin}; +use chia_puzzles::{ + nft::{NftOwnershipLayerSolution, NftStateLayerSolution}, + singleton::{SingletonArgs, SingletonSolution}, + LineageProof, Proof, +}; +use chia_sdk_types::{run_puzzle, Condition, Conditions, NewMetadataOutput, TransferNft}; +use clvm_traits::{clvm_list, FromClvm, ToClvm}; +use clvm_utils::{tree_hash, ToTreeHash}; +use clvmr::{sha2::Sha256, Allocator, NodePtr}; + +use crate::{ + DriverError, Layer, NftOwnershipLayer, NftStateLayer, Puzzle, RoyaltyTransferLayer, + SingletonLayer, Spend, SpendContext, SpendWithConditions, +}; + +mod did_owner; +mod metadata_update; +mod nft_info; +mod nft_launcher; +mod nft_mint; + +pub use did_owner::*; +pub use metadata_update::*; +pub use nft_info::*; +pub use nft_mint::*; + +/// Everything that is required to spend an NFT coin. +#[must_use] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Nft { + /// The coin that holds this NFT. + pub coin: Coin, + /// The lineage proof for the singleton. + pub proof: Proof, + /// The info associated with the NFT, including the metadata. + pub info: NftInfo, +} + +impl Nft { + pub fn new(coin: Coin, proof: Proof, info: NftInfo) -> Self { + Nft { coin, proof, info } + } + + pub fn with_metadata(self, metadata: N) -> Nft { + Nft { + coin: self.coin, + proof: self.proof, + info: self.info.with_metadata(metadata), + } + } +} + +impl Nft +where + M: ToTreeHash, +{ + /// Returns the lineage proof that would be used by the child. + pub fn child_lineage_proof(&self) -> LineageProof { + LineageProof { + parent_parent_coin_info: self.coin.parent_coin_info, + parent_inner_puzzle_hash: self.info.inner_puzzle_hash().into(), + parent_amount: self.coin.amount, + } + } + + /// Creates a new spendable NFT for the child. + pub fn wrapped_child( + &self, + p2_puzzle_hash: Bytes32, + owner: Option, + metadata: N, + ) -> Nft + where + M: Clone, + N: ToTreeHash, + { + let mut info = self.info.clone().with_metadata(metadata); + info.p2_puzzle_hash = p2_puzzle_hash; + info.current_owner = owner; + + let inner_puzzle_hash = info.inner_puzzle_hash(); + + Nft { + coin: Coin::new( + self.coin.coin_id(), + SingletonArgs::curry_tree_hash(info.launcher_id, inner_puzzle_hash).into(), + self.coin.amount, + ), + proof: Proof::Lineage(self.child_lineage_proof()), + info, + } + } +} + +impl Nft +where + M: ToClvm + FromClvm + Clone, +{ + /// Creates a coin spend for this NFT. + pub fn spend(&self, ctx: &mut SpendContext, inner_spend: Spend) -> Result<(), DriverError> { + let layers = self.info.clone().into_layers(inner_spend.puzzle); + + let puzzle = layers.construct_puzzle(ctx)?; + let solution = layers.construct_solution( + ctx, + SingletonSolution { + lineage_proof: self.proof, + amount: self.coin.amount, + inner_solution: NftStateLayerSolution { + inner_solution: NftOwnershipLayerSolution { + inner_solution: inner_spend.solution, + }, + }, + }, + )?; + + ctx.spend(self.coin, Spend::new(puzzle, solution))?; + + Ok(()) + } + + /// Spends this NFT with an inner puzzle that supports being spent with conditions. + pub fn spend_with( + &self, + ctx: &mut SpendContext, + inner: &I, + conditions: Conditions, + ) -> Result<(), DriverError> + where + I: SpendWithConditions, + { + let inner_spend = inner.spend_with_conditions(ctx, conditions)?; + self.spend(ctx, inner_spend) + } + + /// Transfers this NFT to a new p2 puzzle hash, with new metadata. + pub fn transfer_with_metadata( + self, + ctx: &mut SpendContext, + inner: &I, + p2_puzzle_hash: Bytes32, + metadata_update: Spend, + extra_conditions: Conditions, + ) -> Result, DriverError> + where + I: SpendWithConditions, + N: ToClvm + FromClvm + ToTreeHash, + M: ToTreeHash, + { + self.spend_with( + ctx, + inner, + extra_conditions + .create_coin( + p2_puzzle_hash, + self.coin.amount, + vec![p2_puzzle_hash.into()], + ) + .update_nft_metadata(metadata_update.puzzle, metadata_update.solution), + )?; + + let metadata_updater_solution = clvm_list!( + self.info.metadata.clone(), + self.info.metadata_updater_puzzle_hash, + metadata_update.solution + ) + .to_clvm(&mut ctx.allocator)?; + let ptr = run_puzzle( + &mut ctx.allocator, + metadata_update.puzzle, + metadata_updater_solution, + )?; + let output = ctx.extract::>(ptr)?; + + Ok(self.wrapped_child( + p2_puzzle_hash, + self.info.current_owner, + output.metadata_info.new_metadata, + )) + } + + /// Transfers this NFT to a new p2 puzzle hash. + /// + /// Note: This does not update the metadata. If you update the metadata manually, the child will be incorrect. + /// + /// Use can use the [`Self::transfer_with_metadata`] helper method to update the metadata. + /// Alternatively, construct a spend manually with [`Self::spend`] or [`Self::spend_with`]. + pub fn transfer( + self, + ctx: &mut SpendContext, + inner: &I, + p2_puzzle_hash: Bytes32, + extra_conditions: Conditions, + ) -> Result, DriverError> + where + M: ToTreeHash, + I: SpendWithConditions, + { + self.spend_with( + ctx, + inner, + extra_conditions.create_coin( + p2_puzzle_hash, + self.coin.amount, + vec![p2_puzzle_hash.into()], + ), + )?; + + let metadata = self.info.metadata.clone(); + + Ok(self.wrapped_child(p2_puzzle_hash, self.info.current_owner, metadata)) + } + + /// Transfers this NFT to a new p2 puzzle hash and updates the DID owner. + /// Returns a list of conditions to be used in the DID spend. + /// + /// Note: This does not update the metadata. If you update the metadata manually, the child will be incorrect. + /// + /// You can construct a spend manually with [`Self::spend`] or [`Self::spend_with`] if you need to update metadata + /// while transferring to a DID. This is not a common use case, so it's not implemented by default. + pub fn transfer_to_did( + self, + ctx: &mut SpendContext, + inner: &I, + p2_puzzle_hash: Bytes32, + new_owner: Option, + extra_conditions: Conditions, + ) -> Result<(Conditions, Nft), DriverError> + where + M: ToTreeHash, + I: SpendWithConditions, + { + let transfer_condition = TransferNft::new( + new_owner.map(|owner| owner.did_id), + Vec::new(), + new_owner.map(|owner| owner.inner_puzzle_hash), + ); + + self.spend_with( + ctx, + inner, + extra_conditions + .create_coin( + p2_puzzle_hash, + self.coin.amount, + vec![p2_puzzle_hash.into()], + ) + .with(transfer_condition.clone()), + )?; + + let metadata = self.info.metadata.clone(); + + let child = self.wrapped_child( + p2_puzzle_hash, + new_owner.map(|owner| owner.did_id), + metadata, + ); + + let did_conditions = Conditions::new().assert_puzzle_announcement(did_puzzle_assertion( + self.coin.puzzle_hash, + &transfer_condition, + )); + + Ok((did_conditions, child)) + } +} + +impl Nft +where + M: ToClvm + FromClvm + ToTreeHash, +{ + pub fn parse_child( + allocator: &mut Allocator, + parent_coin: Coin, + parent_puzzle: Puzzle, + parent_solution: NodePtr, + ) -> Result, DriverError> + where + Self: Sized, + { + let Some(singleton_layer) = + SingletonLayer::::parse_puzzle(allocator, parent_puzzle)? + else { + return Ok(None); + }; + + let Some(inner_layers) = + NftStateLayer::>::parse_puzzle( + allocator, + singleton_layer.inner_puzzle, + )? + else { + return Ok(None); + }; + + let parent_solution = SingletonLayer::< + NftStateLayer>, + >::parse_solution(allocator, parent_solution)?; + + let inner_puzzle = inner_layers.inner_puzzle.inner_puzzle; + let inner_solution = parent_solution.inner_solution.inner_solution.inner_solution; + + let output = run_puzzle(allocator, inner_puzzle.ptr(), inner_solution)?; + let conditions = Vec::::from_clvm(allocator, output)?; + + let mut create_coin = None; + let mut new_owner = None; + let mut new_metadata = None; + + for condition in conditions { + match condition { + Condition::CreateCoin(condition) if condition.amount % 2 == 1 => { + create_coin = Some(condition); + } + Condition::TransferNft(condition) => { + new_owner = Some(condition); + } + Condition::UpdateNftMetadata(condition) => { + new_metadata = Some(condition); + } + _ => {} + } + } + + let Some(create_coin) = create_coin else { + return Err(DriverError::MissingChild); + }; + + let mut layers = SingletonLayer::new(singleton_layer.launcher_id, inner_layers); + + if let Some(new_owner) = new_owner { + layers.inner_puzzle.inner_puzzle.current_owner = new_owner.did_id; + } + + if let Some(new_metadata) = new_metadata { + let output = run_puzzle( + allocator, + new_metadata.updater_puzzle_reveal, + new_metadata.updater_solution, + )?; + + let output = + NewMetadataOutput::::from_clvm(allocator, output)?.metadata_info; + layers.inner_puzzle.metadata = output.new_metadata; + layers.inner_puzzle.metadata_updater_puzzle_hash = output.new_updater_puzzle_hash; + } + + let mut info = NftInfo::from_layers(layers); + info.p2_puzzle_hash = create_coin.puzzle_hash; + + Ok(Some(Self { + coin: Coin::new( + parent_coin.coin_id(), + SingletonArgs::curry_tree_hash(info.launcher_id, info.inner_puzzle_hash()).into(), + create_coin.amount, + ), + proof: Proof::Lineage(LineageProof { + parent_parent_coin_info: parent_coin.parent_coin_info, + parent_inner_puzzle_hash: singleton_layer.inner_puzzle.curried_puzzle_hash().into(), + parent_amount: parent_coin.amount, + }), + info, + })) + } +} + +pub fn did_puzzle_assertion(nft_full_puzzle_hash: Bytes32, new_nft_owner: &TransferNft) -> Bytes32 { + let mut allocator = Allocator::new(); + + let new_nft_owner_args = clvm_list!( + new_nft_owner.did_id, + &new_nft_owner.trade_prices, + new_nft_owner.did_inner_puzzle_hash + ) + .to_clvm(&mut allocator) + .unwrap(); + + let mut hasher = Sha256::new(); + hasher.update(nft_full_puzzle_hash); + hasher.update([0xad, 0x4c]); + hasher.update(tree_hash(&allocator, new_nft_owner_args)); + + Bytes32::new(hasher.finalize()) +} + +#[cfg(test)] +mod tests { + use crate::{IntermediateLauncher, Launcher, NftMint, StandardLayer}; + + use super::*; + + use chia_puzzles::nft::NftMetadata; + use chia_sdk_test::Simulator; + + #[test] + fn test_nft_transfer() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + let (sk, pk, puzzle_hash, coin) = sim.new_p2(2)?; + let p2 = StandardLayer::new(pk); + + let (create_did, did) = Launcher::new(coin.coin_id(), 1).create_simple_did(ctx, &p2)?; + p2.spend(ctx, coin, create_did)?; + + let mint = NftMint::new( + NftMetadata::default(), + puzzle_hash, + 300, + Some(DidOwner::from_did_info(&did.info)), + ); + + let (mint_nft, nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1) + .create(ctx)? + .mint_nft(ctx, mint)?; + let _did = did.update(ctx, &p2, mint_nft)?; + let _nft = nft.transfer(ctx, &p2, puzzle_hash, Conditions::new())?; + + sim.spend_coins(ctx.take(), &[sk])?; + + Ok(()) + } + + #[test] + fn test_nft_lineage() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + let (sk, pk, puzzle_hash, coin) = sim.new_p2(2)?; + let p2 = StandardLayer::new(pk); + + let (create_did, did) = Launcher::new(coin.coin_id(), 1).create_simple_did(ctx, &p2)?; + p2.spend(ctx, coin, create_did)?; + + let mint = NftMint::new( + NftMetadata::default(), + puzzle_hash, + 300, + Some(DidOwner::from_did_info(&did.info)), + ); + + let (mint_nft, mut nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1) + .create(ctx)? + .mint_nft(ctx, mint)?; + + let mut did = did.update(ctx, &p2, mint_nft)?; + + for i in 0..5 { + let did_owner = DidOwner::from_did_info(&did.info); + + let (spend_nft, new_nft) = nft.transfer_to_did( + ctx, + &p2, + puzzle_hash, + if i % 2 == 0 { Some(did_owner) } else { None }, + Conditions::new(), + )?; + + nft = new_nft; + did = did.update(ctx, &p2, spend_nft)?; + } + + sim.spend_coins(ctx.take(), &[sk])?; + + Ok(()) + } + + #[test] + fn test_parse_nft() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + let (sk, pk, puzzle_hash, coin) = sim.new_p2(2)?; + let p2 = StandardLayer::new(pk); + + let (create_did, did) = Launcher::new(coin.coin_id(), 1).create_simple_did(ctx, &p2)?; + p2.spend(ctx, coin, create_did)?; + + let mut metadata = NftMetadata::default(); + metadata.data_uris.push("example.com".to_string()); + + let (mint_nft, nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1) + .create(ctx)? + .mint_nft( + ctx, + NftMint::new( + metadata, + puzzle_hash, + 300, + Some(DidOwner::from_did_info(&did.info)), + ), + )?; + let _did = did.update(ctx, &p2, mint_nft)?; + + let parent_coin = nft.coin; + let expected_nft = nft.transfer(ctx, &p2, puzzle_hash, Conditions::new())?; + + sim.spend_coins(ctx.take(), &[sk])?; + + let mut allocator = Allocator::new(); + + let puzzle_reveal = sim + .puzzle_reveal(parent_coin.coin_id()) + .expect("missing puzzle") + .to_clvm(&mut allocator)?; + + let solution = sim + .solution(parent_coin.coin_id()) + .expect("missing solution") + .to_clvm(&mut allocator)?; + + let puzzle = Puzzle::parse(&allocator, puzzle_reveal); + + let nft = Nft::::parse_child(&mut allocator, parent_coin, puzzle, solution)? + .expect("could not parse nft"); + + assert_eq!(nft, expected_nft); + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/nft/did_owner.rs b/crates/chia-sdk-driver/src/primitives/nft/did_owner.rs new file mode 100644 index 00000000..8bc4bd29 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/nft/did_owner.rs @@ -0,0 +1,29 @@ +use chia_protocol::Bytes32; +use clvm_utils::ToTreeHash; + +use crate::DidInfo; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DidOwner { + pub did_id: Bytes32, + pub inner_puzzle_hash: Bytes32, +} + +impl DidOwner { + pub fn new(did_id: Bytes32, inner_puzzle_hash: Bytes32) -> Self { + Self { + did_id, + inner_puzzle_hash, + } + } + + pub fn from_did_info(did_info: &DidInfo) -> Self + where + M: ToTreeHash, + { + Self { + did_id: did_info.launcher_id, + inner_puzzle_hash: did_info.inner_puzzle_hash().into(), + } + } +} diff --git a/crates/chia-sdk-driver/src/primitives/nft/metadata_update.rs b/crates/chia-sdk-driver/src/primitives/nft/metadata_update.rs new file mode 100644 index 00000000..a8dc17cf --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/nft/metadata_update.rs @@ -0,0 +1,19 @@ +use crate::{DriverError, Spend, SpendContext}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MetadataUpdate { + NewDataUri(String), + NewMetadataUri(String), + NewLicenseUri(String), +} + +impl MetadataUpdate { + pub fn spend(&self, ctx: &mut SpendContext) -> Result { + let solution = ctx.alloc(&match self { + Self::NewDataUri(uri) => ("u", uri), + Self::NewMetadataUri(uri) => ("mu", uri), + Self::NewLicenseUri(uri) => ("lu", uri), + })?; + Ok(Spend::new(ctx.nft_metadata_updater()?, solution)) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/nft/nft_info.rs b/crates/chia-sdk-driver/src/primitives/nft/nft_info.rs new file mode 100644 index 00000000..ecd7c273 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/nft/nft_info.rs @@ -0,0 +1,208 @@ +use chia_protocol::Bytes32; +use chia_puzzles::nft::{NftOwnershipLayerArgs, NftStateLayerArgs, NFT_STATE_LAYER_PUZZLE_HASH}; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::Allocator; + +use crate::{ + DriverError, Layer, NftOwnershipLayer, NftStateLayer, Puzzle, RoyaltyTransferLayer, + SingletonLayer, +}; + +pub type StandardNftLayers = + SingletonLayer>>; + +#[must_use] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NftInfo { + pub launcher_id: Bytes32, + pub metadata: M, + pub metadata_updater_puzzle_hash: Bytes32, + pub current_owner: Option, + pub royalty_puzzle_hash: Bytes32, + pub royalty_ten_thousandths: u16, + pub p2_puzzle_hash: Bytes32, +} + +impl NftInfo { + pub fn new( + launcher_id: Bytes32, + metadata: M, + metadata_updater_puzzle_hash: Bytes32, + current_owner: Option, + royalty_puzzle_hash: Bytes32, + royalty_ten_thousandths: u16, + p2_puzzle_hash: Bytes32, + ) -> Self { + Self { + launcher_id, + metadata, + metadata_updater_puzzle_hash, + current_owner, + royalty_puzzle_hash, + royalty_ten_thousandths, + p2_puzzle_hash, + } + } + + /// Parses the NFT info and p2 puzzle that corresponds to the p2 puzzle hash. + pub fn parse( + allocator: &Allocator, + puzzle: Puzzle, + ) -> Result, DriverError> + where + M: ToClvm + FromClvm, + { + let Some(layers) = StandardNftLayers::::parse_puzzle(allocator, puzzle)? else { + return Ok(None); + }; + + let p2_puzzle = layers.inner_puzzle.inner_puzzle.inner_puzzle; + + Ok(Some((Self::from_layers(layers), p2_puzzle))) + } + + pub fn from_layers(layers: StandardNftLayers) -> Self + where + I: ToTreeHash, + { + Self { + launcher_id: layers.launcher_id, + metadata: layers.inner_puzzle.metadata, + metadata_updater_puzzle_hash: layers.inner_puzzle.metadata_updater_puzzle_hash, + current_owner: layers.inner_puzzle.inner_puzzle.current_owner, + royalty_puzzle_hash: layers + .inner_puzzle + .inner_puzzle + .transfer_layer + .royalty_puzzle_hash, + royalty_ten_thousandths: layers + .inner_puzzle + .inner_puzzle + .transfer_layer + .royalty_ten_thousandths, + p2_puzzle_hash: layers + .inner_puzzle + .inner_puzzle + .inner_puzzle + .tree_hash() + .into(), + } + } + + #[must_use] + pub fn into_layers(self, p2_puzzle: I) -> StandardNftLayers { + SingletonLayer::new( + self.launcher_id, + NftStateLayer::new( + self.metadata, + self.metadata_updater_puzzle_hash, + NftOwnershipLayer::new( + self.current_owner, + RoyaltyTransferLayer::new( + self.launcher_id, + self.royalty_puzzle_hash, + self.royalty_ten_thousandths, + ), + p2_puzzle, + ), + ), + ) + } + + pub fn with_metadata(self, metadata: N) -> NftInfo { + NftInfo { + launcher_id: self.launcher_id, + metadata, + metadata_updater_puzzle_hash: self.metadata_updater_puzzle_hash, + current_owner: self.current_owner, + royalty_puzzle_hash: self.royalty_puzzle_hash, + royalty_ten_thousandths: self.royalty_ten_thousandths, + p2_puzzle_hash: self.p2_puzzle_hash, + } + } + + pub fn inner_puzzle_hash(&self) -> TreeHash + where + M: ToTreeHash, + { + CurriedProgram { + program: NFT_STATE_LAYER_PUZZLE_HASH, + args: NftStateLayerArgs { + mod_hash: NFT_STATE_LAYER_PUZZLE_HASH.into(), + metadata: self.metadata.tree_hash(), + metadata_updater_puzzle_hash: self.metadata_updater_puzzle_hash, + inner_puzzle: NftOwnershipLayerArgs::curry_tree_hash( + self.current_owner, + RoyaltyTransferLayer::new( + self.launcher_id, + self.royalty_puzzle_hash, + self.royalty_ten_thousandths, + ) + .tree_hash(), + self.p2_puzzle_hash.into(), + ), + }, + } + .tree_hash() + } +} + +#[cfg(test)] +mod tests { + use chia_puzzles::nft::NftMetadata; + use chia_sdk_test::Simulator; + use chia_sdk_types::Conditions; + + use crate::{DidOwner, IntermediateLauncher, Launcher, NftMint, SpendContext, StandardLayer}; + + use super::*; + + #[test] + fn test_parse_nft_info() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + let (sk, pk, puzzle_hash, coin) = sim.new_p2(2)?; + let p2 = StandardLayer::new(pk); + + let (create_did, did) = Launcher::new(coin.coin_id(), 1).create_simple_did(ctx, &p2)?; + p2.spend(ctx, coin, create_did)?; + + let mut metadata = NftMetadata::default(); + metadata.data_uris.push("example.com".to_string()); + + let (mint_nft, nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1) + .create(ctx)? + .mint_nft( + ctx, + NftMint::new( + metadata, + puzzle_hash, + 300, + Some(DidOwner::from_did_info(&did.info)), + ), + )?; + + let _did = did.update(ctx, &p2, mint_nft)?; + let original_nft = nft.clone(); + let _nft = nft.transfer(ctx, &p2, puzzle_hash, Conditions::new())?; + + sim.spend_coins(ctx.take(), &[sk])?; + + let puzzle_reveal = sim + .puzzle_reveal(original_nft.coin.coin_id()) + .expect("missing nft puzzle"); + + let mut allocator = Allocator::new(); + let ptr = puzzle_reveal.to_clvm(&mut allocator)?; + let puzzle = Puzzle::parse(&allocator, ptr); + let (nft_info, p2_puzzle) = + NftInfo::::parse(&allocator, puzzle)?.expect("not an nft"); + + assert_eq!(nft_info, original_nft.info); + assert_eq!(p2_puzzle.curried_puzzle_hash(), puzzle_hash.into()); + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/nft/nft_launcher.rs b/crates/chia-sdk-driver/src/primitives/nft/nft_launcher.rs new file mode 100644 index 00000000..d5d51eb8 --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/nft/nft_launcher.rs @@ -0,0 +1,296 @@ +use chia_protocol::Bytes32; +use chia_puzzles::{EveProof, Proof}; +use chia_sdk_types::{Conditions, TransferNft}; +use clvm_traits::{clvm_quote, FromClvm, ToClvm}; +use clvm_utils::ToTreeHash; +use clvmr::{Allocator, NodePtr}; + +use crate::{did_puzzle_assertion, DriverError, Launcher, Spend, SpendContext}; + +use super::{Nft, NftInfo, NftMint}; + +impl Launcher { + pub fn mint_eve_nft( + self, + ctx: &mut SpendContext, + p2_puzzle_hash: Bytes32, + metadata: M, + metadata_updater_puzzle_hash: Bytes32, + royalty_puzzle_hash: Bytes32, + royalty_ten_thousandths: u16, + ) -> Result<(Conditions, Nft), DriverError> + where + M: ToClvm + FromClvm + ToTreeHash + Clone, + { + let launcher_coin = self.coin(); + + let nft_info = NftInfo::new( + launcher_coin.coin_id(), + metadata, + metadata_updater_puzzle_hash, + None, + royalty_puzzle_hash, + royalty_ten_thousandths, + p2_puzzle_hash, + ); + + let inner_puzzle_hash = nft_info.inner_puzzle_hash(); + let (launch_singleton, eve_coin) = self.spend(ctx, inner_puzzle_hash.into(), ())?; + + let proof = Proof::Eve(EveProof { + parent_parent_coin_info: launcher_coin.parent_coin_info, + parent_amount: launcher_coin.amount, + }); + + Ok(( + launch_singleton.create_puzzle_announcement(launcher_coin.coin_id().to_vec().into()), + Nft::new(eve_coin, proof, nft_info), + )) + } + + pub fn mint_nft( + self, + ctx: &mut SpendContext, + mint: NftMint, + ) -> Result<(Conditions, Nft), DriverError> + where + M: ToClvm + FromClvm + ToTreeHash + Clone, + { + let transfer_condition = mint.owner.map(|owner| { + TransferNft::new( + Some(owner.did_id), + Vec::new(), + Some(owner.inner_puzzle_hash), + ) + }); + + let conditions = Conditions::new() + .create_coin(mint.p2_puzzle_hash, 1, vec![mint.p2_puzzle_hash.into()]) + .extend(transfer_condition.clone()); + + let inner_puzzle = ctx.alloc(&clvm_quote!(conditions))?; + let p2_puzzle_hash = ctx.tree_hash(inner_puzzle).into(); + let inner_spend = Spend::new(inner_puzzle, NodePtr::NIL); + + let (mint_eve_nft, eve_nft) = self.mint_eve_nft( + ctx, + p2_puzzle_hash, + mint.metadata, + mint.metadata_updater_puzzle_hash, + mint.royalty_puzzle_hash, + mint.royalty_ten_thousandths, + )?; + + eve_nft.spend(ctx, inner_spend)?; + + let mut did_conditions = Conditions::new(); + + if let Some(transfer_condition) = transfer_condition { + did_conditions = did_conditions.assert_puzzle_announcement(did_puzzle_assertion( + eve_nft.coin.puzzle_hash, + &transfer_condition, + )); + } + + let metadata = eve_nft.info.metadata.clone(); + + let child = eve_nft.wrapped_child( + mint.p2_puzzle_hash, + mint.owner.map(|owner| owner.did_id), + metadata, + ); + + Ok((mint_eve_nft.extend(did_conditions), child)) + } +} + +#[cfg(test)] +mod tests { + use crate::{DidOwner, IntermediateLauncher, Launcher, StandardLayer}; + + use super::*; + + use chia_consensus::gen::{ + conditions::EmptyVisitor, run_block_generator::run_block_generator, + solution_generator::solution_generator, + }; + use chia_protocol::Coin; + use chia_puzzles::{nft::NftMetadata, standard::StandardArgs}; + use chia_sdk_test::{test_secret_key, Simulator}; + use chia_sdk_types::{announcement_id, TESTNET11_CONSTANTS}; + + #[test] + fn test_nft_mint_cost() -> anyhow::Result<()> { + let ctx = &mut SpendContext::new(); + + let sk = test_secret_key()?; + let pk = sk.public_key(); + let p2 = StandardLayer::new(pk); + + let puzzle_hash = StandardArgs::curry_tree_hash(pk).into(); + let coin = Coin::new(Bytes32::new([0; 32]), puzzle_hash, 1); + + let (create_did, did) = Launcher::new(coin.coin_id(), 1).create_simple_did(ctx, &p2)?; + p2.spend(ctx, coin, create_did)?; + + // We don't want to count the DID creation. + ctx.take(); + + let coin = Coin::new(Bytes32::new([1; 32]), puzzle_hash, 1); + let (mint_nft, _nft) = IntermediateLauncher::new(did.coin.coin_id(), 0, 1) + .create(ctx)? + .mint_nft( + ctx, + NftMint::new(NftMetadata::default(), puzzle_hash, 300, None), + )?; + + let _ = did.update( + ctx, + &p2, + mint_nft.create_coin_announcement(b"$".to_vec().into()), + )?; + + p2.spend( + ctx, + coin, + Conditions::new().assert_coin_announcement(announcement_id(did.coin.coin_id(), "$")), + )?; + + let coin_spends = ctx.take(); + + let generator = solution_generator( + coin_spends + .iter() + .map(|cs| (cs.coin, cs.puzzle_reveal.clone(), cs.solution.clone())), + )?; + let conds = run_block_generator::, EmptyVisitor, _>( + &mut ctx.allocator, + &generator, + [], + 11_000_000_000, + 0, + &TESTNET11_CONSTANTS, + )?; + + assert_eq!(conds.cost, 119_613_445); + + Ok(()) + } + + #[test] + fn test_bulk_mint() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + let sk = test_secret_key()?; + let pk = sk.public_key(); + let p2 = StandardLayer::new(pk); + + let puzzle_hash = StandardArgs::curry_tree_hash(pk).into(); + let coin = sim.new_coin(puzzle_hash, 3); + + let (create_did, did) = Launcher::new(coin.coin_id(), 1).create_simple_did(ctx, &p2)?; + p2.spend(ctx, coin, create_did)?; + + let mint = NftMint::new( + NftMetadata::default(), + puzzle_hash, + 300, + Some(DidOwner::from_did_info(&did.info)), + ); + + let mint_1 = IntermediateLauncher::new(did.coin.coin_id(), 0, 2) + .create(ctx)? + .mint_nft(ctx, mint.clone())? + .0; + + let mint_2 = IntermediateLauncher::new(did.coin.coin_id(), 1, 2) + .create(ctx)? + .mint_nft(ctx, mint)? + .0; + + let _ = did.update(ctx, &p2, mint_1.extend(mint_2))?; + + sim.spend_coins(ctx.take(), &[sk])?; + + Ok(()) + } + + #[test] + fn test_nonstandard_intermediate_mint() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + let sk = test_secret_key()?; + let pk = sk.public_key(); + let p2 = StandardLayer::new(pk); + + let puzzle_hash = StandardArgs::curry_tree_hash(pk).into(); + let coin = sim.new_coin(puzzle_hash, 3); + + let (create_did, did) = Launcher::new(coin.coin_id(), 1).create_simple_did(ctx, &p2)?; + p2.spend(ctx, coin, create_did)?; + + let intermediate_coin = Coin::new(did.coin.coin_id(), puzzle_hash, 0); + + let (create_launcher, launcher) = Launcher::create_early(intermediate_coin.coin_id(), 1); + + let mint = NftMint::new( + NftMetadata::default(), + puzzle_hash, + 300, + Some(DidOwner::from_did_info(&did.info)), + ); + + let (mint_nft, _nft) = launcher.mint_nft(ctx, mint)?; + + let _ = did.update(ctx, &p2, mint_nft.create_coin(puzzle_hash, 0, Vec::new()))?; + p2.spend(ctx, intermediate_coin, create_launcher)?; + + sim.spend_coins(ctx.take(), &[sk])?; + + Ok(()) + } + + #[test] + fn test_nonstandard_intermediate_mint_recreated_did() -> anyhow::Result<()> { + let mut sim = Simulator::new(); + let ctx = &mut SpendContext::new(); + + let sk = test_secret_key()?; + let pk = sk.public_key(); + let p2 = StandardLayer::new(pk); + + let puzzle_hash = StandardArgs::curry_tree_hash(pk).into(); + let coin = sim.new_coin(puzzle_hash, 3); + + let (create_did, did) = Launcher::new(coin.coin_id(), 1).create_simple_did(ctx, &p2)?; + p2.spend(ctx, coin, create_did)?; + + let intermediate_coin = Coin::new(did.coin.coin_id(), puzzle_hash, 0); + + let (create_launcher, launcher) = Launcher::create_early(intermediate_coin.coin_id(), 1); + p2.spend(ctx, intermediate_coin, create_launcher)?; + + let mint = NftMint::new( + NftMetadata::default(), + puzzle_hash, + 300, + Some(DidOwner::from_did_info(&did.info)), + ); + + let (mint_nft, _nft_info) = launcher.mint_nft(ctx, mint)?; + + let did = did.update( + ctx, + &p2, + Conditions::new().create_coin(puzzle_hash, 0, Vec::new()), + )?; + + let _ = did.update(ctx, &p2, mint_nft)?; + + sim.spend_coins(ctx.take(), &[sk])?; + + Ok(()) + } +} diff --git a/crates/chia-sdk-driver/src/primitives/nft/nft_mint.rs b/crates/chia-sdk-driver/src/primitives/nft/nft_mint.rs new file mode 100644 index 00000000..1360c45c --- /dev/null +++ b/crates/chia-sdk-driver/src/primitives/nft/nft_mint.rs @@ -0,0 +1,48 @@ +use chia_protocol::Bytes32; +use chia_puzzles::nft::NFT_METADATA_UPDATER_PUZZLE_HASH; + +use super::DidOwner; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NftMint { + pub metadata: M, + pub metadata_updater_puzzle_hash: Bytes32, + pub royalty_puzzle_hash: Bytes32, + pub royalty_ten_thousandths: u16, + pub p2_puzzle_hash: Bytes32, + pub owner: Option, +} + +impl NftMint { + pub fn new( + metadata: M, + p2_puzzle_hash: Bytes32, + royalty_ten_thousandths: u16, + owner: Option, + ) -> Self { + Self { + metadata, + metadata_updater_puzzle_hash: NFT_METADATA_UPDATER_PUZZLE_HASH.into(), + royalty_puzzle_hash: p2_puzzle_hash, + royalty_ten_thousandths, + p2_puzzle_hash, + owner, + } + } + + #[must_use] + pub fn with_royalty_puzzle_hash(self, royalty_puzzle_hash: Bytes32) -> Self { + Self { + royalty_puzzle_hash, + ..self + } + } + + #[must_use] + pub fn with_custom_metadata_updater(self, metadata_updater_puzzle_hash: Bytes32) -> Self { + Self { + metadata_updater_puzzle_hash, + ..self + } + } +} diff --git a/crates/chia-sdk-driver/src/puzzle.rs b/crates/chia-sdk-driver/src/puzzle.rs new file mode 100644 index 00000000..22e7c203 --- /dev/null +++ b/crates/chia-sdk-driver/src/puzzle.rs @@ -0,0 +1,137 @@ +use clvm_traits::FromClvm; +use clvm_utils::{tree_hash, CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::{Allocator, NodePtr}; + +use crate::{DriverError, Layer, SpendContext}; + +#[derive(Debug, Clone, Copy)] +pub enum Puzzle { + Curried(CurriedPuzzle), + Raw(RawPuzzle), +} + +impl Puzzle { + pub fn parse(allocator: &Allocator, puzzle: NodePtr) -> Self { + CurriedPuzzle::parse(allocator, puzzle).map_or_else( + || { + Self::Raw(RawPuzzle { + puzzle_hash: tree_hash(allocator, puzzle), + ptr: puzzle, + }) + }, + Self::Curried, + ) + } + + pub fn curried_puzzle_hash(&self) -> TreeHash { + match self { + Self::Curried(curried) => curried.curried_puzzle_hash, + Self::Raw(raw) => raw.puzzle_hash, + } + } + + pub fn mod_hash(&self) -> TreeHash { + match self { + Self::Curried(curried) => curried.mod_hash, + Self::Raw(raw) => raw.puzzle_hash, + } + } + + pub fn ptr(&self) -> NodePtr { + match self { + Self::Curried(curried) => curried.curried_ptr, + Self::Raw(raw) => raw.ptr, + } + } + + pub fn is_curried(&self) -> bool { + matches!(self, Self::Curried(_)) + } + + pub fn is_raw(&self) -> bool { + matches!(self, Self::Raw(_)) + } + + pub fn as_curried(&self) -> Option { + match self { + Self::Curried(curried) => Some(*curried), + Self::Raw(_raw) => None, + } + } + + pub fn as_raw(&self) -> Option { + match self { + Self::Curried(_curried) => None, + Self::Raw(raw) => Some(*raw), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct CurriedPuzzle { + pub curried_puzzle_hash: TreeHash, + pub curried_ptr: NodePtr, + pub mod_hash: TreeHash, + pub args: NodePtr, +} + +impl CurriedPuzzle { + pub fn parse(allocator: &Allocator, puzzle: NodePtr) -> Option { + let curried = CurriedProgram::from_clvm(allocator, puzzle).ok()?; + let mod_hash = tree_hash(allocator, curried.program); + let curried_puzzle_hash = CurriedProgram { + program: mod_hash, + args: tree_hash(allocator, curried.args), + } + .tree_hash(); + + Some(Self { + curried_puzzle_hash, + curried_ptr: puzzle, + mod_hash, + args: curried.args, + }) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct RawPuzzle { + pub puzzle_hash: TreeHash, + pub ptr: NodePtr, +} + +impl ToTreeHash for Puzzle { + fn tree_hash(&self) -> TreeHash { + self.curried_puzzle_hash() + } +} + +impl Layer for Puzzle { + type Solution = NodePtr; + + fn parse_puzzle(_allocator: &Allocator, puzzle: Puzzle) -> Result, DriverError> + where + Self: Sized, + { + Ok(Some(puzzle)) + } + + fn parse_solution( + _allocator: &Allocator, + solution: NodePtr, + ) -> Result { + Ok(solution) + } + + fn construct_puzzle(&self, _ctx: &mut SpendContext) -> Result { + Ok(self.ptr()) + } + + fn construct_solution( + &self, + _ctx: &mut SpendContext, + solution: Self::Solution, + ) -> Result { + Ok(solution) + } +} diff --git a/crates/chia-sdk-driver/src/spend.rs b/crates/chia-sdk-driver/src/spend.rs new file mode 100644 index 00000000..720bfc06 --- /dev/null +++ b/crates/chia-sdk-driver/src/spend.rs @@ -0,0 +1,14 @@ +use clvmr::NodePtr; + +#[derive(Debug, Clone, Copy)] +#[must_use] +pub struct Spend { + pub puzzle: NodePtr, + pub solution: NodePtr, +} + +impl Spend { + pub fn new(puzzle: NodePtr, solution: NodePtr) -> Self { + Self { puzzle, solution } + } +} diff --git a/crates/chia-sdk-driver/src/spend_context.rs b/crates/chia-sdk-driver/src/spend_context.rs new file mode 100644 index 00000000..b0be1d14 --- /dev/null +++ b/crates/chia-sdk-driver/src/spend_context.rs @@ -0,0 +1,255 @@ +use std::collections::HashMap; + +use chia_protocol::{Coin, CoinSpend, Program}; +use chia_puzzles::{ + cat::{ + CAT_PUZZLE, CAT_PUZZLE_HASH, EVERYTHING_WITH_SIGNATURE_TAIL_PUZZLE, + EVERYTHING_WITH_SIGNATURE_TAIL_PUZZLE_HASH, GENESIS_BY_COIN_ID_TAIL_PUZZLE, + GENESIS_BY_COIN_ID_TAIL_PUZZLE_HASH, + }, + did::{DID_INNER_PUZZLE, DID_INNER_PUZZLE_HASH}, + nft::{ + NFT_INTERMEDIATE_LAUNCHER_PUZZLE, NFT_INTERMEDIATE_LAUNCHER_PUZZLE_HASH, + NFT_METADATA_UPDATER_PUZZLE, NFT_METADATA_UPDATER_PUZZLE_HASH, NFT_OWNERSHIP_LAYER_PUZZLE, + NFT_OWNERSHIP_LAYER_PUZZLE_HASH, NFT_ROYALTY_TRANSFER_PUZZLE, + NFT_ROYALTY_TRANSFER_PUZZLE_HASH, NFT_STATE_LAYER_PUZZLE, NFT_STATE_LAYER_PUZZLE_HASH, + }, + offer::{SETTLEMENT_PAYMENTS_PUZZLE, SETTLEMENT_PAYMENTS_PUZZLE_HASH}, + singleton::{ + SINGLETON_LAUNCHER_PUZZLE, SINGLETON_LAUNCHER_PUZZLE_HASH, SINGLETON_TOP_LAYER_PUZZLE, + SINGLETON_TOP_LAYER_PUZZLE_HASH, + }, + standard::{STANDARD_PUZZLE, STANDARD_PUZZLE_HASH}, +}; +use chia_sdk_types::run_puzzle; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{tree_hash, TreeHash}; +use clvmr::{serde::node_from_bytes, Allocator, NodePtr}; + +use crate::{ + DriverError, Spend, P2_DELEGATED_CONDITIONS_PUZZLE, P2_DELEGATED_CONDITIONS_PUZZLE_HASH, + P2_DELEGATED_SINGLETON_PUZZLE, P2_DELEGATED_SINGLETON_PUZZLE_HASH, P2_ONE_OF_MANY_PUZZLE, + P2_ONE_OF_MANY_PUZZLE_HASH, P2_SINGLETON_PUZZLE, P2_SINGLETON_PUZZLE_HASH, +}; + +/// A wrapper around [`Allocator`] that caches puzzles and keeps track of a list of [`CoinSpend`]. +/// It's used to construct spend bundles in an easy and efficient way. +#[derive(Debug, Default)] +pub struct SpendContext { + pub allocator: Allocator, + puzzles: HashMap, + coin_spends: Vec, +} + +impl SpendContext { + pub fn new() -> Self { + Self::default() + } + + pub fn iter(&self) -> impl Iterator { + self.coin_spends.iter() + } + + /// Remove all of the [`CoinSpend`] that have been collected so far. + pub fn take(&mut self) -> Vec { + std::mem::take(&mut self.coin_spends) + } + + /// Adds a [`CoinSpend`] to the collection. + pub fn insert(&mut self, coin_spend: CoinSpend) { + self.coin_spends.push(coin_spend); + } + + /// Serializes a [`Spend`] and adds it to the list of [`CoinSpend`]. + pub fn spend(&mut self, coin: Coin, spend: Spend) -> Result<(), DriverError> { + let puzzle_reveal = self.serialize(&spend.puzzle)?; + let solution = self.serialize(&spend.solution)?; + self.insert(CoinSpend::new(coin, puzzle_reveal, solution)); + Ok(()) + } + + /// Allocate a new node and return its pointer. + pub fn alloc(&mut self, value: &T) -> Result + where + T: ToClvm, + { + Ok(value.to_clvm(&mut self.allocator)?) + } + + /// Extract a value from a node pointer. + pub fn extract(&self, ptr: NodePtr) -> Result + where + T: FromClvm, + { + Ok(T::from_clvm(&self.allocator, ptr)?) + } + + /// Compute the tree hash of a node pointer. + pub fn tree_hash(&self, ptr: NodePtr) -> TreeHash { + tree_hash(&self.allocator, ptr) + } + + /// Run a puzzle with a solution and return the result. + pub fn run(&mut self, puzzle: NodePtr, solution: NodePtr) -> Result { + Ok(run_puzzle(&mut self.allocator, puzzle, solution)?) + } + + /// Serialize a value and return a `Program`. + pub fn serialize(&mut self, value: &T) -> Result + where + T: ToClvm, + { + let ptr = value.to_clvm(&mut self.allocator)?; + Ok(Program::from_clvm(&self.allocator, ptr)?) + } + + /// Allocate the standard puzzle and return its pointer. + pub fn standard_puzzle(&mut self) -> Result { + self.puzzle(STANDARD_PUZZLE_HASH, &STANDARD_PUZZLE) + } + + /// Allocate the CAT puzzle and return its pointer. + pub fn cat_puzzle(&mut self) -> Result { + self.puzzle(CAT_PUZZLE_HASH, &CAT_PUZZLE) + } + + /// Allocate the DID inner puzzle and return its pointer. + pub fn did_inner_puzzle(&mut self) -> Result { + self.puzzle(DID_INNER_PUZZLE_HASH, &DID_INNER_PUZZLE) + } + + /// Allocate the NFT intermediate launcher puzzle and return its pointer. + pub fn nft_intermediate_launcher(&mut self) -> Result { + self.puzzle( + NFT_INTERMEDIATE_LAUNCHER_PUZZLE_HASH, + &NFT_INTERMEDIATE_LAUNCHER_PUZZLE, + ) + } + + /// Allocate the NFT royalty transfer puzzle and return its pointer. + pub fn nft_royalty_transfer(&mut self) -> Result { + self.puzzle( + NFT_ROYALTY_TRANSFER_PUZZLE_HASH, + &NFT_ROYALTY_TRANSFER_PUZZLE, + ) + } + + /// Allocate the NFT metadata updater puzzle and return its pointer. + pub fn nft_metadata_updater(&mut self) -> Result { + self.puzzle( + NFT_METADATA_UPDATER_PUZZLE_HASH, + &NFT_METADATA_UPDATER_PUZZLE, + ) + } + + /// Allocate the NFT ownership layer puzzle and return its pointer. + pub fn nft_ownership_layer(&mut self) -> Result { + self.puzzle(NFT_OWNERSHIP_LAYER_PUZZLE_HASH, &NFT_OWNERSHIP_LAYER_PUZZLE) + } + + /// Allocate the NFT state layer puzzle and return its pointer. + pub fn nft_state_layer(&mut self) -> Result { + self.puzzle(NFT_STATE_LAYER_PUZZLE_HASH, &NFT_STATE_LAYER_PUZZLE) + } + + /// Allocate the singleton top layer puzzle and return its pointer. + pub fn singleton_top_layer(&mut self) -> Result { + self.puzzle(SINGLETON_TOP_LAYER_PUZZLE_HASH, &SINGLETON_TOP_LAYER_PUZZLE) + } + + /// Allocate the singleton launcher puzzle and return its pointer. + pub fn singleton_launcher(&mut self) -> Result { + self.puzzle(SINGLETON_LAUNCHER_PUZZLE_HASH, &SINGLETON_LAUNCHER_PUZZLE) + } + + /// Allocate the multi-issuance TAIL puzzle and return its pointer. + pub fn everything_with_signature_tail_puzzle(&mut self) -> Result { + self.puzzle( + EVERYTHING_WITH_SIGNATURE_TAIL_PUZZLE_HASH, + &EVERYTHING_WITH_SIGNATURE_TAIL_PUZZLE, + ) + } + + /// Allocate the single-issuance TAIL puzzle and return its pointer. + pub fn genesis_by_coin_id_tail_puzzle(&mut self) -> Result { + self.puzzle( + GENESIS_BY_COIN_ID_TAIL_PUZZLE_HASH, + &GENESIS_BY_COIN_ID_TAIL_PUZZLE, + ) + } + + /// Allocate the settlement payments puzzle and return its pointer. + pub fn settlement_payments_puzzle(&mut self) -> Result { + self.puzzle(SETTLEMENT_PAYMENTS_PUZZLE_HASH, &SETTLEMENT_PAYMENTS_PUZZLE) + } + + /// Allocate the p2 delegated conditions puzzle and return its pointer. + pub fn p2_delegated_conditions_puzzle(&mut self) -> Result { + self.puzzle( + P2_DELEGATED_CONDITIONS_PUZZLE_HASH, + &P2_DELEGATED_CONDITIONS_PUZZLE, + ) + } + + /// Allocate the p2 one of many puzzle and return its pointer. + pub fn p2_one_of_many_puzzle(&mut self) -> Result { + self.puzzle(P2_ONE_OF_MANY_PUZZLE_HASH, &P2_ONE_OF_MANY_PUZZLE) + } + + /// Allocate the p2 singleton puzzle and return its pointer. + pub fn p2_singleton_puzzle(&mut self) -> Result { + self.puzzle(P2_SINGLETON_PUZZLE_HASH, &P2_SINGLETON_PUZZLE) + } + + /// Allocate the p2 delegated singleton puzzle and return its pointer. + pub fn p2_delegated_singleton_puzzle(&mut self) -> Result { + self.puzzle( + P2_DELEGATED_SINGLETON_PUZZLE_HASH, + &P2_DELEGATED_SINGLETON_PUZZLE, + ) + } + + /// Preload a puzzle into the cache. + pub fn preload(&mut self, puzzle_hash: TreeHash, ptr: NodePtr) { + self.puzzles.insert(puzzle_hash, ptr); + } + + /// Checks whether a puzzle is in the cache. + pub fn get_puzzle(&self, puzzle_hash: &TreeHash) -> Option { + self.puzzles.get(puzzle_hash).copied() + } + + /// Get a puzzle from the cache or allocate a new one. + pub fn puzzle( + &mut self, + puzzle_hash: TreeHash, + puzzle_bytes: &[u8], + ) -> Result { + if let Some(puzzle) = self.puzzles.get(&puzzle_hash) { + Ok(*puzzle) + } else { + let puzzle = node_from_bytes(&mut self.allocator, puzzle_bytes)?; + self.puzzles.insert(puzzle_hash, puzzle); + Ok(puzzle) + } + } +} + +impl IntoIterator for SpendContext { + type Item = CoinSpend; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.coin_spends.into_iter() + } +} + +impl From for SpendContext { + fn from(allocator: Allocator) -> Self { + Self { + allocator, + puzzles: HashMap::new(), + coin_spends: Vec::new(), + } + } +} diff --git a/crates/chia-sdk-driver/src/spend_with_conditions.rs b/crates/chia-sdk-driver/src/spend_with_conditions.rs new file mode 100644 index 00000000..8d7e2dd0 --- /dev/null +++ b/crates/chia-sdk-driver/src/spend_with_conditions.rs @@ -0,0 +1,11 @@ +use chia_sdk_types::Conditions; + +use crate::{DriverError, Spend, SpendContext}; + +pub trait SpendWithConditions { + fn spend_with_conditions( + &self, + ctx: &mut SpendContext, + conditions: Conditions, + ) -> Result; +} diff --git a/crates/chia-sdk-offers/Cargo.toml b/crates/chia-sdk-offers/Cargo.toml new file mode 100644 index 00000000..2fe2b35a --- /dev/null +++ b/crates/chia-sdk-offers/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "chia-sdk-offers" +version = "0.16.0" +edition = "2021" +license = "Apache-2.0" +description = "Implements Chia offer file creation and acceptance." +authors = ["Brandon Haggstrom "] +homepage = "https://github.com/Rigidity/chia-wallet-sdk" +repository = "https://github.com/Rigidity/chia-wallet-sdk" +readme = { workspace = true } +keywords = { workspace = true } +categories = { workspace = true } + +[lints] +workspace = true + +[dependencies] +bech32 = { workspace = true } +thiserror = { workspace = true } +chia-protocol = { workspace = true } +chia-puzzles = { workspace = true } +chia-traits = { workspace = true } +chia-bls = { workspace = true } +clvm-traits = { workspace = true } +clvm-utils = { workspace = true } +clvmr = { workspace = true } +flate2 = { workspace = true, features = ["zlib-ng-compat"] } +indexmap = { workspace = true } +chia-sdk-driver = { workspace = true } +chia-sdk-types = { workspace = true } +once_cell = { workspace = true } + +[dev-dependencies] +hex-literal = { workspace = true } +hex = { workspace = true } +anyhow = { workspace = true } +chia-sdk-test = { path = "../chia-sdk-test" } diff --git a/crates/chia-sdk-offers/src/compress.rs b/crates/chia-sdk-offers/src/compress.rs new file mode 100644 index 00000000..14d0c17d --- /dev/null +++ b/crates/chia-sdk-offers/src/compress.rs @@ -0,0 +1,113 @@ +use std::io::Read; + +use chia_puzzles::{ + cat::{CAT_PUZZLE, CAT_PUZZLE_V1}, + nft::{ + NFT_METADATA_UPDATER_PUZZLE, NFT_OWNERSHIP_LAYER_PUZZLE, NFT_ROYALTY_TRANSFER_PUZZLE, + NFT_STATE_LAYER_PUZZLE, + }, + offer::{SETTLEMENT_PAYMENTS_PUZZLE, SETTLEMENT_PAYMENTS_PUZZLE_V1}, + singleton::SINGLETON_TOP_LAYER_PUZZLE, + standard::STANDARD_PUZZLE, +}; +use flate2::{ + read::{ZlibDecoder, ZlibEncoder}, + Compress, Compression, Decompress, FlushDecompress, +}; +use once_cell::sync::Lazy; + +use crate::OfferError; + +static COMPRESSION_ZDICT: Lazy> = Lazy::new(|| { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&STANDARD_PUZZLE); + bytes.extend_from_slice(&CAT_PUZZLE_V1); + bytes.extend_from_slice(&SETTLEMENT_PAYMENTS_PUZZLE_V1); + bytes.extend_from_slice(&SINGLETON_TOP_LAYER_PUZZLE); + bytes.extend_from_slice(&NFT_STATE_LAYER_PUZZLE); + bytes.extend_from_slice(&NFT_OWNERSHIP_LAYER_PUZZLE); + bytes.extend_from_slice(&NFT_METADATA_UPDATER_PUZZLE); + bytes.extend_from_slice(&NFT_ROYALTY_TRANSFER_PUZZLE); + bytes.extend_from_slice(&CAT_PUZZLE); + bytes.extend_from_slice(&SETTLEMENT_PAYMENTS_PUZZLE); + bytes +}); + +pub fn compress_offer_bytes(bytes: &[u8]) -> Result, OfferError> { + let mut output = 6u16.to_be_bytes().to_vec(); + output.extend(zlib_compress(bytes, &COMPRESSION_ZDICT)?); + Ok(output) +} + +pub fn decompress_offer_bytes(bytes: &[u8]) -> Result, OfferError> { + let version_bytes: [u8; 2] = bytes + .get(0..2) + .ok_or(OfferError::MissingVersionPrefix)? + .try_into()?; + + let version = u16::from_be_bytes(version_bytes); + + if version > 6 { + return Err(OfferError::UnsupportedVersion); + } + + zlib_decompress(&bytes[2..], &COMPRESSION_ZDICT) +} + +fn zlib_compress(input: &[u8], zdict: &[u8]) -> std::io::Result> { + let mut compress = Compress::new(Compression::new(6), true); + compress.set_dictionary(zdict)?; + let mut encoder = ZlibEncoder::new_with_compress(input, compress); + let mut output = Vec::new(); + encoder.read_to_end(&mut output)?; + Ok(output) +} + +fn zlib_decompress(input: &[u8], zdict: &[u8]) -> Result, OfferError> { + let mut decompress = Decompress::new(true); + + if decompress + .decompress(input, &mut [], FlushDecompress::Finish) + .is_ok() + { + return Err(OfferError::NotCompressed); + } + + decompress.set_dictionary(zdict)?; + let i = decompress.total_in(); + let mut decoder = ZlibDecoder::new_with_decompress(&input[usize::try_from(i)?..], decompress); + let mut output = Vec::new(); + decoder.read_to_end(&mut output)?; + Ok(output) +} + +#[cfg(test)] +mod tests { + use chia_protocol::SpendBundle; + use chia_traits::Streamable; + + use super::*; + + #[test] + fn test_compression() { + let decompressed_offer = hex::decode(DECOMPRESSED_OFFER.trim()).unwrap(); + let output = compress_offer_bytes(&decompressed_offer).unwrap(); + assert_eq!(hex::encode(output), COMPRESSED_OFFER.trim()); + } + + #[test] + fn test_decompression() { + let compressed_offer = hex::decode(COMPRESSED_OFFER.trim()).unwrap(); + let output = decompress_offer_bytes(&compressed_offer).unwrap(); + assert_eq!(hex::encode(output), DECOMPRESSED_OFFER.trim()); + } + + #[test] + fn parse_spend_bundle() { + let decompressed_offer = hex::decode(DECOMPRESSED_OFFER.trim()).unwrap(); + SpendBundle::from_bytes(&decompressed_offer).unwrap(); + } + + const COMPRESSED_OFFER: &str = include_str!("../test_data/compressed.offer"); + const DECOMPRESSED_OFFER: &str = include_str!("../test_data/decompressed.offer"); +} diff --git a/crates/chia-sdk-offers/src/encode.rs b/crates/chia-sdk-offers/src/encode.rs new file mode 100644 index 00000000..4fef717f --- /dev/null +++ b/crates/chia-sdk-offers/src/encode.rs @@ -0,0 +1,25 @@ +use bech32::{u5, Variant}; + +use crate::OfferError; + +pub fn encode_offer_data(offer: &[u8]) -> Result { + let data = bech32::convert_bits(offer, 8, 5, true)? + .into_iter() + .map(u5::try_from_u8) + .collect::, bech32::Error>>()?; + Ok(bech32::encode("offer1", data, Variant::Bech32m)?) +} + +pub fn decode_offer_data(offer: &str) -> Result, OfferError> { + let (hrp, data, variant) = bech32::decode(offer)?; + + if variant != Variant::Bech32m { + return Err(OfferError::InvalidFormat); + } + + if hrp.as_str() != "offer" { + return Err(OfferError::InvalidPrefix(hrp)); + } + + Ok(bech32::convert_bits(&data, 5, 8, false)?) +} diff --git a/crates/chia-sdk-offers/src/error.rs b/crates/chia-sdk-offers/src/error.rs new file mode 100644 index 00000000..361089ae --- /dev/null +++ b/crates/chia-sdk-offers/src/error.rs @@ -0,0 +1,49 @@ +use std::{array::TryFromSliceError, io, num::TryFromIntError}; + +use clvm_traits::{FromClvmError, ToClvmError}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum OfferError { + #[error("IO error: {0}")] + Io(#[from] io::Error), + + #[error("Try from slice error: {0}")] + TryFromSlice(#[from] TryFromSliceError), + + #[error("Try from int error: {0}")] + TryFromInt(#[from] TryFromIntError), + + #[error("Missing compression version prefix")] + MissingVersionPrefix, + + #[error("Unsupported compression version")] + UnsupportedVersion, + + #[error("Streamable error: {0}")] + Streamable(#[from] chia_traits::Error), + + #[error("Cannot decompress uncompressed input")] + NotCompressed, + + #[error("Flate2 error: {0}")] + Flate2(#[from] flate2::DecompressError), + + #[error("Invalid prefix: {0}")] + InvalidPrefix(String), + + #[error("Encoding is not bech32m")] + InvalidFormat, + + #[error("Error when decoding address: {0}")] + Decode(#[from] bech32::Error), + + #[error("To CLVM error: {0}")] + ToClvm(#[from] ToClvmError), + + #[error("From CLVM error: {0}")] + FromClvm(#[from] FromClvmError), + + #[error("Requested payment puzzle mismatch")] + PuzzleMismatch, +} diff --git a/crates/chia-sdk-offers/src/lib.rs b/crates/chia-sdk-offers/src/lib.rs new file mode 100644 index 00000000..52a352c1 --- /dev/null +++ b/crates/chia-sdk-offers/src/lib.rs @@ -0,0 +1,13 @@ +mod compress; +mod encode; +mod error; +mod offer; +mod offer_builder; +mod parsed_offer; + +pub use compress::*; +pub use encode::*; +pub use error::*; +pub use offer::*; +pub use offer_builder::*; +pub use parsed_offer::*; diff --git a/crates/chia-sdk-offers/src/offer.rs b/crates/chia-sdk-offers/src/offer.rs new file mode 100644 index 00000000..bfe9a0db --- /dev/null +++ b/crates/chia-sdk-offers/src/offer.rs @@ -0,0 +1,118 @@ +use chia_protocol::{Bytes32, SpendBundle}; +use chia_puzzles::offer::SettlementPaymentsSolution; +use chia_sdk_driver::Puzzle; +use chia_traits::Streamable; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{tree_hash, ToTreeHash}; +use clvmr::Allocator; +use indexmap::IndexMap; + +use crate::{ + compress_offer_bytes, decode_offer_data, decompress_offer_bytes, encode_offer_data, Make, + OfferBuilder, OfferError, ParsedOffer, Take, +}; + +#[derive(Debug, Clone)] +pub struct Offer { + spend_bundle: SpendBundle, +} + +impl Offer { + pub fn new(spend_bundle: SpendBundle) -> Self { + Self { spend_bundle } + } + + pub fn build(coin_ids: Vec) -> OfferBuilder { + Self::build_with_nonce(Self::nonce(coin_ids)) + } + + pub fn build_with_nonce(nonce: Bytes32) -> OfferBuilder { + OfferBuilder::new(nonce) + } + + pub fn nonce(mut coin_ids: Vec) -> Bytes32 { + coin_ids.sort(); + coin_ids.tree_hash().into() + } + + pub fn to_bytes(&self) -> Result, OfferError> { + Ok(self.spend_bundle.to_bytes()?) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(SpendBundle::from_bytes(bytes)?.into()) + } + + pub fn compress(&self) -> Result, OfferError> { + compress_offer_bytes(&self.to_bytes()?) + } + + pub fn decompress(bytes: &[u8]) -> Result { + Self::from_bytes(&decompress_offer_bytes(bytes)?) + } + + pub fn encode(&self) -> Result { + encode_offer_data(&self.compress()?) + } + + pub fn decode(text: &str) -> Result { + Self::decompress(&decode_offer_data(text)?) + } + + pub fn take(self, allocator: &mut Allocator) -> Result, OfferError> { + Ok(self.parse(allocator)?.take()) + } + + pub fn parse(self, allocator: &mut Allocator) -> Result { + let mut parsed = ParsedOffer { + aggregated_signature: self.spend_bundle.aggregated_signature, + coin_spends: Vec::new(), + requested_payments: IndexMap::new(), + }; + + for coin_spend in self.spend_bundle.coin_spends { + if coin_spend.coin.parent_coin_info != Bytes32::default() { + parsed.coin_spends.push(coin_spend); + continue; + } + + if coin_spend.coin.amount != 0 { + parsed.coin_spends.push(coin_spend); + continue; + } + + let puzzle = coin_spend.puzzle_reveal.to_clvm(allocator)?; + let puzzle_hash = tree_hash(allocator, puzzle).into(); + + if puzzle_hash != coin_spend.coin.puzzle_hash { + return Err(OfferError::PuzzleMismatch); + } + + let solution = coin_spend.solution.to_clvm(allocator)?; + let settlement_solution = SettlementPaymentsSolution::from_clvm(allocator, solution)?; + + let puzzle = Puzzle::parse(allocator, puzzle); + + parsed + .requested_payments + .entry(puzzle_hash) + .or_insert_with(|| (puzzle, Vec::new())) + .1 + .extend(settlement_solution.notarized_payments); + } + + Ok(parsed) + } +} + +impl From for Offer { + fn from(spend_bundle: SpendBundle) -> Self { + Self::new(spend_bundle) + } +} + +impl From for SpendBundle { + fn from(offer: Offer) -> Self { + offer.spend_bundle + } +} diff --git a/crates/chia-sdk-offers/src/offer_builder.rs b/crates/chia-sdk-offers/src/offer_builder.rs new file mode 100644 index 00000000..460aa8c2 --- /dev/null +++ b/crates/chia-sdk-offers/src/offer_builder.rs @@ -0,0 +1,157 @@ +use chia_protocol::{Bytes32, Coin, CoinSpend, SpendBundle}; +use chia_puzzles::offer::{NotarizedPayment, Payment, SettlementPaymentsSolution}; +use chia_sdk_driver::{DriverError, Puzzle, SpendContext}; +use chia_sdk_types::{announcement_id, AssertPuzzleAnnouncement}; +use clvm_traits::ToClvm; +use clvmr::Allocator; +use indexmap::IndexMap; + +use crate::{Offer, ParsedOffer}; + +#[derive(Debug, Clone)] +pub struct OfferBuilder { + data: T, +} + +#[derive(Debug, Clone)] +pub struct Make { + nonce: Bytes32, + requested_payments: IndexMap)>, + announcements: Vec, +} + +#[derive(Debug, Clone)] +pub struct Partial { + requested_payments: IndexMap)>, +} + +#[derive(Debug, Clone)] +pub struct Take { + parsed_offer: ParsedOffer, +} + +impl OfferBuilder { + pub fn new(nonce: Bytes32) -> Self { + Self { + data: Make { + nonce, + requested_payments: IndexMap::new(), + announcements: Vec::new(), + }, + } + } + + /// Adds a list of requested payments for a given puzzle. + /// It will use the nonce to create a new [`NotarizedPayment`] and add it to the requested payments. + pub fn request

( + mut self, + ctx: &mut SpendContext, + puzzle: &P, + payments: Vec, + ) -> Result + where + P: ToClvm, + { + let puzzle_ptr = ctx.alloc(puzzle)?; + let puzzle_hash = ctx.tree_hash(puzzle_ptr).into(); + let puzzle = Puzzle::parse(&ctx.allocator, puzzle_ptr); + + let notarized_payment = NotarizedPayment { + nonce: self.data.nonce, + payments, + }; + let notarized_payment_ptr = ctx.alloc(¬arized_payment)?; + let notarized_payment_hash = ctx.tree_hash(notarized_payment_ptr); + + self.data + .requested_payments + .entry(puzzle_hash) + .or_insert_with(|| (puzzle, Vec::new())) + .1 + .push(notarized_payment); + + self.data + .announcements + .push(AssertPuzzleAnnouncement::new(announcement_id( + puzzle_hash, + notarized_payment_hash, + ))); + + Ok(self) + } + + /// This will create a new [`OfferBuilder`] with the requested payments frozen. + /// It returns a list of announcements that can be asserted by the maker side. + pub fn finish(self) -> (Vec, OfferBuilder) { + let partial = OfferBuilder { + data: Partial { + requested_payments: self.data.requested_payments, + }, + }; + (self.data.announcements, partial) + } +} + +impl OfferBuilder { + pub fn bundle( + self, + ctx: &mut SpendContext, + partial_spend_bundle: SpendBundle, + ) -> Result { + let mut spend_bundle = partial_spend_bundle; + + for (puzzle_hash, (puzzle, notarized_payments)) in self.data.requested_payments { + let puzzle_reveal = ctx.serialize(&puzzle.ptr())?; + let solution = ctx.serialize(&SettlementPaymentsSolution { notarized_payments })?; + + spend_bundle.coin_spends.push(CoinSpend { + coin: Coin::new(Bytes32::default(), puzzle_hash, 0), + puzzle_reveal, + solution, + }); + } + + Ok(spend_bundle.into()) + } + + /// This will use the partial spend bundle to create a new [`OfferBuilder`] for taking. + pub fn take(self, partial_spend_bundle: SpendBundle) -> OfferBuilder { + OfferBuilder { + data: Take { + parsed_offer: ParsedOffer { + coin_spends: partial_spend_bundle.coin_spends, + aggregated_signature: partial_spend_bundle.aggregated_signature, + requested_payments: self.data.requested_payments, + }, + }, + } + } +} + +impl OfferBuilder { + pub fn from_parsed_offer(parsed_offer: ParsedOffer) -> Self { + Self { + data: Take { parsed_offer }, + } + } + + pub fn fulfill(&mut self) -> Option<(Puzzle, Vec)> { + Some( + self.data + .parsed_offer + .requested_payments + .shift_remove_index(0)? + .1, + ) + } + + pub fn bundle(self, other_spend_bundle: SpendBundle) -> SpendBundle { + SpendBundle::aggregate(&[ + SpendBundle::new( + self.data.parsed_offer.coin_spends, + self.data.parsed_offer.aggregated_signature, + ), + other_spend_bundle, + ]) + } +} diff --git a/crates/chia-sdk-offers/src/parsed_offer.rs b/crates/chia-sdk-offers/src/parsed_offer.rs new file mode 100644 index 00000000..ace33dc9 --- /dev/null +++ b/crates/chia-sdk-offers/src/parsed_offer.rs @@ -0,0 +1,20 @@ +use chia_bls::Signature; +use chia_protocol::{Bytes32, CoinSpend}; +use chia_puzzles::offer::NotarizedPayment; +use chia_sdk_driver::Puzzle; +use indexmap::IndexMap; + +use crate::{OfferBuilder, Take}; + +#[derive(Debug, Default, Clone)] +pub struct ParsedOffer { + pub coin_spends: Vec, + pub aggregated_signature: Signature, + pub requested_payments: IndexMap)>, +} + +impl ParsedOffer { + pub fn take(self) -> OfferBuilder { + OfferBuilder::from_parsed_offer(self) + } +} diff --git a/crates/chia-sdk-offers/test_data/compressed.offer b/crates/chia-sdk-offers/test_data/compressed.offer new file mode 100644 index 00000000..43e36b45 --- /dev/null +++ b/crates/chia-sdk-offers/test_data/compressed.offer @@ -0,0 +1 @@ +000678bb1ce2864b63606060622000f234ef6ae4725635979d975e6263fb68c9b687879b720f54fbcf0ace3de319c33d09a60ede4e1dadfd476bffd1da7fb4f61fadfd476bff01abfdc1b5fc02f37d9f13deb5361a6f9b1b193521f7482ce3fa00f3b96b03e3760676edae91d502f706fe332e60faa2d1fcdbd8c29043f564e0fb332b04de6c132fbaf9f45696ae2f57eac41d9de0c622a26e1b6d51e0ebfe334264165c5fbc39fc9dddc6cbad357a62deef969eeef978e132fbe11d27dc6c5b16c63e7afceaffff05536aa21f0669fd0f37dbabbd5388e5cfe2d6343efba5f3acb72f7e57303fa8617ee1ff56718fb2170c44a804d939ffdfdecce0963ef306aeab9a46c78edde3ca545853ea74f548877ce883d206f7ec92b01f5a0a75aed10797d77c50fae77cb263d3c9b3b6976e3a24dd0a8adb78a82818da8c8147f3e88a01020320a32b0646570c0cf6150390827b41fd2aa3e0fd772f3eb8779a936dd39d24f7dd47ce27f44798c48a5bdd4e7eb1a566feff052fa7d8ac736a767d9ccfe7ab6b77f67cb1dfb363ad7d6631f56fce68cd7be9bbdf7ac1fb0fa54a9e010907d2bee51bad39b4bccecaaeb8ea54f7ba251797f92d397bd076256a1d31ba4e61749dc2e83a0532d629401a640b7ce78797f9872cb17330d8f7dbb78129af9d5572d5b26493753f0cfc6399271f820de294fe3f1099515252506ca5af9f9498965d949a99945154515a549966945e5e995e5c919a9d9391985e929b5251529c9f5c58969555955258696652949552665c9492975298976ea297599056ac979756525c925f94989eaa979399970d9a94c858a0defb5246dc6d1baff6446f0b5e33becf935bb9965aca18be73555ef7c8ac8fd7fcffffa6dcd2ff0b618e28482c2e494dcaccd34bcecfd52f4a2cd70f31f5712f704a356e0029cc581018c1141fad229377b0a5b3c832c6c2d4634a7cdac144ee5f07eead64df16d79bf0ff7f53711e687eabb80436edb5e05f97b79ffaa247b37ced8f9f9b1b5475fbd4995ddbd62e9e6073de7ae5f2cc4bd7bbb1943ca3cb3c5012d8e8328fd1651e48f320a3cb3c86c0320f48d97774f5abcaf5ab18a443d4ef2f3871dee954bec5d716e98a9f9b0f0515db6dbaa70e2ee7fe332e78d85519687239b1eb14ab8c576cc1d7253ebbf874d77364187073b9eb1d8afa9e87a5881c5d4e31ba9c62282ca7a0770f66c162a785f6effcbeec7a3bfb67faf6cbd30d976eedbd5f3d71764e7eceb7bb8b974e3c0351c6051be8c196b186edf646b0c11b7a5e4c28bdf5233fb0f0d121c1cc3bcf229e7a7aad939950f3e1b963eef1876a77c5e3f6e799ee79f92dd0f456e794ba87a2a710e36228d47fc271f67f41587b60e9a4fd3b98be3b5ab0d7baaf504e9f7275456ea06c53a09a879ee5f4b93c60978196b0801dfcbff11b88011b385b4068ee0e3255f9dff8ff82f3fbefbd8df15974cf96fbabc2ced3dedb4bac4c675f7ab6b0e3e29ce7f7ff285e7e0e329ca0229059f6ff1748c5334f99f8b4e2f9bfe8fb7b7eef78b925a532e2d3fda34e7c2b8cd7daffaa6ccd878dd636ad7fbaa5fd57cb917377bcebb4b9af5f1414e7bf9e505e6a656ee93c21e9f59d0a0b91b947decb5d4e8862a8fd5ffa7bda573e35d6cbcb677a9c49b4dccfffc04bd267a7d51b0bbb64b1e6d0c77737adbfa22c6ef95cfec194745f9defe14a4edba7aa29030018bbefd7 diff --git a/crates/chia-sdk-offers/test_data/decompressed.offer b/crates/chia-sdk-offers/test_data/decompressed.offer new file mode 100644 index 00000000..0eb46828 --- /dev/null +++ b/crates/chia-sdk-offers/test_data/decompressed.offer @@ -0,0 +1 @@ +0000000200000000000000000000000000000000000000000000000000000000000000006e29dd286d097a8376cf1ba43c3de2a4b6e1c3826dc07b4f9a536dcc495c0b920000000000000000ff02ffff01ff02ffff01ff02ff5effff04ff02ffff04ffff04ff05ffff04ffff0bff34ff0580ffff04ff0bff80808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ffff02ff2effff04ff02ffff04ff17ff80808080ffff04ffff02ff2affff04ff02ffff04ff82027fffff04ff82057fffff04ff820b7fff808080808080ffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff820bffff80808080808080808080808080ffff04ffff01ffffffff3d46ff02ff333cffff0401ff01ff81cb02ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff7cffff0bff34ff2480ffff0bff7cffff0bff7cffff0bff34ff2c80ff0980ffff0bff7cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff13ff80808080ff820b9f80ffff01ff02ff56ffff04ff02ffff04ffff02ff13ffff04ff5fffff04ff17ffff04ff2fffff04ff81bfffff04ff82017fffff04ff1bff8080808080808080ffff04ff82017fff8080808080ffff01ff088080ff0180ffff01ff02ffff03ff17ffff01ff02ffff03ffff20ff81bf80ffff0182017fffff01ff088080ff0180ffff01ff088080ff018080ff0180ff04ffff04ff05ff2780ffff04ffff10ff0bff5780ff778080ffffff02ffff03ff05ffff01ff02ffff03ffff09ffff02ffff03ffff09ff11ff5880ffff0159ff8080ff0180ffff01818f80ffff01ff02ff26ffff04ff02ffff04ff0dffff04ff0bffff04ffff04ff81b9ff82017980ff808080808080ffff01ff02ff7affff04ff02ffff04ffff02ffff03ffff09ff11ff5880ffff01ff04ff58ffff04ffff02ff76ffff04ff02ffff04ff13ffff04ff29ffff04ffff0bff34ff5b80ffff04ff2bff80808080808080ff398080ffff01ff02ffff03ffff09ff11ff7880ffff01ff02ffff03ffff20ffff02ffff03ffff09ffff0121ffff0dff298080ffff01ff02ffff03ffff09ffff0cff29ff80ff3480ff5c80ffff01ff0101ff8080ff0180ff8080ff018080ffff0109ffff01ff088080ff0180ffff010980ff018080ff0180ffff04ffff02ffff03ffff09ff11ff5880ffff0159ff8080ff0180ffff04ffff02ff26ffff04ff02ffff04ff0dffff04ff0bffff04ff17ff808080808080ff80808080808080ff0180ffff01ff04ff80ffff04ff80ff17808080ff0180ffff02ffff03ff05ffff01ff04ff09ffff02ff56ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff0bff7cffff0bff34ff2880ffff0bff7cffff0bff7cffff0bff34ff2c80ff0580ffff0bff7cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff04ffff04ff30ffff04ff5fff808080ffff02ff7effff04ff02ffff04ffff04ffff04ff2fff0580ffff04ff5fff82017f8080ffff04ffff02ff26ffff04ff02ffff04ff0bffff04ff05ffff01ff808080808080ffff04ff17ffff04ff81bfffff04ff82017fffff04ffff02ff2affff04ff02ffff04ff8204ffffff04ffff02ff76ffff04ff02ffff04ff09ffff04ff820affffff04ffff0bff34ff2d80ffff04ff15ff80808080808080ffff04ff8216ffff808080808080ffff04ff8205ffffff04ff820bffff808080808080808080808080ff02ff5affff04ff02ffff04ff5fffff04ff3bffff04ffff02ffff03ff17ffff01ff09ff2dffff02ff2affff04ff02ffff04ff27ffff04ffff02ff76ffff04ff02ffff04ff29ffff04ff57ffff04ffff0bff34ff81b980ffff04ff59ff80808080808080ffff04ff81b7ff80808080808080ff8080ff0180ffff04ff17ffff04ff05ffff04ff8202ffffff04ffff04ffff04ff78ffff04ffff0eff5cffff02ff2effff04ff02ffff04ffff04ff2fffff04ff82017fff808080ff8080808080ff808080ffff04ffff04ff20ffff04ffff0bff81bfff5cffff02ff2effff04ff02ffff04ffff04ff15ffff04ffff10ff82017fffff11ff8202dfff2b80ff8202ff80ff808080ff8080808080ff808080ff138080ff80808080808080808080ff018080ffff04ffff01a037bef360ee858133b69d595a906dc45d01af50379dad515eb9518abb7c1d2a7affff04ffff01a002f42883fb3338310825c951efcca810ecb61772d9e5da6a2d4d0a6591b8897effff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff0180808080ffffa0d7a3b357ee3eb1d3857c2e164beea5cb8cf1d0d307c3b8c8463d84a15de2e3eaffffa0947c5be1522aff5736bd2bb91204fca385660e3fa59e3bb7a3ee709f52809f71ff85174876e800ffffa0947c5be1522aff5736bd2bb91204fca385660e3fa59e3bb7a3ee709f52809f71808080809ffebd6953848e37800ad52932c6c6de0a6920ac7542d5c4881f55e07580476b7456f82a207e455bc1a77cf022fe43c988b2c9cd3dd2d94062da525eb1c272530000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0e9943cae428345e36f0e4d2d3ecdcf734ee6c6858e365c7feccc2a9ee94dbf3ba0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffff75ffc05968747470733a2f2f6261666b726569626872787572796632677779677378656b6c686167746d647874736f6371766a6a7a6471793634726a64763372646e64716e67342e697066732e6e667473746f726167652e6c696e6b2f80ffff68a0278de91c1746b60d2b914b380d360ef393850aa5391c31ee4523aee2368e0d37ffff826d75ffa168747470733a2f2f706173746562696e2e636f6d2f7261772f54354c477042653380ffff826d68a05158025f5b241c6ec1848972395c383548945f66c1610bfac0dea907b65e8d60ffff82736e01ffff8273740180ffff04ffff01a0fe8a4b4e27a2e29a4d3fc7ce9d527adbcaccbab6ada3903ccf3ba9a769d2d78bffff04ffff01ff02ffff01ff02ffff01ff02ff26ffff04ff02ffff04ff05ffff04ff17ffff04ff0bffff04ffff02ff2fff5f80ff80808080808080ffff04ffff01ffffff82ad4cff0233ffff3e04ff81f601ffffff0102ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff32ffff0bff3cff3480ffff0bff32ffff0bff32ffff0bff3cff2280ff0980ffff0bff32ff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ff04ffff04ff38ffff04ffff02ff36ffff04ff02ffff04ff05ffff04ff27ffff04ffff02ff2effff04ff02ffff04ffff02ffff03ff81afffff0181afffff010b80ff0180ff80808080ffff04ffff0bff3cff4f80ffff04ffff0bff3cff0580ff8080808080808080ff378080ff82016f80ffffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff2fffff01ff80ff808080808080808080ff0bff32ffff0bff3cff2880ffff0bff32ffff0bff32ffff0bff3cff2280ff0580ffff0bff32ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff5fffff01ff02ffff03ffff09ff82011fff3880ffff01ff02ffff03ffff09ffff18ff82059f80ff3c80ffff01ff02ffff03ffff20ff81bf80ffff01ff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff82019fffff04ff82017fff80808080808080808080ffff01ff088080ff0180ffff01ff04ff819fffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ff82017fff808080808080808080808080ff0180ffff01ff02ffff03ffff09ff82011fff2c80ffff01ff02ffff03ffff20ff82017f80ffff01ff04ffff04ff24ffff04ffff0eff10ffff02ff2effff04ff02ffff04ff82019fff8080808080ff808080ffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ffff02ff0bffff04ff17ffff04ff2fffff04ff82019fff8080808080ff8080808080808080808080ffff01ff088080ff0180ffff01ff02ffff03ffff09ff82011fff2480ffff01ff02ffff03ffff20ffff02ffff03ffff09ffff0122ffff0dff82029f8080ffff01ff02ffff03ffff09ffff0cff82029fff80ffff010280ff1080ffff01ff0101ff8080ff0180ff8080ff018080ffff01ff04ff819fffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ff82017fff8080808080808080808080ffff01ff088080ff0180ffff01ff04ff819fffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ff82017fff808080808080808080808080ff018080ff018080ff0180ffff01ff02ff3affff04ff02ffff04ff05ffff04ff0bffff04ff81bfffff04ffff02ffff03ff82017fffff0182017fffff01ff02ff0bffff04ff17ffff04ff2fffff01ff808080808080ff0180ff8080808080808080ff0180ff018080ffff04ffff01a0c5abea79afaa001b5427dfa0c8cf42ca6f38f5841b78f9b3c252733eb2de2726ffff04ffff01a0e18a795134d3618aca051c4a5d70f5a44cba0e2daf0868300b0a472ec25af76effff04ffff01ff02ffff01ff02ffff01ff02ffff03ff81bfffff01ff04ff82013fffff04ff80ffff04ffff02ffff03ffff22ff82013fffff20ffff09ff82013fff2f808080ffff01ff04ffff04ff10ffff04ffff0bffff02ff2effff04ff02ffff04ff09ffff04ff8205bfffff04ffff02ff3effff04ff02ffff04ffff04ff09ffff04ff82013fff1d8080ff80808080ff808080808080ff1580ff808080ffff02ff16ffff04ff02ffff04ff0bffff04ff17ffff04ff8202bfffff04ff15ff8080808080808080ffff01ff02ff16ffff04ff02ffff04ff0bffff04ff17ffff04ff8202bfffff04ff15ff8080808080808080ff0180ff80808080ffff01ff04ff2fffff01ff80ff80808080ff0180ffff04ffff01ffffff3f02ff04ff0101ffff822710ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff0bff2cff1480ffff0bff2affff0bff2affff0bff2cff3c80ff0980ffff0bff2aff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff17ffff01ff04ffff04ff10ffff04ffff0bff81a7ffff02ff3effff04ff02ffff04ffff04ff2fffff04ffff04ff05ffff04ffff05ffff14ffff12ff47ff0b80ff128080ffff04ffff04ff05ff8080ff80808080ff808080ff8080808080ff808080ffff02ff16ffff04ff02ffff04ff05ffff04ff0bffff04ff37ffff04ff2fff8080808080808080ff8080ff0180ffff0bff2affff0bff2cff1880ffff0bff2affff0bff2affff0bff2cff3c80ff0580ffff0bff2affff02ff3affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0e9943cae428345e36f0e4d2d3ecdcf734ee6c6858e365c7feccc2a9ee94dbf3ba0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01a0a342a13fee4ef4baed9bf967b7d39731a5b58ddf7b919b6c6f6cf6dda3a591ccffff04ffff010aff0180808080ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b08ce89075daf86f5171e2c21169dce658e5494aae1c907cf0e7416dc7e126dd175ebf6e35bce9f65135da89947ee115caff018080ff018080808080ff018080808080ff01808080ffffa0e9943cae428345e36f0e4d2d3ecdcf734ee6c6858e365c7feccc2a9ee94dbf3bffa05687517592bfb802f74138077d47a8236794d5a86d511d825126482e39979d0cff0180ff01ffffffff80ffff01ffff81f6ff80ffffff85174876e800ffa06e29dd286d097a8376cf1ba43c3de2a4b6e1c3826dc07b4f9a536dcc495c0b928080ff8080ffff33ffa0cfbfdeed5c4ca2de3d0bf520b9cb4bb7743a359bd2e6a188d19ce7dffc21d3e7ff01ffffa0cfbfdeed5c4ca2de3d0bf520b9cb4bb7743a359bd2e6a188d19ce7dffc21d3e78080ffff3fffa01a5f039491e578e7fe5bdfbcfbb8e9b4647958f2dfc5420ea833ad3ffa79856f8080ff808080808082afe5b487fa84c4cedc4b7e2b0bd7d111170fd76077753a3739439062ebdc7838149dc4ef1ed3605a007dff75fb96f50e2605d3a79948cc6139bf0fe04a194cb93aec383e63168355e3ddb2afd4231739e71fe094674d2cf7572242b7952623 diff --git a/crates/chia-sdk-signer/Cargo.toml b/crates/chia-sdk-signer/Cargo.toml new file mode 100644 index 00000000..4dd59023 --- /dev/null +++ b/crates/chia-sdk-signer/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "chia-sdk-signer" +version = "0.16.0" +edition = "2021" +license = "Apache-2.0" +description = "Calculates the BLS signatures required for coin spends in a transaction." +authors = ["Brandon Haggstrom "] +homepage = "https://github.com/Rigidity/chia-wallet-sdk" +repository = "https://github.com/Rigidity/chia-wallet-sdk" +readme = { workspace = true } +keywords = { workspace = true } +categories = { workspace = true } + +[lints] +workspace = true + +[dependencies] +chia-bls = { workspace = true } +chia-protocol = { workspace = true } +chia-consensus = { workspace = true } +clvm-traits = { workspace = true } +clvmr = { workspace = true } +thiserror = { workspace = true } +chia-sdk-types = { workspace = true } + +[dev-dependencies] +chia-puzzles = { workspace = true } +hex = { workspace = true } +hex-literal = { workspace = true } diff --git a/crates/chia-sdk-signer/src/agg_sig_constants.rs b/crates/chia-sdk-signer/src/agg_sig_constants.rs new file mode 100644 index 00000000..d6a7d57f --- /dev/null +++ b/crates/chia-sdk-signer/src/agg_sig_constants.rs @@ -0,0 +1,83 @@ +use chia_consensus::consensus_constants::ConsensusConstants; +use chia_protocol::Bytes32; +use clvmr::sha2::Sha256; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AggSigConstants { + me: Bytes32, + parent: Bytes32, + puzzle: Bytes32, + amount: Bytes32, + parent_amount: Bytes32, + puzzle_amount: Bytes32, + parent_puzzle: Bytes32, +} + +impl AggSigConstants { + pub fn new(agg_sig_me: Bytes32) -> Self { + Self { + me: agg_sig_me, + parent: hash(agg_sig_me, 43), + puzzle: hash(agg_sig_me, 44), + amount: hash(agg_sig_me, 45), + puzzle_amount: hash(agg_sig_me, 46), + parent_amount: hash(agg_sig_me, 47), + parent_puzzle: hash(agg_sig_me, 48), + } + } + + pub fn me(&self) -> Bytes32 { + self.me + } + + pub fn parent(&self) -> Bytes32 { + self.parent + } + + pub fn puzzle(&self) -> Bytes32 { + self.puzzle + } + + pub fn amount(&self) -> Bytes32 { + self.amount + } + + pub fn parent_amount(&self) -> Bytes32 { + self.parent_amount + } + + pub fn puzzle_amount(&self) -> Bytes32 { + self.puzzle_amount + } + + pub fn parent_puzzle(&self) -> Bytes32 { + self.parent_puzzle + } +} + +impl From<&ConsensusConstants> for AggSigConstants { + fn from(constants: &ConsensusConstants) -> Self { + Self { + me: constants.agg_sig_me_additional_data, + parent: constants.agg_sig_parent_additional_data, + puzzle: constants.agg_sig_puzzle_additional_data, + amount: constants.agg_sig_amount_additional_data, + puzzle_amount: constants.agg_sig_puzzle_amount_additional_data, + parent_amount: constants.agg_sig_parent_amount_additional_data, + parent_puzzle: constants.agg_sig_parent_puzzle_additional_data, + } + } +} + +impl From for AggSigConstants { + fn from(constants: ConsensusConstants) -> Self { + Self::from(&constants) + } +} + +fn hash(agg_sig_data: Bytes32, byte: u8) -> Bytes32 { + let mut hasher = Sha256::new(); + hasher.update(agg_sig_data); + hasher.update([byte]); + hasher.finalize().into() +} diff --git a/crates/chia-sdk-signer/src/error.rs b/crates/chia-sdk-signer/src/error.rs new file mode 100644 index 00000000..a67b2f93 --- /dev/null +++ b/crates/chia-sdk-signer/src/error.rs @@ -0,0 +1,18 @@ +use clvm_traits::{FromClvmError, ToClvmError}; +use clvmr::reduction::EvalErr; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SignerError { + #[error("Eval error: {0}")] + Eval(#[from] EvalErr), + + #[error("To CLVM error: {0}")] + ToClvm(#[from] ToClvmError), + + #[error("From CLVM error: {0}")] + FromClvm(#[from] FromClvmError), + + #[error("Infinity public key")] + InfinityPublicKey, +} diff --git a/crates/chia-sdk-signer/src/lib.rs b/crates/chia-sdk-signer/src/lib.rs new file mode 100644 index 00000000..e4d27beb --- /dev/null +++ b/crates/chia-sdk-signer/src/lib.rs @@ -0,0 +1,7 @@ +mod agg_sig_constants; +mod error; +mod required_signature; + +pub use agg_sig_constants::*; +pub use error::*; +pub use required_signature::*; diff --git a/crates/chia-sdk-signer/src/required_signature.rs b/crates/chia-sdk-signer/src/required_signature.rs new file mode 100644 index 00000000..1306d7aa --- /dev/null +++ b/crates/chia-sdk-signer/src/required_signature.rs @@ -0,0 +1,261 @@ +use chia_bls::PublicKey; +use chia_protocol::{Bytes, Bytes32, Coin, CoinSpend}; +use chia_sdk_types::{run_puzzle, AggSig, AggSigKind, Condition}; +use clvm_traits::{FromClvm, ToClvm}; +use clvmr::Allocator; + +use crate::{AggSigConstants, SignerError}; + +#[derive(Debug, Clone)] +pub struct RequiredSignature { + public_key: PublicKey, + raw_message: Bytes, + appended_info: Vec, + domain_string: Option, +} + +impl RequiredSignature { + /// Converts a known [`AggSig`] condition to a `RequiredSignature` if possible. + pub fn from_condition(coin: &Coin, condition: AggSig, constants: &AggSigConstants) -> Self { + let domain_string; + + let public_key = condition.public_key; + let message = condition.message; + + let appended_info = match condition.kind { + AggSigKind::Parent => { + domain_string = constants.parent(); + coin.parent_coin_info.to_vec() + } + AggSigKind::Puzzle => { + domain_string = constants.puzzle(); + coin.puzzle_hash.to_vec() + } + AggSigKind::Amount => { + domain_string = constants.amount(); + u64_to_bytes(coin.amount) + } + AggSigKind::PuzzleAmount => { + domain_string = constants.puzzle_amount(); + let puzzle = coin.puzzle_hash; + [puzzle.to_vec(), u64_to_bytes(coin.amount)].concat() + } + AggSigKind::ParentAmount => { + domain_string = constants.parent_amount(); + let parent = coin.parent_coin_info; + [parent.to_vec(), u64_to_bytes(coin.amount)].concat() + } + AggSigKind::ParentPuzzle => { + domain_string = constants.parent_puzzle(); + [coin.parent_coin_info.to_vec(), coin.puzzle_hash.to_vec()].concat() + } + AggSigKind::Unsafe => { + return Self { + public_key, + raw_message: message, + appended_info: Vec::new(), + domain_string: None, + } + } + AggSigKind::Me => { + domain_string = constants.me(); + coin.coin_id().to_vec() + } + }; + + Self { + public_key, + raw_message: message, + appended_info, + domain_string: Some(domain_string), + } + } + + /// Calculates the required signatures for a coin spend. + /// All of these signatures aggregated together should be + /// sufficient, unless secp keys are used as well. + pub fn from_coin_spend( + allocator: &mut Allocator, + coin_spend: &CoinSpend, + constants: &AggSigConstants, + ) -> Result, SignerError> { + let puzzle = coin_spend.puzzle_reveal.to_clvm(allocator)?; + let solution = coin_spend.solution.to_clvm(allocator)?; + let output = run_puzzle(allocator, puzzle, solution)?; + let conditions = Vec::::from_clvm(allocator, output)?; + + let mut result = Vec::new(); + + for condition in conditions { + let Some(agg_sig) = condition.into_agg_sig() else { + continue; + }; + + if agg_sig.public_key.is_inf() { + return Err(SignerError::InfinityPublicKey); + } + + result.push(Self::from_condition(&coin_spend.coin, agg_sig, constants)); + } + + Ok(result) + } + + /// Calculates the required signatures for a spend bundle. + /// All of these signatures aggregated together should be + /// sufficient, unless secp keys are used as well. + pub fn from_coin_spends( + allocator: &mut Allocator, + coin_spends: &[CoinSpend], + constants: &AggSigConstants, + ) -> Result, SignerError> { + let mut required_signatures = Vec::new(); + for coin_spend in coin_spends { + required_signatures.extend(Self::from_coin_spend(allocator, coin_spend, constants)?); + } + Ok(required_signatures) + } + + /// The public key required to verify the signature. + pub fn public_key(&self) -> PublicKey { + self.public_key + } + + /// The message field of the condition, without anything appended. + pub fn raw_message(&self) -> &[u8] { + self.raw_message.as_ref() + } + + /// Additional coin information that is appended to the condition's message. + pub fn appended_info(&self) -> &[u8] { + &self.appended_info + } + + /// The domain string that is appended to the condition's message. + pub fn domain_string(&self) -> Option { + self.domain_string + } + + /// Computes the message that needs to be signed. + pub fn final_message(&self) -> Vec { + let mut message = Vec::from(self.raw_message.as_ref()); + message.extend(&self.appended_info); + if let Some(domain_string) = self.domain_string { + message.extend(domain_string.to_bytes()); + } + message + } +} + +fn u64_to_bytes(value: u64) -> Vec { + let mut allocator = Allocator::new(); + let atom = allocator.new_number(value.into()).unwrap(); + allocator.atom(atom).as_ref().to_vec() +} + +#[cfg(test)] +mod tests { + use super::*; + + use chia_bls::{master_to_wallet_unhardened, SecretKey}; + use chia_protocol::Bytes32; + use chia_puzzles::DeriveSynthetic; + use chia_sdk_types::MAINNET_CONSTANTS; + use hex_literal::hex; + + #[test] + fn test_messages() { + let coin = Coin::new(Bytes32::from([1; 32]), Bytes32::from([2; 32]), 3); + + let root_sk = SecretKey::from_bytes(&hex!( + "1b72f8ed55860ea5441729c8e36ce1d6f4c8be9bbcf658502a7a0169f55638b9" + )) + .unwrap(); + let public_key = master_to_wallet_unhardened(&root_sk.public_key(), 0).derive_synthetic(); + + let message: Bytes = vec![1, 2, 3].into(); + + macro_rules! condition { + ($variant:ident) => { + AggSig { + kind: AggSigKind::$variant, + public_key: public_key.clone(), + message: message.clone(), + } + }; + } + + let cases = vec![ + ( + condition!(Me), + hex::encode(coin.coin_id()), + Some(hex::encode(MAINNET_CONSTANTS.agg_sig_me_additional_data)), + ), + (condition!(Unsafe), String::new(), None), + ( + condition!(Parent), + "0101010101010101010101010101010101010101010101010101010101010101".to_string(), + Some( + "baf5d69c647c91966170302d18521b0a85663433d161e72c826ed08677b53a74".to_string(), + ), + ), + ( + condition!(Puzzle), + "0202020202020202020202020202020202020202020202020202020202020202".to_string(), + Some( + "284fa2ef486c7a41cc29fc99c9d08376161e93dd37817edb8219f42dca7592c4".to_string(), + ), + ), + ( + condition!(ParentPuzzle), + "0101010101010101010101010101010101010101010101010101010101010101\ +0202020202020202020202020202020202020202020202020202020202020202" + .to_string(), + Some( + "2ebfdae17b29d83bae476a25ea06f0c4bd57298faddbbc3ec5ad29b9b86ce5df".to_string(), + ), + ), + ( + condition!(Amount), + "03".to_string(), + Some( + "cda186a9cd030f7a130fae45005e81cae7a90e0fa205b75f6aebc0d598e0348e".to_string(), + ), + ), + ( + condition!(PuzzleAmount), + "020202020202020202020202020202020202020202020202020202020202020203".to_string(), + Some( + "0f7d90dff0613e6901e24dae59f1e690f18b8f5fbdcf1bb192ac9deaf7de22ad".to_string(), + ), + ), + ( + condition!(ParentAmount), + "010101010101010101010101010101010101010101010101010101010101010103".to_string(), + Some( + "585796bd90bb553c0430b87027ffee08d88aba0162c6e1abbbcc6b583f2ae7f9".to_string(), + ), + ), + ]; + + let constants = AggSigConstants::from(&*MAINNET_CONSTANTS); + + for (condition, appended_info, domain_string) in cases { + let required = RequiredSignature::from_condition(&coin, condition, &constants); + + assert_eq!(required.public_key(), public_key); + assert_eq!(required.raw_message(), message.as_ref()); + assert_eq!(hex::encode(required.appended_info()), appended_info); + assert_eq!(required.domain_string().map(hex::encode), domain_string); + + let mut message = Vec::::new(); + message.extend(required.raw_message()); + message.extend(required.appended_info()); + if let Some(domain_string) = required.domain_string() { + message.extend(domain_string.to_bytes()); + } + + assert_eq!(hex::encode(message), hex::encode(required.final_message())); + } + } +} diff --git a/crates/chia-sdk-test/Cargo.toml b/crates/chia-sdk-test/Cargo.toml new file mode 100644 index 00000000..d5abede9 --- /dev/null +++ b/crates/chia-sdk-test/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "chia-sdk-test" +version = "0.16.0" +edition = "2021" +license = "Apache-2.0" +description = "A wallet simulator and related tooling for testing Chia wallet code." +authors = ["Brandon Haggstrom "] +homepage = "https://github.com/Rigidity/chia-wallet-sdk" +repository = "https://github.com/Rigidity/chia-wallet-sdk" +readme = { workspace = true } +keywords = { workspace = true } +categories = { workspace = true } + +[lints] +workspace = true + +[dependencies] +chia-bls = { workspace = true } +chia-consensus = { workspace = true } +chia-protocol = { workspace = true } +chia-traits = { workspace = true } +chia-puzzles = { workspace = true } +clvm-utils = { workspace = true } +clvm-traits = { workspace = true } +clvmr = { workspace = true } +futures-channel = { workspace = true, features = ["sink"] } +futures-util = { workspace = true } +indexmap = { workspace = true } +thiserror = { workspace = true } +log = { workspace = true } +itertools = { workspace = true } +rand = { workspace = true } +rand_chacha = { workspace = true } +fastrand = { workspace = true } +bip39 = { workspace = true } +anyhow = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tokio-tungstenite = { workspace = true } +chia-sdk-types = { workspace = true } +chia-sdk-signer = { workspace = true } +chia-sdk-client = { workspace = true } diff --git a/crates/chia-sdk-test/src/announcements.rs b/crates/chia-sdk-test/src/announcements.rs new file mode 100644 index 00000000..ba73fbdc --- /dev/null +++ b/crates/chia-sdk-test/src/announcements.rs @@ -0,0 +1,105 @@ +use chia_protocol::{Bytes, Bytes32, CoinSpend}; +use chia_sdk_types::{ + announcement_id, AssertCoinAnnouncement, AssertPuzzleAnnouncement, CreateCoinAnnouncement, + CreatePuzzleAnnouncement, +}; +use clvm_traits::{FromClvm, ToClvm}; +use clvmr::{reduction::Reduction, run_program, Allocator, ChiaDialect, NodePtr}; + +#[derive(Debug, Default, Clone)] +pub struct Announcements { + pub created_coin: Vec, + pub asserted_coin: Vec, + pub created_puzzle: Vec, + pub asserted_puzzle: Vec, +} + +/// Print the announcements that are created and asserted by a list of coin spends. +/// +/// # Panics +/// +/// Panics if the announcements cannot be extracted from the coin spends. +pub fn debug_announcements(coin_spends: &[CoinSpend]) { + let all_announcements: Vec = coin_spends + .iter() + .map(|coin_spend| { + announcements_for_spend(coin_spend).expect("could not extract announcements") + }) + .collect(); + + let mut should_panic = false; + + for (i, announcements) in all_announcements.iter().enumerate() { + for &asserted_coin in &announcements.asserted_coin { + let Some(created_index) = all_announcements.iter().enumerate().position(|(i, a)| { + a.created_coin.iter().any(|message| { + asserted_coin == announcement_id(coin_spends[i].coin.coin_id(), message.clone()) + }) + }) else { + println!("spend at index {i} asserted unknown coin announcement"); + should_panic = true; + continue; + }; + + println!( + "spend at index {i} asserted coin announcement created by spend at index {created_index}" + ); + } + + for &asserted_puzzle in &announcements.asserted_puzzle { + let Some(created_index) = all_announcements.iter().enumerate().position(|(i, a)| { + a.created_puzzle.iter().any(|message| { + asserted_puzzle + == announcement_id(coin_spends[i].coin.puzzle_hash, message.clone()) + }) + }) else { + println!("spend at index {i} asserted unknown puzzle announcement"); + should_panic = true; + continue; + }; + + println!( + "spend at index {i} asserted puzzle announcement created by spend at index {created_index}" + ); + } + } + + assert!( + !should_panic, + "asserted announcements do not match created announcements" + ); +} + +pub fn announcements_for_spend(coin_spend: &CoinSpend) -> anyhow::Result { + let mut announcements = Announcements::default(); + + let allocator = &mut Allocator::new(); + let puzzle = coin_spend.puzzle_reveal.to_clvm(allocator)?; + let solution = coin_spend.solution.to_clvm(allocator)?; + + let Reduction(_cost, output) = run_program( + allocator, + &ChiaDialect::new(0), + puzzle, + solution, + 11_000_000_000, + )?; + + let conditions = Vec::::from_clvm(allocator, output)?; + + for condition in conditions { + if let Ok(condition) = CreateCoinAnnouncement::from_clvm(allocator, condition) { + announcements.created_coin.push(condition.message); + } else if let Ok(condition) = CreatePuzzleAnnouncement::from_clvm(allocator, condition) { + announcements.created_puzzle.push(condition.message); + } else if let Ok(condition) = AssertCoinAnnouncement::from_clvm(allocator, condition) { + announcements.asserted_coin.push(condition.announcement_id); + } else if let Ok(condition) = AssertPuzzleAnnouncement::from_clvm(allocator, condition) { + announcements + .asserted_puzzle + .push(condition.announcement_id); + } + } + + Ok(announcements) +} diff --git a/crates/chia-sdk-test/src/error.rs b/crates/chia-sdk-test/src/error.rs new file mode 100644 index 00000000..dbe3e5e8 --- /dev/null +++ b/crates/chia-sdk-test/src/error.rs @@ -0,0 +1,20 @@ +use std::io; + +use chia_consensus::gen::validation_error::ErrorCode; +use chia_sdk_signer::SignerError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SimulatorError { + #[error("IO error: {0}")] + Io(#[from] io::Error), + + #[error("Validation error: {0:?}")] + Validation(ErrorCode), + + #[error("Signer error: {0}")] + Signer(#[from] SignerError), + + #[error("Missing key ")] + MissingKey, +} diff --git a/crates/chia-sdk-test/src/events.rs b/crates/chia-sdk-test/src/events.rs new file mode 100644 index 00000000..e7ce4f87 --- /dev/null +++ b/crates/chia-sdk-test/src/events.rs @@ -0,0 +1,14 @@ +use chia_protocol::{CoinStateUpdate, Message, ProtocolMessageTypes}; +use chia_traits::Streamable; +use tokio::sync::mpsc; + +pub fn coin_state_updates(receiver: &mut mpsc::Receiver) -> Vec { + let mut items = Vec::new(); + while let Ok(message) = receiver.try_recv() { + if message.msg_type != ProtocolMessageTypes::CoinStateUpdate { + continue; + } + items.push(CoinStateUpdate::from_bytes(&message.data).unwrap()); + } + items +} diff --git a/crates/chia-sdk-test/src/keys.rs b/crates/chia-sdk-test/src/keys.rs new file mode 100644 index 00000000..09652df4 --- /dev/null +++ b/crates/chia-sdk-test/src/keys.rs @@ -0,0 +1,26 @@ +use bip39::Mnemonic; +use chia_bls::SecretKey; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +pub fn test_secret_keys(no_keys: usize) -> Result, bip39::Error> { + let mut rng = ChaCha8Rng::seed_from_u64(0); + + let mut keys = Vec::with_capacity(no_keys); + + for _ in 0..no_keys { + let entropy: [u8; 32] = rng.gen(); + let mnemonic = Mnemonic::from_entropy(&entropy)?; + let seed = mnemonic.to_seed(""); + let sk = SecretKey::from_seed(&seed); + keys.push(sk); + } + + Ok(keys) +} + +pub fn test_secret_key() -> Result { + Ok(test_secret_keys(1)? + .pop() + .expect("Unable to get secret key")) +} diff --git a/crates/chia-sdk-test/src/lib.rs b/crates/chia-sdk-test/src/lib.rs new file mode 100644 index 00000000..3bf5d610 --- /dev/null +++ b/crates/chia-sdk-test/src/lib.rs @@ -0,0 +1,34 @@ +mod announcements; +mod error; +mod events; +mod keys; +mod peer_simulator; +mod simulator; +mod transaction; + +pub use announcements::*; +pub use error::*; +pub use events::*; +pub use keys::*; +pub use peer_simulator::*; +pub use simulator::*; +pub use transaction::*; + +use chia_protocol::{Bytes32, Program}; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::tree_hash; +use clvmr::Allocator; + +pub fn to_program(value: impl ToClvm) -> anyhow::Result { + let mut allocator = Allocator::new(); + let ptr = value.to_clvm(&mut allocator)?; + Ok(Program::from_clvm(&allocator, ptr)?) +} + +pub fn to_puzzle(value: impl ToClvm) -> anyhow::Result<(Bytes32, Program)> { + let mut allocator = Allocator::new(); + let ptr = value.to_clvm(&mut allocator)?; + let puzzle_reveal = Program::from_clvm(&allocator, ptr)?; + let puzzle_hash = tree_hash(&allocator, ptr); + Ok((puzzle_hash.into(), puzzle_reveal)) +} diff --git a/crates/chia-sdk-test/src/peer_simulator.rs b/crates/chia-sdk-test/src/peer_simulator.rs new file mode 100644 index 00000000..410884e0 --- /dev/null +++ b/crates/chia-sdk-test/src/peer_simulator.rs @@ -0,0 +1,885 @@ +use std::{net::SocketAddr, sync::Arc}; + +use chia_protocol::{Bytes32, Coin, CoinState, Message}; +use chia_sdk_client::Peer; +use error::PeerSimulatorError; +use peer_map::PeerMap; +use simulator_config::SimulatorConfig; +use subscriptions::Subscriptions; +use tokio::{ + net::TcpListener, + sync::{mpsc, Mutex}, + task::JoinHandle, +}; +use tokio_tungstenite::connect_async; +use ws_connection::ws_connection; + +use crate::Simulator; + +mod error; +mod peer_map; +mod simulator_config; +mod subscriptions; +mod ws_connection; + +#[derive(Debug)] +pub struct PeerSimulator { + config: Arc, + addr: SocketAddr, + simulator: Arc>, + subscriptions: Arc>, + join_handle: JoinHandle<()>, +} + +impl PeerSimulator { + pub async fn new() -> Result { + Self::with_config(SimulatorConfig::default()).await + } + + pub async fn with_config(config: SimulatorConfig) -> Result { + log::info!("starting simulator"); + + let addr = "127.0.0.1:0"; + let peer_map = PeerMap::default(); + let listener = TcpListener::bind(addr).await?; + let addr = listener.local_addr()?; + let simulator = Arc::new(Mutex::new(Simulator::default())); + let subscriptions = Arc::new(Mutex::new(Subscriptions::default())); + let config = Arc::new(config); + + let simulator_clone = simulator.clone(); + let subscriptions_clone = subscriptions.clone(); + let config_clone = config.clone(); + + let join_handle = tokio::spawn(async move { + let simulator = simulator_clone; + let subscriptions = subscriptions_clone; + let config = config_clone; + + while let Ok((stream, addr)) = listener.accept().await { + let stream = match tokio_tungstenite::accept_async(stream).await { + Ok(stream) => stream, + Err(error) => { + log::error!("error accepting websocket connection: {}", error); + continue; + } + }; + tokio::spawn(ws_connection( + peer_map.clone(), + stream, + addr, + config.clone(), + simulator.clone(), + subscriptions.clone(), + )); + } + }); + + Ok(Self { + config, + addr, + simulator, + subscriptions, + join_handle, + }) + } + + pub fn config(&self) -> &SimulatorConfig { + &self.config + } + + pub async fn connect_split( + &self, + ) -> Result<(Peer, mpsc::Receiver), PeerSimulatorError> { + log::info!("connecting new peer to simulator"); + let (ws, _) = connect_async(format!("ws://{}", self.addr)).await?; + Ok(Peer::from_websocket(ws)?) + } + + pub async fn connect(&self) -> Result { + let (peer, mut receiver) = self.connect_split().await?; + + tokio::spawn(async move { + while let Some(message) = receiver.recv().await { + log::debug!("received message: {message:?}"); + } + }); + + Ok(peer) + } + + pub async fn reset(&self) -> Result<(), PeerSimulatorError> { + *self.simulator.lock().await = Simulator::default(); + *self.subscriptions.lock().await = Subscriptions::default(); + Ok(()) + } + + pub async fn mint_coin(&self, puzzle_hash: Bytes32, amount: u64) -> Coin { + self.simulator.lock().await.new_coin(puzzle_hash, amount) + } + + pub async fn add_hint(&self, coin_id: Bytes32, hint: Bytes32) { + self.simulator.lock().await.hint_coin(coin_id, hint); + } + + pub async fn coin_state(&self, coin_id: Bytes32) -> Option { + self.simulator.lock().await.coin_state(coin_id) + } + + pub async fn height(&self) -> u32 { + self.simulator.lock().await.height() + } + + pub async fn header_hash(&self, height: u32) -> Bytes32 { + self.simulator.lock().await.header_hash_of(height).unwrap() + } + + pub async fn peak_hash(&self) -> Bytes32 { + self.simulator.lock().await.header_hash() + } +} + +impl Drop for PeerSimulator { + fn drop(&mut self) { + self.join_handle.abort(); + } +} + +#[cfg(test)] +mod tests { + use chia_bls::{DerivableKey, PublicKey, Signature}; + use chia_protocol::{ + Bytes, CoinSpend, CoinStateFilters, CoinStateUpdate, RespondCoinState, RespondPuzzleState, + SpendBundle, + }; + use chia_sdk_types::{AggSigMe, CreateCoin, Remark}; + + use crate::{coin_state_updates, test_secret_key, test_transaction, to_program, to_puzzle}; + + use super::*; + + #[tokio::test] + async fn test_coin_state() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + + let coin = sim.mint_coin(Bytes32::default(), 1000).await; + let coin_state = sim + .coin_state(coin.coin_id()) + .await + .expect("missing coin state"); + + assert_eq!(coin_state.coin, coin); + assert_eq!(coin_state.created_height, Some(0)); + assert_eq!(coin_state.spent_height, None); + + Ok(()) + } + + #[tokio::test] + async fn test_empty_transaction() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + + let empty_bundle = SpendBundle::new(Vec::new(), Signature::default()); + let transaction_id = empty_bundle.name(); + + let ack = peer.send_transaction(empty_bundle).await?; + assert_eq!(ack.status, 3); + assert_eq!(ack.txid, transaction_id); + + Ok(()) + } + + #[tokio::test] + async fn test_simple_transaction() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new(coin, puzzle_reveal, to_program(())?)], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 1); + + Ok(()) + } + + #[tokio::test] + async fn test_unknown_coin() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = Coin::new(Bytes32::default(), puzzle_hash, 0); + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new(coin, puzzle_reveal, to_program(())?)], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 3); + + Ok(()) + } + + #[tokio::test] + async fn test_bad_signature() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + let public_key = test_secret_key()?.public_key(); + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new( + coin, + puzzle_reveal, + to_program([AggSigMe::new(public_key, Bytes::default())])?, + )], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 3); + + Ok(()) + } + + #[tokio::test] + async fn test_infinity_signature() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new( + coin, + puzzle_reveal, + to_program([AggSigMe::new(PublicKey::default(), Bytes::default())])?, + )], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 3); + + Ok(()) + } + + #[tokio::test] + async fn test_valid_signature() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + let sk = test_secret_key()?; + let pk = sk.public_key(); + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + + test_transaction( + &peer, + vec![CoinSpend::new( + coin, + puzzle_reveal, + to_program([AggSigMe::new(pk, b"Hello, world!".to_vec().into())])?, + )], + &[sk], + &(&sim.config.constants).into(), + ) + .await; + + Ok(()) + } + + #[tokio::test] + async fn test_aggregated_signature() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + + let sk1 = test_secret_key()?.derive_unhardened(0); + let pk1 = sk1.public_key(); + + let sk2 = test_secret_key()?.derive_unhardened(1); + let pk2 = sk2.public_key(); + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + + test_transaction( + &peer, + vec![CoinSpend::new( + coin, + puzzle_reveal, + to_program([ + AggSigMe::new(pk1, b"Hello, world!".to_vec().into()), + AggSigMe::new(pk2, b"Goodbye, world!".to_vec().into()), + ])?, + )], + &[sk1, sk2], + &(&sim.config.constants).into(), + ) + .await; + + Ok(()) + } + + #[tokio::test] + async fn test_excessive_output() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new( + coin, + puzzle_reveal, + to_program([CreateCoin::new(puzzle_hash, 1, Vec::new())])?, + )], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 3); + + Ok(()) + } + + #[tokio::test] + async fn test_lineage() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let mut coin = sim.mint_coin(puzzle_hash, 1000).await; + + for _ in 0..1000 { + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new( + coin, + puzzle_reveal.clone(), + to_program([CreateCoin::new(puzzle_hash, coin.amount - 1, Vec::new())])?, + )], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 1); + + coin = Coin::new(coin.coin_id(), puzzle_hash, coin.amount - 1); + } + + Ok(()) + } + + #[tokio::test] + async fn test_request_children_unknown() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + + let children = peer.request_children(Bytes32::default()).await?; + assert!(children.coin_states.is_empty()); + + Ok(()) + } + + #[tokio::test] + async fn test_request_empty_children() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new(coin, puzzle_reveal, to_program(())?)], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 1); + + let children = peer.request_children(coin.coin_id()).await?; + assert!(children.coin_states.is_empty()); + + Ok(()) + } + + #[tokio::test] + async fn test_request_children() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 3).await; + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new( + coin, + puzzle_reveal, + to_program([ + CreateCoin::new(puzzle_hash, 1, Vec::new()), + CreateCoin::new(puzzle_hash, 2, Vec::new()), + ])?, + )], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 1); + + let children = peer.request_children(coin.coin_id()).await?; + assert_eq!(children.coin_states.len(), 2); + + let found_1 = children + .coin_states + .iter() + .find(|cs| cs.coin.amount == 1) + .copied(); + let found_2 = children + .coin_states + .iter() + .find(|cs| cs.coin.amount == 2) + .copied(); + + let expected_1 = CoinState::new(Coin::new(coin.coin_id(), puzzle_hash, 1), None, Some(0)); + let expected_2 = CoinState::new(Coin::new(coin.coin_id(), puzzle_hash, 2), None, Some(0)); + + assert_eq!(found_1, Some(expected_1)); + assert_eq!(found_2, Some(expected_2)); + + Ok(()) + } + + #[tokio::test] + async fn test_puzzle_solution() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + let solution = to_program([Remark::new(())])?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new( + coin, + puzzle_reveal.clone(), + solution.clone(), + )], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 1); + + let response = peer + .request_puzzle_and_solution(coin.coin_id(), 0) + .await? + .unwrap(); + assert_eq!(response.coin_name, coin.coin_id()); + assert_eq!(response.puzzle, puzzle_reveal); + assert_eq!(response.solution, solution); + assert_eq!(response.height, 0); + + Ok(()) + } + + #[tokio::test] + async fn test_spent_coin_subscription() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let (peer, mut receiver) = sim.connect_split().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + let mut coin_state = sim + .coin_state(coin.coin_id()) + .await + .expect("missing coin state"); + + let coin_states = peer + .register_for_coin_updates(vec![coin.coin_id()], 0) + .await? + .coin_states; + assert_eq!(coin_states.len(), 1); + assert_eq!(coin_states[0], coin_state); + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new(coin, puzzle_reveal, to_program(())?)], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 1); + + coin_state.spent_height = Some(0); + + let updates = coin_state_updates(&mut receiver); + assert_eq!(updates.len(), 1); + + assert_eq!( + updates[0], + CoinStateUpdate::new(1, 1, sim.peak_hash().await, vec![coin_state]) + ); + + Ok(()) + } + + #[tokio::test] + async fn test_created_coin_subscription() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let (peer, mut receiver) = sim.connect_split().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 1).await; + let child_coin = Coin::new(coin.coin_id(), puzzle_hash, 1); + + let coin_states = peer + .register_for_coin_updates(vec![child_coin.coin_id()], 0) + .await? + .coin_states; + assert_eq!(coin_states.len(), 0); + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new( + coin, + puzzle_reveal, + to_program([CreateCoin::new(puzzle_hash, 1, Vec::new())])?, + )], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 1); + + let updates = coin_state_updates(&mut receiver); + assert_eq!(updates.len(), 1); + + let coin_state = CoinState::new(child_coin, None, Some(0)); + + assert_eq!( + updates[0], + CoinStateUpdate::new(1, 1, sim.peak_hash().await, vec![coin_state]) + ); + + Ok(()) + } + + #[tokio::test] + async fn test_spent_puzzle_subscription() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let (peer, mut receiver) = sim.connect_split().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + let mut coin_state = sim + .coin_state(coin.coin_id()) + .await + .expect("missing coin state"); + + let coin_states = peer + .register_for_ph_updates(vec![coin.puzzle_hash], 0) + .await? + .coin_states; + assert_eq!(coin_states.len(), 1); + assert_eq!(coin_states[0], coin_state); + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new(coin, puzzle_reveal, to_program(())?)], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 1); + + coin_state.spent_height = Some(0); + + let updates = coin_state_updates(&mut receiver); + assert_eq!(updates.len(), 1); + + assert_eq!( + updates[0], + CoinStateUpdate::new(1, 1, sim.peak_hash().await, vec![coin_state]) + ); + + Ok(()) + } + + #[tokio::test] + async fn test_created_puzzle_subscription() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let (peer, mut receiver) = sim.connect_split().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 1).await; + let child_coin = Coin::new(coin.coin_id(), Bytes32::default(), 1); + + let coin_states = peer + .register_for_ph_updates(vec![child_coin.puzzle_hash], 0) + .await? + .coin_states; + assert_eq!(coin_states.len(), 0); + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new( + coin, + puzzle_reveal, + to_program([CreateCoin::new(child_coin.puzzle_hash, 1, Vec::new())])?, + )], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 1); + + let updates = coin_state_updates(&mut receiver); + assert_eq!(updates.len(), 1); + + let coin_state = CoinState::new(child_coin, None, Some(0)); + + assert_eq!( + updates[0], + CoinStateUpdate::new(1, 1, sim.peak_hash().await, vec![coin_state]) + ); + + Ok(()) + } + + #[tokio::test] + async fn test_spent_hint_subscription() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let (peer, mut receiver) = sim.connect_split().await?; + + let hint = Bytes32::new([42; 32]); + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + sim.add_hint(coin.coin_id(), hint).await; + + let mut coin_state = sim + .coin_state(coin.coin_id()) + .await + .expect("missing coin state"); + + let coin_states = peer + .register_for_ph_updates(vec![hint], 0) + .await? + .coin_states; + assert_eq!(coin_states.len(), 1); + assert_eq!(coin_states[0], coin_state); + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new(coin, puzzle_reveal, to_program(())?)], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 1); + + coin_state.spent_height = Some(0); + + let updates = coin_state_updates(&mut receiver); + assert_eq!(updates.len(), 1); + + assert_eq!( + updates[0], + CoinStateUpdate::new(1, 1, sim.peak_hash().await, vec![coin_state]) + ); + + Ok(()) + } + + #[tokio::test] + async fn test_created_hint_subscription() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let (peer, mut receiver) = sim.connect_split().await?; + + let hint = Bytes32::new([42; 32]); + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + + let coin_states = peer + .register_for_ph_updates(vec![hint], 0) + .await? + .coin_states; + assert_eq!(coin_states.len(), 0); + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new( + coin, + puzzle_reveal, + to_program([CreateCoin::new(puzzle_hash, 0, vec![hint.into()])])?, + )], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 1); + + let updates = coin_state_updates(&mut receiver); + assert_eq!(updates.len(), 1); + + assert_eq!( + updates[0], + CoinStateUpdate::new( + 1, + 1, + sim.peak_hash().await, + vec![CoinState::new( + Coin::new(coin.coin_id(), puzzle_hash, 0), + None, + Some(0) + )] + ) + ); + + Ok(()) + } + + #[tokio::test] + async fn test_request_coin_state() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + let mut coin_state = sim + .coin_state(coin.coin_id()) + .await + .expect("missing coin state"); + + let response = peer + .request_coin_state( + vec![coin.coin_id()], + None, + sim.config().constants.genesis_challenge, + false, + ) + .await? + .unwrap(); + assert_eq!( + response, + RespondCoinState::new(vec![coin.coin_id()], vec![coin_state]) + ); + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new(coin, puzzle_reveal, to_program(())?)], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 1); + + coin_state.spent_height = Some(0); + + let response = peer + .request_coin_state( + vec![coin.coin_id()], + None, + sim.config().constants.genesis_challenge, + false, + ) + .await? + .unwrap(); + assert_eq!( + response, + RespondCoinState::new(vec![coin.coin_id()], vec![coin_state]) + ); + + Ok(()) + } + + #[tokio::test] + async fn test_request_puzzle_state() -> anyhow::Result<()> { + let sim = PeerSimulator::new().await?; + let peer = sim.connect().await?; + + let (puzzle_hash, puzzle_reveal) = to_puzzle(1)?; + + let coin = sim.mint_coin(puzzle_hash, 0).await; + let mut coin_state = sim + .coin_state(coin.coin_id()) + .await + .expect("missing coin state"); + + let response = peer + .request_puzzle_state( + vec![puzzle_hash], + None, + sim.config().constants.genesis_challenge, + CoinStateFilters::new(true, true, true, 0), + false, + ) + .await? + .unwrap(); + assert_eq!( + response, + RespondPuzzleState::new( + vec![puzzle_hash], + 0, + sim.header_hash(0).await, + true, + vec![coin_state] + ) + ); + + let spend_bundle = SpendBundle::new( + vec![CoinSpend::new(coin, puzzle_reveal, to_program(())?)], + Signature::default(), + ); + + let ack = peer.send_transaction(spend_bundle).await?; + assert_eq!(ack.status, 1); + + coin_state.spent_height = Some(0); + + let response = peer + .request_puzzle_state( + vec![puzzle_hash], + None, + sim.config().constants.genesis_challenge, + CoinStateFilters::new(true, true, true, 0), + false, + ) + .await? + .unwrap(); + assert_eq!( + response, + RespondPuzzleState::new( + vec![puzzle_hash], + 1, + sim.header_hash(1).await, + true, + vec![coin_state] + ) + ); + + Ok(()) + } +} diff --git a/crates/chia-sdk-test/src/peer_simulator/error.rs b/crates/chia-sdk-test/src/peer_simulator/error.rs new file mode 100644 index 00000000..1d5aa0c1 --- /dev/null +++ b/crates/chia-sdk-test/src/peer_simulator/error.rs @@ -0,0 +1,40 @@ +use std::io; + +use chia_protocol::ProtocolMessageTypes; +use chia_sdk_client::ClientError; +use chia_sdk_signer::SignerError; +use futures_channel::mpsc::SendError; +use thiserror::Error; +use tokio_tungstenite::tungstenite; + +use crate::SimulatorError; + +#[derive(Debug, Error)] +pub enum PeerSimulatorError { + #[error("io error: {0}")] + Io(#[from] io::Error), + + #[error("websocket error: {0}")] + WebSocket(#[from] tungstenite::Error), + + #[error("client error: {0}")] + Client(#[from] ClientError), + + #[error("message parser error: {0}")] + Streamable(#[from] chia_traits::Error), + + #[error("consensus error: {0}")] + Consensus(#[from] chia_consensus::error::Error), + + #[error("signer error: {0}")] + Signer(#[from] SignerError), + + #[error("simulator error: {0}")] + Simulator(#[from] SimulatorError), + + #[error("send message error: {0}")] + SendMessage(#[from] SendError), + + #[error("unsupported protocol message type: {0:?}")] + UnsupportedMessage(ProtocolMessageTypes), +} diff --git a/crates/chia-sdk-test/src/peer_simulator/peer_map.rs b/crates/chia-sdk-test/src/peer_simulator/peer_map.rs new file mode 100644 index 00000000..70d5c830 --- /dev/null +++ b/crates/chia-sdk-test/src/peer_simulator/peer_map.rs @@ -0,0 +1,30 @@ +use std::{collections::HashMap, net::SocketAddr, sync::Arc}; + +use futures_channel::mpsc::UnboundedSender; +use tokio::sync::Mutex; +use tokio_tungstenite::tungstenite::Message; + +pub(crate) type Ws = UnboundedSender; +type Peers = HashMap; + +#[derive(Default, Clone)] +pub(crate) struct PeerMap(Arc>); + +impl PeerMap { + pub(crate) async fn insert(&self, addr: SocketAddr, ws: Ws) { + self.0.lock().await.insert(addr, ws); + } + + pub(crate) async fn remove(&self, addr: SocketAddr) { + self.0.lock().await.remove(&addr); + } + + pub(crate) async fn peers(&self) -> Vec<(SocketAddr, Ws)> { + self.0 + .lock() + .await + .iter() + .map(|(addr, ws)| (*addr, ws.clone())) + .collect() + } +} diff --git a/crates/chia-sdk-test/src/peer_simulator/simulator_config.rs b/crates/chia-sdk-test/src/peer_simulator/simulator_config.rs new file mode 100644 index 00000000..61496cca --- /dev/null +++ b/crates/chia-sdk-test/src/peer_simulator/simulator_config.rs @@ -0,0 +1,21 @@ +use chia_consensus::consensus_constants::ConsensusConstants; +use chia_sdk_types::MAINNET_CONSTANTS; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SimulatorConfig { + pub constants: ConsensusConstants, + pub max_subscriptions: usize, + pub max_response_coins: usize, + pub puzzle_state_batch_size: usize, +} + +impl Default for SimulatorConfig { + fn default() -> Self { + Self { + constants: MAINNET_CONSTANTS.clone(), + max_subscriptions: 200_000, + max_response_coins: 100_000, + puzzle_state_batch_size: 30_000, + } + } +} diff --git a/crates/chia-sdk-test/src/peer_simulator/subscriptions.rs b/crates/chia-sdk-test/src/peer_simulator/subscriptions.rs new file mode 100644 index 00000000..e00c02f3 --- /dev/null +++ b/crates/chia-sdk-test/src/peer_simulator/subscriptions.rs @@ -0,0 +1,54 @@ +use std::net::SocketAddr; + +use chia_protocol::Bytes32; +use indexmap::{IndexMap, IndexSet}; + +#[derive(Debug, Default, Clone)] +pub(crate) struct Subscriptions { + puzzle_subscriptions: IndexMap>, + coin_subscriptions: IndexMap>, +} + +impl Subscriptions { + pub(crate) fn add_coin_subscriptions(&mut self, peer: SocketAddr, coin_ids: IndexSet) { + self.coin_subscriptions + .entry(peer) + .or_default() + .extend(coin_ids); + } + + pub(crate) fn add_puzzle_subscriptions( + &mut self, + peer: SocketAddr, + puzzle_hashes: IndexSet, + ) { + self.puzzle_subscriptions + .entry(peer) + .or_default() + .extend(puzzle_hashes); + } + + pub(crate) fn subscription_count(&self, peer: SocketAddr) -> usize { + self.coin_subscriptions.get(&peer).map_or(0, IndexSet::len) + + self + .puzzle_subscriptions + .get(&peer) + .map_or(0, IndexSet::len) + } + + pub(crate) fn peers(&self) -> IndexSet { + self.coin_subscriptions + .keys() + .chain(self.puzzle_subscriptions.keys()) + .copied() + .collect() + } + + pub(crate) fn coin_subscriptions(&self, peer: SocketAddr) -> Option<&IndexSet> { + self.coin_subscriptions.get(&peer) + } + + pub(crate) fn puzzle_subscriptions(&self, peer: SocketAddr) -> Option<&IndexSet> { + self.puzzle_subscriptions.get(&peer) + } +} diff --git a/crates/chia-sdk-test/src/peer_simulator/ws_connection.rs b/crates/chia-sdk-test/src/peer_simulator/ws_connection.rs new file mode 100644 index 00000000..16221e63 --- /dev/null +++ b/crates/chia-sdk-test/src/peer_simulator/ws_connection.rs @@ -0,0 +1,536 @@ +use std::{net::SocketAddr, sync::Arc}; + +use chia_consensus::{ + consensus_constants::ConsensusConstants, + gen::validation_error::{ErrorCode, ValidationErr}, +}; +use chia_protocol::{ + Bytes, Bytes32, CoinState, CoinStateUpdate, Message, NewPeakWallet, ProtocolMessageTypes, + PuzzleSolutionResponse, RegisterForCoinUpdates, RegisterForPhUpdates, RejectCoinState, + RejectPuzzleSolution, RejectPuzzleState, RejectStateReason, RequestChildren, RequestCoinState, + RequestPuzzleSolution, RequestPuzzleState, RespondChildren, RespondCoinState, + RespondPuzzleSolution, RespondPuzzleState, RespondToCoinUpdates, RespondToPhUpdates, + SendTransaction, SpendBundle, TransactionAck, +}; +use chia_traits::Streamable; +use clvmr::NodePtr; +use futures_channel::mpsc; +use futures_util::{SinkExt, StreamExt}; +use indexmap::{IndexMap, IndexSet}; +use itertools::Itertools; +use tokio::{ + net::TcpStream, + sync::{Mutex, MutexGuard}, +}; +use tokio_tungstenite::{tungstenite::Message as WsMessage, WebSocketStream}; + +use crate::{Simulator, SimulatorError}; + +use super::{ + error::PeerSimulatorError, peer_map::Ws, simulator_config::SimulatorConfig, + subscriptions::Subscriptions, PeerMap, +}; + +pub(crate) async fn ws_connection( + peer_map: PeerMap, + ws: WebSocketStream, + addr: SocketAddr, + config: Arc, + simulator: Arc>, + subscriptions: Arc>, +) { + let (tx, mut rx) = mpsc::unbounded(); + peer_map.insert(addr, tx.clone()).await; + + let (mut sink, mut stream) = ws.split(); + + tokio::spawn(async move { + while let Some(message) = rx.next().await { + if let Err(error) = sink.send(message).await { + log::error!("error sending message to peer: {}", error); + continue; + } + } + }); + + while let Some(message) = stream.next().await { + let message = match message { + Ok(message) => message, + Err(error) => { + log::info!("received error from stream: {:?}", error); + break; + } + }; + + if let Err(error) = handle_message( + peer_map.clone(), + &config, + &simulator, + &subscriptions, + message, + addr, + tx.clone(), + ) + .await + { + log::error!("error handling message: {}", error); + break; + } + } + + peer_map.remove(addr).await; +} + +async fn handle_message( + peer_map: PeerMap, + config: &SimulatorConfig, + simulator: &Mutex, + subscriptions: &Mutex, + message: WsMessage, + addr: SocketAddr, + mut ws: Ws, +) -> Result<(), PeerSimulatorError> { + let request = Message::from_bytes(&message.into_data())?; + let simulator = simulator.lock().await; + + let (response_type, response_data) = match request.msg_type { + ProtocolMessageTypes::SendTransaction => { + let request = SendTransaction::from_bytes(&request.data)?; + let subscriptions = subscriptions.lock().await; + let response = + send_transaction(peer_map, request, config, simulator, subscriptions).await?; + (ProtocolMessageTypes::TransactionAck, response) + } + ProtocolMessageTypes::RegisterForCoinUpdates => { + let request = RegisterForCoinUpdates::from_bytes(&request.data)?; + let subscriptions = subscriptions.lock().await; + let response = register_for_coin_updates(addr, request, &simulator, subscriptions)?; + (ProtocolMessageTypes::RespondToCoinUpdates, response) + } + ProtocolMessageTypes::RegisterForPhUpdates => { + let request = RegisterForPhUpdates::from_bytes(&request.data)?; + let subscriptions = subscriptions.lock().await; + let response = register_for_ph_updates(addr, request, &simulator, subscriptions)?; + (ProtocolMessageTypes::RespondToPhUpdates, response) + } + ProtocolMessageTypes::RequestPuzzleSolution => { + let request = RequestPuzzleSolution::from_bytes(&request.data)?; + let response = request_puzzle_solution(&request, &simulator)?; + (ProtocolMessageTypes::RespondPuzzleSolution, response) + } + ProtocolMessageTypes::RequestChildren => { + let request = RequestChildren::from_bytes(&request.data)?; + let response = request_children(&request, &simulator)?; + (ProtocolMessageTypes::RespondChildren, response) + } + ProtocolMessageTypes::RequestCoinState => { + let request = RequestCoinState::from_bytes(&request.data)?; + let subscriptions = subscriptions.lock().await; + let response = request_coin_state(addr, request, config, &simulator, subscriptions)?; + (ProtocolMessageTypes::RespondCoinState, response) + } + ProtocolMessageTypes::RequestPuzzleState => { + let request = RequestPuzzleState::from_bytes(&request.data)?; + let subscriptions = subscriptions.lock().await; + let response = request_puzzle_state(addr, request, config, &simulator, subscriptions)?; + (ProtocolMessageTypes::RespondPuzzleState, response) + } + message_type => { + return Err(PeerSimulatorError::UnsupportedMessage(message_type)); + } + }; + + let message = Message { + msg_type: response_type, + data: response_data, + id: request.id, + } + .to_bytes()?; + + ws.send(message.into()).await?; + + Ok(()) +} + +fn new_transaction( + simulator: &mut MutexGuard<'_, Simulator>, + subscriptions: &mut MutexGuard<'_, Subscriptions>, + spend_bundle: SpendBundle, + constants: &ConsensusConstants, +) -> Result>, PeerSimulatorError> { + let updates = simulator.new_transaction(spend_bundle, constants)?; + let peers = subscriptions.peers(); + + let mut peer_updates = IndexMap::new(); + + // Send updates to peers. + for peer in peers { + let mut coin_states = IndexSet::new(); + + let coin_subscriptions = subscriptions + .coin_subscriptions(peer) + .cloned() + .unwrap_or_default(); + + let puzzle_subscriptions = subscriptions + .puzzle_subscriptions(peer) + .cloned() + .unwrap_or_default(); + + for &coin_id in updates.keys() { + let Some(coin_state) = simulator.coin_state(coin_id) else { + continue; + }; + + if coin_subscriptions.contains(&coin_id) + || puzzle_subscriptions.contains(&coin_state.coin.puzzle_hash) + { + coin_states.insert(coin_state); + } + } + + for &hint in &puzzle_subscriptions { + let coin_ids = simulator.hinted_coins(hint); + + for coin_id in coin_ids { + if updates.contains_key(&coin_id) { + coin_states.extend(simulator.coin_state(coin_id)); + } + } + } + + if coin_states.is_empty() { + continue; + }; + + peer_updates.insert(peer, coin_states); + } + + Ok(peer_updates) +} + +async fn send_transaction( + peer_map: PeerMap, + request: SendTransaction, + config: &SimulatorConfig, + mut simulator: MutexGuard<'_, Simulator>, + mut subscriptions: MutexGuard<'_, Subscriptions>, +) -> Result { + let transaction_id = request.transaction.name(); + + let updates = match new_transaction( + &mut simulator, + &mut subscriptions, + request.transaction, + &config.constants, + ) { + Ok(updates) => updates, + Err(error) => { + log::error!("error processing transaction: {:?}", &error); + + let error_code = match error { + PeerSimulatorError::Simulator(SimulatorError::Validation(error_code)) => error_code, + _ => ErrorCode::Unknown, + }; + + return Ok(TransactionAck::new( + transaction_id, + 3, + Some(format!("{:?}", ValidationErr(NodePtr::NIL, error_code))), + ) + .to_bytes()? + .into()); + } + }; + + let header_hash = simulator.header_hash(); + + let new_peak = Message { + msg_type: ProtocolMessageTypes::NewPeakWallet, + id: None, + data: NewPeakWallet::new(header_hash, simulator.height(), 0, simulator.height()) + .to_bytes() + .unwrap() + .into(), + } + .to_bytes()?; + + // Send updates to peers. + for (addr, mut peer) in peer_map.peers().await { + peer.send(new_peak.clone().into()).await.unwrap(); + + let Some(peer_updates) = updates.get(&addr).cloned() else { + continue; + }; + + let update = Message { + msg_type: ProtocolMessageTypes::CoinStateUpdate, + id: None, + data: CoinStateUpdate::new( + simulator.height(), + simulator.height(), + header_hash, + peer_updates.into_iter().collect(), + ) + .to_bytes() + .unwrap() + .into(), + } + .to_bytes()?; + + peer.send(update.into()).await?; + } + + Ok(TransactionAck::new(transaction_id, 1, None) + .to_bytes()? + .into()) +} + +fn register_for_coin_updates( + peer: SocketAddr, + request: RegisterForCoinUpdates, + simulator: &MutexGuard<'_, Simulator>, + mut subscriptions: MutexGuard<'_, Subscriptions>, +) -> Result { + let coin_ids: IndexSet = request.coin_ids.iter().copied().collect(); + + let coin_states: Vec = simulator + .lookup_coin_ids(&coin_ids) + .into_iter() + .filter(|cs| { + let created_height = cs.created_height.unwrap_or(0); + let spent_height = cs.spent_height.unwrap_or(0); + let height = u32::max(created_height, spent_height); + height >= request.min_height + }) + .collect(); + + subscriptions.add_coin_subscriptions(peer, coin_ids); + + Ok(RespondToCoinUpdates { + coin_ids: request.coin_ids, + min_height: request.min_height, + coin_states, + } + .to_bytes()? + .into()) +} + +fn register_for_ph_updates( + peer: SocketAddr, + request: RegisterForPhUpdates, + simulator: &MutexGuard<'_, Simulator>, + mut subscriptions: MutexGuard<'_, Subscriptions>, +) -> Result { + let puzzle_hashes: IndexSet = request.puzzle_hashes.iter().copied().collect(); + + let coin_states: Vec = simulator + .lookup_puzzle_hashes(puzzle_hashes.clone(), true) + .into_iter() + .filter(|cs| { + let created_height = cs.created_height.unwrap_or(0); + let spent_height = cs.spent_height.unwrap_or(0); + let height = u32::max(created_height, spent_height); + height >= request.min_height + }) + .collect(); + + subscriptions.add_puzzle_subscriptions(peer, puzzle_hashes); + + Ok(RespondToPhUpdates { + puzzle_hashes: request.puzzle_hashes, + min_height: request.min_height, + coin_states, + } + .to_bytes()? + .into()) +} + +fn request_puzzle_solution( + request: &RequestPuzzleSolution, + simulator: &MutexGuard<'_, Simulator>, +) -> Result { + let reject = RejectPuzzleSolution { + coin_name: request.coin_name, + height: request.height, + } + .to_bytes()? + .into(); + + let Some(coin_state) = simulator.coin_state(request.coin_name) else { + return Ok(reject); + }; + + if coin_state.spent_height != Some(request.height) { + return Ok(reject); + } + + let Some(puzzle_reveal) = simulator.puzzle_reveal(request.coin_name) else { + return Ok(reject); + }; + + let Some(solution) = simulator.solution(request.coin_name) else { + return Ok(reject); + }; + + Ok(RespondPuzzleSolution::new(PuzzleSolutionResponse::new( + request.coin_name, + request.height, + puzzle_reveal, + solution, + )) + .to_bytes()? + .into()) +} + +fn request_children( + request: &RequestChildren, + simulator: &MutexGuard<'_, Simulator>, +) -> Result { + Ok(RespondChildren::new(simulator.children(request.coin_name)) + .to_bytes()? + .into()) +} + +fn request_coin_state( + peer: SocketAddr, + request: RequestCoinState, + config: &SimulatorConfig, + simulator: &MutexGuard<'_, Simulator>, + mut subscriptions: MutexGuard<'_, Subscriptions>, +) -> Result { + if let Some(previous_height) = request.previous_height { + if Some(request.header_hash) != simulator.header_hash_of(previous_height) { + return Ok(RejectCoinState::new(RejectStateReason::Reorg) + .to_bytes()? + .into()); + } + } else if request.header_hash != config.constants.genesis_challenge { + return Ok(RejectCoinState::new(RejectStateReason::Reorg) + .to_bytes()? + .into()); + } + + let coin_ids: IndexSet = request.coin_ids.iter().copied().collect(); + let min_height = request.previous_height.map_or(0, |height| height + 1); + let subscription_count = subscriptions.subscription_count(peer); + + if subscription_count + coin_ids.len() > config.max_subscriptions && request.subscribe { + return Ok( + RejectCoinState::new(RejectStateReason::ExceededSubscriptionLimit) + .to_bytes()? + .into(), + ); + } + + let coin_states: Vec = simulator + .lookup_coin_ids(&coin_ids) + .into_iter() + .filter(|cs| { + let created_height = cs.created_height.unwrap_or(0); + let spent_height = cs.spent_height.unwrap_or(0); + let height = u32::max(created_height, spent_height); + height >= min_height + }) + .collect(); + + if request.subscribe { + subscriptions.add_coin_subscriptions(peer, coin_ids); + } + + Ok(RespondCoinState { + coin_ids: request.coin_ids, + coin_states, + } + .to_bytes()? + .into()) +} + +fn request_puzzle_state( + peer: SocketAddr, + request: RequestPuzzleState, + config: &SimulatorConfig, + simulator: &MutexGuard<'_, Simulator>, + mut subscriptions: MutexGuard<'_, Subscriptions>, +) -> Result { + if let Some(previous_height) = request.previous_height { + if Some(request.header_hash) != simulator.header_hash_of(previous_height) { + return Ok(RejectCoinState::new(RejectStateReason::Reorg) + .to_bytes()? + .into()); + } + } else if request.header_hash != config.constants.genesis_challenge { + return Ok(RejectCoinState::new(RejectStateReason::Reorg) + .to_bytes()? + .into()); + } + + let puzzle_hashes: IndexSet = request.puzzle_hashes.iter().copied().collect(); + let min_height = request.previous_height.map_or(0, |height| height + 1); + let subscription_count = subscriptions.subscription_count(peer); + + if subscription_count + puzzle_hashes.len() > config.max_subscriptions + && request.subscribe_when_finished + { + return Ok( + RejectPuzzleState::new(RejectStateReason::ExceededSubscriptionLimit) + .to_bytes()? + .into(), + ); + } + + let puzzle_hashes: IndexSet = request.puzzle_hashes.iter().copied().collect(); + + let mut coin_states: Vec = simulator + .lookup_puzzle_hashes(puzzle_hashes.clone(), request.filters.include_hinted) + .into_iter() + .filter(|cs| { + if cs.spent_height.is_none() && !request.filters.include_unspent { + return false; + } + + if cs.spent_height.is_some() && !request.filters.include_spent { + return false; + } + + let created_height = cs.created_height.unwrap_or(0); + let spent_height = cs.spent_height.unwrap_or(0); + let height = u32::max(created_height, spent_height); + height >= min_height + }) + .sorted_by_key(|cs| u32::max(cs.created_height.unwrap_or(0), cs.spent_height.unwrap_or(0))) + .take(config.max_response_coins + 1) + .collect(); + + let next_height = if coin_states.len() > config.puzzle_state_batch_size { + coin_states + .last() + .map(|cs| u32::max(cs.created_height.unwrap_or(0), cs.spent_height.unwrap_or(0))) + } else { + None + }; + + if let Some(next_height) = next_height { + while coin_states.last().map_or(false, |cs| { + u32::max(cs.created_height.unwrap_or(0), cs.spent_height.unwrap_or(0)) == next_height + }) { + coin_states.pop(); + } + } + + if request.subscribe_when_finished && next_height.is_none() { + subscriptions.add_puzzle_subscriptions(peer, puzzle_hashes); + } + + let height = next_height.unwrap_or(simulator.height()); + + Ok(RespondPuzzleState { + height, + header_hash: simulator.header_hash_of(height).unwrap(), + puzzle_hashes: request.puzzle_hashes, + coin_states, + is_finished: next_height.is_none(), + } + .to_bytes()? + .into()) +} diff --git a/crates/chia-sdk-test/src/simulator.rs b/crates/chia-sdk-test/src/simulator.rs new file mode 100644 index 00000000..5ff0f152 --- /dev/null +++ b/crates/chia-sdk-test/src/simulator.rs @@ -0,0 +1,278 @@ +use std::collections::HashSet; + +use chia_bls::{PublicKey, SecretKey}; +use chia_consensus::{ + consensus_constants::ConsensusConstants, gen::validation_error::ErrorCode, + spendbundle_validation::validate_clvm_and_signature, +}; +use chia_protocol::{Bytes32, Coin, CoinSpend, CoinState, Program, SpendBundle}; +use chia_puzzles::standard::StandardArgs; +use chia_sdk_types::TESTNET11_CONSTANTS; +use fastrand::Rng; +use indexmap::{IndexMap, IndexSet}; + +use crate::{sign_transaction, test_secret_key, SimulatorError}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Simulator { + rng: Rng, + height: u32, + header_hashes: Vec, + coin_states: IndexMap, + hinted_coins: IndexMap>, + puzzle_and_solutions: IndexMap, +} + +impl Default for Simulator { + fn default() -> Self { + Self::new() + } +} + +impl Simulator { + pub fn new() -> Self { + Self::with_seed(1337) + } + + pub fn with_seed(seed: u64) -> Self { + let mut rng = Rng::with_seed(seed); + let mut header_hash = [0; 32]; + rng.fill(&mut header_hash); + + Self { + rng, + height: 0, + header_hashes: vec![header_hash.into()], + coin_states: IndexMap::new(), + hinted_coins: IndexMap::new(), + puzzle_and_solutions: IndexMap::new(), + } + } + + pub fn height(&self) -> u32 { + self.height + } + + pub fn header_hash(&self) -> Bytes32 { + self.header_hashes.last().copied().unwrap() + } + + pub fn header_hash_of(&self, height: u32) -> Option { + self.header_hashes.get(height as usize).copied() + } + + pub fn insert_coin(&mut self, coin: Coin) { + let coin_state = CoinState::new(coin, None, Some(self.height)); + self.coin_states.insert(coin.coin_id(), coin_state); + } + + pub fn new_coin(&mut self, puzzle_hash: Bytes32, amount: u64) -> Coin { + let mut parent_coin_info = [0; 32]; + self.rng.fill(&mut parent_coin_info); + let coin = Coin::new(parent_coin_info.into(), puzzle_hash, amount); + self.insert_coin(coin); + coin + } + + pub fn new_p2( + &mut self, + amount: u64, + ) -> Result<(SecretKey, PublicKey, Bytes32, Coin), bip39::Error> { + let sk = test_secret_key()?; + let pk = sk.public_key(); + let p2 = StandardArgs::curry_tree_hash(pk).into(); + let coin = self.new_coin(p2, amount); + Ok((sk, pk, p2, coin)) + } + + pub(crate) fn hint_coin(&mut self, coin_id: Bytes32, hint: Bytes32) { + self.hinted_coins.entry(hint).or_default().insert(coin_id); + } + + pub fn coin_state(&self, coin_id: Bytes32) -> Option { + self.coin_states.get(&coin_id).copied() + } + + pub fn children(&self, coin_id: Bytes32) -> Vec { + self.coin_states + .values() + .filter(move |cs| cs.coin.parent_coin_info == coin_id) + .copied() + .collect() + } + + pub fn hinted_coins(&self, hint: Bytes32) -> Vec { + self.hinted_coins + .get(&hint) + .into_iter() + .flatten() + .copied() + .collect() + } + + pub fn puzzle_reveal(&self, coin_id: Bytes32) -> Option { + self.puzzle_and_solutions + .get(&coin_id) + .map(|(p, _)| p.clone()) + } + + pub fn solution(&self, coin_id: Bytes32) -> Option { + self.puzzle_and_solutions + .get(&coin_id) + .map(|(_, s)| s.clone()) + } + + pub fn spend_coins( + &mut self, + coin_spends: Vec, + secret_keys: &[SecretKey], + ) -> Result, SimulatorError> { + let signature = + sign_transaction(&coin_spends, secret_keys, &(&*TESTNET11_CONSTANTS).into())?; + self.new_transaction( + SpendBundle::new(coin_spends, signature), + &TESTNET11_CONSTANTS, + ) + } + + /// Processes a spend bunndle and returns the updated coin states. + pub fn new_transaction( + &mut self, + spend_bundle: SpendBundle, + constants: &ConsensusConstants, + ) -> Result, SimulatorError> { + if spend_bundle.coin_spends.is_empty() { + return Err(SimulatorError::Validation(ErrorCode::InvalidSpendBundle)); + } + + // TODO: Fix cost + let (conds, _pairings, _duration) = + validate_clvm_and_signature(&spend_bundle, 7_700_000_000, constants, self.height) + .map_err(SimulatorError::Validation)?; + + let puzzle_hashes: HashSet = + conds.spends.iter().map(|spend| spend.puzzle_hash).collect(); + + let bundle_puzzle_hashes: HashSet = spend_bundle + .coin_spends + .iter() + .map(|cs| cs.coin.puzzle_hash) + .collect(); + + if puzzle_hashes != bundle_puzzle_hashes { + return Err(SimulatorError::Validation(ErrorCode::InvalidSpendBundle)); + } + + let mut removed_coins = IndexMap::new(); + let mut added_coins = IndexMap::new(); + let mut added_hints = IndexMap::new(); + let mut puzzle_solutions = IndexMap::new(); + + for coin_spend in spend_bundle.coin_spends { + puzzle_solutions.insert( + coin_spend.coin.coin_id(), + (coin_spend.puzzle_reveal, coin_spend.solution), + ); + } + + // Calculate additions and removals. + for spend in &conds.spends { + for new_coin in &spend.create_coin { + let coin = Coin::new(spend.coin_id, new_coin.0, new_coin.1); + + added_coins.insert( + coin.coin_id(), + CoinState::new(coin, None, Some(self.height)), + ); + + let Some(hint) = new_coin.2.clone() else { + continue; + }; + + if hint.len() != 32 { + continue; + } + + added_hints + .entry(Bytes32::try_from(hint).unwrap()) + .or_insert_with(IndexSet::new) + .insert(coin.coin_id()); + } + + let coin = Coin::new(spend.parent_id, spend.puzzle_hash, spend.coin_amount); + + let coin_state = self + .coin_states + .get(&spend.coin_id) + .copied() + .unwrap_or(CoinState::new(coin, None, Some(self.height))); + + removed_coins.insert(spend.coin_id, coin_state); + } + + // Validate removals. + for (coin_id, coin_state) in &mut removed_coins { + let height = self.height; + + if !self.coin_states.contains_key(coin_id) && !added_coins.contains_key(coin_id) { + return Err(SimulatorError::Validation(ErrorCode::UnknownUnspent)); + } + + if coin_state.spent_height.is_some() { + return Err(SimulatorError::Validation(ErrorCode::DoubleSpend)); + } + + coin_state.spent_height = Some(height); + } + + // Update the coin data. + let mut updates = added_coins.clone(); + updates.extend(removed_coins); + self.create_block(); + self.coin_states.extend(updates.clone()); + self.hinted_coins.extend(added_hints.clone()); + self.puzzle_and_solutions.extend(puzzle_solutions); + + Ok(updates) + } + + pub fn lookup_coin_ids(&self, coin_ids: &IndexSet) -> Vec { + coin_ids + .iter() + .filter_map(|coin_id| self.coin_states.get(coin_id).copied()) + .collect() + } + + pub fn lookup_puzzle_hashes( + &self, + puzzle_hashes: IndexSet, + include_hints: bool, + ) -> Vec { + let mut coin_states = IndexMap::new(); + + for (coin_id, coin_state) in &self.coin_states { + if puzzle_hashes.contains(&coin_state.coin.puzzle_hash) { + coin_states.insert(*coin_id, self.coin_states[coin_id]); + } + } + + if include_hints { + for puzzle_hash in puzzle_hashes { + if let Some(hinted_coins) = self.hinted_coins.get(&puzzle_hash) { + for coin_id in hinted_coins { + coin_states.insert(*coin_id, self.coin_states[coin_id]); + } + } + } + } + + coin_states.into_values().collect() + } + + fn create_block(&mut self) { + let mut header_hash = [0; 32]; + self.rng.fill(&mut header_hash); + self.header_hashes.push(header_hash.into()); + self.height += 1; + } +} diff --git a/crates/chia-sdk-test/src/transaction.rs b/crates/chia-sdk-test/src/transaction.rs new file mode 100644 index 00000000..a1a76651 --- /dev/null +++ b/crates/chia-sdk-test/src/transaction.rs @@ -0,0 +1,66 @@ +use std::collections::HashMap; + +use chia_bls::{sign, PublicKey, SecretKey, Signature}; +use chia_protocol::{CoinSpend, SpendBundle, TransactionAck}; +use chia_sdk_client::Peer; +use chia_sdk_signer::{AggSigConstants, RequiredSignature}; +use clvmr::Allocator; + +use crate::SimulatorError; + +pub fn sign_transaction( + coin_spends: &[CoinSpend], + secret_keys: &[SecretKey], + constants: &AggSigConstants, +) -> Result { + let mut allocator = Allocator::new(); + + let required_signatures = + RequiredSignature::from_coin_spends(&mut allocator, coin_spends, constants)?; + + let key_pairs = secret_keys + .iter() + .map(|sk| (sk.public_key(), sk)) + .collect::>(); + + let mut aggregated_signature = Signature::default(); + + for required in required_signatures { + let pk = required.public_key(); + let sk = key_pairs.get(&pk).ok_or(SimulatorError::MissingKey)?; + aggregated_signature += &sign(sk, required.final_message()); + } + + Ok(aggregated_signature) +} + +pub async fn test_transaction_raw( + peer: &Peer, + coin_spends: Vec, + secret_keys: &[SecretKey], + constants: &AggSigConstants, +) -> anyhow::Result { + let aggregated_signature = sign_transaction(&coin_spends, secret_keys, constants)?; + + Ok(peer + .send_transaction(SpendBundle::new(coin_spends, aggregated_signature)) + .await?) +} + +/// Signs and tests a transaction with the given coin spends and secret keys. +/// +/// # Panics +/// Will panic if the transaction could not be submitted or was not successful. +pub async fn test_transaction( + peer: &Peer, + coin_spends: Vec, + secret_keys: &[SecretKey], + constants: &AggSigConstants, +) { + let ack = test_transaction_raw(peer, coin_spends, secret_keys, constants) + .await + .expect("could not submit transaction"); + + assert_eq!(ack.error, None); + assert_eq!(ack.status, 1); +} diff --git a/crates/chia-sdk-types/Cargo.toml b/crates/chia-sdk-types/Cargo.toml new file mode 100644 index 00000000..2bb349ce --- /dev/null +++ b/crates/chia-sdk-types/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "chia-sdk-types" +version = "0.16.0" +edition = "2021" +license = "Apache-2.0" +description = "Standard Chia types for things such as puzzle info and conditions." +authors = ["Brandon Haggstrom "] +homepage = "https://github.com/Rigidity/chia-wallet-sdk" +repository = "https://github.com/Rigidity/chia-wallet-sdk" +readme = { workspace = true } +keywords = { workspace = true } +categories = { workspace = true } + +[lints] +workspace = true + +[dependencies] +chia-sdk-derive = { workspace = true } +chia-bls = { workspace = true } +chia-protocol = { workspace = true } +chia-consensus = { workspace = true } +clvm-traits = { workspace = true } +clvmr = { workspace = true } +hex-literal = { workspace = true } +once_cell = { workspace = true } + +[dev-dependencies] +hex = { workspace = true } +anyhow = { workspace = true } diff --git a/crates/chia-sdk-types/src/condition.rs b/crates/chia-sdk-types/src/condition.rs new file mode 100644 index 00000000..8d43e17c --- /dev/null +++ b/crates/chia-sdk-types/src/condition.rs @@ -0,0 +1,209 @@ +use chia_bls::PublicKey; +use chia_protocol::{Bytes, Bytes32}; +use chia_sdk_derive::conditions; +use clvm_traits::{FromClvm, ToClvm}; + +mod agg_sig; + +pub use agg_sig::*; + +conditions! { + pub enum Condition { + Remark as Copy { + opcode: i8 if 1, + ...rest: T, + }, + AggSigParent { + opcode: i8 if 43, + public_key: PublicKey, + message: Bytes, + }, + AggSigPuzzle { + opcode: i8 if 44, + public_key: PublicKey, + message: Bytes, + }, + AggSigAmount { + opcode: i8 if 45, + public_key: PublicKey, + message: Bytes, + }, + AggSigPuzzleAmount { + opcode: i8 if 46, + public_key: PublicKey, + message: Bytes, + }, + AggSigParentAmount { + opcode: i8 if 47, + public_key: PublicKey, + message: Bytes, + }, + AggSigParentPuzzle { + opcode: i8 if 48, + public_key: PublicKey, + message: Bytes, + }, + AggSigUnsafe { + opcode: i8 if 49, + public_key: PublicKey, + message: Bytes, + }, + AggSigMe { + opcode: i8 if 50, + public_key: PublicKey, + message: Bytes, + }, + CreateCoin { + opcode: i8 if 51, + puzzle_hash: Bytes32, + amount: u64, + memos?: Vec, + }, + ReserveFee as Copy { + opcode: i8 if 52, + amount: u64, + }, + CreateCoinAnnouncement { + opcode: i8 if 60, + message: Bytes, + }, + AssertCoinAnnouncement as Copy { + opcode: i8 if 61, + announcement_id: Bytes32, + }, + CreatePuzzleAnnouncement { + opcode: i8 if 62, + message: Bytes, + }, + AssertPuzzleAnnouncement as Copy { + opcode: i8 if 63, + announcement_id: Bytes32, + }, + AssertConcurrentSpend as Copy { + opcode: i8 if 64, + coin_id: Bytes32, + }, + AssertConcurrentPuzzle as Copy { + opcode: i8 if 65, + puzzle_hash: Bytes32, + }, + SendMessage { + opcode: i8 if 66, + mode: u8, + message: Bytes, + ...data: Vec, + }, + ReceiveMessage { + opcode: i8 if 67, + mode: u8, + message: Bytes, + ...data: Vec, + }, + AssertMyCoinId as Copy { + opcode: i8 if 70, + coin_id: Bytes32, + }, + AssertMyParentId as Copy { + opcode: i8 if 71, + parent_id: Bytes32, + }, + AssertMyPuzzleHash as Copy { + opcode: i8 if 72, + puzzle_hash: Bytes32, + }, + AssertMyAmount as Copy { + opcode: i8 if 73, + amount: u64, + }, + AssertMyBirthSeconds as Copy { + opcode: i8 if 74, + seconds: u64, + }, + AssertMyBirthHeight as Copy { + opcode: i8 if 75, + height: u32, + }, + AssertEphemeral as Default + Copy { + opcode: i8 if 76, + }, + AssertSecondsRelative as Copy { + opcode: i8 if 80, + seconds: u64, + }, + AssertSecondsAbsolute as Copy { + opcode: i8 if 81, + seconds: u64, + }, + AssertHeightRelative as Copy { + opcode: i8 if 82, + height: u32, + }, + AssertHeightAbsolute as Copy { + opcode: i8 if 83, + height: u32, + }, + AssertBeforeSecondsRelative as Copy { + opcode: i8 if 84, + seconds: u64, + }, + AssertBeforeSecondsAbsolute as Copy { + opcode: i8 if 85, + seconds: u64, + }, + AssertBeforeHeightRelative as Copy { + opcode: i8 if 86, + height: u32, + }, + AssertBeforeHeightAbsolute as Copy { + opcode: i8 if 87, + height: u32, + }, + Softfork as Copy { + opcode: i8 if 90, + cost: u64, + ...rest: T, + }, + MeltSingleton as Default + Copy { + opcode: i8 if 51, + puzzle_hash: () if (), + magic_amount: i8 if -113, + }, + TransferNft as Default { + opcode: i8 if -10, + did_id: Option, + trade_prices: Vec<(u16, Bytes32)>, + did_inner_puzzle_hash: Option, + }, + RunCatTail as Copy { + opcode: i8 if 51, + puzzle_hash: () if (), + magic_amount: i8 if -113, + program: P, + solution: S, + }, + UpdateNftMetadata as Copy { + opcode: i8 if -24, + updater_puzzle_reveal: P, + updater_solution: S, + }, + UpdateDataStoreMerkleRoot { + opcode: i8 if -13, + new_merkle_root: Bytes32, + ...memos: Vec, + }, + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct NewMetadataInfo { + pub new_metadata: M, + pub new_updater_puzzle_hash: Bytes32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct NewMetadataOutput { + pub metadata_info: NewMetadataInfo, + pub conditions: C, +} diff --git a/crates/chia-sdk-types/src/condition/agg_sig.rs b/crates/chia-sdk-types/src/condition/agg_sig.rs new file mode 100644 index 00000000..56e6f7a6 --- /dev/null +++ b/crates/chia-sdk-types/src/condition/agg_sig.rs @@ -0,0 +1,105 @@ +use chia_bls::PublicKey; +use chia_protocol::{Bytes, Bytes32}; +use clvm_traits::{FromClvm, ToClvm}; +use clvmr::sha2::Sha256; + +use super::Condition; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm, Hash)] +#[repr(u8)] +#[clvm(atom)] +pub enum AggSigKind { + Parent = 43, + Puzzle = 44, + Amount = 45, + PuzzleAmount = 46, + ParentAmount = 47, + ParentPuzzle = 48, + Unsafe = 49, + Me = 50, +} + +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct AggSig { + pub kind: AggSigKind, + pub public_key: PublicKey, + pub message: Bytes, +} + +impl AggSig { + pub fn new(kind: AggSigKind, public_key: PublicKey, message: Bytes) -> Self { + Self { + kind, + public_key, + message, + } + } +} + +impl Condition { + pub fn into_agg_sig(self) -> Option { + match self { + Condition::AggSigParent(inner) => Some(AggSig::new( + AggSigKind::Parent, + inner.public_key, + inner.message, + )), + Condition::AggSigPuzzle(inner) => Some(AggSig::new( + AggSigKind::Puzzle, + inner.public_key, + inner.message, + )), + Condition::AggSigAmount(inner) => Some(AggSig::new( + AggSigKind::Amount, + inner.public_key, + inner.message, + )), + Condition::AggSigPuzzleAmount(inner) => Some(AggSig::new( + AggSigKind::PuzzleAmount, + inner.public_key, + inner.message, + )), + Condition::AggSigParentAmount(inner) => Some(AggSig::new( + AggSigKind::ParentAmount, + inner.public_key, + inner.message, + )), + Condition::AggSigParentPuzzle(inner) => Some(AggSig::new( + AggSigKind::ParentPuzzle, + inner.public_key, + inner.message, + )), + Condition::AggSigUnsafe(inner) => Some(AggSig::new( + AggSigKind::Unsafe, + inner.public_key, + inner.message, + )), + Condition::AggSigMe(inner) => { + Some(AggSig::new(AggSigKind::Me, inner.public_key, inner.message)) + } + _ => None, + } + } + + pub fn is_agg_sig(&self) -> bool { + matches!( + self, + Condition::AggSigParent(..) + | Condition::AggSigPuzzle(..) + | Condition::AggSigAmount(..) + | Condition::AggSigPuzzleAmount(..) + | Condition::AggSigParentAmount(..) + | Condition::AggSigParentPuzzle(..) + | Condition::AggSigUnsafe(..) + | Condition::AggSigMe(..) + ) + } +} + +pub fn announcement_id(coin_info: Bytes32, message: impl AsRef<[u8]>) -> Bytes32 { + let mut hasher = Sha256::new(); + hasher.update(coin_info.as_ref()); + hasher.update(message.as_ref()); + Bytes32::from(hasher.finalize()) +} diff --git a/crates/chia-sdk-types/src/conditions.rs b/crates/chia-sdk-types/src/conditions.rs new file mode 100644 index 00000000..eda7f8d3 --- /dev/null +++ b/crates/chia-sdk-types/src/conditions.rs @@ -0,0 +1,61 @@ +use clvm_traits::{FromClvm, ToClvm}; +use clvmr::NodePtr; + +use crate::Condition; + +#[must_use] +#[derive(Debug, Clone, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(transparent)] +pub struct Conditions { + conditions: Vec>, +} + +impl Default for Conditions { + fn default() -> Self { + Self { + conditions: Vec::new(), + } + } +} + +impl Conditions { + pub fn new() -> Self { + Self::default() + } +} + +impl Conditions { + pub fn with(mut self, condition: impl Into>) -> Self { + self.conditions.push(condition.into()); + self + } + + pub fn extend(mut self, conditions: impl IntoIterator>>) -> Self { + self.conditions + .extend(conditions.into_iter().map(Into::into)); + self + } + + pub fn extend_from_slice(mut self, conditions: &[Condition]) -> Self + where + T: Clone, + { + self.conditions.extend_from_slice(conditions); + self + } +} + +impl AsRef<[Condition]> for Conditions { + fn as_ref(&self) -> &[Condition] { + &self.conditions + } +} + +impl IntoIterator for Conditions { + type Item = Condition; + type IntoIter = std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.conditions.into_iter() + } +} diff --git a/crates/chia-sdk-types/src/constants.rs b/crates/chia-sdk-types/src/constants.rs new file mode 100644 index 00000000..8264822d --- /dev/null +++ b/crates/chia-sdk-types/src/constants.rs @@ -0,0 +1,98 @@ +use chia_consensus::consensus_constants::ConsensusConstants; +use chia_protocol::Bytes32; +use clvmr::sha2::Sha256; +use hex_literal::hex; +use once_cell::sync::Lazy; + +const MAINNET_GENESIS_CHALLENGE: Bytes32 = Bytes32::new(hex!( + "ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb" +)); + +const TESTNET11_GENESIS_CHALLENGE: Bytes32 = Bytes32::new(hex!( + "37a90eb5185a9c4439a91ddc98bbadce7b4feba060d50116a067de66bf236615" +)); + +pub fn default_constants(genesis_challenge: Bytes32, agg_sig_me: Bytes32) -> ConsensusConstants { + ConsensusConstants { + slot_blocks_target: 32, + min_blocks_per_challenge_block: 16, + max_sub_slot_blocks: 128, + num_sps_sub_slot: 64, + sub_slot_iters_starting: 2u64.pow(27), + difficulty_constant_factor: 2u128.pow(67), + difficulty_starting: 7, + difficulty_change_max_factor: 3, + sub_epoch_blocks: 384, + epoch_blocks: 4608, + significant_bits: 8, + discriminant_size_bits: 1024, + number_zero_bits_plot_filter: 9, + min_plot_size: 32, + max_plot_size: 50, + sub_slot_time_target: 600, + num_sp_intervals_extra: 3, + max_future_time2: 120, + number_of_timestamps: 11, + genesis_challenge, + agg_sig_me_additional_data: agg_sig_me, + agg_sig_parent_additional_data: hash(agg_sig_me, 43), + agg_sig_puzzle_additional_data: hash(agg_sig_me, 44), + agg_sig_amount_additional_data: hash(agg_sig_me, 45), + agg_sig_puzzle_amount_additional_data: hash(agg_sig_me, 46), + agg_sig_parent_amount_additional_data: hash(agg_sig_me, 47), + agg_sig_parent_puzzle_additional_data: hash(agg_sig_me, 48), + genesis_pre_farm_pool_puzzle_hash: Bytes32::new(hex!( + "d23da14695a188ae5708dd152263c4db883eb27edeb936178d4d988b8f3ce5fc" + )), + genesis_pre_farm_farmer_puzzle_hash: Bytes32::new(hex!( + "3d8765d3a597ec1d99663f6c9816d915b9f68613ac94009884c4addaefcce6af" + )), + max_vdf_witness_size: 64, + mempool_block_buffer: 10, + max_coin_amount: u64::MAX, + max_block_cost_clvm: 11_000_000_000, + cost_per_byte: 12_000, + weight_proof_threshold: 2, + blocks_cache_size: 4608 + 128 * 4, + weight_proof_recent_blocks: 1000, + max_block_count_per_requests: 32, + max_generator_size: 1_000_000, + max_generator_ref_list_size: 512, + pool_sub_slot_iters: 37_600_000_000, + soft_fork5_height: 5_940_000, + hard_fork_height: 5_496_000, + plot_filter_128_height: 10_542_000, + plot_filter_64_height: 15_592_000, + plot_filter_32_height: 20_643_000, + } +} + +pub static MAINNET_CONSTANTS: Lazy = + Lazy::new(|| default_constants(MAINNET_GENESIS_CHALLENGE, MAINNET_GENESIS_CHALLENGE)); + +pub static TESTNET11_CONSTANTS: Lazy = Lazy::new(|| ConsensusConstants { + sub_slot_iters_starting: 2u64.pow(26), + difficulty_constant_factor: 10_052_721_566_054, + difficulty_starting: 30, + epoch_blocks: 768, + min_plot_size: 18, + genesis_pre_farm_pool_puzzle_hash: Bytes32::new(hex!( + "3ef7c233fc0785f3c0cae5992c1d35e7c955ca37a423571c1607ba392a9d12f7" + )), + genesis_pre_farm_farmer_puzzle_hash: Bytes32::new(hex!( + "08296fc227decd043aee855741444538e4cc9a31772c4d1a9e6242d1e777e42a" + )), + soft_fork5_height: 1_340_000, + hard_fork_height: 0, + plot_filter_128_height: 6_029_568, + plot_filter_64_height: 11_075_328, + plot_filter_32_height: 16_121_088, + ..default_constants(TESTNET11_GENESIS_CHALLENGE, TESTNET11_GENESIS_CHALLENGE) +}); + +fn hash(agg_sig_data: Bytes32, byte: u8) -> Bytes32 { + let mut hasher = Sha256::new(); + hasher.update(agg_sig_data); + hasher.update([byte]); + hasher.finalize().into() +} diff --git a/crates/chia-sdk-types/src/lib.rs b/crates/chia-sdk-types/src/lib.rs new file mode 100644 index 00000000..4ddd3c0d --- /dev/null +++ b/crates/chia-sdk-types/src/lib.rs @@ -0,0 +1,9 @@ +mod condition; +mod conditions; +mod constants; +mod run_puzzle; + +pub use condition::*; +pub use conditions::*; +pub use constants::*; +pub use run_puzzle::*; diff --git a/crates/chia-sdk-types/src/run_puzzle.rs b/crates/chia-sdk-types/src/run_puzzle.rs new file mode 100644 index 00000000..1c779223 --- /dev/null +++ b/crates/chia-sdk-types/src/run_puzzle.rs @@ -0,0 +1,19 @@ +use clvmr::{ + reduction::{EvalErr, Reduction}, + Allocator, NodePtr, +}; + +pub fn run_puzzle( + allocator: &mut Allocator, + puzzle: NodePtr, + solution: NodePtr, +) -> Result { + let Reduction(_cost, output) = clvmr::run_program( + allocator, + &clvmr::ChiaDialect::new(0), + puzzle, + solution, + 11_000_000_000, + )?; + Ok(output) +} diff --git a/examples/address_conversion.rs b/examples/address_conversion.rs new file mode 100644 index 00000000..15fe1ee1 --- /dev/null +++ b/examples/address_conversion.rs @@ -0,0 +1,23 @@ +use chia_wallet_sdk::*; +use hex_literal::hex; + +fn main() -> anyhow::Result<()> { + let puzzle_hash = hex!("aca490e9f3ebcafa3d5342d347db2703b31029511f5b40c11441af1c961f6585"); + let encoded_puzzle_hash = encode_puzzle_hash(puzzle_hash, true); + + let address = encode_address(puzzle_hash, "xch")?; + + println!("Puzzle hash: {}", encoded_puzzle_hash); + println!("XCH address: {}", address); + + let roundtrip = decode_address(&address)?; + println!( + "Address matches puzzle hash: {}", + roundtrip == (puzzle_hash, "xch".to_string()) + ); + + let roundtrip = decode_puzzle_hash(&encoded_puzzle_hash)?; + println!("Puzzle hash matches: {}", roundtrip == puzzle_hash); + + Ok(()) +} diff --git a/examples/cat_spends.rs b/examples/cat_spends.rs new file mode 100644 index 00000000..91ffbec6 --- /dev/null +++ b/examples/cat_spends.rs @@ -0,0 +1,42 @@ +use chia_protocol::{Bytes32, Coin}; +use chia_puzzles::standard::StandardArgs; +use chia_wallet_sdk::*; + +fn main() -> anyhow::Result<()> { + let ctx = &mut SpendContext::new(); + + let sk = test_secret_key()?; + let pk = sk.public_key(); + let p2 = StandardLayer::new(pk); + + let p2_puzzle_hash = StandardArgs::curry_tree_hash(pk).into(); + let coin = Coin::new(Bytes32::default(), p2_puzzle_hash, 1_000); + + // Issue the CAT using the single issuance (genesis by coin id) TAIL. + let conditions = + Conditions::new().create_coin(p2_puzzle_hash, coin.amount, vec![p2_puzzle_hash.into()]); + let (issue_cat, cat) = Cat::single_issuance_eve(ctx, coin.coin_id(), coin.amount, conditions)?; + p2.spend(ctx, coin, issue_cat)?; + println!("Issued test CAT with asset id {}", cat.asset_id); + + // Spend the CAT coin. + let new_cat = cat.wrapped_child(p2_puzzle_hash, 1000); + let cat_spends = [CatSpend::new( + new_cat, + p2.spend_with_conditions( + ctx, + Conditions::new().create_coin(p2_puzzle_hash, coin.amount, vec![p2_puzzle_hash.into()]), + )?, + )]; + + Cat::spend_all(ctx, &cat_spends)?; + + let new_coin = new_cat.wrapped_child(p2_puzzle_hash, 1000).coin; + + println!( + "Spent the CAT coin to create new coin with id {}", + new_coin.coin_id() + ); + + Ok(()) +} diff --git a/examples/custom_p2_puzzle.rs b/examples/custom_p2_puzzle.rs new file mode 100644 index 00000000..401b10a4 --- /dev/null +++ b/examples/custom_p2_puzzle.rs @@ -0,0 +1,145 @@ +use chia_bls::PublicKey; +use chia_protocol::{Coin, CoinSpend}; +use chia_wallet_sdk::*; +use clvm_traits::{FromClvm, ToClvm}; +use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash}; +use clvmr::NodePtr; +use hex_literal::hex; + +// We need to define the puzzle reveal. +// This can be found in `../puzzles/custom_p2_puzzle.clsp.hex`. +pub const CUSTOM_P2_PUZZLE: [u8; 137] = hex!( + " + ff02ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff + 04ff0bff80808080ff80808080ff0b80ffff04ffff01ff32ff02ffff03ffff07 + ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080 + ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff05 + 8080ff0180ff018080 + " +); + +// The puzzle hash can be calculated with `opc -H "$(opd )"` with `chia-dev-tools`. +pub const CUSTOM_P2_PUZZLE_HASH: TreeHash = TreeHash::new(hex!( + "0ff94726f1a8dea5c3f70d3121945190778d3b2b3fcda3735a1f290977e98341" +)); + +// These are the curried arguments that the puzzle accepts. +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(curry)] +pub struct CustomArgs { + pub public_key: PublicKey, +} + +impl CustomArgs { + pub fn new(public_key: PublicKey) -> Self { + Self { public_key } + } + + pub fn curry_tree_hash(public_key: PublicKey) -> TreeHash { + CurriedProgram { + program: CUSTOM_P2_PUZZLE_HASH, + args: CustomArgs::new(public_key), + } + .tree_hash() + } +} + +// And the solution is just a list of conditions. +#[derive(Debug, Clone, Copy, PartialEq, Eq, ToClvm, FromClvm)] +#[clvm(list)] +pub struct CustomSolution { + pub conditions: T, +} + +// For convenience, we can add a way to allocate our puzzle on the `SpendContext`. +pub trait CustomExt { + fn custom_puzzle(&mut self) -> Result; + fn spend_custom_coin( + &mut self, + coin: Coin, + public_key: PublicKey, + conditions: Conditions, + ) -> Result<(), DriverError>; +} + +impl CustomExt for SpendContext { + fn custom_puzzle(&mut self) -> Result { + self.puzzle(CUSTOM_P2_PUZZLE_HASH, &CUSTOM_P2_PUZZLE) + } + + fn spend_custom_coin( + &mut self, + coin: Coin, + public_key: PublicKey, + conditions: Conditions, + ) -> Result<(), DriverError> { + let spend = conditions.custom_spend(self, public_key)?; + let puzzle_reveal = self.serialize(&spend.puzzle)?; + let solution = self.serialize(&spend.solution)?; + self.insert(CoinSpend::new(coin, puzzle_reveal, solution)); + Ok(()) + } +} + +// Let's extend the `Conditions` struct to generate spends for our new p2 puzzle. +pub trait CustomSpend { + fn custom_spend( + self, + ctx: &mut SpendContext, + public_key: PublicKey, + ) -> Result; +} + +impl CustomSpend for Conditions { + fn custom_spend( + self, + ctx: &mut SpendContext, + public_key: PublicKey, + ) -> Result { + let custom_puzzle = ctx.custom_puzzle()?; + + let puzzle = ctx.alloc(&CurriedProgram { + program: custom_puzzle, + args: CustomArgs::new(public_key), + })?; + + let solution = ctx.alloc(&CustomSolution { conditions: self })?; + + Ok(Spend::new(puzzle, solution)) + } +} + +fn main() -> anyhow::Result<()> { + // Create the simulator server and connect the peer client. + let mut sim = Simulator::new(); + + // Setup the key, puzzle hash, and mint a coin. + let sk = test_secret_key()?; + let pk = sk.public_key(); + let puzzle_hash = CustomArgs::curry_tree_hash(pk).into(); + let coin = sim.new_coin(puzzle_hash, 1_000); + + println!("Minted custom test coin with coin id {}", coin.coin_id()); + + // Create the spend context and a simple transaction. + let ctx = &mut SpendContext::new(); + + let conditions = Conditions::new() + .create_coin(puzzle_hash, 900, Vec::new()) + .reserve_fee(100); + + ctx.spend_custom_coin(coin, pk, conditions)?; + + let new_coin = Coin::new(coin.coin_id(), puzzle_hash, 900); + + println!("Spent coin to create new coin {}", new_coin.coin_id()); + + // Sign and submit the transaction to the simulator. + // This will produce an error if the transaction is not successful. + let coin_spends = ctx.take(); + sim.spend_coins(coin_spends, &[sk])?; + + println!("Transaction was successful."); + + Ok(()) +} diff --git a/examples/puzzles/custom_p2_puzzle.clsp b/examples/puzzles/custom_p2_puzzle.clsp new file mode 100644 index 00000000..bf98bd82 --- /dev/null +++ b/examples/puzzles/custom_p2_puzzle.clsp @@ -0,0 +1,17 @@ +(mod (PUBLIC_KEY conditions) + ;; https://docs.chia.net/conditions + (defconstant AGG_SIG_ME 50) + + ;; This is used to calculate a tree hash of a value (for example a puzzle hash). + (defun sha256tree (value) + (if (l value) + (sha256 2 (sha256tree (f value)) (sha256tree (r value))) + (sha256 1 value) + ) + ) + + (c + (list AGG_SIG_ME PUBLIC_KEY (sha256tree conditions)) + conditions + ) +) diff --git a/examples/puzzles/custom_p2_puzzle.clsp.hex b/examples/puzzles/custom_p2_puzzle.clsp.hex new file mode 100644 index 00000000..df08fcea --- /dev/null +++ b/examples/puzzles/custom_p2_puzzle.clsp.hex @@ -0,0 +1,4 @@ +ff02ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff0bff80808080 +ff80808080ff0b80ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff +06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01 +ff0bffff0101ff058080ff0180ff018080 diff --git a/examples/spend_simulator.rs b/examples/spend_simulator.rs new file mode 100644 index 00000000..9077ab52 --- /dev/null +++ b/examples/spend_simulator.rs @@ -0,0 +1,40 @@ +use chia_protocol::Coin; +use chia_puzzles::standard::StandardArgs; +use chia_wallet_sdk::*; + +fn main() -> anyhow::Result<()> { + // Create the simulator server and connect the peer client. + let mut sim = Simulator::new(); + + // Setup the key, puzzle hash, and mint a coin. + let sk = test_secret_key()?; + let pk = sk.public_key(); + let p2 = StandardLayer::new(pk); + + let puzzle_hash = StandardArgs::curry_tree_hash(pk).into(); + let coin = sim.new_coin(puzzle_hash, 1_000); + + println!("Minted test coin with coin id {}", coin.coin_id()); + + // Create the spend context and a simple transaction. + let ctx = &mut SpendContext::new(); + + let conditions = Conditions::new() + .create_coin(puzzle_hash, 900, Vec::new()) + .reserve_fee(100); + + p2.spend(ctx, coin, conditions)?; + + let new_coin = Coin::new(coin.coin_id(), puzzle_hash, 900); + + println!("Spent coin to create new coin {}", new_coin.coin_id()); + + // Sign and submit the transaction to the simulator. + // This will produce an error if the transaction is not successful. + let coin_spends = ctx.take(); + sim.spend_coins(coin_spends, &[sk])?; + + println!("Transaction was successful."); + + Ok(()) +} diff --git a/napi/.gitignore b/napi/.gitignore new file mode 100644 index 00000000..e7ad6386 --- /dev/null +++ b/napi/.gitignore @@ -0,0 +1,2 @@ +/node_modules +*.node diff --git a/napi/.npmignore b/napi/.npmignore new file mode 100644 index 00000000..ec144db2 --- /dev/null +++ b/napi/.npmignore @@ -0,0 +1,13 @@ +target +Cargo.lock +.cargo +.github +npm +.eslintrc +.prettierignore +rustfmt.toml +yarn.lock +*.node +.yarn +__test__ +renovate.json diff --git a/napi/Cargo.toml b/napi/Cargo.toml new file mode 100644 index 00000000..5b3cbc6d --- /dev/null +++ b/napi/Cargo.toml @@ -0,0 +1,31 @@ +[package] +publish = false +name = "chia-wallet-sdk-napi" +version = "0.0.0" +edition = "2021" +license = "Apache-2.0" +description = "Node.js bindings for the Chia Wallet SDK." +authors = ["Brandon Haggstrom "] +homepage = "https://github.com/Rigidity/chia-wallet-sdk" +repository = "https://github.com/Rigidity/chia-wallet-sdk" +readme = { workspace = true } +keywords = { workspace = true } +categories = { workspace = true } + +[lints] +workspace = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +napi = { workspace = true, features = ["napi6"] } +napi-derive = { workspace = true } +chia-wallet-sdk = { workspace = true } +chia = { workspace = true } +clvmr = { workspace = true } +num-bigint = { workspace = true } +hex = { workspace = true } + +[build-dependencies] +napi-build = "2.0.1" diff --git a/napi/__test__/index.spec.ts b/napi/__test__/index.spec.ts new file mode 100644 index 00000000..261be6dc --- /dev/null +++ b/napi/__test__/index.spec.ts @@ -0,0 +1,283 @@ +import test from "ava"; + +import { + ClvmAllocator, + compareBytes, + curryTreeHash, + fromHex, + Simulator, + toCoinId, + toHex, +} from "../index.js"; + +test("calculate coin id", (t) => { + const coinId = toCoinId({ + parentCoinInfo: fromHex( + "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a" + ), + puzzleHash: fromHex( + "dbc1b4c900ffe48d575b5da5c638040125f65db0fe3e24494b76ea986457d986" + ), + amount: 100n, + }); + + t.true( + compareBytes( + coinId, + fromHex( + "fd3e669c27be9d634fe79f1f7d7d8aaacc3597b855cffea1d708f4642f1d542a" + ) + ) + ); +}); + +test("byte equality", (t) => { + const a = Uint8Array.from([1, 2, 3]); + const b = Uint8Array.from([1, 2, 3]); + + t.true(compareBytes(a, b)); + t.true(Buffer.from(a).equals(b)); +}); + +test("byte inequality", (t) => { + const a = Uint8Array.from([1, 2, 3]); + const b = Uint8Array.from([1, 2, 4]); + + t.true(!compareBytes(a, b)); + t.true(!Buffer.from(a).equals(b)); +}); + +test("atom roundtrip", (t) => { + const clvm = new ClvmAllocator(); + + const expected = Uint8Array.from([1, 2, 3]); + const atom = clvm.alloc(expected); + + t.true(compareBytes(atom.toAtom()!, expected)); +}); + +test("string roundtrip", (t) => { + const clvm = new ClvmAllocator(); + + const expected = "hello world"; + const atom = clvm.alloc(expected); + t.is(atom.toString(), expected); +}); + +test("number roundtrip", (t) => { + const clvm = new ClvmAllocator(); + + for (const expected of [ + Number.MIN_SAFE_INTEGER, + -1000, + 0, + 34, + 1000, + Number.MAX_SAFE_INTEGER, + ]) { + const num = clvm.alloc(expected); + t.is(num.toBigInt(), BigInt(expected)); + } +}); + +test("invalid number", (t) => { + const clvm = new ClvmAllocator(); + + for (const expected of [ + Number.MIN_SAFE_INTEGER - 1, + Number.MAX_SAFE_INTEGER + 1, + Infinity, + -Infinity, + NaN, + ]) { + t.throws(() => clvm.alloc(expected)); + } +}); + +test("bigint roundtrip", (t) => { + const clvm = new ClvmAllocator(); + + for (const expected of [ + 0n, + 1n, + 420n, + 67108863n, + -1n, + -100n, + -421489719874198729487129847n, + 4384723984791283749823764732649187498237483927482n, + ]) { + const num = clvm.alloc(expected); + t.is(num.toBigInt(), expected); + } +}); + +test("pair roundtrip", (t) => { + const clvm = new ClvmAllocator(); + + const ptr = clvm.pair(1, 100n); + const [first, rest] = ptr.toPair()!; + + t.is(first.toSmallNumber(), 1); + t.is(rest.toBigInt(), 100n); +}); + +test("list roundtrip", (t) => { + const clvm = new ClvmAllocator(); + + const items = Array.from({ length: 10 }, (_, i) => i); + const ptr = clvm.alloc(items); + const list = ptr.toList().map((ptr) => ptr.toSmallNumber()); + + t.deepEqual(list, items); +}); + +test("clvm value allocation", (t) => { + const clvm = new ClvmAllocator(); + + const shared = clvm.alloc(42); + + const manual = clvm.alloc([ + clvm.alloc(42), + clvm.alloc("Hello, world!"), + clvm.alloc(true), + clvm.alloc(Uint8Array.from([1, 2, 3])), + clvm.alloc([clvm.alloc(34)]), + clvm.alloc(100n), + shared, + ]); + + const auto = clvm.alloc([ + 42, + "Hello, world!", + true, + Uint8Array.from([1, 2, 3]), + [34], + 100n, + shared, + ]); + + t.true(compareBytes(clvm.treeHash(manual), clvm.treeHash(auto))); +}); + +test("curry add function", (t) => { + const clvm = new ClvmAllocator(); + + const addMod = clvm.deserialize(fromHex("ff10ff02ff0580")); + const addToTen = clvm.curry(addMod, [clvm.alloc(10)]); + const result = clvm.run(addToTen, clvm.alloc([5]), 10000000n, true); + + t.is(result.value.toSmallNumber(), 15); + t.is(result.cost, 1082n); +}); + +test("curry roundtrip", (t) => { + const clvm = new ClvmAllocator(); + + const items = Array.from({ length: 10 }, (_, i) => i); + const ptr = clvm.curry( + clvm.nil(), + items.map((i) => clvm.alloc(i)) + ); + const uncurry = ptr.uncurry()!; + const args = uncurry.args.map((ptr) => ptr.toSmallNumber()); + + t.true( + compareBytes(clvm.treeHash(clvm.nil()), clvm.treeHash(uncurry.program)) + ); + t.deepEqual(args, items); +}); + +test("clvm serialization", (t) => { + const clvm = new ClvmAllocator(); + + for (const [ptr, hex] of [ + [clvm.alloc(Uint8Array.from([1, 2, 3])), "83010203"], + [clvm.alloc(420), "8201a4"], + [clvm.alloc(100n), "64"], + [clvm.pair(Uint8Array.from([1, 2, 3]), 100n), "ff8301020364"], + ] as const) { + const serialized = ptr.serialize(); + const deserialized = clvm.deserialize(serialized); + + t.true(compareBytes(clvm.treeHash(ptr), clvm.treeHash(deserialized))); + t.is(hex as string, toHex(serialized)); + } +}); + +test("curry tree hash", (t) => { + const clvm = new ClvmAllocator(); + + const items = Array.from({ length: 10 }, (_, i) => i); + const ptr = clvm.curry( + clvm.nil(), + items.map((i) => clvm.alloc(i)) + ); + + const treeHash = curryTreeHash( + clvm.treeHash(clvm.nil()), + items.map((i) => clvm.treeHash(clvm.alloc(i))) + ); + const expected = clvm.treeHash(ptr); + + t.true(compareBytes(treeHash, expected)); +}); + +test("mint and spend nft", (t) => { + const clvm = new ClvmAllocator(); + const simulator = new Simulator(); + const p2 = simulator.newP2(1n); + + const result = clvm.mintNfts(toCoinId(p2.coin), [ + { + metadata: { + dataUris: ["https://example.com"], + metadataUris: ["https://example.com"], + licenseUris: ["https://example.com"], + editionNumber: 1n, + editionTotal: 1n, + }, + p2PuzzleHash: p2.puzzleHash, + royaltyPuzzleHash: p2.puzzleHash, + royaltyTenThousandths: 300, + }, + ]); + + const spend = clvm.spendP2Standard( + p2.publicKey, + clvm.delegatedSpendForConditions(result.parentConditions) + ); + + simulator.spend( + result.coinSpends.concat([ + { + coin: p2.coin, + puzzleReveal: spend.puzzle.serialize(), + solution: spend.solution.serialize(), + }, + ]), + [p2.secretKey] + ); + + const innerSpend = clvm.spendP2Standard( + p2.publicKey, + clvm.delegatedSpendForConditions([ + clvm.createCoin(p2.puzzleHash, 1n, [p2.puzzleHash]), + ]) + ); + + const coinSpends = clvm.spendNft(result.nfts[0], innerSpend); + + simulator.spend(coinSpends, [p2.secretKey]); + + t.true( + compareBytes( + clvm + .nftMetadata( + clvm.parseNftMetadata(clvm.deserialize(result.nfts[0].info.metadata)) + ) + .serialize(), + result.nfts[0].info.metadata + ) + ); +}); diff --git a/napi/build.rs b/napi/build.rs new file mode 100644 index 00000000..a2fd5363 --- /dev/null +++ b/napi/build.rs @@ -0,0 +1,6 @@ +#[allow(unused_extern_crates)] +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/napi/index.d.ts b/napi/index.d.ts new file mode 100644 index 00000000..0cb45f09 --- /dev/null +++ b/napi/index.d.ts @@ -0,0 +1,168 @@ +/* tslint:disable */ +/* eslint-disable */ + +/* auto-generated by NAPI-RS */ + +export interface Output { + value: Program + cost: bigint +} +export declare function curryTreeHash(treeHash: Uint8Array, args: Array): Uint8Array +export declare function intToSignedBytes(bigInt: bigint): Uint8Array +export declare function signedBytesToInt(bytes: Uint8Array): bigint +export interface Coin { + parentCoinInfo: Uint8Array + puzzleHash: Uint8Array + amount: bigint +} +export declare function toCoinId(coin: Coin): Uint8Array +export interface CoinSpend { + coin: Coin + puzzleReveal: Uint8Array + solution: Uint8Array +} +export interface Spend { + puzzle: Program + solution: Program +} +export interface LineageProof { + parentParentCoinInfo: Uint8Array + parentInnerPuzzleHash?: Uint8Array + parentAmount: bigint +} +export interface Nft { + coin: Coin + lineageProof: LineageProof + info: NftInfo +} +export interface NftInfo { + launcherId: Uint8Array + metadata: Uint8Array + metadataUpdaterPuzzleHash: Uint8Array + currentOwner?: Uint8Array + royaltyPuzzleHash: Uint8Array + royaltyTenThousandths: number + p2PuzzleHash: Uint8Array +} +export interface NftMetadata { + editionNumber: bigint + editionTotal: bigint + dataUris: Array + dataHash?: Uint8Array + metadataUris: Array + metadataHash?: Uint8Array + licenseUris: Array + licenseHash?: Uint8Array +} +export interface ParsedNft { + info: NftInfo + innerPuzzle: Program +} +export interface NftMint { + metadata: NftMetadata + p2PuzzleHash: Uint8Array + royaltyPuzzleHash: Uint8Array + royaltyTenThousandths: number +} +export interface MintedNfts { + nfts: Array + coinSpends: Array + parentConditions: Array +} +export interface Curry { + program: Program + args: Array +} +export interface P2Coin { + coin: Coin + puzzleHash: Uint8Array + publicKey: Uint8Array + secretKey: Uint8Array +} +export declare function compareBytes(a: Uint8Array, b: Uint8Array): boolean +export declare function sha256(bytes: Uint8Array): Uint8Array +export declare function fromHexRaw(hex: string): Uint8Array +export declare function fromHex(hex: string): Uint8Array +export declare function toHex(bytes: Uint8Array): string +export declare class ClvmAllocator { + constructor() + nil(): Program + deserialize(value: Uint8Array): Program + deserializeWithBackrefs(value: Uint8Array): Program + treeHash(program: Program): Uint8Array + run(puzzle: Program, solution: Program, maxCost: bigint, mempoolMode: boolean): Output + curry(program: Program, args: Array): Program + pair(first: ClvmValue, rest: ClvmValue): Program + alloc(value: ClvmValue): Program + nftMetadata(value: NftMetadata): Program + parseNftMetadata(value: Program): NftMetadata + delegatedSpendForConditions(conditions: Array): Spend + spendP2Standard(syntheticKey: Uint8Array, delegatedSpend: Spend): Spend + spendP2DelegatedSingleton(launcherId: Uint8Array, coinId: Uint8Array, singletonInnerPuzzleHash: Uint8Array, delegatedSpend: Spend): Spend + mintNfts(parent_coin_id: Uint8Array, nft_mints: Array): MintedNfts + parseNftInfo(puzzle: Program): ParsedNft | null + parseChildNft(parentCoin: Coin, parentPuzzle: Program, parentSolution: Program): Nft | null + spendNft(nft: Nft, innerSpend: Spend): Array + remark(value: Program): Program + aggSigParent(publicKey: Uint8Array, message: Uint8Array): Program + aggSigPuzzle(publicKey: Uint8Array, message: Uint8Array): Program + aggSigAmount(publicKey: Uint8Array, message: Uint8Array): Program + aggSigPuzzleAmount(publicKey: Uint8Array, message: Uint8Array): Program + aggSigParentAmount(publicKey: Uint8Array, message: Uint8Array): Program + aggSigParentPuzzle(publicKey: Uint8Array, message: Uint8Array): Program + aggSigUnsafe(publicKey: Uint8Array, message: Uint8Array): Program + aggSigMe(publicKey: Uint8Array, message: Uint8Array): Program + createCoin(puzzleHash: Uint8Array, amount: bigint, memos: Array): Program + reserveFee(fee: bigint): Program + createCoinAnnouncement(message: Uint8Array): Program + createPuzzleAnnouncement(message: Uint8Array): Program + assertCoinAnnouncement(announcementId: Uint8Array): Program + assertPuzzleAnnouncement(announcementId: Uint8Array): Program + assertConcurrentSpend(coinId: Uint8Array): Program + assertConcurrentPuzzle(puzzleHash: Uint8Array): Program + assertSecondsRelative(seconds: bigint): Program + assertSecondsAbsolute(seconds: bigint): Program + assertHeightRelative(height: number): Program + assertHeightAbsolute(height: number): Program + assertBeforeSecondsRelative(seconds: bigint): Program + assertBeforeSecondsAbsolute(seconds: bigint): Program + assertBeforeHeightRelative(height: number): Program + assertBeforeHeightAbsolute(height: number): Program + assertMyCoinId(coinId: Uint8Array): Program + assertMyParentId(parentId: Uint8Array): Program + assertMyPuzzleHash(puzzleHash: Uint8Array): Program + assertMyAmount(amount: bigint): Program + assertMyBirthSeconds(seconds: bigint): Program + assertMyBirthHeight(height: number): Program + assertEphemeral(): Program + sendMessage(mode: number, message: Uint8Array, data: Array): Program + receiveMessage(mode: number, message: Uint8Array, data: Array): Program + softfork(cost: bigint, value: Program): Program +} +export declare class Program { + isAtom(): boolean + isPair(): boolean + treeHash(): Uint8Array + serialize(): Uint8Array + serializeWithBackrefs(): Uint8Array + length(): number | null + toAtom(): Uint8Array | null + toPair(): [Program, Program] | null + get first(): Program + get rest(): Program + toList(): Array + uncurry(): Curry | null + toString(): string | null + toSmallNumber(): number | null + toBigInt(): bigint | null +} +export declare class Simulator { + constructor() + newCoin(puzzleHash: Uint8Array, amount: bigint): Coin + newP2(amount: bigint): P2Coin + spend(coinSpends: Array, secretKeys: Array): void +} + +/* auto-generated by `pnpm run update-declarations` */ + +export type ClvmValue = number | bigint | string | boolean | Program | Uint8Array | ClvmValue[]; diff --git a/napi/index.js b/napi/index.js new file mode 100644 index 00000000..86a69cad --- /dev/null +++ b/napi/index.js @@ -0,0 +1,326 @@ +/* tslint:disable */ +/* eslint-disable */ +/* prettier-ignore */ + +/* auto-generated by NAPI-RS */ + +const { existsSync, readFileSync } = require('fs') +const { join } = require('path') + +const { platform, arch } = process + +let nativeBinding = null +let localFileExisted = false +let loadError = null + +function isMusl() { + // For Node 10 + if (!process.report || typeof process.report.getReport !== 'function') { + try { + const lddPath = require('child_process').execSync('which ldd').toString().trim() + return readFileSync(lddPath, 'utf8').includes('musl') + } catch (e) { + return true + } + } else { + const { glibcVersionRuntime } = process.report.getReport().header + return !glibcVersionRuntime + } +} + +switch (platform) { + case 'android': + switch (arch) { + case 'arm64': + localFileExisted = existsSync(join(__dirname, 'chia-wallet-sdk.android-arm64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.android-arm64.node') + } else { + nativeBinding = require('chia-wallet-sdk-android-arm64') + } + } catch (e) { + loadError = e + } + break + case 'arm': + localFileExisted = existsSync(join(__dirname, 'chia-wallet-sdk.android-arm-eabi.node')) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.android-arm-eabi.node') + } else { + nativeBinding = require('chia-wallet-sdk-android-arm-eabi') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Android ${arch}`) + } + break + case 'win32': + switch (arch) { + case 'x64': + localFileExisted = existsSync( + join(__dirname, 'chia-wallet-sdk.win32-x64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.win32-x64-msvc.node') + } else { + nativeBinding = require('chia-wallet-sdk-win32-x64-msvc') + } + } catch (e) { + loadError = e + } + break + case 'ia32': + localFileExisted = existsSync( + join(__dirname, 'chia-wallet-sdk.win32-ia32-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.win32-ia32-msvc.node') + } else { + nativeBinding = require('chia-wallet-sdk-win32-ia32-msvc') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'chia-wallet-sdk.win32-arm64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.win32-arm64-msvc.node') + } else { + nativeBinding = require('chia-wallet-sdk-win32-arm64-msvc') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`) + } + break + case 'darwin': + localFileExisted = existsSync(join(__dirname, 'chia-wallet-sdk.darwin-universal.node')) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.darwin-universal.node') + } else { + nativeBinding = require('chia-wallet-sdk-darwin-universal') + } + break + } catch {} + switch (arch) { + case 'x64': + localFileExisted = existsSync(join(__dirname, 'chia-wallet-sdk.darwin-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.darwin-x64.node') + } else { + nativeBinding = require('chia-wallet-sdk-darwin-x64') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'chia-wallet-sdk.darwin-arm64.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.darwin-arm64.node') + } else { + nativeBinding = require('chia-wallet-sdk-darwin-arm64') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`) + } + break + case 'freebsd': + if (arch !== 'x64') { + throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) + } + localFileExisted = existsSync(join(__dirname, 'chia-wallet-sdk.freebsd-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.freebsd-x64.node') + } else { + nativeBinding = require('chia-wallet-sdk-freebsd-x64') + } + } catch (e) { + loadError = e + } + break + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'chia-wallet-sdk.linux-x64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.linux-x64-musl.node') + } else { + nativeBinding = require('chia-wallet-sdk-linux-x64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'chia-wallet-sdk.linux-x64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.linux-x64-gnu.node') + } else { + nativeBinding = require('chia-wallet-sdk-linux-x64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'chia-wallet-sdk.linux-arm64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.linux-arm64-musl.node') + } else { + nativeBinding = require('chia-wallet-sdk-linux-arm64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'chia-wallet-sdk.linux-arm64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.linux-arm64-gnu.node') + } else { + nativeBinding = require('chia-wallet-sdk-linux-arm64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'chia-wallet-sdk.linux-arm-musleabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.linux-arm-musleabihf.node') + } else { + nativeBinding = require('chia-wallet-sdk-linux-arm-musleabihf') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'chia-wallet-sdk.linux-arm-gnueabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.linux-arm-gnueabihf.node') + } else { + nativeBinding = require('chia-wallet-sdk-linux-arm-gnueabihf') + } + } catch (e) { + loadError = e + } + } + break + case 'riscv64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'chia-wallet-sdk.linux-riscv64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.linux-riscv64-musl.node') + } else { + nativeBinding = require('chia-wallet-sdk-linux-riscv64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'chia-wallet-sdk.linux-riscv64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.linux-riscv64-gnu.node') + } else { + nativeBinding = require('chia-wallet-sdk-linux-riscv64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 's390x': + localFileExisted = existsSync( + join(__dirname, 'chia-wallet-sdk.linux-s390x-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./chia-wallet-sdk.linux-s390x-gnu.node') + } else { + nativeBinding = require('chia-wallet-sdk-linux-s390x-gnu') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`) + } + break + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) +} + +if (!nativeBinding) { + if (loadError) { + throw loadError + } + throw new Error(`Failed to load native binding`) +} + +const { ClvmAllocator, curryTreeHash, intToSignedBytes, signedBytesToInt, toCoinId, Program, Simulator, compareBytes, sha256, fromHexRaw, fromHex, toHex } = nativeBinding + +module.exports.ClvmAllocator = ClvmAllocator +module.exports.curryTreeHash = curryTreeHash +module.exports.intToSignedBytes = intToSignedBytes +module.exports.signedBytesToInt = signedBytesToInt +module.exports.toCoinId = toCoinId +module.exports.Program = Program +module.exports.Simulator = Simulator +module.exports.compareBytes = compareBytes +module.exports.sha256 = sha256 +module.exports.fromHexRaw = fromHexRaw +module.exports.fromHex = fromHex +module.exports.toHex = toHex diff --git a/napi/npm/darwin-arm64/README.md b/napi/npm/darwin-arm64/README.md new file mode 100644 index 00000000..6499fb79 --- /dev/null +++ b/napi/npm/darwin-arm64/README.md @@ -0,0 +1,3 @@ +# `chia-wallet-sdk-darwin-arm64` + +This is the **aarch64-apple-darwin** binary for `chia-wallet-sdk` diff --git a/napi/npm/darwin-arm64/package.json b/napi/npm/darwin-arm64/package.json new file mode 100644 index 00000000..45eeb3cd --- /dev/null +++ b/napi/npm/darwin-arm64/package.json @@ -0,0 +1,22 @@ +{ + "name": "chia-wallet-sdk-darwin-arm64", + "version": "0.16.0", + "repository": { + "type": "git", + "url": "https://github.com/xch-dev/chia-wallet-sdk" + }, + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "main": "chia-wallet-sdk.darwin-arm64.node", + "files": [ + "chia-wallet-sdk.darwin-arm64.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} diff --git a/napi/npm/darwin-universal/README.md b/napi/npm/darwin-universal/README.md new file mode 100644 index 00000000..093e963f --- /dev/null +++ b/napi/npm/darwin-universal/README.md @@ -0,0 +1,3 @@ +# `chia-wallet-sdk-darwin-universal` + +This is the **universal-apple-darwin** binary for `chia-wallet-sdk` diff --git a/napi/npm/darwin-universal/package.json b/napi/npm/darwin-universal/package.json new file mode 100644 index 00000000..defb8c78 --- /dev/null +++ b/napi/npm/darwin-universal/package.json @@ -0,0 +1,19 @@ +{ + "name": "chia-wallet-sdk-darwin-universal", + "version": "0.16.0", + "repository": { + "type": "git", + "url": "https://github.com/xch-dev/chia-wallet-sdk" + }, + "os": [ + "darwin" + ], + "main": "chia-wallet-sdk.darwin-universal.node", + "files": [ + "chia-wallet-sdk.darwin-universal.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} diff --git a/napi/npm/darwin-x64/README.md b/napi/npm/darwin-x64/README.md new file mode 100644 index 00000000..47b13f89 --- /dev/null +++ b/napi/npm/darwin-x64/README.md @@ -0,0 +1,3 @@ +# `chia-wallet-sdk-darwin-x64` + +This is the **x86_64-apple-darwin** binary for `chia-wallet-sdk` diff --git a/napi/npm/darwin-x64/package.json b/napi/npm/darwin-x64/package.json new file mode 100644 index 00000000..a7180b38 --- /dev/null +++ b/napi/npm/darwin-x64/package.json @@ -0,0 +1,22 @@ +{ + "name": "chia-wallet-sdk-darwin-x64", + "version": "0.16.0", + "repository": { + "type": "git", + "url": "https://github.com/xch-dev/chia-wallet-sdk" + }, + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ], + "main": "chia-wallet-sdk.darwin-x64.node", + "files": [ + "chia-wallet-sdk.darwin-x64.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} diff --git a/napi/npm/linux-x64-gnu/README.md b/napi/npm/linux-x64-gnu/README.md new file mode 100644 index 00000000..46662882 --- /dev/null +++ b/napi/npm/linux-x64-gnu/README.md @@ -0,0 +1,3 @@ +# `chia-wallet-sdk-linux-x64-gnu` + +This is the **x86_64-unknown-linux-gnu** binary for `chia-wallet-sdk` diff --git a/napi/npm/linux-x64-gnu/package.json b/napi/npm/linux-x64-gnu/package.json new file mode 100644 index 00000000..e8407677 --- /dev/null +++ b/napi/npm/linux-x64-gnu/package.json @@ -0,0 +1,25 @@ +{ + "name": "chia-wallet-sdk-linux-x64-gnu", + "version": "0.16.0", + "repository": { + "type": "git", + "url": "https://github.com/xch-dev/chia-wallet-sdk" + }, + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "main": "chia-wallet-sdk.linux-x64-gnu.node", + "files": [ + "chia-wallet-sdk.linux-x64-gnu.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "libc": [ + "glibc" + ] +} diff --git a/napi/npm/win32-x64-msvc/README.md b/napi/npm/win32-x64-msvc/README.md new file mode 100644 index 00000000..069e9413 --- /dev/null +++ b/napi/npm/win32-x64-msvc/README.md @@ -0,0 +1,3 @@ +# `chia-wallet-sdk-win32-x64-msvc` + +This is the **x86_64-pc-windows-msvc** binary for `chia-wallet-sdk` diff --git a/napi/npm/win32-x64-msvc/package.json b/napi/npm/win32-x64-msvc/package.json new file mode 100644 index 00000000..5f05d200 --- /dev/null +++ b/napi/npm/win32-x64-msvc/package.json @@ -0,0 +1,22 @@ +{ + "name": "chia-wallet-sdk-win32-x64-msvc", + "version": "0.16.0", + "repository": { + "type": "git", + "url": "https://github.com/xch-dev/chia-wallet-sdk" + }, + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "main": "chia-wallet-sdk.win32-x64-msvc.node", + "files": [ + "chia-wallet-sdk.win32-x64-msvc.node" + ], + "license": "MIT", + "engines": { + "node": ">= 10" + } +} diff --git a/napi/package.json b/napi/package.json new file mode 100644 index 00000000..04db587f --- /dev/null +++ b/napi/package.json @@ -0,0 +1,53 @@ +{ + "name": "chia-wallet-sdk", + "version": "0.16.0", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/xch-dev/chia-wallet-sdk" + }, + "main": "index.js", + "types": "index.d.ts", + "packageManager": "pnpm@9.11.0", + "engines": { + "node": ">= 14" + }, + "scripts": { + "artifacts": "napi artifacts", + "build": "napi build --platform --release && pnpm run update-declarations", + "build:debug": "napi build --platform && pnpm run update-declarations", + "build:macos-arm64": "napi build --platform --release --target aarch64-apple-darwin && pnpm run update-declarations", + "build:macos-x64": "napi build --platform --release --target x86_64-apple-darwin && pnpm run update-declarations", + "build:windows-x64": "napi build --platform --release --target x86_64-pc-windows-msvc && pnpm run update-declarations", + "build:linux-x64": "napi build --platform --release --target x86_64-unknown-linux-gnu && pnpm run update-declarations", + "prepublishOnly": "napi prepublish -t npm --skip-gh-release", + "test": "ava", + "universal": "napi universal", + "version": "napi version", + "update-declarations": "node scripts/update-declarations.js" + }, + "napi": { + "name": "chia-wallet-sdk", + "triples": { + "additional": [ + "aarch64-apple-darwin", + "universal-apple-darwin" + ] + } + }, + "devDependencies": { + "@napi-rs/cli": "^2.18.4", + "@types/node": "^22.6.1", + "ava": "^6.0.1", + "ts-node": "^10.9.2" + }, + "ava": { + "timeout": "3m", + "extensions": [ + "ts" + ], + "require": [ + "ts-node/register" + ] + } +} diff --git a/napi/pnpm-lock.yaml b/napi/pnpm-lock.yaml new file mode 100644 index 00000000..2adc1af8 --- /dev/null +++ b/napi/pnpm-lock.yaml @@ -0,0 +1,1457 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@napi-rs/cli': + specifier: ^2.18.4 + version: 2.18.4 + '@types/node': + specifier: ^22.6.1 + version: 22.6.1 + ava: + specifier: ^6.0.1 + version: 6.1.3 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.6.1)(typescript@5.6.2) + +packages: + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + + '@napi-rs/cli@2.18.4': + resolution: {integrity: sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==} + engines: {node: '>= 10'} + hasBin: true + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + + '@sindresorhus/merge-streams@2.3.0': + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/node@22.6.1': + resolution: {integrity: sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==} + + '@vercel/nft@0.26.5': + resolution: {integrity: sha512-NHxohEqad6Ra/r4lGknO52uc/GrWILXAMs1BB4401GTqww0fw1bAqzpG1XHuDO+dprg4GvsD9ZLLSsdo78p9hQ==} + engines: {node: '>=16'} + hasBin: true + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + array-find-index@1.0.2: + resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} + engines: {node: '>=0.10.0'} + + arrgv@1.0.2: + resolution: {integrity: sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==} + engines: {node: '>=8.0.0'} + + arrify@3.0.0: + resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==} + engines: {node: '>=12'} + + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + + ava@6.1.3: + resolution: {integrity: sha512-tkKbpF1pIiC+q09wNU9OfyTDYZa8yuWvU2up3+lFJ3lr1RmnYh2GBpPwzYUEB0wvTPIUysGjcZLNZr7STDviRA==} + engines: {node: ^18.18 || ^20.8 || ^21 || ^22} + hasBin: true + peerDependencies: + '@ava/typescript': '*' + peerDependenciesMeta: + '@ava/typescript': + optional: true + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + blueimp-md5@2.19.0: + resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + callsites@4.2.0: + resolution: {integrity: sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==} + engines: {node: '>=12.20'} + + cbor@9.0.2: + resolution: {integrity: sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==} + engines: {node: '>=16'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chunkd@2.0.1: + resolution: {integrity: sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==} + + ci-info@4.0.0: + resolution: {integrity: sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==} + engines: {node: '>=8'} + + ci-parallel-vars@1.0.1: + resolution: {integrity: sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + code-excerpt@4.0.0: + resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concordance@5.0.4: + resolution: {integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==} + engines: {node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14'} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + convert-to-spaces@2.0.1: + resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + currently-unhandled@0.4.1: + resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==} + engines: {node: '>=0.10.0'} + + date-time@3.1.0: + resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} + engines: {node: '>=6'} + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + emittery@1.0.3: + resolution: {integrity: sha512-tJdCJitoy2lrC2ldJcqN4vkqJ00lT+tOWNT1hBJjO/3FDMJa5TTIiYGCKGkn/WfCyOzUMObeohbVTj00fhiLiA==} + engines: {node: '>=14.16'} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up-simple@1.0.0: + resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} + engines: {node: '>=18'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.2.0: + resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} + engines: {node: '>=18'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globby@14.0.2: + resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + ignore-by-default@2.1.0: + resolution: {integrity: sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==} + engines: {node: '>=10 <11 || >=12 <13 || >=14'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + irregular-plurals@3.5.0: + resolution: {integrity: sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + js-string-escape@1.0.1: + resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} + engines: {node: '>= 0.8'} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + load-json-file@7.0.1: + resolution: {integrity: sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + matcher@5.0.0: + resolution: {integrity: sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + md5-hex@3.0.1: + resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==} + engines: {node: '>=8'} + + memoize@10.0.0: + resolution: {integrity: sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==} + engines: {node: '>=18'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.2: + resolution: {integrity: sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==} + hasBin: true + + nofilter@3.1.0: + resolution: {integrity: sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==} + engines: {node: '>=12.19'} + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-map@7.0.2: + resolution: {integrity: sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q==} + engines: {node: '>=18'} + + package-config@5.0.0: + resolution: {integrity: sha512-GYTTew2slBcYdvRHqjhwaaydVMvn/qrGC323+nKclYioNSLTDUM/lGgtGTgyHVtYcozb+XkE8CNhwcraOmZ9Mg==} + engines: {node: '>=18'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@3.0.1: + resolution: {integrity: sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==} + engines: {node: '>=10'} + + plur@5.1.0: + resolution: {integrity: sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + pretty-ms@9.1.0: + resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} + engines: {node: '>=18'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + supertap@3.0.1: + resolution: {integrity: sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + temp-dir@3.0.0: + resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} + engines: {node: '>=14.16'} + + time-zone@1.0.0: + resolution: {integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + typescript@5.6.2: + resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + well-known-symbols@2.0.0: + resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==} + engines: {node: '>=6'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + +snapshots: + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@mapbox/node-pre-gyp@1.0.11': + dependencies: + detect-libc: 2.0.3 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.6.3 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@napi-rs/cli@2.18.4': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@rollup/pluginutils@4.2.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + + '@sindresorhus/merge-streams@2.3.0': {} + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/node@22.6.1': + dependencies: + undici-types: 6.19.8 + + '@vercel/nft@0.26.5': + dependencies: + '@mapbox/node-pre-gyp': 1.0.11 + '@rollup/pluginutils': 4.2.1 + acorn: 8.12.1 + acorn-import-attributes: 1.9.5(acorn@8.12.1) + async-sema: 3.1.1 + bindings: 1.5.0 + estree-walker: 2.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + node-gyp-build: 4.8.2 + resolve-from: 5.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + abbrev@1.1.1: {} + + acorn-import-attributes@1.9.5(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.12.1 + + acorn@8.12.1: {} + + agent-base@6.0.2: + dependencies: + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + aproba@2.0.0: {} + + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + array-find-index@1.0.2: {} + + arrgv@1.0.2: {} + + arrify@3.0.0: {} + + async-sema@3.1.1: {} + + ava@6.1.3: + dependencies: + '@vercel/nft': 0.26.5 + acorn: 8.12.1 + acorn-walk: 8.3.4 + ansi-styles: 6.2.1 + arrgv: 1.0.2 + arrify: 3.0.0 + callsites: 4.2.0 + cbor: 9.0.2 + chalk: 5.3.0 + chunkd: 2.0.1 + ci-info: 4.0.0 + ci-parallel-vars: 1.0.1 + cli-truncate: 4.0.0 + code-excerpt: 4.0.0 + common-path-prefix: 3.0.0 + concordance: 5.0.4 + currently-unhandled: 0.4.1 + debug: 4.3.7 + emittery: 1.0.3 + figures: 6.1.0 + globby: 14.0.2 + ignore-by-default: 2.1.0 + indent-string: 5.0.0 + is-plain-object: 5.0.0 + is-promise: 4.0.0 + matcher: 5.0.0 + memoize: 10.0.0 + ms: 2.1.3 + p-map: 7.0.2 + package-config: 5.0.0 + picomatch: 3.0.1 + plur: 5.1.0 + pretty-ms: 9.1.0 + resolve-cwd: 3.0.0 + stack-utils: 2.0.6 + strip-ansi: 7.1.0 + supertap: 3.0.1 + temp-dir: 3.0.0 + write-file-atomic: 5.0.1 + yargs: 17.7.2 + transitivePeerDependencies: + - encoding + - supports-color + + balanced-match@1.0.2: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + blueimp-md5@2.19.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + callsites@4.2.0: {} + + cbor@9.0.2: + dependencies: + nofilter: 3.1.0 + + chalk@5.3.0: {} + + chownr@2.0.0: {} + + chunkd@2.0.1: {} + + ci-info@4.0.0: {} + + ci-parallel-vars@1.0.1: {} + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + code-excerpt@4.0.0: + dependencies: + convert-to-spaces: 2.0.1 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-support@1.1.3: {} + + common-path-prefix@3.0.0: {} + + concat-map@0.0.1: {} + + concordance@5.0.4: + dependencies: + date-time: 3.1.0 + esutils: 2.0.3 + fast-diff: 1.3.0 + js-string-escape: 1.0.1 + lodash: 4.17.21 + md5-hex: 3.0.1 + semver: 7.6.3 + well-known-symbols: 2.0.0 + + console-control-strings@1.1.0: {} + + convert-to-spaces@2.0.1: {} + + create-require@1.1.1: {} + + currently-unhandled@0.4.1: + dependencies: + array-find-index: 1.0.2 + + date-time@3.1.0: + dependencies: + time-zone: 1.0.0 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + delegates@1.0.0: {} + + detect-libc@2.0.3: {} + + diff@4.0.2: {} + + emittery@1.0.3: {} + + emoji-regex@10.4.0: {} + + emoji-regex@8.0.0: {} + + escalade@3.2.0: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@5.0.0: {} + + esprima@4.0.1: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up-simple@1.0.0: {} + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs.realpath@1.0.0: {} + + gauge@3.0.2: + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.2.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globby@14.0.2: + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.2 + ignore: 5.3.2 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 + + graceful-fs@4.2.11: {} + + has-unicode@2.0.1: {} + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + ignore-by-default@2.1.0: {} + + ignore@5.3.2: {} + + imurmurhash@0.1.4: {} + + indent-string@5.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + irregular-plurals@3.5.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-plain-object@5.0.0: {} + + is-promise@4.0.0: {} + + is-unicode-supported@2.1.0: {} + + js-string-escape@1.0.1: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + load-json-file@7.0.1: {} + + lodash@4.17.21: {} + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-error@1.3.6: {} + + matcher@5.0.0: + dependencies: + escape-string-regexp: 5.0.0 + + md5-hex@3.0.1: + dependencies: + blueimp-md5: 2.19.0 + + memoize@10.0.0: + dependencies: + mimic-function: 5.0.1 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-function@5.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp@1.0.4: {} + + ms@2.1.3: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build@4.8.2: {} + + nofilter@3.1.0: {} + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + + object-assign@4.1.1: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-map@7.0.2: {} + + package-config@5.0.0: + dependencies: + find-up-simple: 1.0.0 + load-json-file: 7.0.1 + + parse-ms@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-type@5.0.0: {} + + picomatch@2.3.1: {} + + picomatch@3.0.1: {} + + plur@5.1.0: + dependencies: + irregular-plurals: 3.5.0 + + pretty-ms@9.1.0: + dependencies: + parse-ms: 4.0.0 + + queue-microtask@1.2.3: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + require-directory@2.1.1: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@5.0.0: {} + + reusify@1.0.4: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + semver@6.3.1: {} + + semver@7.6.3: {} + + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + + set-blocking@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + slash@5.1.0: {} + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + sprintf-js@1.0.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.2.0 + strip-ansi: 7.1.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + supertap@3.0.1: + dependencies: + indent-string: 5.0.0 + js-yaml: 3.14.1 + serialize-error: 7.0.1 + strip-ansi: 7.1.0 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + temp-dir@3.0.0: {} + + time-zone@1.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tr46@0.0.3: {} + + ts-node@10.9.2(@types/node@22.6.1)(typescript@5.6.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.6.1 + acorn: 8.12.1 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.6.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + type-fest@0.13.1: {} + + typescript@5.6.2: {} + + undici-types@6.19.8: {} + + unicorn-magic@0.1.0: {} + + util-deprecate@1.0.2: {} + + v8-compile-cache-lib@3.0.1: {} + + webidl-conversions@3.0.1: {} + + well-known-symbols@2.0.0: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + y18n@5.0.8: {} + + yallist@4.0.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} diff --git a/napi/scripts/update-declarations.js b/napi/scripts/update-declarations.js new file mode 100644 index 00000000..5e29eb1e --- /dev/null +++ b/napi/scripts/update-declarations.js @@ -0,0 +1,22 @@ +const { readFileSync, writeFileSync } = require("fs"); +const path = require("path"); + +const declarations = path.resolve(__dirname, "..", "index.d.ts"); +const content = readFileSync(declarations, "utf8"); + +const header = "/* auto-generated by `pnpm run update-declarations` */"; + +if (content.includes(header)) { + console.log("Declarations are already updated."); + process.exit(0); +} + +const lines = [ + "", + header, + "", + "export type ClvmValue = number | bigint | string | boolean | Program | Uint8Array | ClvmValue[];", + "", +]; + +writeFileSync(declarations, content + lines.join("\n"), "utf8"); diff --git a/napi/src/clvm.rs b/napi/src/clvm.rs new file mode 100644 index 00000000..d1f7fef8 --- /dev/null +++ b/napi/src/clvm.rs @@ -0,0 +1,559 @@ +use chia::{ + bls::PublicKey, + clvm_traits::{clvm_quote, ClvmEncoder, FromClvm, ToClvm}, + clvm_utils::{self, CurriedProgram, TreeHash}, + protocol::{self, Bytes32}, + puzzles::nft::{self, NFT_METADATA_UPDATER_PUZZLE_HASH}, +}; +use chia_wallet_sdk::{ + self as sdk, AggSigAmount, AggSigMe, AggSigParent, AggSigParentAmount, AggSigParentPuzzle, + AggSigPuzzle, AggSigPuzzleAmount, AggSigUnsafe, AssertBeforeHeightAbsolute, + AssertBeforeHeightRelative, AssertBeforeSecondsAbsolute, AssertBeforeSecondsRelative, + AssertCoinAnnouncement, AssertConcurrentPuzzle, AssertConcurrentSpend, AssertEphemeral, + AssertHeightAbsolute, AssertHeightRelative, AssertMyAmount, AssertMyBirthHeight, + AssertMyBirthSeconds, AssertMyCoinId, AssertMyParentId, AssertMyPuzzleHash, + AssertPuzzleAnnouncement, AssertSecondsAbsolute, AssertSecondsRelative, CreateCoin, + CreateCoinAnnouncement, CreatePuzzleAnnouncement, HashedPtr, ReceiveMessage, Remark, + ReserveFee, SendMessage, Softfork, SpendContext, +}; +use clvmr::{ + run_program, + serde::{node_from_bytes, node_from_bytes_backrefs}, + ChiaDialect, NodePtr, ENABLE_BLS_OPS_OUTSIDE_GUARD, ENABLE_FIXED_DIV, MEMPOOL_MODE, +}; +use napi::bindgen_prelude::*; + +use crate::{ + clvm_value::{Allocate, ClvmValue}, + traits::{FromJs, IntoJs, IntoRust}, + Coin, CoinSpend, MintedNfts, Nft, NftMetadata, NftMint, ParsedNft, Program, Spend, +}; + +type Clvm = Reference; + +#[napi] +pub struct ClvmAllocator(pub(crate) SpendContext); + +#[napi] +impl ClvmAllocator { + #[napi(constructor)] + pub fn new() -> Result { + Ok(Self(SpendContext::new())) + } + + #[napi(ts_args_type = "")] + pub fn nil(&mut self, this: This) -> Result { + Ok(Program::new(this, NodePtr::NIL)) + } + + #[napi(ts_args_type = "value: Uint8Array")] + pub fn deserialize(&mut self, this: This, value: Uint8Array) -> Result { + let ptr = node_from_bytes(&mut self.0.allocator, &value)?; + Ok(Program::new(this, ptr)) + } + + #[napi(ts_args_type = "value: Uint8Array")] + pub fn deserialize_with_backrefs( + &mut self, + this: This, + value: Uint8Array, + ) -> Result { + let ptr = node_from_bytes_backrefs(&mut self.0.allocator, &value)?; + Ok(Program::new(this, ptr)) + } + + #[napi] + pub fn tree_hash(&self, program: &Program) -> Result { + self.0.tree_hash(program.ptr).to_bytes().into_js() + } + + #[napi( + ts_args_type = "puzzle: Program, solution: Program, maxCost: bigint, mempoolMode: boolean" + )] + pub fn run( + &mut self, + env: Env, + this: This, + puzzle: &Program, + solution: &Program, + max_cost: BigInt, + mempool_mode: bool, + ) -> Result { + let mut flags = ENABLE_BLS_OPS_OUTSIDE_GUARD | ENABLE_FIXED_DIV; + + if mempool_mode { + flags |= MEMPOOL_MODE; + } + + let result = run_program( + &mut self.0.allocator, + &ChiaDialect::new(flags), + puzzle.ptr, + solution.ptr, + max_cost.into_rust()?, + ) + .map_err(|error| Error::from_reason(error.to_string()))?; + + Ok(Output { + value: Program::new(this, result.1).into_instance(env)?, + cost: result.0.into_js()?, + }) + } + + #[napi(ts_args_type = "program: Program, args: Array")] + pub fn curry( + &mut self, + this: This, + program: &Program, + args: Vec>, + ) -> Result { + let mut args_ptr = self.0.allocator.one(); + + for arg in args.into_iter().rev() { + args_ptr = self + .0 + .allocator + .encode_curried_arg(arg.ptr, args_ptr) + .map_err(|error| Error::from_reason(error.to_string()))?; + } + + self.0 + .alloc(&CurriedProgram { + program: program.ptr, + args: args_ptr, + }) + .map_err(|error| Error::from_reason(error.to_string())) + .map(|ptr| Program::new(this, ptr)) + } + + #[napi(ts_args_type = "first: ClvmValue, rest: ClvmValue")] + pub fn pair(&mut self, this: This, first: ClvmValue, rest: ClvmValue) -> Result { + let first = first.allocate(&mut self.0.allocator)?; + let rest = rest.allocate(&mut self.0.allocator)?; + let ptr = self + .0 + .allocator + .new_pair(first, rest) + .map_err(|error| Error::from_reason(error.to_string()))?; + Ok(Program::new(this, ptr)) + } + + #[napi(ts_args_type = "value: ClvmValue")] + pub fn alloc(&mut self, this: This, value: ClvmValue) -> Result { + let ptr = value.allocate(&mut self.0.allocator)?; + Ok(Program::new(this, ptr)) + } + + #[napi(ts_args_type = "value: NftMetadata")] + pub fn nft_metadata(&mut self, this: This, value: NftMetadata) -> Result { + let metadata = nft::NftMetadata::from_js(value)?; + + let ptr = metadata + .to_clvm(&mut self.0.allocator) + .map_err(|error| Error::from_reason(error.to_string()))?; + + Ok(Program::new(this, ptr)) + } + + #[napi(ts_args_type = "value: Program")] + pub fn parse_nft_metadata(&mut self, value: &Program) -> Result { + let metadata = nft::NftMetadata::from_clvm(&self.0.allocator, value.ptr) + .map_err(|error| Error::from_reason(error.to_string()))?; + + metadata.into_js() + } + + #[napi(ts_args_type = "conditions: Array")] + pub fn delegated_spend_for_conditions( + &mut self, + env: Env, + this: This, + conditions: Vec>, + ) -> Result { + let conditions: Vec = conditions.into_iter().map(|program| program.ptr).collect(); + + let delegated_puzzle = self + .0 + .alloc(&clvm_quote!(conditions)) + .map_err(|error| Error::from_reason(error.to_string()))?; + + Ok(Spend { + puzzle: Program::new(this.clone(env)?, delegated_puzzle).into_instance(env)?, + solution: Program::new(this, NodePtr::NIL).into_instance(env)?, + }) + } + + #[napi(ts_args_type = "syntheticKey: Uint8Array, delegatedSpend: Spend")] + pub fn spend_p2_standard( + &mut self, + env: Env, + this: This, + synthetic_key: Uint8Array, + delegated_spend: Spend, + ) -> Result { + let ctx = &mut self.0; + let synthetic_key = PublicKey::from_js(synthetic_key)?; + let p2 = sdk::StandardLayer::new(synthetic_key); + + let spend = p2 + .delegated_inner_spend( + ctx, + sdk::Spend::new(delegated_spend.puzzle.ptr, delegated_spend.solution.ptr), + ) + .map_err(|error| Error::from_reason(error.to_string()))?; + + Ok(Spend { + puzzle: Program::new(this.clone(env)?, spend.puzzle).into_instance(env)?, + solution: Program::new(this, spend.solution).into_instance(env)?, + }) + } + + #[napi( + ts_args_type = "launcherId: Uint8Array, coinId: Uint8Array, singletonInnerPuzzleHash: Uint8Array, delegatedSpend: Spend" + )] + pub fn spend_p2_delegated_singleton( + &mut self, + env: Env, + this: This, + launcher_id: Uint8Array, + coin_id: Uint8Array, + singleton_inner_puzzle_hash: Uint8Array, + delegated_spend: Spend, + ) -> Result { + let p2 = sdk::P2DelegatedSingletonLayer::new(launcher_id.into_rust()?); + + let spend = p2 + .spend( + &mut self.0, + coin_id.into_rust()?, + singleton_inner_puzzle_hash.into_rust()?, + sdk::Spend { + puzzle: delegated_spend.puzzle.ptr, + solution: delegated_spend.solution.ptr, + }, + ) + .map_err(|error| Error::from_reason(error.to_string()))?; + + Ok(Spend { + puzzle: Program::new(this.clone(env)?, spend.puzzle).into_instance(env)?, + solution: Program::new(this, spend.solution).into_instance(env)?, + }) + } + + #[napi(ts_args_type = "parent_coin_id: Uint8Array, nft_mints: Array")] + pub fn mint_nfts( + &mut self, + env: Env, + this: This, + parent_coin_id: Uint8Array, + nft_mints: Vec, + ) -> Result { + let parent_coin_id = parent_coin_id.into_rust()?; + + let mut result = MintedNfts { + nfts: Vec::new(), + coin_spends: Vec::new(), + parent_conditions: Vec::new(), + }; + + for (i, nft_mint) in nft_mints.into_iter().enumerate() { + let (conditions, nft) = sdk::Launcher::new(parent_coin_id, i as u64 * 2 + 1) + .mint_nft( + &mut self.0, + sdk::NftMint:: { + metadata: nft_mint.metadata.into_rust()?, + p2_puzzle_hash: nft_mint.p2_puzzle_hash.into_rust()?, + royalty_puzzle_hash: nft_mint.royalty_puzzle_hash.into_rust()?, + royalty_ten_thousandths: nft_mint.royalty_ten_thousandths, + metadata_updater_puzzle_hash: NFT_METADATA_UPDATER_PUZZLE_HASH.into(), + owner: None, + }, + ) + .map_err(|error| Error::from_reason(error.to_string()))?; + + let serialized_metadata = self + .0 + .serialize(&nft.info.metadata) + .map_err(|error| Error::from_reason(error.to_string()))?; + + result + .nfts + .push(nft.with_metadata(serialized_metadata).into_js()?); + + for condition in conditions { + let condition = condition + .to_clvm(&mut self.0.allocator) + .map_err(|error| Error::from_reason(error.to_string()))?; + + result + .parent_conditions + .push(Program::new(this.clone(env)?, condition).into_instance(env)?); + } + } + + result.coin_spends.extend( + self.0 + .take() + .into_iter() + .map(IntoJs::into_js) + .collect::>>()?, + ); + + Ok(result) + } + + #[napi(ts_args_type = "puzzle: Program")] + pub fn parse_nft_info( + &mut self, + env: Env, + this: This, + puzzle: &Program, + ) -> Result> { + let puzzle = sdk::Puzzle::parse(&self.0.allocator, puzzle.ptr); + + let Some((nft_info, inner_puzzle)) = + sdk::NftInfo::::parse(&self.0.allocator, puzzle) + .map_err(|error| Error::from_reason(error.to_string()))? + else { + return Ok(None); + }; + + Ok(Some(ParsedNft { + info: nft_info.into_js()?, + inner_puzzle: Program::new(this, inner_puzzle.ptr()).into_instance(env)?, + })) + } + + #[napi] + pub fn parse_child_nft( + &mut self, + parent_coin: Coin, + parent_puzzle: &Program, + parent_solution: &Program, + ) -> Result> { + let parent_puzzle = sdk::Puzzle::parse(&self.0.allocator, parent_puzzle.ptr); + + let Some(nft) = sdk::Nft::::parse_child( + &mut self.0.allocator, + parent_coin.into_rust()?, + parent_puzzle, + parent_solution.ptr, + ) + .map_err(|error| Error::from_reason(error.to_string()))? + else { + return Ok(None); + }; + + let serialized_metadata = self + .0 + .serialize(&nft.info.metadata) + .map_err(|error| Error::from_reason(error.to_string()))?; + + Ok(Some(nft.with_metadata(serialized_metadata).into_js()?)) + } + + #[napi] + pub fn spend_nft(&mut self, nft: Nft, inner_spend: Spend) -> Result> { + let ctx = &mut self.0; + let nft = sdk::Nft::::from_js(nft)?; + + nft.spend( + ctx, + sdk::Spend::new(inner_spend.puzzle.ptr, inner_spend.solution.ptr), + ) + .map_err(|error| Error::from_reason(error.to_string()))?; + + ctx.take().into_iter().map(IntoJs::into_js).collect() + } +} + +#[napi(object)] +pub struct Output { + pub value: ClassInstance, + pub cost: BigInt, +} + +#[napi] +pub fn curry_tree_hash(tree_hash: Uint8Array, args: Vec) -> Result { + let tree_hash: Bytes32 = tree_hash.into_rust()?; + let args: Vec = args + .into_iter() + .map(|arg| Ok(TreeHash::new(arg.into_rust()?))) + .collect::>>()?; + clvm_utils::curry_tree_hash(tree_hash.into(), &args) + .to_bytes() + .into_js() +} + +#[napi] +pub fn int_to_signed_bytes(big_int: BigInt) -> Result { + let number: num_bigint::BigInt = big_int.into_rust()?; + number.to_signed_bytes_be().into_js() +} + +#[napi] +pub fn signed_bytes_to_int(bytes: Uint8Array) -> Result { + let bytes: Vec = bytes.into_rust()?; + let number = num_bigint::BigInt::from_signed_bytes_be(&bytes); + number.into_js() +} + +macro_rules! conditions { + ( $( $condition:ident { $hint:literal $function:ident( $( $name:ident: $ty:ty $( => $remap:ty )? ),* ) }, )* ) => { + $( #[napi] + impl ClvmAllocator { + #[napi(ts_args_type = $hint)] + pub fn $function( &mut self, this: This, $( $name: $ty ),* ) -> Result { + $( let $name $( : $remap )? = FromJs::from_js($name)?; )* + let ptr = $condition::new( $( $name ),* ) + .to_clvm(&mut self.0.allocator) + .map_err(|error| Error::from_reason(error.to_string()))?; + + Ok(Program::new(this, ptr)) + } + } )* + }; +} + +conditions!( + Remark { + "value: Program" + remark(value: ClassInstance => NodePtr) + }, + AggSigParent { + "publicKey: Uint8Array, message: Uint8Array" + agg_sig_parent(public_key: Uint8Array, message: Uint8Array) + }, + AggSigPuzzle { + "publicKey: Uint8Array, message: Uint8Array" + agg_sig_puzzle(public_key: Uint8Array, message: Uint8Array) + }, + AggSigAmount { + "publicKey: Uint8Array, message: Uint8Array" + agg_sig_amount(public_key: Uint8Array, message: Uint8Array) + }, + AggSigPuzzleAmount { + "publicKey: Uint8Array, message: Uint8Array" + agg_sig_puzzle_amount(public_key: Uint8Array, message: Uint8Array) + }, + AggSigParentAmount { + "publicKey: Uint8Array, message: Uint8Array" + agg_sig_parent_amount(public_key: Uint8Array, message: Uint8Array) + }, + AggSigParentPuzzle { + "publicKey: Uint8Array, message: Uint8Array" + agg_sig_parent_puzzle(public_key: Uint8Array, message: Uint8Array) + }, + AggSigUnsafe { + "publicKey: Uint8Array, message: Uint8Array" + agg_sig_unsafe(public_key: Uint8Array, message: Uint8Array) + }, + AggSigMe { + "publicKey: Uint8Array, message: Uint8Array" + agg_sig_me(public_key: Uint8Array, message: Uint8Array) + }, + CreateCoin { + "puzzleHash: Uint8Array, amount: bigint, memos: Array" + create_coin(puzzle_hash: Uint8Array, amount: BigInt, memos: Vec) + }, + ReserveFee { + "fee: bigint" + reserve_fee(fee: BigInt) + }, + CreateCoinAnnouncement { + "message: Uint8Array" + create_coin_announcement(message: Uint8Array) + }, + CreatePuzzleAnnouncement { + "message: Uint8Array" + create_puzzle_announcement(message: Uint8Array) + }, + AssertCoinAnnouncement { + "announcementId: Uint8Array" + assert_coin_announcement(announcement_id: Uint8Array) + }, + AssertPuzzleAnnouncement { + "announcementId: Uint8Array" + assert_puzzle_announcement(announcement_id: Uint8Array) + }, + AssertConcurrentSpend { + "coinId: Uint8Array" + assert_concurrent_spend(coin_id: Uint8Array) + }, + AssertConcurrentPuzzle { + "puzzleHash: Uint8Array" + assert_concurrent_puzzle(puzzle_hash: Uint8Array) + }, + AssertSecondsRelative { + "seconds: bigint" + assert_seconds_relative(seconds: BigInt) + }, + AssertSecondsAbsolute { + "seconds: bigint" + assert_seconds_absolute(seconds: BigInt) + }, + AssertHeightRelative { + "height: number" + assert_height_relative(height: u32) + }, + AssertHeightAbsolute { + "height: number" + assert_height_absolute(height: u32) + }, + AssertBeforeSecondsRelative { + "seconds: bigint" + assert_before_seconds_relative(seconds: BigInt) + }, + AssertBeforeSecondsAbsolute { + "seconds: bigint" + assert_before_seconds_absolute(seconds: BigInt) + }, + AssertBeforeHeightRelative { + "height: number" + assert_before_height_relative(height: u32) + }, + AssertBeforeHeightAbsolute { + "height: number" + assert_before_height_absolute(height: u32) + }, + AssertMyCoinId { + "coinId: Uint8Array" + assert_my_coin_id(coin_id: Uint8Array) + }, + AssertMyParentId { + "parentId: Uint8Array" + assert_my_parent_id(parent_id: Uint8Array) + }, + AssertMyPuzzleHash { + "puzzleHash: Uint8Array" + assert_my_puzzle_hash(puzzle_hash: Uint8Array) + }, + AssertMyAmount { + "amount: bigint" + assert_my_amount(amount: BigInt) + }, + AssertMyBirthSeconds { + "seconds: bigint" + assert_my_birth_seconds(seconds: BigInt) + }, + AssertMyBirthHeight { + "height: number" + assert_my_birth_height(height: u32) + }, + AssertEphemeral { + "" + assert_ephemeral() + }, + SendMessage { + "mode: number, message: Uint8Array, data: Array" + send_message(mode: u8, message: Uint8Array, data: Vec> => Vec) + }, + ReceiveMessage { + "mode: number, message: Uint8Array, data: Array" + receive_message(mode: u8, message: Uint8Array, data: Vec> => Vec) + }, + Softfork { + "cost: bigint, value: Program" + softfork(cost: BigInt, value: ClassInstance => NodePtr) + }, +); diff --git a/napi/src/clvm_value.rs b/napi/src/clvm_value.rs new file mode 100644 index 00000000..38f3752a --- /dev/null +++ b/napi/src/clvm_value.rs @@ -0,0 +1,126 @@ +use chia::clvm_traits::ToClvm; +use clvmr::{Allocator, NodePtr}; +use napi::bindgen_prelude::*; + +use crate::{traits::IntoRust, Program}; + +pub(crate) type ClvmValue = + Either7, Uint8Array, Array>; + +pub(crate) trait Allocate { + fn allocate(self, allocator: &mut Allocator) -> Result; +} + +impl Allocate for ClvmValue { + fn allocate(self, allocator: &mut Allocator) -> Result { + match self { + Either7::A(value) => value.allocate(allocator), + Either7::B(value) => value.allocate(allocator), + Either7::C(value) => value.allocate(allocator), + Either7::D(value) => value.allocate(allocator), + Either7::E(value) => value.allocate(allocator), + Either7::F(value) => value.allocate(allocator), + Either7::G(value) => value.allocate(allocator), + } + } +} + +impl Allocate for f64 { + fn allocate(self, allocator: &mut Allocator) -> Result { + if self.is_infinite() { + return Err(Error::from_reason("Value is infinite".to_string())); + } + + if self.is_nan() { + return Err(Error::from_reason("Value is NaN".to_string())); + } + + if self.fract() != 0.0 { + return Err(Error::from_reason( + "Value has a fractional part".to_string(), + )); + } + + if self > 9_007_199_254_740_991.0 { + return Err(Error::from_reason( + "Value is larger than MAX_SAFE_INTEGER".to_string(), + )); + } + + if self < -9_007_199_254_740_991.0 { + return Err(Error::from_reason( + "Value is smaller than MIN_SAFE_INTEGER".to_string(), + )); + } + + let value = self as i64; + + if (0..=67_108_863).contains(&value) { + allocator + .new_small_number(value as u32) + .map_err(|error| Error::from_reason(error.to_string())) + } else { + allocator + .new_number(value.into()) + .map_err(|error| Error::from_reason(error.to_string())) + } + } +} + +impl Allocate for BigInt { + fn allocate(self, allocator: &mut Allocator) -> Result { + let value = self.into_rust()?; + allocator + .new_number(value) + .map_err(|error| Error::from_reason(error.to_string())) + } +} + +impl Allocate for String { + fn allocate(self, allocator: &mut Allocator) -> Result { + allocator + .new_atom(self.as_bytes()) + .map_err(|error| Error::from_reason(error.to_string())) + } +} + +impl Allocate for bool { + fn allocate(self, allocator: &mut Allocator) -> Result { + allocator + .new_small_number(u32::from(self)) + .map_err(|error| Error::from_reason(error.to_string())) + } +} + +impl Allocate for Uint8Array { + fn allocate(self, allocator: &mut Allocator) -> Result { + let value: Vec = self.into_rust()?; + allocator + .new_atom(&value) + .map_err(|error| Error::from_reason(error.to_string())) + } +} + +impl Allocate for Array { + fn allocate(self, allocator: &mut Allocator) -> Result { + let mut items = Vec::with_capacity(self.len() as usize); + + for i in 0..self.len() { + let Some(item) = self.get::(i)? else { + return Err(Error::from_reason(format!("Item at index {i} is missing"))); + }; + + items.push(item.allocate(allocator)?); + } + + items + .to_clvm(allocator) + .map_err(|error| Error::from_reason(error.to_string())) + } +} + +impl Allocate for ClassInstance { + fn allocate(self, _allocator: &mut Allocator) -> Result { + Ok(self.ptr) + } +} diff --git a/napi/src/coin.rs b/napi/src/coin.rs new file mode 100644 index 00000000..20f846c9 --- /dev/null +++ b/napi/src/coin.rs @@ -0,0 +1,36 @@ +use chia::protocol; +use napi::bindgen_prelude::*; + +use crate::traits::{FromJs, IntoJs, IntoRust}; + +#[napi(object)] +pub struct Coin { + pub parent_coin_info: Uint8Array, + pub puzzle_hash: Uint8Array, + pub amount: BigInt, +} + +impl IntoJs for protocol::Coin { + fn into_js(self) -> Result { + Ok(Coin { + parent_coin_info: self.parent_coin_info.into_js()?, + puzzle_hash: self.puzzle_hash.into_js()?, + amount: self.amount.into_js()?, + }) + } +} + +impl FromJs for protocol::Coin { + fn from_js(value: Coin) -> Result { + Ok(Self { + parent_coin_info: value.parent_coin_info.into_rust()?, + puzzle_hash: value.puzzle_hash.into_rust()?, + amount: value.amount.into_rust()?, + }) + } +} + +#[napi] +pub fn to_coin_id(coin: Coin) -> Result { + protocol::Coin::from_js(coin)?.coin_id().into_js() +} diff --git a/napi/src/coin_spend.rs b/napi/src/coin_spend.rs new file mode 100644 index 00000000..b137c5d8 --- /dev/null +++ b/napi/src/coin_spend.rs @@ -0,0 +1,40 @@ +use chia::protocol; +use napi::bindgen_prelude::*; + +use crate::{ + traits::{FromJs, IntoJs, IntoRust}, + Coin, Program, +}; + +#[napi(object)] +pub struct CoinSpend { + pub coin: Coin, + pub puzzle_reveal: Uint8Array, + pub solution: Uint8Array, +} + +impl IntoJs for protocol::CoinSpend { + fn into_js(self) -> Result { + Ok(CoinSpend { + coin: self.coin.into_js()?, + puzzle_reveal: self.puzzle_reveal.into_js()?, + solution: self.solution.into_js()?, + }) + } +} + +impl FromJs for protocol::CoinSpend { + fn from_js(coin_spend: CoinSpend) -> Result { + Ok(protocol::CoinSpend { + coin: coin_spend.coin.into_rust()?, + puzzle_reveal: coin_spend.puzzle_reveal.into_rust()?, + solution: coin_spend.solution.into_rust()?, + }) + } +} + +#[napi(object)] +pub struct Spend { + pub puzzle: ClassInstance, + pub solution: ClassInstance, +} diff --git a/napi/src/lib.rs b/napi/src/lib.rs new file mode 100644 index 00000000..cac4d54e --- /dev/null +++ b/napi/src/lib.rs @@ -0,0 +1,28 @@ +#![allow(missing_debug_implementations)] +#![allow(missing_copy_implementations)] +#![allow(clippy::needless_pass_by_value)] +#![allow(clippy::new_without_default)] +#![allow(clippy::cast_possible_truncation)] +#![allow(clippy::cast_sign_loss)] + +#[macro_use] +extern crate napi_derive; + +mod clvm; +mod clvm_value; +mod coin; +mod coin_spend; +mod lineage_proof; +mod nft; +mod program; +mod simulator; +mod traits; +mod utils; + +pub use clvm::*; +pub use coin::*; +pub use coin_spend::*; +pub use lineage_proof::*; +pub use nft::*; +pub use program::*; +pub use utils::*; diff --git a/napi/src/lineage_proof.rs b/napi/src/lineage_proof.rs new file mode 100644 index 00000000..bff06c65 --- /dev/null +++ b/napi/src/lineage_proof.rs @@ -0,0 +1,45 @@ +use chia::puzzles; +use napi::bindgen_prelude::*; + +use crate::traits::{FromJs, IntoJs, IntoRust}; + +#[napi(object)] +pub struct LineageProof { + pub parent_parent_coin_info: Uint8Array, + pub parent_inner_puzzle_hash: Option, + pub parent_amount: BigInt, +} + +impl FromJs for puzzles::Proof { + fn from_js(value: LineageProof) -> Result { + if let Some(parent_inner_puzzle_hash) = value.parent_inner_puzzle_hash { + Ok(Self::Lineage(puzzles::LineageProof { + parent_parent_coin_info: value.parent_parent_coin_info.into_rust()?, + parent_inner_puzzle_hash: parent_inner_puzzle_hash.into_rust()?, + parent_amount: value.parent_amount.into_rust()?, + })) + } else { + Ok(Self::Eve(puzzles::EveProof { + parent_parent_coin_info: value.parent_parent_coin_info.into_rust()?, + parent_amount: value.parent_amount.into_rust()?, + })) + } + } +} + +impl IntoJs for puzzles::Proof { + fn into_js(self) -> Result { + match self { + Self::Lineage(proof) => Ok(LineageProof { + parent_parent_coin_info: proof.parent_parent_coin_info.into_js()?, + parent_inner_puzzle_hash: Some(proof.parent_inner_puzzle_hash.into_js()?), + parent_amount: proof.parent_amount.into_js()?, + }), + Self::Eve(proof) => Ok(LineageProof { + parent_parent_coin_info: proof.parent_parent_coin_info.into_js()?, + parent_inner_puzzle_hash: None, + parent_amount: proof.parent_amount.into_js()?, + }), + } + } +} diff --git a/napi/src/nft.rs b/napi/src/nft.rs new file mode 100644 index 00000000..f52af5bc --- /dev/null +++ b/napi/src/nft.rs @@ -0,0 +1,141 @@ +use chia::protocol; +use chia::puzzles::nft; +use chia_wallet_sdk as sdk; +use napi::bindgen_prelude::*; + +use crate::{ + traits::{FromJs, IntoJs, IntoRust}, + Coin, CoinSpend, LineageProof, Program, +}; + +#[napi(object)] +pub struct Nft { + pub coin: Coin, + pub lineage_proof: LineageProof, + pub info: NftInfo, +} + +impl IntoJs for sdk::Nft { + fn into_js(self) -> Result { + Ok(Nft { + coin: self.coin.into_js()?, + lineage_proof: self.proof.into_js()?, + info: self.info.into_js()?, + }) + } +} + +impl FromJs for sdk::Nft { + fn from_js(nft: Nft) -> Result { + Ok(sdk::Nft { + coin: nft.coin.into_rust()?, + proof: nft.lineage_proof.into_rust()?, + info: nft.info.into_rust()?, + }) + } +} + +#[napi(object)] +pub struct NftInfo { + pub launcher_id: Uint8Array, + pub metadata: Uint8Array, + pub metadata_updater_puzzle_hash: Uint8Array, + pub current_owner: Option, + pub royalty_puzzle_hash: Uint8Array, + pub royalty_ten_thousandths: u16, + pub p2_puzzle_hash: Uint8Array, +} + +impl IntoJs for sdk::NftInfo { + fn into_js(self) -> Result { + Ok(NftInfo { + launcher_id: self.launcher_id.into_js()?, + metadata: self.metadata.into_js()?, + metadata_updater_puzzle_hash: self.metadata_updater_puzzle_hash.into_js()?, + current_owner: self.current_owner.map(IntoJs::into_js).transpose()?, + royalty_puzzle_hash: self.royalty_puzzle_hash.into_js()?, + royalty_ten_thousandths: self.royalty_ten_thousandths, + p2_puzzle_hash: self.p2_puzzle_hash.into_js()?, + }) + } +} + +impl FromJs for sdk::NftInfo { + fn from_js(info: NftInfo) -> Result { + Ok(sdk::NftInfo { + launcher_id: info.launcher_id.into_rust()?, + metadata: info.metadata.into_rust()?, + metadata_updater_puzzle_hash: info.metadata_updater_puzzle_hash.into_rust()?, + current_owner: info.current_owner.map(IntoRust::into_rust).transpose()?, + royalty_puzzle_hash: info.royalty_puzzle_hash.into_rust()?, + royalty_ten_thousandths: info.royalty_ten_thousandths, + p2_puzzle_hash: info.p2_puzzle_hash.into_rust()?, + }) + } +} + +#[napi(object)] +pub struct NftMetadata { + pub edition_number: BigInt, + pub edition_total: BigInt, + pub data_uris: Vec, + pub data_hash: Option, + pub metadata_uris: Vec, + pub metadata_hash: Option, + pub license_uris: Vec, + pub license_hash: Option, +} + +impl IntoJs for nft::NftMetadata { + fn into_js(self) -> Result { + Ok(NftMetadata { + edition_number: self.edition_number.into_js()?, + edition_total: self.edition_total.into_js()?, + data_uris: self.data_uris, + data_hash: self.data_hash.map(IntoJs::into_js).transpose()?, + metadata_uris: self.metadata_uris, + metadata_hash: self.metadata_hash.map(IntoJs::into_js).transpose()?, + license_uris: self.license_uris, + license_hash: self.license_hash.map(IntoJs::into_js).transpose()?, + }) + } +} + +impl FromJs for nft::NftMetadata { + fn from_js(metadata: NftMetadata) -> Result { + Ok(nft::NftMetadata { + edition_number: metadata.edition_number.into_rust()?, + edition_total: metadata.edition_total.into_rust()?, + data_uris: metadata.data_uris, + data_hash: metadata.data_hash.map(IntoRust::into_rust).transpose()?, + metadata_uris: metadata.metadata_uris, + metadata_hash: metadata + .metadata_hash + .map(IntoRust::into_rust) + .transpose()?, + license_uris: metadata.license_uris, + license_hash: metadata.license_hash.map(IntoRust::into_rust).transpose()?, + }) + } +} + +#[napi(object)] +pub struct ParsedNft { + pub info: NftInfo, + pub inner_puzzle: ClassInstance, +} + +#[napi(object)] +pub struct NftMint { + pub metadata: NftMetadata, + pub p2_puzzle_hash: Uint8Array, + pub royalty_puzzle_hash: Uint8Array, + pub royalty_ten_thousandths: u16, +} + +#[napi(object)] +pub struct MintedNfts { + pub nfts: Vec, + pub coin_spends: Vec, + pub parent_conditions: Vec>, +} diff --git a/napi/src/program.rs b/napi/src/program.rs new file mode 100644 index 00000000..dce58b77 --- /dev/null +++ b/napi/src/program.rs @@ -0,0 +1,183 @@ +use std::{num::TryFromIntError, string::FromUtf8Error}; + +use chia::{ + clvm_traits::{ClvmDecoder, FromClvm}, + clvm_utils::{tree_hash, CurriedProgram}, +}; +use clvmr::{ + serde::{node_to_bytes, node_to_bytes_backrefs}, + Allocator, NodePtr, SExp, +}; +use napi::bindgen_prelude::*; + +use crate::{traits::IntoJs, ClvmAllocator}; + +#[napi] +pub struct Program { + pub(crate) ctx: Reference, + pub(crate) ptr: NodePtr, +} + +impl Program { + pub(crate) fn new(ctx: Reference, ptr: NodePtr) -> Self { + Self { ctx, ptr } + } + + fn alloc(&self) -> &Allocator { + &self.ctx.0.allocator + } +} + +#[napi] +impl Program { + #[napi] + pub fn is_atom(&self) -> bool { + self.ptr.is_atom() + } + + #[napi] + pub fn is_pair(&self) -> bool { + self.ptr.is_pair() + } + + #[napi] + pub fn tree_hash(&self) -> Result { + tree_hash(self.alloc(), self.ptr).to_bytes().into_js() + } + + #[napi] + pub fn serialize(&self) -> Result { + let bytes = node_to_bytes(self.alloc(), self.ptr)?; + bytes.into_js() + } + + #[napi] + pub fn serialize_with_backrefs(&self) -> Result { + let bytes = node_to_bytes_backrefs(self.alloc(), self.ptr)?; + bytes.into_js() + } + + #[napi] + pub fn length(&self) -> Result> { + match self.alloc().sexp(self.ptr) { + SExp::Atom => Ok(Some(self.alloc().atom_len(self.ptr).try_into().map_err( + |error: TryFromIntError| Error::from_reason(error.to_string()), + )?)), + SExp::Pair(..) => Ok(None), + } + } + + #[napi] + pub fn to_atom(&self) -> Result> { + match self.alloc().sexp(self.ptr) { + SExp::Atom => Ok(Some( + self.alloc().atom(self.ptr).as_ref().to_vec().into_js()?, + )), + SExp::Pair(..) => Ok(None), + } + } + + #[napi(ts_return_type = "[Program, Program] | null")] + pub fn to_pair(&self, env: Env) -> Result> { + let SExp::Pair(first, rest) = self.alloc().sexp(self.ptr) else { + return Ok(None); + }; + + let mut array = env.create_array(2)?; + + array.set( + 0, + Program::new(self.ctx.clone(env)?, first).into_instance(env)?, + )?; + + array.set( + 1, + Program::new(self.ctx.clone(env)?, rest).into_instance(env)?, + )?; + + Ok(Some(array)) + } + + #[napi(getter)] + pub fn first(&self, env: Env) -> Result { + let SExp::Pair(first, _rest) = self.alloc().sexp(self.ptr) else { + return Err(Error::from_reason("Cannot call first on an atom")); + }; + Ok(Program::new(self.ctx.clone(env)?, first)) + } + + #[napi(getter)] + pub fn rest(&self, env: Env) -> Result { + let SExp::Pair(_first, rest) = self.alloc().sexp(self.ptr) else { + return Err(Error::from_reason("Cannot call rest on an atom")); + }; + Ok(Program::new(self.ctx.clone(env)?, rest)) + } + + #[napi] + pub fn to_list(&self, env: Env) -> Result>> { + Vec::::from_clvm(self.alloc(), self.ptr) + .map_err(|error| Error::from_reason(error.to_string()))? + .into_iter() + .map(|ptr| Program::new(self.ctx.clone(env)?, ptr).into_instance(env)) + .collect() + } + + #[napi] + pub fn uncurry(&self, env: Env) -> Result> { + let Ok(value) = CurriedProgram::::from_clvm(self.alloc(), self.ptr) + else { + return Ok(None); + }; + + let mut args = Vec::new(); + let mut args_ptr = value.args; + + while let Ok((first, rest)) = self.alloc().decode_curried_arg(&args_ptr) { + args.push(Program::new(self.ctx.clone(env)?, first).into_instance(env)?); + args_ptr = rest; + } + + if self.alloc().small_number(args_ptr) != Some(1) { + return Ok(None); + } + + Ok(Some(Curry { + program: Program::new(self.ctx.clone(env)?, value.program).into_instance(env)?, + args, + })) + } + + #[napi] + pub fn to_string(&self) -> Result> { + match self.alloc().sexp(self.ptr) { + SExp::Atom => Ok(Some( + String::from_utf8(self.alloc().atom(self.ptr).as_ref().to_vec()) + .map_err(|error: FromUtf8Error| Error::from_reason(error.to_string()))?, + )), + SExp::Pair(..) => Ok(None), + } + } + + #[napi] + pub fn to_small_number(&self) -> Option { + match self.alloc().sexp(self.ptr) { + SExp::Atom => self.alloc().small_number(self.ptr), + SExp::Pair(..) => None, + } + } + + #[napi] + pub fn to_big_int(&self) -> Result> { + match self.alloc().sexp(self.ptr) { + SExp::Atom => Ok(Some(self.alloc().number(self.ptr).into_js()?)), + SExp::Pair(..) => Ok(None), + } + } +} + +#[napi(object)] +pub struct Curry { + pub program: ClassInstance, + pub args: Vec>, +} diff --git a/napi/src/simulator.rs b/napi/src/simulator.rs new file mode 100644 index 00000000..4c9615ab --- /dev/null +++ b/napi/src/simulator.rs @@ -0,0 +1,73 @@ +use chia::bls::SecretKey; +use chia_wallet_sdk as sdk; +use napi::bindgen_prelude::*; + +use crate::{ + traits::{FromJs, IntoJs, IntoRust}, + Coin, CoinSpend, +}; + +#[napi] +pub struct Simulator(sdk::Simulator); + +#[napi] +impl Simulator { + #[napi(constructor)] + pub fn new() -> Self { + Self(sdk::Simulator::new()) + } + + #[napi] + pub fn new_coin(&mut self, puzzle_hash: Uint8Array, amount: BigInt) -> Result { + self.0 + .new_coin(puzzle_hash.into_rust()?, amount.into_rust()?) + .into_js() + } + + #[napi] + pub fn new_p2(&mut self, amount: BigInt) -> Result { + let (secret_key, public_key, puzzle_hash, coin) = self + .0 + .new_p2(amount.into_rust()?) + .map_err(|error| Error::from_reason(error.to_string()))?; + + Ok(P2Coin { + coin: coin.into_js()?, + puzzle_hash: puzzle_hash.into_js()?, + public_key: public_key.to_bytes().into_js()?, + secret_key: secret_key.to_bytes().into_js()?, + }) + } + + #[napi] + pub fn spend( + &mut self, + coin_spends: Vec, + secret_keys: Vec, + ) -> Result<()> { + self.0 + .spend_coins( + coin_spends + .into_iter() + .map(FromJs::from_js) + .collect::>>()?, + &secret_keys + .into_iter() + .map(|sk| { + SecretKey::from_bytes(&sk.into_rust()?) + .map_err(|error| Error::from_reason(error.to_string())) + }) + .collect::>>()?, + ) + .map_err(|error| Error::from_reason(error.to_string()))?; + Ok(()) + } +} + +#[napi(object)] +pub struct P2Coin { + pub coin: Coin, + pub puzzle_hash: Uint8Array, + pub public_key: Uint8Array, + pub secret_key: Uint8Array, +} diff --git a/napi/src/traits.rs b/napi/src/traits.rs new file mode 100644 index 00000000..ec77447c --- /dev/null +++ b/napi/src/traits.rs @@ -0,0 +1,235 @@ +use chia::{ + bls::PublicKey, + protocol::{Bytes, BytesImpl, Program}, +}; +use clvmr::NodePtr; +use napi::bindgen_prelude::*; + +pub(crate) trait IntoJs { + fn into_js(self) -> Result; +} + +pub(crate) trait FromJs { + fn from_js(js_value: T) -> Result + where + Self: Sized; +} + +pub(crate) trait IntoRust { + fn into_rust(self) -> Result; +} + +impl IntoRust for T +where + U: FromJs, +{ + fn into_rust(self) -> Result { + U::from_js(self) + } +} + +macro_rules! impl_primitive { + ( $( $ty:ty ),* ) => { + $( impl FromJs<$ty> for $ty { + fn from_js(value: $ty) -> Result { + Ok(value) + } + } + + impl IntoJs<$ty> for $ty { + fn into_js(self) -> Result { + Ok(self) + } + } )* + }; +} + +impl_primitive!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, f64); + +impl FromJs> for Vec +where + T: FromJs, +{ + fn from_js(js_value: Vec) -> Result { + js_value.into_iter().map(FromJs::from_js).collect() + } +} + +impl IntoJs> for Vec +where + F: IntoJs, +{ + fn into_js(self) -> Result> { + self.into_iter().map(IntoJs::into_js).collect() + } +} + +impl FromJs> for NodePtr { + fn from_js(program: ClassInstance) -> Result { + Ok(program.ptr) + } +} + +impl IntoJs for BytesImpl { + fn into_js(self) -> Result { + Ok(Uint8Array::new(self.to_vec())) + } +} + +impl FromJs for BytesImpl { + fn from_js(js_value: Uint8Array) -> Result { + Ok(Self::new(js_value.to_vec().try_into().map_err( + |bytes: Vec| { + Error::from_reason(format!("Expected length {N}, found {}", bytes.len())) + }, + )?)) + } +} + +impl IntoJs for [u8; N] { + fn into_js(self) -> Result { + Ok(Uint8Array::new(self.to_vec())) + } +} + +impl FromJs for [u8; N] { + fn from_js(js_value: Uint8Array) -> Result { + js_value.to_vec().try_into().map_err(|bytes: Vec| { + Error::from_reason(format!("Expected length {N}, found {}", bytes.len())) + }) + } +} + +impl IntoJs for Vec { + fn into_js(self) -> Result { + Ok(Uint8Array::new(self)) + } +} + +impl FromJs for Vec { + fn from_js(js_value: Uint8Array) -> Result { + Ok(js_value.to_vec()) + } +} + +impl IntoJs for Bytes { + fn into_js(self) -> Result { + Ok(Uint8Array::new(self.to_vec())) + } +} + +impl FromJs for Bytes { + fn from_js(js_value: Uint8Array) -> Result { + Ok(Bytes::from(js_value.to_vec())) + } +} + +impl IntoJs for PublicKey { + fn into_js(self) -> Result { + Ok(Uint8Array::new(self.to_bytes().to_vec())) + } +} + +impl FromJs for PublicKey { + fn from_js(js_value: Uint8Array) -> Result { + PublicKey::from_bytes(&js_value.into_rust()?) + .map_err(|error| Error::from_reason(error.to_string())) + } +} + +impl IntoJs for Program { + fn into_js(self) -> Result { + Ok(Uint8Array::new(self.to_vec())) + } +} + +impl FromJs for Program { + fn from_js(js_value: Uint8Array) -> Result { + Ok(Program::from(js_value.to_vec())) + } +} + +impl IntoJs for u64 { + fn into_js(self) -> Result { + Ok(BigInt::from(self)) + } +} + +impl FromJs for u64 { + fn from_js(js_value: BigInt) -> Result { + let (signed, value, lossless) = js_value.get_u64(); + + if signed || !lossless { + return Err(Error::from_reason("Expected u64")); + } + + Ok(value) + } +} + +impl FromJs for num_bigint::BigInt { + fn from_js(num: BigInt) -> Result { + if num.words.is_empty() { + return Ok(num_bigint::BigInt::ZERO); + } + + // Convert u64 words into a big-endian byte array + let bytes = words_to_bytes(&num.words); + + // Create the BigInt from the bytes + let bigint = num_bigint::BigInt::from_bytes_be( + if num.sign_bit { + num_bigint::Sign::Minus + } else { + num_bigint::Sign::Plus + }, + &bytes, + ); + + Ok(bigint) + } +} + +impl IntoJs for num_bigint::BigInt { + fn into_js(self) -> Result { + let (sign, bytes) = self.to_bytes_be(); + + // Convert the byte array into u64 words + let words = bytes_to_words(&bytes); + + Ok(BigInt { + sign_bit: sign == num_bigint::Sign::Minus, + words, + }) + } +} + +/// Helper function to convert Vec (words) into Vec (byte array) +fn words_to_bytes(words: &[u64]) -> Vec { + let mut bytes = Vec::with_capacity(words.len() * 8); + for word in words { + bytes.extend_from_slice(&word.to_be_bytes()); + } + + // Remove leading zeros from the byte array + while let Some(0) = bytes.first() { + bytes.remove(0); + } + + bytes +} + +/// Helper function to convert Vec (byte array) into Vec (words) +fn bytes_to_words(bytes: &[u8]) -> Vec { + let mut padded_bytes = vec![0u8; (8 - bytes.len() % 8) % 8]; + padded_bytes.extend_from_slice(bytes); + + let mut words = Vec::with_capacity(padded_bytes.len() / 8); + + for chunk in padded_bytes.chunks(8) { + let word = u64::from_be_bytes(chunk.try_into().unwrap()); + words.push(word); + } + + words +} diff --git a/napi/src/utils.rs b/napi/src/utils.rs new file mode 100644 index 00000000..54143241 --- /dev/null +++ b/napi/src/utils.rs @@ -0,0 +1,39 @@ +use clvmr::sha2::Sha256; +use napi::bindgen_prelude::*; + +use crate::traits::IntoJs; + +#[napi] +pub fn compare_bytes(a: Uint8Array, b: Uint8Array) -> bool { + a.as_ref() == b.as_ref() +} + +#[napi] +pub fn sha256(bytes: Uint8Array) -> Result { + let mut hasher = Sha256::new(); + hasher.update(bytes.as_ref()); + hasher.finalize().into_js() +} + +#[napi] +pub fn from_hex_raw(hex: String) -> Result { + let bytes = hex::decode(hex).map_err(|error| Error::from_reason(error.to_string()))?; + bytes.into_js() +} + +#[napi] +pub fn from_hex(hex: String) -> Result { + let mut hex = hex.as_str(); + + if let Some(stripped) = hex.strip_prefix("0x") { + hex = stripped; + } + + let bytes = hex::decode(hex).map_err(|error| Error::from_reason(error.to_string()))?; + bytes.into_js() +} + +#[napi] +pub fn to_hex(bytes: Uint8Array) -> String { + hex::encode(bytes.as_ref()) +} diff --git a/napi/tsconfig.json b/napi/tsconfig.json new file mode 100644 index 00000000..3ca73706 --- /dev/null +++ b/napi/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "types": ["node"], + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..beeb8acf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.81.0" +components = ["rustfmt", "clippy"] +targets = ["x86_64-apple-darwin", "x86_64-pc-windows-msvc", "x86_64-unknown-linux-gnu", "aarch64-apple-darwin"] diff --git a/src/address.rs b/src/address.rs new file mode 100644 index 00000000..7156f7e9 --- /dev/null +++ b/src/address.rs @@ -0,0 +1,170 @@ +use bech32::{u5, Variant}; +use hex::FromHexError; +use thiserror::Error; + +/// Errors you can get while trying to decode an address. +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum AddressError { + /// The address was encoded as bech32, rather than bech32m. + #[error("encoding is not bech32m")] + InvalidFormat, + + /// The data was not 32 bytes in length. + #[error("wrong length, expected 32 bytes but found {0}")] + WrongLength(usize), + + /// An error occured while trying to decode the address. + #[error("error when decoding address: {0}")] + Decode(#[from] bech32::Error), +} + +/// Errors you can get while trying to decode a puzzle hash. +#[derive(Error, Debug, Clone, PartialEq)] +pub enum PuzzleHashError { + /// The buffer was not 32 bytes in length. + #[error("wrong length, expected 32 bytes but found {0}")] + WrongLength(usize), + + /// An error occured while trying to decode the puzzle hash. + #[error("error when decoding puzzle hash: {0}")] + Decode(#[from] FromHexError), +} + +/// Decodes a puzzle hash from hex, with or without a prefix. +pub fn decode_puzzle_hash(puzzle_hash: &str) -> Result<[u8; 32], PuzzleHashError> { + let data = hex::decode(strip_prefix(puzzle_hash))?; + let length = data.len(); + data.try_into() + .map_err(|_| PuzzleHashError::WrongLength(length)) +} + +/// Encodes a puzzle hash into hex, with or without a prefix. +pub fn encode_puzzle_hash(puzzle_hash: [u8; 32], include_0x: bool) -> String { + if include_0x { + format!("0x{}", hex::encode(puzzle_hash)) + } else { + hex::encode(puzzle_hash) + } +} + +/// Decodes an address into a puzzle hash and HRP prefix. +pub fn decode_address(address: &str) -> Result<([u8; 32], String), AddressError> { + let (hrp, data, variant) = bech32::decode(address)?; + + if variant != Variant::Bech32m { + return Err(AddressError::InvalidFormat); + } + + let data = bech32::convert_bits(&data, 5, 8, false)?; + let length = data.len(); + let puzzle_hash = data + .try_into() + .map_err(|_| AddressError::WrongLength(length))?; + + Ok((puzzle_hash, hrp)) +} + +/// Encodes an address with a given HRP prefix. +pub fn encode_address(puzzle_hash: [u8; 32], prefix: &str) -> Result { + let data = bech32::convert_bits(&puzzle_hash, 8, 5, true) + .unwrap() + .into_iter() + .map(u5::try_from_u8) + .collect::, bech32::Error>>()?; + bech32::encode(prefix, data, Variant::Bech32m) +} + +/// Removes the `0x` prefix from a puzzle hash in hex format. +pub fn strip_prefix(puzzle_hash: &str) -> &str { + if let Some(puzzle_hash) = puzzle_hash.strip_prefix("0x") { + puzzle_hash + } else if let Some(puzzle_hash) = puzzle_hash.strip_prefix("0X") { + puzzle_hash + } else { + puzzle_hash + } +} + +#[cfg(test)] +mod tests { + use hex_literal::hex; + + use super::*; + + fn check_ph(expected: &str) { + let expected = strip_prefix(expected); + let puzzle_hash = decode_puzzle_hash(expected).unwrap(); + let actual = encode_puzzle_hash(puzzle_hash, false); + assert_eq!(actual, expected); + } + + fn check_addr(expected: &str) { + let (puzzle_hash, prefix) = decode_address(expected).unwrap(); + let actual = encode_address(puzzle_hash, &prefix).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn test_strip_prefix() { + check_ph("0x2999682870bd24e7fd0ef6324c69794ff93fc41b016777d2edd5ea8575bdaa31"); + check_ph("0x99619cc6888f1bd30acd6e8c1f4065dafeba2246bfc3465cddda4e6656083791"); + check_ph("0X7cc6494dd96d32c97b5f6ba77caae269acd6c86593ada66f343050ce709e904a"); + check_ph("0X9f057817ad576b24ec60a25ded08f5bde6db0aa0beeb0c099e3ce176866e1c4b"); + } + + #[test] + fn test_puzzle_hashes() { + check_ph(&hex::encode([0; 32])); + check_ph(&hex::encode([255; 32])); + check_ph(&hex::encode([127; 32])); + check_ph(&hex::encode([1; 32])); + check_ph("f46ec440aeb9b3baa19968810a8537ec4ff406c09c994dd7d3222b87258a52ff"); + check_ph("2f981b2f9510ef9e62523e6b38fc933e2f060c411cfa64906413ddfd56be8dc1"); + check_ph("3e09bdd6b19659555a7c8456c5af54d004d774f3d44689360d4778ce685201ad"); + check_ph("d16c2ad7c5642532659e424dc0d7e4a85779c6dab801b5e6117a8c8587156472"); + } + + #[test] + fn test_invalid_puzzle_hashes() { + assert_eq!( + decode_puzzle_hash("ac4fd55996a1186fffc30c5b60385a88fd78d538f1c9febbfa9c8a9e9a170ad"), + Err(PuzzleHashError::Decode(FromHexError::OddLength)) + ); + assert_eq!( + decode_puzzle_hash(&hex::encode(hex!( + " + dfe399911acc4426f44bf31f4d817f6b69f244bbad138a28 + 25c05550f7d2ab70c35408f764281febd624ac8cdfc91817 + " + ))), + Err(PuzzleHashError::WrongLength(48)) + ); + assert_eq!( + decode_puzzle_hash("hello there!"), + Err(PuzzleHashError::Decode(FromHexError::InvalidHexCharacter { + c: 'h', + index: 0 + })) + ); + } + + #[test] + fn test_addresses() { + check_addr("xch1a0t57qn6uhe7tzjlxlhwy2qgmuxvvft8gnfzmg5detg0q9f3yc3s2apz0h"); + check_addr("xch1ftxk2v033kv94ueucp0a34sgt9398vle7l7g3q9k4leedjmmdysqvv6q96"); + check_addr("xch1ay273ctc9c6nxmzmzsup28scrce8ney84j4nlewdlaxqs22v53ksxgf38f"); + check_addr("xch1avnwmy2fuesq7h2jnxehlrs9msrad9uuvrhms35k2pqwmjv56y5qk7zm6v"); + } + + #[test] + fn test_invalid_addresses() { + assert_eq!( + decode_address("hello there!"), + Err(AddressError::Decode(bech32::Error::MissingSeparator)) + ); + assert_eq!( + decode_address("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"), + Err(AddressError::InvalidFormat) + ); + } +} diff --git a/src/coin_selection.rs b/src/coin_selection.rs new file mode 100644 index 00000000..32af41f4 --- /dev/null +++ b/src/coin_selection.rs @@ -0,0 +1,241 @@ +use std::cmp::Reverse; + +use chia_protocol::Coin; +use indexmap::IndexSet; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; +use thiserror::Error; + +/// An error that occurs when selecting coins. +#[derive(Debug, Clone, Error, PartialEq, Eq)] +pub enum CoinSelectionError { + /// There were no spendable coins to select from. + #[error("no spendable coins")] + NoSpendableCoins, + + /// There weren't enough coins to reach the amount. + #[error("insufficient balance {0}")] + InsufficientBalance(u128), + + /// The selected coins exceeded the maximum. + #[error("exceeded max coins")] + ExceededMaxCoins, +} + +/// Uses the knapsack algorithm to select coins. +pub fn select_coins( + mut spendable_coins: Vec, + amount: u128, +) -> Result, CoinSelectionError> { + let max_coins = 500; + + // You cannot spend no coins. + if spendable_coins.is_empty() { + return Err(CoinSelectionError::NoSpendableCoins); + } + + // Checks to ensure the balance is sufficient before continuing. + let spendable_amount = spendable_coins + .iter() + .fold(0u128, |acc, coin| acc + coin.amount as u128); + + if spendable_amount < amount { + return Err(CoinSelectionError::InsufficientBalance(spendable_amount)); + } + + // Sorts by amount, descending. + spendable_coins.sort_unstable_by_key(|coin| Reverse(coin.amount)); + + // Exact coin match. + for coin in spendable_coins.iter() { + if coin.amount as u128 == amount { + return Ok(vec![*coin]); + } + } + + let mut smaller_coins = IndexSet::new(); + let mut smaller_sum = 0; + + for coin in spendable_coins.iter() { + let coin_amount = coin.amount as u128; + + if coin_amount < amount { + smaller_coins.insert(*coin); + smaller_sum += coin_amount; + } + } + + // Check for an exact match. + if smaller_sum == amount && smaller_coins.len() < max_coins && amount != 0 { + return Ok(smaller_coins.into_iter().collect()); + } + + // There must be a single coin larger than the amount. + if smaller_sum < amount { + return Ok(vec![smallest_coin_above(&spendable_coins, amount).unwrap()]); + } + + // Apply the knapsack algorithm otherwise. + if smaller_sum > amount { + if let Some(result) = knapsack_coin_algorithm( + &mut ChaCha8Rng::seed_from_u64(0), + &spendable_coins, + amount, + u128::MAX, + max_coins, + ) { + return Ok(result.into_iter().collect()); + } + + // Knapsack failed to select coins, so try summing the largest coins. + let summed_coins = sum_largest_coins(&spendable_coins, amount); + + if summed_coins.len() <= max_coins { + return Ok(summed_coins.into_iter().collect()); + } else { + return Err(CoinSelectionError::ExceededMaxCoins); + } + } + + // Try to find a large coin to select. + if let Some(coin) = smallest_coin_above(&spendable_coins, amount) { + return Ok(vec![coin]); + } + + // It would require too many coins to match the amount. + Err(CoinSelectionError::ExceededMaxCoins) +} + +fn sum_largest_coins(coins: &[Coin], amount: u128) -> IndexSet { + let mut selected_coins = IndexSet::new(); + let mut selected_sum = 0; + for coin in coins { + selected_sum += coin.amount as u128; + selected_coins.insert(*coin); + + if selected_sum >= amount { + return selected_coins; + } + } + unreachable!() +} + +fn smallest_coin_above(coins: &[Coin], amount: u128) -> Option { + if (coins[0].amount as u128) < amount { + return None; + } + for coin in coins.iter().rev() { + if (coin.amount as u128) >= amount { + return Some(*coin); + } + } + unreachable!(); +} + +/// Runs the knapsack algorithm on a set of coins, attempting to find an optimal set. +pub fn knapsack_coin_algorithm( + rng: &mut impl Rng, + spendable_coins: &[Coin], + amount: u128, + max_amount: u128, + max_coins: usize, +) -> Option> { + let mut best_sum = max_amount; + let mut best_coins = None; + + for _ in 0..1000 { + let mut selected_coins = IndexSet::new(); + let mut selected_sum = 0; + let mut target_reached = false; + + for pass in 0..2 { + if target_reached { + break; + } + + for coin in spendable_coins { + let filter_first = pass != 0 || !rng.gen::(); + let filter_second = pass != 1 || selected_coins.contains(coin); + + if filter_first && filter_second { + continue; + } + + if selected_coins.len() > max_coins { + break; + } + + selected_sum += coin.amount as u128; + selected_coins.insert(*coin); + + if selected_sum == amount { + return Some(selected_coins); + } + + if selected_sum > amount { + target_reached = true; + + if selected_sum < best_sum { + best_sum = selected_sum; + best_coins = Some(selected_coins.clone()); + + selected_sum -= coin.amount as u128; + selected_coins.shift_remove(coin); + } + } + } + } + } + + best_coins +} + +#[cfg(test)] +mod tests { + use chia_protocol::Bytes32; + + use super::*; + + macro_rules! coin_list { + ( $( $coin:expr ),* $(,)? ) => { + vec![$( coin($coin) ),*] + }; + } + + fn coin(amount: u64) -> Coin { + Coin::new(Bytes32::from([0; 32]), Bytes32::from([0; 32]), amount) + } + + #[test] + fn test_select_coins() { + let coins = coin_list![100, 200, 300, 400, 500]; + + // Sorted by amount, ascending. + let selected = select_coins(coins, 700).unwrap(); + let expected = coin_list![400, 300]; + assert_eq!(selected, expected); + } + + #[test] + fn test_insufficient_balance() { + let coins = coin_list![50, 250, 100000]; + + // Select an amount that is too high. + let selected = select_coins(coins, 9999999); + assert_eq!( + selected, + Err(CoinSelectionError::InsufficientBalance(100300)) + ); + } + + #[test] + fn test_no_coins() { + // There is no amount to select from. + let selected = select_coins(Vec::new(), 100); + assert_eq!(selected, Err(CoinSelectionError::NoSpendableCoins)); + + // Even if the amount is zero, this should fail. + let selected = select_coins(Vec::new(), 0); + assert_eq!(selected, Err(CoinSelectionError::NoSpendableCoins)); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8b137891..26fb01cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,14 @@ +#![doc = include_str!("../README.md")] +mod address; +mod coin_selection; + +pub use address::*; +pub use coin_selection::*; + +pub use chia_sdk_client::*; +pub use chia_sdk_driver::*; +pub use chia_sdk_offers::*; +pub use chia_sdk_signer::*; +pub use chia_sdk_test::*; +pub use chia_sdk_types::*;