diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..0f07734 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,8 @@ +[resolver] +incompatible-rust-versions = "fallback" + +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" + +[target.armv7-unknown-linux-gnueabihf] +linker = "arm-linux-gnueabihf-gcc" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..083ac61 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,37 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "UTC" + + groups: + all-dependencies: + patterns: + - "*" + + cooldown: + default-days: 7 + + open-pull-requests-limit: 1 + + versioning-strategy: "auto" + + labels: + - "dependencies" + - "rust" + + allow: + - dependency-type: "all" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddffd74..3b0ca26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,12 @@ on: tagged-release: required: true type: boolean + python-version-1: + required: true + type: string + python-version-2: + required: true + type: string jobs: build: @@ -24,27 +30,25 @@ jobs: strategy: fail-fast: false matrix: - bitcoind-version: ["29.0"] + bitcoind-version: ["31.0"] experimental: [1] deprecated: [0] - python-version: ["3.9", "3.12"] - os: ["ubuntu-24.04"] + python-version: [ "${{ inputs.python-version-1 }}", "${{ inputs.python-version-2 }}"] + os: ["ubuntu-latest"] runs-on: ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Create cache paths run: | - sudo mkdir /usr/local/libexec - sudo mkdir /usr/local/libexec/c-lightning - sudo mkdir /usr/local/libexec/c-lightning/plugins + sudo mkdir -p /usr/local/libexec/c-lightning/plugins sudo chown -R $USER /usr/local/libexec - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} @@ -58,120 +62,104 @@ jobs: echo "OS version: $OS_VERSION" echo "os_version=$OS_VERSION" >> $GITHUB_OUTPUT - - name: Cache CLN + - name: Restore CLN cache id: cache-cln - uses: actions/cache@v4 + uses: actions/cache/restore@v6 with: path: | /usr/local/bin/lightning* /usr/local/libexec/c-lightning key: cache-cln-${{ inputs.cln-version }}-${{ steps.exact_versions.outputs.os_version }} - - name: Cache bitcoind + - name: Restore bitcoind cache id: cache-bitcoind - uses: actions/cache@v4 + uses: actions/cache/restore@v6 with: path: /usr/local/bin/bitcoin* key: cache-bitcoind-${{ matrix.bitcoind-version }}-${{ steps.exact_versions.outputs.os_version }} - - name: Cache python dependencies - id: cache-python - uses: actions/cache@v4 + - name: Install uv + uses: astral-sh/setup-uv@v7 with: - path: venv - key: cache-python-${{ steps.exact_versions.outputs.python_version }}-${{ steps.exact_versions.outputs.os_version }}-${{ inputs.pyln-version }}-${{ hashFiles('tests/requirements.txt') }} + enable-cache: true + cache-dependency-glob: "**/pyproject.toml" - name: Download Bitcoin ${{ matrix.bitcoind-version }} & install binaries if: ${{ steps.cache-bitcoind.outputs.cache-hit != 'true' }} run: | export BITCOIND_VERSION=${{ matrix.bitcoind-version }} - if [[ "${{ matrix.os }}" =~ "ubuntu" ]]; then - export TARGET_ARCH="x86_64-linux-gnu" - fi - if [[ "${{ matrix.os }}" =~ "macos" ]]; then - export TARGET_ARCH="x86_64-apple-darwin" - fi - wget https://bitcoincore.org/bin/bitcoin-core-${BITCOIND_VERSION}/bitcoin-${BITCOIND_VERSION}-${TARGET_ARCH}.tar.gz + export TARGET_ARCH="x86_64-linux-gnu" + wget --tries=5 --waitretry=5 --retry-connrefused --timeout=20 --continue https://bitcoincore.org/bin/bitcoin-core-${BITCOIND_VERSION}/bitcoin-${BITCOIND_VERSION}-${TARGET_ARCH}.tar.gz tar -xzf bitcoin-${BITCOIND_VERSION}-${TARGET_ARCH}.tar.gz sudo mv bitcoin-${BITCOIND_VERSION}/bin/* /usr/local/bin rm -rf bitcoin-${BITCOIND_VERSION}-${TARGET_ARCH}.tar.gz bitcoin-${BITCOIND_VERSION} + - name: Save bitcoind cache + id: save-cache-bitcoind + if : ${{ success() && steps.cache-bitcoind.outputs.cache-hit != 'true'}} + uses: actions/cache/save@v6 + with: + path: /usr/local/bin/bitcoin* + key: ${{ steps.cache-bitcoind.outputs.cache-primary-key }} + - name: Download Core Lightning ${{ inputs.cln-version }} & install binaries if: ${{ contains(matrix.os, 'ubuntu') && steps.cache-cln.outputs.cache-hit != 'true' }} run: | url=$(curl -f --retry 3 --retry-delay 3 -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/ElementsProject/lightning/releases/tags/${{ inputs.cln-version }} \ | jq '.assets[] | select(.name | contains("24.04")) | .browser_download_url' \ | tr -d '\"') - wget $url + wget --tries=5 --waitretry=5 --retry-connrefused --timeout=20 --continue $url sudo tar -xvf ${url##*/} -C /usr/local --strip-components=2 echo "CLN_VERSION=$(lightningd --version)" >> "$GITHUB_OUTPUT" + - name: Save CLN cache + id: save-cache-cln + if : ${{ success() && steps.cache-cln.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v6 + with: + path: | + /usr/local/bin/lightning* + /usr/local/libexec/c-lightning + key: ${{ steps.cache-cln.outputs.cache-primary-key }} + - name: Set up Rust - if: ${{ inputs.tagged-release == false}} + if: ${{ inputs.tagged-release == false }} uses: dtolnay/rust-toolchain@stable - - name: Set up protoc - if: ${{ inputs.tagged-release == false}} || contains(matrix.os, 'macos') && ${{ steps.cache-cln.outputs.cache-hit != 'true' }} - uses: arduino/setup-protoc@v3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install cargo-binstall + if: ${{ inputs.tagged-release == false }} + uses: taiki-e/install-action@cargo-binstall - - name: Checkout Core Lightning ${{ inputs.cln-version }} - if: ${{ contains(matrix.os, 'macos') && steps.cache-cln.outputs.cache-hit != 'true' }} - uses: actions/checkout@v4 - with: - repository: 'ElementsProject/lightning' - path: 'lightning' - ref: ${{ inputs.cln-version }} - submodules: 'recursive' + - name: Install cargo-msrv + if: ${{ inputs.tagged-release == false }} + env: + GITHUB_TOKEN: ${{ github.token }} + run: cargo binstall --no-confirm cargo-msrv - - name: Install System dependencies - run: | - if [[ "${{ matrix.os }}" =~ "macos" ]]; then - brew install autoconf automake libtool gnu-sed gettext libsodium sqlite - fi - - - name: Install Python dependencies - if: ${{ steps.cache-python.outputs.cache-hit != 'true' }} - run: | - python -m venv venv - source venv/bin/activate - python -m pip install -U pip poetry wheel - pip3 install "pyln-proto<=${{ inputs.pyln-version }}" "pyln-client<=${{ inputs.pyln-version }}" "pyln-testing<=${{ inputs.pyln-version }}" - pip3 install pytest-xdist pytest-test-groups pytest-timeout - if [ -f "tests/requirements.txt" ]; then - pip3 install -r tests/requirements.txt - fi - - - name: Compile Core Lightning ${{ inputs.cln-version }} & install binaries - if: ${{ contains(matrix.os, 'macos') && steps.cache-cln.outputs.cache-hit != 'true' }} - run: | - export EXPERIMENTAL_FEATURES=${{ matrix.experimental }} - export COMPAT=${{ matrix.deprecated }} - export VALGRIND=0 - source venv/bin/activate - - cd lightning - - poetry lock - poetry install - ./configure --disable-valgrind - poetry run make - sudo make install + - name: Verify MSRV + if: ${{ inputs.tagged-release == false }} + run: cargo msrv verify - name: Get plugin binary run: | - source venv/bin/activate if ${{ inputs.tagged-release }}; then cd tests ./setup.sh cd .. else if [ -d "proto" ]; then - python -m grpc_tools.protoc --proto_path="proto" --python_out="tests" --grpc_python_out="tests" proto/*.proto + uv run python -m grpc_tools.protoc --proto_path="proto" --python_out="tests" --grpc_python_out="tests" proto/*.proto fi + url=$(curl -f --retry 3 --retry-delay 3 -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/BoltzExchange/hold/releases/latest \ + | jq '.assets[] | select(.name | endswith("linux-amd64.tar.gz")) | .browser_download_url' \ + | tr -d '\"') + wget $url + tar -xvf ${url##*/} -C tests + mv "tests/build/hold-linux-amd64" "tests/hold" + cargo build + cargo test fi - name: Run tests @@ -184,5 +172,8 @@ jobs: export TRAVIS=1 export VALGRIND=0 export PYTEST_TIMEOUT=600 - source venv/bin/activate - pytest -n=5 tests/test_*.py + + cd tests + tar -xf nostr-rs-relay.tar.gz + uv add --dev pyln-testing==${{ inputs.pyln-version }} pyln-client==${{ inputs.pyln-version }} pyln-proto==${{ inputs.pyln-version }} + uv run -v pytest -n=10 diff --git a/.github/workflows/latest_v24.08.yml b/.github/workflows/latest_v24.08.yml deleted file mode 100644 index 1db4db9..0000000 --- a/.github/workflows/latest_v24.08.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Latest release on CLN v24.08.x - -on: - release: - types: [published, edited] - workflow_dispatch: - -jobs: - call-ci: - uses: ./.github/workflows/ci.yml - with: - cln-version: "v24.08.2" - pyln-version: "24.8.2" - tagged-release: true \ No newline at end of file diff --git a/.github/workflows/latest_v24.11.yml b/.github/workflows/latest_v24.11.yml deleted file mode 100644 index 7174254..0000000 --- a/.github/workflows/latest_v24.11.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Latest release on CLN v24.11.x - -on: - release: - types: [published, edited] - workflow_dispatch: - -jobs: - call-ci: - uses: ./.github/workflows/ci.yml - with: - cln-version: "v24.11.2" - pyln-version: "24.11.1" - tagged-release: true \ No newline at end of file diff --git a/.github/workflows/latest_v25.02.yml b/.github/workflows/latest_v25.02.yml deleted file mode 100644 index 6d8590c..0000000 --- a/.github/workflows/latest_v25.02.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Latest release on CLN v25.02.x - -on: - release: - types: [published, edited] - workflow_dispatch: - -jobs: - call-ci: - uses: ./.github/workflows/ci.yml - with: - cln-version: "v25.02.1" - pyln-version: "25.2.1" - tagged-release: true \ No newline at end of file diff --git a/.github/workflows/latest_v25.05.yml b/.github/workflows/latest_v25.05.yml deleted file mode 100644 index b5502c9..0000000 --- a/.github/workflows/latest_v25.05.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Latest release on CLN v25.05.x - -on: - release: - types: [published, edited] - workflow_dispatch: - -jobs: - call-ci: - uses: ./.github/workflows/ci.yml - with: - cln-version: "v25.05" - pyln-version: "25.5" - tagged-release: true \ No newline at end of file diff --git a/.github/workflows/latest_v25.09.yml b/.github/workflows/latest_v25.09.yml new file mode 100644 index 0000000..d96d48d --- /dev/null +++ b/.github/workflows/latest_v25.09.yml @@ -0,0 +1,16 @@ +name: Latest release on CLN v25.09.x + +on: + release: + types: [published, edited] + workflow_dispatch: + +jobs: + call-ci: + uses: ./.github/workflows/ci.yml + with: + cln-version: "v25.09.3" + pyln-version: "25.9.3" + tagged-release: true + python-version-1: "3.10" + python-version-2: "3.12" diff --git a/.github/workflows/latest_v25.12.yml b/.github/workflows/latest_v25.12.yml new file mode 100644 index 0000000..3a86b18 --- /dev/null +++ b/.github/workflows/latest_v25.12.yml @@ -0,0 +1,16 @@ +name: Latest release on CLN v25.12.x + +on: + release: + types: [published, edited] + workflow_dispatch: + +jobs: + call-ci: + uses: ./.github/workflows/ci.yml + with: + cln-version: "v25.12.1" + pyln-version: "25.12.1" + tagged-release: true + python-version-1: "3.10" + python-version-2: "3.12" diff --git a/.github/workflows/latest_v26.04.yml b/.github/workflows/latest_v26.04.yml new file mode 100644 index 0000000..a0dc3a0 --- /dev/null +++ b/.github/workflows/latest_v26.04.yml @@ -0,0 +1,16 @@ +name: Latest release on CLN v26.04.x + +on: + release: + types: [published, edited] + workflow_dispatch: + +jobs: + call-ci: + uses: ./.github/workflows/ci.yml + with: + cln-version: "v26.04" + pyln-version: "26.4" + tagged-release: true + python-version-1: "3.10" + python-version-2: "3.12" diff --git a/.github/workflows/latest_v26.06.yml b/.github/workflows/latest_v26.06.yml new file mode 100644 index 0000000..27edc2d --- /dev/null +++ b/.github/workflows/latest_v26.06.yml @@ -0,0 +1,16 @@ +name: Latest release on CLN v26.06.x + +on: + release: + types: [published, edited] + workflow_dispatch: + +jobs: + call-ci: + uses: ./.github/workflows/ci.yml + with: + cln-version: "v26.06.6" + pyln-version: "26.6.6" + tagged-release: true + python-version-1: "3.10" + python-version-2: "3.14" diff --git a/.github/workflows/main_v24.11.yml b/.github/workflows/master_v25.09.yml similarity index 61% rename from .github/workflows/main_v24.11.yml rename to .github/workflows/master_v25.09.yml index 7078bd5..1a73ecd 100644 --- a/.github/workflows/main_v24.11.yml +++ b/.github/workflows/master_v25.09.yml @@ -1,9 +1,9 @@ -name: main on CLN v24.11.x +name: master on CLN v25.09.x on: push: branches: - - main + - master paths-ignore: - 'tools/**' - 'CHANGELOG.md' @@ -19,6 +19,8 @@ jobs: call-ci: uses: ./.github/workflows/ci.yml with: - cln-version: "v24.11.2" - pyln-version: "24.11.1" - tagged-release: false \ No newline at end of file + cln-version: "v25.09.3" + pyln-version: "25.9.3" + tagged-release: false + python-version-1: "3.10" + python-version-2: "3.12" diff --git a/.github/workflows/main_v25.05.yml b/.github/workflows/master_v25.12.yml similarity index 61% rename from .github/workflows/main_v25.05.yml rename to .github/workflows/master_v25.12.yml index e99fd3f..7e0ae62 100644 --- a/.github/workflows/main_v25.05.yml +++ b/.github/workflows/master_v25.12.yml @@ -1,9 +1,9 @@ -name: main on CLN v25.05.x +name: master on CLN v25.12.x on: push: branches: - - main + - master paths-ignore: - 'tools/**' - 'CHANGELOG.md' @@ -19,6 +19,8 @@ jobs: call-ci: uses: ./.github/workflows/ci.yml with: - cln-version: "v25.05" - pyln-version: "25.5" - tagged-release: false \ No newline at end of file + cln-version: "v25.12.1" + pyln-version: "25.12.1" + tagged-release: false + python-version-1: "3.10" + python-version-2: "3.12" diff --git a/.github/workflows/main_v24.08.yml b/.github/workflows/master_v26.04.yml similarity index 61% rename from .github/workflows/main_v24.08.yml rename to .github/workflows/master_v26.04.yml index 1036fbb..82715be 100644 --- a/.github/workflows/main_v24.08.yml +++ b/.github/workflows/master_v26.04.yml @@ -1,9 +1,9 @@ -name: main on CLN v24.08.x +name: master on CLN v26.04.x on: push: branches: - - main + - master paths-ignore: - 'tools/**' - 'CHANGELOG.md' @@ -19,6 +19,8 @@ jobs: call-ci: uses: ./.github/workflows/ci.yml with: - cln-version: "v24.08.2" - pyln-version: "24.8.2" - tagged-release: false \ No newline at end of file + cln-version: "v26.04" + pyln-version: "26.4" + tagged-release: false + python-version-1: "3.10" + python-version-2: "3.12" diff --git a/.github/workflows/main_v25.02.yml b/.github/workflows/master_v26.06.yml similarity index 61% rename from .github/workflows/main_v25.02.yml rename to .github/workflows/master_v26.06.yml index 1136903..25fe3a8 100644 --- a/.github/workflows/main_v25.02.yml +++ b/.github/workflows/master_v26.06.yml @@ -1,9 +1,9 @@ -name: main on CLN v25.02.x +name: master on CLN v26.06.x on: push: branches: - - main + - master paths-ignore: - 'tools/**' - 'CHANGELOG.md' @@ -19,6 +19,8 @@ jobs: call-ci: uses: ./.github/workflows/ci.yml with: - cln-version: "v25.02.1" - pyln-version: "25.2.1" - tagged-release: false \ No newline at end of file + cln-version: "v26.06.6" + pyln-version: "26.6.6" + tagged-release: false + python-version-1: "3.10" + python-version-2: "3.14" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4fbf8d..39f2ae2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,77 +5,129 @@ on: - 'v*' jobs: - build: - name: build release binaries on ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: ["ubuntu-24.04"] - runs-on: ${{ matrix.os }} - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Install rust - id: rust - uses: dtolnay/rust-toolchain@stable - - name: Install cross - if: contains(matrix.os, 'ubuntu') - run: | - cargo install cross --git https://github.com/cross-rs/cross - - name: Build unix - id: unix_build - if: contains(matrix.os, 'ubuntu') - run: | - cross build --profile optimized --locked --target x86_64-unknown-linux-gnu - cross build --profile optimized --locked --target armv7-unknown-linux-gnueabihf - cross build --profile optimized --locked --target aarch64-unknown-linux-gnu - tar -czf "${{ github.event.repository.name }}-${{github.ref_name}}-aarch64-linux-gnu.tar.gz" --transform 's|.*/||' "target/aarch64-unknown-linux-gnu/optimized/${{ github.event.repository.name }}" - tar -czf "${{ github.event.repository.name }}-${{github.ref_name}}-armv7-linux-gnueabihf.tar.gz" --transform 's|.*/||' "target/armv7-unknown-linux-gnueabihf/optimized/${{ github.event.repository.name }}" - tar -czf "${{ github.event.repository.name }}-${{github.ref_name}}-x86_64-linux-gnu.tar.gz" --transform 's|.*/||' "target/x86_64-unknown-linux-gnu/optimized/${{ github.event.repository.name }}" - ls -alh - - name: Build macos - id: macos_build - if: contains(matrix.os, 'macos') - run: | - rustup target add aarch64-apple-darwin - export CROSSBUILD_MACOS_SDK="macosx13.1" - export SDKROOT=$(xcrun -sdk $CROSSBUILD_MACOS_SDK --show-sdk-path) - export MACOSX_DEPLOYMENT_TARGET=12.0 - cargo build --profile optimized --locked --target=x86_64-apple-darwin - cargo build --profile optimized --locked --target=aarch64-apple-darwin - lipo -create -output target/${{ github.event.repository.name }} target/aarch64-apple-darwin/optimized/${{ github.event.repository.name }} target/x86_64-apple-darwin/optimized/${{ github.event.repository.name }} - ditto -c -k --sequesterRsrc target/${{ github.event.repository.name }} ${{ github.event.repository.name }}-${{github.ref_name}}-universal-apple-darwin.zip - otool -l target/aarch64-apple-darwin/optimized/${{ github.event.repository.name }} | grep -A 5 LC_BUILD_VERSION - otool -l target/x86_64-apple-darwin/optimized/${{ github.event.repository.name }} | grep -A 5 LC_BUILD_VERSION - echo "macos_version=$MACOSX_DEPLOYMENT_TARGET" >> "$GITHUB_OUTPUT" - echo $(xcodebuild -showsdks) - ls -alh - - name: Upload unix artifacts - uses: actions/upload-artifact@v4 - if: contains(matrix.os, 'ubuntu') - with: - name: unix-binaries - path: | - ${{ github.event.repository.name }}-${{github.ref_name}}-*.tar.gz - - name: Upload macos artifacts - uses: actions/upload-artifact@v4 - if: contains(matrix.os, 'macos') - with: - name: macos-binaries - path: | - ${{ github.event.repository.name }}-${{github.ref_name}}-universal-apple-darwin.zip - - name: Get rust version - id: rversion - run: | - echo "rust_version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" - outputs: - rust-version: ${{ steps.rversion.outputs.rust_version }} - macos-version: ${{ steps.macos_build.outputs.macos_version }} + build-linux: + name: Build Linux binaries (glibc) + runs-on: ubuntu-22.04 + outputs: + glibc-version: ${{ steps.versions.outputs.glibc-version }} + rust-version: ${{ steps.versions.outputs.rust-version }} + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, armv7-unknown-linux-gnueabihf + + - name: Install cross-compilation dependencies + run: | + sudo apt update + sudo apt install -y gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf + + - name: Build Linux binaries (release) + run: | + cargo build --profile optimized --locked --target x86_64-unknown-linux-gnu + cargo build --profile optimized --locked --target aarch64-unknown-linux-gnu + cargo build --profile optimized --locked --target armv7-unknown-linux-gnueabihf + + tar -czf "${{ github.event.repository.name }}-${{github.ref_name}}-aarch64-linux-gnu.tar.gz" --transform 's|.*/||' "target/aarch64-unknown-linux-gnu/optimized/${{ github.event.repository.name }}" + tar -czf "${{ github.event.repository.name }}-${{github.ref_name}}-armv7-linux-gnueabihf.tar.gz" --transform 's|.*/||' "target/armv7-unknown-linux-gnueabihf/optimized/${{ github.event.repository.name }}" + tar -czf "${{ github.event.repository.name }}-${{github.ref_name}}-x86_64-linux-gnu.tar.gz" --transform 's|.*/||' "target/x86_64-unknown-linux-gnu/optimized/${{ github.event.repository.name }}" + + ls -alh *.tar.gz + + - name: Get versions + id: versions + run: | + echo "rust-version=$(rustc --version | awk '{print $2}')" >> "$GITHUB_OUTPUT" + GLIBC_VER=$(ldd --version | awk '/ldd/{print $NF}') + echo "glibc-version=$GLIBC_VER" >> "$GITHUB_OUTPUT" + + - name: Upload Linux artifacts + uses: actions/upload-artifact@v7 + with: + name: linux-binaries + path: | + ${{ github.event.repository.name }}-${{ github.ref_name }}-*.tar.gz + build-macos: + name: Build macOS universal binary + runs-on: macos-latest + outputs: + macos-deployment-target: ${{ steps.build.outputs.macos-deployment-target }} + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-apple-darwin, aarch64-apple-darwin + + - name: Build macOS universal (release) + id: build + env: + MACOSX_DEPLOYMENT_TARGET: "11.0" + run: | + cargo build --profile optimized --locked --target x86_64-apple-darwin + cargo build --profile optimized --locked --target aarch64-apple-darwin + + BINARY_NAME="${{ github.event.repository.name }}" + TAG="${{ github.ref_name }}" + + lipo -create -output "${BINARY_NAME}" \ + target/x86_64-apple-darwin/optimized/"${BINARY_NAME}" \ + target/aarch64-apple-darwin/optimized/"${BINARY_NAME}" + + zip "${BINARY_NAME}-${TAG}-universal-apple-darwin.zip" "${BINARY_NAME}" + + echo "macos-deployment-target=$MACOSX_DEPLOYMENT_TARGET" >> "$GITHUB_OUTPUT" + + ls -alh *.zip + + - name: Verify macOS deployment target + env: + EXPECTED_TARGET: ${{ steps.build.outputs.macos-deployment-target }} + run: | + BINARY="${{ github.event.repository.name }}" + + echo "Verifying deployment target for universal binary: $BINARY" + echo "Expected minimum macOS version: $EXPECTED_TARGET" + echo + + # Check x86_64 slice + X86_MINOS=$(otool -l -arch x86_64 "$BINARY" | awk ' + /LC_BUILD_VERSION/ || /LC_VERSION_MIN_MACOSX/ {getline; getline; getline; if ($1 == "minos") print $2} + ') + echo "x86_64 slice minos: $X86_MINOS" + + # Check arm64 slice + ARM64_MINOS=$(otool -l -arch arm64 "$BINARY" | awk ' + /LC_BUILD_VERSION/ || /LC_VERSION_MIN_MACOSX/ {getline; getline; getline; if ($1 == "minos") print $2} + ') + echo "arm64 slice minos: $ARM64_MINOS" + + # Fail if either doesn't match + if [ "$X86_MINOS" != "$EXPECTED_TARGET" ] || [ "$ARM64_MINOS" != "$EXPECTED_TARGET" ]; then + echo "ERROR: Deployment target mismatch!" + echo "Expected: $EXPECTED_TARGET" + echo "Got: x86_64=$X86_MINOS, arm64=$ARM64_MINOS" + exit 1 + fi + + echo "Success: Both architectures have deployment target $EXPECTED_TARGET" + + - name: Upload macOS artifacts + uses: actions/upload-artifact@v7 + with: + name: macos-binaries + path: | + ${{ github.event.repository.name }}-${{ github.ref_name }}-universal-apple-darwin.zip release: name: Github Release - needs: [build] - runs-on: "ubuntu-24.04" + needs: [build-linux, build-macos] + runs-on: "ubuntu-latest" permissions: contents: write steps: @@ -83,7 +135,7 @@ jobs: id: tag_name run: echo "current_version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Get Changelog Entry id: changelog_reader uses: mindsers/changelog-reader-action@v2 @@ -92,7 +144,7 @@ jobs: version: ${{ steps.tag_name.outputs.current_version }} path: ./CHANGELOG.md - name: Download Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: merge-multiple: true - name: Release @@ -100,5 +152,6 @@ jobs: with: allowUpdates: false artifactErrorsFailBuild: true - body: "${{ steps.changelog_reader.outputs.changes }} \n\n### Release binaries info\n\n- Release binaries were built using rust ${{ needs.build.outputs.rust-version }}\n- Linux release binaries require glibc>=2.31" - artifacts: "${{ github.event.repository.name }}-${{github.ref_name}}-*.tar.gz" + body: "${{ steps.changelog_reader.outputs.changes }} \n\n### Release binaries info\n\n- Release binaries were built using rust ${{ needs.build-linux.outputs.rust-version }}\n- Linux release binaries require glibc>=${{ needs.build-linux.outputs.glibc-version }} (check yours with ``ldd --version``)\n- macOS universal binary (x86_64 + aarch64), requires macOS >= ${{ needs.build-macos.outputs.macos-deployment-target }} (deployment target)" + artifacts: "${{ github.event.repository.name }}-${{github.ref_name}}-*.tar.gz,${{ github.event.repository.name }}-${{github.ref_name}}-*.zip" + immutableCreate: true diff --git a/.gitignore b/.gitignore index 026fddc..9f63775 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ /tests/__pycache__/ /venv/ /result/ -/tests/holdinvoice \ No newline at end of file +/tests/holdinvoice +/tests/hold +/tests/nostr-rs-relay diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..9e2662a --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,4 @@ +imports_granularity = "Crate" +group_imports = "StdExternalCrate" +imports_layout = "HorizontalVertical" +unstable_features = true diff --git a/CHANGELOG.md b/CHANGELOG.md index 96f3e59..e77df0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,66 @@ # Changelog +## [0.2.0] 2026-08-09 + +### Added +- holdinvoice methods: ``make_hold_invoice``, ``cancel_hold_invoice``, ``settle_hold_invoice`` +- holdinvoice notification: ``hold_invoice_accepted`` + +### Changed +- there are now five default relays that will be used if ``nip47-relays`` is not set +- `pay_keysend` will use the new CLN `xkeysend` command using CLN v26.06+ +- Budget is updated on demand instead of by a background task +- `list_transactions` is bounded (at most 500 transactions and ~128kB response) to prevent DoS +- Upgrade ``nostr_sdk`` and CLN dependencies + +### Removed +- ``multi_pay_invoice`` and ``multi_pay_keysend``, they were removed from the spec + +### Fixed +- ``nip47-create`` no longer fails if the NWC fails to start, the error is only logged, since the NWC at that point is already persisted to the DB +- Paying amount-less invoices with a request amount +- Legacy keysend TLV byte decoding +- Report the correct error when the payment route is too expensive +- Notifications are sent per client, so one failing client no longer prevents the others from receiving them +- Available payment methods are now determined from the node's RPC instead of it's (custom) version string +- Deduplication of request events instead of filtering unreliably + +## [0.1.9] 2026-04-23 + +### Fixed +- ``nip47-budget``: race condition where new budget settings would sometimes be ignored + +## [0.1.8] 2026-04-03 + +### Changed +- updated cln dependencies + +## [0.1.7] 2025-11-27 + +### Fixed +- If there were failed payment attempts before success the ``payment_sent`` notification might not have been sent + +## [0.1.6] 2025-11-10 + +### Added +- Include pending and failed payments in ``list_transactions``, now that ``state`` exists wallets can display these more meaningfully +- Include expired invoices in ``list_transactions``, now that ``state`` exists wallets can display these more meaningfully + +### Changed +- Upgrade ``nostr_sdk`` to ``v0.44`` and implement new fields in sync with the ``nip47`` spec, e.g. ``state`` for transactions +- Only process events that were created after ``cln-nip47`` started so it does not sent duplicate responses + +### Fixed +- Some minor fixes around ``bolt11`` invoices with 0 amount (aka any amount) +- Don't ignore `offset` parameter in ``list_transactions`` +- Also add ``notifications`` to the ``info_event``'s ``content`` if enabled to be in line with the spec + +## [0.1.5] 2025-07-24 + +### Changed + +- cap `list_transactions` to under 128kB since more can lead to incompatibilities with certain wallets + ## [0.1.4] 2025-07-24 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index c3b18e4..a6393f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,21 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +version = 4 [[package]] name = "aead" @@ -40,30 +25,41 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] [[package]] name = "async-utility" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151" +checksum = "188f83b9a198af8c336e505611edb00d6d2ac5c694241c5a4f9a12316938cfe9" dependencies = [ "futures-util", "gloo-timers", @@ -73,48 +69,37 @@ dependencies = [ [[package]] name = "async-wsocket" -version = "0.13.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7d8c7d34a225ba919dd9ba44d4b9106d20142da545e086be8ae21d1897e043" +checksum = "2c713e1f14c7b82e32ea159af1c6e2f070cfadbdf23fb2512acce9af0a26f1a2" dependencies = [ - "async-utility", "futures", "futures-util", "js-sys", "tokio", + "tokio-happy-eyeballs", "tokio-rustls", "tokio-socks", "tokio-tungstenite", "url", "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", ] [[package]] -name = "atomic-destructor" -version = "0.3.0" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] -name = "autocfg" -version = "1.5.0" +name = "base58ck" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "365c0acd5b2e8dd0111a46c4faea83fb3cfb6e39a49a7c73a06e090db7b2eff0" dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", + "bitcoin_hashes 0.14.101", ] [[package]] @@ -124,91 +109,109 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "base64ct" -version = "1.6.0" +name = "bech32" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] name = "bech32" -version = "0.10.0-beta" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" - -[[package]] -name = "bech32" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +checksum = "efbd3e1070bbdf4cd88a75264e18e8a26f7cb5c6949eadf0ceb85fb159cf08f8" [[package]] name = "bip39" -version = "2.2.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ - "bitcoin_hashes 0.13.0", + "bitcoin_hashes 0.14.101", "serde", "unicode-normalization", ] [[package]] name = "bitcoin" -version = "0.31.2" +version = "0.32.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c85783c2fe40083ea54a33aa2f0ba58831d90fcd190f5bdc47e74e84d2a96ae" +checksum = "bb0ce8bd5baaa0d303a19915a6d93afed161f528654e42da2a7a97d05c59499a" dependencies = [ - "bech32 0.10.0-beta", - "bitcoin-internals", - "bitcoin_hashes 0.13.0", - "hex-conservative 0.1.2", + "base58ck", + "bech32 0.11.1", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes 0.14.101", + "hex-conservative 0.2.2", "hex_lit", - "secp256k1 0.28.2", + "secp256k1 0.29.1", + "serde", +] + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9" +dependencies = [ + "bitcoin-internals", + "hex-conservative 1.2.0", "serde", ] [[package]] name = "bitcoin-internals" -version = "0.2.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" -dependencies = [ - "serde", -] +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" [[package]] name = "bitcoin-io" -version = "0.1.3" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] [[package]] -name = "bitcoin_hashes" -version = "0.13.0" +name = "bitcoin-units" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +checksum = "9cb95693f371d089a4b5b6fc41c6f3ea6e01ee8c15388335dfac8ea685173b51" dependencies = [ - "bitcoin-internals", - "hex-conservative 0.1.2", + "bitcoin-consensus-encoding", "serde", ] [[package]] name = "bitcoin_hashes" -version = "0.14.0" +version = "0.14.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" dependencies = [ "bitcoin-io", - "hex-conservative 0.2.1", + "hex-conservative 0.2.2", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5304e53726dbe5f93141535e102ed97b5bf4714fbecefdda8f9fb98d7fdaff0e" +dependencies = [ + "bitcoin-consensus-encoding", + "bitcoin-internals", + "hex-conservative 1.2.0", "serde", ] [[package]] name = "bitflags" -version = "2.9.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block-buffer" @@ -230,15 +233,21 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cbc" @@ -251,18 +260,19 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.30" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" @@ -301,28 +311,35 @@ dependencies = [ [[package]] name = "cln-nip47" -version = "0.1.4" +version = "0.2.0" dependencies = [ "anyhow", "cln-plugin", "cln-rpc", + "futures", "hex", "log", "log-panics", + "nostr", "nostr-sdk", "parking_lot", + "prost", + "protoc-bin-vendored", "regex", "serde", "serde_json", "tokio", + "tonic", + "tonic-prost", + "tonic-prost-build", "uuid", ] [[package]] name = "cln-plugin" -version = "0.4.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a0f6827e3c3dfa7f2e92bb0591b5be44c82b945ff88f534857b185810414ce" +checksum = "b9a4bb9c7335040d49c51900305b91155303539e9f54839bb194e915277bf882" dependencies = [ "anyhow", "bytes", @@ -339,9 +356,9 @@ dependencies = [ [[package]] name = "cln-rpc" -version = "0.4.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4436e58f1fccb1faf69df9ac436ae9304b5c200c7d92e6c4229826bdd0a8d0d" +checksum = "5d9f5e26f0a2713ea1393eb3117c148295fa4c5f7db529713b20323ca7cc37aa" dependencies = [ "anyhow", "bitcoin", @@ -366,20 +383,19 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "digest" @@ -389,25 +405,68 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "either" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "fnv" @@ -416,19 +475,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "form_urlencoded" -version = "1.2.1" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -441,9 +506,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -451,15 +516,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -468,38 +533,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -509,7 +574,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -525,34 +589,38 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "r-efi 5.3.0", + "wasip2", ] [[package]] -name = "gimli" -version = "0.31.1" +name = "getrandom" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] [[package]] name = "gloo-timers" @@ -566,6 +634,65 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" @@ -574,15 +701,18 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hex-conservative" -version = "0.1.2" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] [[package]] name = "hex-conservative" -version = "0.2.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10" dependencies = [ "arrayvec", ] @@ -594,23 +724,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" [[package]] -name = "hmac" -version = "0.12.1" +name = "http" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ - "digest", + "bytes", + "itoa", ] [[package]] -name = "http" -version = "1.3.1" +name = "http-body" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", - "fnv", - "itoa", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", ] [[package]] @@ -620,22 +763,84 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "icu_collections" -version = "1.5.0" +name = "httpdate" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -644,104 +849,66 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -750,14 +917,24 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", ] +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + [[package]] name = "inout" version = "0.1.4" @@ -769,41 +946,28 @@ dependencies = [ ] [[package]] -name = "instant" -version = "0.1.13" +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags", - "cfg-if", - "libc", + "either", ] [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -815,31 +979,36 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "log-panics" @@ -852,45 +1021,42 @@ dependencies = [ [[package]] name = "lru" -version = "0.14.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" +checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "mio" -version = "1.0.4" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "negentropy" version = "0.5.0" @@ -899,95 +1065,87 @@ checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" [[package]] name = "nostr" -version = "0.42.2" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193102a62a22b61f9a61b9df54fb19ebab8c1763d088fbb9a6f0f57000fba2d" +checksum = "5dde8c76076d334409d86c2e1db3e97abe5deb8cb92744f939cbc1fa45bd69e7" dependencies = [ "aes", "base64", - "bech32 0.11.0", + "bech32 0.12.0", "bip39", - "bitcoin_hashes 0.14.0", + "bitcoin_hashes 1.2.0", "cbc", "chacha20", "chacha20poly1305", - "getrandom 0.2.16", - "instant", - "regex", - "scrypt", - "secp256k1 0.29.1", + "faster-hex", + "opaquerr", + "rand 0.10.2", + "secp256k1 0.30.0", "serde", "serde_json", "unicode-normalization", + "universal-time", "url", + "zeroize", ] [[package]] name = "nostr-database" -version = "0.42.0" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aafe85dc7c039c399796043b76009fa744c3a45ac073a023932f7b7d91b1e7" +checksum = "4b1fdb9fcba732e32719662afad1b267e50322dbe89e506017ec13f24361bddf" dependencies = [ - "lru", "nostr", - "tokio", + "opaquerr", ] [[package]] -name = "nostr-relay-pool" -version = "0.42.0" +name = "nostr-gossip" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df4d5628d2444349570fb185b0c2d92cfdb7e68d62b13bf3e8a4348b5de7430b" +checksum = "fa07539e52a71cb91fe0d693facaa298f03fcf9edcd66a521094e18e286e2336" dependencies = [ - "async-utility", - "async-wsocket", - "atomic-destructor", - "lru", - "negentropy", "nostr", - "nostr-database", - "tokio", - "tracing", + "opaquerr", ] [[package]] name = "nostr-sdk" -version = "0.42.0" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e928ba9ac2695fbe10b8aefda59f2abfeb23c10a0e86a0b3e0f6a27bb274a2" +checksum = "26c86342f367bd9b173ec4a697e936e3a82d6dad5b4aa06c0d35d9b4f88a8e72" dependencies = [ "async-utility", + "async-wsocket", + "faster-hex", + "futures", + "lru", + "negentropy", "nostr", "nostr-database", - "nostr-relay-pool", + "nostr-gossip", + "opaquerr", + "rand 0.10.2", "tokio", + "tokio-stream", "tracing", + "universal-time", ] [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", + "windows-sys 0.61.2", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "opaque-debug" @@ -996,16 +1154,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] -name = "overload" -version = "0.1.1" +name = "opaquerr" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "4f933a4265d5cdad61d19bbdfc972ea5726d56cd8d3d57b8f2d3c365dd42bee9" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1013,55 +1171,59 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", -] - -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", + "windows-link", ] [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "poly1305" @@ -1074,6 +1236,15 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1084,19 +1255,166 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.95" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] -name = "quote" -version = "1.0.40" +name = "prost" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn 2.0.119", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" +dependencies = [ + "pulldown-cmark", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -1108,10 +1426,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand" -version = "0.8.5" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -1120,12 +1444,22 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -1145,7 +1479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -1154,70 +1488,61 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] -name = "redox_syscall" -version = "0.5.15" +name = "rand_core" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "ring" @@ -1227,24 +1552,32 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] -name = "rustc-demangle" -version = "0.1.25" +name = "rustix" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] [[package]] name = "rustls" -version = "0.23.29" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -1255,18 +1588,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -1275,24 +1608,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "scopeguard" @@ -1300,47 +1618,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "password-hash", - "pbkdf2", - "salsa20", - "sha2", -] - -[[package]] -name = "secp256k1" -version = "0.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" -dependencies = [ - "bitcoin_hashes 0.13.0", - "secp256k1-sys 0.9.2", - "serde", -] - [[package]] name = "secp256k1" version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "rand 0.8.5", - "secp256k1-sys 0.10.1", + "bitcoin_hashes 0.14.101", + "secp256k1-sys", "serde", ] [[package]] -name = "secp256k1-sys" -version = "0.9.2" +name = "secp256k1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ - "cc", + "bitcoin_hashes 0.14.101", + "rand 0.8.7", + "secp256k1-sys", ] [[package]] @@ -1354,52 +1651,52 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures", @@ -1417,37 +1714,37 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "subtle" @@ -1457,15 +1754,32 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "synstructure" version = "0.13.2" @@ -1474,7 +1788,20 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -1488,11 +1815,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.20", ] [[package]] @@ -1503,34 +1830,34 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -1538,9 +1865,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -1553,38 +1880,44 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.1" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "pin-project-lite", - "slab", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-happy-eyeballs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8564c32dfb6f4257f8bc6edfc178a34af97520e0b7b9815500c55eb3d092f29f" +dependencies = [ + "tokio", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -1592,9 +1925,9 @@ dependencies = [ [[package]] name = "tokio-socks" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" dependencies = [ "either", "futures-util", @@ -1604,20 +1937,21 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] name = "tokio-tungstenite" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", @@ -1631,22 +1965,122 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", + "libc", "pin-project-lite", "tokio", ] [[package]] -name = "tracing" -version = "0.1.41" +name = "tonic" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +dependencies = [ + "async-trait", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1882ac3bf5ef12877d7ed57aad87e75154c11931c2ba7e6cde5e22d63522c734" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tonic-prost" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3144df636917574672e93d0f56d7edec49f90305749c668df5101751bb8f95a" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.119", + "tempfile", + "tonic-build", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -1656,20 +2090,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -1688,14 +2122,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -1705,41 +2139,53 @@ dependencies = [ ] [[package]] -name = "tungstenite" -version = "0.26.2" +name = "try-lock" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", "http", "httparse", "log", - "rand 0.9.2", + "rand 0.9.5", "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.12", + "thiserror 2.0.20", "utf-8", ] [[package]] name = "typenum" -version = "1.18.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] @@ -1754,6 +2200,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "universal-time" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a939edecc3c5a7b83c02e5f6b3c31d2bc69eabcc9a87ab12c6d37ee6dbc856" + [[package]] name = "untrusted" version = "0.9.0" @@ -1762,14 +2214,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -1778,12 +2231,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -1792,11 +2239,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.17.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -1813,6 +2260,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1820,58 +2276,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1879,31 +2319,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn", - "wasm-bindgen-backend", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -1915,39 +2355,23 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.2", + "webpki-roots 1.0.9", ] [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] [[package]] -name = "winapi" -version = "0.3.9" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -1960,11 +2384,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", + "windows-link", ] [[package]] @@ -2032,33 +2456,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -2066,68 +2480,79 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -2136,11 +2561,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 258d24c..a21e37a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cln-nip47" -version = "0.1.4" -edition = "2021" -rust-version = "1.75" +version = "0.2.0" +edition = "2024" +rust-version = "1.85.0" [dependencies] anyhow = "1" @@ -12,14 +12,22 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["fs", "sync", "rt-multi-thread"] } -cln-rpc = "0.4" -# cln-rpc = { path="../../lightning/cln-rpc/", version = "^0.4" } -cln-plugin = "0.4" -# cln-plugin = { path="../../lightning/plugins/", version = "^0.4" } +cln-rpc = "0.7" +# cln-rpc = { path = "../lightning/cln-rpc/", version = "^0.6" } +cln-plugin = "0.7" +# cln-plugin = { path = "../lightning/plugins/", version = "^0.6" } parking_lot = "0.12" -# nostr-sdk = { git = "https://github.com/rust-nostr/nostr.git", rev = "f7122f5", features = ["nip47", "nip04", "nip44"]} -nostr-sdk = { version = "0.42", features = ["nip47", "nip04", "nip44"] } +# # nostr-sdk = { git = "https://github.com/rust-nostr/nostr.git", rev = "ff5be7d1416ea201887a02f648795010a3cbdf49" } +# nostr = { git = "https://github.com/rust-nostr/nostr.git", rev = "ff5be7d1416ea201887a02f648795010a3cbdf49", features = [ +# "nip47", +# "nip04", +# "nip44", +# ] } +nostr = { version = "0.45.1", features = ["nip47", "nip04", "nip44"] } +nostr-sdk = { version = "0.45.1" } + +futures = "0.3" uuid = { version = "1", features = ["v4"] } @@ -27,6 +35,18 @@ hex = "0.4" regex = "1" +tonic = { version = "0.14", default-features = false, features = [ + "codegen", + "transport", + "tls-ring", +] } +prost = "0.14" +tonic-prost = "0.14" + +[build-dependencies] +tonic-prost-build = "0.14" +protoc-bin-vendored = "3" + [profile.optimized] inherits = "release" diff --git a/README.md b/README.md index 2de2927..5556fd0 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,49 @@ @@ -51,7 +51,7 @@ # cln-nip47 -A core lightning plugin to connect wallets via Nostr Wallet Connect (NWC) as specified in [NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md). +A core lightning plugin to connect wallets via Nostr Wallet Connect (NWC) as specified in [NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md). It is intended to be used by a single user, since all notifications got to all NWC's. * [Installation](#installation) * [Building](#building) @@ -64,11 +64,10 @@ Release binaries for * x86_64-linux * armv7-linux (Raspberry Pi 32bit) * aarch64-linux (Raspberry Pi 64bit) +* universal-apple-darwin (macOS) can be found on the [release](https://github.com/daywalker90/cln-nip47/releases) page. If you are unsure about your architecture you can run ``uname -m``. -They require ``glibc>=2.31``, which you can check with ``ldd --version``. - # Building You can build the plugin yourself instead of using the release binaries. First clone the repo: @@ -85,7 +84,7 @@ cargo build --release After that the binary will be here: ``target/release/cln-nip47`` -Note: Release binaries are built using ``cross`` and the ``optimized`` profile. +Note: Release binaries are built with the ``optimized`` profile. # Documentation @@ -98,7 +97,12 @@ It is highly recommended to use your own private relay since public relays may l For a private relay you can for example use [nostr-rs-relay](https://github.com/scsibug/nostr-rs-relay) with ``pubkey_whitelist`` set to both ``clientkey_public`` and ``walletkey_public`` (returned from ``nip47-create``/``nip47-list``). ## Options -* ``nip47-relays``: Specify the relays that you want to use with your NWC. Can be set multiple times to use multiple relays. NWC's you create will save these and even if you add or remove relays keep the relays from the moment you created that NWC. You must set this atleast one time. +* ``nip47-relays``: Specify the relays that you want to use with your NWC. Can be set multiple times to use multiple relays. NWC's you create will save these and even if you add or remove relays keep the relays from the moment you created that NWC. If you don't set this yourself these default relays will be used: + * ``wss://nos.lol`` + * ``wss://relay.primal.net`` + * ``wss://relay.getalby.com/v1`` + * ``wss://relay.nostr.net`` + * ``wss://relay.snort.social`` * ``nip47-notifications``: Enable/disable nip47 notifications. Default is enabled (``true``) ## Methods @@ -127,21 +131,26 @@ For a private relay you can for example use [nostr-rs-relay](https://github.com/ * list all NWC configurations or just the one with ``label`` * ***label***: optional. The label the NWC was created with +## Holdinvoice support +For methods or notifications related to holdinvoices you need v0.3.2+ of [hold](https://github.com/BoltzExchange/hold) with enabled grpc (which is the default just make sure the port is free) + ## Supported NWC methods * ``pay_invoice`` -* ``multi_pay_invoice`` * ``pay_keysend`` (no ``preimage`` in request allowed since CLN only supports generating it itself) -* ``multi_pay_keysend`` (no ``preimage`` in request allowed since CLN only supports generating it itself) * ``make_invoice`` * ``lookup_invoice`` * ``list_transactions`` * ``get_balance`` * ``get_info`` (no ``block_hash``) +* ``make_hold_invoice`` (requires [Holdinvoice support](#holdinvoice_support)) +* ``cancel_hold_invoice`` (requires [Holdinvoice support](#holdinvoice_support)) +* ``settle_hold_invoice`` (requires [Holdinvoice support](#holdinvoice_support)) ## Supported NWC notifications * ``payment_received`` * ``payment_sent`` +* ``hold_invoice_accepted`` (requires [Holdinvoice support](#holdinvoice_support)) ## Supported content encryption: * [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) -* [NIP-44v2](https://github.com/nostr-protocol/nips/blob/master/44.md) \ No newline at end of file +* [NIP-44v2](https://github.com/nostr-protocol/nips/blob/master/44.md) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..9b5fbd8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Security Policy + +## Supported Versions + +Only the latest version is supported. + +## Reporting a Vulnerability + +To report security vulnerabilities, please send an email to: +- `daywalker990@gmx.de` + +Note: This email address is exclusively for vulnerability reporting. + +For all other inquiries/communication, please use the Github issues. + +## Signatures For Releases + +The following keys may be used to communicate sensitive information to +developers: + +| Name | Email | Fingerprint | +|------|-------|-------------| +| daywalker90 | `daywalker990@gmx.de` | 8A07 9421 A871 D0B1 0835 1193 7AB4 802E D5A6 39F3 | + +You can import a key by running the following command with that individual’s fingerprint: +`gpg --keyserver hkps://keys.openpgp.org --recv-keys ""`. +Ensure that you put quotes around fingerprints containing spaces. diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..04c8cef --- /dev/null +++ b/build.rs @@ -0,0 +1,8 @@ +fn main() { + let protoc = protoc_bin_vendored::protoc_bin_path().unwrap(); + unsafe { std::env::set_var("PROTOC", protoc) }; + tonic_prost_build::configure() + .protoc_arg("--experimental_allow_proto3_optional") + .compile_protos(&["protos/hold.proto"], &["protos"]) + .unwrap_or_else(|e| panic!("Could not build protos: {e}")); +} diff --git a/coffee.yml b/coffee.yml index 2221faf..fa75d76 100644 --- a/coffee.yml +++ b/coffee.yml @@ -1,6 +1,6 @@ plugin: name: cln-nip47 - version: 0.1.4 + version: 0.2.0 lang: rust install: | cargo build --release && cp target/release/cln-nip47 . && cargo clean diff --git a/flake.lock b/flake.lock index 4df37d2..87c96f0 100644 --- a/flake.lock +++ b/flake.lock @@ -1,17 +1,12 @@ { "nodes": { "crane": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, "locked": { - "lastModified": 1721058578, - "narHash": "sha256-fs/PVa3H5dS1//4BjecWi3nitXm5fRObx0JxXIAo+JA=", + "lastModified": 1755993354, + "narHash": "sha256-FCRRAzSaL/+umLIm3RU3O/+fJ2ssaPHseI2SSFL8yZU=", "owner": "ipetkov", "repo": "crane", - "rev": "17e5109bb1d9fb393d70fba80988f7d70d1ded1a", + "rev": "25bd41b24426c7734278c2ff02e53258851db914", "type": "github" }, "original": { @@ -25,11 +20,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -40,11 +35,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1721116560, - "narHash": "sha256-++TYlGMAJM1Q+0nMVaWBSEvEUjRs7ZGiNQOpqbQApCU=", + "lastModified": 1756159630, + "narHash": "sha256-ohMvsjtSVdT/bruXf5ClBh8ZYXRmD4krmjKrXhEvwMg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9355fa86e6f27422963132c2c9aeedb0fb963d93", + "rev": "84c256e42600cb0fdf25763b48d28df2f25a0c8b", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index a533264..247270f 100644 --- a/flake.nix +++ b/flake.nix @@ -4,10 +4,7 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - crane = { - url = "github:ipetkov/crane"; - inputs.nixpkgs.follows = "nixpkgs"; - }; + crane.url = "github:ipetkov/crane"; flake-utils.url = "github:numtide/flake-utils"; }; diff --git a/protos/hold.proto b/protos/hold.proto new file mode 100644 index 0000000..e7ad240 --- /dev/null +++ b/protos/hold.proto @@ -0,0 +1,185 @@ +syntax = "proto3"; + +package hold; + +service Hold { + rpc GetInfo (GetInfoRequest) returns (GetInfoResponse); + + rpc Invoice (InvoiceRequest) returns (InvoiceResponse) {} + rpc Inject (InjectRequest) returns (InjectResponse) {} + + rpc List (ListRequest) returns (ListResponse) {} + + rpc Settle (SettleRequest) returns (SettleResponse) {} + rpc Cancel (CancelRequest) returns (CancelResponse) {} + + // Cleans cancelled invoices + rpc Clean (CleanRequest) returns (CleanResponse) {} + + rpc Track (TrackRequest) returns (stream TrackResponse) {} + rpc TrackAll (TrackAllRequest) returns (stream TrackAllResponse) {} + + rpc OnionMessages (stream OnionMessageResponse) returns (stream OnionMessage) {} +} + +enum HookAction { + Continue = 0; + Resolve = 1; +} + +message GetInfoRequest {} +message GetInfoResponse { + string version = 1; +} + +message Hop { + bytes public_key = 1; + uint64 short_channel_id = 2; + uint64 base_fee = 3; + uint64 ppm_fee = 4; + uint64 cltv_expiry_delta = 5; +} + +message RoutingHint { + repeated Hop hops = 1; +} + +message InvoiceRequest { + bytes payment_hash = 1; + uint64 amount_msat = 2; + + oneof description { + string memo = 3; + bytes hash = 4; + } + + optional uint64 expiry = 5; + optional uint64 min_final_cltv_expiry = 6; + repeated RoutingHint routing_hints = 7; +} +message InvoiceResponse { + string bolt11 = 1; +} + +message InjectRequest { + string invoice = 1; + optional uint64 min_cltv_expiry = 2; +} +message InjectResponse {} + +message ListRequest { + message Pagination { + // Inclusive + int64 index_start = 1; + uint64 limit = 2; + } + + oneof constraint { + bytes payment_hash = 1; + Pagination pagination = 2; + } +} + +enum InvoiceState { + UNPAID = 0; + ACCEPTED = 1; + PAID = 2; + CANCELLED = 3; +} + +message Htlc { + int64 id = 1; + InvoiceState state = 2; + string scid = 3; + uint64 channel_id = 4; + uint64 msat = 5; + uint64 created_at = 6; + optional uint64 cltv_expiry = 7; +} + +message Invoice { + int64 id = 1; + bytes payment_hash = 2; + optional bytes preimage = 3; + string invoice = 4; + InvoiceState state = 5; + uint64 created_at = 6; + optional uint64 settled_at = 8; + + repeated Htlc htlcs = 7; + + optional uint64 min_cltv_expiry = 9; +} + +message ListResponse { + repeated Invoice invoices = 1; +} + +message SettleRequest { + bytes payment_preimage = 1; +} +message SettleResponse {} + +message CancelRequest { + bytes payment_hash = 1; +} +message CancelResponse {} + +message CleanRequest { + // Clean everything older than age seconds + optional uint64 age = 1; +} +message CleanResponse { + uint64 cleaned = 1; +} + +message TrackRequest { + bytes payment_hash = 1; +} + +message TrackResponse { + InvoiceState state = 1; +} + +message TrackAllRequest { + repeated bytes payment_hashes = 1; +} + +message TrackAllResponse { + bytes payment_hash = 1; + string bolt11 = 2; + InvoiceState state = 3; +} + +message OnionMessage { + message ReplyBlindedPath { + message Hop { + optional bytes blinded_node_id = 1; + optional bytes encrypted_recipient_data = 2; + } + + optional bytes first_node_id = 1; + optional string first_scid = 2; + optional uint64 first_scid_dir = 3; + optional bytes first_path_key = 4; + repeated Hop hops = 5; + } + + message UnknownField { + uint64 number = 1; + bytes value = 2; + } + + uint64 id = 1; + optional bytes pathsecret = 2; + optional ReplyBlindedPath reply_blindedpath = 3; + optional bytes invoice_request = 4; + optional bytes invoice = 5; + optional bytes invoice_error = 6; + repeated UnknownField unknown_fields = 7; +} + +message OnionMessageResponse { + uint64 id = 1; + HookAction action = 2; +} diff --git a/src/main.rs b/src/main.rs index dd6ded4..1c3323a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,41 @@ -use std::{path::Path, time::Duration}; +use std::{ + path::{Path, PathBuf}, + str::FromStr, + time::Duration, +}; use anyhow::anyhow; use cln_plugin::{ + Builder, + Plugin, options::{ConfigOption, DefaultBooleanConfigOption, StringArrayConfigOption}, - Builder, Plugin, }; -use cln_rpc::{model::requests::ListdatastoreRequest, ClnRpc}; - -use nostr_sdk::*; +use cln_rpc::{ + ClnRpc, + model::{ + requests::{DecodeRequest, ListdatastoreRequest}, + responses::DecodeType, + }, +}; +use nostr::nips::nip47; use nwc::run_nwc; use nwc_notifications::{payment_received_handler, payment_sent_handler}; use parse::read_startup_options; use rpc::{nwc_budget, nwc_create, nwc_list, nwc_revoke}; +use serde_json::json; use structs::PluginState; use tokio::time; +use tonic::transport::{Certificate, ClientTlsConfig, Endpoint, Identity}; use util::{load_nwc_store, update_nwc_store}; +use crate::{ + hold::{InvoiceState, ListRequest, hold_client::HoldClient}, + nwc_notifications::holdinvoice_accepted_handler, +}; + mod nwc; mod nwc_balance; +mod nwc_hold; mod nwc_info; mod nwc_invoice; mod nwc_keysend; @@ -30,6 +48,11 @@ mod structs; mod tasks; mod util; +pub const STARTUP_DELAY: u64 = 1; +pub mod hold { + tonic::include_proto!("hold"); +} + const OPT_RELAYS: StringArrayConfigOption = ConfigOption::new_str_arr_no_default( "nip47-relays", "Nostr relays used for nwc. Can be stated multiple times.", @@ -40,31 +63,35 @@ const OPT_NOTIFICATIONS: DefaultBooleanConfigOption = ConfigOption::new_bool_wit "Enable/disable nip47-notifications. Default is `true`", ); pub const PLUGIN_NAME: &str = "cln-nip47"; -pub const WALLET_READ_METHODS: [&str; 5] = [ - "make_invoice", - "lookup_invoice", - "list_transactions", - "get_balance", - "get_info", +pub const WALLET_READ_OR_RECEIVE_METHODS: [nip47::Method; 5] = [ + nip47::Method::MakeInvoice, + nip47::Method::LookupInvoice, + nip47::Method::ListTransactions, + nip47::Method::GetBalance, + nip47::Method::GetInfo, ]; -pub const WALLET_ALL_METHODS: [&str; 9] = [ - "pay_invoice", - "multi_pay_invoice", - "pay_keysend", - "multi_pay_keysend", - WALLET_READ_METHODS[0], - WALLET_READ_METHODS[1], - WALLET_READ_METHODS[2], - WALLET_READ_METHODS[3], - WALLET_READ_METHODS[4], +pub const WALLET_PAY_METHODS: [nip47::Method; 2] = + [nip47::Method::PayInvoice, nip47::Method::PayKeysend]; +pub const WALLET_HOLD_METHODS: [nip47::Method; 3] = [ + nip47::Method::MakeHoldInvoice, + nip47::Method::CancelHoldInvoice, + nip47::Method::SettleHoldInvoice, ]; +pub const WALLET_NOTIFICATIONS: [nip47::NotificationType; 2] = [ + nip47::NotificationType::PaymentReceived, + nip47::NotificationType::PaymentSent, +]; +pub const WALLET_HOLD_NOTIFICATIONS: [nip47::NotificationType; 1] = + [nip47::NotificationType::HoldInvoiceAccepted]; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { - std::env::set_var( - "CLN_PLUGIN_LOG", - "cln_plugin=info,cln_rpc=info,cln_nip47=debug,info", - ); + unsafe { + std::env::set_var( + "CLN_PLUGIN_LOG", + "cln_plugin=info,cln_rpc=info,cln_nip47=trace,info", + ); + }; log_panics::init(); let state; @@ -90,13 +117,13 @@ async fn main() -> Result<(), anyhow::Error> { Ok(state) => state, Err(e) => { return plugin - .disable(format!("Error connecting to cln rpc: {}", e).as_str()) + .disable(format!("Error connecting to cln rpc: {e}").as_str()) .await; } }; match read_startup_options(&plugin, &state).await { Ok(()) => &(), - Err(e) => return plugin.disable(format!("{}", e).as_str()).await, + Err(e) => return plugin.disable(format!("{e}").as_str()).await, }; log::debug!("read startup options done"); plugin @@ -105,15 +132,25 @@ async fn main() -> Result<(), anyhow::Error> { }; let plugin = confplugin.start(state).await?; + match check_hold_support(plugin.clone()).await { + Ok(()) => { + log::info!("Hold support activated, loading pending invoices..."); + if let Err(e) = load_pending_hold_invoices(plugin.clone()).await { + log::error!("Error loading pending hold invoices: {e}"); + } + } + Err(e) => log::info!("Hold support not activated: {e}"), + } + { let mut rpc = plugin.state().rpc_lock.lock().await; // Make sure incase of rapid nip47-create and plugin restarts info_events // have a different timestamp and therefore ID so relays don't disconnect us - time::sleep(Duration::from_secs(1)).await; + time::sleep(Duration::from_secs(STARTUP_DELAY)).await; match load_nwcs(plugin.clone(), &mut rpc).await { - Ok(_) => log::info!("All NWC's loaded"), + Ok(()) => log::info!("All NWC's loaded"), Err(e) => { println!( "{}", @@ -126,6 +163,16 @@ async fn main() -> Result<(), anyhow::Error> { } } + let plugin_clone_cleanup = plugin.clone(); + tokio::spawn(async move { + loop { + if let Err(e) = tasks::cleanup_event_ids(plugin_clone_cleanup.clone()).await { + log::warn!("Error in cleanup_event_ids thread: {e}"); + } + time::sleep(Duration::from_secs(10)).await; + } + }); + plugin.join().await } @@ -134,8 +181,8 @@ async fn shutdown_handler( _args: serde_json::Value, ) -> Result<(), anyhow::Error> { let mut locked_handles = plugin.state().handles.lock().await; - for (_x, (client, _client_pubkey)) in locked_handles.drain() { - client.shutdown().await; + for (_x, wallet_service) in locked_handles.drain() { + wallet_service.client.shutdown().await; } std::process::exit(0) } @@ -146,7 +193,7 @@ async fn load_nwcs(plugin: Plugin, rpc: &mut ClnRpc) -> Result<(), key: Some(vec![PLUGIN_NAME.to_owned()]), }) .await?; - for datastore in labels.datastore.into_iter() { + for datastore in labels.datastore { let label = datastore.key.last().unwrap(); let mut nwc_store = load_nwc_store(rpc, label).await?; @@ -159,7 +206,186 @@ async fn load_nwcs(plugin: Plugin, rpc: &mut ClnRpc) -> Result<(), } } + // We don't keep track of inflight payments succeeding + // while the plugin is dynamically restarted + if nwc_store.reserved_msat != 0 { + log::warn!( + "Releasing {} msat leftover budget reservation for {label}", + nwc_store.reserved_msat + ); + nwc_store.reserved_msat = 0; + update_nwc_store(rpc, label, nwc_store.clone()).await?; + } + run_nwc(plugin.clone(), label.clone(), nwc_store.clone()).await?; } Ok(()) } + +async fn load_pending_hold_invoices(plugin: Plugin) -> Result<(), anyhow::Error> { + let mut hold_client = plugin.state().hold_client.lock().clone().unwrap(); + let invoices_request = ListRequest { constraint: None }; + let invoices = hold_client + .list(invoices_request) + .await? + .into_inner() + .invoices; + + let mut rpc = plugin.state().rpc_lock.lock().await; + + for invoice in invoices { + if invoice.state() == InvoiceState::Accepted || invoice.state() == InvoiceState::Unpaid { + // Bound the accepted handler by the invoice's expiry. The hold + // plugin has no expired state, so without this it would wait + // forever on an invoice that expires while Unpaid. + let invoice_decoded = rpc + .call_typed(&DecodeRequest { + string: invoice.invoice.clone(), + }) + .await?; + + if !invoice_decoded.valid { + log::warn!( + "Skipping hold invoice {}, could not decode it", + hex::encode(&invoice.payment_hash) + ); + continue; + } + + let (created_at, expiry) = match invoice_decoded.item_type { + DecodeType::BOLT12_INVOICE => ( + invoice_decoded.invoice_created_at, + invoice_decoded.invoice_relative_expiry.map(u64::from), + ), + DecodeType::BOLT11_INVOICE => (invoice_decoded.created_at, invoice_decoded.expiry), + _ => continue, + }; + + let (Some(created_at), Some(expiry)) = (created_at, expiry) else { + log::warn!( + "Skipping hold invoice {}: missing creation time or expiry", + hex::encode(&invoice.payment_hash) + ); + continue; + }; + let expires_at = created_at.saturating_add(expiry); + + log::debug!( + "Starting holdinvoice accepted handler for {}", + hex::encode(&invoice.payment_hash) + ); + tokio::spawn(holdinvoice_accepted_handler( + plugin.clone(), + invoice.payment_hash, + expires_at, + )); + } + } + + Ok(()) +} + +async fn check_hold_support(plugin: Plugin) -> Result<(), anyhow::Error> { + let mut rpc = plugin.state().rpc_lock.lock().await; + let hold_grpc_host_response: serde_json::Value = rpc + .call_raw("listconfigs", &json!({"config": "hold-grpc-host"})) + .await?; + + let Some(hold_grpc_host_configs) = hold_grpc_host_response.get("configs") else { + return Err(anyhow!("Unsopprted listconfigs response!")); + }; + let Some(hold_grpc_host_config) = hold_grpc_host_configs.get("hold-grpc-host") else { + return Err(anyhow!("hold-grpc-host config not found")); + }; + let Some(hold_grpc_host_value) = hold_grpc_host_config.get("value_str") else { + return Err(anyhow!("hold-grpc-host config not a string")); + }; + let Some(hold_grpc_host) = hold_grpc_host_value.as_str() else { + return Err(anyhow!("hold-grpc-host config not convertable to string")); + }; + + let hold_grpc_port_response: serde_json::Value = rpc + .call_raw("listconfigs", &json!({"config": "hold-grpc-port"})) + .await?; + let Some(hold_grpc_port_configs) = hold_grpc_port_response.get("configs") else { + return Err(anyhow!("Unsopprted listconfigs response!")); + }; + let Some(hold_grpc_port_config) = hold_grpc_port_configs.get("hold-grpc-port") else { + return Err(anyhow!("hold-grpc-port config not found")); + }; + let Some(hold_grpc_port_value) = hold_grpc_port_config.get("value_int") else { + return Err(anyhow!("hold-grpc-port config not a number")); + }; + let hold_grpc_port = if let Some(hgh) = hold_grpc_port_value.as_u64() { + u16::try_from(hgh)? + } else { + return Err(anyhow!("hold-grpc-port config not convertable to integer")); + }; + + let cert_dir = PathBuf::from_str(&plugin.configuration().lightning_dir)?.join("hold"); + + log::debug!( + "Searching {} for hold plugin certs", + cert_dir.to_str().unwrap() + ); + + let cert_max_retries = 10; + let mut cert_retries = 0; + while cert_retries < cert_max_retries && !do_certificates_exist(&cert_dir) { + log::debug!("Hold certificates incomplete. Waiting..."); + time::sleep(Duration::from_millis(500)).await; + cert_retries += 1; + } + + let ca_cert = tokio::fs::read(cert_dir.join("ca.pem")).await?; + let client_cert = tokio::fs::read(cert_dir.join("client.pem")).await?; + let client_key = tokio::fs::read(cert_dir.join("client-key.pem")).await?; + + let identity = Identity::from_pem(client_cert, client_key); + + let ca = Certificate::from_pem(ca_cert); + + let tls_config = ClientTlsConfig::new() + .ca_certificate(ca) + .identity(identity) + .domain_name("hold"); + + let endpoint = Endpoint::from_shared(format!("https://{hold_grpc_host}:{hold_grpc_port}"))? + .tls_config(tls_config)? + .keep_alive_while_idle(true) + .connect_timeout(Duration::from_secs(5)); + + let chan_max_retries = 20; + let mut chan_retries = 0; + + let channel = loop { + match endpoint.connect().await { + Ok(channel) => break channel, + Err(e) if chan_retries < chan_max_retries => { + chan_retries += 1; + + log::debug!( + "Hold gRPC server not ready, retrying ({chan_retries}/{chan_max_retries}): {e}" + ); + + time::sleep(Duration::from_millis(500)).await; + } + Err(e) => { + return Err(anyhow!("Hold gRPC server did not become ready: {e}")); + } + } + }; + + *plugin.state().hold_client.lock() = Some(HoldClient::new(channel)); + + Ok(()) +} + +fn do_certificates_exist(cert_dir: &Path) -> bool { + let required_files = ["client.pem", "client-key.pem", "ca.pem"]; + + required_files.iter().all(|file| { + let path = cert_dir.join(file); + path.exists() && path.metadata().is_ok_and(|m| m.len() > 0) + }) +} diff --git a/src/nwc.rs b/src/nwc.rs index 338ddad..dfd0e2c 100644 --- a/src/nwc.rs +++ b/src/nwc.rs @@ -1,63 +1,75 @@ use std::time::Duration; -use crate::nwc_balance::get_balance; -use crate::nwc_info::get_info; -use crate::nwc_invoice::make_invoice; -use crate::nwc_keysend::{multi_pay_keysend, pay_keysend}; -use crate::nwc_lookups::{list_transactions, lookup_invoice}; -use crate::nwc_pay::{multi_pay_invoice, pay_invoice}; -use crate::structs::{NwcStore, PluginState}; -use crate::tasks::budget_task; -use crate::util::is_read_only_nwc; -use crate::{OPT_NOTIFICATIONS, WALLET_ALL_METHODS, WALLET_READ_METHODS}; use anyhow::anyhow; use cln_plugin::Plugin; -use nostr_sdk::nips::*; -use nostr_sdk::Client; -use nostr_sdk::*; -use tokio::sync::oneshot; +use futures::StreamExt; +use nostr::{ + event::{Event, EventBuilder, EventId, FinalizeEventAsync, Kind, Tag}, + filter::Filter, + key::{Keys, PublicKey, SecretKey}, + nips::{nip04, nip44, nip47}, + types::Timestamp, +}; +use nostr_sdk::{ + client::{self, Client, ClientNotification}, + error::Error, + relay::RelayStatus, +}; use tokio::time; +use crate::{ + OPT_NOTIFICATIONS, + nwc_balance::get_balance_response, + nwc_hold::{ + cancel_hold_invoice_response, + make_hold_invoice_response, + settle_hold_invoice_response, + }, + nwc_info::get_info_response, + nwc_invoice::make_invoice_response, + nwc_keysend::pay_keysend_response, + nwc_lookups::{list_transactions_response, lookup_invoice_response}, + nwc_pay::pay_invoice_response, + structs::{ID_MAX_AGE, NwcStore, PluginState, WalletService}, + util::{build_capabilities, build_notifications_vec, is_read_only_nwc, save_event_id}, +}; + +#[allow(clippy::too_many_lines)] pub async fn run_nwc( plugin: Plugin, label: String, nwc_store: NwcStore, -) -> Result<(), client::Error> { - let capabilities = if is_read_only_nwc(&nwc_store) { - WALLET_READ_METHODS.join(" ") - } else { - WALLET_ALL_METHODS.join(" ") - }; +) -> Result<(), Error> { + let (method_capabilities, _) = build_capabilities(is_read_only_nwc(&nwc_store), &plugin); - let wallet_keys = Keys::new( - SecretKey::from_hex(&nwc_store.walletkey) - .map_err(|e| client::Error::Signer(SignerError::backend(e)))?, - ); - let client_pubkey = Keys::new(nwc_store.uri.secret.clone()).public_key(); + let wallet_keys = Keys::new(SecretKey::from_hex(&nwc_store.walletkey)?); + let client_keys = Keys::new(nwc_store.uri.secret.clone()); - let client = Client::new(wallet_keys.clone()); + let nostr_client = Client::new(); log::debug!("relay_count:{}", nwc_store.uri.relays.len()); - for relay in nwc_store.uri.relays.iter() { - log::debug!("Adding relay: {}", relay); - client.add_relay(relay).await?; + for relay in &nwc_store.uri.relays { + log::debug!("Adding relay: {relay}"); + nostr_client.add_relay(relay).await?; } if nwc_store.interval_config.is_some() { - start_nwc_budget_job(plugin.clone(), label.clone()); + log::debug!("NWC {label} uses an interval budget"); } - let client_clone = client.clone(); + let nostr_client_clone = nostr_client.clone(); let plugin_clone = plugin.clone(); let label_clone = label.clone(); + let client_keys_clone = client_keys.clone(); + let wallet_keys_clone = wallet_keys.clone(); tokio::spawn(async move { loop { - client_clone.connect().await; - client_clone - .wait_for_connection(Duration::from_secs(30)) + nostr_client_clone + .connect() + .and_wait(Duration::from_secs(30)) .await; - let relays = client_clone.relays().await; + let relays = nostr_client_clone.relays().await; if relays.is_empty() { log::info!("No more relays left, we probably shut down. Exiting..."); break; @@ -67,7 +79,7 @@ pub async fn run_nwc( if relay.status() == RelayStatus::Connected { connected = true; } else { - log::info!("Could not connect to {}", url) + log::info!("Could not connect to {url}"); } } if !connected { @@ -77,90 +89,101 @@ pub async fn run_nwc( } if let Err(e) = send_nwc_info_event( - client_clone.clone(), - plugin_clone.option(&OPT_NOTIFICATIONS).unwrap(), - capabilities.clone(), - wallet_keys.clone(), + plugin_clone.clone(), + nostr_client_clone.clone(), + method_capabilities.clone(), + wallet_keys_clone.clone(), ) .await { - log::warn!("{}", e); - client_clone.disconnect().await; + log::warn!("{e}"); + nostr_client_clone.disconnect().await; time::sleep(Duration::from_secs(5)).await; continue; } let filter = Filter::new() .kind(Kind::WalletConnectRequest) - .author(client_pubkey); + .author(client_keys_clone.public_key()); - if let Err(e) = client_clone.subscribe(filter, None).await { - log::warn!("Could not subscribe to nwc events! {}", e); - client_clone.disconnect().await; - time::sleep(Duration::from_secs(5)).await; - continue; - }; + let mut notifications = nostr_client_clone.notifications(); - let client_clone_handler = client_clone.clone(); - match client_clone - .handle_notifications(|notification| { - let client_clone_handler = client_clone_handler.clone(); - let plugin_clone = plugin_clone.clone(); - let label_clone = label_clone.clone(); - let wallet_keys_clone = wallet_keys.clone(); - nwc_request_handler( - notification, - client_clone_handler, - plugin_clone, - label_clone, - wallet_keys_clone, - client_pubkey, - ) - }) - .await - { - Ok(()) => { - log::info!("NWC handler for `{}` stopped", label_clone); - break; + match nostr_client_clone.subscribe(filter).await { + Ok(o) => { + if o.success.is_empty() { + log::warn!("Could not subscribe to any relay!"); + nostr_client_clone.disconnect().await; + time::sleep(Duration::from_secs(5)).await; + continue; + } } - Err(e) => log::warn!("NWC handler for `{}` had an error: {}", label_clone, e), - }; + Err(e) => { + log::warn!("Error subscribing to relays: {e}"); + nostr_client_clone.disconnect().await; + time::sleep(Duration::from_secs(5)).await; + continue; + } + } + + while let Some(notification) = notifications.next().await { + if let Err(e) = nwc_request_handler( + notification, + &nostr_client_clone, + &plugin_clone, + &label_clone, + &wallet_keys_clone, + client_keys_clone.public_key(), + ) + .await + { + log::warn!("NWC handler for `{label_clone}` had an error: {e}"); + } + } + + {}; } }); let mut locked_handles = plugin.state().handles.lock().await; - locked_handles.insert( - label.clone(), - (client, Keys::new(nwc_store.uri.secret).public_key()), - ); + let wallet_service = WalletService { + client: nostr_client, + client_pubkey: client_keys.public_key(), + wallet_secret: wallet_keys, + }; + locked_handles.insert(label.clone(), wallet_service); Ok(()) } pub async fn send_nwc_info_event( + plugin: Plugin, client: Client, - notifications: bool, capabilities: String, wallet_keys: Keys, ) -> Result<(), anyhow::Error> { - let mut info_event_builder = EventBuilder::new(Kind::WalletConnectInfo, capabilities.clone()) + let mut capabilities = capabilities; + if plugin.option(&OPT_NOTIFICATIONS).unwrap() { + capabilities.push_str(" notifications"); + } + let mut info_event_builder = EventBuilder::new(Kind::WalletConnectInfo, capabilities) .tag(Tag::parse(vec!["encryption", "nip44_v2 nip04"]).unwrap()); - if notifications { + if plugin.option(&OPT_NOTIFICATIONS).unwrap() { + let notification_capabilities = build_notifications_vec(&plugin).join(" "); info_event_builder = info_event_builder - .tag(Tag::parse(vec!["notifications", "payment_received payment_sent"]).unwrap()) + .tag(Tag::parse(vec!["notifications", ¬ification_capabilities]).unwrap()); } - let info_event = match info_event_builder.sign_with_keys(&wallet_keys) { + let info_event = match info_event_builder.finalize_async(&wallet_keys).await { Ok(o) => o, Err(e) => { - return Err(anyhow!("Could not sign info_event! {}", e)); + return Err(anyhow!("Could not sign info_event! {e}")); } }; - log::debug!("info_event:{:?}", info_event); + log::debug!("info_event:{info_event:?}"); let send_result = match client.send_event(&info_event).await { Ok(o) => o, Err(e) => { - return Err(anyhow!("Could not send info_event! {}", e)); + return Err(anyhow!("Could not send info_event! {e}")); } }; if send_result.success.is_empty() { @@ -178,306 +201,123 @@ pub async fn send_nwc_info_event( pub async fn stop_nwc(plugin: Plugin, label: &String) { let mut locked_handles = plugin.state().handles.lock().await; - if let Some((client, _client_pubkey)) = locked_handles.remove(label) { - client.shutdown().await; - } - - stop_nwc_budget_job(plugin.clone(), label); -} - -pub fn start_nwc_budget_job(plugin: Plugin, label: String) { - let (tx, rx) = oneshot::channel::<()>(); - tokio::spawn(budget_task(rx, plugin.clone(), label.clone())); - plugin.state().budget_jobs.lock().insert(label, tx); -} - -pub fn stop_nwc_budget_job(plugin: Plugin, label: &String) { - let mut budget_jobs = plugin.state().budget_jobs.lock(); - let job = budget_jobs.remove(label); - if let Some(j) = job { - let _ = j.send(()); + if let Some(wallet_service) = locked_handles.remove(label) { + wallet_service.client.shutdown().await; } } +#[allow(clippy::too_many_lines)] async fn nwc_request_handler( - notification: RelayPoolNotification, - client: client::Client, - plugin: Plugin, - label: String, - wallet_keys: Keys, + notification: ClientNotification, + nostr_client: &client::Client, + plugin: &Plugin, + label: &str, + wallet_keys: &Keys, client_pubkey: PublicKey, -) -> Result { +) -> Result<(), Box> { let (relay_url, subscription_id, event) = match notification { - RelayPoolNotification::Event { + ClientNotification::Event { relay_url, subscription_id, event, } => (relay_url, subscription_id, event), - RelayPoolNotification::Message { + ClientNotification::Message { relay_url: _, message: _, - } => return Ok(false), - RelayPoolNotification::Shutdown => return Ok(true), + } + | ClientNotification::Shutdown => return Ok(()), }; - if let Some(expi) = event.tags.expiration() { - if *expi < Timestamp::now() { - return Ok(false); - } - } - log::debug!( - "relay_url:{} subscription_id:{} {:?}", - relay_url, - subscription_id, - event - ); - let use_nip44; - let content = match nip44::decrypt(wallet_keys.secret_key(), &client_pubkey, &event.content) { - Ok(o) => { - use_nip44 = true; - o - } - Err(e) => { - log::debug!("Could not decrypt using NIP-44:{}. Trying NIP-04", e); - match nip04::decrypt(wallet_keys.secret_key(), &client_pubkey, &event.content) { - Ok(o) => { - use_nip44 = false; - o - } - Err(e) => { - log::warn!("Could not decrypt using NIP-04 or NIP-44:{}", e); - return Ok(false); - } - } - } - }; - log::debug!("Decrypted:{}", content); - let request: nip47::Request = match serde_json::from_str(&content) { - Ok(o) => o, - Err(e) => { - log::warn!("Error parsing nip47::Request! {}", e); - return Ok(false); - } - }; + log::trace!("relay_url:{relay_url} subscription_id:{subscription_id} {event:?}"); + let mut use_nip44 = check_nip44_support(&event); - let responses = match request.params { - nip47::RequestParams::PayInvoice(pay_invoice_request) => { - vec![ - match pay_invoice(plugin.clone(), pay_invoice_request, &label).await { - Ok((o, id)) => ( - nip47::Response { - result_type: nip47::Method::PayInvoice, - error: None, - result: Some(nip47::ResponseResult::PayInvoice(o)), - }, - id, - ), - Err((e, id)) => ( - nip47::Response { - result_type: nip47::Method::PayInvoice, - error: Some(e), - result: None, - }, - id, - ), - }, - ] - } - nip47::RequestParams::MultiPayInvoice(multi_pay_invoice_request) => { - multi_pay_invoice(plugin.clone(), multi_pay_invoice_request, &label).await - } - nip47::RequestParams::PayKeysend(pay_keysend_request) => { - let id = if let Some(i) = pay_keysend_request.id.clone() { - i - } else { - pay_keysend_request.pubkey.clone() - }; - vec![ - match pay_keysend(plugin.clone(), pay_keysend_request, &label).await { - Ok(o) => ( - nip47::Response { - result_type: nip47::Method::PayKeysend, - error: None, - result: Some(nip47::ResponseResult::PayKeysend(o)), - }, - id, - ), - Err(e) => ( - nip47::Response { - result_type: nip47::Method::PayKeysend, - error: Some(e), - result: None, - }, - id, - ), - }, - ] - } - nip47::RequestParams::MultiPayKeysend(multi_pay_keysend_request) => { - multi_pay_keysend(plugin.clone(), multi_pay_keysend_request, &label).await - } - nip47::RequestParams::MakeInvoice(make_invoice_request) => { - vec![ - match make_invoice(plugin.clone(), make_invoice_request).await { - Ok(o) => ( - nip47::Response { - result_type: nip47::Method::MakeInvoice, - error: None, - result: Some(nip47::ResponseResult::MakeInvoice(o)), - }, - String::new(), - ), - Err(e) => ( - nip47::Response { - result_type: nip47::Method::MakeInvoice, - error: Some(e), - result: None, - }, - String::new(), - ), - }, - ] - } - nip47::RequestParams::LookupInvoice(lookup_invoice_request) => { - vec![ - match lookup_invoice(plugin.clone(), lookup_invoice_request).await { - Ok(o) => ( - nip47::Response { - result_type: nip47::Method::LookupInvoice, - error: None, - result: Some(nip47::ResponseResult::LookupInvoice(o)), - }, - String::new(), - ), - Err(e) => ( - nip47::Response { - result_type: nip47::Method::LookupInvoice, - error: Some(e), - result: None, - }, - String::new(), - ), - }, - ] - } - nip47::RequestParams::ListTransactions(list_transactions_request) => { - vec![ - match list_transactions(plugin.clone(), list_transactions_request).await { - Ok(o) => ( - nip47::Response { - result_type: nip47::Method::ListTransactions, - error: None, - result: Some(nip47::ResponseResult::ListTransactions(o)), - }, - String::new(), - ), - Err(e) => ( - nip47::Response { - result_type: nip47::Method::ListTransactions, - error: Some(e), - result: None, - }, - String::new(), - ), - }, - ] - } - nip47::RequestParams::GetBalance => { - vec![match get_balance(plugin.clone(), &label).await { - Ok(o) => ( - nip47::Response { - result_type: nip47::Method::GetBalance, - error: None, - result: Some(nip47::ResponseResult::GetBalance(o)), - }, - String::new(), - ), - Err(e) => ( - nip47::Response { - result_type: nip47::Method::GetBalance, - error: Some(e), - result: None, - }, - String::new(), - ), - }] - } - nip47::RequestParams::GetInfo => { - vec![match get_info(plugin.clone(), &label).await { - Ok(o) => ( - nip47::Response { - result_type: nip47::Method::GetInfo, - error: None, - result: Some(nip47::ResponseResult::GetInfo(o)), - }, - String::new(), - ), - Err(e) => ( - nip47::Response { - result_type: nip47::Method::GetInfo, - error: Some(e), - result: None, - }, - String::new(), - ), - }] - } - }; - for (response, id) in responses.into_iter() { - let response_str = match serde_json::to_string(&response) { - Ok(o) => o, - Err(e) => { - log::warn!("Error serializing response! {}", e); - continue; - } - }; - log::debug!("RESPONSE:{}", response_str); + let request = decrypt_request(&event.content, wallet_keys, &client_pubkey, &mut use_nip44)?; - let content = if use_nip44 { - match nip44::encrypt( - wallet_keys.secret_key(), - &client_pubkey, - response_str, - nip44::Version::V2, - ) { - Ok(o) => o, - Err(e) => { - log::warn!("Error encrypting response with nip44! {}", e); - continue; - } - } - } else { - match nip04::encrypt(wallet_keys.secret_key(), &client_pubkey, response_str) { - Ok(o) => o, - Err(e) => { - log::warn!("Error encrypting response with nip04! {}", e); - continue; - } - } - }; - let mut response_builder = EventBuilder::new(Kind::WalletConnectResponse, content) - .tag(Tag::event(event.id)) - .tag(Tag::public_key(client_pubkey)); - if !id.is_empty() { - response_builder = response_builder.tag(Tag::custom( - TagKind::SingleLetter(SingleLetterTag { - character: Alphabet::D, - uppercase: false, + let responses = if event.tags.expiration().is_some() + && event.tags.expiration().unwrap() < Timestamp::now() + { + vec![( + nip47::Response { + result_type: request.method, + error: Some(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: "Event expired".to_owned(), }), - vec![id], - )); + result: None, + }, + None, + )] + } else if event.created_at.as_secs() < (Timestamp::now().as_secs() - ID_MAX_AGE) { + vec![( + nip47::Response { + result_type: request.method, + error: Some(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: "Event created too far in the past".to_owned(), + }), + result: None, + }, + None, + )] + } else { + { + let mut rpc = plugin.state().rpc_lock.lock().await; + save_event_id(&mut rpc, event.id.to_hex(), event.created_at).await?; } - let response_event = match response_builder.sign_with_keys(&wallet_keys) { - Ok(o) => o, - Err(e) => { - log::warn!("Error signing reponse event! {}", e); - continue; + + match request.params { + nip47::RequestParams::PayInvoice(pay_invoice_request) => { + pay_invoice_response(plugin.clone(), pay_invoice_request, label).await } - }; - let send_result = match client.send_event(&response_event).await { + nip47::RequestParams::PayKeysend(pay_keysend_request) => { + pay_keysend_response(plugin.clone(), pay_keysend_request, label).await + } + nip47::RequestParams::MakeInvoice(make_invoice_request) => { + make_invoice_response(plugin.clone(), make_invoice_request).await + } + nip47::RequestParams::LookupInvoice(lookup_invoice_request) => { + lookup_invoice_response(plugin.clone(), lookup_invoice_request).await + } + nip47::RequestParams::ListTransactions(list_transactions_request) => { + list_transactions_response(plugin.clone(), list_transactions_request).await + } + nip47::RequestParams::GetBalance => get_balance_response(plugin.clone(), label).await, + nip47::RequestParams::GetInfo => get_info_response(plugin.clone(), label).await, + nip47::RequestParams::MakeHoldInvoice(make_hold_invoice_request) => { + make_hold_invoice_response(plugin.clone(), make_hold_invoice_request).await + } + nip47::RequestParams::CancelHoldInvoice(cancel_hold_invoice_request) => { + cancel_hold_invoice_response(plugin.clone(), cancel_hold_invoice_request).await + } + nip47::RequestParams::SettleHoldInvoice(settle_hold_invoice_request) => { + settle_hold_invoice_response(plugin.clone(), settle_hold_invoice_request).await + } + } + }; + + for (response, id) in responses { + let content = + match encrypt_response_content(&response, wallet_keys, &client_pubkey, use_nip44) { + Ok(o) => o, + Err(e) => { + log::warn!("{e}"); + continue; + } + }; + + let response_event = + match build_response_event(event.id, content, wallet_keys, client_pubkey, id).await { + Ok(o) => o, + Err(e) => { + log::warn!("Error signing reponse event! {e}"); + continue; + } + }; + + let send_result = match nostr_client.send_event(&response_event).await { Ok(o) => o, Err(e) => { - log::warn!("Error sending response event! {}", e); + log::warn!("Error sending response event! {e}"); continue; } }; @@ -492,8 +332,129 @@ async fn nwc_request_handler( ); continue; } - log::debug!("SENT RESPONSE {:?}", response_event); + for success in send_result.success { + log::trace!("Sent to {}", success.0); + } + for failure in send_result.failed { + log::trace!("Failed to send to {}: {}", failure.0, failure.1); + } + log::trace!("SENT RESPONSE {response_event:?}"); } - Ok(false) + Ok(()) +} + +fn check_nip44_support(event: &Event) -> bool { + for tag in event.tags.iter() { + if tag.kind() == "encryption" { + if let Some(enc_tag_content) = tag.content() { + if enc_tag_content.contains("nip44_v2") { + return true; + } + } + } + } + false +} + +fn decrypt_request( + event_content: &str, + wallet_keys: &Keys, + client_pubkey: &PublicKey, + use_nip44: &mut bool, +) -> Result { + let content = if *use_nip44 { + match nip44::decrypt(wallet_keys.secret_key(), client_pubkey, event_content) { + Ok(o) => o, + Err(e) => { + log::debug!("Could not decrypt using NIP-44:{e}. Trying NIP-04"); + match nip04::decrypt(wallet_keys.secret_key(), client_pubkey, event_content) { + Ok(o) => { + *use_nip44 = false; + o + } + Err(e) => { + log::warn!("Could not decrypt using NIP-04 or NIP-44:{e}"); + return Err(e.into()); + } + } + } + } + } else { + match nip04::decrypt(wallet_keys.secret_key(), client_pubkey, event_content) { + Ok(o) => o, + Err(e) => { + log::debug!("Could not decrypt using NIP-04:{e}. Trying NIP-44"); + match nip44::decrypt(wallet_keys.secret_key(), client_pubkey, event_content) { + Ok(o) => { + *use_nip44 = true; + o + } + Err(e) => { + log::warn!("Could not decrypt using NIP-04 or NIP-44:{e}"); + return Err(e.into()); + } + } + } + } + }; + log::trace!("Decrypted (nip44_v2:{use_nip44}):{content}"); + let request: nip47::Request = match serde_json::from_str(&content) { + Ok(o) => o, + Err(e) => { + log::warn!("Error parsing nip47::Request! {e}"); + return Err(e.into()); + } + }; + Ok(request) +} + +fn encrypt_response_content( + response: &nip47::Response, + wallet_keys: &Keys, + client_pubkey: &PublicKey, + use_nip44: bool, +) -> Result { + let response_str = match serde_json::to_string(&response) { + Ok(o) => o, + Err(e) => { + return Err(anyhow!("Error serializing response! {e}")); + } + }; + log::trace!("RESPONSE:{response_str}"); + if use_nip44 { + match nip44::encrypt( + wallet_keys.secret_key(), + client_pubkey, + response_str, + nip44::Version::V2, + ) { + Ok(o) => Ok(o), + Err(e) => Err(anyhow!("Error encrypting response with nip44! {e}")), + } + } else { + match nip04::encrypt(wallet_keys.secret_key(), client_pubkey, response_str) { + Ok(o) => Ok(o), + Err(e) => Err(anyhow!("Error encrypting response with nip04! {e}")), + } + } +} + +async fn build_response_event( + event_id: EventId, + content: String, + wallet_keys: &Keys, + client_pubkey: PublicKey, + id: Option, +) -> Result { + let mut response_builder = EventBuilder::new(Kind::WalletConnectResponse, content) + .tag(Tag::event(event_id)) + .tag(Tag::public_key(client_pubkey)); + if let Some(i) = id { + response_builder = response_builder.tag(Tag::identifier(i)); + } + match response_builder.finalize_async(wallet_keys).await { + Ok(o) => Ok(o), + Err(e) => Err(e.into()), + } } diff --git a/src/nwc_balance.rs b/src/nwc_balance.rs index f95d17c..2fde546 100644 --- a/src/nwc_balance.rs +++ b/src/nwc_balance.rs @@ -1,12 +1,38 @@ use cln_plugin::Plugin; use cln_rpc::{model::requests::ListpeerchannelsRequest, primitives::ChannelState}; -use nostr_sdk::nips::*; +use nostr::nips::nip47; -use crate::{structs::PluginState, util::load_nwc_store}; +use crate::{ + structs::PluginState, + util::{get_budget_msat, load_nwc_store}, +}; -pub async fn get_balance( +pub async fn get_balance_response( plugin: Plugin, - label: &String, + label: &str, +) -> Vec<(nip47::Response, Option)> { + vec![match get_balance(plugin, label).await { + Ok(o) => ( + nip47::Response { + result_type: nip47::Method::GetBalance, + error: None, + result: Some(nip47::ResponseResult::GetBalance(o)), + }, + None, + ), + Err(e) => ( + nip47::Response { + result_type: nip47::Method::GetBalance, + error: Some(e), + result: None, + }, + None, + ), + }] +} +async fn get_balance( + plugin: Plugin, + label: &str, ) -> Result { let mut rpc = plugin.state().rpc_lock.lock().await; @@ -17,11 +43,15 @@ pub async fn get_balance( message: e.to_string(), })?; - let balance = if let Some(bdgt_amt) = nwc_store.budget_msat { + let balance = if let Some(bdgt_amt) = get_budget_msat(&nwc_store) { bdgt_amt } else { let listpeerchannels = rpc - .call_typed(&ListpeerchannelsRequest { id: None }) + .call_typed(&ListpeerchannelsRequest { + id: None, + short_channel_id: None, + channel_id: None, + }) .await .map_err(|e| nip47::NIP47Error { code: nip47::ErrorCode::Internal, @@ -34,7 +64,7 @@ pub async fn get_balance( || chan.state == ChannelState::CHANNELD_AWAITING_SPLICE { if let Some(spend) = chan.spendable_msat { - amount_msat += spend.msat() + amount_msat += spend.msat(); } } } diff --git a/src/nwc_hold.rs b/src/nwc_hold.rs new file mode 100644 index 0000000..4ce23a3 --- /dev/null +++ b/src/nwc_hold.rs @@ -0,0 +1,248 @@ +use std::str::FromStr; + +use cln_plugin::Plugin; +use cln_rpc::primitives::Sha256; +use nostr::{nips::nip47, types::Timestamp}; + +use crate::{ + hold::{CancelRequest, InvoiceRequest, SettleRequest, invoice_request::Description}, + nwc_notifications::holdinvoice_accepted_handler, + structs::PluginState, +}; + +pub async fn make_hold_invoice_response( + plugin: Plugin, + params: nip47::MakeHoldInvoiceRequest, +) -> Vec<(nip47::Response, Option)> { + vec![match make_hold_invoice(plugin, params).await { + Ok(o) => ( + nip47::Response { + result_type: nip47::Method::MakeHoldInvoice, + error: None, + result: Some(nip47::ResponseResult::MakeHoldInvoice(o)), + }, + None, + ), + Err(e) => ( + nip47::Response { + result_type: nip47::Method::MakeHoldInvoice, + error: Some(e), + result: None, + }, + None, + ), + }] +} + +async fn make_hold_invoice( + plugin: Plugin, + params: nip47::MakeHoldInvoiceRequest, +) -> Result { + let Some(mut hold_client) = plugin.state().hold_client.lock().clone() else { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::NotImplemented, + message: "No hold plugin found".to_owned(), + }); + }; + + let description: Option = if let Some(d_hash) = ¶ms.description_hash { + if let Some(description) = ¶ms.description { + let my_description_hash = Sha256::const_hash(description.as_bytes()); + let description_hash = Sha256::from_str(d_hash).map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + })?; + if my_description_hash != description_hash { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: "description_hash not matching description".to_owned(), + }); + } + } + + let desc_hash_bytes = match hex::decode(d_hash) { + Ok(p) => p, + Err(_e) => { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: "Could not convert description hash to bytes".to_owned(), + }); + } + }; + Some(Description::Hash(desc_hash_bytes)) + } else { + params + .description + .as_ref() + .map(|desc| Description::Memo(desc.clone())) + }; + + let payment_hash = match hex::decode(¶ms.payment_hash) { + Ok(p) => p, + Err(_e) => { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: "Invalid payment hash".to_owned(), + }); + } + }; + + let expiry = params.expiry.unwrap_or(60 * 60); + + let holdinvoice_request = InvoiceRequest { + payment_hash: payment_hash.clone(), + amount_msat: params.amount, + expiry: Some(expiry), + min_final_cltv_expiry: params.min_cltv_expiry_delta.map(u64::from), + routing_hints: Vec::new(), + description, + }; + + let holdinvoice = hold_client + .invoice(holdinvoice_request) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: format!("Error creating hold invoice: {e}"), + })? + .into_inner(); + + let expires_at = Timestamp::now() + expiry; + + let response = nip47::MakeHoldInvoiceResponse { + invoice: Some(holdinvoice.bolt11), + transaction_type: nip47::TransactionType::Incoming, + description: params.description, + description_hash: params.description_hash, + amount: params.amount, + created_at: Timestamp::now(), + expires_at, + metadata: None, + payment_hash: params.payment_hash, + }; + + tokio::spawn(holdinvoice_accepted_handler( + plugin, + payment_hash, + expires_at.as_secs(), + )); + Ok(response) +} + +pub async fn cancel_hold_invoice_response( + plugin: Plugin, + params: nip47::CancelHoldInvoiceRequest, +) -> Vec<(nip47::Response, Option)> { + vec![match cancel_hold_invoice(plugin, params).await { + Ok(o) => ( + nip47::Response { + result_type: nip47::Method::CancelHoldInvoice, + error: None, + result: Some(nip47::ResponseResult::CancelHoldInvoice(o)), + }, + None, + ), + Err(e) => ( + nip47::Response { + result_type: nip47::Method::CancelHoldInvoice, + error: Some(e), + result: None, + }, + None, + ), + }] +} + +async fn cancel_hold_invoice( + plugin: Plugin, + params: nip47::CancelHoldInvoiceRequest, +) -> Result { + let Some(mut hold_client) = plugin.state().hold_client.lock().clone() else { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::NotImplemented, + message: "No hold plugin found".to_owned(), + }); + }; + + let payment_hash = match hex::decode(¶ms.payment_hash) { + Ok(p) => p, + Err(_e) => { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: "Invalid payment hash".to_owned(), + }); + } + }; + + let hold_cancel_request = CancelRequest { payment_hash }; + + let hold_cancel_response = hold_client.cancel(hold_cancel_request).await; + + match hold_cancel_response { + Ok(_o) => Ok(nip47::CancelHoldInvoiceResponse {}), + Err(e) => Err(nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + }), + } +} + +pub async fn settle_hold_invoice_response( + plugin: Plugin, + params: nip47::SettleHoldInvoiceRequest, +) -> Vec<(nip47::Response, Option)> { + vec![match settle_hold_invoice(plugin, params).await { + Ok(o) => ( + nip47::Response { + result_type: nip47::Method::SettleHoldInvoice, + error: None, + result: Some(nip47::ResponseResult::SettleHoldInvoice(o)), + }, + None, + ), + Err(e) => ( + nip47::Response { + result_type: nip47::Method::SettleHoldInvoice, + error: Some(e), + result: None, + }, + None, + ), + }] +} + +async fn settle_hold_invoice( + plugin: Plugin, + params: nip47::SettleHoldInvoiceRequest, +) -> Result { + let Some(mut hold_client) = plugin.state().hold_client.lock().clone() else { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::NotImplemented, + message: "No hold plugin found".to_owned(), + }); + }; + + let preimage = match hex::decode(¶ms.preimage) { + Ok(p) => p, + Err(_e) => { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: "Invalid preimage".to_owned(), + }); + } + }; + + let hold_settle_request = SettleRequest { + payment_preimage: preimage, + }; + + let hold_settle_response = hold_client.settle(hold_settle_request).await; + + match hold_settle_response { + Ok(_o) => Ok(nip47::SettleHoldInvoiceResponse {}), + Err(e) => Err(nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + }), + } +} diff --git a/src/nwc_info.rs b/src/nwc_info.rs index 4a7ac4c..27e196a 100644 --- a/src/nwc_info.rs +++ b/src/nwc_info.rs @@ -1,17 +1,39 @@ -use std::str::FromStr; - use cln_plugin::Plugin; use cln_rpc::model::requests::GetinfoRequest; -use nostr_sdk::nips::*; -use nostr_sdk::*; +use nostr::nips::nip47; -use crate::structs::PluginState; -use crate::util::{is_read_only_nwc, load_nwc_store}; -use crate::{OPT_NOTIFICATIONS, WALLET_ALL_METHODS, WALLET_READ_METHODS}; +use crate::{ + structs::PluginState, + util::{build_methods_vec, build_notifications_vec, is_read_only_nwc, load_nwc_store}, +}; -pub async fn get_info( +pub async fn get_info_response( plugin: Plugin, - label: &String, + label: &str, +) -> Vec<(nip47::Response, Option)> { + vec![match get_info(plugin, label).await { + Ok(o) => ( + nip47::Response { + result_type: nip47::Method::GetInfo, + error: None, + result: Some(nip47::ResponseResult::GetInfo(o)), + }, + None, + ), + Err(e) => ( + nip47::Response { + result_type: nip47::Method::GetInfo, + error: Some(e), + result: None, + }, + None, + ), + }] +} + +async fn get_info( + plugin: Plugin, + label: &str, ) -> Result { let mut rpc = plugin.state().rpc_lock.lock().await; @@ -23,23 +45,12 @@ pub async fn get_info( message: e.to_string(), })?; - let pubkey = - nostr_sdk::secp256k1::PublicKey::from_str(&get_info.id.to_string()).map_err(|e| { - nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - } - })?; + let pubkey = get_info.id.to_string(); let network = match get_info.network.as_str() { "bitcoin" => "mainnet".to_owned(), _ => get_info.network, }; - let notifications = if plugin.option(&OPT_NOTIFICATIONS).unwrap() { - vec!["payment_received".to_owned(), "payment_sent".to_owned()] - } else { - vec![] - }; let nwc_store = load_nwc_store(&mut rpc, label) .await @@ -48,20 +59,12 @@ pub async fn get_info( message: e.to_string(), })?; - let methods = if is_read_only_nwc(&nwc_store) { - WALLET_READ_METHODS - .into_iter() - .map(|s| s.to_owned()) - .collect() - } else { - WALLET_ALL_METHODS - .into_iter() - .map(|s| s.to_owned()) - .collect() - }; + let notifications = build_notifications_vec(&plugin); + + let methods = build_methods_vec(is_read_only_nwc(&nwc_store), &plugin); Ok(nip47::GetInfoResponse { - alias: get_info.alias, + alias: Some(get_info.alias), color: Some(get_info.color), pubkey: Some(pubkey), network: Some(network), diff --git a/src/nwc_invoice.rs b/src/nwc_invoice.rs index 98a06be..f521673 100644 --- a/src/nwc_invoice.rs +++ b/src/nwc_invoice.rs @@ -5,12 +5,36 @@ use cln_rpc::{ model::requests::InvoiceRequest, primitives::{Amount, AmountOrAny, Sha256}, }; -use nostr_sdk::nips::*; +use nostr::{nips::nip47, types::Timestamp}; use uuid::Uuid; use crate::structs::PluginState; -pub async fn make_invoice( +pub async fn make_invoice_response( + plugin: Plugin, + params: nip47::MakeInvoiceRequest, +) -> Vec<(nip47::Response, Option)> { + vec![match make_invoice(plugin, params).await { + Ok(o) => ( + nip47::Response { + result_type: nip47::Method::MakeInvoice, + error: None, + result: Some(nip47::ResponseResult::MakeInvoice(o)), + }, + None, + ), + Err(e) => ( + nip47::Response { + result_type: nip47::Method::MakeInvoice, + error: Some(e), + result: None, + }, + None, + ), + }] +} + +async fn make_invoice( plugin: Plugin, params: nip47::MakeInvoiceRequest, ) -> Result { @@ -18,7 +42,7 @@ pub async fn make_invoice( let mut deschashonly = None; - if let Some(d_hash) = params.description_hash { + if let Some(d_hash) = ¶ms.description_hash { if params.description.is_none() { return Err(nip47::NIP47Error { code: nip47::ErrorCode::Other, @@ -27,7 +51,7 @@ pub async fn make_invoice( } let description = params.description.as_ref().unwrap(); let my_description_hash = Sha256::const_hash(description.as_bytes()); - let description_hash = Sha256::from_str(&d_hash).map_err(|e| nip47::NIP47Error { + let description_hash = Sha256::from_str(d_hash).map_err(|e| nip47::NIP47Error { code: nip47::ErrorCode::Internal, message: e.to_string(), })?; @@ -37,9 +61,15 @@ pub async fn make_invoice( message: "description_hash not matching description".to_owned(), }); } - deschashonly = Some(true) + deschashonly = Some(true); } + let amount_msat = if params.amount == 0 { + AmountOrAny::Any + } else { + AmountOrAny::Amount(Amount::from_msat(params.amount)) + }; + match rpc .call_typed(&InvoiceRequest { cltv: None, @@ -48,15 +78,24 @@ pub async fn make_invoice( preimage: None, exposeprivatechannels: None, fallbacks: None, - amount_msat: AmountOrAny::Amount(Amount::from_msat(params.amount)), - description: params.description.unwrap_or("NWC make_invoice".to_owned()), + amount_msat, + description: params + .description + .clone() + .unwrap_or("NWC make_invoice".to_owned()), label: Uuid::new_v4().to_string(), }) .await { Ok(o) => Ok(nip47::MakeInvoiceResponse { invoice: o.bolt11, - payment_hash: o.payment_hash.to_string(), + payment_hash: Some(o.payment_hash.to_string()), + description: params.description, + description_hash: params.description_hash, + preimage: None, + amount: Some(params.amount), + created_at: Some(Timestamp::now()), + expires_at: Some(Timestamp::from_secs(o.expires_at)), }), Err(e) => Err(nip47::NIP47Error { code: nip47::ErrorCode::Internal, diff --git a/src/nwc_keysend.rs b/src/nwc_keysend.rs index 836985b..aec724d 100644 --- a/src/nwc_keysend.rs +++ b/src/nwc_keysend.rs @@ -1,25 +1,66 @@ -use std::{str::FromStr, time::Duration}; +use std::{collections::HashMap, str::FromStr}; use cln_plugin::Plugin; use cln_rpc::{ - model::requests::KeysendRequest, - primitives::{Amount, PublicKey, TlvEntry, TlvStream}, + ClnRpc, + RpcError, + model::requests::{KeysendRequest, XkeysendRequest}, + primitives::{Amount, PublicKey, Secret, TlvEntry, TlvStream}, }; -use nostr_sdk::nips::*; -use tokio::time; +use nostr::nips::nip47::{self}; use crate::{ structs::PluginState, - util::{budget_amount_check, load_nwc_store, update_nwc_store}, + util::{ + budget_amount_check, + get_budget_msat, + load_nwc_store, + payment_fee_reserve_msat, + refund_budget, + reserve_budget, + rpc_socket_path, + settle_budget, + }, }; -pub async fn pay_keysend( +pub const XKEYSEND_COMMAND: &str = "xkeysend"; + +pub async fn pay_keysend_response( plugin: Plugin, params: nip47::PayKeysendRequest, - label: &String, -) -> Result { - let mut rpc = plugin.state().rpc_lock.lock().await; + label: &str, +) -> Vec<(nip47::Response, Option)> { + let id = if let Some(i) = params.id.clone() { + i + } else { + params.pubkey.clone() + }; + vec![match pay_keysend(plugin, params, label).await { + Ok(o) => ( + nip47::Response { + result_type: nip47::Method::PayKeysend, + error: None, + result: Some(nip47::ResponseResult::PayKeysend(o)), + }, + Some(id), + ), + Err(e) => ( + nip47::Response { + result_type: nip47::Method::PayKeysend, + error: Some(e), + result: None, + }, + Some(id), + ), + }] +} + +async fn pay_keysend( + plugin: Plugin, + params: nip47::PayKeysendRequest, + label: &str, +) -> Result { if params.preimage.is_some() { return Err(nip47::NIP47Error { code: nip47::ErrorCode::Other, @@ -27,32 +68,142 @@ pub async fn pay_keysend( }); } - let mut nwc_store = load_nwc_store(&mut rpc, label) - .await - .map_err(|e| nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - })?; - - budget_amount_check(Some(params.amount), None, nwc_store.budget_msat).map_err(|e| { - nip47::NIP47Error { - code: nip47::ErrorCode::QuotaExceeded, - message: e.to_string(), - } - })?; - let pubkey = PublicKey::from_str(¶ms.pubkey).map_err(|e| nip47::NIP47Error { code: nip47::ErrorCode::Other, message: e.to_string(), })?; + let reservation = { + let mut rpc = plugin.state().rpc_lock.lock().await; + + let nwc_store = load_nwc_store(&mut rpc, label) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + })?; + + budget_amount_check(Some(params.amount), None, get_budget_msat(&nwc_store)).map_err( + |e| nip47::NIP47Error { + code: nip47::ErrorCode::QuotaExceeded, + message: e.to_string(), + }, + )?; + + // Reserve amount plus worst case fee so concurrent payments can never + // exceed the budget. + if get_budget_msat(&nwc_store).unwrap_or(u64::MAX) + < params + .amount + .saturating_add(payment_fee_reserve_msat(params.amount)) + { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::QuotaExceeded, + message: "Payment and estimated fees exceed the available budget".to_owned(), + }); + } + + reserve_budget(&mut rpc, label, &nwc_store, params.amount) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + })? + }; + + let has_xkeysend = plugin.state().config.lock().has_xkeysend; + let mut pay_rpc = + ClnRpc::new(rpc_socket_path(&plugin)) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: format!("Could not connect to lightningd: {e}"), + })?; + + let pay_result = if has_xkeysend { + xkeysend(&mut pay_rpc, ¶ms, pubkey).await + } else { + keysend(&mut pay_rpc, ¶ms, pubkey).await + }; + + match pay_result { + Ok((amount_sent_msat, amount_msat, preimage)) => { + let mut rpc = plugin.state().rpc_lock.lock().await; + if let Err(e) = settle_budget(&mut rpc, label, reservation, amount_sent_msat).await { + log::error!("Error updating budget after successful keysend: {e}"); + } + + let preimage = hex::encode(preimage.to_vec()); + let fees_paid = amount_sent_msat.saturating_sub(amount_msat); + + Ok(nip47::PayKeysendResponse { + preimage, + fees_paid: Some(fees_paid), + }) + } + Err(e) => { + let mut rpc = plugin.state().rpc_lock.lock().await; + if let Err(refund_err) = refund_budget(&mut rpc, label, reservation).await { + log::error!( + "Error refunding budget reservation after failed keysend: {refund_err}" + ); + } + Err(map_keysend_error(&e, has_xkeysend)) + } + } +} + +async fn xkeysend( + pay_rpc: &mut ClnRpc, + params: &nip47::PayKeysendRequest, + pubkey: PublicKey, +) -> Result<(u64, u64, Secret), RpcError> { + let mut extratlvs = HashMap::with_capacity(params.tlv_records.len()); + for tlv in ¶ms.tlv_records { + extratlvs.insert(tlv.tlv_type.to_string(), tlv.value.clone()); + } + let extratlvs = if extratlvs.is_empty() { + None + } else { + Some(extratlvs) + }; + + let o = pay_rpc + .call_typed(&XkeysendRequest { + extratlvs, + label: None, + maxdelay: None, + maxfee: None, + retry_for: None, + layers: None, + amount_msat: Amount::from_msat(params.amount), + destination: pubkey, + }) + .await?; + + Ok(( + o.amount_sent_msat.msat(), + o.amount_msat.msat(), + o.payment_preimage, + )) +} + +async fn keysend( + pay_rpc: &mut ClnRpc, + params: &nip47::PayKeysendRequest, + pubkey: PublicKey, +) -> Result<(u64, u64, Secret), RpcError> { let mut extratlvs = TlvStream { entries: Vec::new(), }; - for tlv in params.tlv_records { + for tlv in ¶ms.tlv_records { extratlvs.entries.push(TlvEntry { typ: tlv.tlv_type, - value: tlv.value.as_bytes().to_owned(), + value: hex::decode(&tlv.value).map_err(|e| RpcError { + code: Some(-32700), + message: format!("Could not decode tlv bytes: {e}"), + data: None, + })?, }); } let extratlvs = if extratlvs.entries.is_empty() { @@ -61,7 +212,7 @@ pub async fn pay_keysend( Some(extratlvs) }; - match rpc + let o = pay_rpc .call_typed(&KeysendRequest { exemptfee: None, extratlvs, @@ -74,74 +225,48 @@ pub async fn pay_keysend( amount_msat: Amount::from_msat(params.amount), destination: pubkey, }) - .await - { - Ok(o) => { - if let Some(ref mut bdg) = nwc_store.budget_msat { - *bdg = bdg.saturating_sub(o.amount_sent_msat.msat()); - update_nwc_store(&mut rpc, label, nwc_store) - .await - .map_err(|e| nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - })?; - } + .await?; - let preimage = hex::encode(o.payment_preimage.to_vec()); - Ok(nip47::PayKeysendResponse { preimage }) - } - Err(e) => match e.code { - Some(c) => match c { - 203 | 205 | 210 => Err(nip47::NIP47Error { - code: nip47::ErrorCode::PaymentFailed, - message: e.to_string(), - }), - 206 => Err(nip47::NIP47Error { - code: nip47::ErrorCode::InsufficientBalance, - message: e.to_string(), - }), - _ => Err(nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - }), - }, - None => Err(nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - }), - }, - } + Ok(( + o.amount_sent_msat.msat(), + o.amount_msat.msat(), + o.payment_preimage, + )) } -pub async fn multi_pay_keysend( - plugin: Plugin, - params: nip47::MultiPayKeysendRequest, - label: &String, -) -> Vec<(nip47::Response, String)> { - let mut responses = Vec::new(); - for pay in params.keysends { - let result = pay_keysend(plugin.clone(), pay.clone(), label).await; - let id = if let Some(i) = pay.id { i } else { pay.pubkey }; - let response_res = match result { - Ok(resp) => ( - nip47::Response { - result_type: nip47::Method::MultiPayKeysend, - error: None, - result: Some(nip47::ResponseResult::MultiPayKeysend(resp)), - }, - id, - ), - Err(e) => ( - nip47::Response { - result_type: nip47::Method::MultiPayKeysend, - error: Some(e), - result: None, - }, - id, - ), +fn map_keysend_error(e: &RpcError, is_xkeysend: bool) -> nip47::NIP47Error { + let Some(c) = e.code else { + return nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), }; - responses.push(response_res); - time::sleep(Duration::from_millis(100)).await; + }; + + let failed_codes = if is_xkeysend { + vec![203, 205, 207, 219] + } else { + vec![203, 205, 210] + }; + + if failed_codes.contains(&c) { + nip47::NIP47Error { + code: nip47::ErrorCode::PaymentFailed, + message: e.to_string(), + } + } else if is_xkeysend && c == 209 { + nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: e.to_string(), + } + } else if !is_xkeysend && c == 206 { + nip47::NIP47Error { + code: nip47::ErrorCode::InsufficientBalance, + message: e.to_string(), + } + } else { + nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + } } - responses } diff --git a/src/nwc_lookups.rs b/src/nwc_lookups.rs index b3b97ce..42ce1f6 100644 --- a/src/nwc_lookups.rs +++ b/src/nwc_lookups.rs @@ -2,18 +2,69 @@ use std::{cmp::Reverse, str::FromStr}; use cln_plugin::Plugin; use cln_rpc::{ + ClnRpc, model::{ - requests::{DecodeRequest, ListinvoicesRequest, ListpaysRequest}, - responses::{ListinvoicesInvoicesStatus, ListpaysPaysStatus}, + requests::{ + DecodeRequest, + ListinvoicesIndex, + ListinvoicesRequest, + ListpaysRequest, + WaitIndexname, + WaitRequest, + WaitSubsystem, + }, + responses::{ + DecodeResponse, + DecodeType, + ListinvoicesInvoices, + ListinvoicesInvoicesStatus, + ListpaysPays, + ListpaysPaysStatus, + }, }, primitives::Sha256, }; -use nostr_sdk::nips::*; -use nostr_sdk::*; +use nostr::{nips::nip47, types::Timestamp}; +use tonic::transport::Channel; -use crate::structs::PluginState; +use crate::{ + hold::{self, ListRequest, hold_client::HoldClient, list_request::Constraint}, + structs::{NOT_INV_ERR, PluginState}, + util::rpc_socket_path, +}; -pub async fn lookup_invoice( +#[allow(clippy::doc_markdown)] +/// Hard bound on the number of transactions a single `list_transactions` +/// request will fetch, decode and return (DoS protection). +const MAX_TRANSACTIONS: usize = 500; +/// Bound on the size of a `list_transactions` response in bytes. +const RESPONSE_LIMIT_BYTES: usize = 127 * 1024; + +pub async fn lookup_invoice_response( + plugin: Plugin, + params: nip47::LookupInvoiceRequest, +) -> Vec<(nip47::Response, Option)> { + vec![match lookup_invoice(plugin, params).await { + Ok(o) => ( + nip47::Response { + result_type: nip47::Method::LookupInvoice, + error: None, + result: Some(nip47::ResponseResult::LookupInvoice(o)), + }, + None, + ), + Err(e) => ( + nip47::Response { + result_type: nip47::Method::LookupInvoice, + error: Some(e), + result: None, + }, + None, + ), + }] +} + +async fn lookup_invoice( plugin: Plugin, params: nip47::LookupInvoiceRequest, ) -> Result { @@ -26,15 +77,10 @@ pub async fn lookup_invoice( }); } - let not_invoice_err = Err(nip47::NIP47Error { - code: nip47::ErrorCode::Other, - message: "Not an invoice or invalid invoice".to_owned(), - }); - let invoice = if params.payment_hash.is_some() && params.invoice.is_some() { None } else { - params.invoice + params.invoice.clone() }; let invoices = rpc @@ -55,214 +101,339 @@ pub async fn lookup_invoice( .invoices; if invoices.len() == 1 { - let invoice_response = invoices.first().cloned().unwrap(); - let invstring = if invoice_response.bolt11.is_some() { - invoice_response.bolt11.unwrap() + let invoice_response = invoices.into_iter().next().unwrap(); + + return make_lookup_response_from_listinvoices(&mut rpc, invoice_response).await; + } + + let payment_hash_hash = if let Some(hash) = ¶ms.payment_hash { + if let Ok(res) = Sha256::from_str(hash) { + Some(res) } else { - invoice_response.bolt12.unwrap() + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: "Could not convert payment hash".to_owned(), + }); + } + } else { + None + }; + + let pays = rpc + .call_typed(&ListpaysRequest { + bolt11: invoice, + index: None, + limit: None, + payment_hash: payment_hash_hash, + start: None, + status: None, + }) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + })? + .pays; + + if pays.len() == 1 { + let list_pay = pays.into_iter().next().unwrap(); + + return make_lookup_response_from_listpays(&mut rpc, list_pay).await; + } + + let holdinvoice_support = plugin.state().hold_client.lock().is_some(); + + if holdinvoice_support { + let mut hold_client = plugin.state().hold_client.lock().clone().unwrap(); + return lookup_holdinvoice(&mut hold_client, &mut rpc, params).await; + } + + Err(nip47::NIP47Error { + code: nip47::ErrorCode::NotFound, + message: "Transaction not found".to_owned(), + }) +} + +async fn lookup_holdinvoice( + hold_client: &mut HoldClient, + rpc: &mut ClnRpc, + params: nip47::LookupInvoiceRequest, +) -> Result { + log::debug!("Looking up hold invoice for params {params:#?}"); + let (hold_invoice, invoice_decoded) = + get_and_decode_holdinvoice(rpc, hold_client, params).await?; + let not_invoice_err = Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: NOT_INV_ERR.to_owned(), + }); + + let description = match invoice_decoded.item_type { + DecodeType::BOLT12_INVOICE => invoice_decoded.offer_description, + DecodeType::BOLT11_INVOICE => invoice_decoded.description, + _ => return not_invoice_err, + }; + let description_hash = match invoice_decoded.item_type { + DecodeType::BOLT12_INVOICE => None, + DecodeType::BOLT11_INVOICE => invoice_decoded.description_hash.map(|h| h.to_string()), + _ => return not_invoice_err, + }; + + let created_at = match invoice_decoded.item_type { + DecodeType::BOLT12_INVOICE => { + Timestamp::from_secs(invoice_decoded.invoice_created_at.unwrap()) + } + DecodeType::BOLT11_INVOICE => Timestamp::from_secs(invoice_decoded.created_at.unwrap()), + _ => return not_invoice_err, + }; + + let amount = match invoice_decoded.item_type { + DecodeType::BOLT12_INVOICE => invoice_decoded.invoice_amount_msat.unwrap().msat(), + DecodeType::BOLT11_INVOICE => { + if let Some(amt) = invoice_decoded.amount_msat { + amt.msat() + } else { + // amount: `any` but have to put a value... + 0 + } + } + _ => return not_invoice_err, + }; + + let expires_at = match invoice_decoded.item_type { + DecodeType::BOLT12_INVOICE => invoice_decoded + .invoice_relative_expiry + .map(|e_at| created_at + Timestamp::from_secs(u64::from(e_at))), + DecodeType::BOLT11_INVOICE => invoice_decoded + .expiry + .map(|e_at| created_at + Timestamp::from_secs(e_at)), + _ => return not_invoice_err, + }; + + let state = match hold_invoice.state() { + hold::InvoiceState::Unpaid => nip47::TransactionState::Pending, + hold::InvoiceState::Accepted => nip47::TransactionState::Accepted, + hold::InvoiceState::Paid => nip47::TransactionState::Settled, + hold::InvoiceState::Cancelled => nip47::TransactionState::Expired, + }; + + let settled_at = if hold_invoice.settled_at() != 0 { + Some(Timestamp::from_secs(hold_invoice.settled_at())) + } else { + None + }; + + let preimage = if hold_invoice.state() == hold::InvoiceState::Paid { + Some(hex::encode(hold_invoice.preimage())) + } else { + None + }; + + Ok(nip47::LookupInvoiceResponse { + transaction_type: Some(nip47::TransactionType::Incoming), + invoice: Some(hold_invoice.invoice.clone()), + description, + description_hash, + preimage, + payment_hash: hex::encode(hold_invoice.payment_hash), + amount, + fees_paid: 0, + created_at, + expires_at, + settled_at, + metadata: None, + state: Some(state), + }) +} + +#[allow(clippy::too_many_lines)] +async fn get_and_decode_holdinvoice( + rpc: &mut ClnRpc, + hold_client: &mut HoldClient, + params: nip47::LookupInvoiceRequest, +) -> Result<(hold::Invoice, DecodeResponse), nip47::NIP47Error> { + if let Some(ph) = ¶ms.payment_hash { + let payment_hash_hash = match hex::decode(ph) { + Ok(p) => p, + Err(_e) => { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: "Invalid payment hash".to_owned(), + }); + } }; + let list_request = ListRequest { + constraint: Some(Constraint::PaymentHash(payment_hash_hash)), + }; + let hold_lookup = hold_client + .list(list_request) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: format!("Could not fetch hold invoice: {e}"), + })? + .into_inner(); + + if hold_lookup.invoices.len() != 1 { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: "Transaction not found".to_owned(), + }); + } + + let hold_invoice = hold_lookup.invoices.into_iter().next().unwrap(); let invoice_decoded = rpc .call_typed(&DecodeRequest { - string: invstring.clone(), + string: hold_invoice.invoice.clone(), }) .await .map_err(|e| nip47::NIP47Error { code: nip47::ErrorCode::Internal, message: e.to_string(), })?; - if !invoice_decoded.valid { - return not_invoice_err; + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: "Invalid invoice decoded".to_owned(), + }); } - let description = match invoice_decoded.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { - invoice_decoded.offer_description - } - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => invoice_decoded.description, - _ => return not_invoice_err, - }; - let description_hash = match invoice_decoded.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => None, - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { - invoice_decoded.description_hash.map(|h| h.to_string()) - } - _ => return not_invoice_err, - }; - - let amount = match invoice_decoded.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { - invoice_decoded.invoice_amount_msat.unwrap().msat() - } - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { - if let Some(amt) = invoice_decoded.amount_msat { - amt.msat() - } else if let Some(a) = invoice_response.amount_msat { - a.msat() - } else { - // amount: `any` but have to put a value... - 0 - } - } - _ => return not_invoice_err, - }; - - let created_at = match invoice_decoded.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { - Timestamp::from_secs(invoice_decoded.invoice_created_at.unwrap()) - } - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { - Timestamp::from_secs(invoice_decoded.created_at.unwrap()) - } - _ => return not_invoice_err, - }; - - let preimage = invoice_response - .payment_preimage - .map(|p| hex::encode(p.to_vec())); - - Ok(nip47::LookupInvoiceResponse { - transaction_type: Some(nip47::TransactionType::Incoming), - invoice: Some(invstring), - description, - description_hash, - preimage, - payment_hash: invoice_response.payment_hash.to_string(), - amount, - fees_paid: 0, - created_at, - expires_at: Some(Timestamp::from_secs(invoice_response.expires_at)), - settled_at: invoice_response.paid_at.map(Timestamp::from_secs), - metadata: None, - }) + Ok((hold_invoice, invoice_decoded)) } else { - let payment_hash_hash = if let Some(hash) = params.payment_hash { - if let Ok(res) = Sha256::from_str(&hash) { - Some(res) - } else { - return Err(nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: "Could not convert payment hash".to_owned(), - }); - } - } else { - None - }; - - let pays = rpc - .call_typed(&ListpaysRequest { - bolt11: invoice, - index: None, - limit: None, - payment_hash: payment_hash_hash, - start: None, - status: None, + let invoice_decoded = rpc + .call_typed(&DecodeRequest { + string: params.invoice.unwrap(), }) .await .map_err(|e| nip47::NIP47Error { code: nip47::ErrorCode::Internal, message: e.to_string(), - })? - .pays; - - if pays.len() != 1 { + })?; + if !invoice_decoded.valid { return Err(nip47::NIP47Error { - code: nip47::ErrorCode::NotFound, + code: nip47::ErrorCode::Other, + message: "Invalid invoice decoded".to_owned(), + }); + } + + let ph = match invoice_decoded.item_type { + DecodeType::BOLT12_INVOICE => invoice_decoded.invoice_payment_hash.unwrap(), + DecodeType::BOLT11_INVOICE => invoice_decoded.payment_hash.unwrap().to_string(), + _ => { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: "Not a supported invoice type".to_owned(), + }); + } + }; + let payment_hash_hash = match hex::decode(&ph) { + Ok(p) => p, + Err(_e) => { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: "Invalid payment hash in invoice".to_owned(), + }); + } + }; + + let list_request = ListRequest { + constraint: Some(Constraint::PaymentHash(payment_hash_hash)), + }; + + let hold_lookup = hold_client + .list(list_request) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: format!("Could not fetch hold invoice: {e}"), + })? + .into_inner(); + + if hold_lookup.invoices.len() != 1 { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, message: "Transaction not found".to_owned(), }); } - let list_pay = pays.first().unwrap().clone(); - let invstring = if list_pay.bolt11.is_some() { - list_pay.bolt11 - } else { - list_pay.bolt12 - }; - let invoice_decoded = if let Some(invstr) = &invstring { - Some( - rpc.call_typed(&DecodeRequest { - string: invstr.clone(), - }) - .await - .map_err(|e| nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - })?, - ) - } else { - None - }; - - if invoice_decoded.is_some() && !invoice_decoded.as_ref().unwrap().valid { - return not_invoice_err; + let hold_invoice = hold_lookup.invoices.into_iter().next().unwrap(); + let invoice_decoded = rpc + .call_typed(&DecodeRequest { + string: hold_invoice.invoice.clone(), + }) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + })?; + if !invoice_decoded.valid { + return Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: "Invalid invoice decoded".to_owned(), + }); } - - let description_hash = if let Some(inv_dec) = &invoice_decoded { - match inv_dec.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => None, - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { - inv_dec.description_hash.map(|h| h.to_string()) - } - _ => return not_invoice_err, - } - } else { - None - }; - let amount = if let Some(amt) = list_pay.amount_msat { - amt.msat() - } else if let Some(inv_dec) = &invoice_decoded { - match inv_dec.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { - inv_dec.invoice_amount_msat.unwrap().msat() - } - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { - if let Some(amt) = inv_dec.amount_msat { - amt.msat() - } else { - return not_invoice_err; - } - } - _ => return not_invoice_err, - } - } else { - return not_invoice_err; - }; - - let description = if let Some(inv_dec) = invoice_decoded { - match inv_dec.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => inv_dec.offer_description, - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => inv_dec.description, - _ => return not_invoice_err, - } - } else { - list_pay.description - }; - - let fees_paid = list_pay.amount_sent_msat.unwrap().msat() - amount; - let preimage = list_pay.preimage.map(|p| hex::encode(p.to_vec())); - - Ok(nip47::LookupInvoiceResponse { - transaction_type: Some(nip47::TransactionType::Outgoing), - invoice: invstring, - description, - description_hash, - preimage, - payment_hash: list_pay.payment_hash.to_string(), - amount, - fees_paid, - created_at: Timestamp::from_secs(list_pay.created_at), - expires_at: None, - settled_at: list_pay.completed_at.map(Timestamp::from_secs), - metadata: None, - }) + Ok((hold_invoice, invoice_decoded)) } } -pub async fn list_transactions( +pub async fn list_transactions_response( + plugin: Plugin, + params: nip47::ListTransactionsRequest, +) -> Vec<(nip47::Response, Option)> { + vec![match list_transactions(plugin, params).await { + Ok(o) => ( + nip47::Response { + result_type: nip47::Method::ListTransactions, + error: None, + result: Some(nip47::ResponseResult::ListTransactions(o)), + }, + None, + ), + Err(e) => ( + nip47::Response { + result_type: nip47::Method::ListTransactions, + error: Some(e), + result: None, + }, + None, + ), + }] +} + +async fn list_transactions( plugin: Plugin, params: nip47::ListTransactionsRequest, ) -> Result, nip47::NIP47Error> { - let mut rpc = plugin.state().rpc_lock.lock().await; + if params.limit == Some(0) { + return Ok(Vec::new()); + } + + let mut rpc = ClnRpc::new(rpc_socket_path(&plugin)) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: format!("Could not connect to lightningd: {e}"), + })?; + + let limit = usize::try_from(params.limit.unwrap_or(MAX_TRANSACTIONS as u64)).map_err(|e| { + nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: format!("32-bit usize limit exceeded: {e}"), + } + })?; + let offset = usize::try_from(params.offset.unwrap_or(0)).map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: format!("32-bit usize limit exceeded: {e}"), + })?; + let want = limit.saturating_add(offset); + // Enough candidates from each source to satisfy the requested window even + // if some get filtered out again by `from`/`until`/`unpaid`. + let per_source = want + .saturating_mul(2) + .saturating_add(16) + .min(MAX_TRANSACTIONS); + let from = params.from.map(|t| t.as_secs()); + let until = params.until.map(|t| t.as_secs()); + let unpaid = params.unpaid.unwrap_or(false); let (query_invoices, query_payments) = match params.transaction_type { Some(t) => match t { @@ -272,21 +443,140 @@ pub async fn list_transactions( None => (true, true), }; - let from = params.from.map(|f| f.as_u64()); - let until = params.until.map(|f| f.as_u64()); - let unpaid = params.unpaid.unwrap_or(false); - let mut transactions: Vec = Vec::new(); + if query_invoices { - let list_invoices = rpc + let invoices = collect_invoices(&mut rpc, per_source, from, until, unpaid).await?; + transactions.extend(invoices); + + let hold_client = plugin.state().hold_client.lock().clone(); + if let Some(mut hold_client) = hold_client { + list_holdinvoices_to_transactions( + &mut hold_client, + &mut rpc, + per_source, + from, + until, + &mut transactions, + ) + .await?; + } + } + + if query_payments { + let pays = collect_pays(&mut rpc, per_source, from, until).await?; + transactions.extend(pays); + } + + transactions.sort_by_key(|t| Reverse(t.created_at)); + + if let Some(offset) = params.offset { + let len = transactions.len() as u64; + if offset >= len { + transactions.clear(); + } else { + let off = usize::try_from(offset).unwrap(); + transactions.drain(0..off); + } + } + + if let Some(limit) = params.limit { + let len = transactions.len() as u64; + if limit < len { + let l = usize::try_from(limit).unwrap(); + transactions = transactions.drain(0..l).collect(); + } + } + + transactions = trim_to_size(transactions, RESPONSE_LIMIT_BYTES); + + Ok(transactions) +} + +async fn max_invoice_created_index(rpc: &mut ClnRpc) -> Result, cln_rpc::RpcError> { + let current_index = rpc + .call_typed(&WaitRequest { + indexname: WaitIndexname::CREATED, + subsystem: WaitSubsystem::INVOICES, + nextvalue: 0, + }) + .await? + .created + .ok_or_else(|| cln_rpc::RpcError { + code: Some(-32700), + message: "Missing created field in wait response".to_owned(), + data: None, + })?; + + if current_index == 0 { + return Ok(None); + } + Ok(Some(current_index)) +} + +async fn first_invoice_created_index(rpc: &mut ClnRpc) -> Result { + let first_index = rpc + .call_typed(&ListinvoicesRequest { + label: None, + invstring: None, + payment_hash: None, + offer_id: None, + index: Some(ListinvoicesIndex::CREATED), + start: Some(0), + limit: Some(1), + }) + .await? + .invoices + .first() + .map_or(0, |i| i.created_index); + Ok(first_index) +} + +/// Collect up to `per_source` most recent paid/public invoices within +/// `[from, until]` by paging backwards through `created_index`, decoding only +/// what is needed. Bounded by `MAX_TRANSACTIONS` regardless of node history. +async fn collect_invoices( + rpc: &mut ClnRpc, + per_source: usize, + from: Option, + until: Option, + unpaid: bool, +) -> Result, nip47::NIP47Error> { + const PAGE: u32 = 200; + const MAX_RAW_ROWS: usize = MAX_TRANSACTIONS * 8; + + let Some(max_idx) = max_invoice_created_index(rpc) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + })? + else { + return Ok(Vec::new()); + }; + + let first_index = first_invoice_created_index(rpc) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + })?; + + let mut out = Vec::new(); + let mut raw_seen = 0usize; + let mut start_excl = max_idx.saturating_add(1); + + loop { + let page_start = start_excl.saturating_sub(u64::from(PAGE)); + let rows = rpc .call_typed(&ListinvoicesRequest { - index: None, + index: Some(ListinvoicesIndex::CREATED), invstring: None, label: None, - limit: None, + limit: Some(PAGE), offer_id: None, payment_hash: None, - start: None, + start: Some(page_start), }) .await .map_err(|e| nip47::NIP47Error { @@ -295,231 +585,490 @@ pub async fn list_transactions( })? .invoices; - for list_invoice in list_invoices.into_iter() { - if list_invoice.status == ListinvoicesInvoicesStatus::EXPIRED { - continue; + // Process newest first within the page. + for row in rows.into_iter().rev() { + raw_seen += 1; + if raw_seen > MAX_RAW_ROWS { + return Ok(out); } - if !unpaid && list_invoice.status == ListinvoicesInvoicesStatus::UNPAID { - continue; - } - let invstring = if list_invoice.bolt11.is_some() { - list_invoice.bolt11.unwrap() - } else { - list_invoice.bolt12.unwrap() - }; - let invoice_decoded = rpc - .call_typed(&DecodeRequest { - string: invstring.clone(), - }) - .await - .map_err(|e| nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - })?; - - if !invoice_decoded.valid { + if row.created_index >= start_excl { continue; } - let created_at = match invoice_decoded.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { - Timestamp::from_secs(invoice_decoded.invoice_created_at.unwrap()) - } - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { - Timestamp::from_secs(invoice_decoded.created_at.unwrap()) - } - _ => continue, - }; - if let Some(f) = from { - if created_at.as_u64() < f { - continue; - } + + if !unpaid && row.status == ListinvoicesInvoicesStatus::UNPAID { + continue; } - if let Some(u) = until { - if created_at.as_u64() > u { - continue; - } - } - let description = match invoice_decoded.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { - invoice_decoded.offer_description - } - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { - invoice_decoded.description - } - _ => continue, - }; - let description_hash = match invoice_decoded.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => None, - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { - invoice_decoded.description_hash.map(|h| h.to_string()) - } - _ => continue, - }; - let amount = match invoice_decoded.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { - invoice_decoded.invoice_amount_msat.unwrap().msat() - } - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { - if let Some(amt) = invoice_decoded.amount_msat { - amt.msat() - } else { - // amount: `any` but have to put a value... - 0 + + match make_lookup_response_from_listinvoices(rpc, row).await { + Ok(tx) => { + let created = tx.created_at.as_secs(); + if let Some(f) = from { + // Newest first, so everything older is also out of range. + if created < f { + return Ok(out); + } + } + if let Some(u) = until { + if created > u { + continue; + } + } + out.push(tx); + if out.len() >= per_source { + return Ok(out); } } - _ => continue, - }; - let expires_at = match invoice_decoded.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { - invoice_decoded.invoice_relative_expiry.map(|e_at| { - Timestamp::from_secs( - invoice_decoded.invoice_created_at.unwrap() + (e_at as u64), - ) - }) - } - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => invoice_decoded - .expiry - .map(|e_at| Timestamp::from_secs(invoice_decoded.created_at.unwrap() + e_at)), - _ => continue, - }; - let preimage = list_invoice - .payment_preimage - .map(|p| hex::encode(p.to_vec())); + Err(_e) => (), + } + } - transactions.push(nip47::LookupInvoiceResponse { - transaction_type: Some(nip47::TransactionType::Incoming), - invoice: Some(invstring), - description, - description_hash, - preimage, - payment_hash: list_invoice.payment_hash.to_string(), - amount, - fees_paid: 0, - created_at, - expires_at, - settled_at: list_invoice.paid_at.map(Timestamp::from_secs), - metadata: None, - }); + if page_start == 0 || page_start < first_index { + break; + } + start_excl = page_start; + } + + Ok(out) +} + +/// Collect up to `per_source` most recent payments within `[from, until]`, +/// decoding only the kept subset. `listpays` returns `created_at` directly, so +/// the whole payment history can be filtered before any decode. +async fn collect_pays( + rpc: &mut ClnRpc, + per_source: usize, + from: Option, + until: Option, +) -> Result, nip47::NIP47Error> { + let pays = rpc + .call_typed(&ListpaysRequest { + bolt11: None, + index: None, + limit: None, + payment_hash: None, + start: None, + status: None, + }) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + })? + .pays; + + // Newest first. + let mut pays = pays; + pays.sort_by_key(|p| Reverse((p.created_at, p.created_index.unwrap_or(0)))); + + let mut out = Vec::new(); + for pay in pays { + if out.len() >= per_source { + break; + } + if let Some(u) = until { + if pay.created_at > u { + continue; + } + } + if let Some(f) = from { + // Newest first, so the rest are also out of range. + if pay.created_at < f { + break; + } + } + + match make_lookup_response_from_listpays(rpc, pay).await { + Ok(tx) => out.push(tx), + Err(_e) => (), } } - if query_payments { - let list_pays = rpc - .call_typed(&ListpaysRequest { - bolt11: None, - index: None, - limit: None, - payment_hash: None, - start: None, - status: None, + Ok(out) +} + +async fn list_holdinvoices_to_transactions( + hold_client: &mut HoldClient, + rpc: &mut ClnRpc, + per_source: usize, + from: Option, + until: Option, + transactions: &mut Vec, +) -> Result<(), nip47::NIP47Error> { + let lookup_request = ListRequest { constraint: None }; + + let hold_lookup = hold_client + .list(lookup_request) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: format!("Could not fetch hold invoices: {e}"), + })? + .into_inner(); + + // Decode only the most recent `per_source` hold invoices (newest first). + let mut hold_invoices = hold_lookup.invoices; + hold_invoices.sort_by_key(|inv| Reverse(inv.created_at)); + + let mut kept = 0usize; + for hold_invoice in hold_invoices { + if kept >= per_source { + break; + } + if let Some(u) = until { + if hold_invoice.created_at > u { + continue; + } + } + if let Some(f) = from { + // Newest first, so the rest are also out of range. + if hold_invoice.created_at < f { + break; + } + } + + match make_lookup_response_from_holdinvoice(rpc, &hold_invoice).await { + Ok(tx) => { + transactions.push(tx); + kept += 1; + } + Err(_e) => (), + } + } + + Ok(()) +} + +async fn make_lookup_response_from_holdinvoice( + rpc: &mut ClnRpc, + hold_invoice: &hold::Invoice, +) -> Result { + let not_invoice_err = Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: NOT_INV_ERR.to_owned(), + }); + + let invoice_decoded = rpc + .call_typed(&DecodeRequest { + string: hold_invoice.invoice.clone(), + }) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + })?; + + let description = match invoice_decoded.item_type { + DecodeType::BOLT12_INVOICE => invoice_decoded.offer_description, + DecodeType::BOLT11_INVOICE => invoice_decoded.description, + _ => return not_invoice_err, + }; + let description_hash = match invoice_decoded.item_type { + DecodeType::BOLT12_INVOICE => None, + DecodeType::BOLT11_INVOICE => invoice_decoded.description_hash.map(|h| h.to_string()), + _ => return not_invoice_err, + }; + + let created_at = match invoice_decoded.item_type { + DecodeType::BOLT12_INVOICE => { + Timestamp::from_secs(invoice_decoded.invoice_created_at.unwrap()) + } + DecodeType::BOLT11_INVOICE => Timestamp::from_secs(invoice_decoded.created_at.unwrap()), + _ => return not_invoice_err, + }; + + let amount = match invoice_decoded.item_type { + DecodeType::BOLT12_INVOICE => invoice_decoded.invoice_amount_msat.unwrap().msat(), + DecodeType::BOLT11_INVOICE => { + if let Some(amt) = invoice_decoded.amount_msat { + amt.msat() + } else { + // amount: `any` but have to put a value... + 0 + } + } + _ => return not_invoice_err, + }; + + let expires_at = match invoice_decoded.item_type { + DecodeType::BOLT12_INVOICE => invoice_decoded + .invoice_relative_expiry + .map(|e_at| created_at + Timestamp::from_secs(u64::from(e_at))), + DecodeType::BOLT11_INVOICE => invoice_decoded + .expiry + .map(|e_at| created_at + Timestamp::from_secs(e_at)), + _ => return not_invoice_err, + }; + + let state = match hold_invoice.state() { + hold::InvoiceState::Unpaid => nip47::TransactionState::Pending, + hold::InvoiceState::Accepted => nip47::TransactionState::Accepted, + hold::InvoiceState::Paid => nip47::TransactionState::Settled, + hold::InvoiceState::Cancelled => nip47::TransactionState::Expired, + }; + + let settled_at = if hold_invoice.settled_at() != 0 { + Some(Timestamp::from_secs(hold_invoice.settled_at())) + } else { + None + }; + + let preimage = if hold_invoice.state() == hold::InvoiceState::Paid { + Some(hex::encode(hold_invoice.preimage())) + } else { + None + }; + + Ok(nip47::LookupInvoiceResponse { + transaction_type: Some(nip47::TransactionType::Incoming), + invoice: Some(hold_invoice.invoice.clone()), + description, + description_hash, + preimage, + payment_hash: hex::encode(hold_invoice.payment_hash.clone()), + amount, + fees_paid: 0, + created_at, + expires_at, + settled_at, + metadata: None, + state: Some(state), + }) +} + +async fn make_lookup_response_from_listinvoices( + rpc: &mut ClnRpc, + list_invoice: ListinvoicesInvoices, +) -> Result { + let not_invoice_err = Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: NOT_INV_ERR.to_owned(), + }); + + let invstring = if let Some(bolt11) = list_invoice.bolt11 { + bolt11 + } else if let Some(bolt12) = list_invoice.bolt12 { + bolt12 + } else { + return not_invoice_err; + }; + let invoice_decoded = rpc + .call_typed(&DecodeRequest { + string: invstring.clone(), + }) + .await + .map_err(|e| nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + })?; + + if !invoice_decoded.valid { + return not_invoice_err; + } + + let description = match invoice_decoded.item_type { + cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => invoice_decoded.offer_description, + cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => invoice_decoded.description, + _ => return not_invoice_err, + }; + let description_hash = match invoice_decoded.item_type { + cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => None, + cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { + invoice_decoded.description_hash.map(|h| h.to_string()) + } + _ => return not_invoice_err, + }; + + let amount = match invoice_decoded.item_type { + cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { + invoice_decoded.invoice_amount_msat.unwrap().msat() + } + cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { + if let Some(amt) = invoice_decoded.amount_msat { + amt.msat() + } else if let Some(a) = list_invoice.amount_msat { + a.msat() + } else { + // amount: `any` but have to put a value... + 0 + } + } + _ => return not_invoice_err, + }; + + let created_at = match invoice_decoded.item_type { + cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { + Timestamp::from_secs(invoice_decoded.invoice_created_at.unwrap()) + } + cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { + Timestamp::from_secs(invoice_decoded.created_at.unwrap()) + } + _ => return not_invoice_err, + }; + + let preimage = list_invoice + .payment_preimage + .map(|p| hex::encode(p.to_vec())); + + let state = match list_invoice.status { + ListinvoicesInvoicesStatus::UNPAID => nip47::TransactionState::Pending, + ListinvoicesInvoicesStatus::PAID => nip47::TransactionState::Settled, + ListinvoicesInvoicesStatus::EXPIRED => nip47::TransactionState::Expired, + }; + + Ok(nip47::LookupInvoiceResponse { + transaction_type: Some(nip47::TransactionType::Incoming), + invoice: Some(invstring), + description, + description_hash, + preimage, + payment_hash: list_invoice.payment_hash.to_string(), + amount, + fees_paid: 0, + created_at, + expires_at: Some(Timestamp::from_secs(list_invoice.expires_at)), + settled_at: list_invoice.paid_at.map(Timestamp::from_secs), + metadata: None, + state: Some(state), + }) +} + +async fn make_lookup_response_from_listpays( + rpc: &mut ClnRpc, + list_pay: ListpaysPays, +) -> Result { + let not_invoice_err = Err(nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: NOT_INV_ERR.to_owned(), + }); + + let invstring = if list_pay.bolt11.is_some() { + list_pay.bolt11 + } else { + list_pay.bolt12 + }; + + let invoice_decoded = if let Some(invstr) = &invstring { + Some( + rpc.call_typed(&DecodeRequest { + string: invstr.clone(), }) .await .map_err(|e| nip47::NIP47Error { code: nip47::ErrorCode::Internal, message: e.to_string(), - })? - .pays; + })?, + ) + } else { + None + }; - for list_pay in list_pays.into_iter() { - if list_pay.status != ListpaysPaysStatus::COMPLETE { - continue; - } - let invstring = if list_pay.bolt11.is_some() { - list_pay.bolt11 - } else { - list_pay.bolt12 - }; - - let invoice_decoded = if let Some(invstr) = &invstring { - Some( - rpc.call_typed(&DecodeRequest { - string: invstr.clone(), - }) - .await - .map_err(|e| nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - })?, - ) - } else { - None - }; - - if invoice_decoded.is_some() && !invoice_decoded.as_ref().unwrap().valid { - continue; - } - - let description_hash = if let Some(inv_dec) = &invoice_decoded { - match inv_dec.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => None, - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { - inv_dec.description_hash.map(|h| h.to_string()) - } - _ => continue, - } - } else { - None - }; - let amount = if let Some(amt) = list_pay.amount_msat { - amt.msat() - } else if let Some(inv_dec) = &invoice_decoded { - match inv_dec.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { - inv_dec.invoice_amount_msat.unwrap().msat() - } - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { - if let Some(amt) = inv_dec.amount_msat { - amt.msat() - } else { - continue; - } - } - _ => continue, - } - } else { - continue; - }; - - let description = if let Some(inv_dec) = invoice_decoded { - match inv_dec.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { - inv_dec.offer_description - } - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => inv_dec.description, - _ => continue, - } - } else { - list_pay.description - }; - - let fees_paid = list_pay.amount_sent_msat.unwrap().msat() - amount; - let preimage = list_pay.preimage.map(|p| hex::encode(p.to_vec())); - - transactions.push(nip47::LookupInvoiceResponse { - transaction_type: Some(nip47::TransactionType::Outgoing), - invoice: invstring, - description, - description_hash, - preimage, - payment_hash: list_pay.payment_hash.to_string(), - amount, - fees_paid, - created_at: Timestamp::from_secs(list_pay.created_at), - expires_at: None, - settled_at: list_pay.completed_at.map(Timestamp::from_secs), - metadata: None, - }); - } + if invoice_decoded.is_some() && !invoice_decoded.as_ref().unwrap().valid { + return not_invoice_err; } - transactions.sort_by_key(|t| Reverse(t.created_at)); - - if let Some(l) = params.limit { - if transactions.len() > (l as usize) { - transactions = transactions.drain(0..(l as usize)).collect() + let description_hash = if let Some(inv_dec) = &invoice_decoded { + match inv_dec.item_type { + cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => None, + cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { + inv_dec.description_hash.map(|h| h.to_string()) + } + _ => return not_invoice_err, } - } + } else { + None + }; + let amount = if let Some(amt) = list_pay.amount_msat { + amt.msat() + } else if let Some(inv_dec) = &invoice_decoded { + match inv_dec.item_type { + cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { + inv_dec.invoice_amount_msat.unwrap().msat() + } + cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { + if let Some(amt) = inv_dec.amount_msat { + amt.msat() + } else { + // amount: `any` but have to put a value... + 0 + } + } + _ => return not_invoice_err, + } + } else { + return not_invoice_err; + }; - Ok(transactions) + let description = if let Some(inv_dec) = invoice_decoded { + match inv_dec.item_type { + cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => inv_dec.offer_description, + cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => inv_dec.description, + _ => return not_invoice_err, + } + } else { + list_pay.description + }; + + let fees_paid = if let Some(amt_sent) = list_pay.amount_sent_msat { + amt_sent.msat() - amount + } else { + 0 + }; + let preimage = list_pay.preimage.map(|p| hex::encode(p.to_vec())); + + let state = match list_pay.status { + ListpaysPaysStatus::PENDING => nip47::TransactionState::Pending, + ListpaysPaysStatus::FAILED => nip47::TransactionState::Failed, + ListpaysPaysStatus::COMPLETE => nip47::TransactionState::Settled, + }; + + Ok(nip47::LookupInvoiceResponse { + transaction_type: Some(nip47::TransactionType::Outgoing), + invoice: invstring, + description, + description_hash, + preimage, + payment_hash: list_pay.payment_hash.to_string(), + amount, + fees_paid, + created_at: Timestamp::from_secs(list_pay.created_at), + expires_at: None, + settled_at: list_pay.completed_at.map(Timestamp::from_secs), + metadata: None, + state: Some(state), + }) +} + +/// Keep only the newest transactions that fit within `max_size` bytes. +/// Transactions must be sorted newest first. Serializes each transaction only +/// once and chunks the remaining (O(n), not O(n^2)). +fn trim_to_size( + mut transactions: Vec, + max_size: usize, +) -> Vec { + let mut total_size = 0usize; + let mut keep = 0usize; + for tx in &transactions { + match serde_json::to_vec(tx) { + Ok(serialized) => { + total_size += serialized.len(); + if total_size > max_size { + break; + } + } + Err(e) => { + log::warn!("Failed to serialize transaction: {e}"); + break; + } + } + keep += 1; + } + + let trimmed = transactions.len() - keep; + if trimmed > 0 { + log::info!("Trimmed {trimmed} transactions to stay under {max_size} bytes"); + } + transactions.truncate(keep); + + transactions } diff --git a/src/nwc_notifications.rs b/src/nwc_notifications.rs index fb214c5..302ba96 100644 --- a/src/nwc_notifications.rs +++ b/src/nwc_notifications.rs @@ -2,15 +2,38 @@ use std::str::FromStr; use anyhow::anyhow; use cln_plugin::Plugin; -use cln_rpc::model::requests::{DecodeRequest, ListinvoicesRequest, ListpaysRequest}; -use cln_rpc::model::responses::ListpaysPaysStatus; -use cln_rpc::primitives::Sha256; +use cln_rpc::{ + ClnRpc, + Notification, + model::{ + requests::{DecodeRequest, ListinvoicesRequest, ListpaysRequest, ListpeerchannelsRequest}, + responses::{ + DecodeResponse, + ListinvoicesInvoices, + ListinvoicesInvoicesStatus, + ListpaysPays, + ListpaysPaysStatus, + }, + }, + notifications::{InvoicePaymentNotification, SendPaySuccessNotification}, + primitives::Sha256, +}; +use nostr::{ + event::{EventBuilder, FinalizeEventAsync, Kind, Tag}, + nips::{nip04, nip44, nip47}, + types::Timestamp, +}; -use crate::structs::PluginState; -use crate::OPT_NOTIFICATIONS; +use crate::{ + OPT_NOTIFICATIONS, + hold::{InvoiceState, ListRequest, TrackRequest, list_request::Constraint}, + structs::{NOT_INV_ERR, PluginState, WalletService}, +}; -use nostr_sdk::nips::*; -use nostr_sdk::*; +/// Upper bound on how long a `holdinvoice_accepted_handler` waits for an +/// invoice to be accepted, so a client cannot pin a task and a gRPC stream +/// open forever with an absurd expiry. +const MAX_HOLD_WAIT_SECS: u64 = 24 * 60 * 60; pub async fn payment_received_handler( plugin: Plugin, @@ -19,13 +42,12 @@ pub async fn payment_received_handler( if !plugin.option(&OPT_NOTIFICATIONS).unwrap() { return Ok(()); } - let label = args - .get("invoice_payment") - .ok_or_else(|| anyhow!("Malformed invoice_payment notification: missing invoice_payment"))? - .get("label") - .ok_or_else(|| anyhow!("Malformed invoice_payment notification: missing label"))? - .as_str() - .ok_or_else(|| anyhow!("label not a string"))?; + + let notif: Notification = serde_json::from_value(args)?; + let inv_pay_notif: InvoicePaymentNotification = match notif { + Notification::InvoicePayment(invoice_payment_notification) => invoice_payment_notification, + _ => return Err(anyhow!("Wrong notification type, expected invoice_payment")), + }; let mut rpc = plugin.state().rpc_lock.lock().await; @@ -33,7 +55,7 @@ pub async fn payment_received_handler( .call_typed(&ListinvoicesRequest { index: None, invstring: None, - label: Some(label.to_owned()), + label: Some(inv_pay_notif.label), limit: None, offer_id: None, payment_hash: None, @@ -45,19 +67,52 @@ pub async fn payment_received_handler( let invoice = invoice_resp .first() .ok_or_else(|| anyhow!("invoice not found"))?; - let invstring = if invoice.bolt11.is_some() { - invoice.bolt11.as_ref().unwrap() + let invstring = if let Some(bolt11) = &invoice.bolt11 { + bolt11.clone() + } else if let Some(bolt12) = &invoice.bolt12 { + bolt12.clone() } else { - invoice.bolt12.as_ref().unwrap() + return Err(anyhow!( + "Listinvoices has neither returned bolt11 or bolt12 field" + )); }; + let payment_hash_str = hex::encode(invoice.payment_hash); + let invoice_decoded = rpc .call_typed(&DecodeRequest { string: invstring.clone(), }) .await?; - let not_invoice_err = Err(anyhow!("Not an invoice or invalid invoice".to_owned())); + if !invoice_decoded.valid { + return Err(anyhow!("Invalid invoice decoded for {payment_hash_str}")); + } + + let notification = + make_payment_received_from_listinvoices(invoice, invstring, invoice_decoded)?; + + let clients = plugin.state().handles.lock().await; + + for wallet_service in clients.values() { + if let Err(e) = send_notification(¬ification, wallet_service).await { + log::warn!( + "Failed sending payment_received notification for {payment_hash_str} \ + to client {}: {e}", + wallet_service.client_pubkey + ); + } + } + + Ok(()) +} + +fn make_payment_received_from_listinvoices( + invoice: &ListinvoicesInvoices, + invstring: String, + invoice_decoded: DecodeResponse, +) -> Result { + let not_invoice_err = Err(anyhow!(NOT_INV_ERR.to_owned())); if !invoice_decoded.valid { return not_invoice_err; @@ -112,67 +167,34 @@ pub async fn payment_received_handler( .ok_or_else(|| anyhow!("paid invoice missing paid_at time"))?, ); - let clients = plugin.state().handles.lock().await; + let state = match invoice.status { + ListinvoicesInvoicesStatus::UNPAID => nip47::TransactionState::Pending, + ListinvoicesInvoicesStatus::PAID => nip47::TransactionState::Settled, + ListinvoicesInvoicesStatus::EXPIRED => nip47::TransactionState::Expired, + }; - for (client, client_pubkey) in clients.values() { - let signer = client.signer().await?; - let content = nip47::Notification { - notification_type: nip47::NotificationType::PaymentReceived, - notification: nip47::NotificationResult::PaymentReceived(nip47::PaymentNotification { - transaction_type: Some(nip47::TransactionType::Incoming), - invoice: invstring.clone(), - description: description.clone(), - description_hash: description_hash.clone(), - preimage: preimage.clone(), - payment_hash: invoice.payment_hash.to_string(), - amount, - fees_paid: 0, - created_at, - expires_at: None, - settled_at, - metadata: None, - }), - }; - let notification = serde_json::to_string(&content)?; - log::debug!("NOTIFICATION: {}", notification); - let content_encrypted_nip04 = signer.nip04_encrypt(client_pubkey, ¬ification).await?; - let event_nip04 = EventBuilder::new(Kind::from_u16(23196), content_encrypted_nip04) - .tag(Tag::public_key(*client_pubkey)) - .sign(&signer) - .await?; - let nip04_result = client.send_event(&event_nip04).await?; - if nip04_result.success.is_empty() { - log::warn!( - "None of the relays accepted our nip04 notification: {}", - nip04_result - .failed - .into_values() - .collect::>() - .join(", ") - ) - } - log::debug!("NIP04 NOTIFICATION SENT: {:?}", event_nip04); + let content = nip47::Notification { + notification_type: nip47::NotificationType::PaymentReceived, + notification: nip47::NotificationResult::PaymentReceived(nip47::PaymentNotification { + transaction_type: Some(nip47::TransactionType::Incoming), + invoice: invstring, + description: description.clone(), + description_hash: description_hash.clone(), + preimage: preimage.clone(), + payment_hash: invoice.payment_hash.to_string(), + amount, + fees_paid: 0, + created_at, + expires_at: None, + settled_at, + metadata: None, + state: Some(state), + }), + }; - let content_encrypted_nip44 = signer.nip44_encrypt(client_pubkey, ¬ification).await?; - let event_nip44 = EventBuilder::new(Kind::from_u16(23197), content_encrypted_nip44) - .tag(Tag::public_key(*client_pubkey)) - .sign(&signer) - .await?; - let nip44_result = client.send_event(&event_nip44).await?; - if nip44_result.success.is_empty() { - log::warn!( - "None of the relays accepted our nip44 notification: {}", - nip44_result - .failed - .into_values() - .collect::>() - .join(", ") - ) - } - log::debug!("NIP44 NOTIFICATION SENT: {:?}", event_nip44); - } + let notification = serde_json::to_string(&content)?; - Ok(()) + Ok(notification) } pub async fn payment_sent_handler( @@ -182,18 +204,19 @@ pub async fn payment_sent_handler( if !plugin.option(&OPT_NOTIFICATIONS).unwrap() { return Ok(()); } - let payment_hash = args - .get("sendpay_success") - .ok_or_else(|| anyhow!("Malformed sendpay_success notification: missing sendpay_success"))? - .get("payment_hash") - .ok_or_else(|| anyhow!("Malformed sendpay_success notification: missing payment_hash"))? - .as_str() - .ok_or_else(|| anyhow!("payment_hash not a string"))? - .to_owned(); + + let notif: Notification = serde_json::from_value(args)?; + let send_pay_notif: SendPaySuccessNotification = match notif { + Notification::SendPaySuccess(send_pay_success_notification) => { + send_pay_success_notification + } + _ => return Err(anyhow!("Wrong notification type, expected sendpay_success")), + }; + let payment_hash = hex::encode(send_pay_notif.payment_hash); let mut rpc = plugin.state().rpc_lock.lock().await; - let pays_resp = rpc + let mut pays_resp = rpc .call_typed(&ListpaysRequest { bolt11: None, index: None, @@ -205,14 +228,33 @@ pub async fn payment_sent_handler( .await? .pays; + pays_resp.retain(|p| p.status == ListpaysPaysStatus::COMPLETE); + let pay = pays_resp .first() - .ok_or_else(|| anyhow!("payment not found"))?; + .ok_or_else(|| anyhow!("complete payment not found"))?; - if pay.status != ListpaysPaysStatus::COMPLETE { - return Err(anyhow!("Payment not complete")); + let notification = make_payment_sent_from_listpays(pay, &mut rpc).await?; + + let clients = plugin.state().handles.lock().await; + + for wallet_service in clients.values() { + if let Err(e) = send_notification(¬ification, wallet_service).await { + log::warn!( + "Failed sending payment_sent notification for {payment_hash} to\ + client {}: {e}", + wallet_service.client_pubkey + ); + } } + Ok(()) +} + +async fn make_payment_sent_from_listpays( + pay: &ListpaysPays, + rpc: &mut ClnRpc, +) -> Result { let invstring = if let Some(b11) = &pay.bolt11 { b11 } else if let Some(b12) = &pay.bolt12 { @@ -232,14 +274,23 @@ pub async fn payment_sent_handler( ); let settled_at = Timestamp::from_secs(pay.completed_at.unwrap()); - if !invstring.is_empty() { + if invstring.is_empty() { + description = pay.description.clone(); + description_hash = None; + amount = if let Some(amt) = pay.amount_msat { + amt.msat() + } else { + // Amount missing but required + 0 + } + } else { let invoice_decoded = rpc .call_typed(&DecodeRequest { string: invstring.clone(), }) .await?; - let not_invoice_err = Err(anyhow!("Not an invoice".to_owned())); + let not_invoice_err = Err(anyhow!(NOT_INV_ERR.to_owned())); if !invoice_decoded.valid { return not_invoice_err; @@ -275,78 +326,263 @@ pub async fn payment_sent_handler( } _ => return not_invoice_err, }; + } + + let fees_paid = if let Some(amt_sent) = pay.amount_sent_msat { + amt_sent.msat() - amount } else { - description = pay.description.clone(); - description_hash = None; - amount = if let Some(amt) = pay.amount_msat { - amt.msat() - } else { - // Amount missing but required - 0 - } + 0 + }; + + let state = match pay.status { + ListpaysPaysStatus::PENDING => nip47::TransactionState::Pending, + ListpaysPaysStatus::FAILED => nip47::TransactionState::Failed, + ListpaysPaysStatus::COMPLETE => nip47::TransactionState::Settled, + }; + + let content = nip47::Notification { + notification_type: nip47::NotificationType::PaymentSent, + notification: nip47::NotificationResult::PaymentSent(nip47::PaymentNotification { + transaction_type: Some(nip47::TransactionType::Outgoing), + invoice: invstring.clone(), + description: description.clone(), + description_hash: description_hash.clone(), + preimage: preimage.clone(), + payment_hash: pay.payment_hash.to_string(), + amount, + fees_paid, + created_at, + expires_at: None, + settled_at, + metadata: None, + state: Some(state), + }), + }; + + let notification = serde_json::to_string(&content)?; + + Ok(notification) +} + +async fn send_notification( + notification: &String, + wallet_service: &WalletService, +) -> Result<(), anyhow::Error> { + log::trace!("NOTIFICATION: {notification}"); + let content_encrypted_nip04 = nip04::encrypt( + wallet_service.wallet_secret.secret_key(), + &wallet_service.client_pubkey, + notification, + )?; + let event_nip04 = EventBuilder::new(Kind::from_u16(23196), content_encrypted_nip04) + .tag(Tag::public_key(wallet_service.client_pubkey)) + .finalize_async(&wallet_service.wallet_secret) + .await?; + let nip04_result = wallet_service.client.send_event(&event_nip04).await?; + if nip04_result.success.is_empty() { + log::warn!( + "None of the relays accepted our nip04 notification: {}", + nip04_result + .failed + .into_values() + .collect::>() + .join(", ") + ); } + log::trace!("NIP04 NOTIFICATION SENT: {event_nip04:?}"); - let fees_paid = pay.amount_sent_msat.unwrap().msat() - amount; - - let clients = plugin.state().handles.lock().await; - - for (client, client_pubkey) in clients.values() { - let signer = client.signer().await?; - let content = nip47::Notification { - notification_type: nip47::NotificationType::PaymentSent, - notification: nip47::NotificationResult::PaymentSent(nip47::PaymentNotification { - transaction_type: Some(nip47::TransactionType::Outgoing), - invoice: invstring.clone(), - description: description.clone(), - description_hash: description_hash.clone(), - preimage: preimage.clone(), - payment_hash: pay.payment_hash.to_string(), - amount, - fees_paid, - created_at, - expires_at: None, - settled_at, - metadata: None, - }), - }; - let notification = serde_json::to_string(&content)?; - log::debug!("NOTIFICATION: {}", notification); - let content_encrypted_nip04 = signer.nip04_encrypt(client_pubkey, ¬ification).await?; - let event_nip04 = EventBuilder::new(Kind::from_u16(23196), content_encrypted_nip04) - .tag(Tag::public_key(*client_pubkey)) - .sign(&signer) - .await?; - let nip04_result = client.send_event(&event_nip04).await?; - if nip04_result.success.is_empty() { - log::warn!( - "None of the relays accepted our nip04 notification: {}", - nip04_result - .failed - .into_values() - .collect::>() - .join(", ") - ) - } - log::debug!("NIP04 NOTIFICATION SENT: {:?}", event_nip04); - - let content_encrypted_nip44 = signer.nip44_encrypt(client_pubkey, ¬ification).await?; - let event_nip44 = EventBuilder::new(Kind::from_u16(23197), content_encrypted_nip44) - .tag(Tag::public_key(*client_pubkey)) - .sign(&signer) - .await?; - let nip44_result = client.send_event(&event_nip44).await?; - if nip44_result.success.is_empty() { - log::warn!( - "None of the relays accepted our nip44 notification: {}", - nip44_result - .failed - .into_values() - .collect::>() - .join(", ") - ) - } - log::debug!("NIP44 NOTIFICATION SENT: {:?}", event_nip44); + let content_encrypted_nip44 = nip44::encrypt( + wallet_service.wallet_secret.secret_key(), + &wallet_service.client_pubkey, + notification, + nip44::Version::V2, + )?; + let event_nip44 = EventBuilder::new(Kind::from_u16(23197), content_encrypted_nip44) + .tag(Tag::public_key(wallet_service.client_pubkey)) + .finalize_async(&wallet_service.wallet_secret) + .await?; + let nip44_result = wallet_service.client.send_event(&event_nip44).await?; + if nip44_result.success.is_empty() { + log::warn!( + "None of the relays accepted our nip44 notification: {}", + nip44_result + .failed + .into_values() + .collect::>() + .join(", ") + ); } + log::trace!("NIP44 NOTIFICATION SENT: {event_nip44:?}"); Ok(()) } + +#[allow(clippy::too_many_lines)] +pub async fn holdinvoice_accepted_handler( + plugin: Plugin, + payment_hash: Vec, + expires_at: u64, +) -> Result<(), anyhow::Error> { + let payment_hash_str = hex::encode(&payment_hash); + + let mut hold_client = plugin.state().hold_client.lock().clone().unwrap(); + + let track_request = TrackRequest { + payment_hash: payment_hash.clone(), + }; + let mut track_stream = hold_client.track(track_request).await?.into_inner(); + + // The hold plugin has no expired state: an invoice that expires without + // being accepted stays in the Unpaid state and the track stream never ends + // on its own. Bound the wait by the invoice's expiry (capped) so we do not + // hold this task and gRPC stream open forever. + let wait_duration = expires_at + .saturating_sub(Timestamp::now().as_secs()) + .min(MAX_HOLD_WAIT_SECS); + let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(wait_duration); + + let mut accepted = false; + loop { + match tokio::time::timeout_at(deadline, track_stream.message()).await { + Err(_elapsed) => { + log::debug!("Hold invoice {payment_hash_str} expired before being accepted"); + break; + } + Ok(Err(e)) => return Err(e.into()), + // The stream ended without an Accepted state: the invoice was + // cancelled before it ever got accepted. + Ok(Ok(None)) => break, + Ok(Ok(Some(response))) => { + log::debug!("Invoice status: {}", response.state().as_str_name()); + match response.state() { + InvoiceState::Accepted => { + accepted = true; + break; + } + InvoiceState::Paid | InvoiceState::Cancelled => break, + InvoiceState::Unpaid => (), + } + } + } + } + + if !accepted { + log::debug!("Hold invoice {payment_hash_str} was not accepted, skipping notification"); + return Ok(()); + } + + let list_request = ListRequest { + constraint: Some(Constraint::PaymentHash(payment_hash.clone())), + }; + + let hold_lookup = hold_client.list(list_request).await?.into_inner(); + + if hold_lookup.invoices.len() != 1 { + return Err(anyhow!("hold plugin did not return exactly one invoice")); + } + + let hold_invoice = hold_lookup.invoices.first().unwrap(); + + let mut rpc = plugin.state().rpc_lock.lock().await; + + let invoice_decoded = rpc + .call_typed(&DecodeRequest { + string: hold_invoice.invoice.clone(), + }) + .await?; + + let amount = match invoice_decoded.item_type { + cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { + invoice_decoded.invoice_amount_msat.unwrap().msat() + } + cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { + if let Some(amt) = invoice_decoded.amount_msat { + amt.msat() + } else { + // amount: `any` but have to put a value... + 0 + } + } + _ => return Err(anyhow!("hold plugin did not return an invoice string")), + }; + + let created_at = match invoice_decoded.item_type { + cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { + Timestamp::from_secs(invoice_decoded.invoice_created_at.unwrap()) + } + cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { + Timestamp::from_secs(invoice_decoded.created_at.unwrap()) + } + _ => return Err(anyhow!("hold plugin did not return an invoice string")), + }; + + let expires_at = match invoice_decoded.item_type { + cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { + created_at + + Timestamp::from_secs(u64::from(invoice_decoded.invoice_relative_expiry.unwrap())) + } + cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { + created_at + Timestamp::from_secs(invoice_decoded.expiry.unwrap()) + } + _ => return Err(anyhow!("hold plugin did not return an invoice string")), + }; + + let list_peer_channels = rpc + .call_typed(&ListpeerchannelsRequest { + id: None, + short_channel_id: None, + channel_id: None, + }) + .await? + .channels; + + let payment_hash_hash = Sha256::from_str(&payment_hash_str)?; + + let mut lowest_htlc_expiry = u32::MAX; + + for peer in list_peer_channels { + if let Some(htlcs) = peer.htlcs { + for htlc in htlcs { + if htlc.payment_hash != payment_hash_hash { + continue; + } + if htlc.expiry < lowest_htlc_expiry { + lowest_htlc_expiry = htlc.expiry; + } + } + } + } + + let clients = plugin.state().handles.lock().await; + + let content = nip47::Notification { + notification_type: nip47::NotificationType::HoldInvoiceAccepted, + notification: nip47::NotificationResult::HoldInvoiceAccepted( + nip47::HoldInvoiceAcceptedNotification { + transaction_type: nip47::TransactionType::Incoming, + invoice: hold_invoice.invoice.clone(), + description: None, + description_hash: None, + payment_hash: hex::encode(&hold_invoice.payment_hash), + amount, + created_at, + expires_at, + settle_deadline: lowest_htlc_expiry, + metadata: None, + state: Some(nip47::TransactionState::Accepted), + }, + ), + }; + let notification = serde_json::to_string(&content).unwrap(); + + for wallet_service in clients.values() { + if let Err(e) = send_notification(¬ification, wallet_service).await { + log::warn!( + "Failed sending hold_invoice_accepted {payment_hash_str} notification\ + to client {}: {e}", + wallet_service.client_pubkey + ); + } + } + Ok(()) +} diff --git a/src/nwc_pay.rs b/src/nwc_pay.rs index 821aa36..4799465 100644 --- a/src/nwc_pay.rs +++ b/src/nwc_pay.rs @@ -1,27 +1,180 @@ -use std::time::Duration; - use cln_plugin::Plugin; use cln_rpc::{ - model::requests::{DecodeRequest, PayRequest, XpayRequest}, - primitives::Amount, + ClnRpc, + RpcError, + model::{ + requests::{DecodeRequest, PayRequest, XpayRequest}, + responses::DecodeResponse, + }, + primitives::{Amount, Secret}, }; -use nostr_sdk::nips::*; -use tokio::time; +use nostr::nips::nip47; use crate::{ - structs::PluginState, - util::{at_or_above_version, budget_amount_check, load_nwc_store, update_nwc_store}, + structs::{NOT_INV_ERR, NwcStore, PluginState}, + util::{ + budget_amount_check, + get_budget_msat, + load_nwc_store, + payment_fee_reserve_msat, + refund_budget, + reserve_budget, + rpc_socket_path, + settle_budget, + }, }; -pub async fn pay_invoice( +pub const XPAY_COMMAND: &str = "xpay"; + +pub async fn pay_invoice_response( plugin: Plugin, params: nip47::PayInvoiceRequest, - label: &String, -) -> Result<(nip47::PayInvoiceResponse, String), (nip47::NIP47Error, String)> { - let mut rpc = plugin.state().rpc_lock.lock().await; + label: &str, +) -> Vec<(nip47::Response, Option)> { + vec![match pay_invoice(plugin, params, label).await { + Ok((o, id)) => ( + nip47::Response { + result_type: nip47::Method::PayInvoice, + error: None, + result: Some(nip47::ResponseResult::PayInvoice(o)), + }, + id, + ), + Err((e, id)) => ( + nip47::Response { + result_type: nip47::Method::PayInvoice, + error: Some(e), + result: None, + }, + id, + ), + }] +} - let id = params.id.clone().unwrap_or_default(); +async fn pay_invoice( + plugin: Plugin, + params: nip47::PayInvoiceRequest, + label: &str, +) -> Result<(nip47::PayInvoiceResponse, Option), (nip47::NIP47Error, Option)> { + let (id, reservation) = { + let mut rpc = plugin.state().rpc_lock.lock().await; + let decoded_invoice = decode_and_validate_invoice(&mut rpc, ¶ms).await?; + + let id = get_payment_id(¶ms, &decoded_invoice)?; + + let invoice_amt_msat = get_invoice_amount_msat(&decoded_invoice); + + let nwc_store = + load_nwc_and_check_budget(&mut rpc, label, ¶ms, invoice_amt_msat, &id).await?; + + let amt_msat = match (params.amount, invoice_amt_msat) { + (None, None) => { + return Err(( + nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: "No amount found in request or invoice".to_owned(), + }, + Some(id.clone()), + )); + } + (None, Some(b)) => b, + (Some(a), None) => a, + (Some(a), Some(b)) => { + if a != b { + return Err(( + nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: "request amount does not match invoice amount".to_owned(), + }, + Some(id.clone()), + )); + } + a + } + }; + + // Reserve the invoice amount plus the worst case fee so that no + // combination of concurrent payments can exceed the budget and so that + // balance queries during the payment reflect the reserved amount. + if get_budget_msat(&nwc_store).unwrap_or(u64::MAX) + < amt_msat.saturating_add(payment_fee_reserve_msat(amt_msat)) + { + return Err(( + nip47::NIP47Error { + code: nip47::ErrorCode::QuotaExceeded, + message: "Payment and estimated fees exceed the available budget".to_owned(), + }, + Some(id), + )); + } + + let reservation = reserve_budget(&mut rpc, label, &nwc_store, amt_msat) + .await + .map_err(|e| { + ( + nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + }, + Some(id.clone()), + ) + })?; + + (id, reservation) + }; + + let has_xpay = plugin.state().config.lock().has_xpay; + + let mut pay_rpc = ClnRpc::new(rpc_socket_path(&plugin)).await.map_err(|e| { + ( + nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: format!("Could not connect to lightningd: {e}"), + }, + Some(id.clone()), + ) + })?; + + let pay_result = if has_xpay { + pay_with_xpay(&mut pay_rpc, ¶ms).await + } else { + pay_with_legacy(&mut pay_rpc, ¶ms).await + }; + + match pay_result { + Ok((amount_sent_msat, amount_msat, preimage)) => { + let mut rpc = plugin.state().rpc_lock.lock().await; + if let Err(e) = settle_budget(&mut rpc, label, reservation, amount_sent_msat).await { + log::error!("Error updating budget after successful payment: {e}"); + } + + let preimage_str = hex::encode(preimage.to_vec()); + let fees_paid = amount_sent_msat.saturating_sub(amount_msat); + Ok(( + nip47::PayInvoiceResponse { + preimage: preimage_str, + fees_paid: Some(fees_paid), + }, + Some(id), + )) + } + Err(e) => { + let mut rpc = plugin.state().rpc_lock.lock().await; + if let Err(refund_err) = refund_budget(&mut rpc, label, reservation).await { + log::error!( + "Error refunding budget reservation after failed payment: {refund_err}" + ); + } + Err(map_cln_error_to_nip47(&e, &id, has_xpay)) + } + } +} + +async fn decode_and_validate_invoice( + rpc: &mut ClnRpc, + params: &nip47::PayInvoiceRequest, +) -> Result)> { let invoice_decoded = rpc .call_typed(&DecodeRequest { string: params.invoice.clone(), @@ -33,252 +186,211 @@ pub async fn pay_invoice( code: nip47::ErrorCode::Internal, message: e.to_string(), }, - id.clone(), + params.id.clone(), ) })?; - let not_invoice_error = Err(( - nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: "Not an invoice or invalid invoice".to_owned(), - }, - id.clone(), - )); - if !invoice_decoded.valid { - return not_invoice_error; + return Err(( + nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: NOT_INV_ERR.to_owned(), + }, + params.id.clone(), + )); } - let id = if let Some(i) = params.id { - i + if !matches!( + invoice_decoded.item_type, + cln_rpc::model::responses::DecodeType::BOLT11_INVOICE + ) { + return Err(( + nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: NOT_INV_ERR.to_owned(), + }, + params.id.clone(), + )); + } + + Ok(invoice_decoded) +} + +fn get_payment_id( + params: &nip47::PayInvoiceRequest, + decoded_invoice: &DecodeResponse, +) -> Result)> { + let id = if let Some(i) = ¶ms.id { + i.clone() } else { - match invoice_decoded.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { - invoice_decoded.invoice_payment_hash.unwrap().to_string() - } - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { - invoice_decoded.payment_hash.unwrap().to_string() - } - _ => return not_invoice_error, - } + decoded_invoice + .payment_hash + .as_ref() + .ok_or_else(|| { + ( + nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: "payment_hash missing in decoded invoice".to_owned(), + }, + None, + ) + })? + .to_string() }; - let invoice_amt_msat = match invoice_decoded.item_type { - cln_rpc::model::responses::DecodeType::BOLT12_INVOICE => { - invoice_decoded.invoice_amount_msat.unwrap().msat() - } - cln_rpc::model::responses::DecodeType::BOLT11_INVOICE => { - invoice_decoded.amount_msat.unwrap().msat() - } - _ => return not_invoice_error, - }; + Ok(id) +} - let mut nwc_store = load_nwc_store(&mut rpc, label).await.map_err(|e| { +fn get_invoice_amount_msat(decoded_invoice: &DecodeResponse) -> Option { + decoded_invoice.amount_msat.as_ref().map(Amount::msat) +} + +async fn load_nwc_and_check_budget( + rpc: &mut ClnRpc, + label: &str, + params: &nip47::PayInvoiceRequest, + invoice_amt_msat: Option, + id: &str, +) -> Result)> { + let nwc_store = load_nwc_store(rpc, label).await.map_err(|e| { ( nip47::NIP47Error { code: nip47::ErrorCode::Internal, message: e.to_string(), }, - id.clone(), + Some(id.to_owned()), ) })?; - budget_amount_check(params.amount, Some(invoice_amt_msat), nwc_store.budget_msat).map_err( + budget_amount_check(params.amount, invoice_amt_msat, get_budget_msat(&nwc_store)).map_err( |e| { ( nip47::NIP47Error { code: nip47::ErrorCode::QuotaExceeded, message: e.to_string(), }, - id.clone(), + Some(id.to_owned()), ) }, )?; - let my_version = plugin.state().config.lock().clone().my_cln_version; + Ok(nwc_store) +} - if at_or_above_version(&my_version, "24.11").map_err(|e| { - ( +fn map_cln_error_to_nip47( + e: &RpcError, + id: &str, + is_xpay: bool, +) -> (nip47::NIP47Error, Option) { + match e.code { + Some(c) => { + let other_codes = if is_xpay { + vec![207, 219] + } else { + vec![201, 207, 219] + }; + let failed_codes = if is_xpay { + vec![203, 205, 209] + } else { + vec![203, 205, 209, 210] + }; + + if other_codes.contains(&c) { + ( + nip47::NIP47Error { + code: nip47::ErrorCode::Other, + message: e.to_string(), + }, + Some(id.to_owned()), + ) + } else if failed_codes.contains(&c) { + ( + nip47::NIP47Error { + code: nip47::ErrorCode::PaymentFailed, + message: e.to_string(), + }, + Some(id.to_owned()), + ) + } else if !is_xpay && c == 206 { + ( + nip47::NIP47Error { + code: nip47::ErrorCode::PaymentFailed, + message: format!("Route too expensive: {e}"), + }, + Some(id.to_owned()), + ) + } else { + ( + nip47::NIP47Error { + code: nip47::ErrorCode::Internal, + message: e.to_string(), + }, + Some(id.to_owned()), + ) + } + } + None => ( nip47::NIP47Error { code: nip47::ErrorCode::Internal, message: e.to_string(), }, - id.clone(), - ) - })? { - match rpc - .call_typed(&XpayRequest { - amount_msat: params.amount.map(Amount::from_msat), - maxdelay: None, - maxfee: None, - partial_msat: None, - retry_for: None, - layers: None, - invstring: params.invoice, - }) - .await - { - Ok(o) => { - if let Some(ref mut bdg) = nwc_store.budget_msat { - *bdg = bdg.saturating_sub(o.amount_sent_msat.msat()); - update_nwc_store(&mut rpc, label, nwc_store) - .await - .map_err(|e| { - ( - nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - }, - id.clone(), - ) - })?; - } - - let preimage = hex::encode(o.payment_preimage.to_vec()); - Ok((nip47::PayInvoiceResponse { preimage }, id)) - } - Err(e) => match e.code { - Some(c) => match c { - 207 | 219 => Err(( - nip47::NIP47Error { - code: nip47::ErrorCode::Other, - message: e.to_string(), - }, - id, - )), - 203 | 205 | 209 => Err(( - nip47::NIP47Error { - code: nip47::ErrorCode::PaymentFailed, - message: e.to_string(), - }, - id, - )), - _ => Err(( - nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - }, - id, - )), - }, - None => Err(( - nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - }, - id, - )), - }, - } - } else { - match rpc - .call_typed(&PayRequest { - amount_msat: params.amount.map(Amount::from_msat), - description: None, - exemptfee: None, - label: None, - localinvreqid: None, - maxdelay: None, - maxfee: None, - maxfeepercent: None, - partial_msat: None, - retry_for: None, - riskfactor: None, - exclude: None, - bolt11: params.invoice, - }) - .await - { - Ok(o) => { - if let Some(ref mut bdg) = nwc_store.budget_msat { - *bdg = bdg.saturating_sub(o.amount_sent_msat.msat()); - update_nwc_store(&mut rpc, label, nwc_store) - .await - .map_err(|e| { - ( - nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - }, - id.clone(), - ) - })?; - } - - let preimage = hex::encode(o.payment_preimage.to_vec()); - Ok((nip47::PayInvoiceResponse { preimage }, id)) - } - Err(e) => match e.code { - Some(c) => match c { - 201 | 207 | 219 => Err(( - nip47::NIP47Error { - code: nip47::ErrorCode::Other, - message: e.to_string(), - }, - id, - )), - 203 | 205 | 209 | 210 => Err(( - nip47::NIP47Error { - code: nip47::ErrorCode::PaymentFailed, - message: e.to_string(), - }, - id, - )), - 206 => Err(( - nip47::NIP47Error { - code: nip47::ErrorCode::InsufficientBalance, - message: e.to_string(), - }, - id, - )), - _ => Err(( - nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - }, - id, - )), - }, - None => Err(( - nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: e.to_string(), - }, - id, - )), - }, - } + Some(id.to_owned()), + ), } } -pub async fn multi_pay_invoice( - plugin: Plugin, - params: nip47::MultiPayInvoiceRequest, - label: &String, -) -> Vec<(nip47::Response, String)> { - let mut responses = Vec::new(); - for pay in params.invoices { - let result = pay_invoice(plugin.clone(), pay, label).await; - let response_res = match result { - Ok((resp, id)) => ( - nip47::Response { - result_type: nip47::Method::MultiPayInvoice, - error: None, - result: Some(nip47::ResponseResult::MultiPayInvoice(resp)), - }, - id, - ), - Err((e, id)) => ( - nip47::Response { - result_type: nip47::Method::MultiPayInvoice, - error: Some(e), - result: None, - }, - id, - ), - }; - responses.push(response_res); - time::sleep(Duration::from_millis(100)).await; - } - responses +async fn pay_with_xpay( + pay_rpc: &mut ClnRpc, + params: &nip47::PayInvoiceRequest, +) -> Result<(u64, u64, Secret), RpcError> { + let payment_result = pay_rpc + .call_typed(&XpayRequest { + amount_msat: params.amount.map(Amount::from_msat), + maxdelay: None, + maxfee: None, + partial_msat: None, + retry_for: None, + layers: None, + invstring: params.invoice.clone(), + payer_note: None, + dev_use_shadow: None, + label: None, + localinvreqid: None, + }) + .await?; + + Ok(( + payment_result.amount_sent_msat.msat(), + payment_result.amount_msat.msat(), + payment_result.payment_preimage, + )) +} + +async fn pay_with_legacy( + pay_rpc: &mut ClnRpc, + params: &nip47::PayInvoiceRequest, +) -> Result<(u64, u64, Secret), RpcError> { + let payment_result = pay_rpc + .call_typed(&PayRequest { + amount_msat: params.amount.map(Amount::from_msat), + description: None, + exemptfee: None, + label: None, + localinvreqid: None, + maxdelay: None, + maxfee: None, + maxfeepercent: None, + partial_msat: None, + retry_for: None, + riskfactor: None, + exclude: None, + bolt11: params.invoice.clone(), + }) + .await?; + + Ok(( + payment_result.amount_sent_msat.msat(), + payment_result.amount_msat.msat(), + payment_result.payment_preimage, + )) } diff --git a/src/parse.rs b/src/parse.rs index 935fe28..a8f9768 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,44 +1,72 @@ -use std::path::Path; +use std::{collections::HashSet, path::Path}; use anyhow::anyhow; use cln_plugin::ConfiguredPlugin; -use cln_rpc::{model::requests::GetinfoRequest, ClnRpc}; +use cln_rpc::{ClnRpc, model::requests::HelpRequest}; +use nostr::types::RelayUrl; use crate::{ - structs::{PluginState, TimeUnit}, OPT_RELAYS, + nwc_keysend::XKEYSEND_COMMAND, + nwc_pay::XPAY_COMMAND, + structs::{PluginState, TimeUnit}, }; pub async fn read_startup_options( plugin: &ConfiguredPlugin, state: &PluginState, ) -> Result<(), anyhow::Error> { - let relays_str = if let Some(relays) = plugin.option(&OPT_RELAYS).unwrap() { - if !relays.is_empty() { - relays - } else { - return Err(anyhow!( - "Empty `{}` option, must specify atleast one relay url!", - OPT_RELAYS.name() - )); - } + let relays_str = if plugin + .option(&OPT_RELAYS) + .unwrap() + .is_none_or(|v| v.is_empty()) + { + vec![ + "wss://nos.lol".to_owned(), + "wss://relay.primal.net".to_owned(), + "wss://relay.getalby.com/v1".to_owned(), + "wss://relay.nostr.net".to_owned(), + "wss://relay.snort.social".to_owned(), + ] } else { - return Err(anyhow!( - "`{}` not set, must specify atleast one relay url!", - OPT_RELAYS.name() - )); + plugin.option(&OPT_RELAYS).unwrap().unwrap() }; let mut rpc = ClnRpc::new( Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file), ) .await?; - let version = rpc.call_typed(&GetinfoRequest {}).await?.version; + let help = rpc.call_typed(&HelpRequest { command: None }).await?; + let mut config = state.config.lock(); - config.my_cln_version = version; - for relay in relays_str.into_iter() { - log::debug!("RELAY:{}", relay); - config.relays.push(nostr_sdk::RelayUrl::parse(&relay)?); + + let mut available_commands = HashSet::new(); + for command in &help.help { + if let Some(method) = command.command.split_ascii_whitespace().next() { + available_commands.insert(method); + } + } + + log::debug!( + "Found {} commands available: {}", + available_commands.len(), + available_commands + .iter() + .copied() + .collect::>() + .join(" ") + ); + + config.has_xkeysend = available_commands.contains(XKEYSEND_COMMAND); + config.has_xpay = available_commands.contains(XPAY_COMMAND); + log::debug!( + "Using xpay:{} xkeysend:{}", + config.has_xpay, + config.has_xkeysend + ); + for relay in relays_str { + log::debug!("RELAY:{relay}"); + config.relays.push(RelayUrl::parse(&relay)?); } Ok(()) } @@ -52,15 +80,15 @@ pub fn parse_time_period(input: &str) -> Result { if let Ok(time_unit) = unit.parse() { match time_unit { TimeUnit::Second => Ok(value), - TimeUnit::Minute => Ok(value * 60), - TimeUnit::Hour => Ok(value * 60 * 60), - TimeUnit::Day => Ok(value * 60 * 60 * 24), - TimeUnit::Week => Ok(value * 60 * 60 * 24 * 7), + TimeUnit::Minute => Ok(value.saturating_mul(60)), + TimeUnit::Hour => Ok(value.saturating_mul(60 * 60)), + TimeUnit::Day => Ok(value.saturating_mul(60 * 60 * 24)), + TimeUnit::Week => Ok(value.saturating_mul(60 * 60 * 24 * 7)), } } else { - Err(anyhow!(format!("Unsupported time unit: {}", unit))) + Err(anyhow!(format!("Unsupported time unit: {unit}"))) } } else { - Err(anyhow!("Invalid time format: {}", input)) + Err(anyhow!("Invalid time format: {input}")) } } diff --git a/src/rpc.rs b/src/rpc.rs index 34791be..a2c2c38 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -1,19 +1,31 @@ use anyhow::anyhow; use cln_plugin::Plugin; use cln_rpc::model::requests::{ - DatastoreMode, DatastoreRequest, DeldatastoreRequest, ListdatastoreRequest, + DatastoreMode, + DatastoreRequest, + DeldatastoreRequest, + ListdatastoreRequest, +}; +use nostr::{ + key::{Keys, SecretKey}, + nips::nip47::NostrWalletConnectUri, + types::Timestamp, }; -use nostr_sdk::nips::nip47::*; -use nostr_sdk::*; use serde_json::json; -use crate::nwc::{ - run_nwc, send_nwc_info_event, start_nwc_budget_job, stop_nwc, stop_nwc_budget_job, +use crate::{ + PLUGIN_NAME, + nwc::{run_nwc, send_nwc_info_event, stop_nwc}, + parse::parse_time_period, + structs::{BudgetIntervalConfig, NwcStore, PluginState}, + util::{ + build_capabilities, + get_budget_msat, + is_read_only_nwc, + load_nwc_store, + update_nwc_store, + }, }; -use crate::parse::parse_time_period; -use crate::structs::{BudgetIntervalConfig, NwcStore, PluginState}; -use crate::util::{is_read_only_nwc, load_nwc_store, update_nwc_store}; -use crate::{OPT_NOTIFICATIONS, PLUGIN_NAME, WALLET_ALL_METHODS, WALLET_READ_METHODS}; pub async fn nwc_create( plugin: Plugin, @@ -27,7 +39,7 @@ pub async fn nwc_create( let wallet_keys = Keys::generate(); let client_keys = Keys::generate(); - let uri = NostrWalletConnectURI::new( + let uri = NostrWalletConnectUri::new( wallet_keys.public_key(), config.relays.clone(), client_keys.secret_key().clone(), @@ -55,7 +67,8 @@ pub async fn nwc_create( let conf = BudgetIntervalConfig { interval_secs: interval, reset_budget_msat: bgt_msat, - last_reset: Timestamp::now().as_u64(), + last_reset: Timestamp::now().as_secs(), + spend_since_last_reset: 0, }; result.insert("interval_config".to_owned(), serde_json::to_value(&conf)?); Some(conf) @@ -70,6 +83,7 @@ pub async fn nwc_create( walletkey: wallet_keys.secret_key().to_secret_hex(), budget_msat, interval_config, + reserved_msat: 0, }; rpc.call_typed(&DatastoreRequest { @@ -81,7 +95,9 @@ pub async fn nwc_create( }) .await?; - run_nwc(plugin.clone(), label.clone(), nwc_store.clone()).await?; + if let Err(e) = run_nwc(plugin.clone(), label.clone(), nwc_store.clone()).await { + log::warn!("Failed running nwc: {e}"); + } Ok(serde_json::Value::Object(result)) } @@ -113,8 +129,6 @@ pub async fn nwc_budget( let (label, budget_msat, interval_secs) = parse_full_args(args)?; - stop_nwc_budget_job(plugin.clone(), &label); - let mut nwc_store = load_nwc_store(&mut rpc, &label).await?; let is_old_nwc_read_only = is_read_only_nwc(&nwc_store); @@ -125,7 +139,8 @@ pub async fn nwc_budget( let interval_config = BudgetIntervalConfig { interval_secs: interval, reset_budget_msat: budget, - last_reset: Timestamp::now().as_u64(), + last_reset: Timestamp::now().as_secs(), + spend_since_last_reset: 0, }; nwc_store.interval_config = Some(interval_config.clone()); } else { @@ -138,28 +153,20 @@ pub async fn nwc_budget( let is_new_nwc_read_only = is_read_only_nwc(&nwc_store); - if nwc_store.interval_config.is_some() { - start_nwc_budget_job(plugin.clone(), label.clone()); - } - update_nwc_store(&mut rpc, &label, nwc_store.clone()).await?; if is_old_nwc_read_only != is_new_nwc_read_only { let wallet_keys = Keys::new(SecretKey::from_hex(&nwc_store.walletkey)?); - let capabilities = if is_new_nwc_read_only { - WALLET_READ_METHODS.join(" ") - } else { - WALLET_ALL_METHODS.join(" ") - }; + let (method_capabilities, _) = build_capabilities(is_new_nwc_read_only, &plugin); let clients = plugin.state().handles.lock().await; send_nwc_info_event( + plugin.clone(), clients .get(&label) - .ok_or_else(|| anyhow!("No client found for label: {}", label))? - .0 + .ok_or_else(|| anyhow!("No client found for label: {label}"))? + .client .clone(), - plugin.option(&OPT_NOTIFICATIONS).unwrap(), - capabilities, + method_capabilities, wallet_keys, ) .await?; @@ -179,7 +186,8 @@ pub async fn nwc_list( let mut nwcs = Vec::new(); if let Some(lbl) = label { - let nwc_store = load_nwc_store(&mut rpc, &lbl).await?; + let mut nwc_store = load_nwc_store(&mut rpc, &lbl).await?; + nwc_store.budget_msat = get_budget_msat(&nwc_store); let wallet_key = Keys::new(SecretKey::from_hex(&nwc_store.walletkey)?); let client_key = Keys::new(nwc_store.uri.secret.clone()); let mut nwc_json = json!(nwc_store); @@ -195,16 +203,17 @@ pub async fn nwc_list( }); nwcs.push(json!({lbl:nwc_json})); } else { - let nwcs_store = rpc + let all_stored_nwcs = rpc .call_typed(&ListdatastoreRequest { key: Some(vec![PLUGIN_NAME.to_owned()]), }) .await? .datastore; - for datastore in nwcs_store.into_iter() { + for datastore in all_stored_nwcs { let label = datastore.key.last().unwrap().to_owned(); - let nwc_store = load_nwc_store(&mut rpc, &label).await?; + let mut nwc_store = load_nwc_store(&mut rpc, &label).await?; + nwc_store.budget_msat = get_budget_msat(&nwc_store); let wallet_key = Keys::new(SecretKey::from_hex(&nwc_store.walletkey)?); let client_key = Keys::new(nwc_store.uri.secret.clone()); let mut nwc_json = json!(nwc_store); diff --git a/src/structs.rs b/src/structs.rs index ce7221a..c66de4e 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,19 +1,28 @@ use std::{collections::HashMap, path::PathBuf, str::FromStr, sync::Arc}; use cln_rpc::ClnRpc; -use nostr_sdk::client; -use nostr_sdk::nips::nip47; -use nostr_sdk::nostr; +use nostr::{ + key::{Keys, PublicKey}, + nips::nip47::NostrWalletConnectUri, + types::RelayUrl, +}; +use nostr_sdk::client::Client; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; -use tokio::sync::oneshot; +use tonic::transport::Channel; + +use crate::hold::hold_client::HoldClient; + +pub const NOT_INV_ERR: &str = "Not an invoice or invalid invoice"; +pub const ID_STORE: &str = "eventids"; +pub const ID_MAX_AGE: u64 = 7_200; #[derive(Clone)] pub struct PluginState { pub config: Arc>, - pub handles: Arc>>, + pub handles: Arc>>, pub rpc_lock: Arc>, - pub budget_jobs: Arc>>>, + pub hold_client: Arc>>>, } impl PluginState { pub async fn new(path: PathBuf) -> Result { @@ -21,21 +30,29 @@ impl PluginState { config: Arc::new(Mutex::new(Config::default())), handles: Arc::new(tokio::sync::Mutex::new(HashMap::new())), rpc_lock: Arc::new(tokio::sync::Mutex::new(ClnRpc::new(path).await?)), - budget_jobs: Arc::new(Mutex::new(HashMap::new())), + hold_client: Arc::new(Mutex::new(None)), }) } } +pub struct WalletService { + pub client: Client, + pub client_pubkey: PublicKey, + pub wallet_secret: Keys, +} + #[derive(Clone, Debug)] pub struct Config { - pub relays: Vec, - pub my_cln_version: String, + pub relays: Vec, + pub has_xkeysend: bool, + pub has_xpay: bool, } impl Config { pub fn default() -> Config { Config { relays: Vec::new(), - my_cln_version: String::new(), + has_xkeysend: false, + has_xpay: false, } } } @@ -58,7 +75,7 @@ impl FromStr for TimeUnit { "hour" | "hours" | "h" => Ok(TimeUnit::Hour), "day" | "days" | "d" => Ok(TimeUnit::Day), "week" | "weeks" | "w" => Ok(TimeUnit::Week), - _ => Err(format!("Unsupported time unit: {}", s)), + _ => Err(format!("Unsupported time unit: {s}")), } } } @@ -68,14 +85,18 @@ pub struct BudgetIntervalConfig { pub interval_secs: u64, pub reset_budget_msat: u64, pub last_reset: u64, + #[serde(default)] + pub spend_since_last_reset: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NwcStore { - pub uri: nip47::NostrWalletConnectURI, + pub uri: NostrWalletConnectUri, pub walletkey: String, #[serde(skip_serializing_if = "Option::is_none")] pub budget_msat: Option, #[serde(skip_serializing_if = "Option::is_none")] pub interval_config: Option, + #[serde(default)] + pub reserved_msat: u64, } diff --git a/src/tasks.rs b/src/tasks.rs index d56ea7e..7a53195 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,59 +1,49 @@ -use std::{path::Path, time::Duration}; +use std::{path::Path, str::FromStr, time::Duration}; -use anyhow::anyhow; use cln_plugin::Plugin; -use cln_rpc::ClnRpc; -use nostr_sdk::Timestamp; -use tokio::{sync::oneshot, time}; +use cln_rpc::{ + ClnRpc, + model::requests::{DeldatastoreRequest, ListdatastoreRequest}, +}; +use nostr::types::Timestamp; use crate::{ - structs::PluginState, - util::{load_nwc_store, update_nwc_store}, + PLUGIN_NAME, + structs::{ID_MAX_AGE, ID_STORE, PluginState}, }; -pub async fn budget_task( - mut rx: oneshot::Receiver<()>, - plugin: Plugin, - label: String, -) -> Result<(), anyhow::Error> { +pub async fn cleanup_event_ids(plugin: Plugin) -> Result<(), anyhow::Error> { let mut rpc = ClnRpc::new( Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file), ) .await?; + loop { - let mut nwc_store = load_nwc_store(&mut rpc, &label).await?; - let interval_config = nwc_store - .interval_config - .as_mut() - .ok_or_else(|| anyhow!("interval_config disappeared!"))?; - let now = Timestamp::now().as_u64(); - log::debug!( - "interval:{} now:{} prev:{}", - interval_config.interval_secs, - now, - interval_config.last_reset - ); - let next_reset = std::cmp::max( - interval_config - .interval_secs - .saturating_sub(now.saturating_sub(interval_config.last_reset)), - 1, - ); - tokio::select! { - _ = &mut rx => { - log::info!("Stopping budget task for {}", label); - break; - } - _ = time::sleep(Duration::from_secs(next_reset)) => { - log::info!("Refreshing budget for {}",label); - *nwc_store.budget_msat - .as_mut() - .ok_or_else(||anyhow!("budget_msat missing"))? = interval_config.reset_budget_msat; - interval_config.last_reset = Timestamp::now().as_u64(); - update_nwc_store(&mut rpc, &label, nwc_store).await?; - log::info!("Done refreshing budget for {}",label); + { + let ids = rpc + .call_typed(&ListdatastoreRequest { + key: Some(vec![format!("{}-{}", PLUGIN_NAME, ID_STORE)]), + }) + .await? + .datastore; + + let now = Timestamp::now(); + for id in ids { + if id.string.is_none() { + continue; + } + let timestamp = Timestamp::from_str(&id.string.unwrap())?; + if now.as_secs().saturating_sub(timestamp.as_secs()) > ID_MAX_AGE { + rpc.call_typed(&DeldatastoreRequest { + generation: None, + key: id.key.clone(), + }) + .await?; + log::debug!("Cleaned up event id: {}", id.key.last().unwrap()); + } } } + + tokio::time::sleep(Duration::from_secs(120)).await; } - Ok(()) } diff --git a/src/util.rs b/src/util.rs index 4c4a161..2543e55 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,10 +1,75 @@ -use anyhow::anyhow; -use cln_rpc::{ - model::requests::{DatastoreMode, DatastoreRequest, ListdatastoreRequest}, - ClnRpc, +use std::{ + cmp::max, + path::{Path, PathBuf}, }; -use crate::{structs::NwcStore, PLUGIN_NAME}; +use anyhow::anyhow; +use cln_plugin::Plugin; +use cln_rpc::{ + ClnRpc, + model::requests::{DatastoreMode, DatastoreRequest, ListdatastoreRequest}, +}; +use nostr::{nips::nip47, types::Timestamp}; + +use crate::{ + OPT_NOTIFICATIONS, + PLUGIN_NAME, + WALLET_HOLD_METHODS, + WALLET_HOLD_NOTIFICATIONS, + WALLET_NOTIFICATIONS, + WALLET_PAY_METHODS, + WALLET_READ_OR_RECEIVE_METHODS, + structs::{ID_STORE, NwcStore, PluginState}, +}; + +/// CLN's default maximum fee for a payment is `max(5000msat, 1% of amount)`, +/// so reserving this much guarantees a settled payment never exceeds the budget. +pub const MIN_FEE_RESERVE_MSAT: u64 = 5_000; + +pub fn payment_fee_reserve_msat(amount_msat: u64) -> u64 { + max(MIN_FEE_RESERVE_MSAT, amount_msat.saturating_div(100)) +} + +pub fn get_budget_msat(nwc_store: &NwcStore) -> Option { + match nwc_store.budget_msat { + Some(b) => { + let available = if let Some(conf) = &nwc_store.interval_config { + let now = Timestamp::now().as_secs(); + let spend = if now.saturating_sub(conf.last_reset) >= conf.interval_secs { + 0 + } else { + conf.spend_since_last_reset + }; + conf.reset_budget_msat.saturating_sub(spend) + } else { + b + }; + Some(available.saturating_sub(nwc_store.reserved_msat)) + } + None => None, + } +} + +pub fn update_budget_msat(nwc_store: &mut NwcStore, amount_spent_msat: u64) { + if let Some(bdg) = nwc_store.budget_msat.as_mut() { + if let Some(conf) = nwc_store.interval_config.as_mut() { + let now = Timestamp::now().as_secs(); + if now.saturating_sub(conf.last_reset) >= conf.interval_secs { + conf.last_reset = now; + conf.spend_since_last_reset = amount_spent_msat; + } else { + conf.spend_since_last_reset = conf + .spend_since_last_reset + .saturating_add(amount_spent_msat); + } + *bdg = conf + .reset_budget_msat + .saturating_sub(conf.spend_since_last_reset); + } else { + *bdg = bdg.saturating_sub(amount_spent_msat); + } + } +} pub fn budget_amount_check( request_amt_msat: Option, @@ -12,10 +77,8 @@ pub fn budget_amount_check( budget_msat: Option, ) -> Result<(), anyhow::Error> { log::debug!( - "checking budget and amounts for request:{:?} invoice:{:?} budget:{:?}", - request_amt_msat, - invoice_amt_msat, - budget_msat + "checking budget and amounts for request:{request_amt_msat:?} \ + invoice:{invoice_amt_msat:?} budget:{budget_msat:?}" ); if request_amt_msat.is_none() && invoice_amt_msat.is_none() { return Err(anyhow!("No amount given to check budget against!")); @@ -44,38 +107,104 @@ pub fn budget_amount_check( Ok(()) } -pub async fn load_nwc_store(rpc: &mut ClnRpc, label: &String) -> Result { +pub async fn load_nwc_store(rpc: &mut ClnRpc, label: &str) -> Result { let nwc_store_store = rpc .call_typed(&ListdatastoreRequest { - key: Some(vec![PLUGIN_NAME.to_owned(), label.clone()]), + key: Some(vec![PLUGIN_NAME.to_owned(), label.to_owned()]), }) .await? .datastore; let nwc_store_str = nwc_store_store .first() - .ok_or_else(|| anyhow!("No datastore found for: {}", label))? + .ok_or_else(|| anyhow!("No datastore found for: {label}"))? .string .as_ref() .ok_or_else(|| anyhow!("Malformed nwc_store datastore: missing string"))?; let nwc_store: NwcStore = serde_json::from_str(nwc_store_str)?; - log::debug!("loaded nwc store for label:{}", label); + log::debug!("loaded nwc store for label:{label}"); Ok(nwc_store) } pub async fn update_nwc_store( rpc: &mut ClnRpc, - label: &String, + label: &str, nwc_store: NwcStore, ) -> Result<(), anyhow::Error> { rpc.call_typed(&DatastoreRequest { - key: vec![PLUGIN_NAME.to_owned(), label.clone()], + key: vec![PLUGIN_NAME.to_owned(), label.to_owned()], generation: None, hex: None, mode: Some(DatastoreMode::CREATE_OR_REPLACE), string: Some(serde_json::to_string(&nwc_store)?), }) .await?; - log::debug!("stored nwc store for label:{}", label); + log::debug!("stored nwc store for label:{label}"); + Ok(()) +} + +pub fn rpc_socket_path(plugin: &Plugin) -> PathBuf { + Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file) +} + +/// Reserve the payment amount plus the worst case fee so that no combination +/// of concurrent payments can exceed the budget. Must be called while holding +/// the global rpc lock. Returns the reserved amount (0 if there is no budget). +pub async fn reserve_budget( + rpc: &mut ClnRpc, + label: &str, + nwc_store: &NwcStore, + amount_msat: u64, +) -> Result { + if nwc_store.budget_msat.is_none() { + return Ok(0); + } + + let reservation = amount_msat.saturating_add(payment_fee_reserve_msat(amount_msat)); + let mut new_store = nwc_store.clone(); + new_store.reserved_msat = new_store.reserved_msat.saturating_add(reservation); + update_nwc_store(rpc, label, new_store).await?; + + Ok(reservation) +} + +/// Release the reservation of a payment that did not succeed. Must be called +/// while holding the global rpc lock. +pub async fn refund_budget( + rpc: &mut ClnRpc, + label: &str, + reservation: u64, +) -> Result<(), anyhow::Error> { + if reservation == 0 { + return Ok(()); + } + + let mut nwc_store = load_nwc_store(rpc, label).await?; + nwc_store.reserved_msat = nwc_store.reserved_msat.saturating_sub(reservation); + update_nwc_store(rpc, label, nwc_store).await?; + + Ok(()) +} + +/// Record the actual amount spent by a successful payment and release the +/// remainder of the reservation. Must be called while holding the global rpc +/// lock. Charges the real spend first so that an error leaves the budget +/// conservatively low rather than too high. +pub async fn settle_budget( + rpc: &mut ClnRpc, + label: &str, + reservation: u64, + amount_spent_msat: u64, +) -> Result<(), anyhow::Error> { + // A zero reservation only happens when the nwc has no budget at all. + if reservation == 0 { + return Ok(()); + } + + let mut nwc_store = load_nwc_store(rpc, label).await?; + update_budget_msat(&mut nwc_store, amount_spent_msat); + nwc_store.reserved_msat = nwc_store.reserved_msat.saturating_sub(reservation); + update_nwc_store(rpc, label, nwc_store).await?; + Ok(()) } @@ -88,32 +217,91 @@ pub fn is_read_only_nwc(nwc_store: &NwcStore) -> bool { false } -pub fn at_or_above_version(my_version: &str, min_version: &str) -> Result { - let clean_start_my_version = my_version - .split_once('v') - .ok_or_else(|| anyhow!("Could not find v in version string"))? - .1; - let full_clean_my_version: String = clean_start_my_version - .chars() - .take_while(|x| x.is_ascii_digit() || *x == '.') - .collect(); +pub async fn save_event_id( + rpc: &mut ClnRpc, + id: String, + timestamp: Timestamp, +) -> Result<(), anyhow::Error> { + rpc.call_typed(&DatastoreRequest { + key: vec![format!("{}-{}", PLUGIN_NAME, ID_STORE), id.clone()], + generation: None, + hex: None, + mode: Some(DatastoreMode::MUST_CREATE), + string: Some(timestamp.to_string()), + }) + .await?; + log::debug!("stored event id:{id}"); + Ok(()) +} - let my_version_parts: Vec<&str> = full_clean_my_version.split('.').collect(); - let min_version_parts: Vec<&str> = min_version.split('.').collect(); +pub fn build_capabilities(is_receive_only: bool, plugin: &Plugin) -> (String, String) { + let holdinvoice_support = plugin.state().hold_client.lock().is_some(); - if my_version_parts.len() <= 1 || my_version_parts.len() > 3 { - return Err(anyhow!("Version string parse error: {}", my_version)); + let mut methods = WALLET_READ_OR_RECEIVE_METHODS + .map(|m| m.to_string()) + .join(" "); + if !is_receive_only { + methods.push(' '); + methods.push_str(WALLET_PAY_METHODS.map(|m| m.to_string()).join(" ").as_str()); + } + if holdinvoice_support { + methods.push(' '); + methods.push_str( + WALLET_HOLD_METHODS + .map(|m| m.to_string()) + .join(" ") + .as_str(), + ); } - for (my, min) in my_version_parts.iter().zip(min_version_parts.iter()) { - let my_num: u32 = my.parse()?; - let min_num: u32 = min.parse()?; - if my_num != min_num { - return Ok(my_num > min_num); + let mut notifications = String::new(); + if plugin.option(&OPT_NOTIFICATIONS).unwrap() { + notifications.push_str( + WALLET_NOTIFICATIONS + .map(|m| m.to_string()) + .join(" ") + .as_str(), + ); + if holdinvoice_support { + notifications.push(' '); + notifications.push_str( + WALLET_HOLD_NOTIFICATIONS + .map(|m| m.to_string()) + .join(" ") + .as_str(), + ); } } - Ok(my_version_parts.len() >= min_version_parts.len()) + (methods, notifications) +} + +pub fn build_methods_vec( + is_receive_only: bool, + plugin: &Plugin, +) -> Vec { + let holdinvoice_support = plugin.state().hold_client.lock().is_some(); + let mut methods = WALLET_READ_OR_RECEIVE_METHODS.to_vec(); + if !is_receive_only { + methods.extend_from_slice(&WALLET_PAY_METHODS); + } + if holdinvoice_support { + methods.extend_from_slice(&WALLET_HOLD_METHODS); + } + methods +} + +pub fn build_notifications_vec(plugin: &Plugin) -> Vec { + let holdinvoice_support = plugin.state().hold_client.lock().is_some(); + + let mut notifications = Vec::new(); + if plugin.option(&OPT_NOTIFICATIONS).unwrap() { + notifications.extend_from_slice(&WALLET_NOTIFICATIONS.map(|m| m.to_string())); + if holdinvoice_support { + notifications.extend_from_slice(&WALLET_HOLD_NOTIFICATIONS.map(|m| m.to_string())); + } + } + notifications } #[test] @@ -133,3 +321,105 @@ fn test_budget_check() { assert!(budget_amount_check(Some(0), Some(0), Some(1)).is_ok()); assert!(budget_amount_check(Some(0), Some(0), Some(0)).is_ok()); } + +#[test] +fn test_budget_interval_helpers() { + use crate::structs::BudgetIntervalConfig; + + let now = Timestamp::now().as_secs(); + let conf = BudgetIntervalConfig { + interval_secs: 10, + reset_budget_msat: 1000, + last_reset: now, + spend_since_last_reset: 0, + }; + let mut store = NwcStore { + uri: nostr::nips::nip47::NostrWalletConnectUri::new( + nostr::key::Keys::generate().public_key(), + vec![], + nostr::key::Keys::generate().secret_key().clone(), + None, + ), + walletkey: "test".to_owned(), + budget_msat: Some(1000), + interval_config: Some(conf), + reserved_msat: 0, + }; + + assert_eq!(get_budget_msat(&store), Some(1000)); + + update_budget_msat(&mut store, 300); + assert_eq!( + store + .interval_config + .as_ref() + .unwrap() + .spend_since_last_reset, + 300 + ); + assert_eq!(get_budget_msat(&store), Some(700)); + + update_budget_msat(&mut store, 500); + assert_eq!(get_budget_msat(&store), Some(200)); + + store.interval_config.as_mut().unwrap().last_reset = now.saturating_sub(20); + let old_last_reset = store.interval_config.as_ref().unwrap().last_reset; + assert_eq!(get_budget_msat(&store), Some(1000)); + + update_budget_msat(&mut store, 400); + assert!(store.interval_config.as_ref().unwrap().last_reset > old_last_reset); + assert_eq!( + store + .interval_config + .as_ref() + .unwrap() + .spend_since_last_reset, + 400 + ); + assert_eq!(get_budget_msat(&store), Some(600)); +} + +#[test] +fn test_payment_fee_reserve() { + // fee reserve is at least 5000msat + assert_eq!(payment_fee_reserve_msat(0), 5000); + assert_eq!(payment_fee_reserve_msat(1000), 5000); + assert_eq!(payment_fee_reserve_msat(499_999), 5000); + // and 1% of the amount above that + assert_eq!(payment_fee_reserve_msat(500_000), 5000); + assert_eq!(payment_fee_reserve_msat(1_000_000), 10_000); + assert_eq!(payment_fee_reserve_msat(123_456_789), 1_234_567); +} + +#[test] +fn test_budget_reservations() { + let store = NwcStore { + uri: nostr::nips::nip47::NostrWalletConnectUri::new( + nostr::key::Keys::generate().public_key(), + vec![], + nostr::key::Keys::generate().secret_key().clone(), + None, + ), + walletkey: "test".to_owned(), + budget_msat: Some(10_000), + interval_config: None, + reserved_msat: 0, + }; + + // reserving reduces the available budget + let mut stored = store.clone(); + stored.reserved_msat = 1_500; + assert_eq!(get_budget_msat(&stored), Some(8_500)); + + // a full spend plus reservation can never be under budget + let full_reservation = + stored.clone().budget_msat.unwrap() + payment_fee_reserve_msat(store.budget_msat.unwrap()); + stored.reserved_msat = full_reservation; + assert_eq!(get_budget_msat(&stored), Some(0)); + + // reservations do not affect a store without a budget + let mut no_budget = store; + no_budget.budget_msat = None; + no_budget.reserved_msat = 1_000; + assert_eq!(get_budget_msat(&no_budget), None); +} diff --git a/tests/config.toml b/tests/config.toml new file mode 100644 index 0000000..3afabc9 --- /dev/null +++ b/tests/config.toml @@ -0,0 +1,12 @@ + +[info] +relay_url = "ws://127.0.0.1/" +name = "nostr-rs-relay" +description = "test relay" + +[options] +reject_future_seconds = 1800 +[limits] +limit_scrapers = false +[network] +address = "127.0.0.1" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6f6222a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,50 @@ +import os +import pytest +import tempfile +import shutil +from pathlib import Path +import subprocess +from pyln.testing.fixtures import * # noqa: F403 + + +@pytest.fixture +def nostr_relay(worker_id, node_factory): + port = node_factory.get_unused_port() + + config_path = Path(__file__).parent / "config.toml" + if not config_path.exists(): + raise FileNotFoundError(f"config.toml not found at {config_path}") + + temp_dir = Path(tempfile.mkdtemp()) + + # Copy your original config.toml into it + original_config = Path(__file__).parent / "config.toml" + temp_config = temp_dir / "config.toml" + shutil.copy(original_config, temp_config) + + with temp_config.open("a") as f: + f.write(f"port = {port}\n") + + proc = subprocess.Popen( + ["./nostr-rs-relay", "--config", str(temp_config), "--db", str(temp_dir)], + cwd=Path(__file__).parent, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=os.environ | {"RUST_LOG": "warn,nostr_rs_relay=debug"}, + ) + + try: + import time + + time.sleep(1.0) + + ws_url = f"ws://127.0.0.1:{port}" + yield ws_url + + finally: + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() diff --git a/tests/nostr-rs-relay.tar.gz b/tests/nostr-rs-relay.tar.gz new file mode 100644 index 0000000..705210e Binary files /dev/null and b/tests/nostr-rs-relay.tar.gz differ diff --git a/tests/pyproject.toml b/tests/pyproject.toml new file mode 100644 index 0000000..bcde559 --- /dev/null +++ b/tests/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "cln-nip47" +version = "0.1.0" +requires-python = ">=3.10" +description = "python dependencies for running tests" + +[dependency-groups] +dev = [ + "pytest>=8,<10", + "pytest-asyncio>=0.23.8,<2", + "pytest-xdist>=3.7,<4", + "pytest-timeout>=2.4,<3", + "nostr-sdk>=0.45", + "pyln-testing>=25.9", + "pyln-client>=25.9", + "pyln-proto>=25.9", +] diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index bb18cbc..0000000 --- a/tests/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -pytest-asyncio<0.24 -nostr-sdk -PyYAML -nostr_relay diff --git a/tests/setup.sh b/tests/setup.sh index 8ae9603..829a336 100755 --- a/tests/setup.sh +++ b/tests/setup.sh @@ -49,52 +49,49 @@ github_url="https://github.com/daywalker90/$name/releases/download/v$version/$ar # Download the archive using curl if ! curl -L "$github_url" -o "$script_dir/$archive_file"; then echo "Error downloading the file from $github_url" >&2 - # exit 1 + exit 1 fi # Extract the contents if [[ $archive_file == *.tar.gz ]]; then if ! tar -xzvf "$script_dir/$archive_file" -C "$script_dir"; then echo "Error extracting the contents of $archive_file" >&2 - # exit 1 + exit 1 fi elif [[ $archive_file == *.zip ]]; then if ! unzip "$script_dir/$archive_file" -d "$script_dir"; then echo "Error extracting the contents of $archive_file" >&2 - # exit 1 + exit 1 fi else echo "Unknown archive format or unsupported file extension: $archive_file" >&2 - # exit 1 + exit 1 fi - -# Function to check if a Python package is installed -check_package() { - python_exec="$1" - package_name="$2" - if $python_exec -c "import $package_name" &> /dev/null; then - return 0 - else - return 1 - fi -} +if ! tar -xzvf "$script_dir/nostr-rs-relay.tar.gz" -C "$script_dir"; then + echo "Error extracting the contents of nostr-rs-relay.tar.gz" >&2 + exit 1 +fi proto_path="$script_dir/../proto" if [ -d "$proto_path" ]; then - # Check if the package is installed in the first Python executable - if check_package "$TEST_DIR/bin/python3" "grpc"; then - python_exec="$TEST_DIR/bin/python3" - elif check_package "python3" "grpc"; then - python_exec="python3" - else - echo "Error: Package 'grpcio' is not installed" >&2 - exit 1 - fi - # Generate grpc files - if ! "$python_exec" -m grpc_tools.protoc --proto_path="$proto_path" --python_out=$script_dir --grpc_python_out=$script_dir $proto_path/*.proto; then + if ! uv run python -m grpc_tools.protoc --proto_path="$proto_path" --python_out=$script_dir --grpc_python_out=$script_dir $proto_path/*.proto; then echo "Error generating grpc files" >&2 exit 1 fi fi + +hold_url="https://github.com/BoltzExchange/hold/releases/download/v0.3.3/hold-linux-amd64.tar.gz" + +if ! curl -L "$hold_url" -o "$script_dir/hold-linux-amd64.tar.gz"; then + echo "Error downloading the file from $hold_url" >&2 + exit 1 +fi + +if ! tar -xzvf "$script_dir/hold-linux-amd64.tar.gz" -C "$script_dir"; then + echo "Error extracting the contents of hold-linux-amd64.tar.gz" >&2 + exit 1 +fi + +mv "$script_dir/build/hold-linux-amd64" "$script_dir/hold" diff --git a/tests/test_cln-nip47.py b/tests/test_cln-nip47.py new file mode 100644 index 0000000..36260fd --- /dev/null +++ b/tests/test_cln-nip47.py @@ -0,0 +1,1913 @@ +# ruff: noqa: DTZ005 + +import asyncio +import hashlib +import inspect +import json +import logging +import secrets +import time +import uuid +from collections.abc import Awaitable, Callable +from datetime import datetime, timedelta +from typing import Any, Union + +import pytest +from nostr_sdk import ( + Client, + Event, + EventBuilder, + Filter, + Keys, + KeysendTlvRecord, + Kind, + ListTransactionsRequest, + LookupInvoiceRequest, + MakeInvoiceRequest, + Method, + NostrSdkError, + NostrWalletConnect, + NostrWalletConnectUri, + PayInvoiceRequest, + PayKeysendRequest, + PublicKey, + RelayUrl, + ReqTarget, + Tag, + TransactionType, +) +from pyln.testing.fixtures import * +from pyln.testing.utils import TIMEOUT, RpcError, wait_for +from util import generate_random_label, get_hold, get_plugin # noqa: F401 + +LOGGER = logging.getLogger(__name__) + + +Action = Union[ # noqa: UP007 + Callable[[], Awaitable[None]], + Callable[[], None], + Awaitable[None], +] + + +async def fetch_event_responses( + client: Client, + client_pubkey: PublicKey, + event_kind: int, + action: Action, + stop_after: int, + timeout: int = TIMEOUT, +) -> tuple[list[Event], Any]: + events = [] + response_filter = Filter().kind(Kind(event_kind)).pubkey(client_pubkey) + target = ReqTarget.auto([response_filter]) + + subscription_id = uuid.uuid4().hex + LOGGER.info(f"Subscribing with id {subscription_id} to {response_filter}") + + await client.subscribe(target, subscription_id) + + async def collect_events(): + stream = client.notifications() + + while len(events) < stop_after: + notification = await stream.next() + + if notification.is_new_event(): + event = notification.event + relay_url = notification.relay_url + + LOGGER.info(f"Received new event from {relay_url}: {event.as_json()}") + + events.append(event) + + task = asyncio.create_task(collect_events()) + + await asyncio.sleep(1) + + if inspect.iscoroutine(action): + action_result = await action + elif inspect.iscoroutinefunction(action): + action_result = await action() + elif callable(action): + action_result = await asyncio.to_thread(action) + else: + raise TypeError("action must be a callable or an awaitable") + + try: + await asyncio.wait_for(task, timeout=timeout) + except asyncio.TimeoutError: + print( + f"Timeout reached after {timeout} seconds, collected {len(events)} events", + ) + finally: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + await client.unsubscribe_all() + + assert len(events) == stop_after + return events, action_result + + +async def fetch_info_event( + client: Client, + uri: NostrWalletConnectUri, +) -> Event: + response_filter = Filter().kind(Kind(13194)).author(uri.public_key()) + target = ReqTarget.auto([response_filter]) + events = await client.fetch_events(target, timeout=timedelta(seconds=TIMEOUT)) + start_time = datetime.now() + while len(events) < 1 and (datetime.now() - start_time) < timedelta( + seconds=TIMEOUT + ): + await asyncio.sleep(1) + events = await client.fetch_events(target, timeout=timedelta(seconds=1)) + assert len(events) == 1 + + return events[0] + + +@pytest.mark.asyncio +async def test_get_balance(nostr_relay, node_factory, get_plugin): # noqa: F811 + url = nostr_relay + l1, _l2 = node_factory.line_graph( + 2, + wait_for_announce=True, + opts=[ + { + "plugin": get_plugin, + "nip47-relays": url, + "broken_log": r"Relay receiver exited with error|Connection failed", + }, + {"log-level": "debug"}, + ], + ) + node_balance = l1.rpc.call("listpeerchannels", {})["channels"][0]["spendable_msat"] + uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"] + LOGGER.info(uri_str) + uri = NostrWalletConnectUri.parse(uri_str) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + nwc = NostrWalletConnect(uri) + balance = await nwc.get_balance() + assert balance.balance == 3000 + + uri_str = l1.rpc.call("nip47-create", ["test2"])["uri"] + LOGGER.info(uri_str) + uri = NostrWalletConnectUri.parse(uri_str) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + nwc = NostrWalletConnect(uri) + balance = await nwc.get_balance() + assert balance.balance == node_balance + + uri_str = l1.rpc.call("nip47-create", ["test3", 0])["uri"] + LOGGER.info(uri_str) + uri = NostrWalletConnectUri.parse(uri_str) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + nwc = NostrWalletConnect(uri) + balance = await nwc.get_balance() + assert balance.balance == 0 + + with pytest.raises(RpcError, match="not an integer"): + uri_str = l1.rpc.call("nip47-create", ["test3", -1])["uri"] + + +@pytest.mark.asyncio +async def test_get_info(nostr_relay, node_factory, get_plugin): # noqa: F811 + url = nostr_relay + l1 = node_factory.get_node( + options={ + "plugin": get_plugin, + "nip47-relays": url, + }, + broken_log=r"Relay receiver exited with error|Connection failed", + ) + node_get_info = l1.rpc.call("getinfo", {}) + uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"] + LOGGER.info(uri_str) + uri = NostrWalletConnectUri.parse(uri_str) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + nwc = NostrWalletConnect(uri) + get_info = await nwc.get_info() + assert get_info.alias == node_get_info["alias"] + assert get_info.block_height == node_get_info["blockheight"] + assert get_info.color == node_get_info["color"] + assert get_info.methods == [ + Method.MAKE_INVOICE(), + Method.LOOKUP_INVOICE(), + Method.LIST_TRANSACTIONS(), + Method.GET_BALANCE(), + Method.GET_INFO(), + Method.PAY_INVOICE(), + Method.PAY_KEYSEND(), + ] + assert get_info.network == "regtest" + assert get_info.notifications == ["payment_received", "payment_sent"] + assert get_info.pubkey == node_get_info["id"] + + l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"}) + l1.rpc.call( + "plugin", + { + "subcommand": "start", + "plugin": str(get_plugin), + "nip47-notifications": False, + }, + ) + l1.daemon.wait_for_log("All NWC's loaded") + await asyncio.sleep(5) + await client.connect() + info_event = await fetch_info_event(client, uri) + get_info = await nwc.get_info() + assert get_info.alias == node_get_info["alias"] + assert get_info.block_height == node_get_info["blockheight"] + assert get_info.color == node_get_info["color"] + assert get_info.methods == [ + Method.MAKE_INVOICE(), + Method.LOOKUP_INVOICE(), + Method.LIST_TRANSACTIONS(), + Method.GET_BALANCE(), + Method.GET_INFO(), + Method.PAY_INVOICE(), + Method.PAY_KEYSEND(), + ] + assert get_info.network == "regtest" + assert get_info.notifications == [] + assert get_info.pubkey == node_get_info["id"] + + assert ( + info_event.content() + == "make_invoice lookup_invoice list_transactions get_balance get_info pay_invoice pay_keysend" + ) + encryption_tag = next( + tag for tag in info_event.tags() if tag.kind() == "encryption" + ) + assert encryption_tag.content() == "nip44_v2 nip04" + assert not any(tag.kind() == "notifications" for tag in info_event.tags()) + + uri_str = l1.rpc.call("nip47-create", ["test2", 0])["uri"] + LOGGER.info(uri_str) + uri = NostrWalletConnectUri.parse(uri_str) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + nwc = NostrWalletConnect(uri) + get_info = await nwc.get_info() + assert get_info.methods == [ + Method.MAKE_INVOICE(), + Method.LOOKUP_INVOICE(), + Method.LIST_TRANSACTIONS(), + Method.GET_BALANCE(), + Method.GET_INFO(), + ] + + info_event = await fetch_info_event(client, uri) + assert ( + info_event.content() + == "make_invoice lookup_invoice list_transactions get_balance get_info" + ) + encryption_tag = next( + tag for tag in info_event.tags() if tag.kind() == "encryption" + ) + assert encryption_tag.content() == "nip44_v2 nip04" + assert not any(tag.kind() == "notifications" for tag in info_event.tags()) + + +@pytest.mark.asyncio +async def test_make_invoice(nostr_relay, node_factory, get_plugin): # noqa: F811 + url = nostr_relay + l1 = node_factory.get_node( + options={ + "plugin": get_plugin, + "nip47-relays": url, + }, + broken_log=r"Relay receiver exited with error|Connection failed", + ) + uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"] + LOGGER.info(uri_str) + uri = NostrWalletConnectUri.parse(uri_str) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + nwc = NostrWalletConnect(uri) + timestamp = int(time.time()) + invoice = await nwc.make_invoice( + MakeInvoiceRequest( + amount=3000, description="test1", description_hash=None, expiry=None + ) + ) + node_invoice = l1.rpc.call("decode", [invoice.invoice]) + assert invoice.payment_hash == node_invoice["payment_hash"] + assert node_invoice["amount_msat"] == invoice.amount + assert timestamp + node_invoice["expiry"] == pytest.approx( + invoice.expires_at.as_secs(), abs=3 + ) + assert node_invoice["created_at"] == pytest.approx( + invoice.created_at.as_secs(), abs=3 + ) + assert node_invoice["description"] == invoice.description + assert "description_hash" not in node_invoice + assert invoice.description_hash is None + + timestamp = int(time.time()) + invoice = await nwc.make_invoice( + MakeInvoiceRequest( + amount=3001, + description="test2", + description_hash=hashlib.sha256(b"test2").hexdigest(), + expiry=120, + ) + ) + node_invoice = l1.rpc.call("listinvoices", {"invstring": invoice.invoice})[ + "invoices" + ][0] + node_invoice_decode = l1.rpc.call("decode", [invoice.invoice]) + assert invoice.payment_hash == node_invoice["payment_hash"] + assert node_invoice["amount_msat"] == invoice.amount + assert timestamp + node_invoice_decode["expiry"] == pytest.approx( + invoice.expires_at.as_secs(), abs=3 + ) + assert node_invoice_decode["created_at"] == pytest.approx( + invoice.created_at.as_secs(), abs=3 + ) + assert node_invoice["description"] == invoice.description + assert node_invoice_decode["description_hash"] == invoice.description_hash + + with pytest.raises( + NostrSdkError.Generic, match="Must have description when using description_hash" + ): + await nwc.make_invoice( + MakeInvoiceRequest( + amount=3001, + description=None, + description_hash=hashlib.sha256(b"test2").hexdigest(), + expiry=120, + ) + ) + with pytest.raises( + NostrSdkError.Generic, match="description_hash not matching description" + ): + await nwc.make_invoice( + MakeInvoiceRequest( + amount=3001, + description="test1", + description_hash=hashlib.sha256(b"test2").hexdigest(), + expiry=120, + ) + ) + + +@pytest.mark.asyncio +async def test_pay_keysend(nostr_relay, node_factory, get_plugin): # noqa: F811 + url = nostr_relay + l1, l2, l3 = node_factory.line_graph( + 3, + wait_for_announce=True, + opts=[ + { + "plugin": get_plugin, + "nip47-relays": url, + "broken_log": r"Relay receiver exited with error|Connection failed", + }, + {"log-level": "debug"}, + {"log-level": "debug"}, + ], + ) + uri_str = l1.rpc.call("nip47-create", ["test1", 8000])["uri"] + LOGGER.info(uri_str) + uri = NostrWalletConnectUri.parse(uri_str) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + nwc = NostrWalletConnect(uri) + result = await nwc.pay_keysend( + PayKeysendRequest( + id="id123", amount=1000, pubkey=l3.info["id"], preimage=None, tlv_records=[] + ) + ) + pay = l1.rpc.call("listpays", {})["pays"][0] + assert result.preimage == pay["preimage"] + assert result.fees_paid == pay["amount_sent_msat"] - pay["amount_msat"] + assert result.fees_paid == 1 + + with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): + await nwc.pay_keysend( + PayKeysendRequest( + id="id123", + amount=7500, + pubkey=l2.info["id"], + preimage=None, + tlv_records=[KeysendTlvRecord(tlv_type=1234, value="a5c7e3d9b")], + ) + ) + with pytest.raises( + NostrSdkError.Generic, match="CLN generates the preimage itself" + ): + await nwc.pay_keysend( + PayKeysendRequest( + id="id123", + amount=7500, + pubkey=l2.info["id"], + preimage="or3ijro3ijroi", + tlv_records=[KeysendTlvRecord(tlv_type=1234, value="a5c7e3d9b")], + ) + ) + + +@pytest.mark.asyncio +async def test_lookup_invoice(nostr_relay, node_factory, get_plugin): # noqa: F811 + url = nostr_relay + l1, l2, l3 = node_factory.line_graph( + 3, + wait_for_announce=True, + opts=[ + { + "plugin": get_plugin, + "nip47-relays": url, + "broken_log": r"Relay receiver exited with error|Connection failed", + }, + {"log-level": "debug"}, + {"log-level": "debug"}, + ], + ) + l1.rpc.call( + "pay", + { + "bolt11": l2.rpc.call( + "invoice", + { + "amount_msat": 500000000, + "label": generate_random_label(), + "description": "balancechannel", + }, + )["bolt11"] + }, + ) + wait_for( + lambda: ( + l2.rpc.call("listpeerchannels", [l1.info["id"]])["channels"][0][ + "spendable_msat" + ] + > 3001 + ) + ) + uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"] + LOGGER.info(uri_str) + uri = NostrWalletConnectUri.parse(uri_str) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + nwc = NostrWalletConnect(uri) + invoice = await nwc.make_invoice( + MakeInvoiceRequest( + amount=3000, description="test1", description_hash=None, expiry=None + ) + ) + + with pytest.raises( + NostrSdkError.Generic, match="Neither invoice nor payment_hash given" + ): + await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=None, + invoice=None, + ) + ) + + listpays_rpc = l1.rpc.call("listinvoices", {"invstring": invoice.invoice})[ + "invoices" + ][0] + invoice_decode = l1.rpc.call("decode", [invoice.invoice]) + + invoice_lookup = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=invoice.payment_hash, + invoice=None, + ) + ) + assert invoice_lookup.invoice == invoice.invoice + assert invoice_lookup.amount == 3000 + assert invoice_lookup.description == "test1" + assert invoice_lookup.created_at.as_secs() == pytest.approx( + invoice_decode["created_at"], abs=3 + ) + assert invoice_lookup.description_hash is None + assert invoice_lookup.expires_at.as_secs() == pytest.approx( + listpays_rpc["expires_at"], abs=3 + ) + assert invoice_lookup.fees_paid == 0 + assert invoice_lookup.metadata is None + assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"] + assert invoice_lookup.transaction_type.name == "INCOMING" + assert invoice_lookup.state.name == "PENDING" + assert invoice_lookup.settled_at is None + + invoice_lookup = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=None, + invoice=invoice.invoice, + ) + ) + assert invoice_lookup.invoice == invoice.invoice + assert invoice_lookup.amount == 3000 + assert invoice_lookup.description == "test1" + assert invoice_lookup.created_at.as_secs() == pytest.approx( + invoice_decode["created_at"], abs=3 + ) + assert invoice_lookup.description_hash is None + assert invoice_lookup.expires_at.as_secs() == pytest.approx( + listpays_rpc["expires_at"], abs=3 + ) + assert invoice_lookup.fees_paid == 0 + assert invoice_lookup.metadata is None + assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"] + assert invoice_lookup.transaction_type.name == "INCOMING" + assert invoice_lookup.state.name == "PENDING" + assert invoice_lookup.settled_at is None + + invoice = await nwc.make_invoice( + MakeInvoiceRequest( + amount=3001, + description="test2", + description_hash=hashlib.sha256(b"test2").hexdigest(), + expiry=1000, + ) + ) + + listpays_rpc = l1.rpc.call("listinvoices", {"invstring": invoice.invoice})[ + "invoices" + ][0] + invoice_decode = l1.rpc.call("decode", [invoice.invoice]) + + invoice_lookup = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=invoice.payment_hash, + invoice=None, + ) + ) + assert invoice_lookup.invoice == invoice.invoice + assert invoice_lookup.amount == 3001 + assert invoice_lookup.description is None + assert invoice_lookup.created_at.as_secs() == pytest.approx( + invoice_decode["created_at"], abs=3 + ) + assert invoice_lookup.description_hash == hashlib.sha256(b"test2").hexdigest() + assert invoice_lookup.expires_at.as_secs() == pytest.approx( + listpays_rpc["expires_at"], abs=3 + ) + assert invoice_lookup.fees_paid == 0 + assert invoice_lookup.metadata is None + assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"] + assert invoice_lookup.transaction_type.name == "INCOMING" + assert invoice_lookup.state.name == "PENDING" + assert invoice_lookup.settled_at is None + + l2.rpc.call("pay", {"bolt11": invoice.invoice}) + listpays_rpc = l1.rpc.call("listinvoices", {"invstring": invoice.invoice})[ + "invoices" + ][0] + invoice_lookup = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=invoice.payment_hash, + invoice=None, + ) + ) + assert invoice_lookup.invoice == invoice.invoice + assert invoice_lookup.amount == 3001 + assert invoice_lookup.description is None + assert invoice_lookup.created_at.as_secs() == pytest.approx( + invoice_decode["created_at"], abs=3 + ) + assert invoice_lookup.description_hash == hashlib.sha256(b"test2").hexdigest() + assert invoice_lookup.expires_at.as_secs() == pytest.approx( + listpays_rpc["expires_at"], abs=3 + ) + assert invoice_lookup.fees_paid == 0 + assert invoice_lookup.metadata is None + assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"] + assert invoice_lookup.transaction_type.name == "INCOMING" + assert invoice_lookup.state.name == "SETTLED" + assert invoice_lookup.settled_at.as_secs() == pytest.approx( + listpays_rpc["paid_at"], abs=3 + ) + + invoice = l3.rpc.call( + "invoice", + { + "amount_msat": 4000, + "label": generate_random_label(), + "description": "outgoing", + }, + ) + invoice_decode = l3.rpc.call("decode", [invoice["bolt11"]]) + pay = l1.rpc.call("pay", {"bolt11": invoice["bolt11"]}) + listpays_rpc = l1.rpc.call("listpays", {"bolt11": invoice["bolt11"]})["pays"][0] + invoice_lookup = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=pay["payment_hash"], + invoice=None, + ) + ) + assert invoice_lookup.invoice == invoice["bolt11"] + assert invoice_lookup.amount == 4000 + assert invoice_lookup.description == "outgoing" + assert invoice_lookup.created_at.as_secs() == pytest.approx( + invoice_decode["created_at"], abs=3 + ) + assert invoice_lookup.description_hash is None + assert invoice_lookup.expires_at is None + assert invoice_lookup.fees_paid == 1 + assert invoice_lookup.metadata is None + assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"] + assert invoice_lookup.transaction_type.name == "OUTGOING" + assert invoice_lookup.state.name == "SETTLED" + assert invoice_lookup.settled_at.as_secs() == pytest.approx( + listpays_rpc["completed_at"], abs=3 + ) + + invoice = await nwc.make_invoice( + MakeInvoiceRequest( + amount=0, description="test_0_amt", description_hash=None, expiry=None + ) + ) + invoice_lookup = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=invoice.payment_hash, + invoice=None, + ) + ) + assert invoice_lookup.amount == 0 + + +@pytest.mark.asyncio +async def test_list_transactions(nostr_relay, node_factory, get_plugin): # noqa: F811 + url = nostr_relay + l1, l2 = node_factory.line_graph( + 2, + wait_for_announce=True, + opts=[ + { + "plugin": get_plugin, + "nip47-relays": url, + "broken_log": r"Relay receiver exited with error|Connection failed", + }, + {"log-level": "debug"}, + ], + ) + l1.rpc.call( + "xpay", + { + "invstring": l2.rpc.call( + "invoice", + { + "amount_msat": 500000000, + "label": generate_random_label(), + "description": "balancechannel", + }, + )["bolt11"] + }, + ) + wait_for( + lambda: ( + l2.rpc.call("listpeerchannels", [l1.info["id"]])["channels"][0][ + "spendable_msat" + ] + > 400000000 + ) + ) + uri_str = l1.rpc.call("nip47-create", ["test1"])["uri"] + LOGGER.info(uri_str) + uri = NostrWalletConnectUri.parse(uri_str) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + nwc = NostrWalletConnect(uri) + for i in range(10): + invoice = l2.rpc.call( + "invoice", + { + "label": generate_random_label(), + "description": "test1", + "amount_msat": 3000, + }, + ) + result = await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) + ) + assert result.preimage is not None + for i in range(10): + invoice = await nwc.make_invoice( + MakeInvoiceRequest( + amount=3000, description="test2", description_hash=None, expiry=None + ) + ) + result = l2.rpc.call("xpay", [invoice.invoice]) + + invoice = await nwc.make_invoice( + MakeInvoiceRequest( + amount=0, description="test_0_amt", description_hash=None, expiry=None + ) + ) + result = l2.rpc.call("pay", [invoice.invoice, 1111]) + + result = await nwc.list_transactions( + ListTransactionsRequest( + _from=None, + until=None, + limit=None, + offset=None, + unpaid=None, + transaction_type=None, + ) + ) + assert len(result) == 22 + for tx in result: + assert tx.description is not None + assert tx.invoice is not None + assert tx.amount is not None + assert tx.created_at is not None + assert tx.description_hash is None + assert tx.preimage is not None + assert tx.settled_at is not None + assert tx.metadata is None + assert tx.transaction_type is not None + assert tx.state is not None + assert tx.payment_hash is not None + assert tx.fees_paid is not None + + if tx.transaction_type == TransactionType.INCOMING: + assert tx.expires_at is not None + else: + assert tx.expires_at is None + + +@pytest.mark.asyncio +async def test_notifications(nostr_relay, node_factory, get_plugin): # noqa: F811 + url = nostr_relay + l1, l2, l3 = node_factory.line_graph( + 3, + wait_for_announce=True, + opts=[ + { + "plugin": get_plugin, + "nip47-relays": url, + "broken_log": r"Relay receiver exited with error|Connection failed", + }, + {"log-level": "debug"}, + {"plugin": get_plugin, "nip47-relays": url}, + ], + ) + uri_res = l1.rpc.call("nip47-create", ["test1"]) + uri_str = uri_res["uri"] + client_pubkey = PublicKey.parse(uri_res["clientkey_public"]) + LOGGER.info(uri_str) + + uri = NostrWalletConnectUri.parse(uri_str) + keys = Keys(uri.secret()) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + nwc = NostrWalletConnect(uri) + + invoice = l3.rpc.call( + "invoice", + { + "label": generate_random_label(), + "description": "test1", + "amount_msat": 500000000, + }, + ) + + (responses1, pay1) = await fetch_event_responses( + client, + client_pubkey, + 23196, + lambda: l1.rpc.call("pay", [invoice["bolt11"]]), + 1, + ) + + invoice1_rpc = l3.rpc.call("listinvoices", {"invstring": invoice["bolt11"]})[ + "invoices" + ][0] + invoice1_decode = l3.rpc.call("decode", [invoice["bolt11"]]) + pay1_list = l1.rpc.call("listpays", {"bolt11": invoice["bolt11"]})["pays"][0] + + wait_for( + lambda: ( + l2.rpc.call("listpeerchannels", [l1.info["id"]])["channels"][0][ + "spendable_msat" + ] + > 3000 + ) + ) + wait_for( + lambda: ( + l3.rpc.call("listpeerchannels", [l2.info["id"]])["channels"][0][ + "spendable_msat" + ] + > 3000 + ) + ) + + result = await nwc.make_invoice( + MakeInvoiceRequest( + amount=3000, description="test2", description_hash=None, expiry=None + ) + ) + (responses2, pay2) = await fetch_event_responses( + client, + client_pubkey, + 23196, + lambda: l3.rpc.call("pay", [result.invoice]), + 1, + ) + invoice2_list = l1.rpc.call("listinvoices", {"invstring": result.invoice})[ + "invoices" + ][0] + invoice2_decode = l3.rpc.call("decode", [result.invoice]) + + responses = responses1 + responses2 + LOGGER.info(f"response1: {responses1} response2: {responses2}") + assert len(responses) == 2 + received_events = [] + sent_events = [] + for event in responses: + content = keys.nip04_decrypt(uri.public_key(), event.content()) + content = json.loads(content) + LOGGER.info(content) + if content["notification_type"] == "payment_received": + received_events.append(content) + if content["notification_type"] == "payment_sent": + sent_events.append(content) + assert content["notification"]["preimage"] is not None + assert len(received_events) == 1 + assert len(sent_events) == 1 + assert received_events[0]["notification"]["type"] == "incoming" + assert received_events[0]["notification"]["invoice"] == result.invoice + assert received_events[0]["notification"]["description"] == "test2" + assert "description_hash" not in received_events[0]["notification"] + assert received_events[0]["notification"]["preimage"] == pay2["payment_preimage"] + assert received_events[0]["notification"]["payment_hash"] == pay2["payment_hash"] + assert received_events[0]["notification"]["amount"] == 3000 + assert received_events[0]["notification"]["fees_paid"] == 0 + assert received_events[0]["notification"]["created_at"] == pytest.approx( + invoice2_decode["created_at"], abs=3 + ) + assert "expires_at" not in received_events[0]["notification"] + assert received_events[0]["notification"]["settled_at"] == pytest.approx( + invoice2_list["paid_at"], abs=3 + ) + assert "metadata" not in received_events[0]["notification"] + + assert sent_events[0]["notification"]["type"] == "outgoing" + assert sent_events[0]["notification"]["invoice"] == invoice["bolt11"] + assert sent_events[0]["notification"]["description"] == "test1" + assert "description_hash" not in sent_events[0]["notification"] + assert sent_events[0]["notification"]["preimage"] == pay1["payment_preimage"] + assert ( + sent_events[0]["notification"]["payment_hash"] == invoice1_rpc["payment_hash"] + ) + assert sent_events[0]["notification"]["amount"] == 500000000 + assert sent_events[0]["notification"]["fees_paid"] == 5001 + assert sent_events[0]["notification"]["created_at"] == pytest.approx( + invoice1_decode["created_at"], abs=3 + ) + assert "expires_at" not in sent_events[0]["notification"] + assert sent_events[0]["notification"]["settled_at"] == pytest.approx( + pay1_list["completed_at"], abs=3 + ) + assert "metadata" not in sent_events[0]["notification"] + + l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"}) + l1.rpc.call( + "plugin", + { + "subcommand": "start", + "plugin": str(get_plugin), + "nip47-notifications": False, + }, + ) + l1.daemon.wait_for_log("All NWC's loaded") + await asyncio.sleep(3) + await client.connect() + await fetch_info_event(client, uri) + + invoice = l3.rpc.call( + "invoice", + { + "label": generate_random_label(), + "description": "test3", + "amount_msat": 500, + }, + ) + with pytest.raises(AssertionError, match="0 == 1"): + (_responses3, _pay3) = await fetch_event_responses( + client, + client_pubkey, + 23196, + nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) + ), + 1, + 6, + ) + + +@pytest.mark.asyncio +async def test_pay_invoice(nostr_relay, node_factory, get_plugin): # noqa: F811 + url = nostr_relay + l1, l2 = node_factory.line_graph( + 2, + wait_for_announce=True, + opts=[ + { + "plugin": get_plugin, + "nip47-relays": url, + "broken_log": r"Relay receiver exited with error|Connection failed", + }, + {"log-level": "debug"}, + ], + ) + uri_str = l1.rpc.call("nip47-create", ["test1", 9000])["uri"] + uri = NostrWalletConnectUri.parse(uri_str) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + LOGGER.info(uri_str) + invoice = l2.rpc.call( + "invoice", + {"label": generate_random_label(), "description": "test1", "amount_msat": 3000}, + ) + nwc = NostrWalletConnect(NostrWalletConnectUri.parse(uri_str)) + result = await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) + ) + pay = l1.rpc.call("listpays", {"payment_hash": invoice["payment_hash"]})["pays"][0] + assert result.preimage == pay["preimage"] + + invoice = l2.rpc.call( + "invoice", + {"label": generate_random_label(), "description": "test2", "amount_msat": 1}, + ) + with pytest.raises(NostrSdkError.Generic, match="unnecessary"): + await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=1, invoice=invoice["bolt11"]) + ) + invoice = l2.rpc.call( + "invoice", + {"label": generate_random_label(), "description": "test3", "amount_msat": 6500}, + ) + with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): + await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) + ) + + invoice = l2.rpc.call( + "invoice", + { + "label": generate_random_label(), + "description": "test1", + "amount_msat": "any", + }, + ) + result = await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=2, invoice=invoice["bolt11"]) + ) + pay = l1.rpc.call("listpays", {"payment_hash": invoice["payment_hash"]})["pays"][0] + assert result.preimage == pay["preimage"] + assert pay["amount_msat"] == 2 + + +@pytest.mark.asyncio +async def test_persistency(nostr_relay, node_factory, get_plugin): # noqa: F811 + url = nostr_relay + l1, l2 = node_factory.line_graph( + 2, + wait_for_announce=True, + opts=[ + { + "plugin": get_plugin, + "nip47-relays": url, + "broken_log": r"Relay receiver exited with error|Connection failed", + }, + {"log-level": "debug"}, + ], + ) + uri_str = l1.rpc.call("nip47-create", ["test1", 8000])["uri"] + LOGGER.info(uri_str) + invoice = l2.rpc.call( + "invoice", + {"label": generate_random_label(), "description": "test1", "amount_msat": 3000}, + ) + l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"}) + l1.rpc.call( + "plugin", + { + "subcommand": "start", + "plugin": str(get_plugin), + }, + ) + l1.daemon.wait_for_log("All NWC's loaded") + await asyncio.sleep(3) + uri = NostrWalletConnectUri.parse(uri_str) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + nwc = NostrWalletConnect(uri) + result = await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) + ) + assert result.preimage is not None + + invoice = l2.rpc.call( + "invoice", + {"label": generate_random_label(), "description": "test1", "amount_msat": 5500}, + ) + with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): + await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) + ) + l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"}) + l1.rpc.call( + "plugin", + { + "subcommand": "start", + "plugin": str(get_plugin), + }, + ) + l1.daemon.wait_for_log("All NWC's loaded") + await asyncio.sleep(3) + await client.connect() + await fetch_info_event(client, uri) + with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): + await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) + ) + + revoke = l1.rpc.call("nip47-revoke", ["test1"]) + assert revoke["revoked"] == "test1" + + uri_str = l1.rpc.call("nip47-create", ["test1", 8000, "10sec"])["uri"] + uri = NostrWalletConnectUri.parse(uri_str) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + nwc = NostrWalletConnect(uri) + + invoice = l2.rpc.call( + "invoice", + {"label": generate_random_label(), "description": "test1", "amount_msat": 3000}, + ) + invoice_exceeded = l2.rpc.call( + "invoice", + {"label": generate_random_label(), "description": "test1", "amount_msat": 5500}, + ) + invoice_fee_exceeded = l2.rpc.call( + "invoice", + {"label": generate_random_label(), "description": "test1", "amount_msat": 1}, + ) + result = await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) + ) + assert result.preimage is not None + + list = l1.rpc.call("nip47-list", ["test1"])[0] + assert list["test1"]["budget_msat"] == 5000 + + with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): + await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice_exceeded["bolt11"]) + ) + with pytest.raises( + NostrSdkError.Generic, + match="Payment and estimated fees exceed the available budget", + ): + await nwc.pay_invoice( + PayInvoiceRequest( + id=None, amount=None, invoice=invoice_fee_exceeded["bolt11"] + ) + ) + + await asyncio.sleep(11) + + list = l1.rpc.call("nip47-list", ["test1"])[0] + assert list["test1"]["budget_msat"] == 8000 + + invoice = l2.rpc.call( + "invoice", + {"label": generate_random_label(), "description": "test1", "amount_msat": 3000}, + ) + result = await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) + ) + assert result.preimage is not None + + list = l1.rpc.call("nip47-list", ["test1"])[0] + assert list["test1"]["budget_msat"] == 5000 + + with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): + await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice_exceeded["bolt11"]) + ) + + l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"}) + l1.rpc.call( + "plugin", + { + "subcommand": "start", + "plugin": str(get_plugin), + }, + ) + l1.daemon.wait_for_log("All NWC's loaded") + await asyncio.sleep(3) + await client.connect() + await fetch_info_event(client, uri) + + with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): + await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice_exceeded["bolt11"]) + ) + + await asyncio.sleep(11) + + list = l1.rpc.call("nip47-list", ["test1"])[0] + assert list["test1"]["budget_msat"] == 8000 + + +@pytest.mark.asyncio +async def test_budget_command(nostr_relay, node_factory, get_plugin): # noqa: F811 + url = nostr_relay + l1, l2 = node_factory.line_graph( + 2, + wait_for_announce=True, + opts=[ + { + "plugin": get_plugin, + "nip47-relays": url, + "broken_log": r"Relay receiver exited with error|Connection failed", + }, + {"log-level": "debug"}, + ], + ) + uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"] + LOGGER.info(uri_str) + invoice = l2.rpc.call( + "invoice", + {"label": generate_random_label(), "description": "test1", "amount_msat": 5000}, + ) + uri = NostrWalletConnectUri.parse(uri_str) + client = Client() + await client.add_relay(RelayUrl.parse(url)) + await client.connect() + await fetch_info_event(client, uri) + nwc = NostrWalletConnect(uri) + balance = await nwc.get_balance() + assert balance.balance == 3000 + + with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): + await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) + ) + + l1.rpc.call("nip47-budget", ["test1", 4000]) + balance = await nwc.get_balance() + assert balance.balance == 4000 + + with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): + await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) + ) + + l1.rpc.call("nip47-budget", ["test1", 10000, "15s"]) + balance = await nwc.get_balance() + assert balance.balance == 10000 + + with pytest.raises( + RpcError, match="`budget_msat` must be greater than 0 if you use `interval`" + ): + l1.rpc.call("nip47-budget", ["test1", 0, "1s"]) + + pay = await nwc.pay_invoice( + PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) + ) + assert pay.preimage is not None + + balance = await nwc.get_balance() + assert balance.balance == 5000 + + get_info = await nwc.get_info() + assert get_info.methods == [ + Method.MAKE_INVOICE(), + Method.LOOKUP_INVOICE(), + Method.LIST_TRANSACTIONS(), + Method.GET_BALANCE(), + Method.GET_INFO(), + Method.PAY_INVOICE(), + Method.PAY_KEYSEND(), + ] + + info_event = await fetch_info_event(client, uri) + + assert ( + info_event.content() + == "make_invoice lookup_invoice list_transactions get_balance get_info pay_invoice pay_keysend notifications" + ) + encryption_tag = next( + tag for tag in info_event.tags() if tag.kind() == "encryption" + ) + assert encryption_tag.content() == "nip44_v2 nip04" + notification_tag = next( + tag for tag in info_event.tags() if tag.kind() == "notifications" + ) + assert notification_tag.content() == "payment_received payment_sent" + + await asyncio.sleep(18) + + balance = await nwc.get_balance() + assert balance.balance == 10000 + + l1.rpc.call("nip47-budget", ["test1", 0]) + balance = await nwc.get_balance() + assert balance.balance == 0 + + get_info = await nwc.get_info() + assert get_info.methods == [ + Method.MAKE_INVOICE(), + Method.LOOKUP_INVOICE(), + Method.LIST_TRANSACTIONS(), + Method.GET_BALANCE(), + Method.GET_INFO(), + ] + + info_event = await fetch_info_event(client, uri) + assert ( + info_event.content() + == "make_invoice lookup_invoice list_transactions get_balance get_info notifications" + ) + encryption_tag = next( + tag for tag in info_event.tags() if tag.kind() == "encryption" + ) + assert encryption_tag.content() == "nip44_v2 nip04" + notification_tag = next( + tag for tag in info_event.tags() if tag.kind() == "notifications" + ) + assert notification_tag.content() == "payment_received payment_sent" + + +@pytest.mark.asyncio +async def test_hold_invoice( + node_factory, + executor, + get_plugin, # noqa: F811 + get_hold, # noqa: F811 + nostr_relay, +): + url = nostr_relay + l1, l2 = node_factory.line_graph( + 2, + wait_for_announce=True, + opts=[ + { + "log-level": "debug", + "may_reconnect": True, + }, + { + "plugin": get_plugin, + "important-plugin": get_hold, + "hold-grpc-port": node_factory.get_unused_port(), + "nip47-relays": url, + "may_reconnect": True, + "broken_log": r"Relay receiver exited with error|Connection failed", + }, + ], + ) + uri_res = l2.rpc.call("nip47-create", ["test1", 3010]) + uri_str = uri_res["uri"] + client_pubkey = PublicKey.parse(uri_res["clientkey_public"]) + LOGGER.info(uri_str) + uri = NostrWalletConnectUri.parse(uri_str) + + nwc = NostrWalletConnect(uri) + + preimage = secrets.token_hex(32) + payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() + LOGGER.info(f"preimage: {preimage}") + LOGGER.info(f"payment_hash: {payment_hash}") + content = { + "method": "make_hold_invoice", + "params": { + "amount": 5000, + "payment_hash": payment_hash, + }, + } + content = json.dumps(content) + keys = Keys(uri.secret()) + encrypted_content = keys.nip04_encrypt(uri.public_key(), content) + event = ( + await EventBuilder(Kind(23194), encrypted_content) + .tags([Tag.public_key(uri.public_key())]) + .finalize_async(keys) + ) + client = Client() + relay_url = RelayUrl.parse(url) + await client.add_relay(relay_url) + await client.connect() + + await fetch_info_event(client, uri) + + (responses1, _res) = await fetch_event_responses( + client, client_pubkey, 23195, client.send_event(event), 1 + ) + error_events = [] + success_events = [] + for event in responses1: + LOGGER.info(event) + content = keys.nip04_decrypt(uri.public_key(), event.content()) + content = json.loads(content) + LOGGER.info(content) + if "result" in content and content["result"] is not None: + success_events.append(content) + if "error" in content and content["error"] is not None: + error_events.append(content) + + assert len(success_events) == 1 + assert len(error_events) == 0 + + assert success_events[0]["result_type"] == "make_hold_invoice" + assert success_events[0]["result"]["payment_hash"] == payment_hash + assert success_events[0]["result"]["type"] == "incoming" + assert success_events[0]["result"]["invoice"] is not None + invoice1 = success_events[0]["result"]["invoice"] + assert "description" not in success_events[0]["result"] + assert "description_hash" not in success_events[0]["result"] + assert success_events[0]["result"]["amount"] == 5000 + invoice1_created_at = pytest.approx(int(time.time()), abs=1) + assert success_events[0]["result"]["created_at"] == invoice1_created_at + invoice1_expires_at = pytest.approx(int(time.time()) + 3600, abs=1) + assert success_events[0]["result"]["expires_at"] == invoice1_expires_at + assert "metadata" not in success_events[0]["result"] + + lookup_hold = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=payment_hash, + invoice=None, + ) + ) + assert lookup_hold.invoice == success_events[0]["result"]["invoice"] + assert lookup_hold.amount == 5000 + assert lookup_hold.description is None + assert lookup_hold.created_at.as_secs() == success_events[0]["result"]["created_at"] + assert lookup_hold.description_hash is None + assert lookup_hold.expires_at.as_secs() == success_events[0]["result"]["expires_at"] + assert lookup_hold.fees_paid == 0 + assert lookup_hold.metadata is None + assert lookup_hold.preimage is None + assert lookup_hold.payment_hash == payment_hash + assert lookup_hold.transaction_type.name == "INCOMING" + assert lookup_hold.state.name == "PENDING" + assert lookup_hold.settled_at is None + + getinfo = l2.rpc.getinfo() + + (responses2, _res) = await fetch_event_responses( + client, + client_pubkey, + 23196, + lambda: executor.submit( + l1.rpc.call, "xpay", [success_events[0]["result"]["invoice"]] + ), + 1, + ) + hold_events = [] + for event in responses2: + LOGGER.info(event) + content = keys.nip04_decrypt(uri.public_key(), event.content()) + content = json.loads(content) + LOGGER.info(content) + if content["notification_type"] == "hold_invoice_accepted": + hold_events.append(content) + assert content["notification"]["payment_hash"] == payment_hash + assert len(hold_events) == 1 + assert ( + hold_events[0]["notification"]["settle_deadline"] > getinfo["blockheight"] + 1 + ) + + lookup_hold = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=None, + invoice=invoice1, + ) + ) + assert lookup_hold.invoice == invoice1 + assert lookup_hold.amount == 5000 + assert lookup_hold.description is None + assert lookup_hold.created_at.as_secs() == invoice1_created_at + assert lookup_hold.description_hash is None + assert lookup_hold.expires_at.as_secs() == invoice1_expires_at + assert lookup_hold.fees_paid == 0 + assert lookup_hold.metadata is None + assert lookup_hold.preimage is None + assert lookup_hold.payment_hash == payment_hash + assert lookup_hold.transaction_type.name == "INCOMING" + assert lookup_hold.state.name == "ACCEPTED" + assert lookup_hold.settled_at is None + + content = { + "method": "settle_hold_invoice", + "params": { + "preimage": preimage, + }, + } + content = json.dumps(content) + encrypted_content = keys.nip04_encrypt(uri.public_key(), content) + event = ( + await EventBuilder(Kind(23194), encrypted_content) + .tags([Tag.public_key(uri.public_key())]) + .finalize_async(keys) + ) + + (responses3, _res) = await fetch_event_responses( + client, client_pubkey, 23195, client.send_event(event), 1 + ) + error_events = [] + success_events = [] + for event in responses3: + LOGGER.info(event) + content = keys.nip04_decrypt(uri.public_key(), event.content()) + content = json.loads(content) + LOGGER.info(content) + if ( + "result" in content + and content["result"] is not None + and content["result_type"] == "settle_hold_invoice" + ): + success_events.append(content) + if "error" in content and content["error"] is not None: + error_events.append(content) + + assert len(success_events) == 1 + assert len(error_events) == 0 + for content in success_events: + assert content["result_type"] == "settle_hold_invoice" + lookup_hold = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=None, + invoice=invoice1, + ) + ) + assert lookup_hold.invoice == invoice1 + assert lookup_hold.amount == 5000 + assert lookup_hold.description is None + assert lookup_hold.created_at.as_secs() == invoice1_created_at + assert lookup_hold.description_hash is None + assert lookup_hold.expires_at.as_secs() == invoice1_expires_at + assert lookup_hold.fees_paid == 0 + assert lookup_hold.metadata is None + assert lookup_hold.preimage == preimage + assert lookup_hold.payment_hash == payment_hash + assert lookup_hold.transaction_type.name == "INCOMING" + assert lookup_hold.state.name == "SETTLED" + assert lookup_hold.settled_at.as_secs() == pytest.approx( + int(time.time()), abs=1 + ) + + wait_for( + lambda: ( + l1.rpc.call("listpays", {"payment_hash": payment_hash})["pays"][0]["status"] + == "complete" + ) + ) + + preimage = secrets.token_hex(32) + payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() + content = { + "method": "make_hold_invoice", + "params": { + "amount": 5000, + "payment_hash": payment_hash, + "description": "cancel_hold", + "expiry": 1000, + "min_cltv_expiry_delta": 200, + }, + } + content = json.dumps(content) + encrypted_content = keys.nip04_encrypt(uri.public_key(), content) + event = ( + await EventBuilder(Kind(23194), encrypted_content) + .tags([Tag.public_key(uri.public_key())]) + .finalize_async(keys) + ) + + (responses4, _res) = await fetch_event_responses( + client, client_pubkey, 23195, client.send_event(event), 1 + ) + error_events = [] + success_events = [] + for event in responses4: + LOGGER.info(event) + content = keys.nip04_decrypt(uri.public_key(), event.content()) + content = json.loads(content) + LOGGER.info(content) + if ( + "result" in content + and content["result"] is not None + and content["result_type"] == "make_hold_invoice" + and content["result"]["payment_hash"] == payment_hash + ): + success_events.append(content) + if "error" in content and content["error"] is not None: + error_events.append(content) + + assert len(success_events) == 1 + assert len(error_events) == 0 + for content in success_events: + assert content["result_type"] == "make_hold_invoice" + assert content["result"]["payment_hash"] == payment_hash + invoice2 = content["result"]["invoice"] + invoice2_created_at = pytest.approx(int(time.time()), abs=1) + invoice2_expires_at = pytest.approx(int(time.time()) + 1000, abs=1) + + (responses5, _res) = await fetch_event_responses( + client, + client_pubkey, + 23196, + lambda: executor.submit( + l1.rpc.call, "xpay", [success_events[0]["result"]["invoice"]] + ), + 1, + ) + hold_events = [] + for event in responses5: + LOGGER.info(event) + content = keys.nip04_decrypt(uri.public_key(), event.content()) + content = json.loads(content) + LOGGER.info(content) + if ( + content["notification_type"] == "hold_invoice_accepted" + and content["notification"]["payment_hash"] == payment_hash + ): + hold_events.append(content) + assert len(hold_events) == 1 + + content = { + "method": "cancel_hold_invoice", + "params": { + "payment_hash": payment_hash, + }, + } + content = json.dumps(content) + encrypted_content = keys.nip04_encrypt(uri.public_key(), content) + event = ( + await EventBuilder(Kind(23194), encrypted_content) + .tags([Tag.public_key(uri.public_key())]) + .finalize_async(keys) + ) + + (responses6, _res) = await fetch_event_responses( + client, + client_pubkey, + 23195, + client.send_event(event), + 1, + ) + error_events = [] + success_events = [] + for event in responses6: + LOGGER.info(event) + content = keys.nip04_decrypt(uri.public_key(), event.content()) + content = json.loads(content) + LOGGER.info(content) + if ( + "result" in content + and content["result"] is not None + and content["result_type"] == "cancel_hold_invoice" + ): + success_events.append(content) + if "error" in content and content["error"] is not None: + error_events.append(content) + + assert len(success_events) == 1 + assert len(error_events) == 0 + for content in success_events: + assert content["result_type"] == "cancel_hold_invoice" + lookup_hold = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=None, + invoice=invoice2, + ) + ) + assert lookup_hold.invoice == invoice2 + assert lookup_hold.amount == 5000 + assert lookup_hold.description == "cancel_hold" + assert lookup_hold.created_at.as_secs() == invoice2_created_at + assert lookup_hold.description_hash is None + assert lookup_hold.expires_at.as_secs() == invoice2_expires_at + assert lookup_hold.fees_paid == 0 + assert lookup_hold.metadata is None + assert lookup_hold.preimage is None + assert lookup_hold.payment_hash == payment_hash + assert lookup_hold.transaction_type.name == "INCOMING" + assert lookup_hold.state.name == "EXPIRED" + assert lookup_hold.settled_at is None + + invoice2_decoded = l1.rpc.call("decode", [invoice2]) + assert invoice2_decoded["min_final_cltv_expiry"] == 200 + + wait_for( + lambda: ( + l1.rpc.call("listpays", {"payment_hash": payment_hash})["pays"][0]["status"] + == "failed" + ) + ) + + nwc = NostrWalletConnect(uri) + + invoice_lookup1 = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=payment_hash, + invoice=None, + ) + ) + invoice_lookup2 = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=None, + invoice=invoice2, + ) + ) + invoice_lookup3 = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=payment_hash, + invoice=invoice2, + ) + ) + assert invoice_lookup1 == invoice_lookup2 + assert invoice_lookup1 == invoice_lookup3 + + invoice_lookup4 = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=None, + invoice=invoice1, + ) + ) + + result = await nwc.list_transactions( + ListTransactionsRequest( + _from=None, + until=None, + limit=None, + offset=None, + unpaid=True, + transaction_type=None, + ) + ) + assert len(result) == 2 + assert result == [invoice_lookup1, invoice_lookup4] or result == [ + invoice_lookup4, + invoice_lookup1, + ] + + description3 = "test3" + description_hash3 = hashlib.sha256(description3.encode()).hexdigest() + preimage = secrets.token_hex(32) + payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() + content = { + "method": "make_hold_invoice", + "params": { + "amount": 5001, + "payment_hash": payment_hash, + "description": description3, + "description_hash": description_hash3, + }, + } + content = json.dumps(content) + encrypted_content = keys.nip04_encrypt(uri.public_key(), content) + event = ( + await EventBuilder(Kind(23194), encrypted_content) + .tags([Tag.public_key(uri.public_key())]) + .finalize_async(keys) + ) + await client.send_event(event) + + start_time = datetime.now() + while (datetime.now() - start_time) < timedelta(seconds=10): + await asyncio.sleep(1) + try: + await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=payment_hash, + invoice=None, + ) + ) + break + except Exception as e: # noqa: BLE001 + LOGGER.error(e) + continue + + lookup_hold = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=payment_hash, + invoice=None, + ) + ) + assert lookup_hold.amount == 5001 + assert lookup_hold.description is None + assert lookup_hold.description_hash == description_hash3 + assert lookup_hold.metadata is None + assert lookup_hold.preimage is None + assert lookup_hold.payment_hash == payment_hash + assert lookup_hold.transaction_type.name == "INCOMING" + assert lookup_hold.state.name == "PENDING" + assert lookup_hold.settled_at is None + + description4 = "test4" + description_hash4 = hashlib.sha256(description4.encode()).hexdigest() + preimage = secrets.token_hex(32) + payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() + content = { + "method": "make_hold_invoice", + "params": { + "amount": 5002, + "payment_hash": payment_hash, + "description_hash": description_hash4, + }, + } + content = json.dumps(content) + encrypted_content = keys.nip04_encrypt(uri.public_key(), content) + event = ( + await EventBuilder(Kind(23194), encrypted_content) + .tags([Tag.public_key(uri.public_key())]) + .finalize_async(keys) + ) + await client.send_event(event) + + start_time = datetime.now() + while (datetime.now() - start_time) < timedelta(seconds=10): + await asyncio.sleep(1) + try: + await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=payment_hash, + invoice=None, + ) + ) + break + except Exception as e: # noqa: BLE001 + LOGGER.error(e) + continue + + lookup_hold = await nwc.lookup_invoice( + LookupInvoiceRequest( + payment_hash=payment_hash, + invoice=None, + ) + ) + assert lookup_hold.amount == 5002 + assert lookup_hold.description is None + assert lookup_hold.description_hash == description_hash4 + assert lookup_hold.metadata is None + assert lookup_hold.preimage is None + assert lookup_hold.payment_hash == payment_hash + assert lookup_hold.transaction_type.name == "INCOMING" + assert lookup_hold.state.name == "PENDING" + assert lookup_hold.settled_at is None + + info_event = await fetch_info_event(client, uri) + assert ( + info_event.content() + == "make_invoice lookup_invoice list_transactions get_balance get_info pay_invoice pay_keysend make_hold_invoice cancel_hold_invoice settle_hold_invoice notifications" + ) + encryption_tag = next( + tag for tag in info_event.tags() if tag.kind() == "encryption" + ) + assert encryption_tag.content() == "nip44_v2 nip04" + notification_tag = next( + tag for tag in info_event.tags() if tag.kind() == "notifications" + ) + assert ( + notification_tag.content() + == "payment_received payment_sent hold_invoice_accepted" + ) + + l2.restart() + + l1.rpc.connect(l2.info["id"], "localhost", l2.port) + + (responses7, _res) = await fetch_event_responses( + client, + client_pubkey, + 23196, + lambda: executor.submit(l1.rpc.call, "xpay", [lookup_hold.invoice]), + 1, + ) + hold_events = [] + for event in responses7: + LOGGER.info(event) + content = keys.nip04_decrypt(uri.public_key(), event.content()) + content = json.loads(content) + LOGGER.info(content) + if ( + content["notification_type"] == "hold_invoice_accepted" + and content["notification"]["payment_hash"] == payment_hash + ): + hold_events.append(content) + assert len(hold_events) == 1 + + +@pytest.mark.asyncio +async def test_hold_invoice_expiry( + node_factory, + get_plugin, # noqa: F811 + get_hold, # noqa: F811 + nostr_relay, +): + url = nostr_relay + l2 = node_factory.get_node( + options={ + "plugin": get_plugin, + "important-plugin": get_hold, + "hold-grpc-port": node_factory.get_unused_port(), + "nip47-relays": url, + }, + broken_log=r"Relay receiver exited with error|Connection failed", + ) + uri_res = l2.rpc.call("nip47-create", ["test1", 3010]) + uri_str = uri_res["uri"] + client_pubkey = PublicKey.parse(uri_res["clientkey_public"]) + LOGGER.info(uri_str) + uri = NostrWalletConnectUri.parse(uri_str) + + preimage = secrets.token_hex(32) + payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest() + LOGGER.info(f"preimage: {preimage}") + LOGGER.info(f"payment_hash: {payment_hash}") + content = { + "method": "make_hold_invoice", + "params": { + "amount": 5000, + "payment_hash": payment_hash, + "expiry": 5, + }, + } + content = json.dumps(content) + keys = Keys(uri.secret()) + encrypted_content = keys.nip04_encrypt(uri.public_key(), content) + event = ( + await EventBuilder(Kind(23194), encrypted_content) + .tags([Tag.public_key(uri.public_key())]) + .finalize_async(keys) + ) + client = Client() + relay_url = RelayUrl.parse(url) + await client.add_relay(relay_url) + await client.connect() + + await fetch_info_event(client, uri) + + (responses1, _res) = await fetch_event_responses( + client, client_pubkey, 23195, client.send_event(event), 1 + ) + error_events = [] + success_events = [] + for event in responses1: + LOGGER.info(event) + content = keys.nip04_decrypt(uri.public_key(), event.content()) + content = json.loads(content) + LOGGER.info(content) + if "result" in content and content["result"] is not None: + success_events.append(content) + if "error" in content and content["error"] is not None: + error_events.append(content) + + assert len(success_events) == 1 + assert len(error_events) == 0 + + # The hold plugin has no expired state: an invoice that expires without + # ever being accepted (and without being cancelled) stays `Unpaid` and the + # track stream never ends on its own. The handler must give up at the + # invoice's expiry instead of hanging, and must not send a spurious + # accepted notification. + l2.daemon.wait_for_log("was not accepted, skipping notification", timeout=20) diff --git a/tests/test_clnnwc.py b/tests/test_clnnwc.py deleted file mode 100644 index 2e6d015..0000000 --- a/tests/test_clnnwc.py +++ /dev/null @@ -1,1352 +0,0 @@ -#!/usr/bin/python - -import hashlib -import importlib.resources as pkg_resources -import json -import logging -import socket -import subprocess -import sys -import time -from datetime import datetime, timedelta -from pathlib import Path -from threading import Thread - -import pytest -import pytest_asyncio -import yaml -from pyln.testing.fixtures import * # noqa: F403 -from pyln.testing.utils import RpcError, wait_for -from util import generate_random_label, get_plugin # noqa: F401 - -if sys.version_info >= (3, 9): - from nostr_sdk import ( - Alphabet, - Client, - EventBuilder, - Filter, - Keys, - KeysendTlvRecord, - Kind, - ListTransactionsRequest, - LookupInvoiceRequest, - MakeInvoiceRequest, - NostrSdkError, - NostrSigner, - NostrWalletConnectUri, - Nwc, - PayInvoiceRequest, - PayKeysendRequest, - SingleLetterTag, - Tag, - TagKind, - ) - -LOGGER = logging.getLogger(__name__) - - -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher") -@pytest.mark.asyncio -async def test_get_balance(node_factory, get_plugin, nostr_client): # noqa: F811 - nostr_client, relay_port = nostr_client - url = f"127.0.0.1:{relay_port}" - l1, l2 = node_factory.line_graph( - 2, - wait_for_announce=True, - opts=[ - { - "log-level": "debug", - "plugin": get_plugin, - "nip47-relays": f"ws://{url}", - }, - {"log-level": "debug"}, - ], - ) - node_balance = l1.rpc.call("listpeerchannels", {})["channels"][0]["spendable_msat"] - uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"] - LOGGER.info(uri_str) - nwc = Nwc(NostrWalletConnectUri.parse(uri_str)) - balance = await nwc.get_balance() - assert balance == 3000 - - uri_str = l1.rpc.call("nip47-create", ["test2"])["uri"] - LOGGER.info(uri_str) - nwc = Nwc(NostrWalletConnectUri.parse(uri_str)) - balance = await nwc.get_balance() - assert balance == node_balance - - uri_str = l1.rpc.call("nip47-create", ["test3", 0])["uri"] - LOGGER.info(uri_str) - nwc = Nwc(NostrWalletConnectUri.parse(uri_str)) - balance = await nwc.get_balance() - assert balance == 0 - - with pytest.raises(RpcError, match="not an integer"): - uri_str = l1.rpc.call("nip47-create", ["test3", -1])["uri"] - - -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher") -@pytest.mark.asyncio -async def test_get_info(node_factory, get_plugin, nostr_client): # noqa: F811 - nostr_client, relay_port = nostr_client - url = f"127.0.0.1:{relay_port}" - l1 = node_factory.get_node( - options={ - "log-level": "debug", - "plugin": get_plugin, - "nip47-relays": f"ws://{url}", - }, - ) - node_get_info = l1.rpc.call("getinfo", {}) - uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"] - LOGGER.info(uri_str) - uri = NostrWalletConnectUri.parse(uri_str) - nwc = Nwc(uri) - get_info = await nwc.get_info() - assert get_info.alias == node_get_info["alias"] - assert get_info.block_height == node_get_info["blockheight"] - assert get_info.color == node_get_info["color"] - assert get_info.methods == [ - "pay_invoice", - "multi_pay_invoice", - "pay_keysend", - "multi_pay_keysend", - "make_invoice", - "lookup_invoice", - "list_transactions", - "get_balance", - "get_info", - ] - assert get_info.network == "regtest" - assert get_info.notifications == ["payment_received", "payment_sent"] - assert get_info.pubkey == node_get_info["id"] - - l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"}) - l1.rpc.call( - "plugin", - { - "subcommand": "start", - "plugin": str(get_plugin), - "nip47-notifications": False, - }, - ) - l1.daemon.wait_for_log("All NWC's loaded") - get_info = await nwc.get_info() - assert get_info.alias == node_get_info["alias"] - assert get_info.block_height == node_get_info["blockheight"] - assert get_info.color == node_get_info["color"] - assert get_info.methods == [ - "pay_invoice", - "multi_pay_invoice", - "pay_keysend", - "multi_pay_keysend", - "make_invoice", - "lookup_invoice", - "list_transactions", - "get_balance", - "get_info", - ] - assert get_info.network == "regtest" - assert get_info.notifications == [] - assert get_info.pubkey == node_get_info["id"] - - signer = NostrSigner.keys(Keys(uri.secret())) - client = Client(signer) - await client.add_relay(f"ws://{url}") - await client.connect() - - response_filter = Filter().kind(Kind(13194)).author(uri.public_key()) - events = await client.fetch_events(response_filter, timeout=timedelta(seconds=10)) - start_time = datetime.now() - while events.len() < 1 and (datetime.now() - start_time) < timedelta(seconds=10): - time.sleep(1) - events = await client.fetch_events( - response_filter, timeout=timedelta(seconds=1) - ) - assert events.len() == 1 - assert ( - events.to_vec()[0].content() - == "pay_invoice multi_pay_invoice pay_keysend multi_pay_keysend make_invoice lookup_invoice list_transactions get_balance get_info" - ) - - uri_str = l1.rpc.call("nip47-create", ["test2", 0])["uri"] - LOGGER.info(uri_str) - uri = NostrWalletConnectUri.parse(uri_str) - nwc = Nwc(uri) - get_info = await nwc.get_info() - assert get_info.methods == [ - "make_invoice", - "lookup_invoice", - "list_transactions", - "get_balance", - "get_info", - ] - - signer = NostrSigner.keys(Keys(uri.secret())) - client = Client(signer) - await client.add_relay(f"ws://{url}") - await client.connect() - - response_filter = Filter().kind(Kind(13194)).author(uri.public_key()) - events = await client.fetch_events(response_filter, timeout=timedelta(seconds=10)) - start_time = datetime.now() - while events.len() < 1 and (datetime.now() - start_time) < timedelta(seconds=10): - time.sleep(1) - events = await client.fetch_events( - response_filter, timeout=timedelta(seconds=1) - ) - assert events.len() == 1 - assert ( - events.to_vec()[0].content() - == "make_invoice lookup_invoice list_transactions get_balance get_info" - ) - - -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher") -@pytest.mark.asyncio -async def test_make_invoice(node_factory, get_plugin, nostr_client): # noqa: F811 - nostr_client, relay_port = nostr_client - url = f"127.0.0.1:{relay_port}" - l1 = node_factory.get_node( - options={ - "log-level": "debug", - "plugin": get_plugin, - "nip47-relays": f"ws://{url}", - }, - ) - uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"] - LOGGER.info(uri_str) - nwc = Nwc(NostrWalletConnectUri.parse(uri_str)) - invoice = await nwc.make_invoice( - MakeInvoiceRequest( - amount=3000, description="test1", description_hash=None, expiry=None - ) - ) - node_invoice = l1.rpc.call("decode", [invoice.invoice]) - assert invoice.payment_hash == node_invoice["payment_hash"] - assert node_invoice["amount_msat"] == 3000 - assert node_invoice["expiry"] == 604800 - assert node_invoice["description"] == "test1" - assert "description_hash" not in node_invoice - - invoice = await nwc.make_invoice( - MakeInvoiceRequest( - amount=3001, - description="test2", - description_hash=hashlib.sha256("test2".encode()).hexdigest(), - expiry=120, - ) - ) - node_invoice = l1.rpc.call("listinvoices", {"invstring": invoice.invoice})[ - "invoices" - ][0] - node_invoice_decode = l1.rpc.call("decode", [invoice.invoice]) - assert invoice.payment_hash == node_invoice["payment_hash"] - assert node_invoice["amount_msat"] == 3001 - assert node_invoice_decode["expiry"] == 120 - assert node_invoice["description"] == "test2" - assert ( - node_invoice_decode["description_hash"] - == hashlib.sha256("test2".encode()).hexdigest() - ) - with pytest.raises( - NostrSdkError.Generic, match="Must have description when using description_hash" - ): - await nwc.make_invoice( - MakeInvoiceRequest( - amount=3001, - description=None, - description_hash=hashlib.sha256("test2".encode()).hexdigest(), - expiry=120, - ) - ) - with pytest.raises( - NostrSdkError.Generic, match="description_hash not matching description" - ): - await nwc.make_invoice( - MakeInvoiceRequest( - amount=3001, - description="test1", - description_hash=hashlib.sha256("test2".encode()).hexdigest(), - expiry=120, - ) - ) - - -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher") -@pytest.mark.asyncio -async def test_pay_keysend(node_factory, get_plugin, nostr_client): # noqa: F811 - nostr_client, relay_port = nostr_client - url = f"127.0.0.1:{relay_port}" - l1, l2 = node_factory.line_graph( - 2, - wait_for_announce=True, - opts=[ - { - "log-level": "debug", - "plugin": get_plugin, - "nip47-relays": f"ws://{url}", - }, - {"log-level": "debug"}, - ], - ) - uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"] - LOGGER.info(uri_str) - nwc = Nwc(NostrWalletConnectUri.parse(uri_str)) - result = await nwc.pay_keysend( - PayKeysendRequest( - id="id123", amount=1000, pubkey=l2.info["id"], preimage=None, tlv_records=[] - ) - ) - pay = l1.rpc.call("listpays", {})["pays"][0] - assert result.preimage == pay["preimage"] - - with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): - await nwc.pay_keysend( - PayKeysendRequest( - id="id123", - amount=2001, - pubkey=l2.info["id"], - preimage=None, - tlv_records=[KeysendTlvRecord(tlv_type=1234, value="a5c7e3d9b")], - ) - ) - with pytest.raises( - NostrSdkError.Generic, match="CLN generates the preimage itself" - ): - await nwc.pay_keysend( - PayKeysendRequest( - id="id123", - amount=2001, - pubkey=l2.info["id"], - preimage="or3ijro3ijroi", - tlv_records=[KeysendTlvRecord(tlv_type=1234, value="a5c7e3d9b")], - ) - ) - - -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher") -@pytest.mark.asyncio -async def test_multi_keysend(node_factory, get_plugin, nostr_client): # noqa: F811 - nostr_client, relay_port = nostr_client - url = f"127.0.0.1:{relay_port}" - l1, l2, l3 = node_factory.line_graph( - 3, - wait_for_announce=True, - opts=[ - { - "log-level": "debug", - "plugin": get_plugin, - "nip47-relays": f"ws://{url}", - }, - {"log-level": "debug"}, - {"log-level": "debug"}, - ], - ) - uri_str = l1.rpc.call("nip47-create", ["test1", 3010])["uri"] - LOGGER.info(uri_str) - uri = NostrWalletConnectUri.parse(uri_str) - content = { - "method": "multi_pay_keysend", - "params": { - "keysends": [ - {"id": "4da52c32a1", "pubkey": l2.info["id"], "amount": 1000}, - {"id": "3da52c32a1", "pubkey": l3.info["id"], "amount": 2000}, - ], - }, - } - content = json.dumps(content) - signer = NostrSigner.keys(Keys(uri.secret())) - encrypted_content = await signer.nip04_encrypt(uri.public_key(), content) - event = ( - await EventBuilder(Kind(23194), encrypted_content) - .tags([Tag.public_key(uri.public_key())]) - .sign(signer) - ) - client = Client(signer) - await client.add_relay(f"ws://{url}") - await client.connect() - await client.send_event(event) - - content = { - "method": "multi_pay_keysend", - "params": { - "keysends": [ - {"id": "4da52c32a1", "pubkey": l2.info["id"], "amount": 5}, - {"id": "3da52c32a1", "pubkey": l3.info["id"], "amount": 5}, - ], - }, - } - content = json.dumps(content) - signer = NostrSigner.keys(Keys(uri.secret())) - encrypted_content = await signer.nip04_encrypt(uri.public_key(), content) - event = ( - await EventBuilder(Kind(23194), encrypted_content) - .tags([Tag.public_key(uri.public_key())]) - .sign(signer) - ) - client = Client(signer) - await client.add_relay(f"ws://{url}") - await client.connect() - await client.send_event(event) - - response_filter = Filter().kind(Kind(23195)).author(uri.public_key()) - events = await client.fetch_events(response_filter, timeout=timedelta(seconds=10)) - start_time = datetime.now() - while events.len() < 4 and (datetime.now() - start_time) < timedelta(seconds=10): - time.sleep(1) - events = await client.fetch_events( - response_filter, timeout=timedelta(seconds=1) - ) - assert events.len() == 4 - error_events = [] - success_events = [] - for event in events.to_vec(): - LOGGER.info(event) - assert event.tags().find( - TagKind.SINGLE_LETTER(SingleLetterTag.lowercase(Alphabet.D)) - ) - content = await signer.nip04_decrypt(uri.public_key(), event.content()) - content = json.loads(content) - if "result" in content and content["result"] is not None: - success_events.append(content) - if "error" in content and content["error"] is not None: - error_events.append(content) - - assert len(success_events) == 3 - assert len(error_events) == 1 - for content in success_events: - assert content["result_type"] == "multi_pay_keysend" - assert content["result"]["preimage"] is not None - for content in error_events: - assert content["result_type"] == "multi_pay_keysend" - assert content["error"]["message"] == "Payment exceeds budget!" - assert content["error"]["code"] == "QUOTA_EXCEEDED" - - -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher") -@pytest.mark.asyncio -async def test_lookup_invoice(node_factory, get_plugin, nostr_client): # noqa: F811 - nostr_client, relay_port = nostr_client - url = f"127.0.0.1:{relay_port}" - l1, l2, l3 = node_factory.line_graph( - 3, - wait_for_announce=True, - opts=[ - { - "log-level": "debug", - "plugin": get_plugin, - "nip47-relays": f"ws://{url}", - }, - {"log-level": "debug"}, - {"log-level": "debug"}, - ], - ) - l1.rpc.call( - "pay", - { - "bolt11": l2.rpc.call( - "invoice", - { - "amount_msat": 500000000, - "label": generate_random_label(), - "description": "balancechannel", - }, - )["bolt11"] - }, - ) - wait_for( - lambda: l2.rpc.call("listpeerchannels", [l1.info["id"]])["channels"][0][ - "spendable_msat" - ] - > 3001 - ) - uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"] - LOGGER.info(uri_str) - nwc = Nwc(NostrWalletConnectUri.parse(uri_str)) - invoice = await nwc.make_invoice( - MakeInvoiceRequest( - amount=3000, description="test1", description_hash=None, expiry=None - ) - ) - - with pytest.raises( - NostrSdkError.Generic, match="Neither invoice nor payment_hash given" - ): - await nwc.lookup_invoice( - LookupInvoiceRequest( - payment_hash=None, - invoice=None, - ) - ) - - listpays_rpc = l1.rpc.call("listinvoices", {"invstring": invoice.invoice})[ - "invoices" - ][0] - invoice_decode = l1.rpc.call("decode", [invoice.invoice]) - - invoice_lookup = await nwc.lookup_invoice( - LookupInvoiceRequest( - payment_hash=invoice.payment_hash, - invoice=None, - ) - ) - assert invoice_lookup.invoice == invoice.invoice - assert invoice_lookup.amount == 3000 - assert invoice_lookup.description == "test1" - assert invoice_lookup.created_at.as_secs() == pytest.approx( - invoice_decode["created_at"], abs=1 - ) - assert invoice_lookup.description_hash is None - assert invoice_lookup.expires_at.as_secs() == pytest.approx( - listpays_rpc["expires_at"], abs=1 - ) - assert invoice_lookup.fees_paid == 0 - assert invoice_lookup.metadata is None - assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"] - assert invoice_lookup.transaction_type.name == "INCOMING" - assert invoice_lookup.settled_at is None - - invoice_lookup = await nwc.lookup_invoice( - LookupInvoiceRequest( - payment_hash=None, - invoice=invoice.invoice, - ) - ) - assert invoice_lookup.invoice == invoice.invoice - assert invoice_lookup.amount == 3000 - assert invoice_lookup.description == "test1" - assert invoice_lookup.created_at.as_secs() == pytest.approx( - invoice_decode["created_at"], abs=1 - ) - assert invoice_lookup.description_hash is None - assert invoice_lookup.expires_at.as_secs() == pytest.approx( - listpays_rpc["expires_at"], abs=1 - ) - assert invoice_lookup.fees_paid == 0 - assert invoice_lookup.metadata is None - assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"] - assert invoice_lookup.transaction_type.name == "INCOMING" - assert invoice_lookup.settled_at is None - - invoice = await nwc.make_invoice( - MakeInvoiceRequest( - amount=3001, - description="test2", - description_hash=hashlib.sha256("test2".encode()).hexdigest(), - expiry=1000, - ) - ) - - listpays_rpc = l1.rpc.call("listinvoices", {"invstring": invoice.invoice})[ - "invoices" - ][0] - invoice_decode = l1.rpc.call("decode", [invoice.invoice]) - - invoice_lookup = await nwc.lookup_invoice( - LookupInvoiceRequest( - payment_hash=invoice.payment_hash, - invoice=None, - ) - ) - assert invoice_lookup.invoice == invoice.invoice - assert invoice_lookup.amount == 3001 - assert invoice_lookup.description is None - assert invoice_lookup.created_at.as_secs() == pytest.approx( - invoice_decode["created_at"], abs=1 - ) - assert ( - invoice_lookup.description_hash == hashlib.sha256("test2".encode()).hexdigest() - ) - assert invoice_lookup.expires_at.as_secs() == pytest.approx( - listpays_rpc["expires_at"], abs=1 - ) - assert invoice_lookup.fees_paid == 0 - assert invoice_lookup.metadata is None - assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"] - assert invoice_lookup.transaction_type.name == "INCOMING" - assert invoice_lookup.settled_at is None - - l2.rpc.call("pay", {"bolt11": invoice.invoice}) - listpays_rpc = l1.rpc.call("listinvoices", {"invstring": invoice.invoice})[ - "invoices" - ][0] - invoice_lookup = await nwc.lookup_invoice( - LookupInvoiceRequest( - payment_hash=invoice.payment_hash, - invoice=None, - ) - ) - assert invoice_lookup.invoice == invoice.invoice - assert invoice_lookup.amount == 3001 - assert invoice_lookup.description is None - assert invoice_lookup.created_at.as_secs() == pytest.approx( - invoice_decode["created_at"], abs=1 - ) - assert ( - invoice_lookup.description_hash == hashlib.sha256("test2".encode()).hexdigest() - ) - assert invoice_lookup.expires_at.as_secs() == pytest.approx( - listpays_rpc["expires_at"], abs=1 - ) - assert invoice_lookup.fees_paid == 0 - assert invoice_lookup.metadata is None - assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"] - assert invoice_lookup.transaction_type.name == "INCOMING" - assert invoice_lookup.settled_at.as_secs() == pytest.approx( - listpays_rpc["paid_at"], abs=1 - ) - - invoice = l3.rpc.call( - "invoice", - { - "amount_msat": 4000, - "label": generate_random_label(), - "description": "outgoing", - }, - ) - invoice_decode = l3.rpc.call("decode", [invoice["bolt11"]]) - pay = l1.rpc.call("pay", {"bolt11": invoice["bolt11"]}) - listpays_rpc = l1.rpc.call("listpays", {"bolt11": invoice["bolt11"]})["pays"][0] - invoice_lookup = await nwc.lookup_invoice( - LookupInvoiceRequest( - payment_hash=pay["payment_hash"], - invoice=None, - ) - ) - assert invoice_lookup.invoice == invoice["bolt11"] - assert invoice_lookup.amount == 4000 - assert invoice_lookup.description == "outgoing" - assert invoice_lookup.created_at.as_secs() == pytest.approx( - invoice_decode["created_at"], abs=1 - ) - assert invoice_lookup.description_hash is None - assert invoice_lookup.expires_at is None - assert invoice_lookup.fees_paid == 1 - assert invoice_lookup.metadata is None - assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"] - assert invoice_lookup.transaction_type.name == "OUTGOING" - assert invoice_lookup.settled_at.as_secs() == pytest.approx( - listpays_rpc["completed_at"], abs=1 - ) - - -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher") -@pytest.mark.asyncio -async def test_list_transactions(node_factory, get_plugin, nostr_client): # noqa: F811 - nostr_client, relay_port = nostr_client - url = f"127.0.0.1:{relay_port}" - l1, l2 = node_factory.line_graph( - 2, - wait_for_announce=True, - opts=[ - { - "log-level": "debug", - "plugin": get_plugin, - "nip47-relays": f"ws://{url}", - }, - {"log-level": "debug"}, - ], - ) - l1.rpc.call( - "pay", - { - "bolt11": l2.rpc.call( - "invoice", - { - "amount_msat": 500000000, - "label": generate_random_label(), - "description": "balancechannel", - }, - )["bolt11"] - }, - ) - wait_for( - lambda: l2.rpc.call("listpeerchannels", [l1.info["id"]])["channels"][0][ - "spendable_msat" - ] - > 30001 - ) - uri_str = l1.rpc.call("nip47-create", ["test1"])["uri"] - LOGGER.info(uri_str) - nwc = Nwc(NostrWalletConnectUri.parse(uri_str)) - for i in range(10): - invoice = l2.rpc.call( - "invoice", - { - "label": generate_random_label(), - "description": "test1", - "amount_msat": 3000, - }, - ) - result = await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) - ) - assert result.preimage is not None - for i in range(10): - invoice = await nwc.make_invoice( - MakeInvoiceRequest( - amount=3000, description="test2", description_hash=None, expiry=None - ) - ) - result = l2.rpc.call("pay", [invoice.invoice]) - result = await nwc.list_transactions( - ListTransactionsRequest( - _from=None, - until=None, - limit=None, - offset=None, - unpaid=None, - transaction_type=None, - ) - ) - assert len(result) == 21 - for tx in result: - tx.description is not None - tx.invoice is not None - tx.amount is not None - tx.created_at is not None - tx.description_hash is None - tx.expires_at is None - tx.preimage is not None - tx.settled_at is not None - tx.metadata is None - tx.transaction_type is not None - tx.payment_hash is not None - tx.fees_paid is not None - - -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher") -@pytest.mark.asyncio -async def test_notifications(node_factory, get_plugin, nostr_client): # noqa: F811 - nostr_client, relay_port = nostr_client - url = f"127.0.0.1:{relay_port}" - l1, l2, l3 = node_factory.line_graph( - 3, - wait_for_announce=True, - opts=[ - { - "log-level": "debug", - "plugin": get_plugin, - "nip47-relays": f"ws://{url}", - }, - {"log-level": "debug"}, - {"log-level": "debug"}, - ], - ) - uri_str = l1.rpc.call("nip47-create", ["test1"])["uri"] - LOGGER.info(uri_str) - - uri = NostrWalletConnectUri.parse(uri_str) - nwc = Nwc(uri) - - invoice = l3.rpc.call( - "invoice", - { - "label": generate_random_label(), - "description": "test1", - "amount_msat": 500000000, - }, - ) - pay1 = await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) - ) - invoice1_rpc = l3.rpc.call("listinvoices", {"invstring": invoice["bolt11"]})[ - "invoices" - ][0] - invoice1_decode = l3.rpc.call("decode", [invoice["bolt11"]]) - pay1_list = l1.rpc.call("listpays", {"bolt11": invoice["bolt11"]})["pays"][0] - - wait_for( - lambda: l2.rpc.call("listpeerchannels", [l1.info["id"]])["channels"][0][ - "spendable_msat" - ] - > 3000 - ) - wait_for( - lambda: l3.rpc.call("listpeerchannels", [l2.info["id"]])["channels"][0][ - "spendable_msat" - ] - > 3000 - ) - - result = await nwc.make_invoice( - MakeInvoiceRequest( - amount=3000, description="test2", description_hash=None, expiry=None - ) - ) - pay2 = l3.rpc.call("pay", {"bolt11": result.invoice}) - invoice2_list = l1.rpc.call("listinvoices", {"invstring": result.invoice})[ - "invoices" - ][0] - invoice2_decode = l3.rpc.call("decode", [result.invoice]) - - response_filter = Filter().kind(Kind(23196)).author(uri.public_key()) - events = await nostr_client.fetch_events( - response_filter, timeout=timedelta(seconds=10) - ) - start_time = datetime.now() - while events.len() < 2 and (datetime.now() - start_time) < timedelta(seconds=10): - time.sleep(1) - events = await nostr_client.fetch_events( - response_filter, timeout=timedelta(seconds=1) - ) - assert events.len() == 2 - signer = NostrSigner.keys(Keys(uri.secret())) - received_events = [] - sent_events = [] - for event in events.to_vec(): - LOGGER.info(event) - content = await signer.nip04_decrypt(uri.public_key(), event.content()) - content = json.loads(content) - if content["notification_type"] == "payment_received": - received_events.append(content) - if content["notification_type"] == "payment_sent": - sent_events.append(content) - assert content["notification"]["preimage"] is not None - assert len(received_events) == 1 - assert len(sent_events) == 1 - assert received_events[0]["notification"]["type"] == "incoming" - assert received_events[0]["notification"]["invoice"] == result.invoice - assert received_events[0]["notification"]["description"] == "test2" - assert "description_hash" not in received_events[0]["notification"] - assert received_events[0]["notification"]["preimage"] == pay2["payment_preimage"] - assert received_events[0]["notification"]["payment_hash"] == pay2["payment_hash"] - assert received_events[0]["notification"]["amount"] == 3000 - assert received_events[0]["notification"]["fees_paid"] == 0 - assert received_events[0]["notification"]["created_at"] == pytest.approx( - invoice2_decode["created_at"], abs=1 - ) - assert "expires_at" not in received_events[0]["notification"] - assert received_events[0]["notification"]["settled_at"] == pytest.approx( - invoice2_list["paid_at"], abs=1 - ) - assert "metadata" not in received_events[0]["notification"] - - assert sent_events[0]["notification"]["type"] == "outgoing" - assert sent_events[0]["notification"]["invoice"] == invoice["bolt11"] - assert sent_events[0]["notification"]["description"] == "test1" - assert "description_hash" not in sent_events[0]["notification"] - assert sent_events[0]["notification"]["preimage"] == pay1.preimage - assert ( - sent_events[0]["notification"]["payment_hash"] == invoice1_rpc["payment_hash"] - ) - assert sent_events[0]["notification"]["amount"] == 500000000 - assert sent_events[0]["notification"]["fees_paid"] == 5001 - assert sent_events[0]["notification"]["created_at"] == pytest.approx( - invoice1_decode["created_at"], abs=1 - ) - assert "expires_at" not in sent_events[0]["notification"] - assert sent_events[0]["notification"]["settled_at"] == pytest.approx( - pay1_list["completed_at"], abs=1 - ) - assert "metadata" not in sent_events[0]["notification"] - - l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"}) - l1.rpc.call( - "plugin", - { - "subcommand": "start", - "plugin": str(get_plugin), - "nip47-notifications": False, - }, - ) - l1.daemon.wait_for_log("All NWC's loaded") - - invoice = l3.rpc.call( - "invoice", - { - "label": generate_random_label(), - "description": "test3", - "amount_msat": 500, - }, - ) - pay1 = await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) - ) - events = await nostr_client.fetch_events( - response_filter, timeout=timedelta(seconds=10) - ) - start_time = datetime.now() - while (datetime.now() - start_time) < timedelta(seconds=10): - time.sleep(1) - events = await nostr_client.fetch_events( - response_filter, timeout=timedelta(seconds=1) - ) - assert events.len() == 2 - - -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher") -@pytest.mark.asyncio -async def test_pay_invoice(node_factory, get_plugin, nostr_client): # noqa: F811 - nostr_client, relay_port = nostr_client - url = f"127.0.0.1:{relay_port}" - l1, l2 = node_factory.line_graph( - 2, - wait_for_announce=True, - opts=[ - { - "log-level": "debug", - "plugin": get_plugin, - "nip47-relays": f"ws://{url}", - }, - {"log-level": "debug"}, - ], - ) - uri_str = l1.rpc.call("nip47-create", ["test1", 3001])["uri"] - LOGGER.info(uri_str) - invoice = l2.rpc.call( - "invoice", - {"label": generate_random_label(), "description": "test1", "amount_msat": 3000}, - ) - nwc = Nwc(NostrWalletConnectUri.parse(uri_str)) - result = await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) - ) - pay = l1.rpc.call("listpays", {"payment_hash": invoice["payment_hash"]})["pays"][0] - assert result.preimage == pay["preimage"] - - invoice = l2.rpc.call( - "invoice", - {"label": generate_random_label(), "description": "test2", "amount_msat": 1}, - ) - with pytest.raises(NostrSdkError.Generic, match="unnecessary"): - await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=1, invoice=invoice["bolt11"]) - ) - invoice = l2.rpc.call( - "invoice", - {"label": generate_random_label(), "description": "test3", "amount_msat": 2}, - ) - with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): - await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) - ) - - -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher") -@pytest.mark.asyncio -async def test_multi_pay(node_factory, get_plugin, nostr_client): # noqa: F811 - nostr_client, relay_port = nostr_client - url = f"127.0.0.1:{relay_port}" - l1, l2 = node_factory.line_graph( - 2, - wait_for_announce=True, - opts=[ - { - "log-level": "debug", - "plugin": get_plugin, - "nip47-relays": f"ws://{url}", - }, - {"log-level": "debug"}, - ], - ) - uri_str = l1.rpc.call("nip47-create", ["test1", 30000])["uri"] - LOGGER.info(uri_str) - uri = NostrWalletConnectUri.parse(uri_str) - invoice1 = l2.rpc.call( - "invoice", - {"label": generate_random_label(), "description": "test1", "amount_msat": 3000}, - ) - invoice2 = l2.rpc.call( - "invoice", - {"label": generate_random_label(), "description": "test2", "amount_msat": 4000}, - ) - invoice3 = l2.rpc.call( - "invoice", - { - "label": generate_random_label(), - "description": "test3", - "amount_msat": 23001, - }, - ) - content = { - "method": "multi_pay_invoice", - "params": { - "invoices": [ - {"id": "4da52c32a1", "invoice": invoice1["bolt11"]}, - {"id": "3da52c32a1", "invoice": invoice2["bolt11"]}, - {"id": "af3g2k2o11", "invoice": invoice3["bolt11"]}, - ], - }, - } - content = json.dumps(content) - signer = NostrSigner.keys(Keys(uri.secret())) - encrypted_content = await signer.nip44_encrypt(uri.public_key(), content) - event = ( - await EventBuilder(Kind(23194), encrypted_content) - .tags([Tag.public_key(uri.public_key())]) - .sign(signer) - ) - client = Client(signer) - await client.add_relay(f"ws://{url}") - await client.connect() - await client.send_event(event) - - response_filter = Filter().kind(Kind(23195)).author(uri.public_key()) - events = await client.fetch_events(response_filter, timeout=timedelta(seconds=10)) - start_time = datetime.now() - while events.len() < 3 and (datetime.now() - start_time) < timedelta(seconds=10): - time.sleep(1) - events = await client.fetch_events( - response_filter, timeout=timedelta(seconds=1) - ) - assert events.len() == 3 - success_pays = [] - error_pays = [] - for event in events.to_vec(): - LOGGER.info(event) - d_tag = event.tags().find( - TagKind.SINGLE_LETTER(SingleLetterTag.lowercase(Alphabet.D)) - ) - content = await signer.nip44_decrypt(uri.public_key(), event.content()) - content = json.loads(content) - assert content["result_type"] == "multi_pay_invoice" - if "result" in content and content["result"] is not None: - assert d_tag is not None - assert content["result"]["preimage"] is not None - success_pays.append(content) - if "error" in content and content["error"] is not None: - assert d_tag.content() == "af3g2k2o11" - assert content["error"]["code"] == "QUOTA_EXCEEDED" - assert content["error"]["message"] == "Payment exceeds budget!" - error_pays.append(content) - assert len(success_pays) == 2 - assert len(error_pays) == 1 - - -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher") -@pytest.mark.asyncio -async def test_persistency(node_factory, get_plugin, nostr_client): # noqa: F811 - nostr_client, relay_port = nostr_client - url = f"127.0.0.1:{relay_port}" - l1, l2 = node_factory.line_graph( - 2, - wait_for_announce=True, - opts=[ - { - "log-level": "debug", - "plugin": get_plugin, - "nip47-relays": f"ws://{url}", - }, - {"log-level": "debug"}, - ], - ) - uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"] - LOGGER.info(uri_str) - invoice = l2.rpc.call( - "invoice", - {"label": generate_random_label(), "description": "test1", "amount_msat": 3000}, - ) - l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"}) - l1.rpc.call( - "plugin", - { - "subcommand": "start", - "plugin": str(get_plugin), - }, - ) - l1.daemon.wait_for_log("All NWC's loaded") - nwc = Nwc(NostrWalletConnectUri.parse(uri_str)) - result = await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) - ) - assert result.preimage is not None - - invoice = l2.rpc.call( - "invoice", - {"label": generate_random_label(), "description": "test1", "amount_msat": 1}, - ) - with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): - await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) - ) - l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"}) - l1.rpc.call( - "plugin", - { - "subcommand": "start", - "plugin": str(get_plugin), - }, - ) - l1.daemon.wait_for_log("All NWC's loaded") - with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): - await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) - ) - - revoke = l1.rpc.call("nip47-revoke", ["test1"]) - assert revoke["revoked"] == "test1" - - uri_str = l1.rpc.call("nip47-create", ["test1", 3000, "10sec"])["uri"] - nwc = Nwc(NostrWalletConnectUri.parse(uri_str)) - - invoice = l2.rpc.call( - "invoice", - {"label": generate_random_label(), "description": "test1", "amount_msat": 3000}, - ) - invoice_exceeded = l2.rpc.call( - "invoice", - {"label": generate_random_label(), "description": "test1", "amount_msat": 3000}, - ) - result = await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) - ) - assert result.preimage is not None - - list = l1.rpc.call("nip47-list", ["test1"])[0] - assert list["test1"]["budget_msat"] == 0 - - with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): - await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice_exceeded["bolt11"]) - ) - - time.sleep(11) - - list = l1.rpc.call("nip47-list", ["test1"])[0] - assert list["test1"]["budget_msat"] == 3000 - - invoice = l2.rpc.call( - "invoice", - {"label": generate_random_label(), "description": "test1", "amount_msat": 3000}, - ) - result = await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) - ) - assert result.preimage is not None - - list = l1.rpc.call("nip47-list", ["test1"])[0] - assert list["test1"]["budget_msat"] == 0 - - with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): - await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice_exceeded["bolt11"]) - ) - - l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"}) - l1.rpc.call( - "plugin", - { - "subcommand": "start", - "plugin": str(get_plugin), - }, - ) - l1.daemon.wait_for_log("All NWC's loaded") - - with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): - await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice_exceeded["bolt11"]) - ) - - time.sleep(11) - - list = l1.rpc.call("nip47-list", ["test1"])[0] - assert list["test1"]["budget_msat"] == 3000 - - -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher") -@pytest.mark.asyncio -async def test_budget_command(node_factory, get_plugin, nostr_client): # noqa: F811 - nostr_client, relay_port = nostr_client - url = f"127.0.0.1:{relay_port}" - l1, l2 = node_factory.line_graph( - 2, - wait_for_announce=True, - opts=[ - { - "log-level": "debug", - "plugin": get_plugin, - "nip47-relays": f"ws://{url}", - }, - {"log-level": "debug"}, - ], - ) - uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"] - LOGGER.info(uri_str) - invoice = l2.rpc.call( - "invoice", - {"label": generate_random_label(), "description": "test1", "amount_msat": 5000}, - ) - uri = NostrWalletConnectUri.parse(uri_str) - nwc = Nwc(uri) - balance = await nwc.get_balance() - assert balance == 3000 - - with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): - await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) - ) - - l1.rpc.call("nip47-budget", ["test1", 4000]) - balance = await nwc.get_balance() - assert balance == 4000 - - with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"): - await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) - ) - - l1.rpc.call("nip47-budget", ["test1", 5000, "15s"]) - balance = await nwc.get_balance() - assert balance == 5000 - - with pytest.raises( - RpcError, match="`budget_msat` must be greater than 0 if you use `interval`" - ): - l1.rpc.call("nip47-budget", ["test1", 0, "1s"]) - - pay = await nwc.pay_invoice( - PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"]) - ) - assert pay.preimage is not None - - balance = await nwc.get_balance() - assert balance == 0 - - get_info = await nwc.get_info() - assert get_info.methods == [ - "pay_invoice", - "multi_pay_invoice", - "pay_keysend", - "multi_pay_keysend", - "make_invoice", - "lookup_invoice", - "list_transactions", - "get_balance", - "get_info", - ] - - signer = NostrSigner.keys(Keys(uri.secret())) - client = Client(signer) - await client.add_relay(f"ws://{url}") - await client.connect() - - response_filter = Filter().kind(Kind(13194)).author(uri.public_key()) - events = await client.fetch_events(response_filter, timeout=timedelta(seconds=10)) - start_time = datetime.now() - while events.len() < 1 and (datetime.now() - start_time) < timedelta(seconds=10): - time.sleep(1) - events = await client.fetch_events( - response_filter, timeout=timedelta(seconds=1) - ) - assert events.len() == 1 - assert ( - events.to_vec()[0].content() - == "pay_invoice multi_pay_invoice pay_keysend multi_pay_keysend make_invoice lookup_invoice list_transactions get_balance get_info" - ) - - time.sleep(16) - - balance = await nwc.get_balance() - assert balance == 5000 - - l1.rpc.call("nip47-budget", ["test1", 0]) - balance = await nwc.get_balance() - assert balance == 0 - - get_info = await nwc.get_info() - assert get_info.methods == [ - "make_invoice", - "lookup_invoice", - "list_transactions", - "get_balance", - "get_info", - ] - - events = await client.fetch_events(response_filter, timeout=timedelta(seconds=10)) - start_time = datetime.now() - while events.len() < 1 and (datetime.now() - start_time) < timedelta(seconds=10): - time.sleep(1) - events = await client.fetch_events( - response_filter, timeout=timedelta(seconds=1) - ) - assert events.len() == 1 - assert ( - events.to_vec()[0].content() - == "make_invoice lookup_invoice list_transactions get_balance get_info" - ) - - -@pytest_asyncio.fixture(scope="function") -async def nostr_client(nostr_relay): - port = nostr_relay - keys = Keys.generate() - signer = NostrSigner.keys(keys) - - client = Client(signer) - - relay_url = f"ws://127.0.0.1:{port}" - await client.add_relay(relay_url) - await client.connect() - - yield client, port - - await client.disconnect() - - -@pytest_asyncio.fixture(scope="module") -async def nostr_relay(test_base_dir): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind(("127.0.0.1", 0)) - dynamic_port = s.getsockname()[1] - s.close() - - try: - config_file = pkg_resources.files("nostr_relay").joinpath("config.yaml") - except KeyError: - raise FileNotFoundError("config.yaml not found in the nostr package") - - with open(config_file, "r") as file: - config = yaml.safe_load(file) - - config["gunicorn"]["bind"] = f"127.0.0.1:{dynamic_port}" - config["authentication"]["valid_urls"] = [ - f"ws://localhost:{dynamic_port}", - f"ws://127.0.0.1:{dynamic_port}", - ] - sqlite_file = Path(test_base_dir) / "nostr.sqlite3" - config["storage"]["sqlalchemy.url"] = f"sqlite+aiosqlite:///{str(sqlite_file)}" - config["storage"]["validators"] = [ - "nostr_relay.validators.is_signed", - "nostr_relay.validators.is_recent", - "nostr_relay.validators.is_not_hellthread", - ] - - config_file = Path(test_base_dir) / "config.yaml" - - with open(config_file, "w") as file: - yaml.safe_dump(config, file) - - LOGGER.info(f"{config_file}") - process = subprocess.Popen( - ["nostr-relay", "-c", config_file, "serve"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - - stdout_thread = Thread(target=log_pipe, args=(process.stdout, LOGGER, logging.INFO)) - stderr_thread = Thread( - target=log_pipe, args=(process.stderr, LOGGER, logging.ERROR) - ) - stdout_thread.start() - stderr_thread.start() - - time.sleep(2) - - yield dynamic_port - - process.terminate() - process.wait() - - stdout_thread.join() - stderr_thread.join() - - -def log_pipe(pipe, logger, log_level): - while True: - line = pipe.readline() - if not line: - break - logger.log(log_level, line.strip()) diff --git a/tests/util.py b/tests/util.py index 1f6fbf8..393a970 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,18 +1,21 @@ -import logging import os import random import string from pathlib import Path +from hashlib import sha256 import pytest RUST_PROFILE = os.environ.get("RUST_PROFILE", "debug") -COMPILED_PATH = Path.cwd() / "target" / RUST_PROFILE / "cln-nip47" -DOWNLOAD_PATH = Path.cwd() / "tests" / "cln-nip47" +plugin_dir = Path(__file__).parent.parent.resolve() +COMPILED_PATH = plugin_dir / "target" / RUST_PROFILE / "cln-nip47" +DOWNLOAD_PATH = plugin_dir / "tests" / "cln-nip47" + +DOWNLOAD_HOLD_PATH = plugin_dir / "tests" / "hold" @pytest.fixture -def get_plugin(directory): +def get_plugin(): if COMPILED_PATH.is_file(): return COMPILED_PATH elif DOWNLOAD_PATH.is_file(): @@ -21,6 +24,14 @@ def get_plugin(directory): raise ValueError("No files were found.") +@pytest.fixture +def get_hold(): + if DOWNLOAD_HOLD_PATH.is_file(): + return DOWNLOAD_HOLD_PATH + else: + raise ValueError("No files were found.") + + def generate_random_label(): label_length = 8 random_label = "".join( @@ -33,15 +44,6 @@ def generate_random_number(): return random.randint(1, 20_000_000_000_000_00_000) -def pay_with_thread(rpc, bolt11): - LOGGER = logging.getLogger(__name__) - try: - rpc.dev_pay(bolt11, dev_use_shadow=False) - except Exception as e: - LOGGER.info(f"holdinvoice: Error paying payment hash:{e}") - pass - - def update_config_file_option(lightning_dir, option_name, option_value): with open(lightning_dir + "/config", "r") as file: lines = file.readlines() diff --git a/tests/uv.lock b/tests/uv.lock new file mode 100644 index 0000000..c2c260e --- /dev/null +++ b/tests/uv.lock @@ -0,0 +1,1432 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "base58" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/45/8ae61209bb9015f516102fa559a2914178da1d5868428bd86a1b4421141d/base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c", size = 6528, upload-time = "2021-10-30T22:12:17.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/45/ec96b29162a402fc4c1c5512d114d7b3787b9d1c2ec241d9568b4816ee23/base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2", size = 5621, upload-time = "2021-10-30T22:12:16.658Z" }, +] + +[[package]] +name = "bitarray" +version = "3.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/71/dd598d2d546d11d7aca6cd25f05875e2dad194df0a663f1892690d4fb90d/bitarray-3.9.2.tar.gz", hash = "sha256:37342f81c8f8e10ee5d1e23d5eda2e8dbd9d8d3a9d90e8285181fad57f21cdc1", size = 160932, upload-time = "2026-07-25T06:39:21.234Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/0b/8509a00dc5ab2945e9856582f8a4908a78f15a7a08b425dc067f733299d1/bitarray-3.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a334e162a24c4e85babffb78823d83aa04c882a7dd40c2106b668a8cde3da752", size = 156246, upload-time = "2026-07-25T06:37:14.799Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1e/40d32865af46ff2c8a4ee488c60f1dd3724f3ff2c8405cb6594e75685c7d/bitarray-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8855aab242b4865e7ac7e14b6ce80f4cdf213984f260f31d92505d6877c620f9", size = 152874, upload-time = "2026-07-25T06:37:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/12/07/0a646e68fa1fdb73e260474fdca595b93a7ecae6da49ac1d4c34be2b3b08/bitarray-3.9.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf06a3a5263268a35ed0d5513309791040ec8e8d3089bb5af12f51979dbf0a2a", size = 350232, upload-time = "2026-07-25T06:37:17.071Z" }, + { url = "https://files.pythonhosted.org/packages/38/5c/c1926f34269fb112d4b8b2b4fb0b84fc47b02268c30b300d237cccf07060/bitarray-3.9.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b82130cfba659e6a9da81298f43be678e99c598e64d814cfa86f0df8c1e230aa", size = 372334, upload-time = "2026-07-25T06:37:18.165Z" }, + { url = "https://files.pythonhosted.org/packages/42/b8/9937340228a814d72194543a1d452d552e3ea45d1537e27914deff41bbe0/bitarray-3.9.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ebc32a4fd8659a848d2c92ca5d75a1029cff9711bedbf3a32c515a19d4b3868", size = 384431, upload-time = "2026-07-25T06:37:19.251Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f1/311991e38cae3aa2b0f48726578e98aa584157c1f5bf4c8df51ed5a273d0/bitarray-3.9.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047e6ad06ffe606cdc9d75b83effc5f14ab11f30bf6ca38fd98553410e23880b", size = 353328, upload-time = "2026-07-25T06:37:20.383Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e0/7087f78801ad89fedf4812bd9cf46c204e48c1cfbf83bd0d8eede7dbd076/bitarray-3.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:42e7735c20e3e8e8044bd566ffa8461ad4870a8459c5d3fe9d43984eaa2d6104", size = 348330, upload-time = "2026-07-25T06:37:21.579Z" }, + { url = "https://files.pythonhosted.org/packages/71/b9/42b7265266ceee164942e34c278e3982618ff022b40771004693c6a64055/bitarray-3.9.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:23aa70b05518ee20d965a8cbd1e168ba4085647f12d7bdb654412a253c476652", size = 370012, upload-time = "2026-07-25T06:37:22.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/11/a80fde6c902cdef198a20e218a1005f09d9bc5266a8db310cfd2468d0e7b/bitarray-3.9.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0197a8970233d65e04357308d52e5310ea8219e07587f8db8a98d7ac21b7de50", size = 365747, upload-time = "2026-07-25T06:37:23.791Z" }, + { url = "https://files.pythonhosted.org/packages/30/6b/01ae2bfc9778ff2bc78c20c1fe5a80b7290baa1b57e98f04435eafa09a68/bitarray-3.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1a5b7919ab373df270f268ab35c49aa311ed06412290313cecfae9406003ee", size = 350281, upload-time = "2026-07-25T06:37:25.015Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ec/1ccde07e363791274c6f4f376d1e844254d97ea762fd25a2372a1e56f92a/bitarray-3.9.2-cp310-cp310-win32.whl", hash = "sha256:3aba64f5b449f50498185c81ba16dc9b819b06d985630f1b630e836081cf5051", size = 148006, upload-time = "2026-07-25T06:37:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/85/26/ee9cbd0b3df4cb9939bbbe41b0e9fca5185c33b0788eb5129a432784bf58/bitarray-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:3a6bda7bd4a77926a18dc9890acad77ae96c2e4d9ff163a20438a67a23d7d956", size = 157184, upload-time = "2026-07-25T06:37:27.093Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ae/04a86f8fc6a57205b27bad5e201aaa3bdafd70324eb4407c764018b712a2/bitarray-3.9.2-cp310-cp310-win_arm64.whl", hash = "sha256:4f9576e2e417eb4b45257c135b40d5e465969382dbf4291377f4b7327ca74603", size = 153173, upload-time = "2026-07-25T06:37:28.061Z" }, + { url = "https://files.pythonhosted.org/packages/61/cf/1c8efde1c873445d7609e2b1576edb29a7b946a8135a8a5f60b5e775fb79/bitarray-3.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38c850db9ae8602e21c5c29193546a7ac85f1a70adfd4d65853154825675fce2", size = 156241, upload-time = "2026-07-25T06:37:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/bb/2e/21ebb66fb272295c1e90ee04245e00f3655109b95881cf014485fae35526/bitarray-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:06df682d5b9ef233a21fab71912f131b2c295913202f2ad5370e44b0e0b26241", size = 152869, upload-time = "2026-07-25T06:37:30.329Z" }, + { url = "https://files.pythonhosted.org/packages/e7/db/4af8e55d377c25ca37a1da550d1852e44fd280ce5b550bef91d5aa52d2e9/bitarray-3.9.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0c064263c54fffe8fdfe8e20074816c67ec70608b0efb42a7d9fdf35ef67271", size = 359514, upload-time = "2026-07-25T06:37:31.384Z" }, + { url = "https://files.pythonhosted.org/packages/b6/20/464b03e4388d2093998bb355de1396d318728f9d8e1cc715770295babda7/bitarray-3.9.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0a61eac6a5a47cc7bc458b1f0737a701767543949580605cd85272c48121fb0a", size = 381209, upload-time = "2026-07-25T06:37:32.499Z" }, + { url = "https://files.pythonhosted.org/packages/02/fd/f24ec964e5737180957366473b664f8587a34dc7792e674a9d6c839db2e6/bitarray-3.9.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b62d7de8e4dc4ffbff407e1c7f868b84a00ac4a9919e28755fd299e86740fced", size = 393701, upload-time = "2026-07-25T06:37:33.501Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c2/0ac619df1eb86593c7b7d833325cfa6cf5a3bcd55f3fa0871e30e1d6036d/bitarray-3.9.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea75f495073f95e96e3f3049411854b5db1693901d8b324439a170ef45088fa", size = 362278, upload-time = "2026-07-25T06:37:34.691Z" }, + { url = "https://files.pythonhosted.org/packages/23/6b/3325564d17cebb71f6beca6b41d463d1083acc68ae06abb3ef2729f78b46/bitarray-3.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f3eb99f3cc4ef93b4830e8532ba506be4105f15daddba9302bbd96f24b1cf72", size = 357205, upload-time = "2026-07-25T06:37:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/16/37/6253ca3aa44d257c8a70ce9bb98c0d05302bff586979c22b2264c9ae4457/bitarray-3.9.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e22236531f0193457277a7340d8bdd006f202f563f22f29abb873dc0850eb472", size = 378079, upload-time = "2026-07-25T06:37:36.958Z" }, + { url = "https://files.pythonhosted.org/packages/04/4f/e7595cb740e1b296f601b0d1292702b867292204e8f4ff010772f3a75c02/bitarray-3.9.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:92a438122d35d5e5703983c7dda7de90fc25c50282ce60f73ad61a6ced3ce0bc", size = 374941, upload-time = "2026-07-25T06:37:38.021Z" }, + { url = "https://files.pythonhosted.org/packages/87/61/135e3b0b42d65577725f2ddada426fce35cf816bc7d07e954ff6194b1193/bitarray-3.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:47ec58fceafd38d5839bb9d8262ca6150e222f81d1e4adc8feafda6caf360334", size = 359249, upload-time = "2026-07-25T06:37:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/9b7a01e4d5d634f36fc1151854fdc61c09678b53dfd667f66baaff7efbff/bitarray-3.9.2-cp311-cp311-win32.whl", hash = "sha256:e8ec38401d9370684709f8cc3a0febbb742edf9fbacb1e432bcad8d023990e6d", size = 148130, upload-time = "2026-07-25T06:37:40.15Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/ee0813622d5fc2c6419aa5ec8e12407593f56c41a6e70fe1b8092b158c82/bitarray-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:30e2516878f7745957948e1a2d877f56ceda13b5f6b51d92d2174ef1756ab473", size = 157489, upload-time = "2026-07-25T06:37:41.158Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/1ad11f39eb6b1851777fb25185e839d19d273f2350c5e1bcbf0e014a7fda/bitarray-3.9.2-cp311-cp311-win_arm64.whl", hash = "sha256:89c87ffdb399a53821cb0f5566a7d04e0f2870096a4964b038866a894b40c9c6", size = 153465, upload-time = "2026-07-25T06:37:42.151Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/6d33775d73e22cfe3fe23dae2ed50dbabdb38ea651e8b5c983262c39ce26/bitarray-3.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c967be9bd2fadd58e0d2c4e139e6e7443b5dd9162e52e43ef5e2489218c4fecb", size = 156385, upload-time = "2026-07-25T06:37:43.505Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9f/24b22bfa3016fdd50966966f7dec0ecf11a911429be5178880228068e108/bitarray-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c17c918e569a81a2dd87794e15991c9643accf72b5c005e8791cdedcb2d6189", size = 152941, upload-time = "2026-07-25T06:37:44.64Z" }, + { url = "https://files.pythonhosted.org/packages/48/61/affb7fd0ce338529b57eea7d1fda983b4f4adde89eb7ea8e4201c4aa4e47/bitarray-3.9.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eea0dc21749eeb53b163d45c3dd0524e62c0301dd512bfb4f5ef34ccce7be6ad", size = 362020, upload-time = "2026-07-25T06:37:45.687Z" }, + { url = "https://files.pythonhosted.org/packages/31/e1/43f6eed3ae86d08c11eb424ad0c7ffd40bb081eb50c682c76adeead3ed0d/bitarray-3.9.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:64fb672905884fc236ef200b9fbde529c6673a8217c62bf8651bdce4842ba366", size = 382732, upload-time = "2026-07-25T06:37:47.212Z" }, + { url = "https://files.pythonhosted.org/packages/12/f1/cf664f54fef3c5c921e7133a2795201e508c2c984ffe7c0f3f851f2b976d/bitarray-3.9.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:be25330b9da322a806b29bae0147e6ea3730b6f85a5036e25f8e50247fee013a", size = 396696, upload-time = "2026-07-25T06:37:48.309Z" }, + { url = "https://files.pythonhosted.org/packages/35/fc/6d6409ec30dabd388b6f3ee6c461eae353f04d95dc287f7be19fffff8fcc/bitarray-3.9.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e14fd457ccea13e0adca60d93ffcd17b70184e7f485a986523241a077f3ac9", size = 366507, upload-time = "2026-07-25T06:37:49.671Z" }, + { url = "https://files.pythonhosted.org/packages/37/47/d241a6dde522a213cd624a820e0be998b0e5548d141098e990bba03913cd/bitarray-3.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f719003b0d692d3088e8f783ec684bf7caf00bddb5392efbe7842ba1ff15e8e", size = 359937, upload-time = "2026-07-25T06:37:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a6/0f4dd5a813f87b79e07e4bece697e872f4836486fca92903c63cc75c42db/bitarray-3.9.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a72e6b82f1800a08dff90a9d401ffd3f8c278644e4f6711a7458c9f16ee9446e", size = 379627, upload-time = "2026-07-25T06:37:51.884Z" }, + { url = "https://files.pythonhosted.org/packages/cc/27/cbc55ea191b52710c19702cbbae7814ff55082b02a4c23aba11c23dd1990/bitarray-3.9.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d043ef192ae741f0524b3d3404a2e6e8dbb6affeda421d40d46456cf6cfa1064", size = 378404, upload-time = "2026-07-25T06:37:53.43Z" }, + { url = "https://files.pythonhosted.org/packages/fc/70/20179544538444c0896099a10a035d857c2f4ce707a950834aa62d645714/bitarray-3.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:23fe97bfed91ae4c927871b779caa159000da958fe5521d92420a09ed524efdf", size = 363092, upload-time = "2026-07-25T06:37:54.796Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/c9b206363aad0239cf966ea4eefaaa0da2717cd695ac11af1effbe3819bb/bitarray-3.9.2-cp312-cp312-win32.whl", hash = "sha256:ff72bbdd7c01ca661cb6712e706c0e0f0bc45a6b2426f69e9517cf7508b8d294", size = 148523, upload-time = "2026-07-25T06:37:55.915Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/67f161485ba266ad76064405cd19dda50057f466d796b4ad9d9e2645b877/bitarray-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:12c2e7556f7d7f3d2a2017d66159790ed6cadf849a3aa436d9c3541dd3facd6c", size = 157829, upload-time = "2026-07-25T06:37:57.285Z" }, + { url = "https://files.pythonhosted.org/packages/58/cc/aefdf64e5c3b057538ed6e5f244fa055bc825b10cb3a08ead0895ba9ff67/bitarray-3.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:76c0a98d85b2ed7090ff6c0fff9917a2ddffaffb82c2d0ce7d424f0830dc8dee", size = 153677, upload-time = "2026-07-25T06:37:58.332Z" }, + { url = "https://files.pythonhosted.org/packages/9f/33/a15849ec7ca05b55835b52baa4f226ee1d75cffd0943cf0a648c52bf9048/bitarray-3.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5156ae1fb0d56238b2ba64b8c0da9a14dbc2e16c9d79114ad3850b7aff513b0a", size = 156131, upload-time = "2026-07-25T06:37:59.76Z" }, + { url = "https://files.pythonhosted.org/packages/53/98/f097b0e50c31c4e1cc8f8612e21b8d193a66c0d14ca07f06eba376c5f090/bitarray-3.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:054fc478842a95d8e9e200117a7e170cd8ec17608e3cfa451e44fc78e5ab5007", size = 152618, upload-time = "2026-07-25T06:38:00.769Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c2/ea3786bb4231564482f23f1aaf478a1ddf8e8d08cfb90167c17cca89623d/bitarray-3.9.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d5e428a70d6e395b9fd415bba70bfb463d2b9e8010d471cb99eb0f82e91d179", size = 360335, upload-time = "2026-07-25T06:38:01.829Z" }, + { url = "https://files.pythonhosted.org/packages/fd/93/5772f7c4ffa4d6a1e9e486e8623f4f89041268e8c91b5761ad0920fc53e8/bitarray-3.9.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a34d717b362f96a767b03f350cefecd0f613a44ce774868d3635423eec1e5d75", size = 381029, upload-time = "2026-07-25T06:38:03.1Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/e84a9a61cbf7db68703f6ebeb853c4d323c9b1d83bd4d863ceb424575785/bitarray-3.9.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:068a100b2d3507e1ebd6f0cb3515551ddf3c7db1c98426dc854ddff985508eb8", size = 394606, upload-time = "2026-07-25T06:38:04.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3c/127d48763eaf8941556486fc7883096d9024139276dfc19150523bfc6e31/bitarray-3.9.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a797165662ce08029a5975e05e928b77f979d8322cc719ba46eb8b3d9ab5c9b", size = 364341, upload-time = "2026-07-25T06:38:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9b/d58b96fc10bec25fa59aa894964dadb5cbfb150fe4b30aeecea162c2cda4/bitarray-3.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6468d775054cb4fe777d585c661df89e99f9398fa9cb31e428a9b42105e6ce48", size = 358005, upload-time = "2026-07-25T06:38:06.464Z" }, + { url = "https://files.pythonhosted.org/packages/61/5f/25989fb53e1e4f079b2c75b24d0bcb500b407cb0be2c35953fee092a872a/bitarray-3.9.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd26caa42d12c747af2c3a3995b08db32ac7afa0c5262c0b6c6931293067805", size = 378038, upload-time = "2026-07-25T06:38:07.818Z" }, + { url = "https://files.pythonhosted.org/packages/fa/42/e80c45b496d60e216af776ed284c65cc55434cfbe39cd459975f1fb6a0ca/bitarray-3.9.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:11574711bae2b501141f64cf6d7a9819c9f45e7d8edcd2d9fe282b377dadb900", size = 376295, upload-time = "2026-07-25T06:38:09.079Z" }, + { url = "https://files.pythonhosted.org/packages/04/84/8bc4a8e12a802199b20912f57427e3ba6b8cc3649190fecb7c97bb35a69d/bitarray-3.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c6e16d6a8d93362f65336fc57e76fb1fe94f257bb54a843cf1a83c86887ac1ca", size = 360959, upload-time = "2026-07-25T06:38:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/34/ca/3ebd885ae0df8f9ec508315361e602fada65c5c75d8b1d1660c81659cce0/bitarray-3.9.2-cp313-cp313-win32.whl", hash = "sha256:7fd8d649b10ba26c032c76ffb2610385496ebf5e52c44c26864e9b16ac8102b3", size = 148350, upload-time = "2026-07-25T06:38:11.473Z" }, + { url = "https://files.pythonhosted.org/packages/6b/92/7ecff07e9bbf6f51092bc3a01361bd5bae5814cd3326abc8423a2ae637a0/bitarray-3.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:0df2bd4b5fea65be332cf9c17351d4c68d3eb442c0e13a5bda8019554cb36ce1", size = 157627, upload-time = "2026-07-25T06:38:12.591Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c4/28122fd875b6bdbebfc15bf60ea3704149540b0f3f43e15f247a7e9604ba/bitarray-3.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:94278c9d37a6f4316ae1f100e98e6582369f25b1f8186bb595ee1578fbc5c52e", size = 153528, upload-time = "2026-07-25T06:38:13.863Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b2/307b9857295d37489b3269eb3cbcf62ab55d19e17c6c567602a2fbf3a589/bitarray-3.9.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b0debddd2eb7d1a0417f602b2520dd6fb6897ab140c1d69b8c42a69d164ef61b", size = 156152, upload-time = "2026-07-25T06:38:14.92Z" }, + { url = "https://files.pythonhosted.org/packages/95/55/3d13cb858176c2a9189c35becccef29b45c32720e7a1f36c57af431ff7c0/bitarray-3.9.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5d0387459ac35f6be40c4b809c34f34afe14aa930493e0b06f6df5d327f405c3", size = 152690, upload-time = "2026-07-25T06:38:16.096Z" }, + { url = "https://files.pythonhosted.org/packages/59/1f/7d01c3130fac3d7cd9f52697019e4dbf992fc67befb7c915faf456bc1c1c/bitarray-3.9.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bde034041875a48ea8c0a4b272e9a6e29174b234bd4905c86a65484f34cd140", size = 360268, upload-time = "2026-07-25T06:38:17.27Z" }, + { url = "https://files.pythonhosted.org/packages/25/61/2b6f05eb6c855727799dd3092b70e933ff433e9464740245042c2bfc68f7/bitarray-3.9.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:37c76ff084118ab1db8b826e35c01ce4d0137e66b611542e16b1f296c96e6aa6", size = 381244, upload-time = "2026-07-25T06:38:18.462Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/72d15249f41ffcfad56b4cea7d2a518d936c33c3548f6f474de96ae9da88/bitarray-3.9.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d12c7b574e2903477124a0d77bbc903fd4c3beaefa841f4a63c199014192447b", size = 393965, upload-time = "2026-07-25T06:38:19.823Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6c/e880b52ebed9441a423afdb9f8f3b71186b689cefdcdc6fcb434ca4775ee/bitarray-3.9.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bbff5cccf3a1f4129c8ad43b2bcb789882fba47c87422b30a74bc3bee98a292", size = 363960, upload-time = "2026-07-25T06:38:21.06Z" }, + { url = "https://files.pythonhosted.org/packages/5e/39/2fb0d14bc884de3dda42a6cfbe2325d76af45733e6f97709924352db2bf8/bitarray-3.9.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c374884716ab641c36ee4f51859fa4274fbdf0355ae7242290a29af3cf49381d", size = 357843, upload-time = "2026-07-25T06:38:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/af/ca/5f7fe5898ea35e34ce79b653ae513d4c67bc0ae607229c51a09d8278195e/bitarray-3.9.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fe1d36b6d816512af098699c85e9237ce5978863a336a2833ebc03f59545781", size = 378105, upload-time = "2026-07-25T06:38:23.436Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c9/c6b3121421783f10481836140ab976cba7b73bceb9f58216eabf1cee073b/bitarray-3.9.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4f719595fde8bdf030282c8221c738248a3a73928db3087ea62db2c29de5342d", size = 375952, upload-time = "2026-07-25T06:38:24.695Z" }, + { url = "https://files.pythonhosted.org/packages/30/95/05f50342d799d7f49688371ebd3695b79c6fa65db4eeb9fc335c72aa755e/bitarray-3.9.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:28c4edee03ece6768ee51c1889c5d4df05764f8ad956fed6295912dbe4aef729", size = 360654, upload-time = "2026-07-25T06:38:25.944Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/836ad7a1421cd4dd888ed602eb7e7fb297de866e64dab19150be596e9abf/bitarray-3.9.2-cp314-cp314-win32.whl", hash = "sha256:f01b94e8bd0229c214f0873f8d19cf4db4602a230064317a8be00428922f7987", size = 147264, upload-time = "2026-07-25T06:38:27.106Z" }, + { url = "https://files.pythonhosted.org/packages/2a/cd/ca5feb145af0ad602c88a2bb43ff3c44067846324c58e8c972a397adf2ee/bitarray-3.9.2-cp314-cp314-win_amd64.whl", hash = "sha256:7892775de516287fbc850cfb9476599bc399b971105fbbe216513ac4cb67d6bf", size = 156687, upload-time = "2026-07-25T06:38:28.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f4/8d9516a9aa91b18c9c0075451386b09c342acf3cca536331438d38e8c955/bitarray-3.9.2-cp314-cp314-win_arm64.whl", hash = "sha256:7dcbf363dce364722a3cd3875be5453a36db8c476af551bc5b194ea9960c591f", size = 153104, upload-time = "2026-07-25T06:38:29.357Z" }, + { url = "https://files.pythonhosted.org/packages/38/ac/62db07d42f4713d8032acb2c5776550f99da74ead726d3ab49845ac2b768/bitarray-3.9.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:732fc6464eebdfbbdb28e7a2e98424ba7633619665fb065cb6d756ffef5f6b08", size = 159619, upload-time = "2026-07-25T06:38:30.939Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4a/7ddb362b4dc3f7beb428e96c129fbc96196c7a716658d5d677dffe8fffbd/bitarray-3.9.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c23f1dc6f118c3a885ee2a281261da37e98c35b4361dcf2c9a9d3d70370b32e6", size = 155980, upload-time = "2026-07-25T06:38:32.085Z" }, + { url = "https://files.pythonhosted.org/packages/0f/78/08b2ac57ff404a33e98193481d1a4fe716999ed2255335fbd56ddda7737d/bitarray-3.9.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ada39b08677af76c48a64f1f14e8e518315cf478004ddf4b6c580ce3625c1a9d", size = 378321, upload-time = "2026-07-25T06:38:33.376Z" }, + { url = "https://files.pythonhosted.org/packages/e5/79/5a0734dbc080748b11680483d74f29469c85b56ff6b490028c301732c658/bitarray-3.9.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70e5a48906a8cab0b8d84ad356d8cf8f0fb533b93475da26437a4a3aed8a9a4b", size = 397775, upload-time = "2026-07-25T06:38:34.619Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2f/a4d87cc0a1a01851fb76cb13633413fc8d2187cfbf00d9f85397edba301c/bitarray-3.9.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b14351e75d76662d10c1479c8864f4076ab393a297323b85277d2253ccfb96e", size = 411104, upload-time = "2026-07-25T06:38:35.945Z" }, + { url = "https://files.pythonhosted.org/packages/0e/df/5b2d12198c9403a8b757dfc54fee1199e202ec361ad7fb4cb84f295e917e/bitarray-3.9.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:920edc84385932401ddabe62e244774acaa69e991072768fcf7e621c29599f6f", size = 380204, upload-time = "2026-07-25T06:38:37.14Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5f/b6daaa21059439703d53349d9e9158860fa04d31b8a5613dfdc45c8f4b9c/bitarray-3.9.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3d6dd06fd0e1c88e6da86e4c0479457c74f03f66a2dc16c0c60ecce12c5dee1d", size = 374273, upload-time = "2026-07-25T06:38:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4f/625e820bdc9235f56ef9644892c7b6b89a96cb7f5a0823748e9a912f1d1a/bitarray-3.9.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ee339e8f9a763499c5fc855927c8174680183e8751f03f797234a0bd1223c3ff", size = 394096, upload-time = "2026-07-25T06:38:39.785Z" }, + { url = "https://files.pythonhosted.org/packages/02/27/04ee7179dd670781a3c457969e94503a10c516e43340faf04f6862c7c0ef/bitarray-3.9.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:50f0ff3b5e5cfa7ca42e2469418a26d098a42e47b0e171adf9c6c69ea800d055", size = 391958, upload-time = "2026-07-25T06:38:41.011Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7d/ef4ce2906e6c3056021196d11efb220f351f7db74783882e802c8f81c17f/bitarray-3.9.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c898478e6dde8ca75970b91a602b1f48ebedff49841b89cb8495c1949b6d762c", size = 376178, upload-time = "2026-07-25T06:38:42.535Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/ff961878f0671e921d03a498705b4a15bb96b53474685f2d04abb79204f4/bitarray-3.9.2-cp314-cp314t-win32.whl", hash = "sha256:415675dbae4d1c58a5ae31550db6224e0785443e41d9ed74cf4371aec38d61d3", size = 150524, upload-time = "2026-07-25T06:38:43.939Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ec/a3eb15a4a9730e3f48315f8d510867944f9051b82a465f4fe1d28b9a9e76/bitarray-3.9.2-cp314-cp314t-win_amd64.whl", hash = "sha256:de17695646139d2a9863cf1d8ff83f1335aa3d593f2730c9ae442fb08ef80241", size = 160476, upload-time = "2026-07-25T06:38:45.145Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/73c274307ec6f74fb072623ec2644e6467f36b24f7464f0be2ed236c183c/bitarray-3.9.2-cp314-cp314t-win_arm64.whl", hash = "sha256:19105d5ff84450b341a0dc1021c7f8d63cb1ffedcdc78b46e62f4cd76f28a7f1", size = 156219, upload-time = "2026-07-25T06:38:46.274Z" }, +] + +[[package]] +name = "bitstring" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bitarray" }, + { name = "tibs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/d3/de6fe4e7065df8c2f1ac1766f5fdccbe75bc18af2cf2dbeecd34d68e1518/bitstring-4.4.0.tar.gz", hash = "sha256:e682ac522bb63e041d16cbc9d0ca86a4f00194db16d0847c7efe066f836b2e37", size = 255209, upload-time = "2026-03-10T20:29:14.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/02/1a870bab76f2896d827aa4963be95e56675ffa1453e53525d13c43036edf/bitstring-4.4.0-py3-none-any.whl", hash = "sha256:feac49524fcf3ef27e6081e86f02b10d2adf6c3773bf22fbe0e7eea9534bc737", size = 76846, upload-time = "2026-03-10T20:29:12.832Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" }, + { url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "cheroot" +version = "10.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-functools" }, + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/7c/95c154177b16077de0fec1b821b0d8b3df2b59c5c7b3575a9c1bf52a437e/cheroot-10.0.0.tar.gz", hash = "sha256:59c4a1877fef9969b3c3c080caaaf377e2780919437853fc0d32a9df40b311f0", size = 148461, upload-time = "2023-05-20T15:34:09.67Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/52/ca26d1964531e823961f761026138189f630aaf49fb959c8ab6e1262dc74/cheroot-10.0.0-py3-none-any.whl", hash = "sha256:8f65dd38ad3d56419cfe2d1b5e4b4e3282b1d58758ca2a336231641a80cf0717", size = 101572, upload-time = "2023-05-20T15:34:07.66Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "cln-nip47" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "nostr-sdk" }, + { name = "pyln-client" }, + { name = "pyln-proto" }, + { name = "pyln-testing" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "nostr-sdk", specifier = ">=0.45" }, + { name = "pyln-client", specifier = ">=25.9" }, + { name = "pyln-proto", specifier = ">=25.9" }, + { name = "pyln-testing", specifier = ">=25.9" }, + { name = "pytest", specifier = ">=8,<10" }, + { name = "pytest-asyncio", specifier = ">=0.23.8,<2" }, + { name = "pytest-timeout", specifier = ">=2.4,<3" }, + { name = "pytest-xdist", specifier = ">=3.7,<4" }, +] + +[[package]] +name = "coincurve-cp314-fix" +version = "22.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/f9/e9d3c502369933915d3e72d596a1ddf81b3d5ae986f5bd65bc5ada52e6b9/coincurve_cp314_fix-22.0.1.tar.gz", hash = "sha256:b68397f902a73ba15ab5652fae712f6ba85625e382026be2a5c5e698845a79f4", size = 129346, upload-time = "2026-07-17T14:49:10.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/ca/ec5e2c4ba1df02844e1087e58636ecc78a096fa7e7dfd85cab286c0c1c31/coincurve_cp314_fix-22.0.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:9fec35946bfe7784e92aaa656b298b3b67f0a093f52c7970496dde4621bbe6c0", size = 1388687, upload-time = "2026-07-17T14:48:04.925Z" }, + { url = "https://files.pythonhosted.org/packages/a2/01/5c0435325516a6d7ce2958e90e4d050652d105465a187ff8e983a1271370/coincurve_cp314_fix-22.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f5f4abc2e639a4007ca212b7029ea9fc549dc1e1207348f1c8729e34b6e0a94c", size = 1383962, upload-time = "2026-07-17T14:48:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/4c/93/d9ea4c7955a1f67740e492adca7d8ffdb7b9434c2ddc9628e578d37552bb/coincurve_cp314_fix-22.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2dc1deb3cec48571fa05064fa8dad3488a0d63e3f16fa731c7815f3f204a2cb", size = 1371893, upload-time = "2026-07-17T14:48:08.247Z" }, + { url = "https://files.pythonhosted.org/packages/d6/0c/eec3262ad43b0f40203656d1a1e52377d603a47f76de6352dd15659285bc/coincurve_cp314_fix-22.0.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b60d06533f4820023a4e91679c2181ccc29a23193d55fc51e6d55874186b67", size = 1370994, upload-time = "2026-07-17T14:48:09.9Z" }, + { url = "https://files.pythonhosted.org/packages/36/9c/733843a2423ec759ccdd78338dba53f9bb9093f011ea8b31feab07e510cc/coincurve_cp314_fix-22.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:01428e65cfac5bedbeb7a6c1e146970d8aa8a384a7dc39f0c45c38350c6960cc", size = 1377492, upload-time = "2026-07-17T14:48:11.756Z" }, + { url = "https://files.pythonhosted.org/packages/33/e1/6c8bca371a395b32ddd86b2bcbae146789984b0bdf329524f6e85a73846e/coincurve_cp314_fix-22.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b787b23c80501e1af903bfe4f89a07c51ccc02e2333913e152f4e4c2cdbf644f", size = 1374123, upload-time = "2026-07-17T14:48:13.33Z" }, + { url = "https://files.pythonhosted.org/packages/d7/52/e9a7a1c82d615e9f4060ac77509bae9d95e1c9a4ad24941abca9f356d091/coincurve_cp314_fix-22.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:6b02bca3564e795a945c0a2a7f575d3af7d6e3c0e84133c610ca84cf0b824d21", size = 1329759, upload-time = "2026-07-17T14:48:14.845Z" }, + { url = "https://files.pythonhosted.org/packages/5f/55/c858d0ce0a2a3ec590463838af052492680debae92ca38b071ee459f351c/coincurve_cp314_fix-22.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:c2a0afd16f410f0b9932cc2123ba1adceca8676cffd649c4a5c1fe344d3a55df", size = 1320063, upload-time = "2026-07-17T14:48:16.294Z" }, + { url = "https://files.pythonhosted.org/packages/74/d9/6e191269f241861ed25ce193bb1d24e8758eb83276a0bc28d7f929704d45/coincurve_cp314_fix-22.0.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c7beead9480fc1e47b3727172af2279f1043bacc68f1a7226af7687bd893058e", size = 1388699, upload-time = "2026-07-17T14:48:18.13Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1e/a330eba734c262182bd7d35595c1e4b72a32cced388b649ab826b8b93d47/coincurve_cp314_fix-22.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3a710296ef735bb96434a152101bcdef9bdabbec291305eb0e6cc864935e7db", size = 1383919, upload-time = "2026-07-17T14:48:19.759Z" }, + { url = "https://files.pythonhosted.org/packages/52/ef/fa17567e5044fdbe958534191b31e461affc2019df6e21f6644981bc2cb1/coincurve_cp314_fix-22.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8bb83a3ea92906d5fe1623c3554c4973c0b51cb7f3a5e10dae549eec9b1b1eb", size = 1371952, upload-time = "2026-07-17T14:48:21.412Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d1/b6cbf61dcbb11b053891d593d82fb976e5cc44092fef09d0787c8edb7071/coincurve_cp314_fix-22.0.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9d88f8faf7156fddad08dbd032671d6bb2427a262fc57e81be5fc4f9d1357aa", size = 1370118, upload-time = "2026-07-17T14:48:23.358Z" }, + { url = "https://files.pythonhosted.org/packages/99/f5/8d629287c7c2e7b9efca2cab3cfa95a02c2312881551b39875cb81671edc/coincurve_cp314_fix-22.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8a41bd1c4012d8ff480a3972cc9300400530edb8e7f0d16617e0f241fe0b72d2", size = 1377513, upload-time = "2026-07-17T14:48:24.923Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ca/8aac5adcd9616dd73095a2c4a707b30bc31674fae8522de44992309d34df/coincurve_cp314_fix-22.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b9083d1e5042866a1e28e91e9d5c8fdcf51629dd2a273f6b405059e7c95945fb", size = 1373226, upload-time = "2026-07-17T14:48:26.512Z" }, + { url = "https://files.pythonhosted.org/packages/dd/98/1b4f2f26d6980bdd17a8f2864832f9dff895b032a49234b51389ab58596d/coincurve_cp314_fix-22.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:918b4ad4d1e4f4cf38c11aa39b9d842418987f898caccc6114b45aa081ae2c90", size = 1329815, upload-time = "2026-07-17T14:48:27.899Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e1/21f17125e8b9160dd5f6db3f92da6e18b1f4cdececfc3a154f023a7f9ad7/coincurve_cp314_fix-22.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:ab926906a2718de26a3e23955103e41e39cb316e5cbb68489405d4a2bc2011c3", size = 1320212, upload-time = "2026-07-17T14:48:29.473Z" }, + { url = "https://files.pythonhosted.org/packages/4a/32/24fef91a512c51b57b5980207bdf4974ab6e998aefde3c50abb565b4f346/coincurve_cp314_fix-22.0.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:b077081c058338aaab7fe634275da0248fe59b469be9ec2bfa598e6e328fc5d1", size = 1389725, upload-time = "2026-07-17T14:48:31.19Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7a/24b3079bee4bd2f081865e28a08eb13b909470c7d8ce3a8c4ac9aae80727/coincurve_cp314_fix-22.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ed7e6c3434333ecc17b00834f17effa5f48fc89b3e130d19157e3b18e0bd149", size = 1384501, upload-time = "2026-07-17T14:48:32.942Z" }, + { url = "https://files.pythonhosted.org/packages/41/26/7a4fbaff523784b9339f247a315056f99439fe671705d92bd9df01612e59/coincurve_cp314_fix-22.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72535b8303d45bfc47bb27e7b1b898cfba11ddb9ca6db9e528f0e23360da5f3", size = 1375639, upload-time = "2026-07-17T14:48:34.777Z" }, + { url = "https://files.pythonhosted.org/packages/82/2e/c6f93f5bbbe6c050f5425b9e1ebb1fc8a28e3074b4ebe6542c91be186e27/coincurve_cp314_fix-22.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a56ec57a1ce4116db6281703c5c64d35a9fbcb4ed2ab65ac38109c412afb3b6", size = 1374117, upload-time = "2026-07-17T14:48:37.179Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f0/911d0295ed3a199c7a4eb43d2f8ffff1d1dcca39f2128fe5fc7f1fe52c17/coincurve_cp314_fix-22.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0501e49a206b5ab4105f65daeb338dcf3559bd7d5392a03afc3f31db83f661fc", size = 1381644, upload-time = "2026-07-17T14:48:39.018Z" }, + { url = "https://files.pythonhosted.org/packages/be/68/b3ecefd5edccd3202f24c0cfb696aff0756e6095db9f7c67da78aff57f27/coincurve_cp314_fix-22.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7a5d93127fea898e53882ac106535fbc39ff4f2a26e9c5f23e1efae716bcf6a6", size = 1377529, upload-time = "2026-07-17T14:48:40.608Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/be427f7e8b037b14b94eb97e22fab08b8d41535075dd1b1d7a668fc1d4dc/coincurve_cp314_fix-22.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07fcc0a4b6c86566462c9918f6c68303e3699820c4df595daef9a8d131584f29", size = 1330618, upload-time = "2026-07-17T14:48:42.12Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0b/1833d9b75a8d0dabba52db6a4e665cd0bf24401c513a062f82c8fc4f56a2/coincurve_cp314_fix-22.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:f7b282d27d234b96e064e6bf061d56a4148a4e4fb14f0309fdcc555206892ab6", size = 1320184, upload-time = "2026-07-17T14:48:43.588Z" }, + { url = "https://files.pythonhosted.org/packages/93/9f/2d2576282a16fcddd4bacaae9767852203bc6fe6a851a2410b0dffb0bc87/coincurve_cp314_fix-22.0.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:227158f4cbd329f0b75780198ab312ac72d697b2ec28daade45d0c3d8aa7ae3a", size = 1389695, upload-time = "2026-07-17T14:48:45.298Z" }, + { url = "https://files.pythonhosted.org/packages/9b/6a/fc8e76c2f6d62af177960a719b5987e644412b13fd673175dea4f2696370/coincurve_cp314_fix-22.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6cde6eda5348115cd46555aae8e5fc8a1329cdd34560293a371a3e8394390c16", size = 1384517, upload-time = "2026-07-17T14:48:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/63/f1/f3a62079cb2d4205712545d70e7bd221b276b0b379329d844ed825076014/coincurve_cp314_fix-22.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:155c9364dcea3b4028fc03d02b5ac4c1d791c7989c54ea8d73ff52600fa9bab1", size = 1375601, upload-time = "2026-07-17T14:48:48.388Z" }, + { url = "https://files.pythonhosted.org/packages/ac/1c/d4769d938a000d14ce99f08c42148e8fda85b5b243943e804a5f27e4d951/coincurve_cp314_fix-22.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ccf73b280220759fa02251f09300d85af54ff23631da3e95c14eef30cba7408", size = 1374082, upload-time = "2026-07-17T14:48:49.852Z" }, + { url = "https://files.pythonhosted.org/packages/fd/88/246bad1924267d0064a7a6092d10851f66765caefe9a0010be8c25c94a6c/coincurve_cp314_fix-22.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5eb5a6aa08e8e07d2edb3a6b8e6602f0dc94a1b0fbf4d83138df749b921fe8b2", size = 1381599, upload-time = "2026-07-17T14:48:51.33Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e0/08dbec75aced7f23dba7dc52f1294f30e862de33cc9112bd01ccf3b3f50d/coincurve_cp314_fix-22.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0bde261973270d2313bf82d526a6a395d084faa74f514d98c2dfe96bcb3d331f", size = 1377435, upload-time = "2026-07-17T14:48:53.016Z" }, + { url = "https://files.pythonhosted.org/packages/28/ac/ff86fc78fa9dcf4792f57e0675030dd21701e0f121c165801a2434598e20/coincurve_cp314_fix-22.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:07ab4f37249dab54d3dc02e35dd82f0309c9995a1656f91ab1c8efebf7c35ed4", size = 1330553, upload-time = "2026-07-17T14:48:54.539Z" }, + { url = "https://files.pythonhosted.org/packages/1f/17/5f27565dd86fff04636b5a4a7a5450f6f35474b3c02ad6832852ce98e134/coincurve_cp314_fix-22.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:8d0d6057895da2d4fb7da49463c08ccbc3399a57df5b1ae18b296edcfe724cae", size = 1320215, upload-time = "2026-07-17T14:48:56.047Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/721383a5cc7079360afa5a235c8bbddbdf09505f39cc315d5eafba90f638/coincurve_cp314_fix-22.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6b18156408e60b12b87810ae323ef0b483251c33f05f172e344c325a034014e5", size = 1389892, upload-time = "2026-07-17T14:48:57.533Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f2/9f8518e3b138256d80d40ccdb3cb8edb78dd01846ff221da3d212f6c44b9/coincurve_cp314_fix-22.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:15c9257f42a42c14e098e0f37b8b5fc32371854c84f3bebe2448a307ca674b3e", size = 1384743, upload-time = "2026-07-17T14:48:58.999Z" }, + { url = "https://files.pythonhosted.org/packages/44/40/913a7a3f02678bc1990e508fba9fa156d643a788cceb611d8c35b99a4436/coincurve_cp314_fix-22.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c04475afd71244fb6e04af2d8f2e2ad98264f6b6f549826560ea96925425f33", size = 1375586, upload-time = "2026-07-17T14:49:00.759Z" }, + { url = "https://files.pythonhosted.org/packages/46/6d/21c2888ac918199cedd9b438a2bb5cd9e1746bff4cccbdf27f35c646b94d/coincurve_cp314_fix-22.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2e1066e72ac20310fb2359f0c7bdcc7d09f796eb3d316b3e03b3c5e59fab349", size = 1373843, upload-time = "2026-07-17T14:49:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/61/64/d1ae34a2843dc76e69f65ef6c611fd3f09bc6a11924a56c555b6aea9339e/coincurve_cp314_fix-22.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a629e282b8a9d531b882ee1735a6ce1cfc0c8c37531583131ade5c4ca5d08b1", size = 1381502, upload-time = "2026-07-17T14:49:03.993Z" }, + { url = "https://files.pythonhosted.org/packages/06/ad/1c0b90430feefd17594fda215880137a40bb7858dde62bb60c4b58cf0771/coincurve_cp314_fix-22.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f7697cc7d75de9b9e34e041cfe7dd903db2141a4c31c92ec420674f937c3af8", size = 1377090, upload-time = "2026-07-17T14:49:05.59Z" }, + { url = "https://files.pythonhosted.org/packages/7c/20/1267eb8492b9d2d7a904739476eebc7d4d3921d13e716621ca99128e25cb/coincurve_cp314_fix-22.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:56ddf3b2340c23c22fe16fbf171e18219de291cf2c26f75805c92640ec5ef8c8", size = 1334976, upload-time = "2026-07-17T14:49:07.092Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e7/9765c390984195f79c6b0d3ab299cc17749f57a9856651db30065cc66264/coincurve_cp314_fix-22.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:acfcd79081fc04e4f699731973c41165dfeee85b855dc352ee2d46250dd46f6e", size = 1327417, upload-time = "2026-07-17T14:49:08.695Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, + { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, +] + +[[package]] +name = "ephemeral-port-reserve" +version = "1.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/93/3f0b75f75f94227f67ccfe86f989415e40ac054dd67b55dfac7abdc0a2d2/ephemeral_port_reserve-1.1.4.tar.gz", hash = "sha256:b8f7da2c97090cb0801949dec1d6d40c97220505b742a70935ffbd43234c14b2", size = 3541, upload-time = "2021-02-11T17:18:58.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/2f/28948390896745afd561dad6a39daf49f74637b7b3398582a649b5acc4c5/ephemeral_port_reserve-1.1.4-py2.py3-none-any.whl", hash = "sha256:dae8da99422c643bb52478ed55d5a8428099092391656ba3726ff30c801600c8", size = 4448, upload-time = "2021-02-11T17:18:57.782Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mnemonic" +version = "0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/77/e6232ed59fbd7b90208bb8d4f89ed5aabcf30a524bc2fb8f0dafbe8e7df9/mnemonic-0.21.tar.gz", hash = "sha256:1fe496356820984f45559b1540c80ff10de448368929b9c60a2b55744cc88acf", size = 152462, upload-time = "2024-01-05T10:46:14.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/48/5abb16ce7f9d97b728e6b97c704ceaa614362e0847651f379ed0511942a0/mnemonic-0.21-py3-none-any.whl", hash = "sha256:72dc9de16ec5ef47287237b9b6943da11647a03fe7cf1f139fc3d7c4a7439288", size = 92701, upload-time = "2024-01-05T10:46:12.703Z" }, +] + +[[package]] +name = "more-itertools" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, +] + +[[package]] +name = "nostr-sdk" +version = "0.45.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/73/9401a17b0a43bf22542f31e5c5624db7f838f7690977ec786edd72b0880b/nostr_sdk-0.45.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5bd1a6b972e9f5ae8c4f4448a148f668f124a3389b72eafb9de00f714e829059", size = 3835872, upload-time = "2026-08-05T13:14:21.43Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5d/355adf2a676493101357080857cf7896880c6556fe84795281bc283513c5/nostr_sdk-0.45.0-cp39-abi3-macosx_11_0_x86_64.whl", hash = "sha256:a20cb90f4fa225e57118431fc05d1d567a78a047dab54d34897365fcc5b82498", size = 3901388, upload-time = "2026-08-05T13:14:22.92Z" }, + { url = "https://files.pythonhosted.org/packages/a1/72/489cd53d491afe180f71d24c4f4f334f6da38d1bf683e8907771f436bffd/nostr_sdk-0.45.0-cp39-abi3-manylinux_2_17_aarch64.whl", hash = "sha256:00da5e7e387515d1e12779eccfb2c794856f774b4534694200434aa0146a03ed", size = 4323888, upload-time = "2026-08-05T13:14:24.348Z" }, + { url = "https://files.pythonhosted.org/packages/7e/cb/9a5edbea9db9995704ebc44ec4fca153c5f7d9ea07d668cf7837f01ddf1e/nostr_sdk-0.45.0-cp39-abi3-manylinux_2_17_armv7l.whl", hash = "sha256:e5cae5ef56411d6665de78c39822a4e80382231d44b4a6fc3db096b8b203ec26", size = 3867750, upload-time = "2026-08-05T13:14:26.066Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d3/1a464004596e9ea0366a62e1671fc41adefde97b477bf60e69615dedec64/nostr_sdk-0.45.0-cp39-abi3-manylinux_2_17_i686.whl", hash = "sha256:9d91d519a93c48ab4d689f975f406d8ee96e39d6876d2aeaaee58de7e66ded93", size = 4393993, upload-time = "2026-08-05T13:14:27.766Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/1684e67051af3540666e6aeba4ed5bc9ea027cbe311789dce79863c85b48/nostr_sdk-0.45.0-cp39-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:8152945cb27b48bbf6f89d73d8560bfbadd202b8e5c165bf5b5532e91feed417", size = 4426295, upload-time = "2026-08-05T13:14:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/34/b0/526e134d23e2bdbac523bb38351d940a478cc2d7499c0ee9d1ee499ecfb4/nostr_sdk-0.45.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a7e0a886b427356dc94cd0b1bc8421b01eeb5aa9ea26e1b69c21f0a3cc1bc2ad", size = 4336015, upload-time = "2026-08-05T13:14:30.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/27/764af3a4bc1ec8542401616f11c41bbf9c42fbaef8659bf26bab4097ed6f/nostr_sdk-0.45.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3b314b5160014bf70b39e95b3cf5cb059050e2fc2f782a0d9916c7ae1559e34f", size = 3864414, upload-time = "2026-08-05T13:14:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/bd/31/1289150239dfce59c1c2afb8313dfd21411040103c68e6121f1ba4515fb7/nostr_sdk-0.45.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:15346b498f4df4bb42ef2987cecd9e97ba60294b9bc7bb486e1c4b6bee4f528e", size = 4205457, upload-time = "2026-08-05T13:14:33.858Z" }, + { url = "https://files.pythonhosted.org/packages/8b/63/3964d86ed2f36f691374813751fb342de0c169ea0d926609379e50fd761c/nostr_sdk-0.45.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:78ccdb4766ae627ac9e2c10a7bb224d1e00da48fc7b61c7f48935ae23241fd64", size = 4410510, upload-time = "2026-08-05T13:14:35.224Z" }, + { url = "https://files.pythonhosted.org/packages/0d/15/a9c8da6d37bbbbfe07cba889a3d149f030985e524758ba51b44ca7a29ca0/nostr_sdk-0.45.0-cp39-abi3-win32.whl", hash = "sha256:45ff1ae2f19fedfa9f91b0c81c1be755b706e2fd1e7865ea46612c2ad1d98a3e", size = 3549588, upload-time = "2026-08-05T13:14:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/99/f0/e03b7abb4dc016ea19dc2f311f367e472df47c187fbb69cee6df3ed2f323/nostr_sdk-0.45.0-cp39-abi3-win_amd64.whl", hash = "sha256:b3904abc4d7e4ac46ed66c8aa528e89748f3f25023708e4bbab58bc81d3a99f7", size = 3870530, upload-time = "2026-08-05T13:14:38.079Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d0/f80080bd2aa73ed25c7a9d8d21020fc73eaa10591b39e13ec2b88228a8ce/nostr_sdk-0.45.0-cp39-abi3-win_arm64.whl", hash = "sha256:5a0fc63b69995bf2dac44e479aaec1ce1af71e687c9f4258891e429ed3db25e5", size = 3704095, upload-time = "2026-08-05T13:14:39.494Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/80/49bacf9e51617d8309f6f0123e29edc793f6f5f6700c7d1f1b20782fbb37/psycopg2_binary-2.9.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b818ceff717f98851a64bffd4c5eb5b3059ae280276dcecc52ac658dcf006a4", size = 3712314, upload-time = "2026-04-20T23:33:31.363Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/98eeac7d60c43df9338287834edf9b3e69be68a2db78a57b1b81d705e735/psycopg2_binary-2.9.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fa0d7caca8635c56e373055094eeda3208d901d55dd0ff5abc1d4e47f82b56", size = 3822389, upload-time = "2026-04-20T23:33:34.178Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7c/30575e75f14d5351a56a1971bb43fe7f8bf7edf1b654fb1bec65c42a8812/psycopg2_binary-2.9.12-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:864c261b3690e1207d14bbfe0a61e27567981b80c47a778561e49f676f7ce433", size = 4578448, upload-time = "2026-04-20T23:33:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/4df366d89f28c527dc39d0b6c98a5ca74e30d37ac097b73f3352147568ae/psycopg2_binary-2.9.12-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c5ee5213445dd45312459029b8c4c0a695461eb517b753d2582315bd07995f5e", size = 4273705, upload-time = "2026-04-20T23:33:39.291Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/c566803818eb03161ba869b6ba612bf7ad56816d98b9e5121e0a22ad6b0b/psycopg2_binary-2.9.12-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f9cae1f848779b5b01f417e762c40d026ea93eb0648249a604728cda991dde3", size = 5893784, upload-time = "2026-04-20T23:33:41.658Z" }, + { url = "https://files.pythonhosted.org/packages/63/fe/0dfa5797e0b229e0567bc378695224caf14d547f73b05be0c80549089772/psycopg2_binary-2.9.12-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:63a3ebbd543d3d1eda088ac99164e8c5bac15293ee91f20281fd17d050aee1c4", size = 4109306, upload-time = "2026-04-20T23:33:43.953Z" }, + { url = "https://files.pythonhosted.org/packages/3c/89/28063adf17a4ba501eedd9890feab0c649ee4d8bd0a97df0ff1e9584feab/psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d6fcbba8c9fed08a73b8ac61ea79e4821e45b1e92bb466230c5e746bbf3d5256", size = 3654400, upload-time = "2026-04-20T23:33:46.115Z" }, + { url = "https://files.pythonhosted.org/packages/84/94/5a01de0aa4ead0b8d8d1aa4ec18cec0bd36d03fa714eaa5bb8a0b1b50020/psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:36512911ebb2b60a0c3e44d0bb5048c1980aced91235d133b7874f3d1d93487c", size = 3299215, upload-time = "2026-04-20T23:33:48.202Z" }, + { url = "https://files.pythonhosted.org/packages/7a/85/723bb085a61c6ac2dc0a0043f375f2fe7365363e27b073bad56ca5bda979/psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:8ffdb59fe88f99589e34354a130217aa1fd2d615612402d6edc8b3dbc7a44463", size = 3047724, upload-time = "2026-04-20T23:33:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/b4/67/4d8b1e0d2fc4166677380eac0edf9cdff91013aca2546e8ef7bc04b56158/psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a46fe069b65255df410f856d842bc235f90e22ffdf532dda625fd4213d3fd9b1", size = 3349183, upload-time = "2026-04-20T23:33:59.635Z" }, + { url = "https://files.pythonhosted.org/packages/73/99/21af7a5498637ea4dc91a17c281a53bc1d632fbafe00f6689fbfb32a9fed/psycopg2_binary-2.9.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab29414b25dcb698bf26bf213e3348abdcd07bbd5de032a5bec15bd75b298b03", size = 2757036, upload-time = "2026-04-20T23:34:01.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/d4ce60954f3bb9d8e3bc5e5c4d1f2487de2d3851bf2391d54954c9df12a6/psycopg2_binary-2.9.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b", size = 3712338, upload-time = "2026-04-20T23:34:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/53/71/c85409ee0d78890f0660eff262e815e7dd2bb741a17611d82e9e8cd9dc5e/psycopg2_binary-2.9.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", size = 3822407, upload-time = "2026-04-20T23:34:05.977Z" }, + { url = "https://files.pythonhosted.org/packages/3c/ed/60486c2c7f0d4d1ede2bfb1ed27e2498477ce646bc7f6b2759906303117e/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", size = 4578425, upload-time = "2026-04-20T23:34:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b9/656cb03fad9f4f49f2145c334b1126ee75189929ca4e6187d485a2d59951/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", size = 4273709, upload-time = "2026-04-20T23:34:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/99/66/08cf0da0e25cc6fb142c89be45fc8418792858f0c4cbff5e24530ff02cd6/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", size = 5893779, upload-time = "2026-04-20T23:34:13.905Z" }, + { url = "https://files.pythonhosted.org/packages/17/d7/eecd9ce8e146d3721115d82d3836efdbb712187e4590325df549989d18f4/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", size = 4109308, upload-time = "2026-04-20T23:34:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/b1dc289b362cc8d45697b57eefbd673186f49a4ea0906928988e3affcc98/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", size = 3654405, upload-time = "2026-04-20T23:34:19.303Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/4c4aea6473214dbdbd0fbba11aa4691e76dc01722c55724c5951719865ff/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", size = 3299187, upload-time = "2026-04-20T23:34:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5d/b03b99986446a4f57b170ed9a2579fb7ff9783ca0fa5226b19db99737fee/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", size = 3047716, upload-time = "2026-04-20T23:34:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/14/86/382ee4afbd1d97500c9d2862b20c2fdeddf4b7335e984df3fb4309f64108/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", size = 3349237, upload-time = "2026-04-20T23:34:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/a8/16/9a57c75ba1eda7165c017342f526810d5f5a12647dde749c99ae9a7141d7/psycopg2_binary-2.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936", size = 2757036, upload-time = "2026-04-20T23:34:27.77Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" }, + { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" }, + { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" }, + { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" }, + { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" }, + { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" }, + { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" }, + { url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" }, + { url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" }, + { url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyln-bolt7" +version = "1.0.246" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/3c/6d1d6643c6a501e998c17c25d40c3b68ab75be1d16d9055e6a9ffba30fe4/pyln-bolt7-1.0.246.tar.gz", hash = "sha256:2b53744fa21c1b12d2c9c9df153651b122e38fa65d4a5c3f2957317ee148e089", size = 17905, upload-time = "2022-08-31T17:28:20.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/f1/30b626e7cec279a2f84084898594c8e84537d6d9af9afbe9858f9a6d8e13/pyln_bolt7-1.0.246-py3-none-any.whl", hash = "sha256:54d48ec27fdc8751762cb068b0a9f2757a58fb57933c6d8f8255d02c27eb63c5", size = 18811, upload-time = "2022-08-31T17:28:22.137Z" }, +] + +[[package]] +name = "pyln-client" +version = "26.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyln-bolt7" }, + { name = "pyln-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/5f/7e52fbe6b6d7d65651d2c908e1f68449256b3007a8259fa589282e9a4c0f/pyln_client-26.6.6.tar.gz", hash = "sha256:01c02021a62347d4d7dbaaa97bb627e9c2b60212a2e146107a782c1c156588ba", size = 92480, upload-time = "2026-07-20T12:39:51.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/c0/2efddeec3dda9c915b02fc406bcc1f179d8c45cc9a2e8089608cbdde1a0e/pyln_client-26.6.6-py3-none-any.whl", hash = "sha256:6afa9019aef1e54fa01e185be160773ac4db2fdc7cf864a319e7374b71cd6a6d", size = 37975, upload-time = "2026-07-20T12:39:50.024Z" }, +] + +[[package]] +name = "pyln-proto" +version = "26.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "base58" }, + { name = "bitstring" }, + { name = "coincurve-cp314-fix" }, + { name = "cryptography" }, + { name = "pysocks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/97/219900f708391678165461e6b519c6272edb5966b2e20c62856e9c1f736a/pyln_proto-26.6.6.tar.gz", hash = "sha256:b6d424eb22d33de9fc21e4216502759e34a62eaf460a47cc45dbbb208096db8a", size = 44879, upload-time = "2026-07-20T12:40:07.68Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/18/ed5d323f55350b175e5f8bbed7b0c8acb988eb48ee37dd5b44d9e7a41799/pyln_proto-26.6.6-py3-none-any.whl", hash = "sha256:b7b12769e2c37e6d1ce7c2063bbab67f5959e411459ff089d0dbc14109778e03", size = 31833, upload-time = "2026-07-20T12:40:06.677Z" }, +] + +[[package]] +name = "pyln-testing" +version = "26.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cheroot" }, + { name = "ephemeral-port-reserve" }, + { name = "flask" }, + { name = "jsonschema" }, + { name = "mnemonic" }, + { name = "psutil" }, + { name = "psycopg2-binary" }, + { name = "pyln-client" }, + { name = "pytest" }, + { name = "python-bitcoinlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/86/49b10e9317142f989dc03c7d42d512789a83000da7660177393a9d7e681b/pyln_testing-26.6.6.tar.gz", hash = "sha256:31caa503724d02bb736d8b1e11cd344cff15e0229bc6f709bd1571df5066b432", size = 53213, upload-time = "2026-07-20T12:39:37.337Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/28/58c3524d0d1c1e244580bd82730f7c0e72b60e5a52b0d34c4395e7f46a1d/pyln_testing-26.6.6-py3-none-any.whl", hash = "sha256:24b814f6977faa1e66f98a6a447b397d148ba683c53a73e595a429c53a88af78", size = 54976, upload-time = "2026-07-20T12:39:36.32Z" }, +] + +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-bitcoinlib" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/a1/ca9d770094a0bfd0f2ce66b7180f0a7729b1b646d90f37f6debf38b28fab/python-bitcoinlib-0.12.2.tar.gz", hash = "sha256:c65ab61427c77c38d397bfc431f71d86fd355b453a536496ec3fcb41bd10087d", size = 153798, upload-time = "2023-06-03T18:37:11.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/46/b388d0885cf799f5bc66ca9995c83e5b7e17cb737f812a7c2591aa789ea6/python_bitcoinlib-0.12.2-py3-none-any.whl", hash = "sha256:2f29a9f475f21c12169b3a6cc8820f34f11362d7ff1200a5703dce3e4e903a44", size = 106954, upload-time = "2023-06-03T18:37:08.524Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, +] + +[[package]] +name = "tibs" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/cd/6cf028decf1c2df4d26077dd5d0532587d93d4917233d5e004133166a940/tibs-0.5.7.tar.gz", hash = "sha256:173dfbecb2309edd9771f453580c88cf251e775613461566b23dbd756b3d54cb", size = 78255, upload-time = "2026-03-12T13:06:29.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/4f/1149a5cf2c1be6862e1dcba0c22134c43c44f05ddeef4697ecf20067e508/tibs-0.5.7-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:01ea5258bdf942d21560dc07d532082cd04f07cfef65fedd58ae84f7d0d2562a", size = 401281, upload-time = "2026-03-12T13:06:25.78Z" }, + { url = "https://files.pythonhosted.org/packages/eb/af/59041580d51eb06077029cc64f0b2f9165b1c87075b7fe85f400e01ec6f9/tibs-0.5.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f5eea45851c960628a2bd29847765d55e19a687c5374456ad2c8cf6410eb1efa", size = 377945, upload-time = "2026-03-12T13:06:42.493Z" }, + { url = "https://files.pythonhosted.org/packages/ee/73/3b614d39221f02fca2f37dcdc1c65e25c963bf1da4b90ad9db393f9c130d/tibs-0.5.7-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a9feed5931b881809a950eca0e01e757113e2383a2af06a3e6982f110c869e2", size = 409620, upload-time = "2026-03-12T13:06:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/16/a6/917ca6ca266135f0f52041700c4eb766097258dd987b81a630c061969db5/tibs-0.5.7-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:501728d096e10d9a165aa526743d47418a6bbfd7b084fa47ecb22be7641d3edb", size = 426017, upload-time = "2026-03-12T13:06:40.139Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f6/3c795420f81bac44390d897712aebe186186d88ea5653e20f4ac5097b0b1/tibs-0.5.7-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77103a9f1af72ac4cf5006828d0fb21578d19ce55fd990e9a1c8e46fd549561f", size = 449717, upload-time = "2026-03-12T13:06:45.416Z" }, + { url = "https://files.pythonhosted.org/packages/98/00/700b97377b55973ac233a280d6ff81c0187710c73a5ac3356ef79bf15eb2/tibs-0.5.7-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f95d5db62960205a1e9eba73ce67dc14e7366ae080cd4e5b6f005ebd90faf02", size = 453131, upload-time = "2026-03-12T13:06:46.623Z" }, + { url = "https://files.pythonhosted.org/packages/6d/41/38ccfe6fe48432ea20f6e6a49a42aeb9662042e5f4e8f9a4029047a6c44a/tibs-0.5.7-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace018a057459e3dccd06a4aae1c5c8cd57e352b263dcef534ae39bf3e03b5cf", size = 419054, upload-time = "2026-03-12T13:06:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/73/08/d9a66639564b92d5be07eb30bbd7a5b9052f338da09fd4ec3732346ff129/tibs-0.5.7-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2a618de62004d9217d2d2ab0f7f9bbdd098c12642dc01f07b3fb00f0b5f3131a", size = 448585, upload-time = "2026-03-12T13:06:33.306Z" }, + { url = "https://files.pythonhosted.org/packages/70/c1/24131985486d5bf878468226d9d0bdff5a0b04838b773a7339d22965f74e/tibs-0.5.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42725200f1b02687ed6e6a1c01e0ec150dc829d21d901ffc74cc0ac4d821f57f", size = 586259, upload-time = "2026-03-12T13:06:14.095Z" }, + { url = "https://files.pythonhosted.org/packages/02/0c/f74c6672d28054c55b6c593588792858be420dbf4b56d0adbf79fc1b7f8f/tibs-0.5.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:63255749f937c5e6fedcc7d54e7bd359aef711017e6855f373b0510a14ee2215", size = 701427, upload-time = "2026-03-12T13:06:37.234Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/2c39836a5a1664cda596ba069d065322976245a5f86dab9f2b9a3eaff024/tibs-0.5.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4b7510235379368b7523f624d46e0680f3706e3a3965877a6583cdcb598b8bac", size = 660754, upload-time = "2026-03-12T13:06:38.67Z" }, + { url = "https://files.pythonhosted.org/packages/1d/77/5a7a10001c38f4d1266d4f7a84fae27357c88834a0266bc401e37e1a7884/tibs-0.5.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29480bf03e3372a5f9cc59ea0541f76f8efd696d4f0d214715e94247c342a037", size = 631034, upload-time = "2026-03-12T13:06:18.1Z" }, + { url = "https://files.pythonhosted.org/packages/0d/be/bb20938ab5d1e63ee4c5cf78be815ab2a8674e7aa0b2500db210f7db3e6d/tibs-0.5.7-cp314-cp314t-win32.whl", hash = "sha256:b9535dc7b7484904a58b51bd8e64da7efbf1d8466ff7e84ed1d78f4ddc561c99", size = 278952, upload-time = "2026-03-12T13:06:20.285Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9a/e76888e8567dbe02a67a27d46e5acf06e3504df1268ebc6d8313942ec560/tibs-0.5.7-cp314-cp314t-win_amd64.whl", hash = "sha256:1906729038b85c3b4c040aa28a456d85bc976d0c5007177350eb73374ffa0fd0", size = 294069, upload-time = "2026-03-12T13:06:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/10/37/f74a5f4288984cb909dbccd4cc254154f3ed97b16db1913406f1bd2914c9/tibs-0.5.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7d6592ed93c6748acd39df484c1ee24d40ee247c2a20ca38ba03363506fd24f3", size = 278929, upload-time = "2026-03-12T13:06:43.962Z" }, + { url = "https://files.pythonhosted.org/packages/12/2d/de2c579d3eea0f18212b5b16decb04568b7a0ef912d00581a77492609d4e/tibs-0.5.7-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:859f05315ffb307d3474c505d694f3a547f00730a024c982f5f60316a5505b3c", size = 411352, upload-time = "2026-03-12T13:06:52.016Z" }, + { url = "https://files.pythonhosted.org/packages/74/71/4c21ccc5c2e1672f9cd91ed2c46604c250cffd9d386113772dded128b5cf/tibs-0.5.7-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:a883ca13a922a66b2c1326a9c188123a574741a72510a4bf52fd6f97db191e44", size = 383971, upload-time = "2026-03-12T13:06:50.143Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/399940ac5393772792a209911a5efa42cf55cf621771e48b863211ac5a2a/tibs-0.5.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f70bd250769381c73110d6f24feaf8b6fcd44f680b3cb28a20ea06db3d04fb6f", size = 416256, upload-time = "2026-03-12T13:06:24.222Z" }, + { url = "https://files.pythonhosted.org/packages/02/94/481a73e74d398949f57d297b1809a10a951d252e7ec94b6715ed952ce500/tibs-0.5.7-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76746f01b3db9dbd802f5e615f11f68df7a29ecef521b082dca53f3fa7d0084f", size = 428003, upload-time = "2026-03-12T13:06:23.064Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e0/72db1760a7f7fec1d5f3690e0855fbbccbcf0a4a2fd318c9d71f3b33f3a7/tibs-0.5.7-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:847709c108800ad6a45efaf9a040628278956938a4897f7427a2587013dc3b98", size = 455589, upload-time = "2026-03-12T13:06:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/3e/26/9cd3395914bf705d6ae1e9a6c323f727e9dc88fef716327ce7f486e0b55a/tibs-0.5.7-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad61df93b50f875b277ab736c5d37b6bce56f9abce489a22f4e02d9daa2966e3", size = 459266, upload-time = "2026-03-12T13:06:21.678Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3b/267f19a008d13c704dc0b044138a56239272a43531ccb05464129d0fbd01/tibs-0.5.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e13b9c7ff2604b0146772025e1ac6f85c8c625bf6ac73736ff671eaf357dda41", size = 423466, upload-time = "2026-03-12T13:06:41.212Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d4/424ae3515e0e013ad83186074bf3beb53399b9052c00da703415ccc316ca/tibs-0.5.7-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a7ce857ef05c59dc61abadc31c4b9b1e3c62f9e5fb29217988c308936aea71e", size = 452080, upload-time = "2026-03-12T13:06:32.112Z" }, + { url = "https://files.pythonhosted.org/packages/b0/15/ab80beba83a134745439d33763e1d3b017f994abeb9c309a3ac9fd94e90e/tibs-0.5.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1d5521cc6768bfa6282a0c591ba06b079ab91b5c7d5696925ad2abac59779a54", size = 592311, upload-time = "2026-03-12T13:06:47.807Z" }, + { url = "https://files.pythonhosted.org/packages/4c/21/f5cf41c15431e63aeaefb494e714d48d9e9061b4e01fcc01d1987e2e5faa/tibs-0.5.7-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:477608f9b87e24a22ab6d50b81da04a5cb59bfa49598ff7ec5165035a18fb392", size = 703400, upload-time = "2026-03-12T13:06:16.968Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ec/b3bdb7dcc3de8513c5678a685f4e25bb85ef48526d7d535ddc592f9e8602/tibs-0.5.7-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:ac0aa2aae38f7325c91c261ce1d18f769c4c7033c98d6ea3ea5534585cf16452", size = 664623, upload-time = "2026-03-12T13:06:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/a4/71/7b85af3ad1b2cd9871c8f50ba0eb17e54e12481b467678535e58aced0d98/tibs-0.5.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b56583db148e5094d781c3d746815dbcbb6378c6f813c8ce291efd4ab21da8b", size = 635199, upload-time = "2026-03-12T13:06:34.798Z" }, + { url = "https://files.pythonhosted.org/packages/b9/63/60220fb502beb857306afd4a5bac4a8617ae496f3b1f4968d127380fdefe/tibs-0.5.7-cp38-abi3-win32.whl", hash = "sha256:d4f3ff613d486650816bc5516760c0382a2cc0ca8aeddd8914d011bc3b81d9a2", size = 288454, upload-time = "2026-03-12T13:06:30.978Z" }, + { url = "https://files.pythonhosted.org/packages/46/ab/aab78827ba7e0d65fe346b86d1d61e0792c38d5f9b7547e0f71b7027c835/tibs-0.5.7-cp38-abi3-win_amd64.whl", hash = "sha256:a61d36155f8ab8642e1b6744e13822f72050fc7ec4f86ec6965295afa04949e2", size = 304135, upload-time = "2026-03-12T13:06:35.884Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/e9e6a610928a4bcbf04f0ac1436ee320aa8cbe95181f1aa32687c50e858b/tibs-0.5.7-cp38-abi3-win_arm64.whl", hash = "sha256:130bc68ff500fc8185677df7a97350b5d5339e6ba7e325bc3031337f6424ede7", size = 289272, upload-time = "2026-03-12T13:06:19.247Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +]
- - + + - - + +
- - + + - - + +
- - + + - - + +
- - + + - - + +