diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 0f07734..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,8 +0,0 @@ -[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 deleted file mode 100644 index 083ac61..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,37 +0,0 @@ -# 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 3b0ca26..ddffd74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,12 +17,6 @@ on: tagged-release: required: true type: boolean - python-version-1: - required: true - type: string - python-version-2: - required: true - type: string jobs: build: @@ -30,25 +24,27 @@ jobs: strategy: fail-fast: false matrix: - bitcoind-version: ["31.0"] + bitcoind-version: ["29.0"] experimental: [1] deprecated: [0] - python-version: [ "${{ inputs.python-version-1 }}", "${{ inputs.python-version-2 }}"] - os: ["ubuntu-latest"] + python-version: ["3.9", "3.12"] + os: ["ubuntu-24.04"] runs-on: ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v7 + uses: actions/checkout@v4 - name: Create cache paths run: | - sudo mkdir -p /usr/local/libexec/c-lightning/plugins + sudo mkdir /usr/local/libexec + sudo mkdir /usr/local/libexec/c-lightning + sudo mkdir /usr/local/libexec/c-lightning/plugins sudo chown -R $USER /usr/local/libexec - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v7 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -62,104 +58,120 @@ jobs: echo "OS version: $OS_VERSION" echo "os_version=$OS_VERSION" >> $GITHUB_OUTPUT - - name: Restore CLN cache + - name: Cache CLN id: cache-cln - uses: actions/cache/restore@v6 + uses: actions/cache@v4 with: path: | /usr/local/bin/lightning* /usr/local/libexec/c-lightning key: cache-cln-${{ inputs.cln-version }}-${{ steps.exact_versions.outputs.os_version }} - - name: Restore bitcoind cache + - name: Cache bitcoind id: cache-bitcoind - uses: actions/cache/restore@v6 + uses: actions/cache@v4 with: path: /usr/local/bin/bitcoin* key: cache-bitcoind-${{ matrix.bitcoind-version }}-${{ steps.exact_versions.outputs.os_version }} - - name: Install uv - uses: astral-sh/setup-uv@v7 + - name: Cache python dependencies + id: cache-python + uses: actions/cache@v4 with: - enable-cache: true - cache-dependency-glob: "**/pyproject.toml" + path: venv + key: cache-python-${{ steps.exact_versions.outputs.python_version }}-${{ steps.exact_versions.outputs.os_version }}-${{ inputs.pyln-version }}-${{ hashFiles('tests/requirements.txt') }} - name: Download Bitcoin ${{ matrix.bitcoind-version }} & install binaries if: ${{ steps.cache-bitcoind.outputs.cache-hit != 'true' }} run: | export BITCOIND_VERSION=${{ matrix.bitcoind-version }} - 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 + 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 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 --tries=5 --waitretry=5 --retry-connrefused --timeout=20 --continue $url + wget $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: Install cargo-binstall - if: ${{ inputs.tagged-release == false }} - uses: taiki-e/install-action@cargo-binstall + - 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-msrv - if: ${{ inputs.tagged-release == false }} - env: - GITHUB_TOKEN: ${{ github.token }} - run: cargo binstall --no-confirm cargo-msrv + - 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: Verify MSRV - if: ${{ inputs.tagged-release == false }} - run: cargo msrv verify + - 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: Get plugin binary run: | + source venv/bin/activate if ${{ inputs.tagged-release }}; then cd tests ./setup.sh cd .. else if [ -d "proto" ]; then - uv run python -m grpc_tools.protoc --proto_path="proto" --python_out="tests" --grpc_python_out="tests" proto/*.proto + 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 @@ -172,8 +184,5 @@ jobs: export TRAVIS=1 export VALGRIND=0 export PYTEST_TIMEOUT=600 - - 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 + source venv/bin/activate + pytest -n=5 tests/test_*.py diff --git a/.github/workflows/latest_v24.08.yml b/.github/workflows/latest_v24.08.yml new file mode 100644 index 0000000..1db4db9 --- /dev/null +++ b/.github/workflows/latest_v24.08.yml @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..7174254 --- /dev/null +++ b/.github/workflows/latest_v24.11.yml @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..6d8590c --- /dev/null +++ b/.github/workflows/latest_v25.02.yml @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..b5502c9 --- /dev/null +++ b/.github/workflows/latest_v25.05.yml @@ -0,0 +1,14 @@ +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 deleted file mode 100644 index d96d48d..0000000 --- a/.github/workflows/latest_v25.09.yml +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 3a86b18..0000000 --- a/.github/workflows/latest_v25.12.yml +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index a0dc3a0..0000000 --- a/.github/workflows/latest_v26.04.yml +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 27edc2d..0000000 --- a/.github/workflows/latest_v26.06.yml +++ /dev/null @@ -1,16 +0,0 @@ -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/master_v26.04.yml b/.github/workflows/main_v24.08.yml similarity index 61% rename from .github/workflows/master_v26.04.yml rename to .github/workflows/main_v24.08.yml index 82715be..1036fbb 100644 --- a/.github/workflows/master_v26.04.yml +++ b/.github/workflows/main_v24.08.yml @@ -1,9 +1,9 @@ -name: master on CLN v26.04.x +name: main on CLN v24.08.x on: push: branches: - - master + - main paths-ignore: - 'tools/**' - 'CHANGELOG.md' @@ -19,8 +19,6 @@ jobs: call-ci: uses: ./.github/workflows/ci.yml with: - cln-version: "v26.04" - pyln-version: "26.4" - tagged-release: false - python-version-1: "3.10" - python-version-2: "3.12" + cln-version: "v24.08.2" + pyln-version: "24.8.2" + tagged-release: false \ No newline at end of file diff --git a/.github/workflows/master_v25.09.yml b/.github/workflows/main_v24.11.yml similarity index 61% rename from .github/workflows/master_v25.09.yml rename to .github/workflows/main_v24.11.yml index 1a73ecd..7078bd5 100644 --- a/.github/workflows/master_v25.09.yml +++ b/.github/workflows/main_v24.11.yml @@ -1,9 +1,9 @@ -name: master on CLN v25.09.x +name: main on CLN v24.11.x on: push: branches: - - master + - main paths-ignore: - 'tools/**' - 'CHANGELOG.md' @@ -19,8 +19,6 @@ jobs: call-ci: uses: ./.github/workflows/ci.yml with: - cln-version: "v25.09.3" - pyln-version: "25.9.3" - tagged-release: false - python-version-1: "3.10" - python-version-2: "3.12" + cln-version: "v24.11.2" + pyln-version: "24.11.1" + tagged-release: false \ No newline at end of file diff --git a/.github/workflows/master_v26.06.yml b/.github/workflows/main_v25.02.yml similarity index 61% rename from .github/workflows/master_v26.06.yml rename to .github/workflows/main_v25.02.yml index 25fe3a8..1136903 100644 --- a/.github/workflows/master_v26.06.yml +++ b/.github/workflows/main_v25.02.yml @@ -1,9 +1,9 @@ -name: master on CLN v26.06.x +name: main on CLN v25.02.x on: push: branches: - - master + - main paths-ignore: - 'tools/**' - 'CHANGELOG.md' @@ -19,8 +19,6 @@ jobs: call-ci: uses: ./.github/workflows/ci.yml with: - cln-version: "v26.06.6" - pyln-version: "26.6.6" - tagged-release: false - python-version-1: "3.10" - python-version-2: "3.14" + cln-version: "v25.02.1" + pyln-version: "25.2.1" + tagged-release: false \ No newline at end of file diff --git a/.github/workflows/master_v25.12.yml b/.github/workflows/main_v25.05.yml similarity index 61% rename from .github/workflows/master_v25.12.yml rename to .github/workflows/main_v25.05.yml index 7e0ae62..e99fd3f 100644 --- a/.github/workflows/master_v25.12.yml +++ b/.github/workflows/main_v25.05.yml @@ -1,9 +1,9 @@ -name: master on CLN v25.12.x +name: main on CLN v25.05.x on: push: branches: - - master + - main paths-ignore: - 'tools/**' - 'CHANGELOG.md' @@ -19,8 +19,6 @@ jobs: call-ci: uses: ./.github/workflows/ci.yml with: - cln-version: "v25.12.1" - pyln-version: "25.12.1" - tagged-release: false - python-version-1: "3.10" - python-version-2: "3.12" + cln-version: "v25.05" + pyln-version: "25.5" + tagged-release: false \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 39f2ae2..a4fbf8d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,129 +5,77 @@ on: - 'v*' jobs: - 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 + 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 }} release: name: Github Release - needs: [build-linux, build-macos] - runs-on: "ubuntu-latest" + needs: [build] + runs-on: "ubuntu-24.04" permissions: contents: write steps: @@ -135,7 +83,7 @@ jobs: id: tag_name run: echo "current_version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" - name: Checkout code - uses: actions/checkout@v7 + uses: actions/checkout@v4 - name: Get Changelog Entry id: changelog_reader uses: mindsers/changelog-reader-action@v2 @@ -144,7 +92,7 @@ jobs: version: ${{ steps.tag_name.outputs.current_version }} path: ./CHANGELOG.md - name: Download Artifacts - uses: actions/download-artifact@v8 + uses: actions/download-artifact@v4 with: merge-multiple: true - name: Release @@ -152,6 +100,5 @@ 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-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 + 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" diff --git a/.gitignore b/.gitignore index 9f63775..026fddc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,4 @@ /tests/__pycache__/ /venv/ /result/ -/tests/holdinvoice -/tests/hold -/tests/nostr-rs-relay +/tests/holdinvoice \ No newline at end of file diff --git a/.rustfmt.toml b/.rustfmt.toml deleted file mode 100644 index 9e2662a..0000000 --- a/.rustfmt.toml +++ /dev/null @@ -1,4 +0,0 @@ -imports_granularity = "Crate" -group_imports = "StdExternalCrate" -imports_layout = "HorizontalVertical" -unstable_features = true diff --git a/CHANGELOG.md b/CHANGELOG.md index e77df0d..96f3e59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,66 +1,5 @@ # 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 a6393f7..c3b18e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,21 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +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" [[package]] name = "aead" @@ -25,41 +40,30 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.5" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anyhow" -version = "1.0.104" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arrayvec" -version = "0.7.8" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -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", -] +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-utility" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "188f83b9a198af8c336e505611edb00d6d2ac5c694241c5a4f9a12316938cfe9" +checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151" dependencies = [ "futures-util", "gloo-timers", @@ -69,37 +73,48 @@ dependencies = [ [[package]] name = "async-wsocket" -version = "0.17.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c713e1f14c7b82e32ea159af1c6e2f070cfadbdf23fb2512acce9af0a26f1a2" +checksum = "9a7d8c7d34a225ba919dd9ba44d4b9106d20142da545e086be8ae21d1897e043" 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-waker" -version = "1.1.2" +name = "atomic-destructor" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" [[package]] -name = "base58ck" -version = "0.1.101" +name = "autocfg" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "365c0acd5b2e8dd0111a46c4faea83fb3cfb6e39a49a7c73a06e090db7b2eff0" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ - "bitcoin_hashes 0.14.101", + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", ] [[package]] @@ -109,109 +124,91 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bech32" -version = "0.11.1" +name = "base64ct" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bech32" -version = "0.12.0" +version = "0.10.0-beta" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbd3e1070bbdf4cd88a75264e18e8a26f7cb5c6949eadf0ceb85fb159cf08f8" +checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" + +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" [[package]] name = "bip39" -version = "2.2.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054" dependencies = [ - "bitcoin_hashes 0.14.101", + "bitcoin_hashes 0.13.0", "serde", "unicode-normalization", ] [[package]] name = "bitcoin" -version = "0.32.102" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0ce8bd5baaa0d303a19915a6d93afed161f528654e42da2a7a97d05c59499a" -dependencies = [ - "base58ck", - "bech32 0.11.1", - "bitcoin-io", - "bitcoin-units", - "bitcoin_hashes 0.14.101", - "hex-conservative 0.2.2", - "hex_lit", - "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" +checksum = "6c85783c2fe40083ea54a33aa2f0ba58831d90fcd190f5bdc47e74e84d2a96ae" dependencies = [ + "bech32 0.10.0-beta", "bitcoin-internals", - "hex-conservative 1.2.0", + "bitcoin_hashes 0.13.0", + "hex-conservative 0.1.2", + "hex_lit", + "secp256k1 0.28.2", "serde", ] [[package]] name = "bitcoin-internals" -version = "0.6.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" +dependencies = [ + "serde", +] [[package]] name = "bitcoin-io" -version = "0.1.101" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" -dependencies = [ - "bitcoin-consensus-encoding", -] +checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" [[package]] -name = "bitcoin-units" -version = "0.1.101" +name = "bitcoin_hashes" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cb95693f371d089a4b5b6fc41c6f3ea6e01ee8c15388335dfac8ea685173b51" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" dependencies = [ - "bitcoin-consensus-encoding", + "bitcoin-internals", + "hex-conservative 0.1.2", "serde", ] [[package]] name = "bitcoin_hashes" -version = "0.14.101" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" dependencies = [ "bitcoin-io", - "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", + "hex-conservative 0.2.1", "serde", ] [[package]] name = "bitflags" -version = "2.13.1" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-buffer" @@ -233,21 +230,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.3" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" -version = "1.12.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cbc" @@ -260,19 +251,18 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.2" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ - "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "chacha20" @@ -311,35 +301,28 @@ dependencies = [ [[package]] name = "cln-nip47" -version = "0.2.0" +version = "0.1.4" 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.7.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9a4bb9c7335040d49c51900305b91155303539e9f54839bb194e915277bf882" +checksum = "74a0f6827e3c3dfa7f2e92bb0591b5be44c82b945ff88f534857b185810414ce" dependencies = [ "anyhow", "bytes", @@ -356,9 +339,9 @@ dependencies = [ [[package]] name = "cln-rpc" -version = "0.7.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d9f5e26f0a2713ea1393eb3117c148295fa4c5f7db529713b20323ca7cc37aa" +checksum = "f4436e58f1fccb1faf69df9ac436ae9304b5c200c7d92e6c4229826bdd0a8d0d" dependencies = [ "anyhow", "bitcoin", @@ -383,19 +366,20 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] [[package]] name = "data-encoding" -version = "2.11.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "digest" @@ -405,68 +389,25 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] name = "displaydoc" -version = "0.2.7" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] name = "either" -version = "1.17.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -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" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "fnv" @@ -474,26 +415,20 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "form_urlencoded" -version = "1.2.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.33" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -506,9 +441,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -516,15 +451,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.33" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -533,38 +468,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -574,6 +509,7 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", + "pin-utils", "slab", ] @@ -589,38 +525,34 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.17" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.4" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "r-efi 5.3.0", - "wasip2", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] -name = "getrandom" -version = "0.4.3" +name = "gimli" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", -] +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gloo-timers" @@ -634,65 +566,6 @@ 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" @@ -701,18 +574,15 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hex-conservative" -version = "0.2.2" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" -dependencies = [ - "arrayvec", -] +checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" [[package]] name = "hex-conservative" -version = "1.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" dependencies = [ "arrayvec", ] @@ -723,124 +593,49 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" -version = "1.5.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", + "fnv", "itoa", ] -[[package]] -name = "http-body" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" -dependencies = [ - "bytes", - "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]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", - "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locale_core" -version = "2.1.1" +name = "icu_locid" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", @@ -850,65 +645,103 @@ dependencies = [ ] [[package]] -name = "icu_normalizer" -version = "2.1.1" +name = "icu_locid_transform" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", + "utf16_iter", + "utf8_iter", + "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" -version = "2.1.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ + "displaydoc", "icu_collections", - "icu_locale_core", + "icu_locid_transform", "icu_properties_data", "icu_provider", - "zerotrie", + "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" -version = "2.1.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", - "icu_locale_core", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", "writeable", "yoke", "zerofrom", - "zerotrie", "zerovec", ] [[package]] -name = "idna" -version = "1.1.0" +name = "icu_provider_macros" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", @@ -917,24 +750,14 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 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" @@ -946,28 +769,41 @@ dependencies = [ ] [[package]] -name = "itertools" -version = "0.14.0" +name = "instant" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ - "either", + "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", ] [[package]] name = "itoa" -version = "1.0.18" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.104" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ - "cfg-if", - "futures-util", + "once_cell", "wasm-bindgen", ] @@ -979,36 +815,31 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "litemap" -version = "0.8.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" -version = "0.4.14" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.33" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "log-panics" @@ -1021,41 +852,44 @@ dependencies = [ [[package]] name = "lru" -version = "0.18.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" +checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" [[package]] name = "matchers" -version = "0.2.0" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] name = "memchr" -version = "2.8.3" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] -name = "mio" -version = "1.2.2" +name = "miniz_oxide" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", + "adler2", ] [[package]] -name = "multimap" -version = "0.10.1" +name = "mio" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] [[package]] name = "negentropy" @@ -1065,87 +899,95 @@ checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" [[package]] name = "nostr" -version = "0.45.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dde8c76076d334409d86c2e1db3e97abe5deb8cb92744f939cbc1fa45bd69e7" +checksum = "d193102a62a22b61f9a61b9df54fb19ebab8c1763d088fbb9a6f0f57000fba2d" dependencies = [ "aes", "base64", - "bech32 0.12.0", + "bech32 0.11.0", "bip39", - "bitcoin_hashes 1.2.0", + "bitcoin_hashes 0.14.0", "cbc", "chacha20", "chacha20poly1305", - "faster-hex", - "opaquerr", - "rand 0.10.2", - "secp256k1 0.30.0", + "getrandom 0.2.16", + "instant", + "regex", + "scrypt", + "secp256k1 0.29.1", "serde", "serde_json", "unicode-normalization", - "universal-time", "url", - "zeroize", ] [[package]] name = "nostr-database" -version = "0.45.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b1fdb9fcba732e32719662afad1b267e50322dbe89e506017ec13f24361bddf" +checksum = "b6aafe85dc7c039c399796043b76009fa744c3a45ac073a023932f7b7d91b1e7" dependencies = [ + "lru", "nostr", - "opaquerr", + "tokio", ] [[package]] -name = "nostr-gossip" -version = "0.45.0" +name = "nostr-relay-pool" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa07539e52a71cb91fe0d693facaa298f03fcf9edcd66a521094e18e286e2336" -dependencies = [ - "nostr", - "opaquerr", -] - -[[package]] -name = "nostr-sdk" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c86342f367bd9b173ec4a697e936e3a82d6dad5b4aa06c0d35d9b4f88a8e72" +checksum = "df4d5628d2444349570fb185b0c2d92cfdb7e68d62b13bf3e8a4348b5de7430b" dependencies = [ "async-utility", "async-wsocket", - "faster-hex", - "futures", + "atomic-destructor", "lru", "negentropy", "nostr", "nostr-database", - "nostr-gossip", - "opaquerr", - "rand 0.10.2", "tokio", - "tokio-stream", "tracing", - "universal-time", +] + +[[package]] +name = "nostr-sdk" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e928ba9ac2695fbe10b8aefda59f2abfeb23c10a0e86a0b3e0f6a27bb274a2" +dependencies = [ + "async-utility", + "nostr", + "nostr-database", + "nostr-relay-pool", + "tokio", + "tracing", ] [[package]] name = "nu-ansi-term" -version = "0.50.3" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ - "windows-sys 0.61.2", + "overload", + "winapi", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", ] [[package]] name = "once_cell" -version = "1.21.4" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "opaque-debug" @@ -1154,16 +996,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] -name = "opaquerr" -version = "0.2.0" +name = "overload" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f933a4265d5cdad61d19bbdfc972ea5726d56cd8d3d57b8f2d3c365dd42bee9" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" -version = "0.12.5" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -1171,59 +1013,55 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.12" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-link", + "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", ] [[package]] name = "percent-encoding" -version = "2.3.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -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", -] +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.17" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "poly1305" @@ -1236,15 +1074,6 @@ 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" @@ -1254,167 +1083,20 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.119", -] - [[package]] name = "proc-macro2" -version = "1.0.107" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -1425,17 +1107,11 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "rand" -version = "0.8.7" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -1444,22 +1120,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.5" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "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", + "rand_core 0.9.3", ] [[package]] @@ -1479,7 +1145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", + "rand_core 0.9.3", ] [[package]] @@ -1488,61 +1154,70 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.2.16", ] [[package]] name = "rand_core" -version = "0.9.5" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.3.3", ] -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - [[package]] name = "redox_syscall" -version = "0.5.18" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.13.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] name = "regex-automata" -version = "0.4.18" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +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" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] [[package]] name = "regex-syntax" -version = "0.8.11" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "ring" @@ -1552,32 +1227,24 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.17", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] -name = "rustix" -version = "1.1.4" +name = "rustc-demangle" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ - "log", "once_cell", "ring", "rustls-pki-types", @@ -1588,18 +1255,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -1608,9 +1275,24 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.23" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +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", +] [[package]] name = "scopeguard" @@ -1619,25 +1301,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "secp256k1" -version = "0.29.1" +name = "scrypt" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ - "bitcoin_hashes 0.14.101", - "secp256k1-sys", + "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.30.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes 0.14.101", - "rand 0.8.7", - "secp256k1-sys", + "rand 0.8.5", + "secp256k1-sys 0.10.1", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", ] [[package]] @@ -1651,52 +1354,52 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.229" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -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" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.229" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", + "ryu", "serde", - "serde_core", - "zmij", ] [[package]] name = "sha1" -version = "0.10.7" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +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" dependencies = [ "cfg-if", "cpufeatures", @@ -1714,37 +1417,37 @@ dependencies = [ [[package]] name = "shlex" -version = "2.0.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" -version = "0.4.12" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.5" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] name = "stable_deref_trait" -version = "1.2.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "subtle" @@ -1754,32 +1457,15 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.119" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 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" @@ -1788,20 +1474,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "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", + "syn", ] [[package]] @@ -1815,11 +1488,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.20" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.20", + "thiserror-impl 2.0.12", ] [[package]] @@ -1830,34 +1503,34 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] name = "thiserror-impl" -version = "2.0.20" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] name = "thread_local" -version = "1.1.10" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", ] [[package]] name = "tinystr" -version = "0.8.3" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", @@ -1865,9 +1538,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.12.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -1880,44 +1553,38 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.1" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" dependencies = [ + "backtrace", "bytes", + "io-uring", "libc", "mio", "pin-project-lite", + "slab", "socket2", "tokio-macros", - "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", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.7.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn", ] [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", "tokio", @@ -1925,9 +1592,9 @@ dependencies = [ [[package]] name = "tokio-socks" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", "futures-util", @@ -1937,21 +1604,20 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.19" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", "tokio", - "tokio-util", ] [[package]] name = "tokio-tungstenite" -version = "0.28.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" dependencies = [ "futures-util", "log", @@ -1965,122 +1631,22 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.19" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", "futures-sink", - "libc", "pin-project-lite", "tokio", ] -[[package]] -name = "tonic" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -2090,20 +1656,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.31" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] name = "tracing-core" -version = "0.1.36" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -2122,14 +1688,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.23" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex-automata", + "regex", "sharded-slab", "smallvec", "thread_local", @@ -2138,54 +1704,42 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - [[package]] name = "tungstenite" -version = "0.28.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ "bytes", "data-encoding", "http", "httparse", "log", - "rand 0.9.5", + "rand 0.9.2", "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.20", + "thiserror 2.0.12", "utf-8", ] [[package]] name = "typenum" -version = "1.20.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-normalization" -version = "0.1.25" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] @@ -2200,12 +1754,6 @@ 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" @@ -2214,15 +1762,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.8" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", - "serde_derive", ] [[package]] @@ -2231,6 +1778,12 @@ 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" @@ -2239,11 +1792,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.24.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ - "getrandom 0.4.3", + "getrandom 0.3.3", "js-sys", "wasm-bindgen", ] @@ -2260,15 +1813,6 @@ 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" @@ -2276,42 +1820,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" +name = "wasi" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ - "wit-bindgen", + "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" -version = "0.2.127" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 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.77" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ + "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", + "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.127" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2319,31 +1879,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.127" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ - "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn", + "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.127" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.104" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -2355,23 +1915,39 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.9", + "webpki-roots 1.0.2", ] [[package]] name = "webpki-roots" -version = "1.0.9" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] [[package]] -name = "windows-link" -version = "0.2.1" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +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" [[package]] name = "windows-sys" @@ -2384,11 +1960,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.61.2" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-link", + "windows-targets", ] [[package]] @@ -2456,23 +2032,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "wit-bindgen" -version = "0.46.0" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" -version = "0.6.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "yoke" -version = "0.8.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ + "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -2480,79 +2066,68 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.56" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.56" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] name = "zerofrom" -version = "0.1.8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", "synstructure", ] [[package]] name = "zeroize" -version = "1.9.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -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", -] +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zerovec" -version = "0.11.6" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", @@ -2561,17 +2136,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] - -[[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 a21e37a..258d24c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "cln-nip47" -version = "0.2.0" -edition = "2024" -rust-version = "1.85.0" +version = "0.1.4" +edition = "2021" +rust-version = "1.75" [dependencies] anyhow = "1" @@ -12,22 +12,14 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["fs", "sync", "rt-multi-thread"] } -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" } +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" } parking_lot = "0.12" -# # 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" +# 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"] } uuid = { version = "1", features = ["v4"] } @@ -35,18 +27,6 @@ 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 5556fd0..2de2927 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). It is intended to be used by a single user, since all notifications got to all NWC's. +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). * [Installation](#installation) * [Building](#building) @@ -64,10 +64,11 @@ 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: @@ -84,7 +85,7 @@ cargo build --release After that the binary will be here: ``target/release/cln-nip47`` -Note: Release binaries are built with the ``optimized`` profile. +Note: Release binaries are built using ``cross`` and the ``optimized`` profile. # Documentation @@ -97,12 +98,7 @@ 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. 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-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-notifications``: Enable/disable nip47 notifications. Default is enabled (``true``) ## Methods @@ -131,26 +127,21 @@ 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) +* [NIP-44v2](https://github.com/nostr-protocol/nips/blob/master/44.md) \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 9b5fbd8..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,27 +0,0 @@ -# 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 deleted file mode 100644 index 04c8cef..0000000 --- a/build.rs +++ /dev/null @@ -1,8 +0,0 @@ -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 fa75d76..2221faf 100644 --- a/coffee.yml +++ b/coffee.yml @@ -1,6 +1,6 @@ plugin: name: cln-nip47 - version: 0.2.0 + version: 0.1.4 lang: rust install: | cargo build --release && cp target/release/cln-nip47 . && cargo clean diff --git a/flake.lock b/flake.lock index 87c96f0..4df37d2 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,17 @@ { "nodes": { "crane": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, "locked": { - "lastModified": 1755993354, - "narHash": "sha256-FCRRAzSaL/+umLIm3RU3O/+fJ2ssaPHseI2SSFL8yZU=", + "lastModified": 1721058578, + "narHash": "sha256-fs/PVa3H5dS1//4BjecWi3nitXm5fRObx0JxXIAo+JA=", "owner": "ipetkov", "repo": "crane", - "rev": "25bd41b24426c7734278c2ff02e53258851db914", + "rev": "17e5109bb1d9fb393d70fba80988f7d70d1ded1a", "type": "github" }, "original": { @@ -20,11 +25,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { @@ -35,11 +40,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1756159630, - "narHash": "sha256-ohMvsjtSVdT/bruXf5ClBh8ZYXRmD4krmjKrXhEvwMg=", + "lastModified": 1721116560, + "narHash": "sha256-++TYlGMAJM1Q+0nMVaWBSEvEUjRs7ZGiNQOpqbQApCU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "84c256e42600cb0fdf25763b48d28df2f25a0c8b", + "rev": "9355fa86e6f27422963132c2c9aeedb0fb963d93", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 247270f..a533264 100644 --- a/flake.nix +++ b/flake.nix @@ -4,7 +4,10 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - crane.url = "github:ipetkov/crane"; + crane = { + url = "github:ipetkov/crane"; + inputs.nixpkgs.follows = "nixpkgs"; + }; flake-utils.url = "github:numtide/flake-utils"; }; diff --git a/protos/hold.proto b/protos/hold.proto deleted file mode 100644 index e7ad240..0000000 --- a/protos/hold.proto +++ /dev/null @@ -1,185 +0,0 @@ -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 1c3323a..dd6ded4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,41 +1,23 @@ -use std::{ - path::{Path, PathBuf}, - str::FromStr, - time::Duration, -}; +use std::{path::Path, time::Duration}; use anyhow::anyhow; use cln_plugin::{ - Builder, - Plugin, options::{ConfigOption, DefaultBooleanConfigOption, StringArrayConfigOption}, + Builder, Plugin, }; -use cln_rpc::{ - ClnRpc, - model::{ - requests::{DecodeRequest, ListdatastoreRequest}, - responses::DecodeType, - }, -}; -use nostr::nips::nip47; +use cln_rpc::{model::requests::ListdatastoreRequest, ClnRpc}; + +use nostr_sdk::*; 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; @@ -48,11 +30,6 @@ 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.", @@ -63,35 +40,31 @@ 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_OR_RECEIVE_METHODS: [nip47::Method; 5] = [ - nip47::Method::MakeInvoice, - nip47::Method::LookupInvoice, - nip47::Method::ListTransactions, - nip47::Method::GetBalance, - nip47::Method::GetInfo, +pub const WALLET_READ_METHODS: [&str; 5] = [ + "make_invoice", + "lookup_invoice", + "list_transactions", + "get_balance", + "get_info", ]; -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_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_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> { - unsafe { - std::env::set_var( - "CLN_PLUGIN_LOG", - "cln_plugin=info,cln_rpc=info,cln_nip47=trace,info", - ); - }; + std::env::set_var( + "CLN_PLUGIN_LOG", + "cln_plugin=info,cln_rpc=info,cln_nip47=debug,info", + ); log_panics::init(); let state; @@ -117,13 +90,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 @@ -132,25 +105,15 @@ 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(STARTUP_DELAY)).await; + time::sleep(Duration::from_secs(1)).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!( "{}", @@ -163,16 +126,6 @@ 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 } @@ -181,8 +134,8 @@ async fn shutdown_handler( _args: serde_json::Value, ) -> Result<(), anyhow::Error> { let mut locked_handles = plugin.state().handles.lock().await; - for (_x, wallet_service) in locked_handles.drain() { - wallet_service.client.shutdown().await; + for (_x, (client, _client_pubkey)) in locked_handles.drain() { + client.shutdown().await; } std::process::exit(0) } @@ -193,7 +146,7 @@ async fn load_nwcs(plugin: Plugin, rpc: &mut ClnRpc) -> Result<(), key: Some(vec![PLUGIN_NAME.to_owned()]), }) .await?; - for datastore in labels.datastore { + for datastore in labels.datastore.into_iter() { let label = datastore.key.last().unwrap(); let mut nwc_store = load_nwc_store(rpc, label).await?; @@ -206,186 +159,7 @@ 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 dfd0e2c..338ddad 100644 --- a/src/nwc.rs +++ b/src/nwc.rs @@ -1,75 +1,63 @@ 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 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 nostr_sdk::nips::*; +use nostr_sdk::Client; +use nostr_sdk::*; +use tokio::sync::oneshot; 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<(), Error> { - let (method_capabilities, _) = build_capabilities(is_read_only_nwc(&nwc_store), &plugin); +) -> Result<(), client::Error> { + let capabilities = if is_read_only_nwc(&nwc_store) { + WALLET_READ_METHODS.join(" ") + } else { + WALLET_ALL_METHODS.join(" ") + }; - let wallet_keys = Keys::new(SecretKey::from_hex(&nwc_store.walletkey)?); - let client_keys = Keys::new(nwc_store.uri.secret.clone()); + 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 nostr_client = Client::new(); + let client = Client::new(wallet_keys.clone()); log::debug!("relay_count:{}", nwc_store.uri.relays.len()); - for relay in &nwc_store.uri.relays { - log::debug!("Adding relay: {relay}"); - nostr_client.add_relay(relay).await?; + for relay in nwc_store.uri.relays.iter() { + log::debug!("Adding relay: {}", relay); + client.add_relay(relay).await?; } if nwc_store.interval_config.is_some() { - log::debug!("NWC {label} uses an interval budget"); + start_nwc_budget_job(plugin.clone(), label.clone()); } - let nostr_client_clone = nostr_client.clone(); + let client_clone = 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 { - nostr_client_clone - .connect() - .and_wait(Duration::from_secs(30)) + client_clone.connect().await; + client_clone + .wait_for_connection(Duration::from_secs(30)) .await; - let relays = nostr_client_clone.relays().await; + let relays = client_clone.relays().await; if relays.is_empty() { log::info!("No more relays left, we probably shut down. Exiting..."); break; @@ -79,7 +67,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 { @@ -89,101 +77,90 @@ pub async fn run_nwc( } if let Err(e) = send_nwc_info_event( - plugin_clone.clone(), - nostr_client_clone.clone(), - method_capabilities.clone(), - wallet_keys_clone.clone(), + client_clone.clone(), + plugin_clone.option(&OPT_NOTIFICATIONS).unwrap(), + capabilities.clone(), + wallet_keys.clone(), ) .await { - log::warn!("{e}"); - nostr_client_clone.disconnect().await; + log::warn!("{}", e); + client_clone.disconnect().await; time::sleep(Duration::from_secs(5)).await; continue; } let filter = Filter::new() .kind(Kind::WalletConnectRequest) - .author(client_keys_clone.public_key()); + .author(client_pubkey); - let mut notifications = nostr_client_clone.notifications(); + 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; + }; - 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!("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(), - ) + 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 - { - log::warn!("NWC handler for `{label_clone}` had an error: {e}"); + { + Ok(()) => { + log::info!("NWC handler for `{}` stopped", label_clone); + break; } - } - - {}; + Err(e) => log::warn!("NWC handler for `{}` had an error: {}", label_clone, e), + }; } }); let mut locked_handles = plugin.state().handles.lock().await; - let wallet_service = WalletService { - client: nostr_client, - client_pubkey: client_keys.public_key(), - wallet_secret: wallet_keys, - }; - locked_handles.insert(label.clone(), wallet_service); + locked_handles.insert( + label.clone(), + (client, Keys::new(nwc_store.uri.secret).public_key()), + ); Ok(()) } pub async fn send_nwc_info_event( - plugin: Plugin, client: Client, + notifications: bool, capabilities: String, wallet_keys: Keys, ) -> Result<(), anyhow::Error> { - let mut capabilities = capabilities; - if plugin.option(&OPT_NOTIFICATIONS).unwrap() { - capabilities.push_str(" notifications"); - } - let mut info_event_builder = EventBuilder::new(Kind::WalletConnectInfo, capabilities) + let mut info_event_builder = EventBuilder::new(Kind::WalletConnectInfo, capabilities.clone()) .tag(Tag::parse(vec!["encryption", "nip44_v2 nip04"]).unwrap()); - if plugin.option(&OPT_NOTIFICATIONS).unwrap() { - let notification_capabilities = build_notifications_vec(&plugin).join(" "); + if notifications { info_event_builder = info_event_builder - .tag(Tag::parse(vec!["notifications", ¬ification_capabilities]).unwrap()); + .tag(Tag::parse(vec!["notifications", "payment_received payment_sent"]).unwrap()) } - let info_event = match info_event_builder.finalize_async(&wallet_keys).await { + let info_event = match info_event_builder.sign_with_keys(&wallet_keys) { 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() { @@ -201,123 +178,306 @@ 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(wallet_service) = locked_handles.remove(label) { - wallet_service.client.shutdown().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(()); } } -#[allow(clippy::too_many_lines)] async fn nwc_request_handler( - notification: ClientNotification, - nostr_client: &client::Client, - plugin: &Plugin, - label: &str, - wallet_keys: &Keys, + notification: RelayPoolNotification, + client: client::Client, + plugin: Plugin, + label: String, + wallet_keys: Keys, client_pubkey: PublicKey, -) -> Result<(), Box> { +) -> Result { let (relay_url, subscription_id, event) = match notification { - ClientNotification::Event { + RelayPoolNotification::Event { relay_url, subscription_id, event, } => (relay_url, subscription_id, event), - ClientNotification::Message { + RelayPoolNotification::Message { relay_url: _, message: _, - } - | ClientNotification::Shutdown => return Ok(()), + } => return Ok(false), + RelayPoolNotification::Shutdown => return Ok(true), }; - log::trace!("relay_url:{relay_url} subscription_id:{subscription_id} {event:?}"); - let mut use_nip44 = check_nip44_support(&event); - - let request = decrypt_request(&event.content, wallet_keys, &client_pubkey, &mut use_nip44)?; - - 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(), - }), - 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?; + if let Some(expi) = event.tags.expiration() { + if *expi < Timestamp::now() { + return Ok(false); } - - match request.params { - nip47::RequestParams::PayInvoice(pay_invoice_request) => { - pay_invoice_response(plugin.clone(), pay_invoice_request, label).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 + } + 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); + } + }; - 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 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() }; - - 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 { + 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 sending response event! {e}"); + log::warn!("Error serializing response! {}", e); + continue; + } + }; + log::debug!("RESPONSE:{}", response_str); + + 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, + }), + vec![id], + )); + } + let response_event = match response_builder.sign_with_keys(&wallet_keys) { + Ok(o) => o, + Err(e) => { + log::warn!("Error signing reponse event! {}", e); + continue; + } + }; + let send_result = match client.send_event(&response_event).await { + Ok(o) => o, + Err(e) => { + log::warn!("Error sending response event! {}", e); continue; } }; @@ -332,129 +492,8 @@ async fn nwc_request_handler( ); continue; } - 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:?}"); + log::debug!("SENT RESPONSE {:?}", response_event); } - 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()), - } + Ok(false) } diff --git a/src/nwc_balance.rs b/src/nwc_balance.rs index 2fde546..f95d17c 100644 --- a/src/nwc_balance.rs +++ b/src/nwc_balance.rs @@ -1,38 +1,12 @@ use cln_plugin::Plugin; use cln_rpc::{model::requests::ListpeerchannelsRequest, primitives::ChannelState}; -use nostr::nips::nip47; +use nostr_sdk::nips::*; -use crate::{ - structs::PluginState, - util::{get_budget_msat, load_nwc_store}, -}; +use crate::{structs::PluginState, util::load_nwc_store}; -pub async fn get_balance_response( +pub async fn get_balance( plugin: Plugin, - 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, + label: &String, ) -> Result { let mut rpc = plugin.state().rpc_lock.lock().await; @@ -43,15 +17,11 @@ async fn get_balance( message: e.to_string(), })?; - let balance = if let Some(bdgt_amt) = get_budget_msat(&nwc_store) { + let balance = if let Some(bdgt_amt) = nwc_store.budget_msat { bdgt_amt } else { let listpeerchannels = rpc - .call_typed(&ListpeerchannelsRequest { - id: None, - short_channel_id: None, - channel_id: None, - }) + .call_typed(&ListpeerchannelsRequest { id: None }) .await .map_err(|e| nip47::NIP47Error { code: nip47::ErrorCode::Internal, @@ -64,7 +34,7 @@ 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 deleted file mode 100644 index 4ce23a3..0000000 --- a/src/nwc_hold.rs +++ /dev/null @@ -1,248 +0,0 @@ -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 27e196a..4a7ac4c 100644 --- a/src/nwc_info.rs +++ b/src/nwc_info.rs @@ -1,39 +1,17 @@ +use std::str::FromStr; + use cln_plugin::Plugin; use cln_rpc::model::requests::GetinfoRequest; -use nostr::nips::nip47; +use nostr_sdk::nips::*; +use nostr_sdk::*; -use crate::{ - structs::PluginState, - util::{build_methods_vec, build_notifications_vec, is_read_only_nwc, load_nwc_store}, -}; +use crate::structs::PluginState; +use crate::util::{is_read_only_nwc, load_nwc_store}; +use crate::{OPT_NOTIFICATIONS, WALLET_ALL_METHODS, WALLET_READ_METHODS}; -pub async fn get_info_response( +pub async fn get_info( plugin: Plugin, - 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, + label: &String, ) -> Result { let mut rpc = plugin.state().rpc_lock.lock().await; @@ -45,12 +23,23 @@ async fn get_info( message: e.to_string(), })?; - let pubkey = get_info.id.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 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 @@ -59,12 +48,20 @@ async fn get_info( message: e.to_string(), })?; - let notifications = build_notifications_vec(&plugin); - - let methods = build_methods_vec(is_read_only_nwc(&nwc_store), &plugin); + 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() + }; Ok(nip47::GetInfoResponse { - alias: Some(get_info.alias), + alias: 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 f521673..98a06be 100644 --- a/src/nwc_invoice.rs +++ b/src/nwc_invoice.rs @@ -5,36 +5,12 @@ use cln_rpc::{ model::requests::InvoiceRequest, primitives::{Amount, AmountOrAny, Sha256}, }; -use nostr::{nips::nip47, types::Timestamp}; +use nostr_sdk::nips::*; use uuid::Uuid; use crate::structs::PluginState; -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( +pub async fn make_invoice( plugin: Plugin, params: nip47::MakeInvoiceRequest, ) -> Result { @@ -42,7 +18,7 @@ async fn make_invoice( let mut deschashonly = None; - if let Some(d_hash) = ¶ms.description_hash { + if let Some(d_hash) = params.description_hash { if params.description.is_none() { return Err(nip47::NIP47Error { code: nip47::ErrorCode::Other, @@ -51,7 +27,7 @@ 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(), })?; @@ -61,15 +37,9 @@ 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, @@ -78,24 +48,15 @@ async fn make_invoice( preimage: None, exposeprivatechannels: None, fallbacks: None, - amount_msat, - description: params - .description - .clone() - .unwrap_or("NWC make_invoice".to_owned()), + amount_msat: AmountOrAny::Amount(Amount::from_msat(params.amount)), + description: params.description.unwrap_or("NWC make_invoice".to_owned()), label: Uuid::new_v4().to_string(), }) .await { Ok(o) => Ok(nip47::MakeInvoiceResponse { invoice: o.bolt11, - 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)), + payment_hash: o.payment_hash.to_string(), }), Err(e) => Err(nip47::NIP47Error { code: nip47::ErrorCode::Internal, diff --git a/src/nwc_keysend.rs b/src/nwc_keysend.rs index aec724d..836985b 100644 --- a/src/nwc_keysend.rs +++ b/src/nwc_keysend.rs @@ -1,66 +1,25 @@ -use std::{collections::HashMap, str::FromStr}; +use std::{str::FromStr, time::Duration}; use cln_plugin::Plugin; use cln_rpc::{ - ClnRpc, - RpcError, - model::requests::{KeysendRequest, XkeysendRequest}, - primitives::{Amount, PublicKey, Secret, TlvEntry, TlvStream}, + model::requests::KeysendRequest, + primitives::{Amount, PublicKey, TlvEntry, TlvStream}, }; -use nostr::nips::nip47::{self}; +use nostr_sdk::nips::*; +use tokio::time; use crate::{ structs::PluginState, - util::{ - budget_amount_check, - get_budget_msat, - load_nwc_store, - payment_fee_reserve_msat, - refund_budget, - reserve_budget, - rpc_socket_path, - settle_budget, - }, + util::{budget_amount_check, load_nwc_store, update_nwc_store}, }; -pub const XKEYSEND_COMMAND: &str = "xkeysend"; - -pub async fn pay_keysend_response( +pub async fn pay_keysend( plugin: Plugin, params: nip47::PayKeysendRequest, - 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, + label: &String, ) -> Result { + let mut rpc = plugin.state().rpc_lock.lock().await; + if params.preimage.is_some() { return Err(nip47::NIP47Error { code: nip47::ErrorCode::Other, @@ -68,142 +27,32 @@ 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 ¶ms.tlv_records { + for tlv in params.tlv_records { extratlvs.entries.push(TlvEntry { typ: tlv.tlv_type, - value: hex::decode(&tlv.value).map_err(|e| RpcError { - code: Some(-32700), - message: format!("Could not decode tlv bytes: {e}"), - data: None, - })?, + value: tlv.value.as_bytes().to_owned(), }); } let extratlvs = if extratlvs.entries.is_empty() { @@ -212,7 +61,7 @@ async fn keysend( Some(extratlvs) }; - let o = pay_rpc + match rpc .call_typed(&KeysendRequest { exemptfee: None, extratlvs, @@ -225,48 +74,74 @@ async fn keysend( amount_msat: Amount::from_msat(params.amount), destination: pubkey, }) - .await?; + .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(), + })?; + } - Ok(( - o.amount_sent_msat.msat(), - o.amount_msat.msat(), - o.payment_preimage, - )) -} - -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(), - }; - }; - - 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(), + 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(), + }), + }, } } + +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, + ), + }; + responses.push(response_res); + time::sleep(Duration::from_millis(100)).await; + } + responses +} diff --git a/src/nwc_lookups.rs b/src/nwc_lookups.rs index 42ce1f6..b3b97ce 100644 --- a/src/nwc_lookups.rs +++ b/src/nwc_lookups.rs @@ -2,69 +2,18 @@ use std::{cmp::Reverse, str::FromStr}; use cln_plugin::Plugin; use cln_rpc::{ - ClnRpc, model::{ - requests::{ - DecodeRequest, - ListinvoicesIndex, - ListinvoicesRequest, - ListpaysRequest, - WaitIndexname, - WaitRequest, - WaitSubsystem, - }, - responses::{ - DecodeResponse, - DecodeType, - ListinvoicesInvoices, - ListinvoicesInvoicesStatus, - ListpaysPays, - ListpaysPaysStatus, - }, + requests::{DecodeRequest, ListinvoicesRequest, ListpaysRequest}, + responses::{ListinvoicesInvoicesStatus, ListpaysPaysStatus}, }, primitives::Sha256, }; -use nostr::{nips::nip47, types::Timestamp}; -use tonic::transport::Channel; +use nostr_sdk::nips::*; +use nostr_sdk::*; -use crate::{ - hold::{self, ListRequest, hold_client::HoldClient, list_request::Constraint}, - structs::{NOT_INV_ERR, PluginState}, - util::rpc_socket_path, -}; +use crate::structs::PluginState; -#[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( +pub async fn lookup_invoice( plugin: Plugin, params: nip47::LookupInvoiceRequest, ) -> Result { @@ -77,10 +26,15 @@ 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.clone() + params.invoice }; let invoices = rpc @@ -101,339 +55,214 @@ async fn lookup_invoice( .invoices; if invoices.len() == 1 { - 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) + let invoice_response = invoices.first().cloned().unwrap(); + let invstring = if invoice_response.bolt11.is_some() { + invoice_response.bolt11.unwrap() } else { - return Err(nip47::NIP47Error { + invoice_response.bolt12.unwrap() + }; + let invoice_decoded = rpc + .call_typed(&DecodeRequest { + string: invstring.clone(), + }) + .await + .map_err(|e| nip47::NIP47Error { code: nip47::ErrorCode::Internal, - message: "Could not convert payment hash".to_owned(), - }); - } - } else { - None - }; + message: e.to_string(), + })?; - let pays = rpc - .call_typed(&ListpaysRequest { - bolt11: invoice, - index: None, - limit: None, - payment_hash: payment_hash_hash, - start: None, - status: None, + 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) = 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, }) - .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 { + let payment_hash_hash = if let Some(hash) = params.payment_hash { + if let Ok(res) = Sha256::from_str(&hash) { + Some(res) } 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(), + code: nip47::ErrorCode::Internal, + message: "Could not convert payment hash".to_owned(), }); } + } else { + None }; - let list_request = ListRequest { - constraint: Some(Constraint::PaymentHash(payment_hash_hash)), - }; - let hold_lookup = hold_client - .list(list_request) + + 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::Other, - message: format!("Could not fetch hold invoice: {e}"), + code: nip47::ErrorCode::Internal, + message: e.to_string(), })? - .into_inner(); + .pays; - if hold_lookup.invoices.len() != 1 { + if pays.len() != 1 { return Err(nip47::NIP47Error { - code: nip47::ErrorCode::Other, + code: nip47::ErrorCode::NotFound, 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 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 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; } - Ok((hold_invoice, invoice_decoded)) - } else { - 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(), - })?; - if !invoice_decoded.valid { - return Err(nip47::NIP47Error { - 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 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 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 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 list_request = ListRequest { - constraint: Some(Constraint::PaymentHash(payment_hash_hash)), + 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 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(); + let fees_paid = list_pay.amount_sent_msat.unwrap().msat() - amount; + let preimage = list_pay.preimage.map(|p| hex::encode(p.to_vec())); - 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: 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(), - }); - } - Ok((hold_invoice, invoice_decoded)) + 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, + }) } } -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( +pub async fn list_transactions( plugin: Plugin, params: nip47::ListTransactionsRequest, ) -> Result, nip47::NIP47Error> { - 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 mut rpc = plugin.state().rpc_lock.lock().await; let (query_invoices, query_payments) = match params.transaction_type { Some(t) => match t { @@ -443,140 +272,21 @@ 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 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 + let list_invoices = rpc .call_typed(&ListinvoicesRequest { - index: Some(ListinvoicesIndex::CREATED), + index: None, invstring: None, label: None, - limit: Some(PAGE), + limit: None, offer_id: None, payment_hash: None, - start: Some(page_start), + start: None, }) .await .map_err(|e| nip47::NIP47Error { @@ -585,490 +295,231 @@ async fn collect_invoices( })? .invoices; - // 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 row.created_index >= start_excl { + for list_invoice in list_invoices.into_iter() { + if list_invoice.status == ListinvoicesInvoicesStatus::EXPIRED { continue; } - - if !unpaid && row.status == ListinvoicesInvoicesStatus::UNPAID { + 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() + }; - 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); + 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 { + 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 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 } } - Err(_e) => (), - } - } + _ => 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())); - 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) => (), + 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, + }); } } - 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(), + if query_payments { + let list_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(), - })?, - ) - } else { - None - }; + })? + .pays; - if invoice_decoded.is_some() && !invoice_decoded.as_ref().unwrap().valid { - return not_invoice_err; - } + 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 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()) + 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; } - _ => 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 + + 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, } - } - _ => 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 = 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; + } 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, } - } - Err(e) => { - log::warn!("Failed to serialize transaction: {e}"); - break; - } + } 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, + }); } - 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.sort_by_key(|t| Reverse(t.created_at)); - transactions + if let Some(l) = params.limit { + if transactions.len() > (l as usize) { + transactions = transactions.drain(0..(l as usize)).collect() + } + } + + Ok(transactions) } diff --git a/src/nwc_notifications.rs b/src/nwc_notifications.rs index 302ba96..fb214c5 100644 --- a/src/nwc_notifications.rs +++ b/src/nwc_notifications.rs @@ -2,38 +2,15 @@ use std::str::FromStr; use anyhow::anyhow; use cln_plugin::Plugin; -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 cln_rpc::model::requests::{DecodeRequest, ListinvoicesRequest, ListpaysRequest}; +use cln_rpc::model::responses::ListpaysPaysStatus; +use cln_rpc::primitives::Sha256; -use crate::{ - OPT_NOTIFICATIONS, - hold::{InvoiceState, ListRequest, TrackRequest, list_request::Constraint}, - structs::{NOT_INV_ERR, PluginState, WalletService}, -}; +use crate::structs::PluginState; +use crate::OPT_NOTIFICATIONS; -/// 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; +use nostr_sdk::nips::*; +use nostr_sdk::*; pub async fn payment_received_handler( plugin: Plugin, @@ -42,12 +19,13 @@ pub async fn payment_received_handler( if !plugin.option(&OPT_NOTIFICATIONS).unwrap() { return Ok(()); } - - 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 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 mut rpc = plugin.state().rpc_lock.lock().await; @@ -55,7 +33,7 @@ pub async fn payment_received_handler( .call_typed(&ListinvoicesRequest { index: None, invstring: None, - label: Some(inv_pay_notif.label), + label: Some(label.to_owned()), limit: None, offer_id: None, payment_hash: None, @@ -67,52 +45,19 @@ pub async fn payment_received_handler( let invoice = invoice_resp .first() .ok_or_else(|| anyhow!("invoice not found"))?; - let invstring = if let Some(bolt11) = &invoice.bolt11 { - bolt11.clone() - } else if let Some(bolt12) = &invoice.bolt12 { - bolt12.clone() + let invstring = if invoice.bolt11.is_some() { + invoice.bolt11.as_ref().unwrap() } else { - return Err(anyhow!( - "Listinvoices has neither returned bolt11 or bolt12 field" - )); + invoice.bolt12.as_ref().unwrap() }; - let payment_hash_str = hex::encode(invoice.payment_hash); - let invoice_decoded = rpc .call_typed(&DecodeRequest { string: invstring.clone(), }) .await?; - 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())); + let not_invoice_err = Err(anyhow!("Not an invoice or invalid invoice".to_owned())); if !invoice_decoded.valid { return not_invoice_err; @@ -167,34 +112,67 @@ fn make_payment_received_from_listinvoices( .ok_or_else(|| anyhow!("paid invoice missing paid_at time"))?, ); - let state = match invoice.status { - ListinvoicesInvoicesStatus::UNPAID => nip47::TransactionState::Pending, - ListinvoicesInvoicesStatus::PAID => nip47::TransactionState::Settled, - ListinvoicesInvoicesStatus::EXPIRED => nip47::TransactionState::Expired, - }; + let clients = plugin.state().handles.lock().await; - 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), - }), - }; + 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 notification = serde_json::to_string(&content)?; + 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); + } - Ok(notification) + Ok(()) } pub async fn payment_sent_handler( @@ -204,19 +182,18 @@ pub async fn payment_sent_handler( if !plugin.option(&OPT_NOTIFICATIONS).unwrap() { return Ok(()); } - - 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 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 mut rpc = plugin.state().rpc_lock.lock().await; - let mut pays_resp = rpc + let pays_resp = rpc .call_typed(&ListpaysRequest { bolt11: None, index: None, @@ -228,33 +205,14 @@ 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!("complete payment not found"))?; + .ok_or_else(|| anyhow!("payment not found"))?; - 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 - ); - } + if pay.status != ListpaysPaysStatus::COMPLETE { + return Err(anyhow!("Payment not complete")); } - 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 { @@ -274,23 +232,14 @@ async fn make_payment_sent_from_listpays( ); let settled_at = Timestamp::from_secs(pay.completed_at.unwrap()); - 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 { + if !invstring.is_empty() { let invoice_decoded = rpc .call_typed(&DecodeRequest { string: invstring.clone(), }) .await?; - let not_invoice_err = Err(anyhow!(NOT_INV_ERR.to_owned())); + let not_invoice_err = Err(anyhow!("Not an invoice".to_owned())); if !invoice_decoded.valid { return not_invoice_err; @@ -326,263 +275,78 @@ async fn make_payment_sent_from_listpays( } _ => return not_invoice_err, }; - } - - let fees_paid = if let Some(amt_sent) = pay.amount_sent_msat { - amt_sent.msat() - amount } else { - 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 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 => (), - } - } + description = pay.description.clone(); + description_hash = None; + amount = if let Some(amt) = pay.amount_msat { + amt.msat() + } else { + // Amount missing but required + 0 } } - 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 fees_paid = pay.amount_sent_msat.unwrap().msat() - amount; 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), + 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, - settle_deadline: lowest_htlc_expiry, + expires_at: None, + settled_at, 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 { + }), + }; + 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!( - "Failed sending hold_invoice_accepted {payment_hash_str} notification\ - to client {}: {e}", - wallet_service.client_pubkey - ); + "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); } + Ok(()) } diff --git a/src/nwc_pay.rs b/src/nwc_pay.rs index 4799465..821aa36 100644 --- a/src/nwc_pay.rs +++ b/src/nwc_pay.rs @@ -1,180 +1,27 @@ +use std::time::Duration; + use cln_plugin::Plugin; use cln_rpc::{ - ClnRpc, - RpcError, - model::{ - requests::{DecodeRequest, PayRequest, XpayRequest}, - responses::DecodeResponse, - }, - primitives::{Amount, Secret}, + model::requests::{DecodeRequest, PayRequest, XpayRequest}, + primitives::Amount, }; -use nostr::nips::nip47; +use nostr_sdk::nips::*; +use tokio::time; use crate::{ - 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, - }, + structs::PluginState, + util::{at_or_above_version, budget_amount_check, load_nwc_store, update_nwc_store}, }; -pub const XPAY_COMMAND: &str = "xpay"; - -pub async fn pay_invoice_response( +pub async fn pay_invoice( plugin: Plugin, params: nip47::PayInvoiceRequest, - 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, - ), - }] -} + label: &String, +) -> Result<(nip47::PayInvoiceResponse, String), (nip47::NIP47Error, String)> { + let mut rpc = plugin.state().rpc_lock.lock().await; -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 id = params.id.clone().unwrap_or_default(); - 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(), @@ -186,211 +33,252 @@ async fn decode_and_validate_invoice( code: nip47::ErrorCode::Internal, message: e.to_string(), }, - params.id.clone(), + 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 Err(( - nip47::NIP47Error { - code: nip47::ErrorCode::Internal, - message: NOT_INV_ERR.to_owned(), - }, - params.id.clone(), - )); + return not_invoice_error; } - 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() + let id = if let Some(i) = params.id { + i } else { - 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() + 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, + } }; - Ok(id) -} + 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, + }; -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| { + let mut nwc_store = load_nwc_store(&mut rpc, label).await.map_err(|e| { ( nip47::NIP47Error { code: nip47::ErrorCode::Internal, message: e.to_string(), }, - Some(id.to_owned()), + id.clone(), ) })?; - budget_amount_check(params.amount, invoice_amt_msat, get_budget_msat(&nwc_store)).map_err( + budget_amount_check(params.amount, Some(invoice_amt_msat), nwc_store.budget_msat).map_err( |e| { ( nip47::NIP47Error { code: nip47::ErrorCode::QuotaExceeded, message: e.to_string(), }, - Some(id.to_owned()), + id.clone(), ) }, )?; - Ok(nwc_store) -} + let my_version = plugin.state().config.lock().clone().my_cln_version; -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 => ( + if at_or_above_version(&my_version, "24.11").map_err(|e| { + ( nip47::NIP47Error { code: nip47::ErrorCode::Internal, message: e.to_string(), }, - Some(id.to_owned()), - ), + 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, + )), + }, + } } } -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, - )) +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 } diff --git a/src/parse.rs b/src/parse.rs index a8f9768..935fe28 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,72 +1,44 @@ -use std::{collections::HashSet, path::Path}; +use std::path::Path; use anyhow::anyhow; use cln_plugin::ConfiguredPlugin; -use cln_rpc::{ClnRpc, model::requests::HelpRequest}; -use nostr::types::RelayUrl; +use cln_rpc::{model::requests::GetinfoRequest, ClnRpc}; use crate::{ - OPT_RELAYS, - nwc_keysend::XKEYSEND_COMMAND, - nwc_pay::XPAY_COMMAND, structs::{PluginState, TimeUnit}, + OPT_RELAYS, }; pub async fn read_startup_options( plugin: &ConfiguredPlugin, state: &PluginState, ) -> Result<(), anyhow::Error> { - 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(), - ] + 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() + )); + } } else { - plugin.option(&OPT_RELAYS).unwrap().unwrap() + return Err(anyhow!( + "`{}` not set, must specify atleast one relay url!", + OPT_RELAYS.name() + )); }; let mut rpc = ClnRpc::new( Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file), ) .await?; - let help = rpc.call_typed(&HelpRequest { command: None }).await?; - + let version = rpc.call_typed(&GetinfoRequest {}).await?.version; let mut config = state.config.lock(); - - 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)?); + config.my_cln_version = version; + for relay in relays_str.into_iter() { + log::debug!("RELAY:{}", relay); + config.relays.push(nostr_sdk::RelayUrl::parse(&relay)?); } Ok(()) } @@ -80,15 +52,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.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)), + 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), } } 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 a2c2c38..34791be 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -1,31 +1,19 @@ use anyhow::anyhow; use cln_plugin::Plugin; use cln_rpc::model::requests::{ - DatastoreMode, - DatastoreRequest, - DeldatastoreRequest, - ListdatastoreRequest, -}; -use nostr::{ - key::{Keys, SecretKey}, - nips::nip47::NostrWalletConnectUri, - types::Timestamp, + DatastoreMode, DatastoreRequest, DeldatastoreRequest, ListdatastoreRequest, }; +use nostr_sdk::nips::nip47::*; +use nostr_sdk::*; use serde_json::json; -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::nwc::{ + run_nwc, send_nwc_info_event, start_nwc_budget_job, stop_nwc, stop_nwc_budget_job, }; +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, @@ -39,7 +27,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(), @@ -67,8 +55,7 @@ pub async fn nwc_create( let conf = BudgetIntervalConfig { interval_secs: interval, reset_budget_msat: bgt_msat, - last_reset: Timestamp::now().as_secs(), - spend_since_last_reset: 0, + last_reset: Timestamp::now().as_u64(), }; result.insert("interval_config".to_owned(), serde_json::to_value(&conf)?); Some(conf) @@ -83,7 +70,6 @@ pub async fn nwc_create( walletkey: wallet_keys.secret_key().to_secret_hex(), budget_msat, interval_config, - reserved_msat: 0, }; rpc.call_typed(&DatastoreRequest { @@ -95,9 +81,7 @@ pub async fn nwc_create( }) .await?; - if let Err(e) = run_nwc(plugin.clone(), label.clone(), nwc_store.clone()).await { - log::warn!("Failed running nwc: {e}"); - } + run_nwc(plugin.clone(), label.clone(), nwc_store.clone()).await?; Ok(serde_json::Value::Object(result)) } @@ -129,6 +113,8 @@ 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); @@ -139,8 +125,7 @@ pub async fn nwc_budget( let interval_config = BudgetIntervalConfig { interval_secs: interval, reset_budget_msat: budget, - last_reset: Timestamp::now().as_secs(), - spend_since_last_reset: 0, + last_reset: Timestamp::now().as_u64(), }; nwc_store.interval_config = Some(interval_config.clone()); } else { @@ -153,20 +138,28 @@ 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 (method_capabilities, _) = build_capabilities(is_new_nwc_read_only, &plugin); + let capabilities = if is_new_nwc_read_only { + WALLET_READ_METHODS.join(" ") + } else { + WALLET_ALL_METHODS.join(" ") + }; 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}"))? - .client + .ok_or_else(|| anyhow!("No client found for label: {}", label))? + .0 .clone(), - method_capabilities, + plugin.option(&OPT_NOTIFICATIONS).unwrap(), + capabilities, wallet_keys, ) .await?; @@ -186,8 +179,7 @@ pub async fn nwc_list( let mut nwcs = Vec::new(); if let Some(lbl) = label { - let mut nwc_store = load_nwc_store(&mut rpc, &lbl).await?; - nwc_store.budget_msat = get_budget_msat(&nwc_store); + let nwc_store = load_nwc_store(&mut rpc, &lbl).await?; 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); @@ -203,17 +195,16 @@ pub async fn nwc_list( }); nwcs.push(json!({lbl:nwc_json})); } else { - let all_stored_nwcs = rpc + let nwcs_store = rpc .call_typed(&ListdatastoreRequest { key: Some(vec![PLUGIN_NAME.to_owned()]), }) .await? .datastore; - for datastore in all_stored_nwcs { + for datastore in nwcs_store.into_iter() { let label = datastore.key.last().unwrap().to_owned(); - let mut nwc_store = load_nwc_store(&mut rpc, &label).await?; - nwc_store.budget_msat = get_budget_msat(&nwc_store); + let nwc_store = load_nwc_store(&mut rpc, &label).await?; 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 c66de4e..ce7221a 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,28 +1,19 @@ use std::{collections::HashMap, path::PathBuf, str::FromStr, sync::Arc}; use cln_rpc::ClnRpc; -use nostr::{ - key::{Keys, PublicKey}, - nips::nip47::NostrWalletConnectUri, - types::RelayUrl, -}; -use nostr_sdk::client::Client; +use nostr_sdk::client; +use nostr_sdk::nips::nip47; +use nostr_sdk::nostr; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; -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; +use tokio::sync::oneshot; #[derive(Clone)] pub struct PluginState { pub config: Arc>, - pub handles: Arc>>, + pub handles: Arc>>, pub rpc_lock: Arc>, - pub hold_client: Arc>>>, + pub budget_jobs: Arc>>>, } impl PluginState { pub async fn new(path: PathBuf) -> Result { @@ -30,29 +21,21 @@ 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?)), - hold_client: Arc::new(Mutex::new(None)), + budget_jobs: Arc::new(Mutex::new(HashMap::new())), }) } } -pub struct WalletService { - pub client: Client, - pub client_pubkey: PublicKey, - pub wallet_secret: Keys, -} - #[derive(Clone, Debug)] pub struct Config { - pub relays: Vec, - pub has_xkeysend: bool, - pub has_xpay: bool, + pub relays: Vec, + pub my_cln_version: String, } impl Config { pub fn default() -> Config { Config { relays: Vec::new(), - has_xkeysend: false, - has_xpay: false, + my_cln_version: String::new(), } } } @@ -75,7 +58,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)), } } } @@ -85,18 +68,14 @@ 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: NostrWalletConnectUri, + pub uri: nip47::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 7a53195..d56ea7e 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,49 +1,59 @@ -use std::{path::Path, str::FromStr, time::Duration}; +use std::{path::Path, time::Duration}; +use anyhow::anyhow; use cln_plugin::Plugin; -use cln_rpc::{ - ClnRpc, - model::requests::{DeldatastoreRequest, ListdatastoreRequest}, -}; -use nostr::types::Timestamp; +use cln_rpc::ClnRpc; +use nostr_sdk::Timestamp; +use tokio::{sync::oneshot, time}; use crate::{ - PLUGIN_NAME, - structs::{ID_MAX_AGE, ID_STORE, PluginState}, + structs::PluginState, + util::{load_nwc_store, update_nwc_store}, }; -pub async fn cleanup_event_ids(plugin: Plugin) -> Result<(), anyhow::Error> { +pub async fn budget_task( + mut rx: oneshot::Receiver<()>, + plugin: Plugin, + label: String, +) -> Result<(), anyhow::Error> { let mut rpc = ClnRpc::new( Path::new(&plugin.configuration().lightning_dir).join(&plugin.configuration().rpc_file), ) .await?; - loop { - { - 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()); - } + 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); } } - - tokio::time::sleep(Duration::from_secs(120)).await; } + Ok(()) } diff --git a/src/util.rs b/src/util.rs index 2543e55..4c4a161 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,75 +1,10 @@ -use std::{ - cmp::max, - path::{Path, PathBuf}, -}; - 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}, + ClnRpc, }; -/// 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); - } - } -} +use crate::{structs::NwcStore, PLUGIN_NAME}; pub fn budget_amount_check( request_amt_msat: Option, @@ -77,8 +12,10 @@ pub fn budget_amount_check( budget_msat: Option, ) -> Result<(), anyhow::Error> { log::debug!( - "checking budget and amounts for request:{request_amt_msat:?} \ - invoice:{invoice_amt_msat:?} budget:{budget_msat:?}" + "checking budget and amounts for request:{:?} invoice:{:?} budget:{:?}", + request_amt_msat, + invoice_amt_msat, + budget_msat ); if request_amt_msat.is_none() && invoice_amt_msat.is_none() { return Err(anyhow!("No amount given to check budget against!")); @@ -107,104 +44,38 @@ pub fn budget_amount_check( Ok(()) } -pub async fn load_nwc_store(rpc: &mut ClnRpc, label: &str) -> Result { +pub async fn load_nwc_store(rpc: &mut ClnRpc, label: &String) -> Result { let nwc_store_store = rpc .call_typed(&ListdatastoreRequest { - key: Some(vec![PLUGIN_NAME.to_owned(), label.to_owned()]), + key: Some(vec![PLUGIN_NAME.to_owned(), label.clone()]), }) .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: &str, + label: &String, nwc_store: NwcStore, ) -> Result<(), anyhow::Error> { rpc.call_typed(&DatastoreRequest { - key: vec![PLUGIN_NAME.to_owned(), label.to_owned()], + key: vec![PLUGIN_NAME.to_owned(), label.clone()], 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}"); - 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?; - + log::debug!("stored nwc store for label:{}", label); Ok(()) } @@ -217,91 +88,32 @@ pub fn is_read_only_nwc(nwc_store: &NwcStore) -> bool { false } -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(()) -} +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 fn build_capabilities(is_receive_only: bool, plugin: &Plugin) -> (String, String) { - let holdinvoice_support = plugin.state().hold_client.lock().is_some(); + let my_version_parts: Vec<&str> = full_clean_my_version.split('.').collect(); + let min_version_parts: Vec<&str> = min_version.split('.').collect(); - 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(), - ); + if my_version_parts.len() <= 1 || my_version_parts.len() > 3 { + return Err(anyhow!("Version string parse error: {}", my_version)); } + 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()?; - 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(), - ); + if my_num != min_num { + return Ok(my_num > min_num); } } - (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 + Ok(my_version_parts.len() >= min_version_parts.len()) } #[test] @@ -321,105 +133,3 @@ 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 deleted file mode 100644 index 3afabc9..0000000 --- a/tests/config.toml +++ /dev/null @@ -1,12 +0,0 @@ - -[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 deleted file mode 100644 index 6f6222a..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 705210e..0000000 Binary files a/tests/nostr-rs-relay.tar.gz and /dev/null differ diff --git a/tests/pyproject.toml b/tests/pyproject.toml deleted file mode 100644 index bcde559..0000000 --- a/tests/pyproject.toml +++ /dev/null @@ -1,17 +0,0 @@ -[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 new file mode 100644 index 0000000..bb18cbc --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,4 @@ +pytest-asyncio<0.24 +nostr-sdk +PyYAML +nostr_relay diff --git a/tests/setup.sh b/tests/setup.sh index 829a336..8ae9603 100755 --- a/tests/setup.sh +++ b/tests/setup.sh @@ -49,49 +49,52 @@ 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 -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 + +# 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 +} 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 ! uv run python -m grpc_tools.protoc --proto_path="$proto_path" --python_out=$script_dir --grpc_python_out=$script_dir $proto_path/*.proto; then + if ! "$python_exec" -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 deleted file mode 100644 index 36260fd..0000000 --- a/tests/test_cln-nip47.py +++ /dev/null @@ -1,1913 +0,0 @@ -# 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 new file mode 100644 index 0000000..2e6d015 --- /dev/null +++ b/tests/test_clnnwc.py @@ -0,0 +1,1352 @@ +#!/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 393a970..1f6fbf8 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,21 +1,18 @@ +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") -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" +COMPILED_PATH = Path.cwd() / "target" / RUST_PROFILE / "cln-nip47" +DOWNLOAD_PATH = Path.cwd() / "tests" / "cln-nip47" @pytest.fixture -def get_plugin(): +def get_plugin(directory): if COMPILED_PATH.is_file(): return COMPILED_PATH elif DOWNLOAD_PATH.is_file(): @@ -24,14 +21,6 @@ def get_plugin(): 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( @@ -44,6 +33,15 @@ 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 deleted file mode 100644 index c2c260e..0000000 --- a/tests/uv.lock +++ /dev/null @@ -1,1432 +0,0 @@ -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" }, -]
- - + + - - + +
- - + + - - + +
- - + + - - + +
- - + + - - + +