diff --git a/.github/workflows/build-kotlin.yml b/.github/workflows/build-kotlin.yml new file mode 100644 index 000000000..503b2e11e --- /dev/null +++ b/.github/workflows/build-kotlin.yml @@ -0,0 +1,69 @@ +name: Build Kotlin/Android Bindings + +on: + workflow_call: + inputs: + ref: + description: 'Git ref to check out and build' + required: true + type: string + +jobs: + build: + name: Build Kotlin Multiplatform Bindings (macOS) + runs-on: macos-15 + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: >- + x86_64-linux-android, + aarch64-linux-android, + armv7-linux-androideabi, + i686-linux-android, + aarch64-apple-ios, + aarch64-apple-ios-sim, + x86_64-apple-ios + + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "greenlight-kotlin" + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Task + run: | + sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin + + - name: Install cargo-ndk + run: cargo install cargo-ndk --version 3.5.4 + + - name: Build Kotlin Multiplatform bindings + run: task build:kotlin + + - name: Build Android library + working-directory: libs/gl-sdk-android + run: ./gradlew :lib:assemble --console=plain + + - name: Publish to Maven Local (validation) + working-directory: libs/gl-sdk-android + run: ./gradlew :lib:publishToMavenLocal --console=plain + + - name: Upload Kotlin build outputs + uses: actions/upload-artifact@v7 + with: + name: kotlin-outputs + path: libs/gl-sdk-android/lib/build/outputs/ + retention-days: 7 diff --git a/.github/workflows/build-napi.yml b/.github/workflows/build-napi.yml new file mode 100644 index 000000000..cbf2ca6c2 --- /dev/null +++ b/.github/workflows/build-napi.yml @@ -0,0 +1,93 @@ +name: Build N-API Bindings + +on: + workflow_call: + inputs: + ref: + description: 'Git ref to check out and build' + required: true + type: string + +jobs: + build: + name: Build N-API - ${{ matrix.host }} / ${{ matrix.target }} + runs-on: ${{ matrix.host }} + strategy: + fail-fast: false + matrix: + include: + - host: ubuntu-latest + target: x86_64-unknown-linux-gnu + strip: strip -x *.node + + - host: ubuntu-latest + target: aarch64-unknown-linux-gnu + strip: aarch64-linux-gnu-strip -x *.node + + - host: macos-14 + target: aarch64-apple-darwin + strip: strip -x *.node + + - host: windows-latest + target: x86_64-pc-windows-msvc + strip: "" + + defaults: + run: + working-directory: libs/gl-sdk-napi + + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + cache-dependency-path: libs/gl-sdk-napi/package-lock.json + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "greenlight-${{ matrix.target }}" + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + version: '23.2' + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup cross-compilation (Linux ARM64) + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build -- --target ${{ matrix.target }} + shell: bash + + - name: Strip binary + if: matrix.strip != '' + run: ${{ matrix.strip }} + shell: bash + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: napi-${{ matrix.target }} + path: | + libs/gl-sdk-napi/*.node + libs/gl-sdk-napi/index.js + libs/gl-sdk-napi/index.d.ts + if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/build-python.yml b/.github/workflows/build-python.yml new file mode 100644 index 000000000..fe05865ee --- /dev/null +++ b/.github/workflows/build-python.yml @@ -0,0 +1,148 @@ +name: Build Python Wheels + +on: + workflow_call: + inputs: + ref: + description: 'Git ref to check out and build' + required: true + type: string + +jobs: + source: + name: Source Distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + + - uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + + - name: Build source distribution + run: uv build --package gl-client --sdist + + - name: Upload source distribution + uses: actions/upload-artifact@v7 + with: + name: python-sdist + path: dist/gl_client-*.tar.gz + retention-days: 7 + + wheels: + name: Wheels - ${{ matrix.os }} / ${{ matrix.target }} + runs-on: ${{ matrix.host }} + strategy: + fail-fast: false + matrix: + include: + - host: ubuntu-latest + os: linux + target: x86_64 + architecture: x64 + + - host: ubuntu-latest + os: linux + target: i686 + architecture: x64 + + - host: macos-14 + os: macos + target: aarch64 + architecture: arm64 + + - host: windows-latest + os: windows + target: x64 + architecture: x64 + + - host: windows-latest + os: windows + target: x86 + architecture: x86 + + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + + - uses: actions/setup-python@v6 + with: + python-version: '3.10' + architecture: ${{ matrix.architecture }} + + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + + - name: Install Rust Toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Rust Cache (Per Architecture) + uses: Swatinem/rust-cache@v2 + with: + shared-key: "greenlight-${{ matrix.target || 'host' }}" + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + version: '23.2' + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + working-directory: libs/gl-client-py + rust-toolchain: stable + target: ${{ matrix.target }} + manylinux: auto + args: --release --out dist + # Mount host protoc into maturin's Docker container (Linux only) + docker-options: >- + -v /opt/hostedtoolcache/protoc/v23.2/x64/bin/protoc:/usr/bin/protoc:ro + + - name: Build gl-sdk shared library (64-bit only) + if: matrix.target == 'x86_64' || matrix.target == 'aarch64' || matrix.target == 'x64' + run: cargo build --release -p gl-sdk + + - name: Build and retag gl-sdk wheel (64-bit only) + if: matrix.target == 'x86_64' || matrix.target == 'aarch64' || matrix.target == 'x64' + shell: bash + run: | + if [ "${{ matrix.os }}" = "macos" ]; then + LIB_EXT="dylib" + elif [ "${{ matrix.os }}" = "windows" ]; then + LIB_EXT="dll" + else + LIB_EXT="so" + fi + mkdir -p libs/gl-sdk/glsdk + LIB_PATH=$(find target -name "libglsdk.${LIB_EXT}" -o -name "glsdk.${LIB_EXT}" | head -n 1) + install -m 0755 "${LIB_PATH}" libs/gl-sdk/glsdk/libglsdk.${LIB_EXT} + if [ "${{ matrix.os }}" = "linux" ] && [ -d "libs/gl-client-py/dist" ]; then + sudo chown -R $(id -u):$(id -g) libs/gl-client-py/dist + fi + uv build --package gl-sdk --wheel --out-dir libs/gl-client-py/dist + # Retag: hatchling produces py3-none-any but the wheel contains a native shared lib + pip install wheel + if [ "${{ matrix.os }}" = "linux" ]; then + PLAT="manylinux_2_17_${{ matrix.target }}" + elif [ "${{ matrix.os }}" = "macos" ]; then + PLAT="${{ matrix.target == 'aarch64' && 'macosx_11_0_arm64' || 'macosx_10_12_x86_64' }}" + elif [ "${{ matrix.os }}" = "windows" ]; then + PLAT="${{ matrix.target == 'x64' && 'win_amd64' || 'win32' }}" + fi + for whl in libs/gl-client-py/dist/gl_sdk*.whl; do + python -m wheel tags --remove --platform-tag "$PLAT" "$whl" + done + + - name: Upload wheels + uses: actions/upload-artifact@v7 + with: + name: python-wheel-${{ matrix.os }}-${{ matrix.target }} + path: libs/gl-client-py/dist/ + retention-days: 7 diff --git a/.github/workflows/build-rust.yml b/.github/workflows/build-rust.yml new file mode 100644 index 000000000..39a016d0f --- /dev/null +++ b/.github/workflows/build-rust.yml @@ -0,0 +1,36 @@ +name: Build and Test Rust + +on: + workflow_call: + inputs: + ref: + description: 'Git ref to check out and build' + required: true + type: string + +jobs: + build-and-test: + name: Rust build and unit tests (Linux) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "greenlight-rust" + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + version: '23.2' + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build workspace + run: cargo build --workspace + + - name: Run tests + run: cargo test --workspace diff --git a/.github/workflows/build-swift.yml b/.github/workflows/build-swift.yml new file mode 100644 index 000000000..a61c20c9d --- /dev/null +++ b/.github/workflows/build-swift.yml @@ -0,0 +1,74 @@ +name: Build Swift Bindings + +on: + workflow_call: + inputs: + ref: + description: 'Git ref to check out and build' + required: true + type: string + outputs: + xcframework_checksum: + description: 'SHA-256 checksum of the XCFramework zip (for Package.swift)' + value: ${{ jobs.build.outputs.xcframework_checksum }} + +jobs: + build: + name: Build Swift XCFramework (macOS) + runs-on: macos-15 + outputs: + xcframework_checksum: ${{ steps.checksum.outputs.checksum }} + steps: + - uses: actions/checkout@v6 + with: + submodules: true + ref: ${{ inputs.ref }} + + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-apple-ios,aarch64-apple-ios,aarch64-apple-ios-sim + + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "greenlight-swift" + + - name: Setup Task + run: | + sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + version: '23.2' + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install XcodeGen + run: brew install xcodegen + + - name: Build iOS targets and Swift bindings + run: | + task build:ios + task build:ios-sim + task build:swift + + - name: Compress XCFramework and compute checksum + id: checksum + run: | + cd target + zip -9 -r glsdkFFI.xcframework.zip glsdkFFI.xcframework + CHECKSUM=$(swift package compute-checksum glsdkFFI.xcframework.zip) + echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT + + - name: Upload XCFramework zip + uses: actions/upload-artifact@v7 + with: + name: swift-xcframework + path: target/glsdkFFI.xcframework.zip + retention-days: 7 + + - name: Upload generated Swift source + uses: actions/upload-artifact@v7 + with: + name: swift-generated-source + path: target/swift/glsdk.swift + retention-days: 7 diff --git a/.github/workflows/build-test-binaries.yml b/.github/workflows/build-test-binaries.yml new file mode 100644 index 000000000..810c585a6 --- /dev/null +++ b/.github/workflows/build-test-binaries.yml @@ -0,0 +1,42 @@ +name: Build Test Binaries + +on: + workflow_call: + inputs: + ref: + description: 'Git ref to check out and build' + required: true + type: string + +jobs: + build: + name: Build gl-plugin and gl-signerproxy (Linux) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "greenlight-test-binaries" + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + version: '23.2' + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build test binaries + run: cargo build -p gl-plugin -p gl-signerproxy + + - name: Upload test binaries + uses: actions/upload-artifact@v7 + with: + name: test-binaries-linux-x86_64 + path: | + target/debug/gl-plugin + target/debug/gl-signerproxy + retention-days: 1 diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml deleted file mode 100644 index 0f24a16d8..000000000 --- a/.github/workflows/check-formatting.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Check Formatting - -on: - pull_request: - types: - - synchronize - - opened - workflow_dispatch: - -jobs: - check-formatting: - runs-on: ubuntu-latest - strategy: - fail-fast: false - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Install Rust Toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.73 - components: rustfmt - - - name: Check Rust Formatting - continue-on-error: true - run: | - make check-rustfmt diff --git a/.github/workflows/check-self.yml b/.github/workflows/check-self.yml index b5a277eff..318230d91 100644 --- a/.github/workflows/check-self.yml +++ b/.github/workflows/check-self.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Install Task uses: arduino/setup-task@v2 @@ -25,7 +25,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install the latest version of uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v8.1.0 env: UV_PYTHON: 3.11 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..2d4d95cb2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,244 @@ +# ============================================================================== +# CI ORCHESTRATOR +# ============================================================================== +# Runs all build and test jobs on PRs and pushes to main. +# Delegates compilation to the reusable build-*.yml workflows (shared with +# the release orchestrator), then runs language-specific tests on the +# resulting artifacts. +# ============================================================================== + +name: CI + +on: + push: + branches: + - main + pull_request: + types: + - synchronize + - opened + workflow_dispatch: + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + # ============================================================================ + # Rust + # ============================================================================ + rust: + name: Rust + uses: ./.github/workflows/build-rust.yml + with: + ref: ${{ github.sha }} + + # ============================================================================ + # Kotlin / Android + # ============================================================================ + kotlin: + name: Kotlin + uses: ./.github/workflows/build-kotlin.yml + with: + ref: ${{ github.sha }} + + # ============================================================================ + # Test binaries (gl-plugin, gl-signerproxy) + # ============================================================================ + test-binaries: + name: Test Binaries + uses: ./.github/workflows/build-test-binaries.yml + with: + ref: ${{ github.sha }} + + # ============================================================================ + # Node.js / N-API โ€” build then test + # ============================================================================ + napi-build: + name: N-API + uses: ./.github/workflows/build-napi.yml + with: + ref: ${{ github.sha }} + + napi-test: + name: N-API Test + needs: [napi-build, python-build, test-binaries] + runs-on: ubuntu-latest + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/target + defaults: + run: + working-directory: libs/gl-sdk-napi + steps: + - uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + cache-dependency-path: libs/gl-sdk-napi/package-lock.json + + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + + - name: Install cfssl + run: | + curl -sL -o /usr/local/bin/cfssl https://github.com/cloudflare/cfssl/releases/download/v1.6.5/cfssl_1.6.5_linux_amd64 + curl -sL -o /usr/local/bin/cfssljson https://github.com/cloudflare/cfssl/releases/download/v1.6.5/cfssljson_1.6.5_linux_amd64 + chmod +x /usr/local/bin/cfssl /usr/local/bin/cfssljson + + - name: Download test binaries + uses: actions/download-artifact@v8 + with: + name: test-binaries-linux-x86_64 + path: target/debug + + - name: Make test binaries executable + run: chmod +x ${{ github.workspace }}/target/debug/gl-plugin ${{ github.workspace }}/target/debug/gl-signerproxy + + - name: Install bitcoind + run: | + BITCOIN_VERSION=28.2 + curl -sO https://bitcoincore.org/bin/bitcoin-core-${BITCOIN_VERSION}/bitcoin-${BITCOIN_VERSION}-x86_64-linux-gnu.tar.gz + tar -xzf bitcoin-${BITCOIN_VERSION}-x86_64-linux-gnu.tar.gz + sudo install -m 0755 -t /usr/local/bin bitcoin-${BITCOIN_VERSION}/bin/* + + - name: Install lightningd via clnvm + run: | + CLN_PATH=$(uv run --package cln-version-manager --with click --with rich clnvm get --tag v25.12gl1) + echo "$(dirname "$CLN_PATH")" >> $GITHUB_PATH + + - name: Download Linux x86_64 Python wheel + uses: actions/download-artifact@v8 + with: + name: python-wheel-linux-x86_64 + path: ${{ github.workspace }}/dist + + - name: Pre-build Python test dependencies + run: | + uv pip install ${{ github.workspace }}/dist/gl_client*.whl ${{ github.workspace }}/dist/gl_sdk*.whl + uv pip install ${{ github.workspace }}/libs/gl-testing --no-deps + uv pip install ${{ github.workspace }}/libs/cln-version-manager \ + flaky "grpcio-tools>=1.66" "grpcio>=1.66.0" "httpx[http2]==0.27.2" \ + "purerpc>=0.8.0" "pyln-client==24.2" "pyln-testing==24.2" \ + "pytest-timeout>=2.3.1" "pytest-xdist>=3.6.1" "rich>=13.9.3" \ + "sh>=1.14.3" "sonora>=0.2.3" "bip39>=0.0.2" + + - name: Install dependencies + run: npm ci + + - name: Download N-API artifacts + uses: actions/download-artifact@v8 + with: + name: napi-x86_64-unknown-linux-gnu + path: libs/gl-sdk-napi + + - name: Run tests + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 10 + shell: bash + command: cd libs/gl-sdk-napi && npm test -- tests/basic.spec.ts + + # ============================================================================ + # Python โ€” build then install-test + # ============================================================================ + python-build: + name: Python + uses: ./.github/workflows/build-python.yml + with: + ref: ${{ github.sha }} + + python-test: + name: Python Install Test + needs: [python-build, test-binaries] + runs-on: ubuntu-latest + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/target + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + + - name: Install cfssl + run: | + curl -sL -o /usr/local/bin/cfssl https://github.com/cloudflare/cfssl/releases/download/v1.6.5/cfssl_1.6.5_linux_amd64 + curl -sL -o /usr/local/bin/cfssljson https://github.com/cloudflare/cfssl/releases/download/v1.6.5/cfssljson_1.6.5_linux_amd64 + chmod +x /usr/local/bin/cfssl /usr/local/bin/cfssljson + + - name: Install bitcoind + run: | + BITCOIN_VERSION=28.2 + curl -sO https://bitcoincore.org/bin/bitcoin-core-${BITCOIN_VERSION}/bitcoin-${BITCOIN_VERSION}-x86_64-linux-gnu.tar.gz + tar -xzf bitcoin-${BITCOIN_VERSION}-x86_64-linux-gnu.tar.gz + sudo install -m 0755 -t /usr/local/bin bitcoin-${BITCOIN_VERSION}/bin/* + + - name: Install lightningd via clnvm + run: | + CLN_PATH=$(uv run --package cln-version-manager --with click --with rich clnvm get --tag v25.12gl1) + echo "$(dirname "$CLN_PATH")" >> $GITHUB_PATH + + - name: Download test binaries + uses: actions/download-artifact@v8 + with: + name: test-binaries-linux-x86_64 + path: target/debug + + - name: Make test binaries executable + run: chmod +x target/debug/gl-plugin target/debug/gl-signerproxy + + - name: Download Linux x86_64 wheel + uses: actions/download-artifact@v8 + with: + name: python-wheel-linux-x86_64 + path: dist + + - name: Install prebuilt wheel and run tests + run: | + uv venv --clear + # Install the prebuilt wheels (gl_client and gl_sdk) + uv pip install dist/gl_client*.whl dist/gl_sdk*.whl --force-reinstall + + # Install gl-testing and its deps, but skip gl-client (already installed from prebuilt wheel) + uv pip install libs/gl-testing --no-deps + uv pip install libs/cln-version-manager pytest pytest-timeout \ + flaky "grpcio-tools>=1.66" "grpcio>=1.66.0" "httpx[http2]==0.27.2" \ + "purerpc>=0.8.0" "pyln-client==24.2" "pyln-testing==24.2" \ + "pytest-xdist>=3.6.1" "rich>=13.9.3" "sh>=1.14.3" \ + "sonora>=0.2.3" "bip39>=0.0.2" + + # Run the gl-client-py unit test suite using the prebuilt wheel! + echo "Running gl-client-py unit tests..." + uv run --no-sync pytest libs/gl-client-py/tests + + # Run the gl-sdk Python unit test suite using the prebuilt wheel! + echo "Running gl-sdk Python unit tests..." + uv run --no-sync pytest libs/gl-sdk/tests + + # ============================================================================ + # Formatting + # ============================================================================ + formatting: + name: Check Formatting + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Check Rust Formatting + run: cargo fmt --all -- --check diff --git a/.github/workflows/cln-version-manager-py.yml b/.github/workflows/cln-version-manager-py.yml index 45aadf8a1..7c2e93e3d 100644 --- a/.github/workflows/cln-version-manager-py.yml +++ b/.github/workflows/cln-version-manager-py.yml @@ -16,13 +16,13 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Install Task uses: arduino/setup-task@v2 - name: Install the latest version of uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v8.1.0 with: version: "latest" enable-cache: true diff --git a/.github/workflows/docs-action.yml b/.github/workflows/docs-action.yml index ad3baea19..092fdabb9 100644 --- a/.github/workflows/docs-action.yml +++ b/.github/workflows/docs-action.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.ref }} token: ${{ secrets.GITHUB_TOKEN }} @@ -40,7 +40,7 @@ jobs: sudo apt-get install -y python3 protobuf-compiler openssl libpq5 golang-cfssl ca-certificates - name: Setup uv - uses: astral-sh/setup-uv@v1 + uses: astral-sh/setup-uv@v8.1.0 with: version: "0.9.5" @@ -71,7 +71,8 @@ jobs: - name: Copy gl-sdk shared library to Python package location run: | mkdir -p libs/gl-sdk/glsdk - cp target/debug/libglsdk.so libs/gl-sdk/glsdk/libglsdk.so + LIB_PATH=$(find target -name "libglsdk.so" | head -n 1) + install -m 0755 "${LIB_PATH}" libs/gl-sdk/glsdk/libglsdk.so - name: Sync Python dependencies run: | @@ -134,7 +135,7 @@ jobs: - name: Upload gltestserver logs if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: gltestserver-logs path: /tmp/gltests/gltestserver.log diff --git a/.github/workflows/kotlin.yml b/.github/workflows/kotlin.yml deleted file mode 100644 index dfefecf72..000000000 --- a/.github/workflows/kotlin.yml +++ /dev/null @@ -1,64 +0,0 @@ -on: - push: - branches: - - master - tags: - - 'gl-sdk-*' - pull_request: {} - -name: Kotlin library - -jobs: - - build: - runs-on: macos-26 - steps: - - name: "Show default version of NDK" - run: echo $ANDROID_NDK_ROOT - - - name: "Check out PR branch" - uses: actions/checkout@v4 - - - name: "Set up JDK" - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: 21 - - - uses: dtolnay/rust-toolchain@1.88.0 - with: - targets: x86_64-linux-android, aarch64-linux-android, armv7-linux-androideabi, i686-linux-android, aarch64-apple-ios, aarch64-apple-ios-sim, x86_64-apple-ios - - - name: Install Protoc - uses: arduino/setup-protoc@v3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Task - run: | - sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin - - - name: "Install NDK" - run: | - cargo install cargo-ndk --version 3.5.4 - - - name: "Build Kotlin Multiplatform bindings" - run: | - /usr/local/bin/task build:kotlin - - - name: "Build Android library" - working-directory: libs/gl-sdk-android - run: | - ./gradlew :lib:assemble --console=plain - - - name: "Puiblish to Maven" - working-directory: libs/gl-sdk-android - run: | - ./gradlew :lib:publishToMavenLocal - - - name: Temporarily save artifact - uses: actions/upload-artifact@v4 - with: - name: glsdk-artifact - path: libs/gl-sdk-android/lib/build/outputs/ - retention-days: 14 diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml deleted file mode 100644 index c1a9ce535..000000000 --- a/.github/workflows/python.yml +++ /dev/null @@ -1,192 +0,0 @@ -name: Python - -on: - push: - branches: - - main - pull_request: - types: - - synchronize - - opened - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - source: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v3 - - - uses: actions/setup-python@v4 - with: - python-version: 3.9 - architecture: x64 - - name: Install the latest version of uv - uses: astral-sh/setup-uv@v7 - with: - version: "latest" - - - name: Build Source Distribution - run: uv build --package gl-client --sdist - - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-source - path: dist/gl_client-*.tar.gz - - linux: - runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - target: - - x86_64 - - i686 - # aarch64 does not compile due to an old(-ish) compiler with the error - # `ARM assembler must define __ARM_ARCH` - # - aarch64 - # Temporarily disable armv7 as to github issues fetching a manifest for the architecture. - # - armv7 - steps: - - uses: actions/checkout@v3 - - name: Install uv - uses: astral-sh/setup-uv@v7 - - - name: Install Protoc - uses: arduino/setup-protoc@v3 - with: - version: "23.2" # Fixed since we mount the path below - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - working-directory: libs/gl-client-py - rust-toolchain: stable - target: ${{ matrix.target }} - manylinux: auto - args: --release --out dist - docker-options: -v /opt/hostedtoolcache/protoc/v23.2/x64/bin/protoc:/usr/bin/protoc:ro - - - name: Install built wheel (emulated) - uses: uraimo/run-on-arch-action@v2.5.0 - if: matrix.target != 'ppc64' && matrix.target != 'x86_64' && matrix.target != 'i686' - with: - arch: ${{ matrix.target }} - distro: ubuntu24.04 - githubToken: ${{ github.token }} - install: | - apt-get update - apt-get install -y --no-install-recommends python3 python3-pip - uv sync - run: | - uv pip install libs/gl-client-py/dist/gl_client*.whl --force-reinstall - python3 -c "import glclient;creds=glclient.Credentials();signer=glclient.Signer(b'\x00'*32,'bitcoin', creds);print(repr(creds));print(signer.version())" - - - name: Install built wheel (native) - if: matrix.target == 'x86_64' - run: | - uv sync - uv pip install libs/gl-client-py/dist/gl_client*.whl --force-reinstall - uv run python3 -c "import glclient;creds=glclient.Credentials();signer=glclient.Signer(b'\x00'*32,'bitcoin', creds);print(repr(creds));print(signer.version())" - - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-linux-${{ matrix.target }} - path: libs/gl-client-py/dist/ - - macos: - runs-on: macos-latest - strategy: - fail-fast: false - matrix: - target: - - x86_64 - - aarch64 - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: 3.9 - - - uses: dtolnay/rust-toolchain@nightly - - - name: Install the latest version of uv - uses: astral-sh/setup-uv@v7 - with: - version: "latest" - - name: Install Protoc - uses: arduino/setup-protoc@v3 - with: - version: "23.2" - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Build wheels - ${{ matrix.target }} - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - working-directory: libs/gl-client-py - args: --release --out dist - docker-options: -v /opt/hostedtoolcache/protoc/v23.2/x64/bin/protoc:/usr/bin/protoc:ro - env: - MACOSX_DEPLOYMENT_TARGET: 10.9 - - - name: Install built wheel - env: - PATH: $PATH:$HOME/.local/bin - if: matrix.target == 'aarch64' - run: | - uv sync - uv pip install libs/gl-client-py/dist/gl_client*.whl --force-reinstall - uv run python3 -c "import glclient;creds=glclient.Credentials();signer=glclient.Signer(b'\x00'*32,'bitcoin', creds);print(repr(creds));print(signer.version())" - - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-macos-${{ matrix.target }} - path: libs/gl-client-py/dist/ - - windows: - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - target: - - x64 - - x86 - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: 3.9 - architecture: ${{ matrix.target }} - - uses: dtolnay/rust-toolchain@nightly - - - name: Install Protoc - uses: arduino/setup-protoc@v3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - working-directory: libs\\gl-client-py - args: --release --out dist - - # Wildcard expansion on windows is different... - # - name: Install built wheel - # run: | - # pip install libs\gl-client-py\dist\gl_client*.whl --force-reinstall - # python3 -c "import glclient;creds=glclient.Credentials();signer=glclient.Signer(b'\x00'*32,'bitcoin', creds);print(repr(creds));print(signer.version())" - - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-win-${{ matrix.target }} - path: libs\gl-client-py\dist diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16dc90cb9..bdef424b9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,553 +1,477 @@ # ============================================================================== -# CRATE PUBLISHING WORKFLOW +# RELEASE ORCHESTRATOR # ============================================================================== -# Purpose: Automatically publish Rust crates from a monorepo to crates.io +# Unified release workflow for gl-client, gl-sdk, and all derived bindings. # -# Trigger: Push a git tag matching the pattern `-v` -# Example: `mycrate-v1.2.3` +# Publishes Rust crates in topological dependency order (with crates.io +# index polling between each step), builds all binding artifacts in +# parallel via reusable workflows, and pushes to npm, PyPI, Swift SPM, +# and creates GitHub Releases. # -# Safety Philosophy: This workflow implements multiple validation gates to catch -# issues before publishing. Once a crate version is published to crates.io, it -# cannot be unpublished (only yanked), so we want to ensure everything is correct. +# Dry-run mode runs `cargo publish --dry-run`, `npm publish --dry-run`, +# and builds all bindings, but does not push tags, create releases, or +# upload to any registry. # -# Workflow Steps: -# 1. Version Match - Ensures Cargo.toml version matches the tag -# 2. Changelog Format - Validates changelog follows Keep a Changelog format -# 3. Changelog Entry - Ensures this version is documented in changelog -# 4. Semver Check - Prevents accidental breaking changes in minor/patch bumps -# 5. Tests - Runs the crate's test suite -# 6. Publish - Publishes to crates.io and creates GitHub release +# Usage: +# 1. Merge the release PR into main (version bumps + changelogs). +# 2. Go to Actions > Release Orchestrator > Run workflow. +# 3. Fill in version numbers; set dry_run=true for validation only. # -# These checks should have already passed in CI (on PRs/main branch), but we -# run them again here as a final safety gate before the irreversible publish. +# See RELEASE.md for the full process documentation. # ============================================================================== -name: Publish Crate +name: Release Orchestrator on: - push: - tags: - # Matches tags like: mycrate-v1.2.3, my-crate-v0.1.0, etc. - # The 'v' prefix helps distinguish version tags from other tags - - '*-v[0-9]+.[0-9]+.[0-9]+' - - # Manual trigger for dry-run testing - # This allows testing all validation checks without actually publishing workflow_dispatch: inputs: - crate: - description: 'Crate name to test (e.g., gl-client)' + gl_client_version: + description: 'gl-client version (e.g. 0.6.0)' required: true type: string - version: - description: 'Version to test (e.g., 1.2.3)' + gl_sdk_version: + description: 'gl-sdk version (e.g. 0.4.0)' required: true type: string + gl_sdk_cli_version: + description: 'gl-sdk-cli version (e.g. 0.3.0)' + required: true + type: string + dry_run: + description: 'Dry run: validate and build, skip real publishing' + required: true + type: boolean + default: true + +concurrency: + group: ${{ github.workflow }}-release + cancel-in-progress: false permissions: contents: write + id-token: write +# ============================================================================== +# STAGE 1: VALIDATE +# ============================================================================== jobs: - publish: + validate: + name: Validate release configuration runs-on: ubuntu-latest steps: - # ========================================================================== - # SETUP: Checkout and Parse Tag - # ========================================================================== - - - uses: actions/checkout@v4 - with: - # fetch-depth: 0 gets the full git history, which is required for - # cargo-semver-checks to compare against previous published versions - fetch-depth: 0 - - - name: Parse tag - id: parse - run: | - # Check if this is a manual dispatch (dry-run) or tag-triggered run - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - # Manual dispatch - use inputs - CRATE="${{ inputs.crate }}" - VERSION="${{ inputs.version }}" - echo "๐Ÿงช DRY-RUN MODE: Testing $CRATE version $VERSION" - echo "dry_run=true" >> $GITHUB_OUTPUT - else - # Tag-triggered - parse from tag - # Extract the full tag name from the GitHub ref - # GITHUB_REF format: refs/tags/mycrate-v1.2.3 - TAG=${GITHUB_REF#refs/tags/} - echo "Full tag: $TAG" - - # Parse the tag to extract crate name and version - # Example: mycrate-v1.2.3 โ†’ CRATE=mycrate, VERSION=1.2.3 - # This regex removes everything from '-v' onwards to get the crate name - CRATE=$(echo $TAG | sed -E 's/-v[0-9]+\.[0-9]+\.[0-9]+$//') - - # This regex extracts just the version numbers after '-v' - VERSION=$(echo $TAG | sed -E 's/.*-v([0-9]+\.[0-9]+\.[0-9]+)$/\1/') - - echo "๐Ÿ“ฆ Publishing $CRATE version $VERSION" - echo "dry_run=false" >> $GITHUB_OUTPUT - fi + - uses: actions/checkout@v6 - # Make these available to subsequent steps via GITHUB_OUTPUT - echo "crate=$CRATE" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - - # Parse semantic version components for later use in semver checks - # We need to know if this is a major/minor/patch bump to determine - # whether breaking changes are allowed - MAJOR=$(echo $VERSION | cut -d. -f1) - MINOR=$(echo $VERSION | cut -d. -f2) - PATCH=$(echo $VERSION | cut -d. -f3) - - echo "major=$MAJOR" >> $GITHUB_OUTPUT - echo "minor=$MINOR" >> $GITHUB_OUTPUT - echo "patch=$PATCH" >> $GITHUB_OUTPUT - - uses: dtolnay/rust-toolchain@stable - name: Install Protoc uses: arduino/setup-protoc@v3 with: + version: '23.2' repo-token: ${{ secrets.GITHUB_TOKEN }} - # ========================================================================== - # CHECK 1: Version Match - # ========================================================================== - # Rationale: The tag version must match the version in Cargo.toml. - # This prevents accidentally publishing the wrong version or forgetting to - # bump the version in Cargo.toml before tagging. - # - # Why this matters: If we published with a mismatched version, the published - # crate would have a different version than expected, breaking consumer - # dependencies and causing confusion. - # ========================================================================== - - - name: Verify version matches + - name: Verify Cargo.toml versions match inputs run: | - echo "๐Ÿ” CHECK 1/5: Verifying Cargo.toml version..." - - # Use cargo metadata to extract the version from Cargo.toml - # This is more reliable than parsing TOML manually - # The jq command filters packages by name and extracts the version field - CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 | \ - jq -r '.packages[] | select(.name == "${{ steps.parse.outputs.crate }}") | .version') - - # Check if we found the crate in the workspace - if [ -z "$CARGO_VERSION" ]; then - echo "โŒ ERROR: Crate '${{ steps.parse.outputs.crate }}' not found in workspace" - echo "" - echo "This usually means:" - echo " 1. The crate name in the tag doesn't match any crate in the workspace" - echo " 2. The crate is not included in the workspace members" - exit 1 - fi - - # Compare versions - if [ "$CARGO_VERSION" != "${{ steps.parse.outputs.version }}" ]; then - echo "โŒ ERROR: Version mismatch!" - echo " Tag version: ${{ steps.parse.outputs.version }}" - echo " Cargo.toml version: $CARGO_VERSION" - echo "" - echo "Please update Cargo.toml to version ${{ steps.parse.outputs.version }}" - echo "and create a new tag, or delete this tag and create a new one matching" - echo "the Cargo.toml version." - exit 1 - fi - - echo "โœ… Version verified: $CARGO_VERSION" - - # ========================================================================== - # CHECK 2: Changelog Format Validation - # ========================================================================== - # Rationale: Validates that the changelog follows the Keep a Changelog format. - # This ensures consistency and makes changelogs machine-readable. - # - # Why this matters: A properly formatted changelog is easier to parse - # programmatically, looks professional, and helps users quickly find - # information about specific versions. It also catches common formatting - # errors like missing dates, wrong heading levels, etc. - # - # Tool choice: python-kacl provides excellent error messages with line - # numbers and specific issues, making it easy to fix problems. - # ========================================================================== - + check_version() { + local crate=$1 expected=$2 + local actual + actual=$(cargo metadata --no-deps --format-version 1 \ + | jq -r ".packages[] | select(.name == \"$crate\") | .version") + if [ "$actual" != "$expected" ]; then + echo "::error::$crate version mismatch: Cargo.toml=$actual, input=$expected" + exit 1 + fi + echo " $crate: $actual โœ“" + } + echo "Checking versions..." + check_version gl-client "${{ inputs.gl_client_version }}" + check_version gl-sdk "${{ inputs.gl_sdk_version }}" + check_version gl-sdk-cli "${{ inputs.gl_sdk_cli_version }}" + - name: Setup Python for changelog validation - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' - - - name: Install python-kacl - run: pip install python-kacl - - - name: Validate changelog format + + - name: Validate changelogs run: | - echo "๐Ÿ” CHECK 2/5: Validating changelog format..." - - CRATE="${{ steps.parse.outputs.crate }}" - - # Find the crate's directory in the workspace - # We use cargo metadata rather than assuming a directory structure - CRATE_DIR=$(cargo metadata --no-deps --format-version 1 | \ - jq -r ".packages[] | select(.name == \"$CRATE\") | .manifest_path" | \ - xargs dirname) - - # Look for changelog files in order of preference - # We check multiple variants because conventions vary - CHANGELOG="" - for name in CHANGELOG.md changelog.md CHANGELOG; do - if [ -f "$CRATE_DIR/$name" ]; then - CHANGELOG="$CRATE_DIR/$name" - break - fi + pip install python-kacl + for crate_dir in libs/gl-client libs/gl-sdk libs/gl-sdk-cli; do + echo "Validating $crate_dir/CHANGELOG.md..." + (cd "$crate_dir" && kacl-cli verify) done - - if [ -z "$CHANGELOG" ]; then - echo "โŒ ERROR: No changelog found in $CRATE_DIR" - echo "" - echo "Expected one of: CHANGELOG.md, changelog.md, CHANGELOG" - echo "Please create a changelog following the Keep a Changelog format:" - echo "https://keepachangelog.com/" - exit 1 - fi - - echo "Validating $CHANGELOG..." - - # Run kacl-cli verify with JSON output for structured error reporting - # The JSON output makes it easier to parse and display errors - # Note: kacl-cli reads CHANGELOG.md from cwd, does not accept file arguments - if (cd "$CRATE_DIR" && kacl-cli verify --json) > /tmp/kacl-output.json 2>&1; then - echo "โœ… Changelog format is valid" - else - echo "โŒ ERROR: Changelog format validation failed" - echo "" - echo "The following issues were found:" - echo "" - - # Parse and display errors in a human-readable format - cat /tmp/kacl-output.json | jq -r '.errors[] | "Line \(.line_number): \(.error_message)\n โ†’ \(.line)\n"' - - echo "" - echo "Please fix these issues and try again." - echo "See https://keepachangelog.com/ for format guidelines." - exit 1 - fi - - # ========================================================================== - # CHECK 3: Changelog Entry Exists - # ========================================================================== - # Rationale: Ensures that this specific version has been documented in the - # changelog before we publish it. - # - # Why this matters: Publishing without changelog documentation means users - # won't know what changed in this version. This is especially important for - # breaking changes or new features. It also enforces good release hygiene - # by ensuring documentation happens before release, not after. - # - # Note: This check is separate from format validation because a changelog - # can be properly formatted but missing the entry for the version being - # published (e.g., if someone forgot to move changes from Unreleased). - # ========================================================================== - - - name: Verify changelog entry exists + + - name: Verify changelog entries exist for each version run: | - echo "๐Ÿ” CHECK 3/5: Checking for changelog entry..." - - CRATE="${{ steps.parse.outputs.crate }}" - VERSION="${{ steps.parse.outputs.version }}" - - # Find the changelog (same logic as CHECK 2) - CRATE_DIR=$(cargo metadata --no-deps --format-version 1 | \ - jq -r ".packages[] | select(.name == \"$CRATE\") | .manifest_path" | \ - xargs dirname) - - CHANGELOG="" - for name in CHANGELOG.md changelog.md CHANGELOG; do - if [ -f "$CRATE_DIR/$name" ]; then - CHANGELOG="$CRATE_DIR/$name" - break + check_entry() { + local file=$1 version=$2 + if ! grep -qE "^## \[?v?$version\]?" "$file"; then + echo "::error::No changelog entry for $version in $file" + exit 1 fi - done - - # Search for version entry using regex that matches common formats: - # - ## [1.2.3] (preferred Keep a Changelog format) - # - ## 1.2.3 (without brackets) - # - ## [v1.2.3] (with v prefix) - # - # [1.2.3] (single # for projects not using nested headers) - # - # The regex breakdown: - # ^##? - Start of line, one or two # symbols - # \[? - Optional opening bracket - # v? - Optional 'v' prefix - # $VERSION - The actual version number - # \]? - Optional closing bracket - if grep -qE "^##? \[?v?$VERSION\]?" "$CHANGELOG"; then - echo "โœ… Changelog entry found for version $VERSION" - echo "" - echo "Entry preview:" - # Show the version header and the next 10 lines to give context - grep -A 10 -E "^##? \[?v?$VERSION\]?" "$CHANGELOG" | head -15 + echo " $file: [$version] โœ“" + } + echo "Checking changelog entries..." + check_entry libs/gl-client/CHANGELOG.md "${{ inputs.gl_client_version }}" + check_entry libs/gl-sdk/CHANGELOG.md "${{ inputs.gl_sdk_version }}" + check_entry libs/gl-sdk-cli/CHANGELOG.md "${{ inputs.gl_sdk_cli_version }}" + + - name: Cargo check (workspace compiles) + run: cargo check + + # ============================================================================ + # STAGE 2: PUBLISH RUST CRATES (sequential, topological order) + # ============================================================================ + # In dry-run mode these jobs run `cargo publish --dry-run` which + # packages, verifies, and compiles the crate but does not upload. + # In real mode they use trusted publishing (OIDC) to upload. + # ============================================================================ + publish-gl-client: + name: Publish gl-client + needs: validate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + version: '23.2' + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish gl-client + run: | + if [ "${{ inputs.dry_run }}" = "true" ]; then + echo "=== DRY RUN: cargo publish --dry-run ===" + cargo publish -p gl-client --dry-run else - echo "โŒ ERROR: No changelog entry found for version $VERSION" - echo "" - echo "Please add a changelog entry with one of these formats:" - echo " ## [$VERSION] - $(date +%Y-%m-%d)" - echo " ## $VERSION" - echo " ## [v$VERSION]" - echo "" - echo "Example:" - echo " ## [$VERSION] - $(date +%Y-%m-%d)" - echo " ### Added" - echo " - New feature X" - echo " ### Fixed" - echo " - Bug Y" - echo "" - echo "Current changelog preview:" - head -30 "$CHANGELOG" - exit 1 + if cargo info "gl-client@${{ inputs.gl_client_version }}" --registry crates-io &>/dev/null; then + echo "gl-client ${{ inputs.gl_client_version }} already published -- skipping" + else + cargo publish -p gl-client + fi fi - - # ========================================================================== - # CHECK 4: Semver Compatibility - # ========================================================================== - # Rationale: Prevents accidental breaking changes in patch and minor version - # bumps by comparing the API surface against the last published version. - # - # Why this matters: Semantic versioning is a contract with users. A patch - # bump (1.2.3 โ†’ 1.2.4) promises only bug fixes, no breaking changes. A minor - # bump (1.2.3 โ†’ 1.3.0) promises new features but no breaking changes. Only - # major bumps (1.2.3 โ†’ 2.0.0) can break compatibility. Breaking this contract - # causes downstream breakage and frustration. - # - # Semver rules applied: - # - For 1.0.0+: Breaking changes require major bump (X.0.0) - # - For 0.x.y: Breaking changes require minor bump (0.X.0) - # (0.x is considered unstable, so minor bump = breaking OK) - # - Patch bumps: Never allow breaking changes - # - # Tool: cargo-semver-checks analyzes the compiled crate's public API and - # detects breaking changes like removed functions, changed signatures, etc. - # ========================================================================== - - - name: Install cargo-semver-checks - uses: cargo-bins/cargo-binstall@main - - run: cargo binstall --no-confirm cargo-semver-checks - - - name: Check semver compatibility + + publish-gl-sdk: + name: Publish gl-sdk + needs: publish-gl-client + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + version: '23.2' + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Wait for gl-client on crates.io index + if: ${{ inputs.dry_run == false }} run: | - echo "๐Ÿ” CHECK 4/5: Running semver checks..." - - CRATE="${{ steps.parse.outputs.crate }}" - - # Check if this crate has been published before - # If not, we can't run semver checks (nothing to compare against) - if ! cargo info "$CRATE" --registry crates-io &>/dev/null; then - echo "โ„น๏ธ Crate not yet published to crates.io - skipping semver check" - echo " (First release has nothing to compare against)" - exit 0 - fi - - # Determine if breaking changes are allowed based on the version bump - MAJOR="${{ steps.parse.outputs.major }}" - MINOR="${{ steps.parse.outputs.minor }}" - PATCH="${{ steps.parse.outputs.patch }}" - - SKIP_CHECK=false - - # Check if this is a major version bump (X.0.0) - if [ "$MAJOR" != "0" ]; then - # For stable versions (1.0.0+), only X.0.0 allows breaking changes - if [ "$MINOR" == "0" ] && [ "$PATCH" == "0" ]; then - echo "โ„น๏ธ Major version bump detected ($MAJOR.0.0)" - echo " Breaking changes are allowed per semver rules" - SKIP_CHECK=true + for i in $(seq 1 30); do + if cargo info "gl-client@${{ inputs.gl_client_version }}" --registry crates-io &>/dev/null; then + echo "gl-client ${{ inputs.gl_client_version }} available on crates.io" + exit 0 fi + echo "Waiting for crates.io index... (attempt $i/30)" + sleep 20 + done + echo "::error::Timed out waiting for gl-client on crates.io" + exit 1 + + - name: Publish gl-sdk + run: | + if [ "${{ inputs.dry_run }}" = "true" ]; then + echo "=== DRY RUN: cargo publish --dry-run ===" + cargo publish -p gl-sdk --dry-run else - # For pre-1.0 versions (0.x.y), 0.X.0 allows breaking changes - # Rationale: 0.x versions are considered unstable/development - if [ "$PATCH" == "0" ]; then - echo "โ„น๏ธ Minor version bump in 0.x series (0.$MINOR.0)" - echo " Breaking changes are allowed in 0.x per semver rules" - SKIP_CHECK=true + if cargo info "gl-sdk@${{ inputs.gl_sdk_version }}" --registry crates-io &>/dev/null; then + echo "gl-sdk ${{ inputs.gl_sdk_version }} already published -- skipping" + else + cargo publish -p gl-sdk fi fi - - if [ "$SKIP_CHECK" = true ]; then - echo "โญ๏ธ Skipping semver check (breaking changes expected for this bump)" - exit 0 - fi - - # Run semver checks - # If this fails, it means there are breaking changes but the version - # bump doesn't allow them (e.g., patch bump with breaking changes) - echo "Running cargo-semver-checks..." - echo "(Breaking changes are NOT allowed for this version bump)" - - if cargo semver-checks check-release -p "$CRATE"; then - echo "โœ… No semver violations detected" + + publish-gl-sdk-cli: + name: Publish gl-sdk-cli + needs: publish-gl-sdk + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + version: '23.2' + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Wait for gl-sdk on crates.io index + if: ${{ inputs.dry_run == false }} + run: | + for i in $(seq 1 30); do + if cargo info "gl-sdk@${{ inputs.gl_sdk_version }}" --registry crates-io &>/dev/null; then + echo "gl-sdk ${{ inputs.gl_sdk_version }} available on crates.io" + exit 0 + fi + echo "Waiting for crates.io index... (attempt $i/30)" + sleep 20 + done + echo "::error::Timed out waiting for gl-sdk on crates.io" + exit 1 + + - name: Publish gl-sdk-cli + run: | + if [ "${{ inputs.dry_run }}" = "true" ]; then + echo "=== DRY RUN: cargo publish --dry-run ===" + cargo publish -p gl-sdk-cli --dry-run else - echo "" - echo "โŒ ERROR: Semver violations detected!" - echo "" - echo "Breaking changes were found, but this version bump doesn't allow them." - echo "" - echo "Your options:" - echo " 1. Fix the breaking changes to maintain API compatibility" - if [ "$MAJOR" != "0" ]; then - echo " 2. Bump to next major version (e.g., $MAJOR.0.0 โ†’ $(($MAJOR + 1)).0.0)" + if cargo info "gl-sdk-cli@${{ inputs.gl_sdk_cli_version }}" --registry crates-io &>/dev/null; then + echo "gl-sdk-cli ${{ inputs.gl_sdk_cli_version }} already published -- skipping" else - echo " 2. Bump to next minor version (e.g., 0.$MINOR.x โ†’ 0.$(($MINOR + 1)).0)" + cargo publish -p gl-sdk-cli fi - echo "" - echo "See cargo-semver-checks output above for details on what broke." - exit 1 fi - - # ========================================================================== - # CHECK 5: Run Tests - # ========================================================================== - # Rationale: Final verification that the crate's tests pass before publish. - # - # Why this matters: Even though tests should have passed in CI on the PR, - # we run them again here as a final gate. This catches edge cases like: - # - Someone force-pushed changes after CI passed - # - Flaky tests that passed in CI but might fail now - # - Issues with the tag itself or checkout process - # - # Note: We use `-p ` to only test this specific crate, not the - # entire workspace. This is faster and only tests what we're publishing. - # ========================================================================== - - - name: Run tests + + # ============================================================================ + # STAGE 3: BUILD BINDINGS (parallel, via reusable workflows) + # ============================================================================ + build-swift: + name: Build Swift + needs: validate + uses: ./.github/workflows/build-swift.yml + with: + ref: ${{ github.sha }} + + build-kotlin: + name: Build Kotlin + needs: validate + uses: ./.github/workflows/build-kotlin.yml + with: + ref: ${{ github.sha }} + + build-napi: + name: Build N-API + needs: validate + uses: ./.github/workflows/build-napi.yml + with: + ref: ${{ github.sha }} + + build-python: + name: Build Python + needs: validate + uses: ./.github/workflows/build-python.yml + with: + ref: ${{ github.sha }} + + # ============================================================================ + # STAGE 4: PUBLISH BINDINGS & CREATE RELEASES + # ============================================================================ + # This job only runs when dry_run is false. It publishes all binding + # artifacts to their respective registries, pushes git tags, and + # creates GitHub Releases. + # ============================================================================ + publish-bindings: + name: Publish bindings to registries + needs: + - publish-gl-sdk-cli + - build-swift + - build-kotlin + - build-napi + - build-python + runs-on: ubuntu-latest + if: ${{ inputs.dry_run == false }} + steps: + - uses: actions/checkout@v6 + + - name: Download all build artifacts + uses: actions/download-artifact@v8 + with: + path: artifacts + + - name: List artifacts + run: find artifacts -type f | sort + + # -- npm ----------------------------------------------------------- + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' + + - name: Publish to npm + working-directory: libs/gl-sdk-napi + run: | + npm ci + find ../../artifacts/napi-* -name "*.node" -exec cp {} . \; + ls -la *.node index.js index.d.ts 2>/dev/null || true + npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + # -- PyPI ---------------------------------------------------------- + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + + - name: Publish to PyPI run: | - echo "๐Ÿ” CHECK 5/5: Running tests..." - - # -p flag: Run tests only for this specific crate - # This is important in a monorepo to avoid testing unrelated crates - cargo test -p ${{ steps.parse.outputs.crate }} - - echo "โœ… All tests passed" - - # ========================================================================== - # PUBLISH: Publish to crates.io and Create GitHub Release - # ========================================================================== - # Rationale: All checks passed, now we can safely publish. - # - # Why two steps (crates.io + GitHub release): - # 1. crates.io publish - Makes the crate available to Rust users - # 2. GitHub release - Creates a release on GitHub with changelog notes, - # providing a nice UI for browsing releases and downloading source - # - # Note: Publishing to crates.io is IRREVERSIBLE. You cannot unpublish, - # only yank (which hides from new installs but doesn't delete). That's - # why we have all the checks above! - # ========================================================================== - - - name: Publish to crates.io - if: steps.parse.outputs.dry_run == 'false' + mkdir -p pypi-dist + find artifacts/python-* -name "*.whl" -o -name "*.tar.gz" | xargs -I{} cp {} pypi-dist/ + ls pypi-dist/ + uv publish pypi-dist/* + env: + UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }} + + # -- Swift --------------------------------------------------------- + - name: Checkout gl-sdk-swift repo + uses: actions/checkout@v6 + with: + repository: Blockstream/gl-sdk-swift + token: ${{ secrets.SWIFT_RELEASE_TOKEN }} + path: swift-repo + + - name: Render Swift package manifests and copy sources + env: + VERSION: gl-sdk-v${{ inputs.gl_sdk_version }} + XCF_CHECKSUM: ${{ needs.build-swift.outputs.xcframework_checksum }} run: | - CRATE="${{ steps.parse.outputs.crate }}" - VERSION="${{ steps.parse.outputs.version }}" - echo "๐Ÿš€ Publishing $CRATE v$VERSION to crates.io..." + cp artifacts/swift-generated-source/glsdk.swift \ + swift-repo/Sources/GreenlightSDK/GreenlightSDK.swift - # Check if this version is already published (idempotent retries) - if cargo info "${CRATE}@${VERSION}" --registry crates-io &>/dev/null; then - echo "โœ… $CRATE v$VERSION is already published on crates.io โ€” skipping" - else - cargo publish -p "$CRATE" - echo "โœ… Successfully published to crates.io!" - fi - echo "๐Ÿ”— https://crates.io/crates/$CRATE" + VARS='$VERSION:$XCF_CHECKSUM' + envsubst "$VARS" < swift-repo/templates/Package.swift > swift-repo/Package.swift + envsubst "$VARS" < swift-repo/templates/glsdkFFI.podspec > swift-repo/glsdkFFI.podspec + envsubst "$VARS" < swift-repo/templates/GreenlightSDK.podspec > swift-repo/GreenlightSDK.podspec + + - name: Commit, tag, and push to gl-sdk-swift + working-directory: swift-repo env: - # The token is stored in GitHub Secrets for security - # To create one: https://crates.io/settings/tokens - # Add it to repo: Settings โ†’ Secrets and variables โ†’ Actions - CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} - - - name: Create GitHub Release - if: steps.parse.outputs.dry_run == 'false' - uses: softprops/action-gh-release@v1 + VERSION: gl-sdk-v${{ inputs.gl_sdk_version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add Package.swift Sources/ glsdkFFI.podspec GreenlightSDK.podspec + git commit -m "Bump Greenlight SDK to version $VERSION" + git tag "$VERSION" -m "$VERSION" + git push origin main + git push origin "$VERSION" + + - name: Create Swift GitHub Release with XCFramework + uses: softprops/action-gh-release@v3 + with: + repository: Blockstream/gl-sdk-swift + token: ${{ secrets.SWIFT_RELEASE_TOKEN }} + tag_name: gl-sdk-v${{ inputs.gl_sdk_version }} + name: gl-sdk-v${{ inputs.gl_sdk_version }} + files: artifacts/swift-xcframework/glsdkFFI.xcframework.zip + prerelease: false + + # -- Git tags & GitHub Releases ------------------------------------ + - name: Create git tags + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "gl-client-v${{ inputs.gl_client_version }}" -m "gl-client v${{ inputs.gl_client_version }}" + git tag "gl-sdk-v${{ inputs.gl_sdk_version }}" -m "gl-sdk v${{ inputs.gl_sdk_version }}" + git tag "gl-sdk-cli-v${{ inputs.gl_sdk_cli_version }}" -m "gl-sdk-cli v${{ inputs.gl_sdk_cli_version }}" + git push origin \ + "gl-client-v${{ inputs.gl_client_version }}" \ + "gl-sdk-v${{ inputs.gl_sdk_version }}" \ + "gl-sdk-cli-v${{ inputs.gl_sdk_cli_version }}" + + - name: Create GitHub Release for gl-client + uses: softprops/action-gh-release@v3 + with: + tag_name: gl-client-v${{ inputs.gl_client_version }} + name: gl-client v${{ inputs.gl_client_version }} + body: | + Published gl-client v${{ inputs.gl_client_version }} to [crates.io](https://crates.io/crates/gl-client/${{ inputs.gl_client_version }}). + + - name: Create GitHub Release for gl-sdk + uses: softprops/action-gh-release@v3 with: - # Use the tag that triggered this workflow - tag_name: ${{ github.ref }} + tag_name: gl-sdk-v${{ inputs.gl_sdk_version }} + name: gl-sdk v${{ inputs.gl_sdk_version }} + body: | + Published gl-sdk v${{ inputs.gl_sdk_version }} to [crates.io](https://crates.io/crates/gl-sdk/${{ inputs.gl_sdk_version }}). - # Format: "mycrate v1.2.3" - name: ${{ steps.parse.outputs.crate }} v${{ steps.parse.outputs.version }} + Bindings published: + - Swift: [gl-sdk-swift gl-sdk-v${{ inputs.gl_sdk_version }}](https://github.com/Blockstream/gl-sdk-swift/releases/tag/gl-sdk-v${{ inputs.gl_sdk_version }}) + - npm: [@greenlightcln/glsdk](https://www.npmjs.com/package/@greenlightcln/glsdk) + - PyPI: [gl-client](https://pypi.org/project/gl-client/) - # Release notes body - # In the future, you could extract the changelog section here - # to automatically populate the release notes + - name: Create GitHub Release for gl-sdk-cli + uses: softprops/action-gh-release@v3 + with: + tag_name: gl-sdk-cli-v${{ inputs.gl_sdk_cli_version }} + name: gl-sdk-cli v${{ inputs.gl_sdk_cli_version }} body: | - Published ${{ steps.parse.outputs.crate }} v${{ steps.parse.outputs.version }} to crates.io + Published gl-sdk-cli v${{ inputs.gl_sdk_cli_version }} to [crates.io](https://crates.io/crates/gl-sdk-cli/${{ inputs.gl_sdk_cli_version }}). + + # ============================================================================ + # DRY-RUN: test npm publish + # ============================================================================ + dry-run-npm: + name: Dry-run npm publish + needs: + - build-napi + - publish-gl-sdk-cli + runs-on: ubuntu-latest + if: ${{ inputs.dry_run == true }} + steps: + - uses: actions/checkout@v6 - ๐Ÿ”— [View on crates.io](https://crates.io/crates/${{ steps.parse.outputs.crate }}) + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' - See [CHANGELOG](https://github.com/${{ github.repository }}/blob/${{ github.ref_name }}/CHANGELOG.md) for details. - env: - # GITHUB_TOKEN is automatically provided by GitHub Actions - # No need to create this secret manually - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Download N-API artifacts + uses: actions/download-artifact@v8 + with: + path: artifacts - - name: Dry-run complete - if: steps.parse.outputs.dry_run == 'true' + - name: Dry-run npm publish + working-directory: libs/gl-sdk-napi + run: | + npm ci + find ../../artifacts/napi-* -name "*.node" -exec cp {} . \; + ls -la *.node index.js index.d.ts 2>/dev/null || true + echo "=== DRY RUN: npm publish --dry-run ===" + npm publish --access public --dry-run + + # ============================================================================ + # DRY-RUN SUMMARY + # ============================================================================ + dry-run-summary: + name: Dry-run summary + needs: + - validate + - publish-gl-sdk-cli + - build-swift + - build-kotlin + - build-napi + - build-python + - dry-run-npm + runs-on: ubuntu-latest + if: ${{ inputs.dry_run == true }} + steps: + - name: Summary run: | - echo "โœ… DRY-RUN COMPLETE!" + echo "=============================================" + echo " DRY RUN COMPLETE" + echo "=============================================" echo "" - echo "All validation checks passed for ${{ steps.parse.outputs.crate }} v${{ steps.parse.outputs.version }}" + echo "All validation checks passed." + echo "All binding builds completed successfully." + echo "Cargo dry-run publishes passed." + echo "npm dry-run publish passed." echo "" - echo "The following checks were successful:" - echo " โœ… Version matches Cargo.toml" - echo " โœ… Changelog format is valid" - echo " โœ… Changelog entry exists" - echo " โœ… Semver compatibility check passed" - echo " โœ… All tests passed" + echo "Versions validated:" + echo " gl-client: ${{ inputs.gl_client_version }}" + echo " gl-sdk: ${{ inputs.gl_sdk_version }}" + echo " gl-sdk-cli: ${{ inputs.gl_sdk_cli_version }}" echo "" - echo "To publish for real, push a tag:" - echo " git tag ${{ steps.parse.outputs.crate }}-v${{ steps.parse.outputs.version }}" - echo " git push origin ${{ steps.parse.outputs.crate }}-v${{ steps.parse.outputs.version }}" - -# ============================================================================== -# MAINTENANCE NOTES -# ============================================================================== -# -# Common modifications you might want to make: -# -# 1. Change tag format: -# - Modify the regex in the `on.push.tags` section -# - Update the parsing logic in the "Parse tag" step -# -# 2. Skip semver checks entirely (not recommended): -# - Remove or comment out CHECK 4 -# -# 3. Add additional checks: -# - cargo clippy for linting -# - cargo fmt --check for formatting -# - Documentation build check: cargo doc --no-deps -# -# 4. Auto-extract changelog for GitHub release: -# - Use kacl-cli or similar tool to extract the version section -# - Replace the static body in "Create GitHub Release" -# -# 5. Publish multiple crates at once: -# - This workflow publishes one crate at a time by design -# - For dependency order, consider using cargo-release instead -# -# 6. Customize python-kacl validation rules: -# - Create a .kacl.yml file in your crate directories -# - Pass it to kacl-cli: kacl-cli verify --config .kacl.yml -# -# Troubleshooting: -# -# - If semver-checks fails unexpectedly: -# Run locally: cargo semver-checks check-release -p -# -# - If changelog validation fails: -# Run locally: kacl-cli verify --json CHANGELOG.md | jq -# -# - If version mismatch: -# Ensure you updated Cargo.toml before tagging -# -# - If crate not found: -# Check workspace members in root Cargo.toml -# ============================================================================== + echo "To publish for real, re-run with dry_run=false." diff --git a/.github/workflows/rust-unit.yml b/.github/workflows/rust-unit.yml deleted file mode 100644 index 71b7145d5..000000000 --- a/.github/workflows/rust-unit.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Rust Unit Test - -on: - pull_request: - types: - - synchronize - - opened - workflow_dispatch: - merge_group: - -jobs: - unit_test: - name: Rust unit tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - uses: arduino/setup-protoc@v3 - with: - version: "23.2" # Fixed since we mount the path below - repo-token: ${{ secrets.GITHUB_TOKEN }} - - run: (cd libs; cargo test) diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml deleted file mode 100644 index 160f87523..000000000 --- a/.github/workflows/typescript.yml +++ /dev/null @@ -1,286 +0,0 @@ -on: - push: - paths: - - '.github/workflows/typescript.yml' - - 'libs/gl-sdk-napi/**' - branches: - - main - pull_request: {} - workflow_dispatch: - inputs: - shouldPublish: - description: 'Publish to NPM' - type: boolean - default: true - -name: Typescript Library - -jobs: - check-version: - runs-on: ubuntu-latest - outputs: - version-changed: ${{ steps.check.outputs.changed }} - defaults: - run: - working-directory: libs/gl-sdk-napi - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 2 - - - id: check - run: | - CURRENT_VERSION=$(cat package.json | jq -r '.version') - git checkout HEAD^ - PREVIOUS_VERSION=$(cat package.json | jq -r '.version') - git checkout - - if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then - echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION" - echo "changed=true" >> $GITHUB_OUTPUT - else - echo "::warning::Will not trigger publishing because version is unchanged" - echo "changed=false" >> $GITHUB_OUTPUT - exit 0 - fi - - build: - needs: check-version - if: needs.check-version.outputs.version-changed == 'true' - strategy: - fail-fast: false - matrix: - settings: - - host: ubuntu-latest - target: x86_64-unknown-linux-gnu - strip: strip -x *.node - - - host: ubuntu-latest - target: aarch64-unknown-linux-gnu - strip: aarch64-linux-gnu-strip -x *.node - - - host: macos-15-intel - target: x86_64-apple-darwin - strip: strip -x *.node - - - host: macos-14 - target: aarch64-apple-darwin - strip: strip -x *.node - - - host: windows-latest - target: x86_64-pc-windows-msvc - strip: "" - - name: Build - ${{ matrix.settings.target }} - runs-on: ${{ matrix.settings.host }} - - defaults: - run: - working-directory: libs/gl-sdk-napi - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - cache-dependency-path: libs/gl-sdk-napi/package-lock.json - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.settings.target }} - - - name: Install protobuf compiler (Linux) - if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y protobuf-compiler - - - name: Install protobuf compiler (macOS) - if: runner.os == 'macOS' - run: brew install protobuf - - - name: Install protobuf compiler (Windows) - if: runner.os == 'Windows' - run: choco install protoc - - - name: Setup cross-compilation (Linux ARM64) - if: matrix.settings.target == 'aarch64-unknown-linux-gnu' - run: | - sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu - echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV - - - name: Cache cargo registry - uses: actions/cache@v4 - with: - path: ~/.cargo/registry/cache - key: ${{ matrix.settings.target }}-cargo-registry - - - name: Cache cargo index - uses: actions/cache@v4 - with: - path: ~/.cargo/registry/index - key: ${{ matrix.settings.target }}-cargo-index - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build -- --target ${{ matrix.settings.target }} - shell: bash - - - name: Strip binary - if: matrix.settings.strip != '' - run: ${{ matrix.settings.strip }} - shell: bash - - - name: List build output - run: ls -la *.node index.js index.d.ts - shell: bash - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: bindings-${{ matrix.settings.target }} - path: | - libs/gl-sdk-napi/*.node - libs/gl-sdk-napi/index.js - libs/gl-sdk-napi/index.d.ts - if-no-files-found: error - - test: - name: Test - ${{ matrix.settings.target }} - needs: build - continue-on-error: true - strategy: - fail-fast: false - matrix: - settings: - - host: ubuntu-latest - target: x86_64-unknown-linux-gnu - runs-on: ${{ matrix.settings.host }} - - defaults: - run: - working-directory: libs/gl-sdk-napi - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - cache-dependency-path: libs/gl-sdk-napi/package-lock.json - - - name: Install uv - uses: astral-sh/setup-uv@v7 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: 'stable' - cache: false - - - name: Install cfssl - shell: bash - run: | - go install github.com/cloudflare/cfssl/cmd/cfssl@latest - go install github.com/cloudflare/cfssl/cmd/cfssljson@latest - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - - - name: Install Protoc - uses: arduino/setup-protoc@v3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install bitcoind - run: | - BITCOIN_VERSION=28.2 - curl -sO https://bitcoincore.org/bin/bitcoin-core-${BITCOIN_VERSION}/bitcoin-${BITCOIN_VERSION}-x86_64-linux-gnu.tar.gz - tar -xzf bitcoin-${BITCOIN_VERSION}-x86_64-linux-gnu.tar.gz - sudo install -m 0755 -t /usr/local/bin bitcoin-${BITCOIN_VERSION}/bin/* - - - name: Install lightningd via clnvm - shell: bash - run: | - CLN_PATH=$(uv run --package cln-version-manager --with click --with rich clnvm get --tag v25.12gl1) - echo "$(dirname "$CLN_PATH")" >> $GITHUB_PATH - - - name: Pre-build Python test dependencies - shell: bash - run: uv sync --package gl-testing - - - name: Install dependencies - run: npm ci - - - name: Download artifact - uses: actions/download-artifact@v4 - with: - name: bindings-${{ matrix.settings.target }} - path: libs/gl-sdk-napi - - - name: List downloaded files - run: ls -la *.node index.js index.d.ts - shell: bash - - - name: Run tests - uses: nick-fields/retry@v3 - with: - timeout_minutes: 5 - max_attempts: 3 - retry_wait_seconds: 10 - shell: bash - command: cd libs/gl-sdk-napi && npm test -- tests/basic.spec.ts - - publish: - name: Publish to NPM - runs-on: ubuntu-latest - needs: test - if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'workflow_dispatch' && inputs.shouldPublish == true) - - defaults: - run: - working-directory: libs/gl-sdk-napi - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - registry-url: 'https://registry.npmjs.org' - - - name: Install dependencies - run: npm ci - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Move artifacts to package directory - run: | - for dir in ../../artifacts/bindings-*; do - if [ -d "$dir" ]; then - echo "Processing $dir" - cp "$dir"/* . 2>/dev/null || true - fi - done - ls -la *.* - - - name: List package contents - run: npm pack --dry-run - - - name: Publish to NPM - run: | - echo "Token length: ${#NODE_AUTH_TOKEN}" - npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/Cargo.toml b/Cargo.toml index 5aef094ee..f2f182dfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ lto = true opt-level = "z" codegen-units = 1 -panic = "abort" # The profile that 'cargo dist' will build with [profile.dist] diff --git a/examples/rust/snippets/getting_started.rs b/examples/rust/snippets/getting_started.rs index f07bb4016..fd9c38dc1 100644 --- a/examples/rust/snippets/getting_started.rs +++ b/examples/rust/snippets/getting_started.rs @@ -1,10 +1,13 @@ -use anyhow::{Result}; +use anyhow::Result; use bip39::{Language, Mnemonic}; use gl_client::{ bitcoin::Network, credentials::{Device, Nobody}, node::ClnClient, - pb::{cln, cln::{amount_or_any, Amount, AmountOrAny}}, + pb::{ + cln, + cln::{amount_or_any, Amount, AmountOrAny}, + }, scheduler::Scheduler, signer::Signer, }; @@ -76,7 +79,10 @@ fn load_developer_creds() -> Result { Ok(developer_creds) } -async fn register_node(seed: Vec, developer_creds: Nobody) -> Result<(Scheduler, Device, Signer)> { +async fn register_node( + seed: Vec, + developer_creds: Nobody, +) -> Result<(Scheduler, Device, Signer)> { // ---8<--- [start: init_signer] let signer = Signer::new(seed.clone(), NETWORK, developer_creds.clone())?; // ---8<--- [end: init_signer] @@ -102,7 +108,13 @@ async fn get_node(scheduler: &Scheduler) -> Result { Ok(node) } -async fn start_node(device_creds_file_path: &str) -> Result<(cln::GetinfoResponse, cln::ListpeersResponse, cln::InvoiceResponse)> { +async fn start_node( + device_creds_file_path: &str, +) -> Result<( + cln::GetinfoResponse, + cln::ListpeersResponse, + cln::InvoiceResponse, +)> { // ---8<--- [start: start_node] let creds = Device::from_path(device_creds_file_path); let scheduler = Scheduler::new(NETWORK, creds.clone()).await?; @@ -168,21 +180,28 @@ async fn main() -> Result<()> { println!("Getting node information..."); let device_scheduler = Scheduler::new(NETWORK, device_creds.clone()).await?; - let _gl_node = get_node(&device_scheduler).await?; + let _gl_node = get_node(&device_scheduler).await?; - let (info, peers, invoice) = start_node(&format!("{TEST_NODE_DATA_DIR}/credentials.gfs")).await?; + let (info, peers, invoice) = + start_node(&format!("{TEST_NODE_DATA_DIR}/credentials.gfs")).await?; println!("Node pubkey: {}", hex::encode(info.id)); println!("Peers list: {:?}", peers.peers); println!("Invoice created: {}", invoice.bolt11); println!("Upgrading certs..."); - let _upgraded = upgrade_device_certs_to_creds(&scheduler, &signer, &format!("{TEST_NODE_DATA_DIR}/credentials.gfs")).await?; + let _upgraded = upgrade_device_certs_to_creds( + &scheduler, + &signer, + &format!("{TEST_NODE_DATA_DIR}/credentials.gfs"), + ) + .await?; println!("Recovering node..."); let (_scheduler2, _device_creds2, _signer2) = recover_node(developer_creds.clone()).await?; println!("Node Recovered!"); - let (info, _peers, _invoice) = start_node(&format!("{TEST_NODE_DATA_DIR}/credentials.gfs")).await?; + let (info, _peers, _invoice) = + start_node(&format!("{TEST_NODE_DATA_DIR}/credentials.gfs")).await?; println!("Node pubkey: {}", hex::encode(info.id)); println!("All steps completed successfully!"); diff --git a/libs/gl-cli/src/signer.rs b/libs/gl-cli/src/signer.rs index e1f8acbc2..568e41537 100644 --- a/libs/gl-cli/src/signer.rs +++ b/libs/gl-cli/src/signer.rs @@ -2,9 +2,7 @@ use crate::error::{Error, Result}; use crate::util; use clap::{Subcommand, ValueEnum}; use core::fmt::Debug; -use gl_client::signer::{ - Signer, SignerConfig, StateSignatureMode, StateSignatureOverrideConfig, -}; +use gl_client::signer::{Signer, SignerConfig, StateSignatureMode, StateSignatureOverrideConfig}; use lightning_signer::bitcoin::Network; use std::path::Path; use tokio::{join, signal}; @@ -110,11 +108,9 @@ async fn run_handler>( )); } - let state_signature_override = state_override.map(|ack| { - StateSignatureOverrideConfig { - ack, - note: state_override_note, - } + let state_signature_override = state_override.map(|ack| StateSignatureOverrideConfig { + ack, + note: state_override_note, }); let signer = Signer::new_with_config( diff --git a/libs/gl-client-py/src/credentials.rs b/libs/gl-client-py/src/credentials.rs index 26be9cc8e..d8f74b182 100644 --- a/libs/gl-client-py/src/credentials.rs +++ b/libs/gl-client-py/src/credentials.rs @@ -182,7 +182,7 @@ impl Credentials { let d = creds.clone().with_ca(ca); let inner = UnifiedCredentials::Device(d); Self { inner } - }, + } } } } diff --git a/libs/gl-client-py/src/pairing.rs b/libs/gl-client-py/src/pairing.rs index 39b728033..1cd2637d2 100644 --- a/libs/gl-client-py/src/pairing.rs +++ b/libs/gl-client-py/src/pairing.rs @@ -61,12 +61,7 @@ impl AttestationDeviceClient { }))?) } - fn approve_pairing( - &self, - device_id: &str, - device_name: &str, - restrs: &str, - ) -> Result> { + fn approve_pairing(&self, device_id: &str, device_name: &str, restrs: &str) -> Result> { Ok(convert(exec(async move { self.inner .approve_pairing(device_id, device_name, restrs) diff --git a/libs/gl-client-py/src/scheduler.rs b/libs/gl-client-py/src/scheduler.rs index b63acfc2b..d550e4748 100644 --- a/libs/gl-client-py/src/scheduler.rs +++ b/libs/gl-client-py/src/scheduler.rs @@ -3,8 +3,8 @@ use crate::runtime::exec; use crate::Signer; use anyhow::{anyhow, Result}; use gl_client::bitcoin::Network; -use gl_client::credentials::{NodeIdProvider, RuneProvider}; use gl_client::credentials::TlsConfigProvider; +use gl_client::credentials::{NodeIdProvider, RuneProvider}; use gl_client::pb; use gl_client::scheduler; use prost::Message; @@ -153,16 +153,14 @@ impl Scheduler { let inner = match creds.inner { crate::credentials::UnifiedCredentials::Nobody(_) => { let scheduler = exec(async move { - gl_client::scheduler::Scheduler::with(network, creds.inner.clone(), uri) - .await + gl_client::scheduler::Scheduler::with(network, creds.inner.clone(), uri).await }) .map_err(|e| PyValueError::new_err(e.to_string()))?; UnifiedScheduler::Unauthenticated(scheduler) } crate::credentials::UnifiedCredentials::Device(_) => { let scheduler = exec(async move { - gl_client::scheduler::Scheduler::with(network, creds.inner.clone(), uri) - .await + gl_client::scheduler::Scheduler::with(network, creds.inner.clone(), uri).await }) .map_err(|e| PyValueError::new_err(e.to_string()))?; UnifiedScheduler::Authenticated(scheduler) @@ -195,9 +193,7 @@ impl Scheduler { e.to_string() )) })?; - Ok(Scheduler { - inner: s, - }) + Ok(Scheduler { inner: s }) } fn export_node(&self) -> PyResult> { diff --git a/libs/gl-client/src/credentials.rs b/libs/gl-client/src/credentials.rs index 4d44875af..8ba7fbe1a 100644 --- a/libs/gl-client/src/credentials.rs +++ b/libs/gl-client/src/credentials.rs @@ -193,11 +193,11 @@ impl Device { cert: cert.into(), key: key.into(), rune: rune.into(), - ca + ca, } } - pub fn with_ca(self, ca: V) -> Self + pub fn with_ca(self, ca: V) -> Self where V: Into>, { @@ -248,7 +248,6 @@ impl TlsConfigProvider for Device { fn tls_config(&self) -> TlsConfig { tls::TlsConfig::with(&self.cert, &self.key, &self.ca) } - } impl RuneProvider for Device { @@ -344,8 +343,16 @@ fn load_file_or_default(varname: &str, default: &[u8]) -> Result> { match std::env::var(varname) { Ok(fname) => { debug!("Loading file {} for envvar {}", fname, varname); - let f = std::fs::read(fname.clone())?; - Ok(f) + match std::fs::read(&fname) { + Ok(f) => Ok(f), + Err(e) => { + debug!( + "Failed to read {} from {}: {}, using compiled-in default", + varname, fname, e + ); + Ok(default.to_vec()) + } + } } Err(_) => Ok(default.to_vec()), } diff --git a/libs/gl-client/src/lib.rs b/libs/gl-client/src/lib.rs index 80508bed8..0cac39f1a 100644 --- a/libs/gl-client/src/lib.rs +++ b/libs/gl-client/src/lib.rs @@ -28,8 +28,8 @@ pub mod scheduler; /// move your funds. pub mod signer; -pub mod persist; pub mod metrics; +pub mod persist; pub mod lnurl; diff --git a/libs/gl-client/src/lnurl/mod.rs b/libs/gl-client/src/lnurl/mod.rs index 5c63a4084..d11655cd9 100644 --- a/libs/gl-client/src/lnurl/mod.rs +++ b/libs/gl-client/src/lnurl/mod.rs @@ -37,10 +37,7 @@ impl LNURL { pub async fn resolve(&self, url: &str) -> Result { let json = self.http_client.get_json(url).await?; - let tag = json - .get("tag") - .and_then(|t| t.as_str()) - .unwrap_or(""); + let tag = json.get("tag").and_then(|t| t.as_str()).unwrap_or(""); match tag { "payRequest" => { @@ -84,8 +81,7 @@ impl LNURL { node: &mut ClnClient, ) -> Result<()> { let url = parse_lnurl(lnurl)?; - let withdrawal_request_response = - withdraw::parse_withdraw_request_response_from_url(&url); + let withdrawal_request_response = withdraw::parse_withdraw_request_response_from_url(&url); let withdrawal_request_response = match withdrawal_request_response { Some(w) => w, @@ -109,8 +105,7 @@ impl LNURL { .map_err(|e| anyhow!(e))? .into_inner(); - let callback_url = - withdrawal_request_response.build_callback_url(&invoice.bolt11)?; + let callback_url = withdrawal_request_response.build_callback_url(&invoice.bolt11)?; let _ = self .http_client diff --git a/libs/gl-client/src/lnurl/models.rs b/libs/gl-client/src/lnurl/models.rs index 547cdd5ab..2f1c24f8e 100644 --- a/libs/gl-client/src/lnurl/models.rs +++ b/libs/gl-client/src/lnurl/models.rs @@ -86,9 +86,17 @@ pub enum SuccessAction { /// plaintext using the payment preimage. #[derive(Clone, Debug, PartialEq, Eq)] pub enum ProcessedSuccessAction { - Message { message: String }, - Url { description: String, url: String }, - Aes { description: String, plaintext: String }, + Message { + message: String, + }, + Url { + description: String, + url: String, + }, + Aes { + description: String, + plaintext: String, + }, } impl SuccessAction { @@ -137,8 +145,7 @@ impl SuccessAction { iv.len() == 24, "AES success action IV must be exactly 24 base64 chars" ); - let plaintext = - super::pay::decrypt_aes_success_action(preimage, &ciphertext, &iv)?; + let plaintext = super::pay::decrypt_aes_success_action(preimage, &ciphertext, &iv)?; Ok(ProcessedSuccessAction::Aes { description, plaintext, @@ -226,7 +233,8 @@ mod tests { #[test] fn test_success_action_url_serde() { - let json = r#"{"tag":"url","description":"View order","url":"https://example.com/order/123"}"#; + let json = + r#"{"tag":"url","description":"View order","url":"https://example.com/order/123"}"#; let action: SuccessAction = serde_json::from_str(json).unwrap(); match action { SuccessAction::Url { description, url } => { diff --git a/libs/gl-client/src/lnurl/pay/mod.rs b/libs/gl-client/src/lnurl/pay/mod.rs index 8ece34da4..3d734d078 100644 --- a/libs/gl-client/src/lnurl/pay/mod.rs +++ b/libs/gl-client/src/lnurl/pay/mod.rs @@ -27,16 +27,10 @@ impl PayRequestResponse { } if amount_msats < self.min_sendable { - return Err(anyhow!( - "Amount must be {} or greater", - self.min_sendable - )); + return Err(anyhow!("Amount must be {} or greater", self.min_sendable)); } if amount_msats > self.max_sendable { - return Err(anyhow!( - "Amount must be {} or less", - self.max_sendable - )); + return Err(anyhow!("Amount must be {} or less", self.max_sendable)); } debug!( @@ -46,9 +40,8 @@ impl PayRequestResponse { // For lightning addresses, verify the identifier appears in metadata if !is_lnurl(identifier) { - let entries: Vec> = - serde_json::from_str(&self.metadata) - .map_err(|e| anyhow!("Failed to deserialize metadata: {}", e))?; + let entries: Vec> = serde_json::from_str(&self.metadata) + .map_err(|e| anyhow!("Failed to deserialize metadata: {}", e))?; let found = entries.iter().any(|entry| { entry.len() >= 2 @@ -119,11 +112,7 @@ pub async fn fetch_invoice( pub const LNURL_SERVICE_ERROR_PREFIX: &str = "LNURL service error: "; /// Build a callback URL with amount and optional comment query parameters. -fn build_callback_url( - callback: &str, - amount: u64, - comment: Option<&str>, -) -> Result { +fn build_callback_url(callback: &str, amount: u64, comment: Option<&str>) -> Result { let mut url = Url::parse(callback)?; url.query_pairs_mut() .append_pair("amount", &amount.to_string()); @@ -216,11 +205,12 @@ pub async fn resolve_lnurl_to_invoice( debug!("Domain: {}", Url::parse(&url).unwrap().host().unwrap()); - let pay_request: PayRequestResponse = - http_client.get_pay_request_response(&url).await?; + let pay_request: PayRequestResponse = http_client.get_pay_request_response(&url).await?; pay_request.validate(lnurl_identifier, amount_msats)?; - pay_request.get_invoice(http_client, amount_msats, comment).await + pay_request + .get_invoice(http_client, amount_msats, comment) + .await } /// Parse a lightning address into its well-known LNURL-pay URL (LUD-16). @@ -339,7 +329,10 @@ mod tests { let lnurl = "@cipherpunk.com"; let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, 100000, None).await; assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Username can not be empty")); + assert!(result + .unwrap_err() + .to_string() + .contains("Username can not be empty")); } #[tokio::test] @@ -348,7 +341,10 @@ mod tests { let lnurl = "satoshi@"; let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, 100000, None).await; assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Domain can not be empty")); + assert!(result + .unwrap_err() + .to_string() + .contains("Domain can not be empty")); } #[tokio::test] @@ -357,7 +353,10 @@ mod tests { let lnurl = "LNURL1111111111111111111111111111111111111111111111111111111111111111111"; let result = resolve_lnurl_to_invoice(&mock_http_client, lnurl, 100000, None).await; - assert!(result.unwrap_err().to_string().contains("Failed to decode lnurl: invalid length")); + assert!(result + .unwrap_err() + .to_string() + .contains("Failed to decode lnurl: invalid length")); } #[tokio::test] diff --git a/libs/gl-client/src/lnurl/utils.rs b/libs/gl-client/src/lnurl/utils.rs index 744b6949e..7b11cc3fa 100644 --- a/libs/gl-client/src/lnurl/utils.rs +++ b/libs/gl-client/src/lnurl/utils.rs @@ -83,8 +83,7 @@ mod tests { #[test] fn test_extract_description_from_metadata_with_multiple_entries() { - let metadata = - r#"[["text/identifier", "user@example.com"], ["text/plain", "Pay user"]]"#; + let metadata = r#"[["text/identifier", "user@example.com"], ["text/plain", "Pay user"]]"#; assert_eq!( extract_description_from_metadata(metadata), Some("Pay user".to_string()) diff --git a/libs/gl-client/src/lnurl/withdraw/mod.rs b/libs/gl-client/src/lnurl/withdraw/mod.rs index 5b2295f4a..e04da01f4 100644 --- a/libs/gl-client/src/lnurl/withdraw/mod.rs +++ b/libs/gl-client/src/lnurl/withdraw/mod.rs @@ -98,10 +98,7 @@ mod test { let query_params: &Map = query_pairs.as_object().unwrap(); assert_eq!(query_params.get("k1").unwrap().as_str().unwrap(), "unique"); - assert_eq!( - query_params.get("pr").unwrap().as_str().unwrap(), - "invoice" - ); + assert_eq!(query_params.get("pr").unwrap().as_str().unwrap(), "invoice"); Ok(()) } diff --git a/libs/gl-client/src/persist.rs b/libs/gl-client/src/persist.rs index dbbc37ed0..1db5c05d2 100644 --- a/libs/gl-client/src/persist.rs +++ b/libs/gl-client/src/persist.rs @@ -1058,10 +1058,13 @@ mod tests { #[test] fn state_entry_canonical_value_bytes_sorts_nested_object_keys() { - let entry = StateEntry::new(0, json!({ - "z": {"b": 1, "a": 2}, - "a": [{"d": 4, "c": 3}] - })); + let entry = StateEntry::new( + 0, + json!({ + "z": {"b": 1, "a": 2}, + "a": [{"d": 4, "c": 3}] + }), + ); let bytes = entry.canonical_value_bytes().unwrap(); diff --git a/libs/gl-client/src/scheduler.rs b/libs/gl-client/src/scheduler.rs index 8a5f68724..e7bf503a7 100644 --- a/libs/gl-client/src/scheduler.rs +++ b/libs/gl-client/src/scheduler.rs @@ -1,10 +1,10 @@ -use crate::credentials::{self, RuneProvider, NodeIdProvider, TlsConfigProvider}; +use crate::credentials::{self, NodeIdProvider, RuneProvider, TlsConfigProvider}; use crate::node::{self, GrpcClient}; use crate::pb::scheduler::scheduler_client::SchedulerClient; use crate::tls::{self}; use crate::utils::scheduler_uri; use crate::{pb, signer::Signer}; -use anyhow::{Result}; +use anyhow::Result; use lightning_signer::bitcoin::Network; use log::debug; use runeauth; diff --git a/libs/gl-client/src/signer/approver.rs b/libs/gl-client/src/signer/approver.rs index 2d7e570dd..8346b62d8 100644 --- a/libs/gl-client/src/signer/approver.rs +++ b/libs/gl-client/src/signer/approver.rs @@ -19,7 +19,7 @@ impl ReportingApprover { impl Approve for ReportingApprover { fn approve_invoice(&self, inv: &lightning_signer::invoice::Invoice) -> bool { - log::warn!("unapproved invoice: {:?}", inv); + log::warn!("unapproved invoice: {:?}", inv); self.inner.approve_invoice(inv) } fn approve_keysend( diff --git a/libs/gl-client/src/signer/auth.rs b/libs/gl-client/src/signer/auth.rs index 04af2c690..b7d5d5d39 100644 --- a/libs/gl-client/src/signer/auth.rs +++ b/libs/gl-client/src/signer/auth.rs @@ -1,39 +1,32 @@ //! Utilities used to authorize a signature request based on pending RPCs -use std::str::FromStr; -use lightning_signer::invoice::Invoice; -use vls_protocol_signer::approver::Approval; use crate::signer::model::Request; use crate::Error; +use lightning_signer::invoice::Invoice; +use std::str::FromStr; +use vls_protocol_signer::approver::Approval; pub trait Authorizer { - fn authorize( - &self, - requests: &Vec, - ) -> Result, Error>; + fn authorize(&self, requests: &Vec) -> Result, Error>; } pub struct GreenlightAuthorizer {} impl Authorizer for GreenlightAuthorizer { - fn authorize( - &self, - requests: &Vec, - ) -> Result, Error> { + fn authorize(&self, requests: &Vec) -> Result, Error> { let mut approvals = Vec::new(); for request in requests.iter() { match request { - Request::Pay(req) => { - match Invoice::from_str(&req.bolt11) { - Ok(invoice) => { - approvals.push(Approval::Invoice(invoice)); - } - Err(e) => { - return Err(crate::Error::IllegalArgument( - format!("Failed to parse invoice from Pay request: {:?}", e) - )); - } + Request::Pay(req) => match Invoice::from_str(&req.bolt11) { + Ok(invoice) => { + approvals.push(Approval::Invoice(invoice)); + } + Err(e) => { + return Err(crate::Error::IllegalArgument(format!( + "Failed to parse invoice from Pay request: {:?}", + e + ))); } - } + }, _ => {} } } diff --git a/libs/gl-client/src/signer/mod.rs b/libs/gl-client/src/signer/mod.rs index 4289fe569..d77b44149 100644 --- a/libs/gl-client/src/signer/mod.rs +++ b/libs/gl-client/src/signer/mod.rs @@ -195,16 +195,14 @@ impl Signer { )); } - let note = override_config - .note - .and_then(|n| { - let trimmed = n.trim().to_string(); - if trimmed.is_empty() { - None - } else { - Some(trimmed) - } - }); + let note = override_config.note.and_then(|n| { + let trimmed = n.trim().to_string(); + if trimmed.is_empty() { + None + } else { + Some(trimmed) + } + }); (true, note) } None => (false, None), @@ -470,7 +468,8 @@ impl Signer { if self.state_signature_mode == StateSignatureMode::Hard { usage.missing_keys.push(entry.key.clone()); if first_error.is_none() { - first_error = Some(anyhow!("missing state signature for key {}", entry.key)); + first_error = + Some(anyhow!("missing state signature for key {}", entry.key)); } } continue; @@ -479,16 +478,19 @@ impl Signer { if let Err(e) = self.verify_state_entry_signature(entry) { usage.invalid_keys.push(entry.key.clone()); if first_error.is_none() { - first_error = Some(anyhow!("invalid state signature for key {}: {}", entry.key, e)); + first_error = Some(anyhow!( + "invalid state signature for key {}: {}", + entry.key, + e + )); } } } if usage.is_used() && !self.state_signature_override_enabled { - return Err(Error::Other( - first_error - .unwrap_or_else(|| anyhow!("state signature verification failed unexpectedly")), - )); + return Err(Error::Other(first_error.unwrap_or_else(|| { + anyhow!("state signature verification failed unexpectedly") + }))); } Ok(usage) @@ -752,7 +754,6 @@ impl Signer { let incoming_state = crate::persist::State::try_from(req.signer_state.as_slice()) .map_err(|e| Error::Other(anyhow!("Failed to decode signer state: {e}")))?; - // Create sketch from incoming state (nodelet's view) so we can // send back any entries the nodelet doesn't know about yet, // including the initial VLS state created during Signer::new(). @@ -760,12 +761,13 @@ impl Signer { let prestate_log = { debug!("Updating local signer state with state from node"); - let mut state = self.state.lock().map_err(|e| { - Error::Other(anyhow!("Failed to acquire state lock: {:?}", e)) - })?; - let merge_res = state.merge(&incoming_state).map_err(|e| { - Error::Other(anyhow!("Failed to merge signer state: {:?}", e)) - })?; + let mut state = self + .state + .lock() + .map_err(|e| Error::Other(anyhow!("Failed to acquire state lock: {:?}", e)))?; + let merge_res = state + .merge(&incoming_state) + .map_err(|e| Error::Other(anyhow!("Failed to merge signer state: {:?}", e)))?; if merge_res.has_conflicts() { debug!( "State merge ignored stale versions (count={})", @@ -774,7 +776,10 @@ impl Signer { } trace!("Processing request {}", hex::encode(&req.raw)); serde_json::to_string(&*state).map_err(|e| { - Error::Other(anyhow!("Failed to serialize signer state for logging: {:?}", e)) + Error::Other(anyhow!( + "Failed to serialize signer state for logging: {:?}", + e + )) })? }; @@ -1821,7 +1826,7 @@ mod tests { context: None, raw: heartbeat_raw(), signer_state: vec![invalid1], - requests: vec![], + requests: vec![], }) .await .unwrap(); @@ -1829,7 +1834,10 @@ mod tests { let state_guard = signer.state.lock().unwrap(); state_guard.clone().into() }; - let persisted1 = snapshot1.iter().find(|e| e.key == "state/invalid1").unwrap(); + let persisted1 = snapshot1 + .iter() + .find(|e| e.key == "state/invalid1") + .unwrap(); assert_eq!(persisted1.signature, vec![4u8; COMPACT_SIGNATURE_LEN]); let mut invalid2 = mk_state_entry("state/invalid2", 1, json!({"v": 2})); @@ -1849,7 +1857,10 @@ mod tests { let state_guard = signer.state.lock().unwrap(); state_guard.clone().into() }; - let persisted2 = snapshot2.iter().find(|e| e.key == "state/invalid2").unwrap(); + let persisted2 = snapshot2 + .iter() + .find(|e| e.key == "state/invalid2") + .unwrap(); assert_eq!(persisted2.signature, vec![5u8; COMPACT_SIGNATURE_LEN]); } @@ -1932,14 +1943,20 @@ mod tests { state_guard.clone().into() }; - let persisted_valid = snapshot.iter().find(|entry| entry.key == "state/valid").unwrap(); + let persisted_valid = snapshot + .iter() + .find(|entry| entry.key == "state/valid") + .unwrap(); let preserved_invalid = snapshot .iter() .find(|entry| entry.key == "state/invalid") .unwrap(); assert_eq!(persisted_valid.signature, valid.signature); - assert_eq!(preserved_invalid.signature, vec![7u8; COMPACT_SIGNATURE_LEN]); + assert_eq!( + preserved_invalid.signature, + vec![7u8; COMPACT_SIGNATURE_LEN] + ); } #[test] diff --git a/libs/gl-client/src/signer/report.rs b/libs/gl-client/src/signer/report.rs index 02e2ab5cf..41b8803d0 100644 --- a/libs/gl-client/src/signer/report.rs +++ b/libs/gl-client/src/signer/report.rs @@ -99,8 +99,9 @@ pub fn build_state_signature_override_used_message( #[cfg(test)] mod tests { use super::{ - build_state_signature_override_enabled_message, build_state_signature_override_used_message, - STATE_SIGNATURE_OVERRIDE_ENABLED_PREFIX, STATE_SIGNATURE_OVERRIDE_USED_PREFIX, + build_state_signature_override_enabled_message, + build_state_signature_override_used_message, STATE_SIGNATURE_OVERRIDE_ENABLED_PREFIX, + STATE_SIGNATURE_OVERRIDE_USED_PREFIX, }; #[test] diff --git a/libs/gl-client/src/signer/resolve.rs b/libs/gl-client/src/signer/resolve.rs index 4534c7099..beae92203 100644 --- a/libs/gl-client/src/signer/resolve.rs +++ b/libs/gl-client/src/signer/resolve.rs @@ -82,10 +82,10 @@ impl Resolver { // later on } (Message::SignInvoice(_l), Request::LspInvoice(_r)) => { - // TODO: This could also need some - // strengthening. See below. - true - } + // TODO: This could also need some + // strengthening. See below. + true + } (Message::SignInvoice(_l), Request::Invoice(_r)) => { // TODO: This could be strengthened by parsing the // invoice from `l.u5bytes` and verify the diff --git a/libs/gl-plugin/src/context.rs b/libs/gl-plugin/src/context.rs index 48692fc56..fc1cfec8d 100644 --- a/libs/gl-plugin/src/context.rs +++ b/libs/gl-plugin/src/context.rs @@ -10,9 +10,9 @@ //! sign off actually match the authentic commands by a valid //! caller. +use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::Mutex; -use serde::{Serialize, Deserialize}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Request { diff --git a/libs/gl-plugin/src/messages.rs b/libs/gl-plugin/src/messages.rs index f5bec0843..571744964 100644 --- a/libs/gl-plugin/src/messages.rs +++ b/libs/gl-plugin/src/messages.rs @@ -280,7 +280,7 @@ where /// `peer_connected` hook. #[derive(Serialize, Deserialize, Debug)] pub struct PeerConnectedCall { - pub peer: Peer + pub peer: Peer, } #[derive(Serialize, Deserialize, Debug)] @@ -295,10 +295,9 @@ pub struct Peer { #[serde(rename_all = "snake_case")] pub enum Direction { In, - Out + Out, } - #[cfg(test)] mod test { use super::*; @@ -315,7 +314,10 @@ mod test { }); let call = serde_json::from_str::(&msg.to_string()).unwrap(); - assert_eq!(call.peer.id, "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"); + assert_eq!( + call.peer.id, + "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + ); assert_eq!(call.peer.direction, Direction::In); assert_eq!(call.peer.addr, "34.239.230.56:9735"); assert_eq!(call.peer.features, ""); diff --git a/libs/gl-plugin/src/node/mod.rs b/libs/gl-plugin/src/node/mod.rs index 2ed8890ad..e538a207d 100644 --- a/libs/gl-plugin/src/node/mod.rs +++ b/libs/gl-plugin/src/node/mod.rs @@ -7,10 +7,8 @@ use anyhow::{Context, Error, Result}; use base64::{engine::general_purpose, Engine as _}; use bytes::BufMut; use cln_rpc::Notification; +use gl_client::metrics::{savings_percent, signer_state_request_wire_bytes}; use gl_client::persist::{State, StateSketch}; -use gl_client::metrics::{ - signer_state_request_wire_bytes, savings_percent, -}; use governor::{ clock::MonotonicClock, state::direct::NotKeyed, state::InMemoryState, Quota, RateLimiter, }; @@ -201,14 +199,12 @@ impl Node for PluginNodeServer { // We require capacity + 5% buffer to account for fees and routing. // Only check for specific amounts (not "any" amount invoices). if req.amount_msat > 0 { - let receivable = self - .get_receivable_capacity(&mut rpc) - .await - .unwrap_or(0); + let receivable = self.get_receivable_capacity(&mut rpc).await.unwrap_or(0); // Add 5% buffer: capacity >= amount * 1.05 // Equivalent to: capacity * 100 >= amount * 105 - let has_sufficient_capacity = req.amount_msat + let has_sufficient_capacity = req + .amount_msat .saturating_mul(105) .checked_div(100) .map(|required| receivable >= required) @@ -245,7 +241,10 @@ impl Node for PluginNodeServer { bolt11: res.bolt11, created_index: res.created_index.unwrap_or(0) as u32, expires_at: res.expires_at as u32, - payment_hash: >::borrow(&res.payment_hash).to_vec(), + payment_hash: >::borrow( + &res.payment_hash, + ) + .to_vec(), payment_secret: res.payment_secret.to_vec(), opening_fee_msat: 0, })); @@ -417,9 +416,8 @@ impl Node for PluginNodeServer { // the large state with them. let state_snapshot = signer_state.lock().await.clone(); - let state_entries: Vec = state_snapshot - .omit_tombstones() - .into(); + let state_entries: Vec = + state_snapshot.omit_tombstones().into(); let state_wire_bytes = signer_state_request_wire_bytes(&state_entries); let state_entries: Vec = state_entries .into_iter() @@ -472,7 +470,6 @@ impl Node for PluginNodeServer { hsm_id ); - let state_snapshot = signer_state.lock().await.clone(); // Estimate the size of the full state to calculate the bandwidth savings of sending diffs let full_entries: Vec = @@ -725,10 +722,7 @@ impl Node for PluginNodeServer { if let Err(e) = address.require_network(network) { return Err(Status::new( Code::Unknown, - format!( - "Network validation failed: {}", - e - ), + format!("Network validation failed: {}", e), )); } } diff --git a/libs/gl-plugin/src/responses.rs b/libs/gl-plugin/src/responses.rs index 556f89e6f..913ac44ed 100644 --- a/libs/gl-plugin/src/responses.rs +++ b/libs/gl-plugin/src/responses.rs @@ -349,8 +349,7 @@ pub struct InvoiceResponse { #[derive(Debug, Clone, Deserialize)] pub struct LspGetinfoResponse { -pub opening_fee_params_menu: Vec, - + pub opening_fee_params_menu: Vec, } #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] // LSPS2 requires the client to fail if a field is unrecognized. @@ -360,8 +359,8 @@ pub struct OpeningFeeParams { pub valid_until: String, pub min_lifetime: u32, pub max_client_to_self_delay: u32, - pub min_payment_size_msat: String , - pub max_payment_size_msat: String , + pub min_payment_size_msat: String, + pub max_payment_size_msat: String, pub promise: String, // Max 512 bytes } diff --git a/libs/gl-sdk-cli/Cargo.toml b/libs/gl-sdk-cli/Cargo.toml index aec4cd090..c70f76425 100644 --- a/libs/gl-sdk-cli/Cargo.toml +++ b/libs/gl-sdk-cli/Cargo.toml @@ -3,6 +3,8 @@ name = "gl-sdk-cli" version = "0.3.0" edition = "2021" description = "CLI wrapper for gl-sdk" +license = "MIT" +repository = "https://github.com/Blockstream/greenlight" [[bin]] name = "glsdk" diff --git a/libs/gl-sdk-cli/src/lib.rs b/libs/gl-sdk-cli/src/lib.rs index 260449b20..48a9a8296 100644 --- a/libs/gl-sdk-cli/src/lib.rs +++ b/libs/gl-sdk-cli/src/lib.rs @@ -18,7 +18,13 @@ pub struct Cli { data_dir: Option, /// Bitcoin network (bitcoin or regtest) - #[arg(short, long, default_value = "bitcoin", global = true, help_heading = "Global options")] + #[arg( + short, + long, + default_value = "bitcoin", + global = true, + help_heading = "Global options" + )] network: String, /// Enable debug logging diff --git a/libs/gl-sdk-cli/src/util.rs b/libs/gl-sdk-cli/src/util.rs index 0c2b1d1dc..4812964ee 100644 --- a/libs/gl-sdk-cli/src/util.rs +++ b/libs/gl-sdk-cli/src/util.rs @@ -30,9 +30,8 @@ pub enum Secret { pub fn read_secret(data_dir: &DataDir) -> Result { let path = data_dir.0.join(PHRASE_FILE_NAME); - let raw = fs::read(&path).map_err(|_| { - Error::PhraseNotFound(format!("could not read from {}", path.display())) - })?; + let raw = fs::read(&path) + .map_err(|_| Error::PhraseNotFound(format!("could not read from {}", path.display())))?; // Try UTF-8 mnemonic first (glsdk format) if let Ok(text) = std::str::from_utf8(&raw) { diff --git a/libs/gl-sdk-napi/src/lib.rs b/libs/gl-sdk-napi/src/lib.rs index 887de054a..38920d25f 100644 --- a/libs/gl-sdk-napi/src/lib.rs +++ b/libs/gl-sdk-napi/src/lib.rs @@ -10,14 +10,14 @@ use glsdk::{ Credentials as GlCredentials, DeveloperCert as GlDeveloperCert, Handle as GlHandle, - ParsedInput as GlParsedInput, - ResolvedInput as GlResolvedInput, Network as GlNetwork, Node as GlNode, NodeEvent as GlNodeEvent, NodeEventStream as GlNodeEventStream, OutputStatus as GlOutputStatus, + ParsedInput as GlParsedInput, ParsedInvoice as GlParsedInvoice, + ResolvedInput as GlResolvedInput, Scheduler as GlScheduler, Signer as GlSigner, }; @@ -679,10 +679,12 @@ impl Node { #[napi(constructor)] pub fn new(credentials: &Credentials) -> Result { // Connection is established lazily on first RPC. - let inner = - GlNode::signerless(credentials.inner.clone()).map_err(|e| Error::from_reason(e.to_string()))?; + let inner = GlNode::signerless(credentials.inner.clone()) + .map_err(|e| Error::from_reason(e.to_string()))?; - Ok(Self { inner: std::sync::Arc::new(inner) }) + Ok(Self { + inner: std::sync::Arc::new(inner), + }) } /// Stop the node if it is currently running @@ -1043,7 +1045,10 @@ impl Node { /// Build the request from `LnUrlWithdrawRequestData` (obtained out /// of band) and a chosen amount. #[napi] - pub async fn lnurl_withdraw(&self, request: LnUrlWithdrawRequest) -> Result { + pub async fn lnurl_withdraw( + &self, + request: LnUrlWithdrawRequest, + ) -> Result { let inner = self.inner.clone(); let gl_request = gl_lnurl_withdraw_request_from_napi(request); let result = tokio::task::spawn_blocking(move || { @@ -1221,8 +1226,7 @@ fn napi_resolved_input_from_gl(input: GlResolvedInput) -> ResolvedInput { /// but **not fetched** โ€” call `resolveInput` for that. #[napi] pub fn parse_input(input: String) -> Result { - let parsed = - glsdk::parse_input(input).map_err(|e| Error::from_reason(e.to_string()))?; + let parsed = glsdk::parse_input(input).map_err(|e| Error::from_reason(e.to_string()))?; Ok(napi_parsed_input_from_gl(parsed)) } diff --git a/libs/gl-sdk-napi/tests/jest.globalSetup.ts b/libs/gl-sdk-napi/tests/jest.globalSetup.ts index 6fd41e493..d17395dfb 100644 --- a/libs/gl-sdk-napi/tests/jest.globalSetup.ts +++ b/libs/gl-sdk-napi/tests/jest.globalSetup.ts @@ -58,15 +58,10 @@ export default async function globalSetup(): Promise { console.log('\n๐Ÿš€ Starting gltestserver...'); - // Run from the workspace root so uv resolves gl-testing and other - // workspace dependencies from the top-level pyproject.toml. - const workspaceRoot = path.resolve(__dirname, '..', '..', '..'); - const server: ChildProcess = spawn( 'uv', - ['run', '--package', 'gl-testing', 'python', path.join(__dirname, 'test_setup.py')], + ['run', '--no-sync', 'python', path.join(__dirname, 'test_setup.py')], { - cwd: workspaceRoot, detached: true, stdio: ['ignore', 'pipe', 'pipe'], } diff --git a/libs/gl-sdk/hooks/libglsdk_force_include.py b/libs/gl-sdk/hooks/libglsdk_force_include.py index 15314d8c6..21a2a5d8d 100644 --- a/libs/gl-sdk/hooks/libglsdk_force_include.py +++ b/libs/gl-sdk/hooks/libglsdk_force_include.py @@ -20,4 +20,5 @@ def initialize(self, version, build_data): lib_ext = ".so" shared_file = f"glsdk/libglsdk{lib_ext}" - build_data['force_include'][shared_file] = shared_file \ No newline at end of file + build_data['force_include'][shared_file] = shared_file + build_data['force_include']['glsdk/glsdk.py'] = 'glsdk/glsdk.py' \ No newline at end of file diff --git a/libs/gl-sdk/src/config.rs b/libs/gl-sdk/src/config.rs index 94d3a371d..0e969e94e 100644 --- a/libs/gl-sdk/src/config.rs +++ b/libs/gl-sdk/src/config.rs @@ -1,9 +1,9 @@ // SDK configuration for Greenlight node operations. // Holds network selection and optional developer certificate. -use std::sync::Arc; -use crate::credentials::DeveloperCert; use crate::Network; +use crate::credentials::DeveloperCert; +use std::sync::Arc; #[derive(uniffi::Object, Clone)] pub struct Config { diff --git a/libs/gl-sdk/src/credentials.rs b/libs/gl-sdk/src/credentials.rs index 4f30e5a73..da55dfa76 100644 --- a/libs/gl-sdk/src/credentials.rs +++ b/libs/gl-sdk/src/credentials.rs @@ -50,6 +50,8 @@ impl Credentials { } pub fn node_id(&self) -> Result, Error> { - self.inner.node_id().map_err(|e| Error::other(e.to_string())) + self.inner + .node_id() + .map_err(|e| Error::other(e.to_string())) } } diff --git a/libs/gl-sdk/src/input.rs b/libs/gl-sdk/src/input.rs index 265f91032..1d5c1b60e 100644 --- a/libs/gl-sdk/src/input.rs +++ b/libs/gl-sdk/src/input.rs @@ -20,8 +20,8 @@ // Wallets handling a QR scan that should proceed straight to the // pay/withdraw screen call `resolve_input`. -use crate::lnurl::{LnUrlPayRequestData, LnUrlWithdrawRequestData}; use crate::Error; +use crate::lnurl::{LnUrlPayRequestData, LnUrlWithdrawRequestData}; /// Parsed BOLT11 invoice with extracted fields. #[derive(Clone, uniffi::Record)] @@ -132,7 +132,7 @@ pub fn parse_input(input: String) -> Result { /// withdraw request data. pub async fn resolve_input(input: String) -> Result { use gl_client::lnurl::models::LnUrlHttpClearnetClient; - use gl_client::lnurl::{LnUrlResponse, LNURL}; + use gl_client::lnurl::{LNURL, LnUrlResponse}; // Capture the user's original input (post-trim) so that // `data.lnurl` on the resolved response carries the exact string @@ -371,15 +371,19 @@ mod tests { #[test] fn test_parse_input_invalid_node_id_errors() { // 66 chars but starts with 0x04 (uncompressed pubkey prefix) - assert!(parse_input( - "04eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619".to_string() - ) - .is_err()); + assert!( + parse_input( + "04eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619".to_string() + ) + .is_err() + ); // 66 non-hex chars - assert!(parse_input( - "not_valid_hex_at_all_but_66_chars_long_xxxxxxxxxxxxxxxxxxxxxxxxxxx".to_string() - ) - .is_err()); + assert!( + parse_input( + "not_valid_hex_at_all_but_66_chars_long_xxxxxxxxxxxxxxxxxxxxxxxxxxx".to_string() + ) + .is_err() + ); } // โ”€โ”€ resolve_input pass-through paths (no HTTP needed) โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/libs/gl-sdk/src/lib.rs b/libs/gl-sdk/src/lib.rs index cf1e89435..4263d9062 100644 --- a/libs/gl-sdk/src/lib.rs +++ b/libs/gl-sdk/src/lib.rs @@ -139,21 +139,21 @@ mod util; pub use crate::{ config::Config, credentials::{Credentials, DeveloperCert}, - node::{ - ChannelState, FundChannel, FundOutput, GetInfoResponse, Invoice, - InvoicePaidEvent, InvoiceStatus, ListFundsResponse, ListIndex, ListInvoicesResponse, - ListPaymentsRequest, ListPeerChannelsResponse, ListPaysResponse, ListPeersResponse, - Node, NodeEvent, NodeEventListener, NodeEventStream, NodeState, OnchainBalanceState, - OnchainFeeRates, OnchainReceiveResponse, OnchainSendResponse, Outpoint, OutputStatus, - Pay, PayStatus, Payment, PaymentStatus, PaymentType, PaymentTypeFilter, Peer, - PeerChannel, PreparedOnchainSend, ReceiveResponse, SendResponse, - }, input::{ParsedInput, ParsedInvoice, ResolvedInput}, - logging::{LogEntry, LogLevel, LogListener}, lnurl::{ - LnUrlErrorData, LnUrlPayRequest, LnUrlPayRequestData, LnUrlPayResult, - LnUrlPaySuccessData, LnUrlWithdrawRequest, LnUrlWithdrawRequestData, - LnUrlWithdrawResult, LnUrlWithdrawSuccessData, SuccessActionProcessed, + LnUrlErrorData, LnUrlPayRequest, LnUrlPayRequestData, LnUrlPayResult, LnUrlPaySuccessData, + LnUrlWithdrawRequest, LnUrlWithdrawRequestData, LnUrlWithdrawResult, + LnUrlWithdrawSuccessData, SuccessActionProcessed, + }, + logging::{LogEntry, LogLevel, LogListener}, + node::{ + ChannelState, FundChannel, FundOutput, GetInfoResponse, Invoice, InvoicePaidEvent, + InvoiceStatus, ListFundsResponse, ListIndex, ListInvoicesResponse, ListPaymentsRequest, + ListPaysResponse, ListPeerChannelsResponse, ListPeersResponse, Node, NodeEvent, + NodeEventListener, NodeEventStream, NodeState, OnchainBalanceState, OnchainFeeRates, + OnchainReceiveResponse, OnchainSendResponse, Outpoint, OutputStatus, Pay, PayStatus, + Payment, PaymentStatus, PaymentType, PaymentTypeFilter, Peer, PeerChannel, + PreparedOnchainSend, ReceiveResponse, SendResponse, }, node_builder::NodeBuilder, scheduler::Scheduler, @@ -179,9 +179,8 @@ fn schedule_node( let seed_for_async = seed.clone(); let credentials = util::exec(async move { - let signer = - gl_client::signer::Signer::new(seed_for_async, network, nobody.clone()) - .map_err(|e| Error::other(e.to_string()))?; + let signer = gl_client::signer::Signer::new(seed_for_async, network, nobody.clone()) + .map_err(|e| Error::other(e.to_string()))?; let scheduler = gl_client::scheduler::Scheduler::new(network, nobody) .await @@ -226,7 +225,7 @@ fn map_scheduler_error(e: anyhow::Error, node_id_hex: &str) -> Error { if let Some(status) = cause.downcast_ref::() { match status.code() { tonic::Code::AlreadyExists => { - return Error::duplicate_node(node_id_hex.to_string()) + return Error::duplicate_node(node_id_hex.to_string()); } tonic::Code::NotFound => return Error::no_such_node(node_id_hex.to_string()), // Don't return here โ€” the tonic status might be a generic @@ -272,9 +271,8 @@ pub(crate) fn connect_internal( let network = config.network; let creds = credentials::Credentials::load(credentials)?; - let authenticated_signer = - gl_client::signer::Signer::new(seed, network, creds.inner.clone()) - .map_err(|e| Error::other(e.to_string()))?; + let authenticated_signer = gl_client::signer::Signer::new(seed, network, creds.inner.clone()) + .map_err(|e| Error::other(e.to_string()))?; let handle = signer::Handle::spawn(authenticated_signer); let node = node::Node::with_signer(creds, handle, network)?; diff --git a/libs/gl-sdk/src/lnurl.rs b/libs/gl-sdk/src/lnurl.rs index 53030c1f9..a64509bff 100644 --- a/libs/gl-sdk/src/lnurl.rs +++ b/libs/gl-sdk/src/lnurl.rs @@ -156,7 +156,10 @@ pub enum SuccessActionProcessed { /// Display a URL to the user. Url { description: String, url: String }, /// Decrypted AES payload (LUD-10). - Aes { description: String, plaintext: String }, + Aes { + description: String, + plaintext: String, + }, } // โ”€โ”€ From conversions (gl-client โ†’ gl-sdk) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/libs/gl-sdk/src/logging.rs b/libs/gl-sdk/src/logging.rs index 4e037d456..61af4df8f 100644 --- a/libs/gl-sdk/src/logging.rs +++ b/libs/gl-sdk/src/logging.rs @@ -102,9 +102,8 @@ impl Log for SdkLogger { /// `set_log_level`. pub fn set_logger(level: LogLevel, listener: Box) -> Result<(), Error> { let filter: LevelFilter = level.into(); - log::set_boxed_logger(Box::new(SdkLogger { listener })).map_err(|e| { - Error::other(format!("a `log` logger is already installed: {e}")) - })?; + log::set_boxed_logger(Box::new(SdkLogger { listener })) + .map_err(|e| Error::other(format!("a `log` logger is already installed: {e}")))?; log::set_max_level(filter); Ok(()) } diff --git a/libs/gl-sdk/src/node.rs b/libs/gl-sdk/src/node.rs index e97f522b1..93f3fbdbf 100644 --- a/libs/gl-sdk/src/node.rs +++ b/libs/gl-sdk/src/node.rs @@ -1,11 +1,11 @@ -use crate::{credentials::Credentials, signer::Handle, util::exec, Error}; -use std::str::FromStr; -use std::sync::atomic::{AtomicBool, Ordering}; +use crate::{Error, credentials::Credentials, signer::Handle, util::exec}; use gl_client::credentials::NodeIdProvider; use gl_client::lnurl::models::LnUrlHttpClient as _; use gl_client::node::{Client as GlClient, ClnClient, Node as ClientNode}; use gl_client::pb::{self as glpb, cln as clnpb}; use lightning_invoice::Bolt11Invoice; +use std::str::FromStr; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use tokio::sync::OnceCell; @@ -72,7 +72,6 @@ impl Node { #[uniffi::export] impl Node { - /// Stop the node if it is currently running. pub fn stop(&self) -> Result<(), Error> { self.check_connected()?; @@ -317,8 +316,7 @@ impl Node { if let (Some(rate), Ok(rates)) = (sat_per_vbyte, feerates_res.as_ref()) && let Some(perkw) = rates.get_ref().perkw.as_ref() { - let min_sat_per_vbyte = - sat_per_vbyte_from_perkw(perkw.min_acceptable).max(1); + let min_sat_per_vbyte = sat_per_vbyte_from_perkw(perkw.min_acceptable).max(1); if (rate as u64) < min_sat_per_vbyte { return Err(Error::argument( "sat_per_vbyte", @@ -354,8 +352,7 @@ impl Node { // fee_sat = weight_wu ร— feerate_per_kw / 1000. The proto-level // `estimated_final_weight` already includes the destination // output we declared via `startweight`, plus any change output. - let fee_sat: u64 = - (res.estimated_final_weight as u64 * res.feerate_per_kw as u64) / 1000; + let fee_sat: u64 = (res.estimated_final_weight as u64 * res.feerate_per_kw as u64) / 1000; // Sum input values directly from the PSBT. Each PSBT input // carries its prevout amount in `witness_utxo` (segwit) or @@ -373,9 +370,7 @@ impl Node { tx.output .get(vout) .map(|o| o.value) - .ok_or_else(|| { - Error::rpc("psbt non_witness_utxo missing vout") - })? + .ok_or_else(|| Error::rpc("psbt non_witness_utxo missing vout"))? } else { return Err(Error::rpc(format!( "psbt input {} has no witness_utxo or non_witness_utxo", @@ -402,8 +397,7 @@ impl Node { // Round up so passing this back to `onchain_send` produces a // feerate at least as high as the previewed one; that way the // broadcast fee is never below what the user agreed to. - let effective_sat_per_vbyte: u32 = - (res.feerate_per_kw as u64).div_ceil(250) as u32; + let effective_sat_per_vbyte: u32 = (res.feerate_per_kw as u64).div_ceil(250) as u32; Ok(PreparedOnchainSend { utxos, @@ -523,9 +517,7 @@ impl Node { .map(|p| { p.inputs .iter() - .filter_map(|i| { - i.witness_utxo.as_ref().map(|t| t.value.to_sat()) - }) + .filter_map(|i| i.witness_utxo.as_ref().map(|t| t.value.to_sat())) .sum::() }) .unwrap_or(0); @@ -534,9 +526,8 @@ impl Node { .as_ref() .map(|a| a.msat / 1000) .unwrap_or(0); - let fee_sat = (resp.estimated_final_weight as u64 - * resp.feerate_per_kw as u64) - / 1000; + let fee_sat = + (resp.estimated_final_weight as u64 * resp.feerate_per_kw as u64) / 1000; total_input_sat .saturating_sub(excess_sat) .saturating_sub(fee_sat) @@ -722,11 +713,9 @@ impl Node { } } - let connected_channel_peers: Vec = - connected_channel_peer_set.into_iter().collect(); + let connected_channel_peers: Vec = connected_channel_peer_set.into_iter().collect(); - let max_chan_reserve_msat = - channels_balance_msat.saturating_sub(max_payable_msat); + let max_chan_reserve_msat = channels_balance_msat.saturating_sub(max_payable_msat); let mut onchain_balance_msat: u64 = 0; let mut unconfirmed_onchain_balance_msat: u64 = 0; @@ -741,12 +730,8 @@ impl Node { } match output.status { OutputStatus::Confirmed => onchain_balance_msat += output.amount_msat, - OutputStatus::Unconfirmed => { - unconfirmed_onchain_balance_msat += output.amount_msat - } - OutputStatus::Immature => { - immature_onchain_balance_msat += output.amount_msat - } + OutputStatus::Unconfirmed => unconfirmed_onchain_balance_msat += output.amount_msat, + OutputStatus::Immature => immature_onchain_balance_msat += output.amount_msat, OutputStatus::Spent => {} } } @@ -759,7 +744,6 @@ impl Node { .saturating_add(pending_onchain_balance_msat); let spendable_balance_msat = max_payable_msat.saturating_add(onchain_balance_msat); - Ok(NodeState { id: info.id, block_height: info.blockheight, @@ -899,8 +883,7 @@ impl Node { .invoices .into_iter() .filter(|i| { - i.status() - == clnpb::listinvoices_invoices::ListinvoicesInvoicesStatus::Paid + i.status() == clnpb::listinvoices_invoices::ListinvoicesInvoicesStatus::Paid }) .map(|i| -> Payment { i.into() }), ); @@ -1015,14 +998,12 @@ impl Node { // Phase 1: Get invoice from service callback let comment = request.comment.as_deref(); - let (invoice_str, success_action) = match exec( - gl_client::lnurl::pay::fetch_invoice( - &http_client, - &request.data.callback, - request.amount_msat, - comment, - ), - ) { + let (invoice_str, success_action) = match exec(gl_client::lnurl::pay::fetch_invoice( + &http_client, + &request.data.callback, + request.amount_msat, + comment, + )) { Ok(v) => v, Err(e) => { let msg = e.to_string(); @@ -1072,9 +1053,8 @@ impl Node { .process(&pay_response.payment_preimage) .map_err(|e| Error::other(e.to_string()))?; if validate_url { - if let gl_client::lnurl::models::ProcessedSuccessAction::Url { - url, .. - } = &processed + if let gl_client::lnurl::models::ProcessedSuccessAction::Url { url, .. } = + &processed { if let Some(reason) = url_action_domain_mismatch(&request.data.callback, url) @@ -1507,11 +1487,7 @@ fn classify_onchain_balance( ) -> OnchainBalanceState { let withdrawable_sat = confirmed_sat.saturating_sub(reserve_sat); - if confirmed_sat == 0 - && unconfirmed_sat == 0 - && immature_sat == 0 - && pending_close_sat == 0 - { + if confirmed_sat == 0 && unconfirmed_sat == 0 && immature_sat == 0 && pending_close_sat == 0 { return OnchainBalanceState::Unavailable; } if withdrawable_sat > ONCHAIN_DUST_THRESHOLD_SAT { @@ -1574,8 +1550,7 @@ pub struct OnchainSendResponse { /// Parse an `amount_or_all` argument into the protobuf `AmountOrAll`. /// Accepts `"all"`, `""`, `"sat"`, or `"msat"`. fn parse_amount_or_all(amount_or_all: &str) -> Result { - let (num, suffix): (String, String) = - amount_or_all.chars().partition(|c| c.is_ascii_digit()); + let (num, suffix): (String, String) = amount_or_all.chars().partition(|c| c.is_ascii_digit()); let num = if num.is_empty() { 0 @@ -1591,7 +1566,9 @@ fn parse_amount_or_all(amount_or_all: &str) -> Result })), }), (n, "msat") => Ok(clnpb::AmountOrAll { - value: Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount { msat: n })), + value: Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount { + msat: n, + })), }), (0, "all") => Ok(clnpb::AmountOrAll { value: Some(clnpb::amount_or_all::Value::All(true)), @@ -1612,8 +1589,7 @@ fn feerate_perkw_from_sat_per_vbyte(sat_per_vbyte: u32) -> clnpb::Feerate { /// Convert a public `Outpoint` (hex txid) into the protobuf form /// (raw txid bytes) used by CLN's `WithdrawRequest`. fn outpoint_to_pb(o: Outpoint) -> Result { - let txid = hex::decode(&o.txid) - .map_err(|_| Error::argument("utxos.txid", o.txid.clone()))?; + let txid = hex::decode(&o.txid).map_err(|_| Error::argument("utxos.txid", o.txid.clone()))?; Ok(clnpb::Outpoint { txid, outnum: o.vout, @@ -1934,7 +1910,6 @@ impl ChannelState { fn is_open(&self) -> bool { matches!(self, ChannelState::ChanneldNormal) } - } /// Returns true when the channel still holds on-chain funds that have @@ -2620,24 +2595,33 @@ mod tests { #[test] fn parse_amount_or_all_handles_all_variants() { let all = parse_amount_or_all("all").unwrap(); - assert!(matches!(all.value, Some(clnpb::amount_or_all::Value::All(true)))); + assert!(matches!( + all.value, + Some(clnpb::amount_or_all::Value::All(true)) + )); let plain = parse_amount_or_all("50000").unwrap(); assert!(matches!( plain.value, - Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount { msat: 50_000_000 })) + Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount { + msat: 50_000_000 + })) )); let sat = parse_amount_or_all("50000sat").unwrap(); assert!(matches!( sat.value, - Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount { msat: 50_000_000 })) + Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount { + msat: 50_000_000 + })) )); let msat = parse_amount_or_all("50000msat").unwrap(); assert!(matches!( msat.value, - Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount { msat: 50_000 })) + Some(clnpb::amount_or_all::Value::Amount(clnpb::Amount { + msat: 50_000 + })) )); assert!(parse_amount_or_all("notanumber").is_err()); @@ -2665,7 +2649,10 @@ mod tests { assert_eq!(emergency_reserve_sat, 25_000); assert_eq!(unconfirmed_sat, 5_000); } - other => panic!("expected Available, got {:?}", std::mem::discriminant(&other)), + other => panic!( + "expected Available, got {:?}", + std::mem::discriminant(&other) + ), } } @@ -2702,7 +2689,10 @@ mod tests { OnchainBalanceState::Immature { immature_sat } => { assert_eq!(immature_sat, 100_000); } - other => panic!("expected Immature, got {:?}", std::mem::discriminant(&other)), + other => panic!( + "expected Immature, got {:?}", + std::mem::discriminant(&other) + ), } } @@ -2888,10 +2878,16 @@ mod tests { ); // P2SH โ€” script_pubkey is 23 bytes, output = (8+1+23)*4 = 128 - assert_eq!(output_weight_for_address("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy"), 128); + assert_eq!( + output_weight_for_address("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy"), + 128 + ); // P2PKH โ€” script_pubkey is 25 bytes, output = (8+1+25)*4 = 136 - assert_eq!(output_weight_for_address("1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2"), 136); + assert_eq!( + output_weight_for_address("1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2"), + 136 + ); // Garbage falls back to the conservative 172 wu. assert_eq!(output_weight_for_address("not-an-address"), 172); diff --git a/libs/gl-sdk/src/node_builder.rs b/libs/gl-sdk/src/node_builder.rs index c1b0d036d..c818fa9f5 100644 --- a/libs/gl-sdk/src/node_builder.rs +++ b/libs/gl-sdk/src/node_builder.rs @@ -29,9 +29,9 @@ use std::sync::Arc; use crate::{ + Error, config::Config, node::{Node, NodeEventListener}, - Error, }; /// Configurable Node construction. See module docs. @@ -69,10 +69,7 @@ impl NodeBuilder { /// Returns a new builder that shares the rest of the /// configuration. Build calls on the returned builder will /// install the listener; the original builder is unchanged. - pub fn with_event_listener( - self: Arc, - listener: Box, - ) -> Arc { + pub fn with_event_listener(self: Arc, listener: Box) -> Arc { // UniFFI's callback-interface lowering hands us a // `Box`. We re-wrap it as `Arc` because // the builder is reusable across multiple build calls โ€” each @@ -142,8 +139,7 @@ impl NodeBuilder { mnemonic: String, invite_code: Option, ) -> Result, Error> { - let node = - crate::register_or_recover_internal(mnemonic, invite_code, &self.config)?; + let node = crate::register_or_recover_internal(mnemonic, invite_code, &self.config)?; self.attach_observers(&node)?; Ok(node) } diff --git a/libs/gl-sdk/src/scheduler.rs b/libs/gl-sdk/src/scheduler.rs index c31a70256..c2e762b30 100644 --- a/libs/gl-sdk/src/scheduler.rs +++ b/libs/gl-sdk/src/scheduler.rs @@ -1,8 +1,8 @@ use crate::{ + Error, credentials::{Credentials, DeveloperCert}, signer::Signer, util::exec, - Error, }; #[derive(uniffi::Object, Clone)] diff --git a/libs/gl-sdk/tests/test_auth_api.py b/libs/gl-sdk/tests/test_auth_api.py index 9c75f6d6e..0dace6418 100644 --- a/libs/gl-sdk/tests/test_auth_api.py +++ b/libs/gl-sdk/tests/test_auth_api.py @@ -240,10 +240,10 @@ def test_credentials_still_works_after_disconnect(self, scheduler, nobody_id): class TestLowLevelCredentials: - """Test that Node created via low-level API exposes credentials.""" + """Test that Node created via signerless connect exposes credentials.""" - def test_node_new_stores_credentials(self, scheduler, nobody_id): - """Node::new(creds) should allow calling node.credentials().""" + def test_node_connect_stores_credentials(self, scheduler, nobody_id): + """NodeBuilder.connect(creds, None) should allow calling node.credentials().""" dev_cert = glsdk.DeveloperCert(nobody_id.cert_chain, nobody_id.private_key) config = glsdk.Config().with_developer_cert(dev_cert) @@ -252,8 +252,8 @@ def test_node_new_stores_credentials(self, scheduler, nobody_id): saved_creds = node1.credentials() node1.disconnect() - # Create node via low-level API - creds_obj = glsdk.Credentials.load(saved_creds) - node2 = glsdk.Node(creds_obj) + # Create node via signerless connect + node2 = glsdk.NodeBuilder(config).connect(saved_creds, None) roundtripped = node2.credentials() assert len(roundtripped) > 0 + node2.disconnect() diff --git a/libs/gl-sdk/tests/test_basic.py b/libs/gl-sdk/tests/test_basic.py index 1899b7ddc..a336b26c0 100644 --- a/libs/gl-sdk/tests/test_basic.py +++ b/libs/gl-sdk/tests/test_basic.py @@ -50,11 +50,9 @@ def test_credentials_multiple_loads(): def test_node_creation_fails_with_empty_creds(): """Test that creating a Node with empty credentials fails as expected.""" - creds = glsdk.Credentials.load(b"") - # Node creation should fail with these invalid credentials with pytest.raises(glsdk.Error): - node = glsdk.Node(creds) + glsdk.NodeBuilder(glsdk.Config()).connect(b"", None) def test_developer_cert_construction(): diff --git a/libs/gl-sdk/tests/test_lnurl.py b/libs/gl-sdk/tests/test_lnurl.py index acd526b3c..8bd4eca63 100644 --- a/libs/gl-sdk/tests/test_lnurl.py +++ b/libs/gl-sdk/tests/test_lnurl.py @@ -8,8 +8,6 @@ gl_sdk_node โ”€โ”€ channel โ”€โ”€ relay โ”€โ”€ channel โ”€โ”€ service_node (LNURL server) """ -import asyncio - from gltesting.fixtures import * # noqa: F401, F403 from pyln.testing.utils import wait_for @@ -66,7 +64,7 @@ def fund_and_connect(node_factory, bitcoind, lnurl_service): def test_parse_input_lnurl_pay(lnurl_service): """parse_input on an LNURL-pay URL returns LnUrlPay with fetched data.""" - resolved = asyncio.run(glsdk.resolve_input(lnurl_service.pay_url)) + resolved = glsdk.resolve_input(lnurl_service.pay_url) assert isinstance(resolved, glsdk.ResolvedInput.LN_URL_PAY) data = resolved.data @@ -79,7 +77,7 @@ def test_parse_input_lnurl_pay(lnurl_service): def test_parse_input_lnurl_withdraw(lnurl_service): """parse_input on an LNURL-withdraw URL returns LnUrlWithdraw with fetched data.""" - resolved = asyncio.run(glsdk.resolve_input(lnurl_service.withdraw_url)) + resolved = glsdk.resolve_input(lnurl_service.withdraw_url) assert isinstance(resolved, glsdk.ResolvedInput.LN_URL_WITHDRAW) data = resolved.data @@ -91,7 +89,7 @@ def test_parse_input_lnurl_withdraw(lnurl_service): def test_parse_input_lightning_address_url(lnurl_service): """parse_input on a well-known LUD-16 URL returns LnUrlPay.""" - resolved = asyncio.run(glsdk.resolve_input(lnurl_service.lightning_address_url)) + resolved = glsdk.resolve_input(lnurl_service.lightning_address_url) assert isinstance(resolved, glsdk.ResolvedInput.LN_URL_PAY) assert resolved.data.min_sendable == lnurl_service.min_sendable @@ -115,7 +113,7 @@ def test_parse_input_bolt11_no_http(lnurl_service): "qqqzsqygarl9fh38s0gyuxjjgux34w75dnc6xp2l35j7es3jd4ugt3lu0xzre26yg5" "m7ke54n2d5sym4xcmxtl8238xxvw5h5h5j5r6drg6k6zcqj0fcwg" ) - resolved = asyncio.run(glsdk.resolve_input(invoice)) + resolved = glsdk.resolve_input(invoice) assert isinstance(resolved, glsdk.ResolvedInput.BOLT11) assert resolved.invoice.amount_msat == 10 @@ -165,11 +163,11 @@ def test_lnurl_pay_end_to_end( # Now build an SDK-level Node for LNURL operations creds_bytes = c.creds().to_bytes() - sdk_node = glsdk.Node(glsdk.Credentials.load(creds_bytes)) + sdk_node = glsdk.NodeBuilder(glsdk.Config()).connect(creds_bytes, None) try: # Resolve - resolved = asyncio.run(glsdk.resolve_input(lnurl_service.pay_url)) + resolved = glsdk.resolve_input(lnurl_service.pay_url) assert isinstance(resolved, glsdk.ResolvedInput.LN_URL_PAY) pay_data = resolved.data @@ -232,10 +230,10 @@ def test_lnurl_pay_with_message_success_action( ) creds_bytes = c.creds().to_bytes() - sdk_node = glsdk.Node(glsdk.Credentials.load(creds_bytes)) + sdk_node = glsdk.NodeBuilder(glsdk.Config()).connect(creds_bytes, None) try: - resolved = asyncio.run(glsdk.resolve_input(lnurl_service.pay_url)) + resolved = glsdk.resolve_input(lnurl_service.pay_url) assert isinstance(resolved, glsdk.ResolvedInput.LN_URL_PAY) pay_data = resolved.data diff --git a/libs/gl-sdk/tests/test_node_events.py b/libs/gl-sdk/tests/test_node_events.py index 6e9b35533..990223666 100644 --- a/libs/gl-sdk/tests/test_node_events.py +++ b/libs/gl-sdk/tests/test_node_events.py @@ -4,7 +4,6 @@ and related types. """ -import pytest import glsdk @@ -27,10 +26,6 @@ def test_node_event_has_invoice_paid_variant(self): """Test that NodeEvent has INVOICE_PAID variant.""" assert hasattr(glsdk.NodeEvent, "INVOICE_PAID") - def test_node_event_has_unknown_variant(self): - """Test that NodeEvent has UNKNOWN variant.""" - assert hasattr(glsdk.NodeEvent, "UNKNOWN") - def test_node_has_stream_node_events_method(self): """Test that Node class has stream_node_events method.""" assert hasattr(glsdk.Node, "stream_node_events") @@ -46,24 +41,24 @@ class TestInvoicePaidEventFields: def test_invoice_paid_event_can_be_constructed(self): """Test InvoicePaidEvent can be constructed with all fields.""" event = glsdk.InvoicePaidEvent( - payment_hash=b"\x00" * 32, + payment_hash="00" * 32, bolt11="lnbcrt1...", - preimage=b"\x01" * 32, + preimage="01" * 32, label="test-invoice", amount_msat=100000, ) - assert event.payment_hash == b"\x00" * 32 + assert event.payment_hash == "00" * 32 assert event.bolt11 == "lnbcrt1..." - assert event.preimage == b"\x01" * 32 + assert event.preimage == "01" * 32 assert event.label == "test-invoice" assert event.amount_msat == 100000 def test_invoice_paid_event_str(self): """Test InvoicePaidEvent has a reasonable string representation.""" event = glsdk.InvoicePaidEvent( - payment_hash=b"\x00" * 32, + payment_hash="00" * 32, bolt11="lnbcrt1...", - preimage=b"\x01" * 32, + preimage="01" * 32, label="test-invoice", amount_msat=100000, ) @@ -78,38 +73,23 @@ class TestNodeEventVariants: def test_invoice_paid_variant_construction(self): """Test that INVOICE_PAID variant can be constructed.""" details = glsdk.InvoicePaidEvent( - payment_hash=b"\x00" * 32, + payment_hash="00" * 32, bolt11="lnbcrt1...", - preimage=b"\x01" * 32, + preimage="01" * 32, label="test-invoice", amount_msat=100000, ) event = glsdk.NodeEvent.INVOICE_PAID(details=details) assert event.details == details - def test_unknown_variant_construction(self): - """Test that UNKNOWN variant can be constructed.""" - event = glsdk.NodeEvent.UNKNOWN() - assert event is not None - def test_invoice_paid_is_invoice_paid(self): """Test is_invoice_paid() method on INVOICE_PAID variant.""" details = glsdk.InvoicePaidEvent( - payment_hash=b"\x00" * 32, + payment_hash="00" * 32, bolt11="lnbcrt1...", - preimage=b"\x01" * 32, + preimage="01" * 32, label="test-invoice", amount_msat=100000, ) event = glsdk.NodeEvent.INVOICE_PAID(details=details) assert event.is_invoice_paid() - assert not event.is_unknown() - - def test_unknown_is_unknown(self): - """Test is_unknown() method on UNKNOWN variant.""" - event = glsdk.NodeEvent.UNKNOWN() - assert event.is_unknown() - assert not event.is_invoice_paid() - - - diff --git a/libs/gl-signerproxy/src/hsmproxy.rs b/libs/gl-signerproxy/src/hsmproxy.rs index 36790e187..0566b9f7d 100644 --- a/libs/gl-signerproxy/src/hsmproxy.rs +++ b/libs/gl-signerproxy/src/hsmproxy.rs @@ -97,11 +97,11 @@ fn process_requests( return Err(e); } } - 28 => { - eprintln!("Locally handling the `hsmd_check_pubkey` call"); - let msg = Message::new(vec![0, 128, 1]); - conn.write(msg)? - }, + 28 => { + eprintln!("Locally handling the `hsmd_check_pubkey` call"); + let msg = Message::new(vec![0, 128, 1]); + conn.write(msg)? + } _ => { // By default we forward to the remote HSMd let req = tonic::Request::new(HsmRequest { diff --git a/libs/gl-testing/gltesting/fixtures.py b/libs/gl-testing/gltesting/fixtures.py index 655848d2e..4f59052e7 100644 --- a/libs/gl-testing/gltesting/fixtures.py +++ b/libs/gl-testing/gltesting/fixtures.py @@ -82,6 +82,9 @@ def root_id(cert_directory): yield identity + os.environ.pop("GL_CERT_PATH", None) + os.environ.pop("GL_CA_CRT", None) + @pytest.fixture() def scheduler_id(root_id): @@ -107,6 +110,9 @@ def nobody_id(users_id): yield identity + os.environ.pop("GL_NOBODY_CRT", None) + os.environ.pop("GL_NOBODY_KEY", None) + @pytest.fixture() def scheduler(scheduler_id, bitcoind): diff --git a/libs/gl-testing/gltesting/scheduler.py b/libs/gl-testing/gltesting/scheduler.py index 97b43c67d..34ceacd2a 100644 --- a/libs/gl-testing/gltesting/scheduler.py +++ b/libs/gl-testing/gltesting/scheduler.py @@ -178,7 +178,7 @@ async def Register( # Check that we don't already have this node registered: if len([n for n in self.nodes if n.node_id == req.node_id]) > 0: raise ValueError( - "could not register the node with the DB, does the node already exist?" + "ALREADY_EXISTS: node already registered" ) num = len(self.nodes) @@ -250,6 +250,9 @@ async def Recover(self, req): # TODO Verify that the response matches the challenge. hex_node_id = challenge.node_id.hex() + if not any(n.node_id == req.node_id for n in self.nodes): + raise ValueError(f"Recovery failed: no node with node_id={hex_node_id}") + # Check if the request contains a csr and use it to generate the # certificate. Use the old flow if csr is not present. if req.csr is not None: