diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 957abf4..1bd25c6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: Build KeepKey Vault v5 on: push: - branches: [ main, master ] + branches: [ main, master, 'release-*' ] pull_request: - branches: [ main, master ] + branches: [ main, master, 'release-*' ] workflow_dispatch: # Add permissions for GitHub Actions to create releases @@ -14,373 +14,210 @@ permissions: pull-requests: read jobs: - build-kkcli: - name: Build kkcli - strategy: - fail-fast: false - matrix: - include: - - platform: ubuntu-24.04 - target: x86_64-unknown-linux-gnu - name: kkcli-linux-x86_64 - - platform: ubuntu-24.04 - target: i686-unknown-linux-gnu - name: kkcli-linux-i686 - - platform: ubuntu-24.04 - target: aarch64-unknown-linux-gnu - name: kkcli-linux-aarch64 - - platform: windows-latest - target: x86_64-pc-windows-msvc - name: kkcli-windows-x86_64 - - platform: windows-latest - target: aarch64-pc-windows-msvc - name: kkcli-windows-aarch64 - - runs-on: ${{ matrix.platform }} - - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Install dependencies (Ubuntu/Debian) - if: startsWith(matrix.platform, 'ubuntu-') - run: | - sudo apt-get update - - # Enable multiarch support for 32-bit packages - sudo dpkg --add-architecture i386 - sudo apt-get update - - sudo apt-get install -y pkg-config libusb-1.0-0-dev libudev-dev protobuf-compiler \ - libhidapi-dev libhidapi-hidraw0 libhidapi-libusb0 libssl-dev \ - gcc-multilib g++-multilib libc6-dev-i386 libssl-dev:i386 libssl3:i386 - - # Verify protoc installation - protoc --version || (echo "❌ protoc installation failed" && exit 1) - - - name: Install dependencies (macOS) - if: matrix.platform == 'macos-latest' - run: | - echo "Installing macOS dependencies..." - # Install protobuf compiler - brew install protobuf - - # Verify protoc installation - protoc --version || (echo "❌ protoc installation failed" && exit 1) - - - name: Install dependencies (Windows) - if: matrix.platform == 'windows-latest' - shell: powershell - run: | - echo "Installing Windows dependencies..." - # Install protobuf compiler - choco install protoc --yes - - # Verify protoc installation - protoc --version - if ($LASTEXITCODE -ne 0) { - echo "❌ protoc installation failed" - exit 1 - } - - - name: Build kkcli - working-directory: ./projects/kkcli - run: | - echo "πŸ”¨ Building kkcli for ${{ matrix.target }}..." - cargo build --release --target ${{ matrix.target }} - - - name: Package kkcli binary - shell: bash - run: | - echo "πŸ“¦ Packaging kkcli binary..." - cd projects/kkcli/target/${{ matrix.target }}/release - - if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then - # Windows executable - cp kkcli.exe kkcli-${{ matrix.target }}.exe - echo "BINARY_NAME=kkcli-${{ matrix.target }}.exe" >> $GITHUB_ENV - else - # Unix executable - cp kkcli kkcli-${{ matrix.target }} - echo "BINARY_NAME=kkcli-${{ matrix.target }}" >> $GITHUB_ENV - fi - - - name: Upload kkcli binary - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.name }} - path: projects/kkcli/target/${{ matrix.target }}/release/${{ env.BINARY_NAME }} - retention-days: 7 - build: strategy: fail-fast: false matrix: include: - - platform: macos-latest - runner: macos-latest - name: macOS-universal - - platform: ubuntu-24.04 - runner: ubuntu-24.04 - name: Ubuntu-24.04-LTS - - platform: ubuntu-22.04 - runner: ubuntu-22.04 - name: Ubuntu-22.04-LTS - - platform: ubuntu-24.04 - runner: ubuntu-24.04 - name: Ubuntu-24.04-LTS-i686 - target: i686-unknown-linux-gnu - - platform: ubuntu-24.04 - runner: ubuntu-24.04 - name: Ubuntu-24.04-LTS-aarch64 - target: aarch64-unknown-linux-gnu - - platform: windows-latest - runner: windows-latest - name: Windows-x64 - - platform: windows-latest - runner: windows-latest - name: Windows-aarch64 - target: aarch64-pc-windows-msvc + - platform: 'macos-latest' + args: '--target universal-apple-darwin' + name: 'macOS-Universal' + - platform: 'ubuntu-22.04' + args: '' + name: 'Linux-AppImage' + - platform: 'windows-latest' + args: '' + name: 'Windows-x64' - runs-on: ${{ matrix.runner }} - + runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Check signing credentials - id: check-signing - shell: bash - run: | - echo "πŸ” Checking for signing credentials..." - - # Check for Tauri signing credentials - if [ -n "${{ secrets.TAURI_PRIVATE_KEY }}" ]; then - echo "βœ… TAURI_PRIVATE_KEY is set" - echo "has_tauri_signing=true" >> $GITHUB_OUTPUT - else - echo "⚠️ TAURI_PRIVATE_KEY is NOT set - will build without Tauri signing" - echo "has_tauri_signing=false" >> $GITHUB_OUTPUT - fi - - if [ -n "${{ secrets.TAURI_KEY_PASSWORD }}" ]; then - echo "βœ… TAURI_KEY_PASSWORD is set" - else - echo "⚠️ TAURI_KEY_PASSWORD is NOT set" - fi - - # Check for Apple signing credentials (macOS only) - if [[ "${{ matrix.platform }}" == "macos-latest" ]]; then - if [ -n "${{ secrets.APPLE_ID }}" ] && [ -n "${{ secrets.APPLE_PASSWORD }}" ] && [ -n "${{ secrets.APPLE_TEAM_ID }}" ]; then - echo "βœ… Apple notarization credentials are set" - echo "has_apple_notarization=true" >> $GITHUB_OUTPUT - else - echo "⚠️ Apple notarization credentials are NOT set" - echo "has_apple_notarization=false" >> $GITHUB_OUTPUT + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + + - name: Install dependencies (Ubuntu/Debian) + if: startsWith(matrix.platform, 'ubuntu-') + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev \ + libayatana-appindicator3-dev librsvg2-dev patchelf protobuf-compiler \ + pkg-config libusb-1.0-0-dev libudev-dev libhidapi-dev libhidapi-hidraw0 libhidapi-libusb0 libssl-dev + + # Verify protoc installation + protoc --version || (echo "❌ protoc installation failed" && exit 1) + + # Log distribution info for debugging + echo "πŸ“‹ Distribution Info:" + lsb_release -a || echo "lsb_release not available" + uname -a + + - name: Install dependencies (macOS) + if: matrix.platform == 'macos-latest' + run: | + echo "Installing macOS dependencies..." + # Install protobuf compiler + brew install protobuf + + # Verify protoc installation + protoc --version || (echo "❌ protoc installation failed" && exit 1) + + - name: Install dependencies (Windows) + if: matrix.platform == 'windows-latest' + shell: powershell + run: | + echo "Installing Windows dependencies..." + # Install protobuf compiler + choco install protoc --yes + + # Verify protoc installation + protoc --version + if ($LASTEXITCODE -ne 0) { + echo "❌ protoc installation failed" + exit 1 + } + + - name: Install Bun + uses: oven-sh/setup-bun@v2 + + - name: Install app dependencies + working-directory: ./projects/vault-v2 + run: bun install + + - name: Import Apple Certificate + if: matrix.platform == 'macos-latest' + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + CERT_PASSWORD: ${{ secrets.CERT_PW }} + run: | + # Check if certificate is provided + if [ -z "$APPLE_CERTIFICATE" ]; then + echo "⚠️ APPLE_CERTIFICATE not set, skipping certificate import" + echo "To enable code signing, add APPLE_CERTIFICATE secret to your repository" + exit 0 + fi + + echo "βœ… Certificate environment variable is set" + echo "Certificate length: ${#APPLE_CERTIFICATE}" + + # Debug: Check first few characters (safe, won't expose the cert) + echo "Certificate starts with: ${APPLE_CERTIFICATE:0:20}..." + + # Create a temporary keychain + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + KEYCHAIN_PASSWORD=$(openssl rand -base64 32) + + echo "Creating keychain at $KEYCHAIN_PATH" + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # Import certificate - handle potential newlines in base64 + echo "Decoding certificate..." + # Remove any whitespace/newlines from the base64 string + echo "$APPLE_CERTIFICATE" | tr -d '\n\r ' > certificate.base64 + + # Check if base64 file is valid + if [ ! -s certificate.base64 ]; then + echo "❌ Error: certificate.base64 is empty" + exit 1 fi - if [ -n "${{ secrets.MACOS_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.MACOS_CERTIFICATE_PASSWORD }}" ]; then - echo "βœ… Apple code signing credentials are set" - echo "has_apple_codesigning=true" >> $GITHUB_OUTPUT + echo "Base64 file size: $(wc -c < certificate.base64) bytes" + + # Decode base64 (macOS uses -D flag) + echo "Attempting to decode certificate..." + if base64 -D certificate.base64 > certificate.p12 2>/dev/null; then + echo "βœ… Certificate decoded successfully using base64 -D" + ls -la certificate.p12 + elif base64 --decode certificate.base64 > certificate.p12 2>/dev/null; then + echo "βœ… Certificate decoded successfully using base64 --decode" + ls -la certificate.p12 else - echo "⚠️ Apple code signing credentials are NOT set" - echo "has_apple_codesigning=false" >> $GITHUB_OUTPUT + echo "❌ Error: Failed to decode certificate." + echo "Trying alternative method..." + # Try with input redirection + if base64 -D < certificate.base64 > certificate.p12 2>/dev/null; then + echo "βœ… Certificate decoded successfully with input redirection" + ls -la certificate.p12 + else + echo "❌ All decode methods failed. The certificate may be corrupted." + exit 1 + fi fi - else - echo "has_apple_notarization=false" >> $GITHUB_OUTPUT - echo "has_apple_codesigning=false" >> $GITHUB_OUTPUT - fi - - # Summary - echo "" - echo "πŸ”’ Signing Summary:" - echo " - Tauri Signing: ${{ steps.check-signing.outputs.has_tauri_signing == 'true' && 'βœ… Available' || '❌ Missing' }}" - if [[ "${{ matrix.platform }}" == "macos-latest" ]]; then - echo " - Apple Code Signing: ${{ steps.check-signing.outputs.has_apple_codesigning == 'true' && 'βœ… Available' || '❌ Missing' }}" - echo " - Apple Notarization: ${{ steps.check-signing.outputs.has_apple_notarization == 'true' && 'βœ… Available' || '❌ Missing' }}" - fi - echo "" - - if [[ "${{ steps.check-signing.outputs.has_tauri_signing }}" == "false" ]]; then - echo "⚠️ Warning: Building without signing - suitable for development only" - echo "" - echo "To enable signing, add these secrets to your repository:" - echo " - TAURI_PRIVATE_KEY: Your Tauri signing private key" - echo " - TAURI_KEY_PASSWORD: Password for your Tauri signing key" - if [[ "${{ matrix.platform }}" == "macos-latest" ]]; then - echo " - APPLE_ID: Apple developer account email" - echo " - APPLE_PASSWORD: Apple app-specific password" - echo " - APPLE_TEAM_ID: Apple developer team ID" - echo " - MACOS_CERTIFICATE_BASE64: base64-encoded Apple certificate" - echo " - MACOS_CERTIFICATE_PASSWORD: Apple certificate password" - echo " - CODESIGN_IDENTITY: Apple code signing identity" - echo " - KEYCHAIN_NAME: macOS keychain name" - echo " - KEYCHAIN_PASSWORD: macOS keychain password" + # Debug password format (safely) + echo "=== Certificate Import Debug v2 ===" + echo "Checking certificate password..." + echo "Password length: ${#CERT_PASSWORD}" + echo "Password starts with: ${CERT_PASSWORD:0:3}..." + echo "Password ends with: ...${CERT_PASSWORD: -2}" + + # Check if password contains expected pattern + if [[ "$CERT_PASSWORD" == dsa* ]]; then + echo "βœ… Password starts with expected prefix" + else + echo "⚠️ Password does NOT start with expected prefix 'dsa'" fi - fi - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || matrix.target || '' }} - - - name: Install dependencies (Ubuntu/Debian) - if: startsWith(matrix.platform, 'ubuntu-') - run: | - sudo apt-get update - - # Enable multiarch support for 32-bit packages - sudo dpkg --add-architecture i386 - sudo apt-get update - - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev \ - libayatana-appindicator3-dev librsvg2-dev patchelf protobuf-compiler \ - pkg-config libusb-1.0-0-dev libudev-dev libhidapi-dev libhidapi-hidraw0 libhidapi-libusb0 libssl-dev \ - gcc-multilib g++-multilib libc6-dev-i386 libssl-dev:i386 libssl3:i386 - - # Verify protoc installation - protoc --version || (echo "❌ protoc installation failed" && exit 1) - - # Log distribution info for debugging - echo "πŸ“‹ Distribution Info:" - lsb_release -a || echo "lsb_release not available" - uname -a - - - name: Install dependencies (macOS) - if: matrix.platform == 'macos-latest' - run: | - echo "Installing macOS dependencies..." - # Install protobuf compiler - brew install protobuf - - # Verify protoc installation - protoc --version || (echo "❌ protoc installation failed" && exit 1) - - - name: Install dependencies (Windows) - if: matrix.platform == 'windows-latest' - shell: powershell - run: | - echo "Installing Windows dependencies..." - # Install protobuf compiler - choco install protoc --yes - - # Verify protoc installation - protoc --version - if ($LASTEXITCODE -ne 0) { - echo "❌ protoc installation failed" - exit 1 - } - - - name: Install Bun - uses: oven-sh/setup-bun@v2 - - - name: Install app dependencies - working-directory: ./projects/vault-v2 - run: | - echo "πŸ“ Current directory: $(pwd)" - echo "πŸ“¦ Installing dependencies..." - bun install - - - name: Debug build environment - working-directory: ./projects/vault-v2 - shell: bash - run: | - echo "πŸ” Debugging build environment..." - echo "πŸ“ Current directory: $(pwd)" - echo "πŸ“ Parent directories:" - ls -la ../ - ls -la ../../ - ls -la ../../../ - echo "πŸ“ Looking for device-protocol:" - find ../../../ -name "device-protocol" -type d 2>/dev/null || echo "Not found" - echo "πŸ“ Looking for types.proto:" - find ../../../ -name "types.proto" 2>/dev/null || echo "Not found" - - - name: Build the app - uses: tauri-apps/tauri-action@v0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RUST_BACKTRACE: 1 # Enable Rust backtraces for better error messages - # Conditionally set Tauri signing credentials - TAURI_SIGNING_PRIVATE_KEY: ${{ steps.check-signing.outputs.has_tauri_signing == 'true' && secrets.TAURI_PRIVATE_KEY || '' }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ steps.check-signing.outputs.has_tauri_signing == 'true' && secrets.TAURI_KEY_PASSWORD || '' }} - # Conditionally set Apple notarization credentials - APPLE_ID: ${{ steps.check-signing.outputs.has_apple_notarization == 'true' && secrets.APPLE_ID || '' }} - APPLE_PASSWORD: ${{ steps.check-signing.outputs.has_apple_notarization == 'true' && secrets.APPLE_PASSWORD || '' }} - APPLE_TEAM_ID: ${{ steps.check-signing.outputs.has_apple_notarization == 'true' && secrets.APPLE_TEAM_ID || '' }} - # Conditionally set Apple code signing credentials - CODESIGN_IDENTITY: ${{ steps.check-signing.outputs.has_apple_codesigning == 'true' && secrets.CODESIGN_IDENTITY || '' }} - KEYCHAIN_NAME: ${{ steps.check-signing.outputs.has_apple_codesigning == 'true' && secrets.KEYCHAIN_NAME || '' }} - KEYCHAIN_PASSWORD: ${{ steps.check-signing.outputs.has_apple_codesigning == 'true' && secrets.KEYCHAIN_PASSWORD || '' }} - MACOS_CERTIFICATE_BASE64: ${{ steps.check-signing.outputs.has_apple_codesigning == 'true' && secrets.MACOS_CERTIFICATE_BASE64 || '' }} - MACOS_CERTIFICATE_PASSWORD: ${{ steps.check-signing.outputs.has_apple_codesigning == 'true' && secrets.MACOS_CERTIFICATE_PASSWORD || '' }} - with: - projectPath: ./projects/vault-v2 - tagName: v__VERSION__ # the action automatically replaces __VERSION__ with the app version - releaseName: 'KeepKey Vault v__VERSION__' - args: ${{ matrix.platform == 'macos-latest' && '--target universal-apple-darwin' || matrix.target && format('--target {0}', matrix.target) || '' }} - releaseBody: | - ## KeepKey Vault v__VERSION__ - ### πŸš€ Features - - Institutional-grade self-custody solution - - Multi-user governance and approval workflows - - Advanced security features with hardware wallet integration - - Enterprise API for programmatic access + # Verify certificate integrity + echo "Verifying certificate file..." + echo "Certificate MD5: $(md5 -q certificate.p12)" + echo "Certificate size: $(wc -c < certificate.p12) bytes" - ### πŸ“¦ Downloads - - **macOS**: Download the `.dmg` file below - - **Windows**: Download the `.msi` installer - - **Linux**: Download the `.AppImage` or `.deb` package - ### πŸ”’ Security - ${{ steps.check-signing.outputs.has_tauri_signing == 'true' && 'All releases are signed and verified. Please verify signatures before installation.' || '⚠️ This release is NOT signed. Use at your own risk in development environments only.' }} - releaseDraft: true - prerelease: false - includeUpdaterJson: true - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - if: always() # Upload even if build fails for debugging - with: - name: ${{ matrix.name }}-artifacts - path: | - projects/vault-v2/src-tauri/target/release/bundle/ - retention-days: 7 - - - name: Upload build logs - uses: actions/upload-artifact@v4 - if: failure() - with: - name: ${{ matrix.name }}-build-logs - path: | - projects/vault-v2/src-tauri/target/release/build/ - retention-days: 3 - - - name: Build summary - shell: bash - run: | - echo "## πŸ“¦ Build Summary - ${{ matrix.name }}" >> $GITHUB_STEP_SUMMARY - echo "- **Platform**: ${{ matrix.platform }}" >> $GITHUB_STEP_SUMMARY - echo "- **Tauri Signing**: ${{ steps.check-signing.outputs.has_tauri_signing == 'true' && 'βœ… Enabled' || '⚠️ Disabled' }}" >> $GITHUB_STEP_SUMMARY - if [[ "${{ matrix.platform }}" == "macos-latest" ]]; then - echo "- **Apple Code Signing**: ${{ steps.check-signing.outputs.has_apple_codesigning == 'true' && 'βœ… Enabled' || '⚠️ Disabled' }}" >> $GITHUB_STEP_SUMMARY - echo "- **Apple Notarization**: ${{ steps.check-signing.outputs.has_apple_notarization == 'true' && 'βœ… Enabled' || '⚠️ Disabled' }}" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - if [[ "${{ steps.check-signing.outputs.has_tauri_signing }}" == "false" ]]; then - echo "⚠️ **Warning**: This build is not signed and should only be used for development/testing purposes." >> $GITHUB_STEP_SUMMARY - fi \ No newline at end of file + # Import to keychain + echo "Importing certificate to keychain..." + # Use printf to handle special characters in password + security import certificate.p12 -k $KEYCHAIN_PATH -P "${CERT_PASSWORD}" -T /usr/bin/codesign -T /usr/bin/productsign || { + echo "❌ Failed to import certificate. Trying with different password format..." + # Try without quotes in case of escaping issues + security import certificate.p12 -k $KEYCHAIN_PATH -P ${CERT_PASSWORD} -T /usr/bin/codesign -T /usr/bin/productsign || { + echo "❌ Certificate import failed. The CERT_PW secret may be incorrect." + echo "Make sure CERT_PW is set to the exact password used when creating the .p12 file" + exit 1 + } + } + + # Add keychain to search list + security list-keychains -d user -s $KEYCHAIN_PATH $(security list-keychains -d user | sed 's/\"//g') + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # Verify the certificate was imported + echo "Verifying certificate import..." + security find-identity -v -p codesigning $KEYCHAIN_PATH + + # Clean up + rm -f certificate.p12 certificate.base64 + + echo "βœ… Certificate imported successfully" + + - name: Build Tauri app + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Apple notarization credentials + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + with: + projectPath: ./projects/vault-v2 + args: ${{ matrix.args }} + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ matrix.name }}-${{ github.run_number }} + path: | + projects/vault-v2/src-tauri/target/release/bundle/ + retention-days: 7# Trigger CI build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5ad8fda..dd3fe77 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release KeepKey Vault v5 +ο»Ώname: Release KeepKey Vault v5 on: push: @@ -8,6 +8,7 @@ on: jobs: create-release: + if: startsWith(github.ref, 'refs/tags/') permissions: contents: write runs-on: ubuntu-latest @@ -30,127 +31,9 @@ jobs: draft: true prerelease: false - build-kkcli: - needs: create-release - permissions: - contents: write - - strategy: - fail-fast: false - matrix: - include: - - platform: ubuntu-24.04 - target: x86_64-unknown-linux-gnu - name: kkcli-linux-x86_64 - - platform: ubuntu-24.04 - target: i686-unknown-linux-gnu - name: kkcli-linux-i686 - - platform: ubuntu-24.04 - target: aarch64-unknown-linux-gnu - name: kkcli-linux-aarch64 - - platform: windows-latest - target: x86_64-pc-windows-msvc - name: kkcli-windows-x86_64 - - platform: windows-latest - target: aarch64-pc-windows-msvc - name: kkcli-windows-aarch64 - - runs-on: ${{ matrix.platform }} - - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Install dependencies (Ubuntu/Debian) - if: startsWith(matrix.platform, 'ubuntu-') - run: | - sudo apt-get update - - # Enable multiarch support for 32-bit packages - sudo dpkg --add-architecture i386 - sudo apt-get update - - sudo apt-get install -y pkg-config libusb-1.0-0-dev libudev-dev protobuf-compiler \ - libhidapi-dev libhidapi-hidraw0 libhidapi-libusb0 libssl-dev \ - gcc-multilib g++-multilib libc6-dev-i386 libssl-dev:i386 libssl3:i386 - - # Verify protoc installation - protoc --version || (echo "❌ protoc installation failed" && exit 1) - - - name: Install dependencies (macOS) - if: matrix.platform == 'macos-latest' - run: | - echo "Installing macOS dependencies..." - # Install protobuf compiler - brew install protobuf - - # Verify protoc installation - protoc --version || (echo "❌ protoc installation failed" && exit 1) - - - name: Install dependencies (Windows) - if: matrix.platform == 'windows-latest' - shell: powershell - run: | - echo "Installing Windows dependencies..." - # Install protobuf compiler - choco install protoc --yes - - # Verify protoc installation - protoc --version - if ($LASTEXITCODE -ne 0) { - echo "❌ protoc installation failed" - exit 1 - } - - - name: Build kkcli - working-directory: ./projects/kkcli - run: | - echo "πŸ”¨ Building kkcli for ${{ matrix.target }}..." - cargo build --release --target ${{ matrix.target }} - - - name: Package kkcli binary - shell: bash - run: | - echo "πŸ“¦ Packaging kkcli binary..." - cd projects/kkcli/target/${{ matrix.target }}/release - - if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then - # Windows executable - cp kkcli.exe kkcli-${{ matrix.target }}.exe - echo "BINARY_NAME=kkcli-${{ matrix.target }}.exe" >> $GITHUB_ENV - echo "ASSET_NAME=kkcli-${{ matrix.target }}.exe" >> $GITHUB_ENV - else - # Unix executable - cp kkcli kkcli-${{ matrix.target }} - echo "BINARY_NAME=kkcli-${{ matrix.target }}" >> $GITHUB_ENV - echo "ASSET_NAME=kkcli-${{ matrix.target }}" >> $GITHUB_ENV - fi - - - name: Upload kkcli to release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.release_upload_url }} - asset_path: projects/kkcli/target/${{ matrix.target }}/release/${{ env.BINARY_NAME }} - asset_name: ${{ env.ASSET_NAME }} - asset_content_type: application/octet-stream - - - name: Upload kkcli artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.name }} - path: projects/kkcli/target/${{ matrix.target }}/release/${{ env.BINARY_NAME }} - retention-days: 7 - build-tauri: - needs: create-release + needs: [create-release] + if: always() permissions: contents: write @@ -161,24 +44,12 @@ jobs: - platform: 'macos-latest' args: '--target universal-apple-darwin' name: 'macOS-universal' - - platform: 'ubuntu-24.04' - args: '' - name: 'Ubuntu-24.04-LTS' - platform: 'ubuntu-22.04' args: '' - name: 'Ubuntu-22.04-LTS' - - platform: 'ubuntu-24.04' - args: '--target i686-unknown-linux-gnu' - name: 'Ubuntu-24.04-LTS-i686' - - platform: 'ubuntu-24.04' - args: '--target aarch64-unknown-linux-gnu' - name: 'Ubuntu-24.04-LTS-aarch64' + name: 'Linux-AppImage' - platform: 'windows-latest' args: '' name: 'Windows-x64' - - platform: 'windows-latest' - args: '--target aarch64-pc-windows-msvc' - name: 'Windows-aarch64' runs-on: ${{ matrix.platform }} steps: @@ -209,7 +80,7 @@ jobs: # Check for Apple signing credentials (macOS only) if [[ "${{ matrix.platform }}" == "macos-latest" ]]; then - if [ -n "${{ secrets.APPLE_ID }}" ] && [ -n "${{ secrets.APPLE_PASSWORD }}" ] && [ -n "${{ secrets.APPLE_TEAM_ID }}" ]; then + if [ -n "${{ secrets.APPLE_ID }}" ] && [ -n "${{ secrets.APPLE_ID_PASSWORD }}" ] && [ -n "${{ secrets.APPLE_TEAM_ID }}" ]; then echo "βœ… Apple notarization credentials are set" echo "has_apple_notarization=true" >> $GITHUB_OUTPUT else @@ -217,7 +88,7 @@ jobs: echo "has_apple_notarization=false" >> $GITHUB_OUTPUT fi - if [ -n "${{ secrets.MACOS_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.MACOS_CERTIFICATE_PASSWORD }}" ]; then + if [ -n "${{ secrets.APPLE_CERTIFICATE }}" ] && [ -n "${{ secrets.CERT_PW }}" ]; then echo "βœ… Apple code signing credentials are set" echo "has_apple_codesigning=true" >> $GITHUB_OUTPUT else @@ -248,10 +119,10 @@ jobs: if [[ "${{ matrix.platform }}" == "macos-latest" ]]; then echo " - APPLE_ID: Apple developer account email" - echo " - APPLE_PASSWORD: Apple app-specific password" + echo " - APPLE_ID_PASSWORD: Apple app-specific password" echo " - APPLE_TEAM_ID: Apple developer team ID" - echo " - MACOS_CERTIFICATE_BASE64: base64-encoded Apple certificate" - echo " - MACOS_CERTIFICATE_PASSWORD: Apple certificate password" + echo " - APPLE_CERTIFICATE: base64-encoded Apple certificate" + echo " - CERT_PW: Apple certificate password" echo " - CODESIGN_IDENTITY: Apple code signing identity" echo " - KEYCHAIN_NAME: macOS keychain name" echo " - KEYCHAIN_PASSWORD: macOS keychain password" @@ -273,14 +144,9 @@ jobs: run: | sudo apt-get update - # Enable multiarch support for 32-bit packages - sudo dpkg --add-architecture i386 - sudo apt-get update - sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev \ libayatana-appindicator3-dev librsvg2-dev patchelf protobuf-compiler \ - pkg-config libusb-1.0-0-dev libudev-dev libhidapi-dev libhidapi-hidraw0 libhidapi-libusb0 libssl-dev \ - gcc-multilib g++-multilib libc6-dev-i386 libssl-dev:i386 libssl3:i386 + pkg-config libusb-1.0-0-dev libudev-dev libhidapi-dev libhidapi-hidraw0 libhidapi-libusb0 libssl-dev # Verify protoc installation protoc --version || (echo "❌ protoc installation failed" && exit 1) @@ -322,6 +188,112 @@ jobs: working-directory: ./projects/vault-v2 run: bun install + - name: Import Apple Certificate + if: matrix.platform == 'macos-latest' + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + CERT_PASSWORD: ${{ secrets.CERT_PW }} + run: | + # Check if certificate is provided + if [ -z "$APPLE_CERTIFICATE" ]; then + echo "⚠️ APPLE_CERTIFICATE not set, skipping certificate import" + echo "To enable code signing, add APPLE_CERTIFICATE secret to your repository" + exit 0 + fi + + echo "βœ… Certificate environment variable is set" + echo "Certificate length: ${#APPLE_CERTIFICATE}" + + # Debug: Check first few characters (safe, won't expose the cert) + echo "Certificate starts with: ${APPLE_CERTIFICATE:0:20}..." + + # Create a temporary keychain + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + KEYCHAIN_PASSWORD=$(openssl rand -base64 32) + + echo "Creating keychain at $KEYCHAIN_PATH" + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # Import certificate - handle potential newlines in base64 + echo "Decoding certificate..." + # Remove any whitespace/newlines from the base64 string + echo "$APPLE_CERTIFICATE" | tr -d '\n\r ' > certificate.base64 + + # Check if base64 file is valid + if [ ! -s certificate.base64 ]; then + echo "❌ Error: certificate.base64 is empty" + exit 1 + fi + + echo "Base64 file size: $(wc -c < certificate.base64) bytes" + + # Decode base64 (macOS uses -D flag) + echo "Attempting to decode certificate..." + if base64 -D certificate.base64 > certificate.p12 2>/dev/null; then + echo "βœ… Certificate decoded successfully using base64 -D" + ls -la certificate.p12 + elif base64 --decode certificate.base64 > certificate.p12 2>/dev/null; then + echo "βœ… Certificate decoded successfully using base64 --decode" + ls -la certificate.p12 + else + echo "❌ Error: Failed to decode certificate." + echo "Trying alternative method..." + # Try with input redirection + if base64 -D < certificate.base64 > certificate.p12 2>/dev/null; then + echo "βœ… Certificate decoded successfully with input redirection" + ls -la certificate.p12 + else + echo "❌ All decode methods failed. The certificate may be corrupted." + exit 1 + fi + fi + + # Debug password format (safely) + echo "Checking certificate password..." + echo "Password length: ${#CERT_PASSWORD}" + echo "Password starts with: ${CERT_PASSWORD:0:3}..." + echo "Password ends with: ...${CERT_PASSWORD: -2}" + + # Check if password contains expected pattern + if [[ "$CERT_PASSWORD" == dsa* ]]; then + echo "βœ… Password starts with expected prefix" + else + echo "⚠️ Password does NOT start with expected prefix 'dsa'" + fi + + # Verify certificate integrity + echo "Verifying certificate file..." + echo "Certificate MD5: $(md5 -q certificate.p12)" + + + # Import to keychain + echo "Importing certificate to keychain..." + # Use printf to handle special characters in password + security import certificate.p12 -k $KEYCHAIN_PATH -P "${CERT_PASSWORD}" -T /usr/bin/codesign -T /usr/bin/productsign || { + echo "❌ Failed to import certificate. Trying with different password format..." + # Try without quotes in case of escaping issues + security import certificate.p12 -k $KEYCHAIN_PATH -P ${CERT_PASSWORD} -T /usr/bin/codesign -T /usr/bin/productsign || { + echo "❌ Certificate import failed. The CERT_PW secret may be incorrect." + echo "Make sure CERT_PW is set to the exact password used when creating the .p12 file" + exit 1 + } + } + + # Add keychain to search list + security list-keychains -d user -s $KEYCHAIN_PATH $(security list-keychains -d user | sed 's/\"//g') + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # Verify the certificate was imported + echo "Verifying certificate import..." + security find-identity -v -p codesigning $KEYCHAIN_PATH + + # Clean up + rm -f certificate.p12 certificate.base64 + + echo "βœ… Certificate imported successfully" + - name: Build Tauri app uses: tauri-apps/tauri-action@v0 env: @@ -331,17 +303,11 @@ jobs: TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ steps.check-signing.outputs.has_tauri_signing == 'true' && secrets.TAURI_KEY_PASSWORD || '' }} # Conditionally set Apple notarization credentials APPLE_ID: ${{ steps.check-signing.outputs.has_apple_notarization == 'true' && secrets.APPLE_ID || '' }} - APPLE_PASSWORD: ${{ steps.check-signing.outputs.has_apple_notarization == 'true' && secrets.APPLE_PASSWORD || '' }} + APPLE_PASSWORD: ${{ steps.check-signing.outputs.has_apple_notarization == 'true' && secrets.APPLE_ID_PASSWORD || '' }} APPLE_TEAM_ID: ${{ steps.check-signing.outputs.has_apple_notarization == 'true' && secrets.APPLE_TEAM_ID || '' }} - # Conditionally set Apple code signing credentials - CODESIGN_IDENTITY: ${{ steps.check-signing.outputs.has_apple_codesigning == 'true' && secrets.CODESIGN_IDENTITY || '' }} - KEYCHAIN_NAME: ${{ steps.check-signing.outputs.has_apple_codesigning == 'true' && secrets.KEYCHAIN_NAME || '' }} - KEYCHAIN_PASSWORD: ${{ steps.check-signing.outputs.has_apple_codesigning == 'true' && secrets.KEYCHAIN_PASSWORD || '' }} - MACOS_CERTIFICATE_BASE64: ${{ steps.check-signing.outputs.has_apple_codesigning == 'true' && secrets.MACOS_CERTIFICATE_BASE64 || '' }} - MACOS_CERTIFICATE_PASSWORD: ${{ steps.check-signing.outputs.has_apple_codesigning == 'true' && secrets.MACOS_CERTIFICATE_PASSWORD || '' }} with: projectPath: ./projects/vault-v2 - releaseId: ${{ needs.create-release.outputs.release_id }} + releaseId: ${{ startsWith(github.ref, 'refs/tags/') && needs.create-release.outputs.release_id || '' }} args: ${{ matrix.args }} - name: Upload artifacts @@ -369,7 +335,8 @@ jobs: fi update-release-notes: - needs: [create-release, build-kkcli, build-tauri] + if: startsWith(github.ref, 'refs/tags/') && always() && needs.create-release.result == 'success' + needs: [create-release, build-tauri] permissions: contents: write runs-on: ubuntu-latest @@ -400,14 +367,9 @@ jobs: ### Downloads - **GUI Application:** - - macOS: \`.dmg\` - - Windows: \`.msi\` - - Linux: \`.AppImage\` or \`.deb\` - - **Command Line:** - - Linux: \`kkcli-x86_64-unknown-linux-gnu\` - - Windows: \`kkcli-x86_64-pc-windows-msvc.exe\` + - **macOS**: Universal app (\`.dmg\`) + - **Windows**: x64 installer (\`.exe\`) + - **Linux**: AppImage (works on most distributions) ${signingStatus}`; @@ -419,12 +381,22 @@ jobs: }); publish-release: + if: startsWith(github.ref, 'refs/tags/') && always() && needs.create-release.result == 'success' permissions: contents: write runs-on: ubuntu-latest - needs: [create-release, build-kkcli, build-tauri, update-release-notes] + needs: [create-release, build-tauri, update-release-notes] steps: + - name: Check build results + run: | + echo "## πŸ“‹ Build Results Summary" >> $GITHUB_STEP_SUMMARY + echo "- **macOS**: ${{ needs.build-tauri.result == 'success' && 'βœ… Success' || '❌ Failed - Manual upload needed' }}" >> $GITHUB_STEP_SUMMARY + echo "- **Windows**: ${{ needs.build-tauri.result == 'success' && 'βœ… Success' || '❌ Failed - Manual upload needed' }}" >> $GITHUB_STEP_SUMMARY + echo "- **Linux**: ${{ needs.build-tauri.result == 'success' && 'βœ… Success' || '❌ Failed - Manual upload needed' }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "ℹ️ **Note**: Release will be published with available builds. Manual upload may be needed for failed platforms." >> $GITHUB_STEP_SUMMARY + - name: Publish release id: publish-release uses: actions/github-script@v6 @@ -442,10 +414,19 @@ jobs: console.log('βœ… Release published successfully!'); + // Log build status + const buildResult = '${{ needs.build-tauri.result }}'; + if (buildResult === 'success') { + console.log('πŸŽ‰ All builds completed successfully'); + } else { + console.log('⚠️ Some builds failed - manual upload may be needed'); + console.log('Check the build summary for details'); + } + // Log signing status for transparency const hasSigningSecrets = '${{ secrets.TAURI_PRIVATE_KEY }}' !== ''; if (hasSigningSecrets) { - console.log('πŸ”’ Release is signed and verified'); + console.log('πŸ”’ Available builds are signed and verified'); } else { - console.log('⚠️ Release is NOT signed - development build only'); - } \ No newline at end of file + console.log('⚠️ Builds are NOT signed - development build only'); + } diff --git a/Makefile b/Makefile index c40578a..b7198d6 100644 --- a/Makefile +++ b/Makefile @@ -10,11 +10,23 @@ # make check-deps - Verify keepkey-rust dependency linking # make test-keepkey-rust - Run keepkey-rust tests # +# Release workflow targets: +# make release-branch VERSION=2.2.6 - Create release-2.2.6 branch and update version +# make release-patch-branch - Auto-increment patch version and create branch +# make release-minor-branch - Auto-increment minor version and create branch +# make release-major-branch - Auto-increment major version and create branch +# +# Version update only: +# make release VERSION=2.2.6 - Update version without creating branch +# make release-patch - Bump patch version (X.Y.Z+1) +# make release-minor - Bump minor version (X.Y+1.0) +# make release-major - Bump major version (X+1.0.0) +# # Dependencies: # - Rust/Cargo (for keepkey-rust and Tauri backend) # - Bun (for frontend dependencies) # - jq (for dependency verification) -.PHONY: all firmware kkcli rest vault-ui vault test test-rest clean keepkey-rust vault-build rebuild check-deps help +.PHONY: all firmware kkcli rest vault-ui vault test test-rest clean keepkey-rust vault-build rebuild check-deps help release release-patch release-minor release-major release-branch release-patch-branch release-minor-branch release-major-branch # Display help information help: @@ -30,6 +42,16 @@ help: @echo " check-deps - Verify keepkey-rust dependency linking" @echo " test-keepkey-rust - Run keepkey-rust tests" @echo "" + @echo "Release targets:" + @echo " release - Bump version across all projects (requires VERSION=x.y.z)" + @echo " release-branch - Create release branch and bump version (requires VERSION=x.y.z)" + @echo " release-patch - Bump patch version (X.Y.Z+1)" + @echo " release-minor - Bump minor version (X.Y+1.0)" + @echo " release-major - Bump major version (X+1.0.0)" + @echo " release-patch-branch - Create release branch with patch version bump" + @echo " release-minor-branch - Create release branch with minor version bump" + @echo " release-major-branch - Create release branch with major version bump" + @echo "" @echo "Dependencies:" @echo " - Rust/Cargo (for keepkey-rust and Tauri backend)" @echo " - Bun (for frontend dependencies)" @@ -73,6 +95,7 @@ vault: keepkey-rust check-deps # Build vault for production vault-build: keepkey-rust check-deps + lsof -ti:1420 | xargs kill -9 \ @echo "πŸ”§ Building vault-v2 for production with latest keepkey-rust..." cd projects/vault-v2 && bun i && tauri build @@ -93,3 +116,77 @@ rebuild: clean all vault-dev: @echo "πŸš€ Quick vault-v2 development build..." cd projects/vault-v2 && bun i && tauri dev + +# Release targets for version management +release: + @chmod +x scripts/release.sh + @./scripts/release.sh $(VERSION) + +# Create release branch and update version +release-branch: + @if [ -z "$(VERSION)" ]; then \ + echo "Error: VERSION is required. Usage: make release-branch VERSION=2.2.6"; \ + exit 1; \ + fi + @echo "πŸ”€ Creating release branch for version $(VERSION)..." + @git checkout -b release-$(VERSION) 2>/dev/null || (echo "⚠️ Branch release-$(VERSION) already exists, switching to it..." && git checkout release-$(VERSION)) + @chmod +x scripts/release.sh + @./scripts/release.sh $(VERSION) + @echo "βœ… Release branch release-$(VERSION) created and version updated" + @echo "" + @echo "Next steps:" + @echo " 1. Review changes: git diff" + @echo " 2. Build and test: make vault-build" + @echo " 3. Commit: git add . && git commit -m 'chore: release v$(VERSION)'" + @echo " 4. Push branch: git push -u origin release-$(VERSION)" + @echo " 5. Create PR to master branch" + @echo " 6. After merge, tag: git tag -a v$(VERSION) -m 'Release v$(VERSION)' && git push --tags" + +# Quick release helpers +release-patch: + @CURRENT=$$(grep '"version"' projects/vault-v2/package.json | head -1 | cut -d'"' -f4); \ + IFS='.' read -r major minor patch <<< "$$CURRENT"; \ + NEW_PATCH=$$((patch + 1)); \ + NEW_VERSION="$$major.$$minor.$$NEW_PATCH"; \ + chmod +x scripts/release.sh; \ + ./scripts/release.sh $$NEW_VERSION + +release-minor: + @CURRENT=$$(grep '"version"' projects/vault-v2/package.json | head -1 | cut -d'"' -f4); \ + IFS='.' read -r major minor patch <<< "$$CURRENT"; \ + NEW_MINOR=$$((minor + 1)); \ + NEW_VERSION="$$major.$$NEW_MINOR.0"; \ + chmod +x scripts/release.sh; \ + ./scripts/release.sh $$NEW_VERSION + +release-major: + @CURRENT=$$(grep '"version"' projects/vault-v2/package.json | head -1 | cut -d'"' -f4); \ + IFS='.' read -r major minor patch <<< "$$CURRENT"; \ + NEW_MAJOR=$$((major + 1)); \ + NEW_VERSION="$$NEW_MAJOR.0.0"; \ + chmod +x scripts/release.sh; \ + ./scripts/release.sh $$NEW_VERSION + +# Create patch release branch (convenience wrapper) +release-patch-branch: + @CURRENT=$$(grep '"version"' projects/vault-v2/package.json | head -1 | cut -d'"' -f4); \ + IFS='.' read -r major minor patch <<< "$$CURRENT"; \ + NEW_PATCH=$$((patch + 1)); \ + NEW_VERSION="$$major.$$minor.$$NEW_PATCH"; \ + $(MAKE) release-branch VERSION=$$NEW_VERSION + +# Create minor release branch (convenience wrapper) +release-minor-branch: + @CURRENT=$$(grep '"version"' projects/vault-v2/package.json | head -1 | cut -d'"' -f4); \ + IFS='.' read -r major minor patch <<< "$$CURRENT"; \ + NEW_MINOR=$$((minor + 1)); \ + NEW_VERSION="$$major.$$NEW_MINOR.0"; \ + $(MAKE) release-branch VERSION=$$NEW_VERSION + +# Create major release branch (convenience wrapper) +release-major-branch: + @CURRENT=$$(grep '"version"' projects/vault-v2/package.json | head -1 | cut -d'"' -f4); \ + IFS='.' read -r major minor patch <<< "$$CURRENT"; \ + NEW_MAJOR=$$((major + 1)); \ + NEW_VERSION="$$NEW_MAJOR.0.0"; \ + $(MAKE) release-branch VERSION=$$NEW_VERSION diff --git a/README.md b/README.md index 6320b00..86327f7 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,82 @@ make vault - **Unified UI**: One Vite UI bundle for both web and desktop. - **Easy builds**: One command to build everything. +## Release Process + +### Creating a Release + +The project uses a Makefile-based release workflow for versioning and branch management: + +#### Quick Release Commands + +```bash +# Create release branch with specific version +make release-branch VERSION=2.2.7 + +# Auto-increment versions and create branch +make release-patch-branch # Bumps X.Y.Z -> X.Y.(Z+1) +make release-minor-branch # Bumps X.Y.Z -> X.(Y+1).0 +make release-major-branch # Bumps X.Y.Z -> (X+1).0.0 +``` + +#### Manual Release Steps + +1. **Create release branch and update version:** + ```bash + make release-branch VERSION=2.2.7 + ``` + +2. **Build and test locally with signing:** + ```bash + cd projects/vault-v2 + ./build-signed.sh + ``` + +3. **Commit changes:** + ```bash + git add . + git commit -m "chore: release v2.2.7" + ``` + +4. **Push branch:** + ```bash + git push -u origin release-2.2.7 + ``` + +5. **Create Pull Request to master** + +6. **After merge, create and push tag:** + ```bash + git tag -a v2.2.7 -m "Release v2.2.7" + git push --tags + ``` + +### CI/CD Signing + +The GitHub Actions workflow automatically signs and notarizes macOS builds when the following repository secrets are configured: + +- `APPLE_ID` - Apple developer account email +- `APPLE_ID_PASSWORD` - App-specific password for notarization +- `APPLE_TEAM_ID` - Apple developer team ID (e.g., DR57X8Z394) +- `APPLE_CERTIFICATE` - Base64-encoded Developer ID certificate +- `CERT_PW` - Certificate password + +### Local Signing (macOS) + +For local signed builds: + +```bash +cd projects/vault-v2 + +# Build and sign +./build-signed.sh + +# Notarize (requires Apple credentials) +APPLE_ID="your-email@example.com" \ +APPLE_PASSWORD="app-specific-password" \ +./notarize.sh +``` + ## Contributing See [docs/contributing.md](docs/contributing.md). @@ -42,7 +118,6 @@ See [docs/contributing.md](docs/contributing.md). ## Architecture See [docs/architecture.md](docs/architecture.md). - Bitcoin Only Stack diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..45ffeea --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,172 @@ +# Release Process + +This document describes the release process for KeepKey Bitcoin-Only Vault. + +## Quick Start + +### Creating a New Release (e.g., 2.2.6) + +```bash +# Method 1: Create release branch and update version in one command +make release-branch VERSION=2.2.6 + +# Method 2: Auto-increment version and create branch +make release-patch-branch # For patch release (2.2.5 -> 2.2.6) +make release-minor-branch # For minor release (2.2.5 -> 2.3.0) +make release-major-branch # For major release (2.2.5 -> 3.0.0) +``` + +## Release Workflow + +### 1. Create Release Branch and Update Version + +```bash +# Create release branch and update all version files +make release-branch VERSION=2.2.6 +``` + +This command will: +- Create a new branch named `release-2.2.6` +- Update version in all relevant files: + - `VERSION` + - `projects/vault-v2/package.json` + - `projects/vault-v2/src-tauri/tauri.conf.json` + - `projects/vault-v2/src-tauri/Cargo.toml` + - `projects/keepkey-rust/Cargo.toml` + - `projects/vault-v2/VERSION` + - `projects/keepkey-rust/VERSION` + - Lock files (`package-lock.json`, `Cargo.lock`) + +### 2. Review Changes + +```bash +git diff +``` + +### 3. Build and Test + +```bash +# Build the application +make vault-build + +# Run tests +make test-keepkey-rust +``` + +### 4. Commit Changes + +```bash +git add . +git commit -m "chore: release v2.2.6" +``` + +### 5. Push Release Branch + +```bash +git push -u origin release-2.2.6 +``` + +### 6. Create Pull Request + +Create a PR from `release-2.2.6` to `master` branch on GitHub. + +### 7. After PR Merge + +Once the PR is merged to master: + +```bash +# Checkout master and pull latest +git checkout master +git pull + +# Create and push tag +git tag -a v2.2.6 -m "Release v2.2.6" +git push --tags +``` + +The tag will trigger the GitHub Actions release workflow to: +- Build applications for all platforms (Windows, macOS, Linux) +- Create a GitHub release with artifacts +- Upload built binaries + +## Version Management + +### Version Sources + +The primary version source is `projects/vault-v2/package.json`. All other version files are synchronized from this source. + +### Files Updated During Release + +- `VERSION` - Root version file +- `projects/vault-v2/package.json` - Main application package +- `projects/vault-v2/src-tauri/tauri.conf.json` - Tauri configuration +- `projects/vault-v2/src-tauri/Cargo.toml` - Rust backend +- `projects/keepkey-rust/Cargo.toml` - KeepKey Rust library +- `projects/vault-v2/VERSION` - Project version file +- `projects/keepkey-rust/VERSION` - Library version file +- Lock files (`package-lock.json`, `Cargo.lock`) + +### Makefile Targets + +#### Release Branch Creation +- `make release-branch VERSION=x.y.z` - Create branch and set specific version +- `make release-patch-branch` - Auto-increment patch version and create branch +- `make release-minor-branch` - Auto-increment minor version and create branch +- `make release-major-branch` - Auto-increment major version and create branch + +#### Version Update Only +- `make release VERSION=x.y.z` - Update version without creating branch +- `make release-patch` - Increment patch version (2.2.5 -> 2.2.6) +- `make release-minor` - Increment minor version (2.2.5 -> 2.3.0) +- `make release-major` - Increment major version (2.2.5 -> 3.0.0) + +## GitHub Actions + +The project includes automated workflows: + +### Build Workflow (`.github/workflows/build.yml`) +**Triggered by:** +- Push to `main`, `master`, or `release-*` branches +- Pull requests to these branches +- Manual workflow dispatch + +**Purpose:** CI/CD builds and testing for all platforms + +### Release Workflow (`.github/workflows/release.yml`) +**Triggered by:** +- Tags starting with `v*` (e.g., `v2.2.6`) +- Manual workflow dispatch + +**Purpose:** Creates official GitHub releases with artifacts for: +- Windows (MSI installer) +- macOS (DMG installer) +- Linux (AppImage, DEB packages) + +## Best Practices + +1. **Always create a release branch** for version updates +2. **Test thoroughly** before pushing the release branch +3. **Use semantic versioning**: + - Patch (x.y.Z): Bug fixes, minor updates + - Minor (x.Y.z): New features, backward compatible + - Major (X.y.z): Breaking changes +4. **Document changes** in commit messages and PR descriptions +5. **Tag only after** PR is merged to master + +## Troubleshooting + +### Version Mismatch +If version files get out of sync: +```bash +make release VERSION= +``` + +### Failed GitHub Actions +Check the Actions tab in GitHub for build logs and errors. + +### Local Build Issues +```bash +# Clean and rebuild +make clean +make rebuild +``` \ No newline at end of file diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..23a63f5 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +2.2.8 diff --git a/audit_and_sign_releases.ps1 b/audit_and_sign_releases.ps1 new file mode 100644 index 0000000..757c89a --- /dev/null +++ b/audit_and_sign_releases.ps1 @@ -0,0 +1,239 @@ +# KeepKey Vault Release Audit and Signing Script +# Downloads releases, signs them locally with Sectigo EV cert, and re-uploads +param( + [string]$GitHubRepo = "keepkey/keepkey-bitcoin-only", # Main repo + [string]$ReleaseTag = "", # Leave empty to process latest release + [string]$GitHubToken = "", # GitHub Personal Access Token for uploads + [string]$Thumbprint = "986AEBA61CF6616393E74D8CBD3A09E836213BAA", + [string]$TimestampUrl = "http://timestamp.sectigo.com", + [switch]$AuditOnly = $false, # Only audit, don't sign or upload + [switch]$Force = $false, # Force re-signing even if already signed + [switch]$UseFork = $false # Use BitHighlander fork for testing +) + +$ErrorActionPreference = "Stop" + +# Handle fork/main repo selection +if ($UseFork) { + $GitHubRepo = "BitHighlander/keepkey-bitcoin-only" + Write-Host "πŸ”„ Auditing FORK for testing: $GitHubRepo" -ForegroundColor Yellow +} else { + Write-Host "🎯 Auditing MAIN repository: $GitHubRepo" -ForegroundColor Green +} + +Write-Host "=== KeepKey Vault Release Audit & Signing ===" -ForegroundColor Cyan +Write-Host "Repository: $GitHubRepo" -ForegroundColor Gray +Write-Host "Mode: $(if($AuditOnly) {'Audit Only'} else {'Audit + Sign + Upload'})" -ForegroundColor Gray +Write-Host "" + +# Ensure required tools are available +$requiredTools = @( + @{ Name = "git"; Command = "git --version" }, + @{ Name = "gh"; Command = "gh --version" }, + @{ Name = "curl"; Command = "curl --version" } +) + +foreach ($tool in $requiredTools) { + try { + $null = Invoke-Expression $tool.Command 2>&1 + Write-Host "βœ… $($tool.Name) is available" -ForegroundColor Green + } catch { + Write-Host "❌ $($tool.Name) not found. Please install GitHub CLI and ensure it's in PATH" -ForegroundColor Red + exit 1 + } +} +Write-Host "" + +# Get release information +if ([string]::IsNullOrEmpty($ReleaseTag)) { + Write-Host "πŸ” Getting latest release information..." -ForegroundColor Yellow + try { + $releaseJson = gh release view --repo $GitHubRepo --json tagName,name,assets,draft + $release = $releaseJson | ConvertFrom-Json + $ReleaseTag = $release.tagName + Write-Host "Found latest release: $($release.name) ($ReleaseTag)" -ForegroundColor Green + } catch { + Write-Host "❌ Failed to get release information: $_" -ForegroundColor Red + exit 1 + } +} else { + Write-Host "πŸ” Getting release information for $ReleaseTag..." -ForegroundColor Yellow + try { + $releaseJson = gh release view $ReleaseTag --repo $GitHubRepo --json tagName,name,assets,draft + $release = $releaseJson | ConvertFrom-Json + Write-Host "Found release: $($release.name)" -ForegroundColor Green + } catch { + Write-Host "❌ Release $ReleaseTag not found" -ForegroundColor Red + exit 1 + } +} + +# Create working directory +$workDir = "release_audit_$($ReleaseTag.Replace('v', '').Replace('.', '_'))" +if (Test-Path $workDir) { + Write-Host "πŸ—‚οΈ Cleaning existing work directory..." -ForegroundColor Yellow + Remove-Item $workDir -Recurse -Force +} +New-Item -ItemType Directory -Path $workDir | Out-Null +Set-Location $workDir + +Write-Host "πŸ“ Working in: $((Get-Location).Path)" -ForegroundColor Gray +Write-Host "" + +# Audit and process assets +$windowsAssets = $release.assets | Where-Object { $_.name -like "*.exe" -or $_.name -like "*.msi" } +$signatureResults = @() + +if ($windowsAssets.Count -eq 0) { + Write-Host "⚠️ No Windows executables found in this release" -ForegroundColor Yellow + exit 0 +} + +foreach ($asset in $windowsAssets) { + Write-Host "πŸ“¦ Processing: $($asset.name)" -ForegroundColor Cyan + + # Download asset + Write-Host "⬇️ Downloading..." -ForegroundColor Yellow + try { + gh release download $ReleaseTag --repo $GitHubRepo --pattern $asset.name + if (-not (Test-Path $asset.name)) { + throw "Downloaded file not found" + } + $fileSize = (Get-Item $asset.name).Length + Write-Host " Downloaded: $([math]::Round($fileSize / 1MB, 2)) MB" -ForegroundColor Gray + } catch { + Write-Host " ❌ Download failed: $_" -ForegroundColor Red + continue + } + + # Check current signature + Write-Host "πŸ” Checking signature..." -ForegroundColor Yellow + try { + $signature = Get-AuthenticodeSignature $asset.name + $isValidSig = $signature.Status -eq 'Valid' + $statusColor = if ($isValidSig) { 'Green' } else { 'Red' } + + Write-Host " Status: $($signature.Status)" -ForegroundColor $statusColor + + if ($signature.SignerCertificate) { + Write-Host " Signer: $($signature.SignerCertificate.Subject)" -ForegroundColor Gray + $isSectigo = $signature.SignerCertificate.Issuer -like "*Sectigo*" + Write-Host " Issuer: $($signature.SignerCertificate.Issuer)" -ForegroundColor Gray + Write-Host " Sectigo Cert: $(if($isSectigo) {'βœ… Yes'} else {'❌ No'})" -ForegroundColor $(if($isSectigo) {'Green'} else {'Red'}) + } else { + Write-Host " ❌ No signature found" -ForegroundColor Red + $isSectigo = $false + } + + $signatureResults += @{ + Asset = $asset.name + Status = $signature.Status + IsSectigo = $isSectigo + Signer = if($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { "None" } + NeedsResigning = -not $isValidSig -or (-not $isSectigo -and -not $AuditOnly) + } + + } catch { + Write-Host " ❌ Error checking signature: $_" -ForegroundColor Red + $signatureResults += @{ + Asset = $asset.name + Status = "Error" + IsSectigo = $false + Signer = "Error" + NeedsResigning = $true + } + } + + # Sign if needed and not audit-only + $needsSigning = ($signatureResults[-1].NeedsResigning -or $Force) -and -not $AuditOnly + + if ($needsSigning) { + Write-Host "✍️ Signing with Sectigo certificate..." -ForegroundColor Yellow + + try { + $signArgs = @( + "sign", + "/sha1", $Thumbprint, + "/t", $TimestampUrl, + "/fd", "sha256", + "/tr", $TimestampUrl, + "/td", "sha256", + "/v", + $asset.name + ) + + $signTool = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe" + if (-not (Test-Path $signTool)) { + throw "SignTool not found at $signTool" + } + + $result = & $signTool @signArgs 2>&1 + + if ($LASTEXITCODE -eq 0) { + Write-Host " βœ… Signed successfully" -ForegroundColor Green + + # Verify new signature + $newSig = Get-AuthenticodeSignature $asset.name + Write-Host " New status: $($newSig.Status)" -ForegroundColor Green + + # TODO: Re-upload to GitHub Release (requires GitHub token) + if (-not [string]::IsNullOrEmpty($GitHubToken)) { + Write-Host " ⬆️ Re-uploading to GitHub..." -ForegroundColor Yellow + # Implementation would go here using GitHub API + Write-Host " ⚠️ Re-upload not implemented yet - manual upload required" -ForegroundColor Yellow + } + + } else { + Write-Host " ❌ Signing failed: $result" -ForegroundColor Red + } + + } catch { + Write-Host " ❌ Signing error: $_" -ForegroundColor Red + } + } elseif ($AuditOnly) { + Write-Host " ℹ️ Audit mode - skipping signing" -ForegroundColor Blue + } else { + Write-Host " βœ… Already properly signed" -ForegroundColor Green + } + + Write-Host "" +} + +# Summary Report +Write-Host "πŸ“Š AUDIT SUMMARY FOR $ReleaseTag" -ForegroundColor Cyan +Write-Host "================================" -ForegroundColor Cyan +Write-Host "" + +foreach ($result in $signatureResults) { + $statusEmoji = switch ($result.Status) { + "Valid" { "βœ…" } + "NotSigned" { "❌" } + "Error" { "⚠️" } + Default { "?" } + } + + Write-Host "$statusEmoji $($result.Asset)" -ForegroundColor White + Write-Host " Status: $($result.Status)" -ForegroundColor $(if($result.Status -eq 'Valid') {'Green'} else {'Red'}) + Write-Host " Sectigo: $(if($result.IsSectigo) {'βœ… Yes'} else {'❌ No'})" -ForegroundColor $(if($result.IsSectigo) {'Green'} else {'Red'}) + Write-Host " Signer: $($result.Signer)" -ForegroundColor Gray + Write-Host "" +} + +# Recommendations +$needsAction = $signatureResults | Where-Object { $_.NeedsResigning } +if ($needsAction.Count -gt 0) { + Write-Host "🚨 ACTIONS NEEDED:" -ForegroundColor Red + Write-Host "β€’ $($needsAction.Count) file(s) need proper Sectigo signing" -ForegroundColor Yellow + if ($AuditOnly) { + Write-Host "β€’ Run again without -AuditOnly to sign files" -ForegroundColor Yellow + } + Write-Host "β€’ Consider switching to NSIS (.exe) installers for easier signing" -ForegroundColor Yellow +} else { + Write-Host "βœ… All Windows executables are properly signed with Sectigo certificates!" -ForegroundColor Green +} + +Write-Host "" +Write-Host "πŸ“ Files available in: $((Get-Location).Path)" -ForegroundColor Gray + +# Return to original directory +Set-Location ".." \ No newline at end of file diff --git a/build_and_sign.ps1 b/build_and_sign.ps1 new file mode 100644 index 0000000..0df0ea0 --- /dev/null +++ b/build_and_sign.ps1 @@ -0,0 +1,142 @@ +# Build KeepKey Vault with NSIS installer and sign it +param( + [switch]$SkipBuild = $false, + [switch]$SkipSign = $false, + [string]$BuildMode = "release" # release or debug +) + +$ErrorActionPreference = "Stop" + +Write-Host "=== KeepKey Vault Build & Sign Pipeline ===" -ForegroundColor Cyan +Write-Host "Build Mode: $BuildMode" -ForegroundColor Gray +Write-Host "Skip Build: $SkipBuild" -ForegroundColor Gray +Write-Host "Skip Sign: $SkipSign" -ForegroundColor Gray +Write-Host "" + +# Verify we're in the right directory +$requiredPath = "projects\vault-v2" +if (-not (Test-Path $requiredPath)) { + Write-Host "❌ Must run from repository root (missing $requiredPath)" -ForegroundColor Red + exit 1 +} + +# Check configuration +Write-Host "πŸ”§ Checking Tauri configuration..." -ForegroundColor Yellow +$tauriConfig = Get-Content "projects\vault-v2\src-tauri\tauri.conf.json" | ConvertFrom-Json +$isNsisConfigured = $tauriConfig.bundle.targets -contains "nsis" + +if ($isNsisConfigured) { + Write-Host "βœ… NSIS target configured" -ForegroundColor Green +} else { + Write-Host "❌ NSIS target not found in bundle.targets" -ForegroundColor Red + Write-Host "Current targets: $($tauriConfig.bundle.targets -join ', ')" -ForegroundColor Gray + exit 1 +} + +if ($tauriConfig.bundle.windows.nsis) { + Write-Host "βœ… NSIS settings configured" -ForegroundColor Green + Write-Host " Desktop shortcut: $($tauriConfig.bundle.windows.nsis.createDesktopShortcut)" -ForegroundColor Gray + Write-Host " Start menu: $($tauriConfig.bundle.windows.nsis.createStartMenuShortcut)" -ForegroundColor Gray +} else { + Write-Host "⚠️ NSIS settings not configured (will use defaults)" -ForegroundColor Yellow +} + +# Build if not skipped +if (-not $SkipBuild) { + Write-Host "" + Write-Host "πŸ”¨ Building Vault v2..." -ForegroundColor Yellow + + # Navigate to project directory + Push-Location "projects\vault-v2" + + try { + # Install dependencies + Write-Host "πŸ“¦ Installing dependencies..." -ForegroundColor Yellow + bun install + if ($LASTEXITCODE -ne 0) { throw "Dependency installation failed" } + + # Build the application + Write-Host "πŸ—οΈ Building application..." -ForegroundColor Yellow + if ($BuildMode -eq "debug") { + bun run tauri build --debug + } else { + bun run tauri build + } + + if ($LASTEXITCODE -ne 0) { throw "Build failed" } + + Write-Host "βœ… Build completed successfully" -ForegroundColor Green + + } catch { + Write-Host "❌ Build failed: $_" -ForegroundColor Red + Pop-Location + exit 1 + } finally { + Pop-Location + } +} else { + Write-Host "⏭️ Skipping build" -ForegroundColor Blue +} + +# Check for built files +Write-Host "" +Write-Host "πŸ“‹ Checking build artifacts..." -ForegroundColor Yellow + +$bundlePath = "projects\vault-v2\src-tauri\target\release\bundle" +if (-not (Test-Path $bundlePath)) { + Write-Host "❌ Bundle directory not found: $bundlePath" -ForegroundColor Red + exit 1 +} + +# List available bundle types +$bundleTypes = Get-ChildItem $bundlePath -Directory | Select-Object -ExpandProperty Name +Write-Host "Available bundle types: $($bundleTypes -join ', ')" -ForegroundColor Gray + +# Check for NSIS installer +$nsisPath = Join-Path $bundlePath "nsis" +if (Test-Path $nsisPath) { + $nsisFiles = Get-ChildItem $nsisPath -Filter "*.exe" + if ($nsisFiles.Count -gt 0) { + Write-Host "βœ… NSIS installer found:" -ForegroundColor Green + foreach ($file in $nsisFiles) { + Write-Host " πŸ“„ $($file.Name) ($([math]::Round($file.Length / 1MB, 2)) MB)" -ForegroundColor Gray + } + } else { + Write-Host "❌ No .exe files found in NSIS bundle" -ForegroundColor Red + exit 1 + } +} else { + Write-Host "❌ NSIS bundle not found at $nsisPath" -ForegroundColor Red + exit 1 +} + +# Sign the installer +if (-not $SkipSign) { + Write-Host "" + Write-Host "✍️ Signing NSIS installer..." -ForegroundColor Yellow + + try { + .\sign_nsis_setup.ps1 -BuildPath $nsisPath + if ($LASTEXITCODE -ne 0) { throw "Signing failed" } + + Write-Host "βœ… Signing completed successfully" -ForegroundColor Green + + } catch { + Write-Host "❌ Signing failed: $_" -ForegroundColor Red + Write-Host "πŸ’‘ Make sure your Sectigo certificate is installed" -ForegroundColor Yellow + exit 1 + } +} else { + Write-Host "⏭️ Skipping signing" -ForegroundColor Blue +} + +Write-Host "" +Write-Host "πŸŽ‰ Build and sign pipeline completed!" -ForegroundColor Green +Write-Host "πŸ“ Artifacts location: $bundlePath" -ForegroundColor Cyan +Write-Host "" +Write-Host "✨ Next steps:" -ForegroundColor Yellow +Write-Host " 1. Test the installer on a clean Windows system" -ForegroundColor Gray +Write-Host " 2. Verify desktop shortcut creation" -ForegroundColor Gray +Write-Host " 3. Check Start Menu entry" -ForegroundColor Gray +Write-Host " 4. Test install/uninstall process" -ForegroundColor Gray +Write-Host " 5. Upload to GitHub release if everything works" -ForegroundColor Gray \ No newline at end of file diff --git a/check_release_status.ps1 b/check_release_status.ps1 new file mode 100644 index 0000000..6bbcd24 --- /dev/null +++ b/check_release_status.ps1 @@ -0,0 +1,146 @@ +# Check Release Status and Download When Ready +# Monitors GitHub release status and downloads unsigned builds for signing + +param( + [string]$GitHubRepo = "keepkey/keepkey-bitcoin-only", # Main repo + [string]$ReleaseTag = "v2.2.7-test", + [int]$CheckIntervalSeconds = 60, + [int]$MaxWaitMinutes = 30, + [switch]$DownloadWhenReady = $true, + [switch]$UseFork = $false # Use BitHighlander fork for testing +) + +# Handle fork/main repo selection +if ($UseFork) { + $GitHubRepo = "BitHighlander/keepkey-bitcoin-only" + Write-Host "πŸ”„ Monitoring FORK for testing: $GitHubRepo" -ForegroundColor Yellow +} else { + Write-Host "🎯 Monitoring MAIN repository: $GitHubRepo" -ForegroundColor Green +} + +Write-Host "=== KeepKey Vault Release Monitor ===" -ForegroundColor Cyan +Write-Host "Repository: $GitHubRepo" -ForegroundColor Gray +Write-Host "Release Tag: $ReleaseTag" -ForegroundColor Gray +Write-Host "Check Interval: $CheckIntervalSeconds seconds" -ForegroundColor Gray +Write-Host "" + +$repoUrl = "https://github.com/$GitHubRepo" +$releaseUrl = "$repoUrl/releases/tag/$ReleaseTag" +$actionsUrl = "$repoUrl/actions" + +Write-Host "πŸ“‹ Manual Monitoring Links:" -ForegroundColor Yellow +Write-Host "πŸ”— GitHub Actions: $actionsUrl" -ForegroundColor Cyan +Write-Host "πŸ”— Release Page: $releaseUrl" -ForegroundColor Cyan +Write-Host "" + +# Check if GitHub CLI is available +try { + $null = Get-Command gh -ErrorAction Stop + $hasGhCli = $true + Write-Host "βœ… GitHub CLI found - will auto-check release status" -ForegroundColor Green +} catch { + $hasGhCli = $false + Write-Host "⚠️ GitHub CLI not found - manual monitoring only" -ForegroundColor Yellow + Write-Host "πŸ’‘ Install GitHub CLI for automated checking: https://cli.github.com/" -ForegroundColor Gray +} + +Write-Host "" + +if (-not $hasGhCli) { + Write-Host "πŸ” MANUAL STEPS TO MONITOR RELEASE:" -ForegroundColor Yellow + Write-Host "1. Open GitHub Actions: $actionsUrl" -ForegroundColor Gray + Write-Host "2. Look for 'Release KeepKey Vault v5' workflow running" -ForegroundColor Gray + Write-Host "3. Wait for all builds (Windows, macOS, Linux) to complete" -ForegroundColor Gray + Write-Host "4. Check the release page: $releaseUrl" -ForegroundColor Gray + Write-Host "5. When ready, run the download script:" -ForegroundColor Gray + Write-Host " .\download_sign_upload.ps1 -ReleaseTag $ReleaseTag" -ForegroundColor Cyan + Write-Host "" + Write-Host "🎯 Expected Artifacts:" -ForegroundColor Yellow + Write-Host " πŸ–₯️ vault-v2_2.2.7_x64-setup.exe (Windows NSIS installer)" -ForegroundColor Gray + Write-Host " 🍎 vault-v2_2.2.7.dmg (macOS universal)" -ForegroundColor Gray + Write-Host " 🐧 vault-v2_2.2.7.AppImage (Linux)" -ForegroundColor Gray + Write-Host "" + return +} + +# Automated monitoring with GitHub CLI +$maxChecks = [math]::Ceiling($MaxWaitMinutes * 60 / $CheckIntervalSeconds) +$checkCount = 0 + +Write-Host "πŸ€– Starting automated monitoring..." -ForegroundColor Green +Write-Host "Will check every $CheckIntervalSeconds seconds for up to $MaxWaitMinutes minutes" -ForegroundColor Gray +Write-Host "" + +while ($checkCount -lt $maxChecks) { + $checkCount++ + $timeRemaining = [math]::Max(0, $MaxWaitMinutes - ($checkCount * $CheckIntervalSeconds / 60)) + + Write-Host "[$($checkCount.ToString().PadLeft(2))/$maxChecks] Checking release status... (${timeRemaining:F1} min remaining)" -ForegroundColor Yellow + + try { + # Check if release exists + $releaseJson = gh release view $ReleaseTag --repo $GitHubRepo --json assets,draft,prerelease 2>$null + + if ($LASTEXITCODE -eq 0) { + $release = $releaseJson | ConvertFrom-Json + $assetCount = $release.assets.Count + $isDraft = $release.draft + $isPrerelease = $release.prerelease + + Write-Host " βœ… Release found with $assetCount assets" -ForegroundColor Green + + if ($assetCount -gt 0) { + Write-Host " πŸ“¦ Available assets:" -ForegroundColor Cyan + foreach ($asset in $release.assets) { + $sizeMB = [math]::Round($asset.size / 1MB, 1) + Write-Host " πŸ“„ $($asset.name) (${sizeMB} MB)" -ForegroundColor Gray + } + + # Check for expected Windows asset + $windowsAsset = $release.assets | Where-Object { $_.name -like "*setup.exe" -or $_.name -like "*.msi" } + $macosAsset = $release.assets | Where-Object { $_.name -like "*.dmg" } + $linuxAsset = $release.assets | Where-Object { $_.name -like "*.AppImage" } + + if ($windowsAsset -and $macosAsset -and $linuxAsset) { + Write-Host " πŸŽ‰ All expected platforms found!" -ForegroundColor Green + + if ($DownloadWhenReady) { + Write-Host "" + Write-Host "⬇️ Starting download and signing process..." -ForegroundColor Cyan + .\download_sign_upload.ps1 -ReleaseTag $ReleaseTag + return + } else { + Write-Host "" + Write-Host "βœ… Release is ready! Run this command to download and sign:" -ForegroundColor Green + Write-Host ".\download_sign_upload.ps1 -ReleaseTag $ReleaseTag" -ForegroundColor Cyan + return + } + } else { + Write-Host " ⏳ Waiting for all platforms..." -ForegroundColor Yellow + if (-not $windowsAsset) { Write-Host " ⏳ Windows build pending" -ForegroundColor Gray } + if (-not $macosAsset) { Write-Host " ⏳ macOS build pending" -ForegroundColor Gray } + if (-not $linuxAsset) { Write-Host " ⏳ Linux build pending" -ForegroundColor Gray } + } + } else { + Write-Host " ⏳ Release exists but no assets yet..." -ForegroundColor Yellow + } + } else { + Write-Host " ⏳ Release not found yet..." -ForegroundColor Yellow + } + + } catch { + Write-Host " ❌ Error checking release: $_" -ForegroundColor Red + } + + if ($checkCount -lt $maxChecks) { + Write-Host " Waiting $CheckIntervalSeconds seconds..." -ForegroundColor Gray + Start-Sleep -Seconds $CheckIntervalSeconds + } + + Write-Host "" +} + +Write-Host "⏰ Timeout reached after $MaxWaitMinutes minutes" -ForegroundColor Yellow +Write-Host "πŸ“‹ Manual check needed:" -ForegroundColor Gray +Write-Host " πŸ”— Actions: $actionsUrl" -ForegroundColor Cyan +Write-Host " πŸ”— Release: $releaseUrl" -ForegroundColor Cyan \ No newline at end of file diff --git a/docs/bootloader-update-usb-transition.md b/docs/bootloader-update-usb-transition.md new file mode 100644 index 0000000..448ad41 --- /dev/null +++ b/docs/bootloader-update-usb-transition.md @@ -0,0 +1,221 @@ +# KeepKey Bootloader Update and USB Protocol Transition Guide + +## Overview + +This document describes the critical issues and solutions for handling KeepKey device bootloader updates, particularly the transition from legacy HID-based bootloaders to modern WebUSB-enabled bootloaders. These insights are crucial for any vault implementation handling device onboarding and updates. + +## Problem Statement + +When updating a KeepKey device from bootloader v1.x to v2.x, several critical issues occur: + +1. **USB Protocol Change**: The device changes its Product ID (PID) from `0x0001` (HID) to `0x0002` (WebUSB/USB) +2. **Transport Incompatibility**: The device disconnects and reconnects with a different USB transport type +3. **Device Identity Loss**: The system loses track of the device due to the PID change +4. **Data Corruption**: Devices may have corrupted policy data after the update + +## USB Protocol Details + +### Product ID Mapping + +| Bootloader Version | Product ID | Transport Type | Capabilities | +|-------------------|------------|----------------|--------------| +| v1.x (Legacy) | `0x0001` | HID Only | Limited API, no debug interface | +| v2.x (Modern) | `0x0002` | WebUSB/USB | Full API, debug interface, bulk transfers | + +### Transport Detection Logic + +```rust +// Legacy devices (PID 0x0001) must use HID on all platforms +if device_info.pid == 0x0001 { + return TransportType::HidOnly; +} + +// Modern devices (PID 0x0002) use WebUSB/USB transport +if device_info.pid == 0x0002 { + return TransportType::TraditionalUsb; +} +``` + +## Implementation Solutions + +### 1. Handling PID Transition During Bootloader Update + +**Location**: `device_queue.rs::handle_update_bootloader` + +**Key Changes**: +- Detect when updating from old bootloader (PID `0x0001`) +- After successful update, automatically update device info to expect PID `0x0002` +- Clear transport to force recreation with new protocol + +```rust +// Remember if we started with PID 0x0001 (old bootloader) +let started_with_old_bootloader = self.device_info.pid == 0x0001; + +// After successful bootloader update +if started_with_old_bootloader { + info!("πŸ“ Device will reconnect with PID 0x0002 after bootloader update"); + self.device_info.pid = 0x0002; + self.transport = None; // Force transport recreation +} +``` + +### 2. Dynamic Device Reconnection Detection + +**Location**: `device_queue.rs::ensure_transport` + +**Key Changes**: +- If transport creation fails with expected PID, search for device with same serial but different PID +- Automatically update device info when PID change is detected +- Retry transport creation with updated information + +```rust +// If failed and PID is 0x0002, try looking for device with same serial +if transport_result.is_err() && self.device_info.pid == 0x0002 { + // Search for device by serial number, ignoring PID + let devices = rusb::devices().unwrap(); + for device in devices.iter() { + if let Ok(desc) = device.device_descriptor() { + if desc.vendor_id() == self.device_info.vid { + // Check serial number match + if device_serial == expected_serial && desc.product_id() != self.device_info.pid { + // Update PID and retry + self.device_info.pid = desc.product_id(); + transport_result = create_transport_for_device(&self.device_info); + } + } + } + } +} +``` + +### 3. Flexible Device Discovery + +**Location**: `device_queue.rs::find_physical_device_by_info` + +**Key Changes**: +- Prioritize serial number matching over PID matching +- Log PID changes for debugging +- Fall back to any KeepKey device if exact match fails + +```rust +// Match by serial number (flexible - allows PID change after bootloader update) +if desc.vendor_id() == device_info.vid { + if device_serial == expected_serial { + // Log if PID changed (happens after bootloader update) + if desc.product_id() != device_info.pid { + info!("πŸ“ Device reconnected with different PID: 0x{:04x} -> 0x{:04x}", + device_info.pid, desc.product_id()); + } + return Ok(device.clone()); + } +} +``` + +### 4. Handling Corrupted Policy Data + +**Location**: `device_queue.rs::handle_get_features` + +**Problem**: After bootloader update, devices may have corrupted policy data causing protobuf decode errors. + +**Solution**: +- Detect "invalid UTF-8" errors in policy fields +- Automatically attempt device wipe to clear corruption +- Provide clear user feedback if manual confirmation needed + +```rust +if error_str.contains("invalid string value: data is not UTF-8 encoded") || + error_str.contains("PolicyType.policy_name") { + warn!("⚠️ Device appears to have corrupted policy data after bootloader update"); + + // Try to wipe the device to clear corrupted data + let wipe_result = transport.handle(WipeDevice {}.into()); + + match wipe_result { + Ok(Message::Success(_)) => { + // Retry GetFeatures after successful wipe + transport.handle(GetFeatures {}.into())? + } + Ok(Message::ButtonRequest(_)) => { + return Err(anyhow!("Device has corrupted data and needs to be wiped. + Please confirm the wipe on your device.")); + } + _ => return Err(e); + } +} +``` + +## Critical Implementation Considerations + +### 1. Device State After Bootloader Update + +After a bootloader update from v1.x to v2.x: +- Device will disconnect and reconnect with different PID +- Device may be in bootloader mode requiring firmware update +- Device may have corrupted or uninitialized storage +- Transport type changes from HID to WebUSB/USB + +### 2. Timing and Retry Logic + +- Allow longer timeouts after bootloader updates (2-5 seconds) +- Implement exponential backoff for reconnection attempts +- Clear transport and force recreation after updates + +### 3. Error Recovery Strategies + +1. **Transport Creation Failure**: Search for device with different PID +2. **Protobuf Decode Errors**: Attempt automatic device wipe +3. **Communication Timeouts**: Wait for device reboot and retry +4. **Device Not Found**: Check all PIDs for matching serial number + +## Testing Scenarios + +### Scenario 1: Legacy to Modern Bootloader Update +1. Start with device on bootloader v1.0.3 (PID `0x0001`) +2. Perform bootloader update to v2.1.4 +3. Verify device reconnects with PID `0x0002` +4. Confirm transport switches from HID to WebUSB/USB + +### Scenario 2: Corrupted Device Recovery +1. Device with corrupted policy data after update +2. System detects protobuf decode error +3. Automatic wipe initiated +4. Device successfully initialized after wipe + +### Scenario 3: Multiple Device Handling +1. Multiple KeepKey devices connected +2. Update one device's bootloader +3. Verify correct device tracking through PID change +4. Ensure other devices remain unaffected + +## Common Issues and Solutions + +| Issue | Symptom | Solution | +|-------|---------|----------| +| Device lost after update | "Device not found" errors | Search by serial number, ignore PID | +| HID read timeout | Communication timeouts after update | Device switched to WebUSB, update transport | +| Protobuf decode error | "invalid UTF-8" in policies | Device needs wipe and re-initialization | +| Wrong transport type | Connection failures | Detect PID and choose appropriate transport | + +## Code Locations + +All critical changes are in: `/projects/keepkey-bitcoin-only/projects/keepkey-rust/device_queue.rs` + +- **Lines 553-631**: `handle_update_bootloader` - PID transition handling +- **Lines 291-371**: `ensure_transport` - Dynamic reconnection logic +- **Lines 375-421**: `handle_get_features` - Corruption detection and recovery +- **Lines 990-1028**: `find_physical_device_by_info` - Flexible device discovery + +## Recommendations for Future Vault Versions + +1. **Pre-Update Detection**: Check device PID before starting updates +2. **User Communication**: Inform users about expected disconnection/reconnection +3. **Progress Tracking**: Maintain update state across disconnections +4. **Automatic Recovery**: Implement all recovery mechanisms from the start +5. **Testing**: Always test with actual hardware transitions, not just emulators + +## References + +- [USB Rules Documentation](./usb/docs/usb-rules.md) +- [WebUSB Requirements](./usb/docs/keepkey-webusb-requirements.md) +- [Transport History](./keepkey/transport_history.md) +- [Device Controller Implementation](./usb/docs/device_controller_implementation.md) \ No newline at end of file diff --git a/docs/vault-onboarding-implementation-notes.md b/docs/vault-onboarding-implementation-notes.md new file mode 100644 index 0000000..4726abd --- /dev/null +++ b/docs/vault-onboarding-implementation-notes.md @@ -0,0 +1,205 @@ +# Vault Onboarding Implementation Notes + +## Executive Summary + +This document captures critical implementation details discovered while fixing bootloader update and device recovery issues in vault-v2. These findings are essential for implementing robust device onboarding in future vault versions. + +## Key Discoveries + +### 1. USB Protocol Transition is Critical + +**Finding**: KeepKey devices undergo a fundamental USB protocol change when updating from legacy bootloaders (v1.x) to modern bootloaders (v2.x). + +**Impact**: +- Device identity changes (PID 0x0001 β†’ 0x0002) +- Transport layer must switch (HID β†’ WebUSB/USB) +- Existing connections become invalid + +**Implementation Requirements**: +- Device tracking must use serial numbers, not PID +- Transport creation must be dynamic and adaptable +- Reconnection logic must handle protocol changes + +### 2. Device State Corruption is Common + +**Finding**: Devices frequently have corrupted policy data after bootloader updates. + +**Symptoms**: +``` +failed to decode Protobuf message: PolicyType.policy_name: Features.policies: +invalid string value: data is not UTF-8 encoded +``` + +**Solution**: Automatic device wipe and re-initialization must be part of the update flow. + +### 3. Transport Detection Must Be Flexible + +**Current Implementation Gaps**: +- Hard-coded PID expectations +- Rigid transport type assumptions +- No fallback mechanisms + +**Required Flexibility**: +```rust +// Don't assume PID remains constant +// Don't assume transport type +// Always have fallback strategies +``` + +## Onboarding Flow Requirements + +### Phase 1: Device Detection +1. Scan for all KeepKey devices (VID 0x2b24) +2. Don't filter by PID initially +3. Determine device state (bootloader mode, firmware version, initialized) +4. Track devices by serial number + +### Phase 2: Bootloader Update +1. Detect legacy bootloader (v1.x with PID 0x0001) +2. Prepare for USB protocol change +3. Execute update +4. Handle disconnection gracefully +5. Wait for reconnection with new PID +6. Update internal device tracking + +### Phase 3: Firmware Update +1. Ensure device in bootloader mode +2. Handle potential corruption from previous state +3. Upload firmware +4. Wait for device restart +5. Verify successful update + +### Phase 4: Device Initialization +1. Check for corrupted data (protobuf errors) +2. Wipe if necessary +3. Initialize device +4. Set up PIN +5. Generate or restore seed + +## Critical Code Patterns + +### Pattern 1: Flexible Device Discovery +```rust +// Good: Find by serial, adapt to PID changes +fn find_device(serial: &str) -> Result { + for device in list_all_devices() { + if device.serial == serial { + // Found it, even if PID changed + return Ok(device); + } + } +} + +// Bad: Rigid PID expectations +fn find_device(pid: u16) -> Result { + // Will fail after bootloader update +} +``` + +### Pattern 2: Transport Adaptation +```rust +// Good: Detect and adapt to transport type +fn create_transport(device: &Device) -> Result { + match device.pid { + 0x0001 => create_hid_transport(), + 0x0002 => create_webusb_transport(), + _ => detect_and_create_transport() + } +} + +// Bad: Hard-coded transport assumptions +fn create_transport() -> HidTransport { + // Fails for modern devices +} +``` + +### Pattern 3: Error Recovery +```rust +// Good: Detect corruption and recover +match get_features() { + Err(e) if e.contains("invalid UTF-8") => { + wipe_device()?; + get_features() + } + result => result +} + +// Bad: Propagate errors without recovery +get_features()? // User stuck with corrupted device +``` + +## Testing Checklist + +### Hardware Scenarios +- [ ] Legacy bootloader (v1.0.3) to modern (v2.1.4) update +- [ ] Device with corrupted policy data +- [ ] Multiple devices connected simultaneously +- [ ] Device disconnection during update +- [ ] Power loss during update + +### Software Scenarios +- [ ] Transport type detection +- [ ] PID change handling +- [ ] Serial number tracking +- [ ] Error recovery mechanisms +- [ ] Timeout and retry logic + +## Common Pitfalls to Avoid + +1. **Assuming Static Device Properties** + - PIDs change during updates + - Transport types vary by bootloader version + - Device paths change on reconnection + +2. **Insufficient Error Handling** + - Protobuf decode errors are common + - Devices often need wiping after updates + - Communication timeouts are expected + +3. **Rigid Transport Layer** + - Must support HID, WebUSB, and USB + - Must handle transitions between types + - Must have fallback mechanisms + +4. **Poor User Communication** + - Users need to know when to wait + - Users need to know when to act (button presses) + - Users need clear error messages + +## Recommendations for Future Implementations + +### Architecture +1. Implement device tracking by serial number from the start +2. Design transport layer to be protocol-agnostic +3. Build in automatic recovery mechanisms +4. Separate device identity from USB properties + +### User Experience +1. Show clear progress during updates +2. Explain disconnections are expected +3. Provide recovery instructions for failures +4. Auto-detect and fix common issues + +### Code Organization +1. Centralize USB protocol knowledge +2. Abstract transport differences +3. Implement comprehensive logging +4. Create reusable recovery utilities + +## File References + +### Modified Files (vault-v2) +- `/projects/keepkey-rust/device_queue.rs` - Core fixes for USB transitions + +### Documentation Created +- `/docs/bootloader-update-usb-transition.md` - Technical details +- `/docs/vault-onboarding-implementation-notes.md` - This document + +### Existing Documentation +- `/docs/usb/docs/usb-rules.md` - USB device rules +- `/docs/usb/docs/keepkey-webusb-requirements.md` - WebUSB requirements +- `/docs/keepkey/transport_history.md` - Transport evolution + +## Conclusion + +Successful vault onboarding requires understanding and handling the complex USB protocol transitions that occur during device updates. The key is flexibility - don't assume device properties remain static, always have fallback mechanisms, and implement automatic recovery for common issues. These lessons learned from vault-v2 should be applied to all future vault implementations to ensure robust device onboarding. \ No newline at end of file diff --git a/download_sign_upload.ps1 b/download_sign_upload.ps1 new file mode 100644 index 0000000..97f0688 --- /dev/null +++ b/download_sign_upload.ps1 @@ -0,0 +1,214 @@ +# Download, Sign, and Prepare for Upload Script +# Downloads unsigned release from GitHub, signs with Sectigo, prepares for manual upload + +param( + [string]$GitHubRepo = "keepkey/keepkey-bitcoin-only", # Main repo + [string]$ReleaseTag = "", # Leave empty for latest release + [string]$Thumbprint = "986AEBA61CF6616393E74D8CBD3A09E836213BAA", + [string]$TimestampUrl = "http://timestamp.sectigo.com", + [string]$OutputDir = "signed_release", + [switch]$DownloadOnly = $false, # Only download, don't sign + [switch]$CleanStart = $false, # Remove existing output directory + [switch]$UseFork = $false # Use BitHighlander fork for testing +) + +$ErrorActionPreference = "Stop" + +# Handle fork/main repo selection +if ($UseFork) { + $GitHubRepo = "BitHighlander/keepkey-bitcoin-only" + Write-Host "πŸ”„ Using FORK for testing: $GitHubRepo" -ForegroundColor Yellow +} else { + Write-Host "🎯 Using MAIN repository: $GitHubRepo" -ForegroundColor Green +} + +Write-Host "=== KeepKey Vault Release Download & Sign ===" -ForegroundColor Cyan +Write-Host "Repository: $GitHubRepo" -ForegroundColor Gray +Write-Host "Output Directory: $OutputDir" -ForegroundColor Gray +Write-Host "" + +# Clean start if requested +if ($CleanStart -and (Test-Path $OutputDir)) { + Write-Host "πŸ—‘οΈ Cleaning existing output directory..." -ForegroundColor Yellow + Remove-Item $OutputDir -Recurse -Force +} + +# Create output directory +if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir | Out-Null +} + +# Get release information +if ([string]::IsNullOrEmpty($ReleaseTag)) { + Write-Host "πŸ” Getting latest release information..." -ForegroundColor Yellow + try { + $releaseJson = gh release view --repo $GitHubRepo --json tagName,name,assets,url + $release = $releaseJson | ConvertFrom-Json + $ReleaseTag = $release.tagName + Write-Host "Found latest release: $($release.name) ($ReleaseTag)" -ForegroundColor Green + Write-Host "Release URL: $($release.url)" -ForegroundColor Gray + } catch { + Write-Host "❌ Failed to get release information: $_" -ForegroundColor Red + Write-Host "πŸ’‘ Make sure GitHub CLI is installed and authenticated" -ForegroundColor Yellow + exit 1 + } +} else { + Write-Host "πŸ” Getting release information for $ReleaseTag..." -ForegroundColor Yellow + try { + $releaseJson = gh release view $ReleaseTag --repo $GitHubRepo --json tagName,name,assets,url + $release = $releaseJson | ConvertFrom-Json + Write-Host "Found release: $($release.name)" -ForegroundColor Green + Write-Host "Release URL: $($release.url)" -ForegroundColor Gray + } catch { + Write-Host "❌ Release $ReleaseTag not found" -ForegroundColor Red + exit 1 + } +} + +Write-Host "" + +# Set working directory +Push-Location $OutputDir + +try { + # Download all assets + Write-Host "πŸ“₯ Downloading release assets..." -ForegroundColor Yellow + try { + gh release download $ReleaseTag --repo $GitHubRepo + if ($LASTEXITCODE -ne 0) { throw "GitHub CLI download failed" } + Write-Host "βœ… Assets downloaded successfully" -ForegroundColor Green + } catch { + Write-Host "❌ Download failed: $_" -ForegroundColor Red + Pop-Location + exit 1 + } + + Write-Host "" + + # List downloaded files + Write-Host "πŸ“‹ Downloaded assets:" -ForegroundColor Cyan + $allFiles = Get-ChildItem -File | Sort-Object Name + foreach ($file in $allFiles) { + $size = [math]::Round($file.Length / 1MB, 2) + $icon = switch -Wildcard ($file.Name) { + "*.exe" { "πŸ–₯️" } + "*.msi" { "πŸ–₯️" } + "*.dmg" { "🍎" } + "*.deb" { "🐧" } + "*.AppImage" { "🐧" } + "*.tar.gz" { "πŸ“¦" } + "*.zip" { "πŸ“¦" } + Default { "πŸ“„" } + } + Write-Host " $icon $($file.Name) ($size MB)" -ForegroundColor Gray + } + Write-Host "" + + if ($DownloadOnly) { + Write-Host "πŸ“ Download complete! Files are in: $((Get-Location).Path)" -ForegroundColor Green + Pop-Location + return + } + + # Find Windows executables to sign + $windowsFiles = Get-ChildItem -File | Where-Object { + $_.Name -like "*.exe" -or $_.Name -like "*.msi" + } + + if ($windowsFiles.Count -eq 0) { + Write-Host "⚠️ No Windows executables found to sign" -ForegroundColor Yellow + Write-Host "πŸ“ Files are in: $((Get-Location).Path)" -ForegroundColor Gray + Pop-Location + return + } + + Write-Host "✍️ Signing Windows executables..." -ForegroundColor Yellow + $signTool = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe" + + if (-not (Test-Path $signTool)) { + Write-Host "❌ SignTool not found at: $signTool" -ForegroundColor Red + Write-Host "πŸ’‘ Install Windows SDK to get SignTool" -ForegroundColor Yellow + Pop-Location + exit 1 + } + + $signedCount = 0 + foreach ($file in $windowsFiles) { + Write-Host "" + Write-Host "πŸ“ Signing: $($file.Name)" -ForegroundColor Cyan + + # Check current signature + try { + $currentSig = Get-AuthenticodeSignature $file.FullName + $isAlreadySigned = $currentSig.Status -eq 'Valid' + + if ($isAlreadySigned) { + Write-Host " βœ… Already signed by: $($currentSig.SignerCertificate.Subject)" -ForegroundColor Green + $signedCount++ + continue + } else { + Write-Host " ⚠️ Current status: $($currentSig.Status)" -ForegroundColor Yellow + } + } catch { + Write-Host " ⚠️ Could not check current signature" -ForegroundColor Yellow + } + + # Sign the file + try { + $signArgs = @( + "sign", + "/sha1", $Thumbprint, + "/fd", "sha256", + "/tr", $TimestampUrl, + "/td", "sha256", + "/v", + $file.FullName + ) + + $result = & $signTool @signArgs 2>&1 + + if ($LASTEXITCODE -eq 0) { + Write-Host " βœ… Signed successfully" -ForegroundColor Green + $signedCount++ + + # Verify signature + $newSig = Get-AuthenticodeSignature $file.FullName + Write-Host " Status: $($newSig.Status)" -ForegroundColor Green + } else { + Write-Host " ❌ Signing failed: $result" -ForegroundColor Red + } + + } catch { + Write-Host " ❌ Signing error: $_" -ForegroundColor Red + } + } + + Write-Host "" + + # Create summary + Write-Host "πŸ“Š SIGNING SUMMARY" -ForegroundColor Cyan + Write-Host "=================" -ForegroundColor Cyan + Write-Host "" + Write-Host "βœ… Successfully signed: $signedCount of $($windowsFiles.Count) Windows files" -ForegroundColor Green + Write-Host "πŸ“ Signed files location: $((Get-Location).Path)" -ForegroundColor Gray + Write-Host "" + + # Instructions for manual upload + Write-Host "πŸ“‹ NEXT STEPS FOR MANUAL UPLOAD:" -ForegroundColor Yellow + Write-Host "1. Go to the GitHub release: $($release.url)" -ForegroundColor Gray + Write-Host "2. Click 'Edit release'" -ForegroundColor Gray + Write-Host "3. Delete the unsigned Windows files" -ForegroundColor Gray + Write-Host "4. Upload the signed files from this directory:" -ForegroundColor Gray + + foreach ($file in $windowsFiles) { + Write-Host " πŸ“Ž $($file.Name)" -ForegroundColor Cyan + } + + Write-Host "5. Update release notes to indicate signed status" -ForegroundColor Gray + Write-Host "6. Save and publish the release" -ForegroundColor Gray + Write-Host "" + Write-Host "πŸŽ‰ Ready for manual upload!" -ForegroundColor Green + +} finally { + Pop-Location +} \ No newline at end of file diff --git a/examples.md b/examples.md new file mode 100644 index 0000000..02459e0 --- /dev/null +++ b/examples.md @@ -0,0 +1,85 @@ +# KeepKey Vault Signing Workflow Examples + +## Overview + +This repository supports building and signing releases for both: +- **Main Repository**: `keepkey/keepkey-bitcoin-only` (production releases) +- **Fork Repository**: `BitHighlander/keepkey-bitcoin-only` (testing) + +## Quick Examples + +### Testing on Fork +```powershell +# Test build and sign on the fork +.\download_sign_upload.ps1 -UseFork -ReleaseTag "v2.2.7-test" + +# Monitor fork releases +.\check_release_status.ps1 -UseFork -ReleaseTag "v2.2.7-test" + +# Audit fork releases +.\audit_and_sign_releases.ps1 -UseFork -AuditOnly +``` + +### Production on Main Repo +```powershell +# Build and sign main repo release +.\download_sign_upload.ps1 -ReleaseTag "v2.2.7" + +# Monitor main repo releases +.\check_release_status.ps1 -ReleaseTag "v2.2.7" + +# Audit main repo releases +.\audit_and_sign_releases.ps1 -AuditOnly +``` + +## Typical Workflow + +### 1. Test on Fork First +```powershell +# 1. Push test release to fork +git push origin release-2.2.7 +git tag v2.2.7-test +git push origin v2.2.7-test + +# 2. Monitor and sign fork release +.\check_release_status.ps1 -UseFork -ReleaseTag "v2.2.7-test" +``` + +### 2. Deploy to Main Repo +```powershell +# 1. Push to main repository +git push upstream release-2.2.7 +git tag v2.2.7 +git push upstream v2.2.7 + +# 2. Monitor and sign main release +.\check_release_status.ps1 -ReleaseTag "v2.2.7" +``` + +## All Script Options + +### download_sign_upload.ps1 +- `-UseFork`: Use BitHighlander fork instead of main repo +- `-ReleaseTag`: Specific release (default: latest) +- `-DownloadOnly`: Only download, don't sign +- `-CleanStart`: Remove existing output directory + +### check_release_status.ps1 +- `-UseFork`: Monitor fork instead of main repo +- `-ReleaseTag`: Release to monitor +- `-CheckIntervalSeconds`: How often to check (default: 60) +- `-MaxWaitMinutes`: Maximum wait time (default: 30) + +### audit_and_sign_releases.ps1 +- `-UseFork`: Audit fork instead of main repo +- `-AuditOnly`: Only audit, don't sign +- `-Force`: Force re-signing of already signed files + +## Repository Structure + +``` +origin -> BitHighlander/keepkey-bitcoin-only (fork) +upstream -> keepkey/keepkey-bitcoin-only (main) +``` + +Push test releases to `origin`, production releases to `upstream`. \ No newline at end of file diff --git a/projects/keepkey-rust/Cargo.toml b/projects/keepkey-rust/Cargo.toml index d82f939..53acc2a 100644 --- a/projects/keepkey-rust/Cargo.toml +++ b/projects/keepkey-rust/Cargo.toml @@ -4,7 +4,7 @@ name = "keepkey_rust" [lib] name = "keepkey_rust" path = "core_lib.rs" -version = "0.1.0" +version = "2.2.7" edition = "2021" license = "MIT OR Apache-2.0" description = "Headless multi-device queue and transport layer for KeepKey hardware wallets." diff --git a/projects/keepkey-rust/VERSION b/projects/keepkey-rust/VERSION new file mode 100644 index 0000000..5bc1cc4 --- /dev/null +++ b/projects/keepkey-rust/VERSION @@ -0,0 +1 @@ +2.2.7 diff --git a/projects/keepkey-rust/device_controller.rs b/projects/keepkey-rust/device_controller.rs index 2c0bb98..399aceb 100644 --- a/projects/keepkey-rust/device_controller.rs +++ b/projects/keepkey-rust/device_controller.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; use std::time::{Duration, Instant}; -use tokio::sync::{mpsc, broadcast}; +use tokio::sync::{mpsc, broadcast, oneshot}; use tokio::time::interval; use tauri::{AppHandle, Emitter}; use crate::features::{DeviceFeatures, get_device_features_with_fallback}; @@ -46,6 +46,8 @@ pub struct DeviceController { retry_counts: Arc>>, /// Devices currently being processed (to prevent overlapping attempts) active_fetches: Arc>>, + /// Cancellation tokens for active feature fetch tasks + fetch_cancellation_tokens: Arc>>>, /// Blocking actions state pub blocking_actions: BlockingActionsState, /// App handle for emitting events @@ -73,6 +75,7 @@ impl DeviceController { pending_features: Arc::new(RwLock::new(HashMap::new())), retry_counts: Arc::new(RwLock::new(HashMap::new())), active_fetches: Arc::new(RwLock::new(HashMap::new())), + fetch_cancellation_tokens: Arc::new(RwLock::new(HashMap::new())), blocking_actions, app_handle, }; @@ -93,6 +96,7 @@ impl DeviceController { let pending_features = Arc::clone(&self.pending_features); let retry_counts = Arc::clone(&self.retry_counts); let active_fetches = Arc::clone(&self.active_fetches); + let fetch_cancellation_tokens = Arc::clone(&self.fetch_cancellation_tokens); let event_tx = self.event_tx.clone(); tokio::spawn(async move { @@ -113,6 +117,20 @@ impl DeviceController { }; for device_id in devices_to_update { + // Check if device is still connected before attempting to fetch features + { + let registry_check = crate::device_registry::get_all_device_entries(); + if let Ok(entries) = registry_check { + if !entries.iter().any(|e| e.device.unique_id == device_id) { + log::debug!("Device {} no longer connected, removing from pending", device_id); + pending_features.write().unwrap().remove(&device_id); + retry_counts.write().unwrap().remove(&device_id); + active_fetches.write().unwrap().remove(&device_id); + continue; + } + } + } + // Check if device is already being processed { let active = active_fetches.read().unwrap(); @@ -181,6 +199,11 @@ impl DeviceController { let pending_features_clone = Arc::clone(&pending_features); let retry_counts_clone = Arc::clone(&retry_counts); let active_fetches_clone = Arc::clone(&active_fetches); + let fetch_cancellation_tokens_clone = Arc::clone(&fetch_cancellation_tokens); + + // Create cancellation token for this fetch + let (cancel_tx, cancel_rx) = oneshot::channel::<()>(); + fetch_cancellation_tokens.write().unwrap().insert(device_id.clone(), cancel_tx); // Mark device as actively being processed active_fetches.write().unwrap().insert(device_id.clone(), Instant::now()); @@ -192,13 +215,24 @@ impl DeviceController { // Update last attempt time pending_features_clone.write().unwrap().insert(device_id_clone.clone(), Instant::now()); - // Use tokio::task::spawn_blocking for the USB operation - let result = tokio::time::timeout( - FEATURE_FETCH_TIMEOUT, - tokio::task::spawn_blocking(move || { - get_device_features_with_fallback(&device) - }) - ).await; + // Use tokio::task::spawn_blocking for the USB operation with cancellation support + let result = tokio::select! { + _ = cancel_rx => { + log::info!("Feature fetch for device {} was cancelled", device_id_clone); + // Clean up and exit + pending_features_clone.write().unwrap().remove(&device_id_clone); + retry_counts_clone.write().unwrap().remove(&device_id_clone); + active_fetches_clone.write().unwrap().remove(&device_id_clone); + fetch_cancellation_tokens_clone.write().unwrap().remove(&device_id_clone); + return; + } + result = tokio::time::timeout( + FEATURE_FETCH_TIMEOUT, + tokio::task::spawn_blocking(move || { + get_device_features_with_fallback(&device) + }) + ) => result + }; match result { Ok(Ok(Ok(features))) => { @@ -241,8 +275,9 @@ impl DeviceController { status, }); - // Clean up active fetch + // Clean up active fetch and cancellation token active_fetches_clone.write().unwrap().remove(&device_id_clone); + fetch_cancellation_tokens_clone.write().unwrap().remove(&device_id_clone); } Ok(Ok(Err(e))) => { log::warn!("Failed to fetch features for device {}: {}", device_id_clone, e); @@ -253,15 +288,17 @@ impl DeviceController { error: e.to_string(), }); DeviceController::emit_status_message(&event_tx_clone, Some(device_id_clone.clone()), "Please reconnect your keepkey"); - // Clean up active fetch + // Clean up active fetch and cancellation token active_fetches_clone.write().unwrap().remove(&device_id_clone); + fetch_cancellation_tokens_clone.write().unwrap().remove(&device_id_clone); } Ok(Err(_)) => { log::error!("Task panicked while fetching features for device {}", device_id_clone); retry_counts_clone.write().unwrap().insert(device_id_clone.clone(), retry_count + 1); - // Clean up active fetch + // Clean up active fetch and cancellation token active_fetches_clone.write().unwrap().remove(&device_id_clone); + fetch_cancellation_tokens_clone.write().unwrap().remove(&device_id_clone); } Err(_) => { log::warn!("Timeout fetching features for device {}", device_id_clone); @@ -272,8 +309,9 @@ impl DeviceController { error: "Operation timed out".to_string(), }); DeviceController::emit_status_message(&event_tx_clone, Some(device_id_clone.clone()), "Please reconnect your keepkey"); - // Clean up active fetch + // Clean up active fetch and cancellation token active_fetches_clone.write().unwrap().remove(&device_id_clone); + fetch_cancellation_tokens_clone.write().unwrap().remove(&device_id_clone); } } }); @@ -335,13 +373,21 @@ impl DeviceController { } } + // Cancel any active feature fetch for this device + if let Some(cancel_tx) = self.fetch_cancellation_tokens.write().unwrap().remove(&device.unique_id) { + let _ = cancel_tx.send(()); + log::info!("Cancelled active feature fetch for disconnected device {}", device.unique_id); + } + // Remove from pending and active self.pending_features.write().unwrap().remove(&device.unique_id); self.retry_counts.write().unwrap().remove(&device.unique_id); self.active_fetches.write().unwrap().remove(&device.unique_id); // Emit event - let _ = self.event_tx.send(DeviceControllerEvent::DeviceDisconnected(device.unique_id)); + let _ = self.event_tx.send(DeviceControllerEvent::DeviceDisconnected(device.unique_id.clone())); + + log::info!("♻️ Cleaned up device state for disconnected device: {}", device.unique_id); } } diff --git a/projects/keepkey-rust/device_queue.rs b/projects/keepkey-rust/device_queue.rs index 78410e5..c7a0e7c 100644 --- a/projects/keepkey-rust/device_queue.rs +++ b/projects/keepkey-rust/device_queue.rs @@ -292,7 +292,56 @@ impl DeviceWorker { loop { if self.transport.is_none() { info!("πŸ”— Attempting to create transport for device {}", self.device_id); - match DeviceQueueFactory::create_transport_for_device(&self.device_info) { + + // Try to create transport with current device info + let mut transport_result = DeviceQueueFactory::create_transport_for_device(&self.device_info); + + // If failed and PID is 0x0002, try looking for a device with same serial but different PID + // This handles the case where device reconnected after bootloader update + if transport_result.is_err() && self.device_info.pid == 0x0002 { + info!("πŸ” Device with PID 0x0002 not found, checking if device reconnected with different PID..."); + + // Try to find the device with same serial number but possibly different PID + // We need to check physical USB devices directly + let usb_devices = rusb::devices().unwrap_or_else(|_| rusb::DeviceList::new().unwrap()); + let mut found_reconnected = false; + + for device in usb_devices.iter() { + if let Ok(desc) = device.device_descriptor() { + // Check if it's a KeepKey device (VID 0x2b24) + if desc.vendor_id() == self.device_info.vid { + // Try to read serial number + if let Ok(handle) = device.open() { + let timeout = std::time::Duration::from_millis(100); + if let Ok(langs) = handle.read_languages(timeout) { + if let Some(lang) = langs.first() { + if let Ok(device_serial) = handle.read_serial_number_string(*lang, &desc, timeout) { + // Check if serial matches + if let Some(expected_serial) = &self.device_info.serial_number { + if device_serial == *expected_serial && desc.product_id() != self.device_info.pid { + info!("πŸ”„ Device reconnected with different PID: 0x{:04x} -> 0x{:04x}", + self.device_info.pid, desc.product_id()); + info!("πŸ“ Updating device info with new PID for {}", self.device_id); + self.device_info.pid = desc.product_id(); + found_reconnected = true; + break; + } + } + } + } + } + } + } + } + } + + if found_reconnected { + // Try again with updated device info + transport_result = DeviceQueueFactory::create_transport_for_device(&self.device_info); + } + } + + match transport_result { Ok(transport) => { self.transport = Some(transport); info!("βœ… Transport ready for {}", self.device_id); @@ -511,6 +560,9 @@ impl DeviceWorker { self.cache.clear(); info!("🧹 Cache cleared for bootloader update"); + // Remember if we started with PID 0x0001 (old bootloader) + let started_with_old_bootloader = self.device_info.pid == 0x0001; + // Get transport let transport = self.ensure_transport().await?; let mut handler = transport.with_standard_handler(); @@ -538,13 +590,29 @@ impl DeviceWorker { info!("πŸ“€ Sending FirmwareUpload command..."); let payload_hash = Sha256::digest(&bootloader_bytes).to_vec(); - match handler.handle(FirmwareUpload { + let result = handler.handle(FirmwareUpload { payload_hash, payload: bootloader_bytes, - }.into()) { + }.into()); + + // Clear transport after upload completes (device will disconnect) + drop(handler); + self.transport = None; + + match result { Ok(Message::Success(s)) => { info!("βœ… Bootloader update successful: {}", s.message()); info!("πŸ”„ Device may reboot. Please wait a moment."); + + // IMPORTANT: After bootloader update, the device will reconnect with a different PID + // Old bootloaders (v1.x) use PID 0x0001, new bootloaders (v2.x) use PID 0x0002 + if started_with_old_bootloader { + info!("πŸ“ Device will reconnect with PID 0x0002 after bootloader update from v1.x to v2.x"); + info!("πŸ“ Updating device info to expect PID 0x0002 for reconnection"); + self.device_info.pid = 0x0002; + info!("πŸ”Œ Cleared transport to force recreation with new PID after device reconnects"); + } + Ok(true) } Ok(Message::Failure(f)) => { @@ -949,16 +1017,24 @@ impl DeviceQueueFactory { /// Find the physical device matching device info (static method) fn find_physical_device_by_info(device_info: &FriendlyUsbDevice, devices: &[rusb::Device]) -> Result> { if let Some(serial) = &device_info.serial_number { - // Match by serial number + // Match by serial number (flexible - allows PID change after bootloader update) for device in devices { if let Ok(handle) = device.open() { let timeout = std::time::Duration::from_millis(100); if let Ok(langs) = handle.read_languages(timeout) { if let Some(lang) = langs.first() { if let Ok(desc) = device.device_descriptor() { - if let Ok(device_serial) = handle.read_serial_number_string(*lang, &desc, timeout) { - if device_serial == *serial { - return Ok(device.clone()); + // Only check VID matches (KeepKey vendor ID) + if desc.vendor_id() == device_info.vid { + if let Ok(device_serial) = handle.read_serial_number_string(*lang, &desc, timeout) { + if device_serial == *serial { + // Log if PID changed (happens after bootloader update) + if desc.product_id() != device_info.pid { + info!("πŸ“ Device reconnected with different PID: 0x{:04x} -> 0x{:04x}", + device_info.pid, desc.product_id()); + } + return Ok(device.clone()); + } } } } @@ -983,6 +1059,7 @@ impl DeviceQueueFactory { } } - Err(anyhow!("Physical device not found for {}", device_info.unique_id)) + Err(anyhow!("Physical device not found for {} (VID: 0x{:04x}, PID: 0x{:04x}, Serial: {:?})", + device_info.unique_id, device_info.vid, device_info.pid, device_info.serial_number)) } } \ No newline at end of file diff --git a/projects/keepkey-rust/messages/timeouts.rs b/projects/keepkey-rust/messages/timeouts.rs index 45270a7..08c13ab 100644 --- a/projects/keepkey-rust/messages/timeouts.rs +++ b/projects/keepkey-rust/messages/timeouts.rs @@ -57,6 +57,7 @@ impl Message { if Message::is_hid_transport_mode() { return match self { Message::ButtonAck(_) => LONG_TIMEOUT, + Message::FirmwareErase(_) => LONG_TIMEOUT, Message::FirmwareUpload(_) => LONG_TIMEOUT, Message::Initialize(_) => WINDOWS_HID_QUICK_TIMEOUT, // 10 seconds for Windows HID Message::GetFeatures(_) => WINDOWS_HID_QUICK_TIMEOUT, // 10 seconds for Windows HID @@ -69,6 +70,7 @@ impl Message { // For Initialize messages or when in legacy device mode, use appropriate timeout match self { Message::ButtonAck(_) => LONG_TIMEOUT, + Message::FirmwareErase(_) => LONG_TIMEOUT, // Firmware erase needs time for device confirmation Message::FirmwareUpload(_) => LONG_TIMEOUT, // Bootloader updates need more time Message::Initialize(_) if Message::is_legacy_device_mode() => LEGACY_DEVICE_TIMEOUT, Message::Initialize(_) => QUICK_TIMEOUT, // Initialize is usually very fast @@ -84,6 +86,7 @@ impl Message { { if Message::is_hid_transport_mode() { return match self { + Message::FirmwareErase(_) => LONG_TIMEOUT, Message::FirmwareUpload(_) => LONG_TIMEOUT, Message::Initialize(_) => WINDOWS_HID_QUICK_TIMEOUT, // 10 seconds for Windows HID Message::GetFeatures(_) => WINDOWS_HID_QUICK_TIMEOUT, // 10 seconds for Windows HID @@ -94,6 +97,7 @@ impl Message { } match self { + Message::FirmwareErase(_) => LONG_TIMEOUT, Message::FirmwareUpload(_) => LONG_TIMEOUT, Message::Initialize(_) if Message::is_legacy_device_mode() => LEGACY_DEVICE_TIMEOUT, Message::Initialize(_) => QUICK_TIMEOUT, diff --git a/projects/kkcli/Cargo.toml b/projects/kkcli/Cargo.toml index 3819f59..b29e758 100644 --- a/projects/kkcli/Cargo.toml +++ b/projects/kkcli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kkcli" -version = "0.2.3" +version = "2.2.3" authors = ["MrNerdHair "] edition = "2021" diff --git a/projects/vault-v2/README.md b/projects/vault-v2/README.md index 987a8cb..b395be9 100644 --- a/projects/vault-v2/README.md +++ b/projects/vault-v2/README.md @@ -1,6 +1,28 @@ -# KeepKey Vault v2 +# KeepKey Vault V2 - Bitcoin Only Edition -A modern Tauri-based GUI application for KeepKey hardware wallets, built with clean architectural boundaries and efficient hardware communication. +A secure, modern desktop wallet application for KeepKey hardware wallets, focused exclusively on Bitcoin support. Built with Tauri, React, and Rust for optimal performance and security. + +## πŸš€ Features + +- **Bitcoin-Only Focus**: Streamlined for Bitcoin transactions without altcoin distractions +- **Hardware Security**: Full integration with KeepKey hardware wallet +- **Modern Stack**: Built with Tauri 2.0, React, TypeScript, and Rust +- **Native Performance**: Desktop application with native OS integration +- **SegWit Support**: Full support for Legacy (P2PKH), Wrapped SegWit (P2SH-P2WPKH), and Native SegWit (P2WPKH) addresses +- **Real-time Updates**: Live portfolio tracking and transaction status +- **Secure Transaction Signing**: Device-based signing with proper UTXO validation + +## 🎯 Recent Major Fixes (January 2025) + +### Bitcoin Transaction Signing - "Invalid Prevhash 2" Resolution +Successfully resolved critical transaction signing issues that were preventing KeepKey from signing Bitcoin transactions. Key fixes included: + +- **SegWit Transaction Parsing**: Fixed parser to correctly handle SegWit transactions with witness data +- **Script Type Validation**: Only fetch previous transaction hex for legacy (P2PKH) inputs, not for SegWit inputs +- **Change Address Security**: Enforced native SegWit (BIP84) for all change addresses to prevent funds loss +- **API Migration**: Migrated from Blockstream to Mempool.space for improved reliability + +See [detailed documentation](docs/bitcoin-transaction-signing-fix.md) for complete technical details. ## πŸ—οΈ **Architecture Overview** diff --git a/projects/vault-v2/VERSION b/projects/vault-v2/VERSION new file mode 100644 index 0000000..5bc1cc4 --- /dev/null +++ b/projects/vault-v2/VERSION @@ -0,0 +1 @@ +2.2.7 diff --git a/projects/vault-v2/build-signed.sh b/projects/vault-v2/build-signed.sh new file mode 100755 index 0000000..1e84647 --- /dev/null +++ b/projects/vault-v2/build-signed.sh @@ -0,0 +1,148 @@ +#!/bin/bash + +# KeepKey Vault v2 - Local Signed Build Script +# This script builds and signs the Tauri app for macOS + +set -e + +echo "πŸ” KeepKey Vault v2 - Signed Build Script" +echo "=========================================" +echo "" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +SIGNING_IDENTITY="Developer ID Application: KEY HODLERS LLC (DR57X8Z394)" +APPLE_ID="${APPLE_ID:-}" # Set via environment variable +APPLE_PASSWORD="${APPLE_PASSWORD:-}" # App-specific password +TEAM_ID="DR57X8Z394" + +echo "πŸ“‹ Build Configuration:" +echo " - Signing Identity: $SIGNING_IDENTITY" +echo " - Team ID: $TEAM_ID" +echo "" + +# Check for signing identity +echo "πŸ” Checking signing identity..." +if security find-identity -v -p codesigning | grep -q "$SIGNING_IDENTITY"; then + echo -e "${GREEN}βœ“ Signing identity found${NC}" +else + echo -e "${RED}βœ— Signing identity not found!${NC}" + echo "Available identities:" + security find-identity -v -p codesigning + exit 1 +fi + +# Check for Rust and Tauri +echo "" +echo "πŸ” Checking dependencies..." + +if ! command -v rustc &> /dev/null; then + echo -e "${RED}βœ— Rust is not installed${NC}" + exit 1 +fi +echo -e "${GREEN}βœ“ Rust found: $(rustc --version)${NC}" + +if ! command -v cargo &> /dev/null; then + echo -e "${RED}βœ— Cargo is not installed${NC}" + exit 1 +fi + +if ! command -v bun &> /dev/null; then + echo -e "${RED}βœ— Bun is not installed${NC}" + echo "Install with: curl -fsSL https://bun.sh/install | bash" + exit 1 +fi +echo -e "${GREEN}βœ“ Bun found: $(bun --version)${NC}" + +# Install dependencies +echo "" +echo "πŸ“¦ Installing dependencies..." +bun install + +# Build the app +echo "" +echo "πŸ”¨ Building Tauri app..." +echo "This may take several minutes..." + +# Set environment variables for signing +export TAURI_SIGNING_IDENTITY="$SIGNING_IDENTITY" +export TAURI_APPLE_TEAM_ID="$TEAM_ID" + +# Build for universal macOS (both Intel and Apple Silicon) +bun tauri build --target universal-apple-darwin + +echo "" +echo -e "${GREEN}βœ… Build completed successfully!${NC}" +echo "" + +# Show build output location +DMG_PATH="src-tauri/target/universal-apple-darwin/release/bundle/dmg" +APP_PATH="src-tauri/target/universal-apple-darwin/release/bundle/macos" + +echo "πŸ“¦ Build outputs:" +if [ -d "$DMG_PATH" ]; then + echo " - DMG files: $DMG_PATH" + ls -la "$DMG_PATH"/*.dmg 2>/dev/null || echo " No DMG files found" +fi + +if [ -d "$APP_PATH" ]; then + echo " - App bundles: $APP_PATH" + ls -la "$APP_PATH"/*.app 2>/dev/null || echo " No app bundles found" +fi + +# Verify signature +echo "" +echo "πŸ” Verifying signatures..." +for app in "$APP_PATH"/*.app; do + if [ -d "$app" ]; then + echo "Checking: $(basename "$app")" + if codesign -dv --verbose=4 "$app" 2>&1 | grep -q "$TEAM_ID"; then + echo -e "${GREEN}βœ“ Signature valid${NC}" + else + echo -e "${YELLOW}⚠ Signature verification needs review${NC}" + fi + fi +done + +# Optional: Notarization (requires Apple ID credentials) +if [ -n "$APPLE_ID" ] && [ -n "$APPLE_PASSWORD" ]; then + echo "" + echo "πŸ“ Starting notarization process..." + echo "This may take 5-10 minutes..." + + for dmg in "$DMG_PATH"/*.dmg; do + if [ -f "$dmg" ]; then + echo "Notarizing: $(basename "$dmg")" + + # Submit for notarization + xcrun notarytool submit "$dmg" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_PASSWORD" \ + --team-id "$TEAM_ID" \ + --wait + + # Staple the notarization ticket + xcrun stapler staple "$dmg" + echo -e "${GREEN}βœ“ Notarization complete for $(basename "$dmg")${NC}" + fi + done +else + echo "" + echo -e "${YELLOW}ℹ️ Notarization skipped (no Apple ID credentials provided)${NC}" + echo "To enable notarization, set these environment variables:" + echo " export APPLE_ID='your-apple-id@example.com'" + echo " export APPLE_PASSWORD='your-app-specific-password'" +fi + +echo "" +echo "πŸŽ‰ Build and signing complete!" +echo "" +echo "Next steps:" +echo "1. Test the signed app from: $APP_PATH" +echo "2. Distribute the DMG from: $DMG_PATH" +echo "3. If notarization is needed, run with APPLE_ID and APPLE_PASSWORD set" \ No newline at end of file diff --git a/projects/vault-v2/docs/PIN_FLOW_PROTECTION.md b/projects/vault-v2/docs/PIN_FLOW_PROTECTION.md new file mode 100644 index 0000000..1df9968 --- /dev/null +++ b/projects/vault-v2/docs/PIN_FLOW_PROTECTION.md @@ -0,0 +1,205 @@ +# PIN Flow Protection System + +## Overview +This document describes the PIN flow protection system implemented in KeepKey Vault v2 to prevent device operations from interrupting PIN entry on the hardware device. + +## Problem Statement +When a KeepKey device displays the PIN entry screen, any USB communication can dismiss the screen, causing a poor user experience. Common culprits include: +- Background xpub fetching for portfolio display +- Status polling for device updates +- GetFeatures calls for device state checking + +## Solution Architecture + +### 1. PIN Flow State Tracking +The system maintains a global registry of devices currently in PIN entry mode: + +```rust +// In commands.rs +lazy_static! { + static ref DEVICES_IN_PIN_FLOW: Arc>> = + Arc::new(Mutex::new(HashSet::new())); +} +``` + +### 2. Two-Layer Protection + +#### Layer 1: Request Blocking at Queue Entry +**Location**: `src/device/queue.rs` lines 82-95 + +Before ANY request enters the device queue, the system checks if the device is in PIN flow: + +```rust +if crate::commands::is_device_in_pin_flow(&request.device_id) { + match &request.request { + DeviceRequest::GetXpub { .. } | + DeviceRequest::GetAddress { .. } | + DeviceRequest::SignTransaction { .. } => { + return Err("Device is currently in PIN entry mode. Please complete PIN entry first.".to_string()); + }, + _ => { + // Allow GetFeatures and SendRaw during PIN flow as they might be needed + } + } +} +``` + +**Blocked Operations During PIN Flow:** +- ❌ GetXpub - Would interrupt PIN screen +- ❌ GetAddress - Would interrupt PIN screen +- ❌ SignTransaction - Would interrupt PIN screen +- βœ… SendRaw - Needed for PIN-related messages +- βœ… GetFeatures - Handled by Layer 2 + +#### Layer 2: GetFeatures Caching +**Location**: `src/device/queue.rs` lines 101-138 + +Even allowed operations avoid unnecessary device communication: + +```rust +let raw_features_opt = if crate::commands::is_device_in_pin_flow(&request.device_id) { + // Use cached features instead of querying device + let cache = DEVICE_STATE_CACHE.read().await; + cache.get(&request.device_id).and_then(|state| state.last_features.clone()) +} else { + // Normal flow - fetch fresh features from device + match keepkey_rust::device_queue::DeviceQueueHandle::get_features(&queue_handle).await { + // ... normal GetFeatures handling + } +}; +``` + +### 3. PIN Flow Lifecycle + +#### Starting PIN Flow +```rust +// In trigger_pin_request() or start_pin_creation() +mark_device_in_pin_flow(&device_id)?; +``` + +#### During PIN Flow +- All non-essential requests are blocked +- Status checks use cached data +- PIN-related messages (SendRaw) pass through + +#### Ending PIN Flow +```rust +// After PIN entry complete or cancelled +unmark_device_in_pin_flow(&device_id)?; +``` + +## Implementation Details + +### Helper Functions + +**Check PIN Flow Status** +```rust +pub fn is_device_in_pin_flow(device_id: &str) -> bool { + if let Ok(devices) = DEVICES_IN_PIN_FLOW.lock() { + devices.contains(device_id) + } else { + false + } +} +``` + +**Mark Device in PIN Flow** +```rust +pub fn mark_device_in_pin_flow(device_id: &str) -> Result<(), String> { + let mut devices = DEVICES_IN_PIN_FLOW.lock() + .map_err(|_| "Failed to lock PIN flow registry")?; + devices.insert(device_id.to_string()); + Ok(()) +} +``` + +**Remove from PIN Flow** +```rust +pub fn unmark_device_in_pin_flow(device_id: &str) -> Result<(), String> { + let mut devices = DEVICES_IN_PIN_FLOW.lock() + .map_err(|_| "Failed to lock PIN flow registry")?; + devices.remove(device_id); + Ok(()) +} +``` + +### Error Handling + +When requests are blocked during PIN flow: +1. User-friendly error message returned +2. No device communication attempted +3. Frontend can display appropriate UI feedback + +Example error response: +```json +{ + "error": "Device is currently in PIN entry mode. Please complete PIN entry first." +} +``` + +## Benefits + +1. **Uninterrupted PIN Entry**: Users can complete PIN entry without the screen disappearing +2. **Reduced USB Traffic**: Fewer unnecessary device queries during sensitive operations +3. **Better UX**: Clear error messages explain why operations are temporarily blocked +4. **Cache Efficiency**: Features cached and reused during PIN flow + +## Testing + +### Test Scenarios + +1. **Basic PIN Flow Protection** + - Trigger PIN request + - Attempt to fetch xpub while PIN screen visible + - Verify request is blocked with appropriate error + +2. **Cache Usage During PIN** + - Get device features before PIN flow + - Enter PIN flow + - Request device status + - Verify cached features are used (no USB traffic) + +3. **Flow Cleanup** + - Enter PIN flow + - Complete or cancel PIN entry + - Verify normal operations resume + - Verify xpub requests succeed + +### Debug Logging + +Key log messages to monitor: +``` +🚫 Blocking request during PIN flow - device is entering PIN +⚠️ Skipping GetFeatures check - device is in PIN flow +πŸ“‹ Using cached features for device during PIN flow +``` + +## Related Files + +- `src/commands.rs` - PIN flow state management functions +- `src/device/queue.rs` - Request blocking and caching logic +- `src/event_controller.rs` - Device event handling + +## Future Improvements + +1. **Timeout Handling**: Auto-clear PIN flow state after timeout (e.g., 5 minutes) +2. **Multi-Device Support**: Better handling when multiple devices are in PIN flow +3. **State Persistence**: Restore PIN flow state after app restart +4. **WebSocket Notifications**: Notify frontend when device enters/exits PIN flow + +## Troubleshooting + +### Issue: PIN Screen Still Disappearing +- Check logs for any SendRaw messages during PIN flow +- Verify all background polling is using the device queue +- Ensure frontend isn't bypassing the queue system + +### Issue: Operations Blocked After PIN Entry +- Check if `unmark_device_in_pin_flow()` is called after PIN completion +- Verify PIN flow cleanup on error conditions +- Check DEVICES_IN_PIN_FLOW state in debugger + +### Issue: Features Not Cached +- Ensure device has been queried at least once before PIN flow +- Check DEVICE_STATE_CACHE has entry for device +- Verify cache insertion in GetFeatures success path \ No newline at end of file diff --git a/projects/vault-v2/docs/bitcoin-transaction-signing-fix.md b/projects/vault-v2/docs/bitcoin-transaction-signing-fix.md new file mode 100644 index 0000000..5114e81 --- /dev/null +++ b/projects/vault-v2/docs/bitcoin-transaction-signing-fix.md @@ -0,0 +1,223 @@ +# Bitcoin Transaction Signing: Invalid Prevhash Fix Documentation + +## Executive Summary + +This document details the resolution of the "Encountered invalid prevhash 2" error that was preventing KeepKey hardware wallets from signing Bitcoin transactions. The issue involved multiple interconnected problems across the JavaScript frontend, Rust backend, and transaction data handling. + +## The Problem + +When attempting to sign Bitcoin transactions, the KeepKey device would consistently return: +``` +Device communication error: Failure: Encountered invalid prevhash 2 +``` + +This error occurs when the device cannot properly validate the previous transaction data for security verification. + +## Root Causes Identified + +### 1. SegWit Transaction Format Mishandling + +**Issue**: The transaction hex being fetched from the blockchain was in SegWit format (containing witness data), but the parser was attempting to read it as a legacy transaction. + +**Example of problematic hex**: +``` +0100000000010192dc12975c6e4ceb7678e2972c6a300091d799ab94281a47adbc8bb673bfbf3b... +``` + +Breaking this down: +- `01000000` = version 1 +- `0001` = SegWit marker (0x00) and flag (0x01) +- The parser incorrectly interpreted `0001` as the input count + +**Solution**: Updated the transaction parser to: +- Detect SegWit marker and flag +- Properly skip witness data when present +- Correctly parse the actual input count + +### 2. Incorrect Script Type Requirements + +**Issue**: The system was attempting to fetch previous transaction hex for ALL inputs, but KeepKey only requires this for legacy (P2PKH) inputs. + +**HDWallet Behavior** (reference implementation): +- Legacy inputs (P2PKH): REQUIRE full previous transaction hex +- SegWit inputs (P2SH-P2WPKH, P2WPKH): Do NOT need hex + +**Solution**: +- Only fetch transaction hex for legacy (p2pkh) inputs +- Skip hex fetching for SegWit inputs (p2sh-p2wpkh, p2wpkh) + +### 3. Script Type Naming Inconsistency + +**Issue**: Frontend was using `p2sh` while backend expected `p2sh-p2wpkh`. + +**Solution**: Standardized script type names across the stack: +- `p2pkh` β†’ Legacy +- `p2sh-p2wpkh` β†’ SegWit wrapped in P2SH (not just `p2sh`) +- `p2wpkh` β†’ Native SegWit + +### 4. Change Address Derivation Bug + +**Issue**: Change addresses were mixing derivation paths from different script types, potentially sending funds to addresses outside the gap limit. + +**Example of the bug**: +```javascript +// Wrong: Using P2SH change index with P2WPKH path +{ + "address_n_list": [2147483732, 2147483648, 2147483648, 1, 58], // Index 58 from P2SH + "script_type": "p2wpkh" // But using native SegWit type! +} +``` + +**Solution**: +- Always use native SegWit (p2wpkh) for change addresses +- Use correct BIP84 derivation path: `m/84'/0'/0'/1/x` +- Ensure change address index matches the script type + +## Implementation Details + +### Frontend Changes (TypeScript/React) + +#### 1. Transaction Building (`createUnsignedUxtoTx.ts`) + +```typescript +// Only fetch hex for legacy inputs +if (scriptType === 'p2pkh') { + // Legacy inputs REQUIRE the full previous transaction + hex = await PioneerAPI.getRawTransaction(hash); +} else { + // SegWit inputs (p2sh-p2wpkh, p2wpkh) do NOT need hex + console.log(`⚑ SegWit input detected (${scriptType}) - no hex needed`); +} +``` + +#### 2. Change Address Logic + +```typescript +// Always use native segwit (p2wpkh) for change addresses +const changeScriptType = 'p2wpkh'; +const changeXpub = relevantPubkeys.find(pk => pk.scriptType === 'p2wpkh')?.pubkey; +const path = `m/84'/0'/0'/1/${changeAddressIndex}`; // BIP84 path +``` + +### Backend Changes (Rust) + +#### 1. Transaction Parser (`commands.rs`) + +```rust +// Check for SegWit marker and flag +let mut is_segwit = false; +let input_count = { + let first_byte = read_varint(&mut cursor)?; + if first_byte == 0 { + // This might be SegWit marker (0x00) followed by flag (0x01) + let flag = read_u8(&mut cursor)?; + if flag == 1 { + is_segwit = true; + // Now read the actual input count + read_varint(&mut cursor)? + } + } else { + first_byte + } +}; + +// Skip witness data if present +if is_segwit { + for _ in 0..input_count { + let witness_count = read_varint(&mut cursor)?; + for _ in 0..witness_count { + let witness_len = read_varint(&mut cursor)? as usize; + // Skip witness bytes + let mut witness_data = vec![0u8; witness_len]; + read_exact(&mut cursor, &mut witness_data)?; + } + } +} +``` + +#### 2. Input Validation (`device/queue.rs`) + +```rust +// Only legacy (p2pkh) inputs require previous transaction hex +let needs_hex = input.script_type == "p2pkh"; + +if needs_hex && input.prev_tx_hex.is_none() { + return Err(format!("Legacy input {} missing required previous transaction hex", idx)); +} else if !needs_hex { + println!("⚑ SegWit input {} ({}): no hex required", idx, input.script_type); +} +``` + +## Testing & Verification + +### Test Scenarios + +1. **Legacy Input Transaction**: + - Input type: P2PKH + - Hex required: YES + - Result: βœ… Successfully signs + +2. **SegWit Input Transaction**: + - Input type: P2SH-P2WPKH or P2WPKH + - Hex required: NO + - Result: βœ… Successfully signs + +3. **Mixed Input Transaction**: + - Multiple input types + - Hex fetched only for legacy inputs + - Result: βœ… Successfully signs + +### Verification Steps + +1. Check that hex is only fetched for legacy inputs in browser console +2. Verify Rust backend logs show "SegWit input: no hex required" for non-legacy inputs +3. Confirm change addresses always use native SegWit (BIP84) paths +4. Validate successful transaction signing and broadcasting + +## Migration to Mempool.space API + +As part of this fix, we also migrated from Blockstream API to Mempool.space for improved reliability: + +```typescript +// Primary API +const response = await axios.get( + `https://mempool.space/api/tx/${txid}/hex` +); + +// Fallback to Blockstream if needed +if (error) { + const fallback = await axios.get( + `https://blockstream.info/api/tx/${txid}/hex` + ); +} +``` + +Benefits: +- Better uptime and reliability +- Richer transaction data +- More responsive API +- Better rate limits + +## Key Learnings + +1. **Hardware Wallet Security**: KeepKey validates previous transactions differently for legacy vs SegWit inputs as a security measure +2. **Transaction Format Evolution**: Bitcoin's SegWit upgrade added complexity that must be handled at the parsing level +3. **HDWallet Compatibility**: Following the HDWallet reference implementation patterns ensures device compatibility +4. **Script Type Consistency**: Maintaining consistent script type naming across the entire stack is crucial + +## References + +- [BIP141 - SegWit](https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki) +- [BIP84 - Native SegWit Addresses](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) +- [HDWallet Bitcoin Implementation](https://github.com/shapeshift/hdwallet/blob/master/packages/hdwallet-keepkey/src/bitcoin.ts) +- [KeepKey Protocol Documentation](https://github.com/keepkey/keepkey-firmware/wiki) + +## Conclusion + +The "invalid prevhash 2" error was caused by a combination of: +1. Incorrect SegWit transaction parsing +2. Unnecessary hex fetching for SegWit inputs +3. Script type naming inconsistencies +4. Change address derivation bugs + +All issues have been resolved, and the system now correctly handles both legacy and SegWit transactions according to KeepKey's security requirements. \ No newline at end of file diff --git a/projects/vault-v2/docs/bootloader-update-fix-analysis.md b/projects/vault-v2/docs/bootloader-update-fix-analysis.md new file mode 100644 index 0000000..886d9fa --- /dev/null +++ b/projects/vault-v2/docs/bootloader-update-fix-analysis.md @@ -0,0 +1,94 @@ +# Bootloader Update UI Fix - Analysis and Lessons Learned + +## Issue Summary +The bootloader update page was failing to render due to a React component error. When users clicked "Update Bootloader", the update command was sent to the device, but the UI failed to show the update progress page. + +## Root Cause +The error was caused by an incorrect import statement for the Progress component: +```tsx +// Incorrect - trying to import from a non-existent path +import { Progress } from "../../ui/progress"; + +// Correct - Progress is a Chakra UI component +import { Progress } from "@chakra-ui/react"; +``` + +## The Fix +The solution was simple - add Progress to the existing Chakra UI import: +```tsx +import { VStack, HStack, Text, Button, Box, Icon, Image, Alert, Progress } from "@chakra-ui/react"; +``` + +## Why It Took Multiple Attempts + +### 1. **Initial Misdiagnosis** +- Started by looking in the wrong project (`keepkey-desktop-v5` instead of `keepkey-bitcoin-only/vault-v2`) +- Assumed the issue was with a missing BootloaderUpdateWizard component +- Spent time trying to integrate a wizard component that wasn't needed + +### 2. **Overcomplication** +- Attempted to fix a complex workflow issue when the problem was a simple import error +- Modified DialogContext and BootloaderUpdateDialog in the wrong project +- Created unnecessary complexity by trying to add wizard functionality + +### 3. **Incorrect Assumptions** +- Assumed Progress was a custom UI component that needed a special import path +- Didn't immediately recognize that Progress is a standard Chakra UI component +- Initially tried to use the new Chakra UI v3 API (Progress.Root, Progress.Track, etc.) when the project uses the standard API + +### 4. **Not Following Error Messages** +- The error clearly stated: "Failed to resolve import '../../ui/progress'" +- Should have immediately checked if Progress was available in @chakra-ui/react +- The line number in the error (line 253) was misleading - the actual issue was the import + +## Strategy for Better Debugging Next Time + +### 1. **Start with the Exact Error** +- Read the full error message carefully +- Focus on import/module resolution errors first +- Check the exact file path mentioned in the error + +### 2. **Verify Project Context** +- Confirm which project is running (check the URL, port, or user clarification) +- Don't assume - ask for clarification if unsure +- Use the correct project path from the start + +### 3. **Check Existing Patterns** +- Look at how similar components are imported in the same project +- Use grep to find existing usage patterns: + ```bash + grep -r "Progress" --include="*.tsx" . + ``` +- Follow established conventions in the codebase + +### 4. **Simple Solutions First** +- Start with the simplest possible fix +- Don't introduce new components or complex workflows unless necessary +- Import errors are usually just incorrect paths or missing imports + +### 5. **Use the Framework Documentation** +- Chakra UI components should be imported from @chakra-ui/react +- Check if it's a built-in component before assuming it's custom +- Verify the correct API version (v2 vs v3) + +### 6. **Test Incrementally** +- Fix one issue at a time +- Verify each fix before moving to the next +- Don't make changes in multiple files unless necessary + +## Best Practices for React Import Issues + +1. **Standard UI Library Components**: Always check if a component is part of the UI library first +2. **Relative Path Validation**: Ensure relative paths (../) actually lead to existing files +3. **Named vs Default Imports**: Use the correct import syntax for the component +4. **IDE Support**: Use IDE autocomplete to verify available imports +5. **Build Errors**: Pay attention to build/transpilation errors - they often pinpoint the exact issue + +## Conclusion + +This issue was a simple import error that was overcomplicated by: +- Working in the wrong project initially +- Making assumptions about component architecture +- Not following the error message directly + +The key lesson is to always start with the simplest explanation for an error and verify the basic requirements (correct imports, correct project, correct file paths) before attempting complex solutions. \ No newline at end of file diff --git a/projects/vault-v2/docs/debugging-strategy-improvements.md b/projects/vault-v2/docs/debugging-strategy-improvements.md new file mode 100644 index 0000000..6478f2c --- /dev/null +++ b/projects/vault-v2/docs/debugging-strategy-improvements.md @@ -0,0 +1,115 @@ +# Debugging Strategy Improvements + +## Quick Debugging Checklist + +### 1. Error Analysis Phase (First 2 minutes) +- [ ] Read the EXACT error message +- [ ] Note the file path and line number +- [ ] Identify error type: Import/Syntax/Runtime/Type +- [ ] Confirm which project is affected + +### 2. Context Verification Phase (Next 1 minute) +- [ ] Verify the project directory +- [ ] Check running port/URL matches expected project +- [ ] Confirm framework version (React/Chakra UI/etc.) +- [ ] Note any recent changes or context + +### 3. Pattern Recognition Phase (Next 2 minutes) +- [ ] Search for similar component usage in the project +- [ ] Check imports in nearby files +- [ ] Look for established patterns +- [ ] Verify component is not already imported + +### 4. Solution Implementation Phase +- [ ] Start with the simplest fix +- [ ] Make one change at a time +- [ ] Test after each change +- [ ] Document what worked + +## Common React Error Patterns + +### Import Errors +``` +Failed to resolve import "X" from "Y" +``` +**Quick Fix Sequence:** +1. Check if X is from a node_module (npm package) +2. Check if X exists at the relative path +3. Check other files for correct import pattern +4. Verify the component name and export type + +### Component Rendering Errors +``` +Element type is invalid: expected a string... but got: object +``` +**Quick Fix Sequence:** +1. Check the import statement +2. Verify named vs default export +3. Ensure component is exported +4. Check for circular dependencies + +### Module Not Found +``` +Module not found: Can't resolve 'X' +``` +**Quick Fix Sequence:** +1. Run `npm install` if it's a package +2. Check package.json for the dependency +3. Verify the import path +4. Check for typos in import statement + +## Grep Commands for Quick Debugging + +```bash +# Find how a component is used elsewhere +grep -r "ComponentName" --include="*.tsx" --include="*.ts" . + +# Find import patterns for a package +grep -r "from [@'\"]chakra-ui" --include="*.tsx" . + +# Find all Progress component usage +grep -r "Progress" --include="*.tsx" -B2 -A2 . + +# Find specific import patterns +grep -r "import.*Progress.*from" --include="*.tsx" . +``` + +## Project Structure Quick Reference + +When debugging, quickly identify: +1. **UI Components Location**: Usually in `/components/ui/` or from UI library +2. **Context Providers**: Usually in `/contexts/` +3. **Types**: Usually in `/types/` +4. **Assets**: Usually in `/assets/` + +## Time-Boxing Strategy + +- **0-5 minutes**: Simple import/syntax fixes +- **5-10 minutes**: Component integration issues +- **10-15 minutes**: Complex state/props issues +- **15+ minutes**: Architectural problems - step back and reconsider + +## Red Flags to Watch For + +1. **Making changes in multiple projects** - Stop and verify which one is correct +2. **Creating new components to fix errors** - Usually not necessary +3. **Modifying core infrastructure** - Simple errors rarely need this +4. **Import paths with many ../../../** - Consider absolute imports +5. **Assuming custom components** - Check if it's from the UI library first + +## The "Occam's Razor" Debugging Principle + +The simplest explanation is usually correct: +- Import error? Wrong import path +- Component not rendering? Missing or incorrect import +- Type error? Props mismatch +- Module not found? Not installed or wrong name + +## Post-Fix Verification + +After fixing an issue: +1. Document the fix +2. Check for similar issues elsewhere +3. Consider adding a lint rule +4. Update team knowledge base +5. Create a unit test if applicable \ No newline at end of file diff --git a/projects/vault-v2/notarize.sh b/projects/vault-v2/notarize.sh new file mode 100755 index 0000000..e2b3643 --- /dev/null +++ b/projects/vault-v2/notarize.sh @@ -0,0 +1,190 @@ +#!/bin/bash + +# KeepKey Vault v2 - Notarization Helper Script +# This script helps with Apple notarization after building + +set -e + +echo "🍎 KeepKey Vault v2 - Notarization Helper" +echo "=========================================" +echo "" + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Configuration +TEAM_ID="DR57X8Z394" +BUNDLE_ID="com.vault-v2.app" + +# Check for credentials +if [ -z "$APPLE_ID" ]; then + echo -e "${RED}❌ APPLE_ID environment variable not set${NC}" + echo "" + echo "Please set your Apple ID:" + echo " export APPLE_ID='your-apple-id@example.com'" + exit 1 +fi + +if [ -z "$APPLE_PASSWORD" ]; then + echo -e "${RED}❌ APPLE_PASSWORD environment variable not set${NC}" + echo "" + echo "Please set your app-specific password:" + echo " export APPLE_PASSWORD='your-app-specific-password'" + echo "" + echo "To create an app-specific password:" + echo " 1. Go to https://appleid.apple.com/account/manage" + echo " 2. Sign in and go to Security" + echo " 3. Under App-Specific Passwords, click Generate Password" + echo " 4. Use 'KeepKey Vault Notarization' as the label" + exit 1 +fi + +echo -e "${GREEN}βœ… Credentials configured${NC}" +echo " Apple ID: $APPLE_ID" +echo " Team ID: $TEAM_ID" +echo "" + +# Find DMG files +DMG_PATH="target/universal-apple-darwin/release/bundle/dmg" +APP_PATH="target/universal-apple-darwin/release/bundle/macos" + +if [ ! -d "$DMG_PATH" ]; then + DMG_PATH="src-tauri/target/universal-apple-darwin/release/bundle/dmg" + APP_PATH="src-tauri/target/universal-apple-darwin/release/bundle/macos" +fi + +if [ ! -d "$DMG_PATH" ]; then + DMG_PATH="src-tauri/target/release/bundle/dmg" + APP_PATH="src-tauri/target/release/bundle/macos" +fi + +echo "πŸ” Looking for built artifacts..." +echo "" + +# Function to notarize a file +notarize_file() { + local FILE="$1" + local FILENAME=$(basename "$FILE") + + echo -e "${BLUE}πŸ“ Notarizing: $FILENAME${NC}" + echo "This may take 5-10 minutes..." + + # Submit for notarization + echo "Submitting to Apple..." + SUBMISSION_ID=$(xcrun notarytool submit "$FILE" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_PASSWORD" \ + --team-id "$TEAM_ID" \ + --output-format json \ + --wait 2>&1 | tee /tmp/notarization.log | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | head -1) + + if [ -z "$SUBMISSION_ID" ]; then + echo -e "${RED}❌ Failed to submit for notarization${NC}" + echo "Check /tmp/notarization.log for details" + return 1 + fi + + echo "Submission ID: $SUBMISSION_ID" + + # Check status + echo "Checking notarization status..." + xcrun notarytool info "$SUBMISSION_ID" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_PASSWORD" \ + --team-id "$TEAM_ID" + + # Get the log if needed + echo "" + echo "Getting notarization log..." + xcrun notarytool log "$SUBMISSION_ID" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_PASSWORD" \ + --team-id "$TEAM_ID" \ + /tmp/notarization-log.json + + # Check if successful + if grep -q '"status": "Accepted"' /tmp/notarization-log.json 2>/dev/null || grep -q '"status":"Accepted"' /tmp/notarization-log.json 2>/dev/null; then + echo -e "${GREEN}βœ… Notarization successful!${NC}" + + # Staple the ticket + echo "Stapling notarization ticket..." + if [[ "$FILE" == *.dmg ]]; then + xcrun stapler staple "$FILE" + echo -e "${GREEN}βœ… Ticket stapled to $FILENAME${NC}" + elif [[ "$FILE" == *.app ]]; then + xcrun stapler staple "$FILE" + echo -e "${GREEN}βœ… Ticket stapled to $FILENAME${NC}" + fi + + # Verify + echo "Verifying..." + xcrun stapler validate "$FILE" + + return 0 + else + echo -e "${YELLOW}⚠️ Notarization may have issues${NC}" + echo "Check the log at: /tmp/notarization-log.json" + return 1 + fi +} + +# Process DMG files +DMG_COUNT=0 +if [ -d "$DMG_PATH" ]; then + for dmg in "$DMG_PATH"/*.dmg; do + if [ -f "$dmg" ]; then + DMG_COUNT=$((DMG_COUNT + 1)) + notarize_file "$dmg" + echo "" + fi + done +fi + +# Process App bundles (if no DMG) +if [ $DMG_COUNT -eq 0 ] && [ -d "$APP_PATH" ]; then + echo -e "${YELLOW}No DMG files found, notarizing app bundles...${NC}" + for app in "$APP_PATH"/*.app; do + if [ -d "$app" ]; then + # Create a temporary DMG for notarization + APP_NAME=$(basename "$app" .app) + TEMP_DMG="/tmp/${APP_NAME}-temp.dmg" + + echo "Creating temporary DMG for notarization..." + hdiutil create -volname "$APP_NAME" -srcfolder "$app" -ov -format UDBZ "$TEMP_DMG" + + notarize_file "$TEMP_DMG" + + # Staple to the original app + echo "Stapling to original app..." + xcrun stapler staple "$app" + + rm -f "$TEMP_DMG" + echo "" + fi + done +fi + +if [ $DMG_COUNT -eq 0 ] && [ ! -d "$APP_PATH" ]; then + echo -e "${RED}❌ No DMG files or app bundles found${NC}" + echo "" + echo "Please build the app first:" + echo " ./build-signed.sh" + exit 1 +fi + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" +echo -e "${GREEN}πŸŽ‰ Notarization process complete!${NC}" +echo "" +echo "Your app is now ready for distribution." +echo "" +echo "To verify notarization status:" +echo " xcrun stapler validate " +echo "" +echo "To check notarization history:" +echo " xcrun notarytool history --apple-id $APPLE_ID --team-id $TEAM_ID" \ No newline at end of file diff --git a/projects/vault-v2/package-lock.json b/projects/vault-v2/package-lock.json index f5aab3f..8213fe4 100644 --- a/projects/vault-v2/package-lock.json +++ b/projects/vault-v2/package-lock.json @@ -1,12 +1,12 @@ { "name": "vault-v2", - "version": "0.1.0", + "version": "2.2.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vault-v2", - "version": "0.1.0", + "version": "2.2.7", "dependencies": { "@chakra-ui/react": "^3.19.1", "@emotion/react": "^11.14.0", @@ -14,10 +14,13 @@ "@pioneer-platform/pioneer-coins": "^9.2.26", "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-process": "^2.2.2", "@tauri-apps/plugin-sql": "^2.2.0", + "@types/canvas-confetti": "^1.9.0", "@types/qrcode": "^1.5.5", "axios": "^1.9.0", "bitcoin-address-validation": "^3.0.0", + "canvas-confetti": "^1.9.3", "coinselect": "^3.1.13", "framer-motion": "^12.15.0", "qrcode": "^1.5.4", @@ -661,7 +664,9 @@ } }, "node_modules/@tauri-apps/api": { - "version": "2.5.0", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.7.0.tgz", + "integrity": "sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg==", "license": "Apache-2.0 OR MIT", "funding": { "type": "opencollective", @@ -718,6 +723,15 @@ "@tauri-apps/api": "^2.0.0" } }, + "node_modules/@tauri-apps/plugin-process": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.0.tgz", + "integrity": "sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.6.0" + } + }, "node_modules/@tauri-apps/plugin-sql": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-sql/-/plugin-sql-2.2.0.tgz", @@ -788,6 +802,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", + "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "dev": true, @@ -1764,6 +1784,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-confetti": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz", + "integrity": "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/cliui": { "version": "6.0.0", "license": "ISC", diff --git a/projects/vault-v2/package.json b/projects/vault-v2/package.json index 90fa772..36dff3c 100644 --- a/projects/vault-v2/package.json +++ b/projects/vault-v2/package.json @@ -1,7 +1,7 @@ { "name": "vault-v2", "private": true, - "version": "2.2.0", + "version": "2.2.7", "type": "module", "scripts": { "dev": "vite", diff --git a/projects/vault-v2/src-tauri/Cargo.toml b/projects/vault-v2/src-tauri/Cargo.toml index f781c56..b12948b 100644 --- a/projects/vault-v2/src-tauri/Cargo.toml +++ b/projects/vault-v2/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vault-v2" -version = "2.2.0" +version = "2.2.7" description = "A Tauri App" authors = ["you"] edition = "2021" @@ -48,5 +48,9 @@ utoipa-axum = "0.2.0" utoipa-swagger-ui = { version = "5", features = ["axum", "debug-embed"] } once_cell = "1.18.0" tauri-plugin-process = "2" +# Proxy dependencies +reqwest = { version = "0.11", features = ["json", "stream"] } +url = "2.4" +regex = "1.10" # Note: rusb removed - handled internally by keepkey-rust diff --git a/projects/vault-v2/src-tauri/entitlements.plist b/projects/vault-v2/src-tauri/entitlements.plist new file mode 100644 index 0000000..d66db5b --- /dev/null +++ b/projects/vault-v2/src-tauri/entitlements.plist @@ -0,0 +1,29 @@ + + + + + + com.apple.security.cs.allow-jit + + + + com.apple.security.cs.allow-unsigned-executable-memory + + + + com.apple.security.cs.allow-dyld-environment-variables + + + + com.apple.security.device.usb + + + + com.apple.security.network.client + + + + com.apple.security.files.user-selected.read-write + + + \ No newline at end of file diff --git a/projects/vault-v2/src-tauri/firmware_backup/bl_v1.1.0/blupdater.bin b/projects/vault-v2/src-tauri/firmware_backup/bl_v1.1.0/blupdater.bin new file mode 100644 index 0000000..716051a Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/bl_v1.1.0/blupdater.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/bl_v1.1.0/keepkey-firmware-bl_v1.1.0.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/bl_v1.1.0/keepkey-firmware-bl_v1.1.0.tar.gz new file mode 100644 index 0000000..390cdc3 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/bl_v1.1.0/keepkey-firmware-bl_v1.1.0.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/bl_v1.1.0/keepkey-firmware-bl_v1.1.0.zip b/projects/vault-v2/src-tauri/firmware_backup/bl_v1.1.0/keepkey-firmware-bl_v1.1.0.zip new file mode 100644 index 0000000..47375c4 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/bl_v1.1.0/keepkey-firmware-bl_v1.1.0.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/bl_v2.0.0/blupdater.bin b/projects/vault-v2/src-tauri/firmware_backup/bl_v2.0.0/blupdater.bin new file mode 100644 index 0000000..bec3d96 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/bl_v2.0.0/blupdater.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/bl_v2.0.0/keepkey-firmware-bl_v2.0.0.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/bl_v2.0.0/keepkey-firmware-bl_v2.0.0.tar.gz new file mode 100644 index 0000000..f640641 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/bl_v2.0.0/keepkey-firmware-bl_v2.0.0.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/bl_v2.0.0/keepkey-firmware-bl_v2.0.0.zip b/projects/vault-v2/src-tauri/firmware_backup/bl_v2.0.0/keepkey-firmware-bl_v2.0.0.zip new file mode 100644 index 0000000..3346883 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/bl_v2.0.0/keepkey-firmware-bl_v2.0.0.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/bl_v2.1.4/blupdater.bin b/projects/vault-v2/src-tauri/firmware_backup/bl_v2.1.4/blupdater.bin new file mode 100644 index 0000000..380b4c0 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/bl_v2.1.4/blupdater.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/releases.json b/projects/vault-v2/src-tauri/firmware_backup/releases.json new file mode 100644 index 0000000..3f20173 --- /dev/null +++ b/projects/vault-v2/src-tauri/firmware_backup/releases.json @@ -0,0 +1,104 @@ +{ + "latest": { + "firmware": { + "version": "v7.10.0", + "url": "v7.10.0/firmware.keepkey.bin", + "hash": "958764cf3baa53eec0002eab9c54e02ce6f5fdab71e7efbbe723f958e26ff419" + }, + "bootloader": { + "version": "v2.1.4", + "url": "bl_v2.1.4/blupdater.bin", + "hash": "6bb7cfd28262fcd61c450fdc3f6932650bdf16a134ab6c1bc6f90b0d1578e620" + }, + "updater": { + "version": "v2.1.4" + } + }, + "beta": { + "firmware": { + "version": "v7.10.0", + "url": "v7.10.0/firmware.keepkey.bin", + "hash": "958764cf3baa53eec0002eab9c54e02ce6f5fdab71e7efbbe723f958e26ff419" + }, + "bootloader": { + "version": "v2.1.4", + "url": "bl_v2.1.4/blupdater.bin", + "hash": "6bb7cfd28262fcd61c450fdc3f6932650bdf16a134ab6c1bc6f90b0d1578e620" + }, + "updater": { + "version": "v2.1.4" + } + }, + "hashes": { + "bootloader": { + "6397c446f6b9002a8b150bf4b9b4e0bb66800ed099b881ca49700139b0559f10": "v1.0.0", + "f13ce228c0bb2bdbc56bdcb5f4569367f8e3011074ccc63331348deb498f2d8f": "v1.0.0", + "d544b5e06b0c355d68b868ac7580e9bab2d224a1e2440881cc1bca2b816752d5": "v1.0.1", + "ec618836f86423dbd3114c37d6e3e4ffdfb87d9e4c6199cf3e163a67b27498a2": "v1.0.1", + "cd702b91028a2cfa55af43d3407ba0f6f752a4a2be0583a172983b303ab1032e": "v1.0.2", + "bcafb38cd0fbd6e2bdbea89fb90235559fdda360765b74e4a8758b4eff2d4921": "v1.0.2", + "cb222548a39ff6cbe2ae2f02c8d431c9ae0df850f814444911f521b95ab02f4c": "v1.0.3", + "917d1952260c9b89f3a96bea07eea4074afdcc0e8cdd5d064e36868bdd68ba7d": "v1.0.3", + "6465bc505586700a8111c4bf7db6f40af73e720f9e488d20db56135e5a690c4f": "v1.0.3", + "db4bc389335e876e942ae3b12558cecd202b745903e79b34dd2c32532708860e": "v1.0.3", + "2e38950143cf350345a6ddada4c0c4f21eb2ed337309f39c5dbc70b6c091ae00": "v1.0.3", + "83d14cb6c7c48af2a83bc326353ee6b9abdd74cfe47ba567de1cb564da65e8e9": "v1.0.3", + "770b30aaa0be884ee8621859f5d055437f894a5c9c7ca22635e7024e059857b7": "v1.0.4", + "fc4e5c4dc2e5127b6814a3f69424c936f1dc241d1daf2c5a2d8f0728eb69d20d": "v1.0.4", + "e45f587fb07533d832548402d0e71d8e8234881da54d86c4b699c28a6482b0ee": "v1.1.0", + "9bf1580d1b21250f922b68794cdadd6c8e166ae5b15ce160a42f8c44a2f05936": "v2.0.0", + "e1ad2667d1924e4ddbeb623bd6939e94114d8471b84f8fb056e0c9abf0c4e4f4": "v2.1.0", + "a3f8c745ff33cd92a7e95d37c76c65523d258a70352ea44a232038ec4ec38dea": "v2.1.1", + "3b97596ed612aa29a74a7f51f33ea85fd6e0cfe7340dfbb96f0c17077b363498": "v2.1.2", + "e6685ab14844d0a381d658d77e13d6145fe7ae80469e5a5360210ae9c3447a77": "v2.1.3", + "fe98454e7ebd4aef4a6db5bd4c60f52cf3f58b974283a7c1e1fcc5fea02cf3eb": "v2.1.4" + }, + "firmware": { + "cac0256bd334fee270547c99ca77af1934863a95151b8dcac726c84da585b22f": "v7.9.2", + "9e691874bb6966aa0616d36b60489b82fab166d96e5166518eaa3e11468bf6a8": "v7.9.1", + "387ec4c8d3dcc83df8707aa0129eeb44e824c3797fb629a493be845327669da1": "v7.9.0", + "31c1cdd945a7331e01b3cced866cb28add5b49eef87c2bbc08370e5aa7daf9bf": "v7.8.0", + "1eb79470f73e40464d5e689e5008dddb47e7eb53bc87c50b1de4f3f150ed36bf": "v7.7.0", + "b4022a002278d1c00ccea54eb4d03934542ac509d03d77c0b7a8b8485b731f11": "v7.6.0", + "a94ba237468243929e0363a1bd2f48914580abfe2a90abbb533b0a201c434d54": "v7.5.2", + "b1083a159b6ef4a576574d09c86255d02d634a41f0380ac71ecd1275bf297c09": "v7.5.1", + "08b1153a6e9ba5f45776094d62c8d055632d414a38f0c70acd1e751229bf097c": "v7.5.0", + "43472b6fc1a3c9a2546ba771af830005f5758acbd9ea0679d4f20d480f63a040": "v7.4.0", + "efcdcb32f199110e9a38010bc48d2acc66da89d41fb30c7d0b64c1ef74c90359": "v7.3.2", + "47f3ead32f7be5926018163a2324f7dd1c47ef0b4cebec9bd8ae380d0a803314": "v7.3.1", + "28932f4ee19f88936c76fb5179e3d680443fede2fa782da0e674988963190a96": "v7.3.0", + "c6cf79e7c2cc1b9cf7eca57aacaab5310b4dd0eff1559cda307295d753251eff": "v7.2.1", + "72838adfe3762760dbbbadd74c8914b783660ea0ef3b8fe340e4a663442c5549": "v7.1.8", + "2b7edd319536076e0a00058d0cfd1b1863c8d616ba5851668796d04966df8594": "v7.1.7", + "53adf5693f7c23bc30c8cad8acc6ea939b063e205a4f4201eb9bd24fdc8285f0": "v7.1.5", + "7a52fa75be2e3e9794c4a01e74fc2a68cd502aace13fca1f272d5296156f1499": "v7.1.4", + "aa5834bb591c40dffd5e083797fe25e6d5591199a781220025fa469a965d0279": "v7.1.2", + "eb3d8853d549671dee532b51363cffdfa2038bc7730117e72dc17bb1452de4db": "v7.1.1", + "d8b2b43eada45ded399f347289750a7083081186b37158b85eab41a38cbc6e50": "v7.1.0", + "6a5e2bcf98aeafbb2faa98ea425ac066a7b4733e5b9edb29e544bad659cb3766": "v7.0.3", + "24071db7596f0824e51ce971c1ec39ac5a07e7a5bcaf5f1b33313de844e25580": "v6.7.0", + "85a44f1872b4b4ed0d5ff062711cfd4d4d69d9274312c9e3780b7db8da9072e8": "v6.6.0", + "89d1b5230bbca2e02901b091cbd77207d0636e6f1956f6f27a0ecb10c43cce3d": "v6.5.1", + "0ef1b51a450fafd8e0586103bda38526c5d012fc260618b8df5437cba7682c5b": "v6.4.0", + "0e2463b777f39dc8b450aca78f55b3355e906c69d19b59e84052786c5fa8f78c": "v6.3.0", + "5bcbeecea0a1c78cbd11344bb31c809072a01cb775f1e42368ef275888012208": "v6.2.2", + "0158073bb527b3b14148641722e77346ecec66a12fc4a5b4457dc0559c63169e": "v6.2.0", + "f9dfd903e6d4d8189409a72b9d31897ca1753a4000a24cc1c9217f4b8141403c": "v6.1.1", + "4246ff0e1b71a2a6b3e89e2cfd0882dc207f96b2516640d6c5fff406c02097bf": "v6.1.0", + "61c157a7fbc22f4d9825909ac067277a94e44c174e77db419fbb78b361fbf4ea": "v6.0.4", + "14cf71b0872a5c3cda1af2007aafd9bd0d5401be927e08e5b226fe764334d515": "v6.0.2", + "699f75ae5936977bf4f9df0478afe40106ea21bc2d94746bbe244a7832d4c5ca": "v6.0.1", + "d380357b7403064d7b1ea963dc56032239541a21ef0b7e08082fb36ed470de82": "v6.0.0", + "a05b992c1cadb151117704a03af8b7020482061200ce7bc72f90e8e4aba01a4f": "v5.11.0" + } + }, + "links": { + "app": "https://keepkey.com", + "support": "https://support.keepkey.info", + "updater": "https://github.com/keepkey/keepkey-desktop/releases/latest" + }, + "strings": { + "goToApp": "Head over to KeepKey.com!", + "updateUpdater": "Update Available" + } +} diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.0.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v6.0.0/firmware.keepkey.bin new file mode 100644 index 0000000..3dd17b0 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.0.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.0.0/keepkey-firmware-6.0.0.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/v6.0.0/keepkey-firmware-6.0.0.tar.gz new file mode 100644 index 0000000..5c6e1d3 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.0.0/keepkey-firmware-6.0.0.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.0.0/keepkey-firmware-6.0.0.zip b/projects/vault-v2/src-tauri/firmware_backup/v6.0.0/keepkey-firmware-6.0.0.zip new file mode 100644 index 0000000..8ee796e Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.0.0/keepkey-firmware-6.0.0.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.0.1/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v6.0.1/firmware.keepkey.bin new file mode 100644 index 0000000..134c581 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.0.1/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.0.1/keepkey-firmware-6.0.1.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/v6.0.1/keepkey-firmware-6.0.1.tar.gz new file mode 100644 index 0000000..18a2cef Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.0.1/keepkey-firmware-6.0.1.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.0.1/keepkey-firmware-6.0.1.zip b/projects/vault-v2/src-tauri/firmware_backup/v6.0.1/keepkey-firmware-6.0.1.zip new file mode 100644 index 0000000..805f751 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.0.1/keepkey-firmware-6.0.1.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.0.2/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v6.0.2/firmware.keepkey.bin new file mode 100644 index 0000000..c37c935 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.0.2/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.0.2/keepkey-firmware-6.0.2.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/v6.0.2/keepkey-firmware-6.0.2.tar.gz new file mode 100644 index 0000000..2d8c79f Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.0.2/keepkey-firmware-6.0.2.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.0.2/keepkey-firmware-6.0.2.zip b/projects/vault-v2/src-tauri/firmware_backup/v6.0.2/keepkey-firmware-6.0.2.zip new file mode 100644 index 0000000..2bb7a7f Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.0.2/keepkey-firmware-6.0.2.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.0.4/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v6.0.4/firmware.keepkey.bin new file mode 100644 index 0000000..6ae6955 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.0.4/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.0.4/keepkey-firmware-6.0.4.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/v6.0.4/keepkey-firmware-6.0.4.tar.gz new file mode 100644 index 0000000..9cbe35f Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.0.4/keepkey-firmware-6.0.4.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.0.4/keepkey-firmware-6.0.4.zip b/projects/vault-v2/src-tauri/firmware_backup/v6.0.4/keepkey-firmware-6.0.4.zip new file mode 100644 index 0000000..40400b6 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.0.4/keepkey-firmware-6.0.4.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.1.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v6.1.0/firmware.keepkey.bin new file mode 100644 index 0000000..d9da770 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.1.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.1.0/keepkey-firmware-6.1.0.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/v6.1.0/keepkey-firmware-6.1.0.tar.gz new file mode 100644 index 0000000..e537105 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.1.0/keepkey-firmware-6.1.0.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.1.0/keepkey-firmware-6.1.0.zip b/projects/vault-v2/src-tauri/firmware_backup/v6.1.0/keepkey-firmware-6.1.0.zip new file mode 100644 index 0000000..d3832fb Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.1.0/keepkey-firmware-6.1.0.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.1.1/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v6.1.1/firmware.keepkey.bin new file mode 100644 index 0000000..ff4d1f5 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.1.1/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.1.1/keepkey-firmware-6.1.1.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/v6.1.1/keepkey-firmware-6.1.1.tar.gz new file mode 100644 index 0000000..aad708c Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.1.1/keepkey-firmware-6.1.1.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.1.1/keepkey-firmware-6.1.1.zip b/projects/vault-v2/src-tauri/firmware_backup/v6.1.1/keepkey-firmware-6.1.1.zip new file mode 100644 index 0000000..213a03e Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.1.1/keepkey-firmware-6.1.1.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.2.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v6.2.0/firmware.keepkey.bin new file mode 100644 index 0000000..163e133 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.2.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.2.0/keepkey-firmware-6.2.0.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/v6.2.0/keepkey-firmware-6.2.0.tar.gz new file mode 100644 index 0000000..c21b536 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.2.0/keepkey-firmware-6.2.0.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.2.0/keepkey-firmware-6.2.0.zip b/projects/vault-v2/src-tauri/firmware_backup/v6.2.0/keepkey-firmware-6.2.0.zip new file mode 100644 index 0000000..9df2a89 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.2.0/keepkey-firmware-6.2.0.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.2.2/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v6.2.2/firmware.keepkey.bin new file mode 100644 index 0000000..9908ef4 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.2.2/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.2.2/keepkey-firmware-6.2.2.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/v6.2.2/keepkey-firmware-6.2.2.tar.gz new file mode 100644 index 0000000..d7017a4 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.2.2/keepkey-firmware-6.2.2.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.2.2/keepkey-firmware-6.2.2.zip b/projects/vault-v2/src-tauri/firmware_backup/v6.2.2/keepkey-firmware-6.2.2.zip new file mode 100644 index 0000000..2d2ea0f Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.2.2/keepkey-firmware-6.2.2.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.3.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v6.3.0/firmware.keepkey.bin new file mode 100644 index 0000000..234e083 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.3.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.3.0/keepkey-firmware-6.3.0.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/v6.3.0/keepkey-firmware-6.3.0.tar.gz new file mode 100644 index 0000000..e289046 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.3.0/keepkey-firmware-6.3.0.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.3.0/keepkey-firmware-6.3.0.zip b/projects/vault-v2/src-tauri/firmware_backup/v6.3.0/keepkey-firmware-6.3.0.zip new file mode 100644 index 0000000..1c5a33d Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.3.0/keepkey-firmware-6.3.0.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.4.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v6.4.0/firmware.keepkey.bin new file mode 100644 index 0000000..fb215d9 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.4.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.4.0/keepkey-firmware-6.4.0.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/v6.4.0/keepkey-firmware-6.4.0.tar.gz new file mode 100644 index 0000000..4c2fce2 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.4.0/keepkey-firmware-6.4.0.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.4.0/keepkey-firmware-6.4.0.zip b/projects/vault-v2/src-tauri/firmware_backup/v6.4.0/keepkey-firmware-6.4.0.zip new file mode 100644 index 0000000..7802d46 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.4.0/keepkey-firmware-6.4.0.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.5.1/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v6.5.1/firmware.keepkey.bin new file mode 100644 index 0000000..bfefa2e Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.5.1/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.5.1/keepkey-firmware-6.5.1.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/v6.5.1/keepkey-firmware-6.5.1.tar.gz new file mode 100644 index 0000000..2fb3f2c Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.5.1/keepkey-firmware-6.5.1.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.5.1/keepkey-firmware-6.5.1.zip b/projects/vault-v2/src-tauri/firmware_backup/v6.5.1/keepkey-firmware-6.5.1.zip new file mode 100644 index 0000000..6643c7b Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.5.1/keepkey-firmware-6.5.1.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.6.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v6.6.0/firmware.keepkey.bin new file mode 100644 index 0000000..9f8375d Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.6.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.6.0/keepkey-firmware-6.6.0.tar.gz b/projects/vault-v2/src-tauri/firmware_backup/v6.6.0/keepkey-firmware-6.6.0.tar.gz new file mode 100644 index 0000000..acd51ef Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.6.0/keepkey-firmware-6.6.0.tar.gz differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.6.0/keepkey-firmware-6.6.0.zip b/projects/vault-v2/src-tauri/firmware_backup/v6.6.0/keepkey-firmware-6.6.0.zip new file mode 100644 index 0000000..cab9487 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.6.0/keepkey-firmware-6.6.0.zip differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v6.7.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v6.7.0/firmware.keepkey.bin new file mode 100644 index 0000000..5b0714e Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v6.7.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.0.3/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.0.3/firmware.keepkey.bin new file mode 100644 index 0000000..31ec3cc Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.0.3/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.1.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.1.0/firmware.keepkey.bin new file mode 100644 index 0000000..076e654 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.1.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.1.1/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.1.1/firmware.keepkey.bin new file mode 100644 index 0000000..b7e68eb Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.1.1/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.1.2/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.1.2/firmware.keepkey.bin new file mode 100644 index 0000000..39a896b Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.1.2/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.1.4/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.1.4/firmware.keepkey.bin new file mode 100644 index 0000000..0e25364 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.1.4/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.1.7/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.1.7/firmware.keepkey.bin new file mode 100644 index 0000000..0767f86 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.1.7/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.1.8/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.1.8/firmware.keepkey.bin new file mode 100644 index 0000000..9d05718 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.1.8/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.10.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.10.0/firmware.keepkey.bin new file mode 100644 index 0000000..a369e11 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.10.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.2.1/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.2.1/firmware.keepkey.bin new file mode 100644 index 0000000..b61ccd4 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.2.1/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.3.2/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.3.2/firmware.keepkey.bin new file mode 100644 index 0000000..0a494ed Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.3.2/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.4.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.4.0/firmware.keepkey.bin new file mode 100644 index 0000000..911b921 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.4.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.5.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.5.0/firmware.keepkey.bin new file mode 100644 index 0000000..2f413ec Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.5.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.5.1/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.5.1/firmware.keepkey.bin new file mode 100644 index 0000000..16fbf79 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.5.1/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.5.2/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.5.2/firmware.keepkey.bin new file mode 100644 index 0000000..7675891 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.5.2/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.6.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.6.0/firmware.keepkey.bin new file mode 100644 index 0000000..efaca8d Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.6.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.7.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.7.0/firmware.keepkey.bin new file mode 100644 index 0000000..39f53de Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.7.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.8.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.8.0/firmware.keepkey.bin new file mode 100644 index 0000000..a92f477 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.8.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.9.0/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.9.0/firmware.keepkey.bin new file mode 100644 index 0000000..3155c93 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.9.0/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.9.1/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.9.1/firmware.keepkey.bin new file mode 100644 index 0000000..1edfb29 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.9.1/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.9.2/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.9.2/firmware.keepkey.bin new file mode 100644 index 0000000..a9a9550 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.9.2/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/firmware_backup/v7.9.3/firmware.keepkey.bin b/projects/vault-v2/src-tauri/firmware_backup/v7.9.3/firmware.keepkey.bin new file mode 100644 index 0000000..63db542 Binary files /dev/null and b/projects/vault-v2/src-tauri/firmware_backup/v7.9.3/firmware.keepkey.bin differ diff --git a/projects/vault-v2/src-tauri/src/commands.rs b/projects/vault-v2/src-tauri/src/commands.rs index 6deaa19..6a71abc 100644 --- a/projects/vault-v2/src-tauri/src/commands.rs +++ b/projects/vault-v2/src-tauri/src/commands.rs @@ -10,12 +10,14 @@ use hex; use std::io::Cursor; // Removed unused imports that were moved to device/updates.rs use crate::logging::{log_device_request, log_device_response, log_raw_device_message}; +use crate::device; use lazy_static; use std::path::PathBuf; use std::fs; use serde_json::Value; use log; -use std::time::Duration; +use std::time::{Duration, Instant}; +use once_cell::sync::Lazy; pub type DeviceQueueManager = Arc>>; @@ -58,6 +60,7 @@ pub struct BitcoinUtxoInput { pub amount: String, // Amount in satoshis as string pub vout: u32, // Output index pub txid: String, // Transaction ID + #[serde(alias = "hex")] // Accept both "prev_tx_hex" and "hex" field names pub prev_tx_hex: Option, // Raw previous transaction hex } @@ -337,7 +340,23 @@ pub async fn test_device_queue() -> Result { pub async fn get_device_status( device_id: String, queue_manager: State<'_, DeviceQueueManager>, + bootloader_tracker: State<'_, device::updates::BootloaderUpdateTracker>, ) -> Result, String> { + // Rate limit status checks - ignore rapid duplicate requests + static LAST_STATUS_CHECK: once_cell::sync::Lazy>>> = + once_cell::sync::Lazy::new(|| Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new()))); + + { + let mut last_checks = LAST_STATUS_CHECK.lock().await; + if let Some(last_check) = last_checks.get(&device_id) { + if last_check.elapsed() < Duration::from_millis(500) { + // Skip if checked within last 500ms + return Ok(None); + } + } + last_checks.insert(device_id.clone(), std::time::Instant::now()); + } + println!("Getting device status for: {}", device_id); let request_id = uuid::Uuid::new_v4().to_string(); @@ -373,41 +392,91 @@ pub async fn get_device_status( } }; - // Fetch device features through the queue - let features = match tokio::time::timeout( - Duration::from_secs(30), // Increased from 15 to 30 seconds to match Windows HID timeout - queue_handle.get_features() - ).await { - Ok(Ok(raw_features)) => { - // Convert from raw Features message to DeviceFeatures - Some(convert_features_to_device_features(raw_features)) + // Fetch device features through the queue with retry logic + let features = { + let mut last_error = None; + let mut success_features = None; + + // Check if we just did a bootloader update (device might be rebooting) + let just_updated_bootloader = { + let tracker = bootloader_tracker.read().await; + if let Some(update_time) = tracker.get(&device_id) { + // Check if update was within last 30 seconds + update_time.elapsed() < Duration::from_secs(30) + } else { + false + } + }; + + if just_updated_bootloader { + println!("πŸ”„ Device {} just completed bootloader update, using extended retry logic", device_id); } - Ok(Err(e)) => { - println!("Failed to get features for device {}: {}", device_id, e); - - // Log failed feature retrieval - let device_response_data = serde_json::json!({ - "error": format!("Failed to get features: {}", e), - "operation": "get_features_for_device" - }); + + let max_attempts = if just_updated_bootloader { 10 } else { 3 }; + + for attempt in 1..=max_attempts { + println!("πŸ”„ Attempting to get features for device {} (attempt {}/{})", device_id, attempt, max_attempts); - if let Err(log_err) = log_device_response(&device_id, &request_id, false, &device_response_data, Some(&format!("Failed to get features: {}", e))).await { - eprintln!("Failed to log device features error response: {}", log_err); + match tokio::time::timeout( + Duration::from_secs(10), // Reduced to 10 seconds per attempt for faster retries + queue_handle.get_features() + ).await { + Ok(Ok(raw_features)) => { + println!("βœ… Successfully got features for device {} on attempt {}", device_id, attempt); + // Convert from raw Features message to DeviceFeatures + success_features = Some(convert_features_to_device_features(raw_features)); + break; + } + Ok(Err(e)) => { + let error_detail = format!("{:?}", e); + println!("⚠️ Failed to get features for device {} on attempt {}: {}", device_id, attempt, error_detail); + + // Check for specific error conditions + if error_detail.contains("PinRequired") || error_detail.contains("pin") { + last_error = Some("Device requires PIN unlock".to_string()); + } else if error_detail.contains("Bootloader") || error_detail.contains("bootloader") { + last_error = Some("Device is in bootloader mode".to_string()); + } else if error_detail.contains("Busy") || error_detail.contains("busy") { + last_error = Some("Device is busy, please wait".to_string()); + } else { + last_error = Some(format!("Device error: {}", e)); + } + } + Err(_) => { + println!("⏱️ Timeout getting features for device {} on attempt {} (10s timeout)", device_id, attempt); + last_error = Some("Device operation timed out".to_string()); + } } - None + // Wait before retrying (exponential backoff) + if attempt < max_attempts { + let delay_ms = if just_updated_bootloader { + // Longer delays after bootloader update (2s, 3s, 4s, etc.) + 2000 + (1000 * attempt as u64) + } else { + // Normal delays (500ms, 1000ms, 1500ms) + 500 * attempt as u64 + }; + println!("⏳ Waiting {}ms before retry for device {}", delay_ms, device_id); + tokio::time::sleep(Duration::from_millis(delay_ms)).await; + } } - Err(_) => { - println!("Timeout getting features for device {}", device_id); + + // Handle final result + if let Some(features) = success_features { + Some(features) + } else { + // Log the final failure + let error_msg = last_error.unwrap_or_else(|| "Unknown error".to_string()); + println!("❌ All attempts failed for device {}: {}", device_id, error_msg); - // Log timeout let device_response_data = serde_json::json!({ - "error": "Timeout getting features", + "error": error_msg, "operation": "get_features_for_device" }); - if let Err(e) = log_device_response(&device_id, &request_id, false, &device_response_data, Some("Timeout getting features")).await { - eprintln!("Failed to log device features timeout response: {}", e); + if let Err(log_err) = log_device_response(&device_id, &request_id, false, &device_response_data, Some(&error_msg)).await { + eprintln!("Failed to log device features error response: {}", log_err); } None @@ -1026,53 +1095,69 @@ pub async fn get_connected_devices_with_features( } }; - // Try to fetch features through the queue - let features = match tokio::time::timeout( - Duration::from_secs(30), // Increased from 15 to 30 seconds to match Windows HID timeout - queue_handle.get_features() - ).await { - Ok(Ok(raw_features)) => { - // Convert from raw Features message to DeviceFeatures - let device_features = convert_features_to_device_features(raw_features); - - // Log successful feature retrieval - let device_response_data = serde_json::json!({ - "features": device_features, - "operation": "get_features_for_device" - }); + // Try to fetch features through the queue with retry logic + let features = { + let mut last_error = None; + let mut success_features = None; + + for attempt in 1..=3 { + println!("πŸ”„ Attempting to get features for device {} (attempt {}/3)", device_id, attempt); - if let Err(e) = log_device_response(&device_id, &device_request_id, true, &device_response_data, None).await { - eprintln!("Failed to log device features response: {}", e); + match tokio::time::timeout( + Duration::from_secs(30), // 30 seconds per attempt + queue_handle.get_features() + ).await { + Ok(Ok(raw_features)) => { + println!("βœ… Successfully got features for device {} on attempt {}", device_id, attempt); + // Convert from raw Features message to DeviceFeatures + let device_features = convert_features_to_device_features(raw_features); + + // Log successful feature retrieval + let device_response_data = serde_json::json!({ + "features": device_features, + "operation": "get_features_for_device" + }); + + if let Err(e) = log_device_response(&device_id, &device_request_id, true, &device_response_data, None).await { + eprintln!("Failed to log device features response: {}", e); + } + + success_features = Some(device_features); + break; + } + Ok(Err(e)) => { + println!("⚠️ Failed to get features for device {} on attempt {}: {}", device_id, attempt, e); + last_error = Some(format!("Failed to get features: {}", e)); + } + Err(_) => { + println!("⏱️ Timeout getting features for device {} on attempt {}", device_id, attempt); + last_error = Some("Timeout getting features".to_string()); + } } - Some(device_features) - } - Ok(Err(e)) => { - println!("Failed to get features for device {}: {}", device_id, e); - - // Log failed feature retrieval - let device_response_data = serde_json::json!({ - "error": format!("Failed to get features: {}", e), - "operation": "get_features_for_device" - }); - - if let Err(log_err) = log_device_response(&device_id, &device_request_id, false, &device_response_data, Some(&format!("Failed to get features: {}", e))).await { - eprintln!("Failed to log device features error response: {}", log_err); + // Wait before retrying (exponential backoff) + if attempt < 3 { + let delay_ms = 500 * attempt as u64; // 500ms, 1000ms + println!("⏳ Waiting {}ms before retry for device {}", delay_ms, device_id); + tokio::time::sleep(Duration::from_millis(delay_ms)).await; } - - None } - Err(_) => { - println!("Timeout getting features for device {}", device_id); + + // Handle final result + if let Some(features) = success_features { + Some(features) + } else { + // Log the final failure + let error_msg = last_error.unwrap_or_else(|| "Unknown error".to_string()); + println!("❌ All attempts failed for device {}: {}", device_id, error_msg); - // Log timeout let device_response_data = serde_json::json!({ - "error": "Timeout getting features", + "error": error_msg, "operation": "get_features_for_device" }); - if let Err(e) = log_device_response(&device_id, &device_request_id, false, &device_response_data, Some("Timeout getting features")).await { - eprintln!("Failed to log device features timeout response: {}", e); + if let Err(log_err) = log_device_response(&device_id, &device_request_id, false, &device_response_data, Some(&error_msg)).await { + eprintln!("Failed to log device features error response: {}", log_err); } None @@ -1152,9 +1237,10 @@ pub fn evaluate_device_status(device_id: String, features: Option<&DeviceFeature } } else { // Device is in normal firmware mode - check if it's an OOB device - if features.version.starts_with("1.0.") { - // OOB device: firmware version 1.0.3 = bootloader version 1.0.3 - features.version.clone() + if features.version.starts_with("1.0.") || features.version == "4.0.0" { + // OOB device: firmware version 1.0.3 or 4.0.0 = old bootloader + // Firmware 4.0.0 is known to have an old bootloader that needs updating + "1.0.3".to_string() // OOB devices have old bootloaders } else if let Some(ref bl_version) = features.bootloader_version { // Use explicit bootloader version if available bl_version.clone() @@ -1216,9 +1302,10 @@ pub fn evaluate_device_status(device_id: String, features: Option<&DeviceFeature } else { // Device is in normal firmware mode - use the actual firmware version let current_fw_version = features.version.clone(); - let needs_update = if current_fw_version.starts_with("1.0.") { + let needs_update = if current_fw_version.starts_with("1.0.") || current_fw_version == "4.0.0" { // OOB device - firmware update only after bootloader update - false // Bootloader has higher priority + // Firmware 4.0.0 is an OOB firmware that needs bootloader update first + true // Both bootloader and firmware need updates } else { !current_fw_version.starts_with("7.10.") }; @@ -1399,8 +1486,24 @@ pub fn parse_transaction_from_hex(hex_data: &str) -> Result<((u32, u32, u32, u32 // Parse version (4 bytes, little-endian) let version = read_u32_le(&mut cursor)?; - // Parse input count (varint) - let input_count = read_varint(&mut cursor)?; + // Check for SegWit marker and flag + let mut is_segwit = false; + let input_count = { + let first_byte = read_varint(&mut cursor)?; + if first_byte == 0 { + // This might be SegWit marker (0x00) followed by flag (0x01) + let flag = read_u8(&mut cursor)?; + if flag == 1 { + is_segwit = true; + // Now read the actual input count + read_varint(&mut cursor)? + } else { + return Err("Invalid transaction format: unexpected marker/flag".to_string()); + } + } else { + first_byte + } + }; // Parse inputs let mut inputs = Vec::new(); @@ -1460,12 +1563,31 @@ pub fn parse_transaction_from_hex(hex_data: &str) -> Result<((u32, u32, u32, u32 }); } + // If this is a SegWit transaction, skip witness data + if is_segwit { + // Skip witness data for each input + for _ in 0..input_count { + let witness_count = read_varint(&mut cursor)?; + for _ in 0..witness_count { + let witness_len = read_varint(&mut cursor)? as usize; + let mut witness_data = vec![0u8; witness_len]; + read_exact(&mut cursor, &mut witness_data)?; + } + } + } + // Parse lock time (4 bytes, little-endian) let lock_time = read_u32_le(&mut cursor)?; Ok(((version, input_count as u32, output_count as u32, lock_time), inputs, outputs)) } +fn read_u8(cursor: &mut Cursor>) -> Result { + let mut buf = [0u8; 1]; + read_exact(cursor, &mut buf)?; + Ok(buf[0]) +} + fn read_u32_le(cursor: &mut Cursor>) -> Result { let mut buf = [0u8; 4]; read_exact(cursor, &mut buf)?; @@ -3809,27 +3931,29 @@ pub async fn test_oob_device_status_evaluation() -> Result { pub async fn check_device_pin_ready( device_id: String, queue_manager: tauri::State<'_, DeviceQueueManager>, + bootloader_tracker: tauri::State<'_, device::updates::BootloaderUpdateTracker>, ) -> Result { log::info!("Checking if device {} is ready for PIN operations", device_id); - // Check if device is already in PIN flow + // Check if device is already in PIN flow - this means PIN is ready if is_device_in_pin_flow(&device_id) { - log::info!("Device {} is already in PIN flow", device_id); + log::info!("Device {} is already in PIN flow - PIN is ready", device_id); return Ok(true); } // Get device status first - let device_status = get_device_status(device_id.clone(), queue_manager.clone()).await?; + let device_status = get_device_status(device_id.clone(), queue_manager.clone(), bootloader_tracker).await?; match device_status { Some(status) => { - // Device must be connected and need PIN unlock + // Device must be connected if !status.connected { log::info!("Device {} is not connected", device_id); return Ok(false); } - if !status.needs_pin_unlock { + // If device doesn't need PIN unlock and isn't in PIN flow, then PIN is not needed + if !status.needs_pin_unlock && !is_device_in_pin_flow(&device_id) { log::info!("Device {} does not need PIN unlock", device_id); return Ok(false); } @@ -3853,4 +3977,28 @@ pub async fn check_device_pin_ready( Ok(false) } } +} + +/// Clear all device-related caches (used for backend restart) +pub async fn clear_all_device_caches() { + // Clear PIN flow devices + if let Ok(mut pin_flows) = DEVICE_PIN_FLOWS.lock() { + println!(" πŸ“‹ Clearing {} device PIN flow(s)", pin_flows.len()); + pin_flows.clear(); + } + + // Clear PIN sessions + if let Ok(mut pin_sessions) = PIN_SESSIONS.lock() { + println!(" πŸ“‹ Clearing {} PIN session(s)", pin_sessions.len()); + pin_sessions.clear(); + } + + // Clear frontend ready state and queued events + let mut state = FRONTEND_READY_STATE.write().await; + println!(" πŸ“‹ Clearing {} queued event(s)", state.queued_events.len()); + state.queued_events.clear(); + // Don't reset is_ready as frontend is still connected + drop(state); // Explicitly drop to release the lock + + println!(" βœ… All device caches cleared"); } \ No newline at end of file diff --git a/projects/vault-v2/src-tauri/src/device/mod.rs b/projects/vault-v2/src-tauri/src/device/mod.rs index 95dc695..692f03e 100644 --- a/projects/vault-v2/src-tauri/src/device/mod.rs +++ b/projects/vault-v2/src-tauri/src/device/mod.rs @@ -1,2 +1,5 @@ pub mod queue; -pub mod updates; \ No newline at end of file +pub mod updates; + +// Re-export the bootloader update tracker +pub use updates::BootloaderUpdateTracker; \ No newline at end of file diff --git a/projects/vault-v2/src-tauri/src/device/queue.rs b/projects/vault-v2/src-tauri/src/device/queue.rs index b48e687..559a34e 100644 --- a/projects/vault-v2/src-tauri/src/device/queue.rs +++ b/projects/vault-v2/src-tauri/src/device/queue.rs @@ -77,37 +77,62 @@ pub async fn add_to_device_queue( }; // ------------------------------------------------------------------ - // -------------------------------------------------------------- + // Check if device is in PIN flow BEFORE doing anything else + // ------------------------------------------------------------------ + if crate::commands::is_device_in_pin_flow(&request.device_id) { + // Don't interrupt PIN flow with ANY device operations + match &request.request { + DeviceRequest::GetXpub { .. } | + DeviceRequest::GetAddress { .. } | + DeviceRequest::SignTransaction { .. } => { + println!("🚫 Blocking request during PIN flow - device is entering PIN"); + return Err("Device is currently in PIN entry mode. Please complete PIN entry first.".to_string()); + }, + _ => { + // Allow GetFeatures and SendRaw during PIN flow as they might be needed + } + } + } + + // ------------------------------------------------------------------ // Pre-flight status check – ensure the device can service this request // ------------------------------------------------------------------ - // We fetch the current features via the queue (which opens a temporary - // transport) so that we have accurate mode/version information. - let raw_features_opt = match keepkey_rust::device_queue::DeviceQueueHandle::get_features(&queue_handle).await { - Ok(f) => { - // Successfully got features, update cache - let mut cache = DEVICE_STATE_CACHE.write().await; - cache.insert(request.device_id.clone(), DeviceStateCache { - is_oob_bootloader: false, - last_features: Some(f.clone()), - last_update: std::time::Instant::now(), - }); - Some(f) - }, - Err(e) => { - eprintln!("⚠️ Unable to fetch features for status check: {e}"); - - // Check if we have cached state for this device - let cache = DEVICE_STATE_CACHE.read().await; - if let Some(cached_state) = cache.get(&request.device_id) { - // If we know this is an OOB bootloader from a previous successful check - if cached_state.is_oob_bootloader { - println!("πŸ“‹ Using cached OOB bootloader state for device {}", request.device_id); - cached_state.last_features.clone() + // Skip GetFeatures if device is in PIN flow to avoid interrupting the PIN screen + let raw_features_opt = if crate::commands::is_device_in_pin_flow(&request.device_id) { + println!("⚠️ Skipping GetFeatures check - device is in PIN flow"); + // Check cache for last known features + let cache = DEVICE_STATE_CACHE.read().await; + cache.get(&request.device_id).and_then(|state| state.last_features.clone()) + } else { + // We fetch the current features via the queue (which opens a temporary + // transport) so that we have accurate mode/version information. + match keepkey_rust::device_queue::DeviceQueueHandle::get_features(&queue_handle).await { + Ok(f) => { + // Successfully got features, update cache + let mut cache = DEVICE_STATE_CACHE.write().await; + cache.insert(request.device_id.clone(), DeviceStateCache { + is_oob_bootloader: false, + last_features: Some(f.clone()), + last_update: std::time::Instant::now(), + }); + Some(f) + }, + Err(e) => { + eprintln!("⚠️ Unable to fetch features for status check: {e}"); + + // Check if we have cached state for this device + let cache = DEVICE_STATE_CACHE.read().await; + if let Some(cached_state) = cache.get(&request.device_id) { + // If we know this is an OOB bootloader from a previous successful check + if cached_state.is_oob_bootloader { + println!("πŸ“‹ Using cached OOB bootloader state for device {}", request.device_id); + cached_state.last_features.clone() + } else { + None + } } else { None } - } else { - None } } }; @@ -323,38 +348,48 @@ pub async fn add_to_device_queue( // Build transaction map with previous transactions and unsigned transaction let mut tx_map = std::collections::HashMap::new(); - // Cache previous transactions + // Cache previous transactions (only required for legacy inputs) for (idx, input) in inputs.iter().enumerate() { + // Only legacy (p2pkh) inputs require previous transaction hex + // SegWit inputs (p2sh, p2sh-p2wpkh, p2wpkh) do NOT need hex + let needs_hex = input.script_type == "p2pkh"; + if let Some(hex_data) = &input.prev_tx_hex { - let tx_hash = hex::decode(&input.txid).map_err(|e| format!("Invalid txid hex: {}", e))?; - let tx_hash_hex = hex::encode(&tx_hash); - - // Parse the previous transaction from hex - match parse_transaction_from_hex(hex_data) { - Ok((metadata, tx_inputs, tx_outputs)) => { - let tx = keepkey_rust::messages::TransactionType { - version: Some(metadata.0), - lock_time: Some(metadata.3), - inputs_cnt: Some(metadata.1), - outputs_cnt: Some(metadata.2), - inputs: tx_inputs, - bin_outputs: tx_outputs, - outputs: vec![], - extra_data: None, - extra_data_len: Some(0), - ..Default::default() - }; - tx_map.insert(tx_hash_hex.clone(), tx); - println!("βœ… Cached previous transaction: {} (v{}, {} inputs, {} outputs)", - tx_hash_hex, metadata.0, metadata.1, metadata.2); - } - Err(e) => { - eprintln!("⚠️ Failed to parse previous transaction for input {}: {}", idx, e); - return Err(format!("Failed to parse previous transaction for input {}: {}", idx, e)); + if !hex_data.is_empty() { + let tx_hash = hex::decode(&input.txid).map_err(|e| format!("Invalid txid hex: {}", e))?; + let tx_hash_hex = hex::encode(&tx_hash); + + // Parse the previous transaction from hex + match parse_transaction_from_hex(hex_data) { + Ok((metadata, tx_inputs, tx_outputs)) => { + let tx = keepkey_rust::messages::TransactionType { + version: Some(metadata.0), + lock_time: Some(metadata.3), + inputs_cnt: Some(metadata.1), + outputs_cnt: Some(metadata.2), + inputs: tx_inputs, + bin_outputs: tx_outputs, + outputs: vec![], + extra_data: None, + extra_data_len: Some(0), + ..Default::default() + }; + tx_map.insert(tx_hash_hex.clone(), tx); + println!("βœ… Cached previous transaction for legacy input: {} (v{}, {} inputs, {} outputs)", + tx_hash_hex, metadata.0, metadata.1, metadata.2); + } + Err(e) => { + eprintln!("⚠️ Failed to parse previous transaction for input {}: {}", idx, e); + return Err(format!("Failed to parse previous transaction for input {}: {}", idx, e)); + } } + } else if needs_hex { + return Err(format!("Legacy input {} missing required previous transaction hex", idx)); } + } else if needs_hex { + return Err(format!("Legacy input {} missing required previous transaction hex", idx)); } else { - return Err(format!("Input {} missing previous transaction hex", idx)); + println!("⚑ SegWit input {} ({}): no hex required", idx, input.script_type); } } @@ -363,7 +398,7 @@ pub async fn add_to_device_queue( for input in inputs { let script_type = match input.script_type.as_str() { "p2pkh" => keepkey_rust::messages::InputScriptType::Spendaddress, - "p2sh-p2wpkh" => keepkey_rust::messages::InputScriptType::Spendp2shwitness, + "p2sh" | "p2sh-p2wpkh" => keepkey_rust::messages::InputScriptType::Spendp2shwitness, "p2wpkh" => keepkey_rust::messages::InputScriptType::Spendwitness, _ => keepkey_rust::messages::InputScriptType::Spendaddress, }; diff --git a/projects/vault-v2/src-tauri/src/device/updates.rs b/projects/vault-v2/src-tauri/src/device/updates.rs index 5b2c68a..e1adf4e 100644 --- a/projects/vault-v2/src-tauri/src/device/updates.rs +++ b/projects/vault-v2/src-tauri/src/device/updates.rs @@ -3,15 +3,22 @@ use std::fs; use std::path::PathBuf; use semver::Version; use uuid; +use std::sync::Arc; +use tokio::sync::RwLock; +use std::collections::HashMap; use crate::logging::{log_device_request, log_device_response}; use crate::commands::DeviceQueueManager; +// Track devices that just completed bootloader updates +pub type BootloaderUpdateTracker = Arc>>; + /// Update device bootloader using the device queue #[tauri::command] pub async fn update_device_bootloader( device_id: String, target_version: String, queue_manager: State<'_, DeviceQueueManager>, + bootloader_tracker: State<'_, BootloaderUpdateTracker>, ) -> Result { println!("πŸ”„ Starting bootloader update for device {}: target version {}", device_id, target_version); @@ -258,6 +265,8 @@ pub async fn update_device_bootloader( match queue_handle.update_bootloader(target_version.clone(), bootloader_bytes).await { Ok(success) => { println!("βœ… Bootloader update successful for device {}", device_id); + println!("⚠️ Note: The device will now reboot. It will disconnect and reconnect automatically."); + println!(" The frontend should wait for the device:connected event before proceeding."); // Log the successful response let response_data = serde_json::json!({ @@ -270,6 +279,21 @@ pub async fn update_device_bootloader( eprintln!("Failed to log bootloader update success response: {}", e); } + // Track that this device just completed a bootloader update + { + let mut tracker = bootloader_tracker.write().await; + tracker.insert(device_id.clone(), std::time::Instant::now()); + println!("πŸ“ Marked device {} as having just completed bootloader update", device_id); + } + + // Clean up the device queue handle as the device will disconnect + { + let mut manager = queue_manager.lock().await; + if manager.remove(&device_id).is_some() { + println!("♻️ Cleaned up device queue for {} after bootloader update", device_id); + } + } + Ok(success) } Err(e) => { @@ -441,19 +465,44 @@ pub async fn update_device_firmware( println!("πŸ“¦ Loaded firmware binary: {} bytes", firmware_bytes.len()); - // Get or create device queue handle + // Get or create device queue handle with retry logic for reconnecting devices let queue_handle = { let mut manager = queue_manager.lock().await; if let Some(handle) = manager.get(&device_id) { handle.clone() } else { - // Find the device by ID - let devices = keepkey_rust::features::list_connected_devices(); - let device_info = devices - .iter() - .find(|d| d.unique_id == device_id); + // Device might be reconnecting after bootloader update, try with retries + let mut device_info = None; + let max_retries = 5; + + for retry in 0..max_retries { + if retry > 0 { + // Wait before retry to allow device to reconnect + println!("⏳ Waiting for device {} to reconnect (attempt {}/{})", device_id, retry + 1, max_retries); + drop(manager); // Release lock during sleep + tokio::time::sleep(tokio::time::Duration::from_millis(1000 * retry as u64)).await; + manager = queue_manager.lock().await; + + // Check again if handle was created by event controller + if let Some(handle) = manager.get(&device_id) { + println!("βœ… Device queue handle found after waiting"); + break; + } + } + let devices = keepkey_rust::features::list_connected_devices(); + device_info = devices + .iter() + .find(|d| d.unique_id == device_id) + .cloned(); + + if device_info.is_some() { + println!("βœ… Device {} found on attempt {}", device_id, retry + 1); + break; + } + } + match device_info { Some(device_info) => { // Spawn a new device worker @@ -462,19 +511,24 @@ pub async fn update_device_firmware( handle } None => { - let error = format!("Device {} not found", device_id); - - // Log the error response - let response_data = serde_json::json!({ - "error": error, - "operation": "update_device_firmware" - }); - - if let Err(e) = log_device_response(&device_id, &request_id, false, &response_data, Some(&error)).await { - eprintln!("Failed to log firmware update error response: {}", e); + // Check if handle was created while we were waiting + if let Some(handle) = manager.get(&device_id) { + handle.clone() + } else { + let error = format!("Device {} not found after {} retries", device_id, max_retries); + + // Log the error response + let response_data = serde_json::json!({ + "error": error, + "operation": "update_device_firmware" + }); + + if let Err(e) = log_device_response(&device_id, &request_id, false, &response_data, Some(&error)).await { + eprintln!("Failed to log firmware update error response: {}", e); + } + + return Err(error); } - - return Err(error); } } } @@ -542,6 +596,8 @@ pub async fn update_device_firmware( match queue_handle.update_firmware(target_version.clone(), firmware_bytes).await { Ok(success) => { println!("βœ… Firmware update successful for device {}", device_id); + println!("⚠️ Note: The device will now reboot. It will disconnect and reconnect automatically."); + println!(" The frontend should wait for the device:connected event before proceeding."); // Log the successful response let response_data = serde_json::json!({ @@ -554,6 +610,14 @@ pub async fn update_device_firmware( eprintln!("Failed to log firmware update success response: {}", e); } + // Clean up the device queue handle as the device will disconnect + { + let mut manager = queue_manager.lock().await; + if manager.remove(&device_id).is_some() { + println!("♻️ Cleaned up device queue for {} after firmware update", device_id); + } + } + Ok(success) } Err(e) => { diff --git a/projects/vault-v2/src-tauri/src/event_controller.rs b/projects/vault-v2/src-tauri/src/event_controller.rs index c873b91..b4fac11 100644 --- a/projects/vault-v2/src-tauri/src/event_controller.rs +++ b/projects/vault-v2/src-tauri/src/event_controller.rs @@ -77,6 +77,17 @@ impl EventController { // Check for newly connected devices for device in ¤t_devices { if !last_devices.iter().any(|d| d.unique_id == device.unique_id) { + // Check if this is a duplicate of an already connected device + let is_duplicate = current_devices.iter().any(|other| { + other.unique_id != device.unique_id && + crate::commands::are_devices_potentially_same(&device.unique_id, &other.unique_id) + }); + + if is_duplicate { + println!("⚠️ Skipping duplicate device: {} (already connected with different ID)", device.unique_id); + continue; + } + println!("πŸ”Œ Device connected: {} (VID: 0x{:04x}, PID: 0x{:04x})", device.unique_id, device.vid, device.pid); println!(" Device info: {} - {}", @@ -126,6 +137,8 @@ impl EventController { let app_for_task = app_handle.clone(); let device_for_task = device.clone(); tokio::spawn(async move { + // Give device a moment to settle after connection + tokio::time::sleep(Duration::from_millis(500)).await; println!("πŸ“‘ Fetching device features for: {}", device_for_task.unique_id); // Emit getting features status @@ -464,42 +477,68 @@ async fn try_get_device_features(device: &FriendlyUsbDevice, app_handle: &AppHan return Err("Device entered PIN flow - aborting feature fetch".to_string()); } - // Try to get features with a timeout using the shared worker - match tokio::time::timeout(Duration::from_secs(30), queue_handle.get_features()).await { - Ok(Ok(raw_features)) => { - // Convert features to our DeviceFeatures format - let device_features = crate::commands::convert_features_to_device_features(raw_features); - Ok(device_features) + // Try to get features with retry logic for timeout resilience + let mut last_error = None; + for attempt in 1..=3 { + println!("πŸ”„ Attempting to get features for device {} (attempt {}/3)", device.unique_id, attempt); + + // Check PIN flow status before each attempt + if crate::commands::is_device_in_pin_flow(&device.unique_id) { + return Err("Device entered PIN flow during feature fetch".to_string()); } - Ok(Err(e)) => { - let error_str = e.to_string(); - - // Check if this looks like an OOB bootloader that doesn't understand GetFeatures - if error_str.contains("Unknown message") || - error_str.contains("Failure: Unknown message") || - error_str.contains("Unexpected response") { - - println!("πŸ”§ Device may be in OOB bootloader mode, trying Initialize message..."); + + match tokio::time::timeout(Duration::from_secs(5), queue_handle.get_features()).await { + Ok(Ok(raw_features)) => { + println!("βœ… Successfully got features for device {} on attempt {}", device.unique_id, attempt); + // Convert features to our DeviceFeatures format + let device_features = crate::commands::convert_features_to_device_features(raw_features); + return Ok(device_features); + } + Ok(Err(e)) => { + let error_str = e.to_string(); - // Try the direct approach using keepkey-rust's proven method - match try_oob_bootloader_detection(device).await { - Ok(features) => { - println!("βœ… Successfully detected OOB bootloader mode for device {}", device.unique_id); - Ok(features) - } - Err(oob_err) => { - println!("❌ OOB bootloader detection also failed for {}: {}", device.unique_id, oob_err); - Err(format!("Failed to get device features: {} (OOB attempt: {})", error_str, oob_err)) + // Check if this looks like an OOB bootloader that doesn't understand GetFeatures + if error_str.contains("Unknown message") || + error_str.contains("Failure: Unknown message") || + error_str.contains("Unexpected response") { + + println!("πŸ”§ Device may be in OOB bootloader mode, trying Initialize message..."); + + // Try the direct approach using keepkey-rust's proven method + match try_oob_bootloader_detection(device).await { + Ok(features) => { + println!("βœ… Successfully detected OOB bootloader mode for device {}", device.unique_id); + return Ok(features); + } + Err(oob_err) => { + println!("❌ OOB bootloader detection also failed for {}: {}", device.unique_id, oob_err); + last_error = Some(format!("Failed to get device features: {} (OOB attempt: {})", error_str, oob_err)); + } } + } else { + println!("⚠️ Failed to get features for device {} on attempt {}: {}", device.unique_id, attempt, error_str); + last_error = Some(format!("Failed to get device features: {}", error_str)); } - } else { - Err(format!("Failed to get device features: {}", error_str)) + } + Err(_) => { + println!("⏱️ Timeout getting features for device {} on attempt {}", device.unique_id, attempt); + last_error = Some("Timeout while fetching device features".to_string()); } } - Err(_) => { - Err("Timeout while fetching device features".to_string()) + + // Wait before retrying (exponential backoff) + if attempt < 3 { + let delay_ms = 500 * attempt as u64; // 500ms, 1000ms + println!("⏳ Waiting {}ms before retry for device {}", delay_ms, device.unique_id); + tokio::time::sleep(Duration::from_millis(delay_ms)).await; } } + + // All attempts failed + match last_error { + Some(err) => Err(err), + None => Err(format!("All feature fetch attempts failed for device {}", device.unique_id)) + } } else { // Fallback to the old method if queue manager is not available println!("⚠️ DeviceQueueManager not available, using fallback method"); diff --git a/projects/vault-v2/src-tauri/src/lib.rs b/projects/vault-v2/src-tauri/src/lib.rs index 24ddfa9..9e05b77 100644 --- a/projects/vault-v2/src-tauri/src/lib.rs +++ b/projects/vault-v2/src-tauri/src/lib.rs @@ -43,9 +43,14 @@ fn vault_open_support(app: tauri::AppHandle) -> Result<(), String> { "view": "browser" })).map_err(|e| format!("Failed to emit view change event: {}", e))?; - app.emit("browser:navigate", serde_json::json!({ - "url": "https://support.keepkey.com" - })).map_err(|e| format!("Failed to emit navigation event: {}", e))?; + // Add a small delay to ensure the browser view is mounted before navigation + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(100)); + + let _ = app.emit("browser:navigate", serde_json::json!({ + "url": "https://support.keepkey.com" + })); + }); Ok(()) } @@ -77,49 +82,105 @@ async fn open_url(app_handle: tauri::AppHandle, url: String) -> Result<(), Strin } #[tauri::command] -fn restart_backend_startup(app: tauri::AppHandle) -> Result<(), String> { - println!("Restarting backend startup process"); - // Emit event to indicate restart - match app.emit("application:state", serde_json::json!({ - "status": "Restarting...", +async fn restart_backend_startup(app: tauri::AppHandle) -> Result<(), String> { + println!("πŸ”„ PERFORMING COMPREHENSIVE BACKEND RESTART"); + + // Emit restart status + let _ = app.emit("application:state", serde_json::json!({ + "status": "Restarting backend services...", "connected": false, "features": null - })) { - Ok(_) => { - // Simulate restart process - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_millis(1000)); - let _ = app.emit("application:state", serde_json::json!({ - "status": "Device ready", - "connected": true, - "features": { - "label": "KeepKey", - "vendor": "KeepKey", - "model": "KeepKey", - "firmware_variant": "keepkey", - "device_id": "keepkey-001", - "language": "english", - "bootloader_mode": false, - "version": "7.7.0", - "firmware_hash": null, - "bootloader_hash": null, - "initialized": true, - "imported": false, - "no_backup": false, - "pin_protection": true, - "pin_cached": false, - "passphrase_protection": false, - "passphrase_cached": false, - "wipe_code_protection": false, - "auto_lock_delay_ms": null, - "policies": [] - } - })); - }); - Ok(()) - }, - Err(e) => Err(format!("Failed to emit restart event: {}", e)) + })); + + // 1. Clear all device queues + if let Some(queue_manager_state) = app.try_state::>>>() { + let mut manager = queue_manager_state.inner().lock().await; + println!(" πŸ“‹ Clearing {} device queue(s)...", manager.len()); + + // Note: Device workers will be cleaned up when dropped + for (device_id, _handle) in manager.iter() { + println!(" πŸ›‘ Removing device worker for: {}", device_id); + // Workers will be stopped when the handle is dropped + } + + // Clear the queue manager + manager.clear(); + println!(" βœ… All device queues cleared"); + } + + // 2. Clear response tracking + if let Some(responses_state) = app.try_state::>>>() { + let mut responses = responses_state.inner().lock().await; + println!(" πŸ“‹ Clearing {} cached response(s)...", responses.len()); + responses.clear(); + println!(" βœ… Response cache cleared"); + } + + // 3. Clear any cached device states + println!(" πŸ“‹ Clearing device state caches..."); + commands::clear_all_device_caches().await; + // The function will print its own completion message + + // 4. Stop and restart the event controller (if we had a handle to it) + // Note: Current implementation doesn't store event controller handle, + // but we can emit a signal to restart device scanning + println!(" πŸ”„ Triggering device rescan..."); + + // 5. Small delay to let everything settle + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + + // 6. Emit scanning status to trigger new device discovery + let _ = app.emit("application:state", serde_json::json!({ + "status": "Scanning for devices...", + "connected": false, + "features": null + })); + + // 7. Force a device rescan by listing devices + let devices = keepkey_rust::features::list_connected_devices(); + let device_count = devices.len(); + println!(" πŸ” Found {} device(s) after restart", device_count); + + // 8. Emit device events for any found devices + for device in devices { + println!(" πŸ“‘ Re-emitting device:connected for {}", device.unique_id); + let _ = app.emit("device:connected", &device); + + // Also trigger feature fetch for each device + let app_for_device = app.clone(); + let device_for_task = device.clone(); + tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + + // Try to get features for the device + println!(" πŸ” Attempting to get features for {} after restart", device_for_task.unique_id); + // Note: We'll let the event controller handle feature fetching + // Just emit the device connected event + let _ = app_for_device.emit("device:ready", serde_json::json!({ + "device": device_for_task, + "status": "reconnected_after_restart" + })); + }); + } + + println!("βœ… BACKEND RESTART COMPLETE"); + + // Final status update + if device_count == 0 { + let _ = app.emit("application:state", serde_json::json!({ + "status": "No devices found. Please connect your KeepKey.", + "connected": false, + "features": null + })); + } else { + let _ = app.emit("application:state", serde_json::json!({ + "status": format!("Found {} device(s)", device_count), + "connected": true, + "features": null + })); } + + Ok(()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -146,8 +207,14 @@ pub fn run() { std::collections::HashMap::::new() )); + // Initialize bootloader update tracker + let bootloader_tracker: device::BootloaderUpdateTracker = Arc::new(tokio::sync::RwLock::new( + std::collections::HashMap::::new() + )); + app.manage(device_queue_manager.clone()); app.manage(last_responses); + app.manage(bootloader_tracker); // Start event controller with proper management let _event_controller = event_controller::spawn_event_controller(&app.handle()); diff --git a/projects/vault-v2/src-tauri/src/logging.rs b/projects/vault-v2/src-tauri/src/logging.rs index 78fd527..d21b234 100644 --- a/projects/vault-v2/src-tauri/src/logging.rs +++ b/projects/vault-v2/src-tauri/src/logging.rs @@ -138,15 +138,36 @@ impl DeviceLogger { /// Write a log entry to the current log file async fn write_log_entry(&self, log_entry: &serde_json::Value) -> Result<(), String> { - let mut file = self.get_current_log_file().await?; + let current_date = Self::get_current_date(); - // Write the log entry as a JSON line - writeln!(file, "{}", serde_json::to_string(log_entry).unwrap()) - .map_err(|e| format!("Failed to write log entry: {}", e))?; + // Hold both locks for the entire write operation to prevent interleaving + let mut current_date_lock = self.current_date.lock().await; + let mut current_log_file_lock = self.current_log_file.lock().await; - // Flush to ensure it's written immediately - file.flush() - .map_err(|e| format!("Failed to flush log file: {}", e))?; + // Check if we need to create a new log file (new day or first time) + if *current_date_lock != current_date || current_log_file_lock.is_none() { + let log_file_path = self.logs_dir.join(format!("device-communications-{}.log", current_date)); + + let file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_file_path) + .map_err(|e| format!("Failed to open log file: {}", e))?; + + *current_date_lock = current_date; + *current_log_file_lock = Some(file); + } + + // Write to the file while holding the lock + if let Some(ref mut file) = *current_log_file_lock { + // Write the log entry as a JSON line + writeln!(file, "{}", serde_json::to_string(log_entry).unwrap()) + .map_err(|e| format!("Failed to write log entry: {}", e))?; + + // Flush to ensure it's written immediately + file.flush() + .map_err(|e| format!("Failed to flush log file: {}", e))?; + } Ok(()) } diff --git a/projects/vault-v2/src-tauri/src/server/mod.rs b/projects/vault-v2/src-tauri/src/server/mod.rs index 1e331a9..4cc385d 100644 --- a/projects/vault-v2/src-tauri/src/server/mod.rs +++ b/projects/vault-v2/src-tauri/src/server/mod.rs @@ -1,15 +1,17 @@ pub mod routes; pub mod context; +pub mod proxy; use axum::{ Router, serve, routing::{get, post}, + response::Json, }; use tokio::net::TcpListener; use tower_http::cors::CorsLayer; -use tracing::info; +use tracing::{info, debug}; use std::sync::Arc; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; @@ -78,6 +80,11 @@ pub async fn start_server(device_queue_manager: crate::commands::DeviceQueueMana // System endpoints .route("/api/health", get(routes::health_check)) + // Add compatibility route for Pioneer SDK kkapi detection + .route("/spec/swagger.json", get(|| async move { + Json(ApiDoc::openapi()) + })) + // Context endpoints - commented out until full device interaction is implemented // .route("/api/context", get(routes::api_get_context)) // .route("/api/context", post(routes::api_set_context)) @@ -94,19 +101,59 @@ pub async fn start_server(device_queue_manager: crate::commands::DeviceQueueMana .merge(swagger_ui) // Then add state and middleware .with_state(server_state) - .layer(CorsLayer::permissive()); + .layer( + CorsLayer::new() + // Allow any origin with wildcard (includes localhost:8080 proxy) + .allow_origin(tower_http::cors::Any) + // Allow all methods + .allow_methods(tower_http::cors::Any) + // Allow all headers including X-Requested-With for AJAX + .allow_headers(tower_http::cors::Any) + // Max age for preflight caching + .max_age(std::time::Duration::from_secs(3600)) + // Note: credentials cannot be used with wildcard origin + .allow_credentials(false) + ); let addr = "127.0.0.1:1646"; let listener = TcpListener::bind(addr).await?; - info!("πŸš€ Server started successfully:"); + // Start the proxy server on port 8080 + let proxy_addr = "127.0.0.1:8080"; + let proxy_app = proxy::create_proxy_router(); + let proxy_listener = TcpListener::bind(proxy_addr).await?; + + info!("πŸš€ Starting servers:"); info!(" πŸ“‹ REST API: http://{}/api", addr); + info!(" 🌍 Proxy: http://{} -> keepkey.com", proxy_addr); info!(" πŸ“š API Documentation: http://{}/docs", addr); - info!(" πŸ”Œ Device Management: http://{}/api/devices", addr); - info!(" πŸ€– MCP Endpoint: http://{}/mcp", addr); + debug!(" πŸ”Œ Device Management: http://{}/api/devices", addr); + debug!(" πŸ€– MCP Endpoint: http://{}/mcp", addr); + debug!(" πŸ“„ Swagger JSON: http://{}/spec/swagger.json", addr); + + // Start the proxy server in a separate task + let proxy_handle = tokio::spawn(async move { + serve(proxy_listener, proxy_app).await + }); - // Spawn the server - serve(listener, app).await?; + // Small delay to let proxy server start + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + info!("βœ… Both servers started successfully and are ready"); + + // Run both servers concurrently + tokio::select! { + result = serve(listener, app) => { + if let Err(e) = result { + tracing::error!("API server error: {}", e); + } + } + result = proxy_handle => { + if let Err(e) = result { + tracing::error!("Proxy server error: {}", e); + } + } + } Ok(()) } \ No newline at end of file diff --git a/projects/vault-v2/src-tauri/src/server/proxy.rs b/projects/vault-v2/src-tauri/src/server/proxy.rs new file mode 100644 index 0000000..1270493 --- /dev/null +++ b/projects/vault-v2/src-tauri/src/server/proxy.rs @@ -0,0 +1,698 @@ +use axum::{ + extract::{Host, Path as AxumPath, Query, Request}, + http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode}, + response::Response, + routing::{any, delete, get, head, options, patch, post, put}, + Router, + body::Body, +}; +use std::collections::HashMap; +use std::str::FromStr; +use reqwest; +use serde_json; +use regex::Regex; +use url; + +/// Create the proxy router with wildcard *.keepkey.com support +pub fn create_proxy_router() -> Router { + use tower_http::cors::CorsLayer; + + Router::new() + .route("/", get(proxy_root_handler).post(proxy_root_post_handler)) + .route("/*path", get(proxy_handler).post(proxy_post_handler).put(proxy_put_handler).delete(proxy_delete_handler).patch(proxy_patch_handler).options(proxy_options_handler).head(proxy_head_handler)) + .fallback(proxy_fallback_handler) + // Add CORS layer to proxy server as well + .layer( + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods(tower_http::cors::Any) + .allow_headers(tower_http::cors::Any) + .max_age(std::time::Duration::from_secs(3600)) + .allow_credentials(false) + ) +} + +/// Handle GET requests to the root path +async fn proxy_root_handler( + Host(host): Host, + Query(params): Query>, + headers: HeaderMap, +) -> Response { + let target_domain = determine_target_domain(&host, &headers); + tracing::info!("🌐 PROXY ROOT GET: / -> {}", target_domain); + proxy_keepkey_request("", Method::GET, params, headers, None, &target_domain).await +} + +/// Handle POST requests to the root path +async fn proxy_root_post_handler( + Host(host): Host, + Query(params): Query>, + headers: HeaderMap, + request: Request, +) -> Response { + let target_domain = determine_target_domain(&host, &headers); + tracing::info!("🌐 PROXY ROOT POST: / -> {}", target_domain); + let body = extract_body(request).await; + proxy_keepkey_request("", Method::POST, params, headers, body, &target_domain).await +} + +/// Handle GET requests to any path +async fn proxy_handler( + Host(host): Host, + AxumPath(path): AxumPath, + Query(params): Query>, + headers: HeaderMap, +) -> Response { + let target_domain = determine_target_domain(&host, &headers); + tracing::info!("🌐 PROXY GET: /{} -> {}/{}", path, target_domain, path); + proxy_keepkey_request(&path, Method::GET, params, headers, None, &target_domain).await +} + +/// Handle POST requests to any path +async fn proxy_post_handler( + Host(host): Host, + AxumPath(path): AxumPath, + Query(params): Query>, + headers: HeaderMap, + request: Request, +) -> Response { + let target_domain = determine_target_domain(&host, &headers); + tracing::info!("🌐 PROXY POST: /{} -> {}/{}", path, target_domain, path); + let body = extract_body(request).await; + proxy_keepkey_request(&path, Method::POST, params, headers, body, &target_domain).await +} + +/// Handle PUT requests to any path +async fn proxy_put_handler( + Host(host): Host, + AxumPath(path): AxumPath, + Query(params): Query>, + headers: HeaderMap, + request: Request, +) -> Response { + let target_domain = determine_target_domain(&host, &headers); + tracing::info!("🌐 PROXY PUT: /{} -> {}/{}", path, target_domain, path); + let body = extract_body(request).await; + proxy_keepkey_request(&path, Method::PUT, params, headers, body, &target_domain).await +} + +/// Handle DELETE requests to any path +async fn proxy_delete_handler( + Host(host): Host, + AxumPath(path): AxumPath, + Query(params): Query>, + headers: HeaderMap, + request: Request, +) -> Response { + let target_domain = determine_target_domain(&host, &headers); + tracing::info!("🌐 PROXY DELETE: /{} -> {}/{}", path, target_domain, path); + let body = extract_body(request).await; + proxy_keepkey_request(&path, Method::DELETE, params, headers, body, &target_domain).await +} + +/// Handle PATCH requests to any path +async fn proxy_patch_handler( + Host(host): Host, + AxumPath(path): AxumPath, + Query(params): Query>, + headers: HeaderMap, + request: Request, +) -> Response { + let target_domain = determine_target_domain(&host, &headers); + tracing::info!("🌐 PROXY PATCH: /{} -> {}/{}", path, target_domain, path); + let body = extract_body(request).await; + proxy_keepkey_request(&path, Method::PATCH, params, headers, body, &target_domain).await +} + +/// Handle OPTIONS requests to any path +async fn proxy_options_handler( + Host(host): Host, + AxumPath(path): AxumPath, + Query(params): Query>, + headers: HeaderMap, +) -> Response { + let target_domain = determine_target_domain(&host, &headers); + tracing::info!("🌐 PROXY OPTIONS: /{} -> {}/{}", path, target_domain, path); + proxy_keepkey_request(&path, Method::OPTIONS, params, headers, None, &target_domain).await +} + +/// Handle HEAD requests to any path +async fn proxy_head_handler( + Host(host): Host, + AxumPath(path): AxumPath, + Query(params): Query>, + headers: HeaderMap, +) -> Response { + let target_domain = determine_target_domain(&host, &headers); + tracing::info!("🌐 PROXY HEAD: /{} -> {}/{}", path, target_domain, path); + proxy_keepkey_request(&path, Method::HEAD, params, headers, None, &target_domain).await +} + +/// Fallback handler for any method/path combination +async fn proxy_fallback_handler( + request: Request, +) -> Response { + let method = request.method().clone(); + let uri = request.uri().clone(); + let path = uri.path(); + let headers = request.headers().clone(); + + // Extract host from headers if not available from extractor + let host = headers.get("host") + .and_then(|h| h.to_str().ok()) + .unwrap_or("localhost:8080"); + + let target_domain = determine_target_domain(host, &headers); + tracing::info!("🌐 PROXY FALLBACK: {} {} -> {}{}", method, path, target_domain, path); + + let query_params = extract_query_params(uri.query()); + let body = extract_body(request).await; + + proxy_keepkey_request(path.trim_start_matches('/'), method, query_params, headers, body, &target_domain).await +} + +/// Determine the target KeepKey domain based on routing rules with wildcard support +fn determine_target_domain(host: &str, headers: &HeaderMap) -> String { + // Check for explicit subdomain routing in headers + if let Some(target_header) = headers.get("x-keepkey-target") { + if let Ok(target) = target_header.to_str() { + if is_valid_keepkey_domain(target) { + return format!("https://{}", target); + } + } + } + + // Parse the incoming host to determine target subdomain + let host_clean = host.split(':').next().unwrap_or(host); // Remove port if present + + // Check if the request is for a specific subdomain pattern + if let Some(subdomain) = extract_keepkey_subdomain(host_clean) { + return format!("https://{}.keepkey.com", subdomain); + } + + // Check for wildcard subdomain in query params (for development) + if let Some(subdomain_header) = headers.get("x-keepkey-subdomain") { + if let Ok(subdomain) = subdomain_header.to_str() { + if is_valid_subdomain(subdomain) { + return format!("https://{}.keepkey.com", subdomain); + } + } + } + + // Default routing to real KeepKey domain + "https://keepkey.com".to_string() +} + +/// Extract subdomain from host if it follows KeepKey patterns (true wildcard support) +fn extract_keepkey_subdomain(host: &str) -> Option { + // Handle localhost with subdomain simulation for development + if host.starts_with("localhost") || host.starts_with("127.0.0.1") { + // For local development, route to keepkey.com (no subdomain) + return None; + } + + // Handle actual subdomain requests (for when deployed) + // Pattern: subdomain.keepkey.local or subdomain.keepkey.dev (for development) + if host.ends_with(".keepkey.local") || host.ends_with(".keepkey.dev") { + let parts: Vec<&str> = host.split('.').collect(); + if parts.len() >= 3 { + return Some(parts[0].to_string()); + } + } + + // Handle production patterns: any subdomain of keepkey.com + if host.ends_with(".keepkey.com") { + let parts: Vec<&str> = host.split('.').collect(); + if parts.len() >= 3 { + // Extract the subdomain (everything before .keepkey.com) + let subdomain_parts = &parts[..parts.len()-2]; + if !subdomain_parts.is_empty() { + return Some(subdomain_parts.join(".")); + } + } + } + + None +} + +/// Validate that a domain is a legitimate KeepKey domain (wildcard support) +fn is_valid_keepkey_domain(domain: &str) -> bool { + // Use regex to match *.keepkey.com pattern + lazy_static::lazy_static! { + static ref KEEPKEY_DOMAIN_REGEX: Regex = Regex::new(r"^([a-zA-Z0-9-]+\.)*keepkey\.com$").unwrap(); + } + + // Check exact match for root domain + if domain == "keepkey.com" { + return true; + } + + // Check wildcard pattern *.keepkey.com + KEEPKEY_DOMAIN_REGEX.is_match(domain) +} + +/// Validate subdomain name +fn is_valid_subdomain(subdomain: &str) -> bool { + // Basic validation for subdomain names + lazy_static::lazy_static! { + static ref SUBDOMAIN_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9-]+$").unwrap(); + } + + !subdomain.is_empty() && + subdomain.len() <= 63 && + !subdomain.starts_with('-') && + !subdomain.ends_with('-') && + SUBDOMAIN_REGEX.is_match(subdomain) +} + +/// Extract body from request with size limit +async fn extract_body(request: Request) -> Option> { + const MAX_BODY_SIZE: usize = 10 * 1024 * 1024; // 10MB limit + match axum::body::to_bytes(request.into_body(), MAX_BODY_SIZE).await { + Ok(bytes) => if bytes.is_empty() { None } else { Some(bytes.to_vec()) }, + Err(e) => { + tracing::warn!("Failed to extract request body (可能 exceeding size limit): {}", e); + None + } + } +} + +/// Extract query parameters from query string +fn extract_query_params(query: Option<&str>) -> HashMap { + query.map(|q| { + url::form_urlencoded::parse(q.as_bytes()) + .into_owned() + .collect() + }).unwrap_or_default() +} + +/// Core proxy function that handles all requests to *.keepkey.com domains +async fn proxy_keepkey_request( + path: &str, + method: Method, + params: HashMap, + headers: HeaderMap, + body: Option>, + target_domain: &str, +) -> Response { + // Build the target URL + let target_url = if path.is_empty() { + format!("{}/", target_domain) + } else { + format!("{}/{}", target_domain, path) + }; + + tracing::debug!("πŸ”„ Proxying {} {} -> {}", method, path, target_url); + + // Create HTTP client with appropriate settings for connecting to vault.keepkey.com + let client = reqwest::Client::builder() + .danger_accept_invalid_certs(false) // Use proper SSL validation for production + .timeout(std::time::Duration::from_secs(30)) // Reasonable timeout + .connect_timeout(std::time::Duration::from_secs(10)) // DNS/connect timeout + .user_agent("KeepKey-Vault-Proxy/2.0") + .tcp_keepalive(std::time::Duration::from_secs(60)) + .pool_idle_timeout(std::time::Duration::from_secs(90)) + .build() + .unwrap(); + + // Convert axum Method to reqwest Method + let reqwest_method = match method { + Method::GET => reqwest::Method::GET, + Method::POST => reqwest::Method::POST, + Method::PUT => reqwest::Method::PUT, + Method::DELETE => reqwest::Method::DELETE, + Method::PATCH => reqwest::Method::PATCH, + Method::HEAD => reqwest::Method::HEAD, + Method::OPTIONS => reqwest::Method::OPTIONS, + _ => reqwest::Method::GET, + }; + + // Build request + let mut request = client.request(reqwest_method, &target_url); + + // Add query parameters + if !params.is_empty() { + request = request.query(¶ms); + } + + // Forward appropriate headers (exclude problematic ones) + for (name, value) in headers.iter() { + let name_str = name.as_str().to_lowercase(); + if !is_hop_by_hop_header(&name_str) && !is_problematic_header(&name_str) { + if let Ok(value_str) = value.to_str() { + // Special handling for Host header - set it to target domain + if name_str == "host" { + let target_host = target_domain.trim_start_matches("https://").trim_start_matches("http://"); + request = request.header("host", target_host); + } else { + request = request.header(name.as_str(), value_str); + } + } + } + } + + // Add body for POST/PUT/PATCH requests + if let Some(body_bytes) = body { + request = request.body(body_bytes); + } + + // Make the request + match request.send().await { + Ok(response) => { + tracing::debug!("βœ… Proxy response: {} {}", response.status(), target_url); + convert_response_to_axum(response, target_domain).await + } + Err(e) => { + // Provide more detailed error information for vault.keepkey.com connectivity + let error_msg = if e.is_timeout() { + tracing::warn!("⏰ Timeout connecting to {} (30s limit)", target_url); + "Request timeout - vault.keepkey.com may be slow or unreachable" + } else if e.is_connect() { + // Check if this is a DNS resolution error specifically + let error_str = e.to_string(); + if error_str.contains("dns error") || error_str.contains("failed to lookup address") { + tracing::error!("🌐 DNS resolution failed for {}: {}", target_url, e); + "DNS resolution failed - cannot resolve vault.keepkey.com. Check your internet connection and DNS settings" + } else { + tracing::error!("πŸ”Œ Connection failed to {}: {}", target_url, e); + "Failed to connect to vault.keepkey.com - the server may be down or unreachable" + } + } else if e.is_request() { + tracing::error!("πŸ“€ Request error to {}: {}", target_url, e); + "Request formatting error" + } else { + tracing::error!("❌ Unknown proxy error for {}: {}", target_url, e); + "Unknown proxy error" + }; + + create_error_response(StatusCode::BAD_GATEWAY, &format!("{}: {}", error_msg, e)) + } + } +} + +/// Convert reqwest Response to axum Response +async fn convert_response_to_axum(response: reqwest::Response, target_domain: &str) -> Response { + // Convert status code + let status_code = match response.status().as_u16() { + 200 => StatusCode::OK, + 201 => StatusCode::CREATED, + 204 => StatusCode::NO_CONTENT, + 301 => StatusCode::MOVED_PERMANENTLY, + 302 => StatusCode::FOUND, + 304 => StatusCode::NOT_MODIFIED, + 400 => StatusCode::BAD_REQUEST, + 401 => StatusCode::UNAUTHORIZED, + 403 => StatusCode::FORBIDDEN, + 404 => StatusCode::NOT_FOUND, + 405 => StatusCode::METHOD_NOT_ALLOWED, + 429 => StatusCode::TOO_MANY_REQUESTS, + 500 => StatusCode::INTERNAL_SERVER_ERROR, + 502 => StatusCode::BAD_GATEWAY, + 503 => StatusCode::SERVICE_UNAVAILABLE, + 504 => StatusCode::GATEWAY_TIMEOUT, + code => StatusCode::from_u16(code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + }; + + let response_headers = response.headers().clone(); + let content_type = response_headers.get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_lowercase(); + + // Check if this is a React Server Components (RSC) streaming response + let is_rsc_stream = content_type.contains("text/x-component") || + content_type.contains("text/plain") || + content_type.contains("application/x-ndjson") || + content_type.contains("text/x-server-sent-events") || + response_headers.get("transfer-encoding") + .and_then(|v| v.to_str().ok()) + .map(|v| v.contains("chunked")) + .unwrap_or(false) || + response_headers.get("x-nextjs-stream") + .is_some() || + response_headers.get("x-nextjs-page") + .is_some(); + + // For RSC streaming responses, pass through directly without buffering + if is_rsc_stream { + tracing::debug!("πŸ”„ Streaming RSC response without buffering"); + return stream_response_directly(response, status_code, response_headers, target_domain); + } + + // For non-streaming responses, process the body (URL rewriting, etc.) + let body_bytes = match response.bytes().await { + Ok(bytes) => bytes, + Err(e) => { + tracing::error!("Failed to read response body: {}", e); + return create_error_response(StatusCode::BAD_GATEWAY, "Failed to read response body"); + } + }; + + // Process the body based on content type + let processed_body = if content_type.contains("text/html") { + // For HTML responses, rewrite URLs + let body_str = String::from_utf8_lossy(&body_bytes); + let processed = rewrite_urls(&body_str, target_domain); + processed.into_bytes() + } else { + // For other content types, return as-is + body_bytes.to_vec() + }; + + // Build response with appropriate headers + let mut resp_builder = Response::builder().status(status_code); + + // Copy headers from the original response, but skip content-length if we modified the body + let body_was_modified = content_type.contains("text/html"); + for (name, value) in response_headers.iter() { + let name_str = name.as_str().to_lowercase(); + if !is_hop_by_hop_header(&name_str) && + !(body_was_modified && name_str == "content-length") { // Skip content-length if body was modified + if let Ok(header_name) = HeaderName::from_str(name.as_str()) { + if let Ok(header_value) = HeaderValue::from_str(&value.to_str().unwrap_or_default()) { + resp_builder = resp_builder.header(header_name, header_value); + } + } + } + } + + // Set correct content-length for the processed body + resp_builder = resp_builder.header("content-length", processed_body.len().to_string()); + + // Create the response + match resp_builder.body(Body::from(processed_body)) { + Ok(response) => response, + Err(e) => { + eprintln!("❌ Failed to build response: {}", e); + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("Internal Server Error")) + .unwrap() + } + } +} + +/// Stream response directly without buffering (for RSC and other streaming responses) +fn stream_response_directly(response: reqwest::Response, status_code: StatusCode, response_headers: reqwest::header::HeaderMap, target_domain: &str) -> Response { + // Convert reqwest body to axum body stream + let body_stream = response.bytes_stream(); + let body = Body::from_stream(body_stream); + + // Build response with appropriate headers + let mut resp_builder = Response::builder().status(status_code); + + // Copy safe headers from the original response, preserving streaming headers + for (name, value) in response_headers.iter() { + let name_str = name.as_str().to_lowercase(); + // For streaming responses, allow transfer-encoding and don't filter content-length + if name_str == "transfer-encoding" || name_str == "content-type" { + if let Ok(value_str) = value.to_str() { + resp_builder = resp_builder.header(name.as_str(), value_str); + } + } else if !is_hop_by_hop_header(&name_str) && !is_security_header(&name_str) && name_str != "content-length" { + if let Ok(value_str) = value.to_str() { + resp_builder = resp_builder.header(name.as_str(), value_str); + } + } + } + + // Add proxy-specific headers but preserve streaming headers + resp_builder = resp_builder + .header("x-proxy-by", "keepkey-vault") + .header("x-proxy-target", target_domain) + .header("access-control-allow-origin", "*") + .header("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") + .header("access-control-allow-headers", "content-type, authorization, x-requested-with, x-keepkey-target, x-keepkey-subdomain"); + + // Don't override cache-control for streaming responses + resp_builder.body(body).unwrap() +} + +/// Rewrite URLs in HTML content to point to our proxy for all KeepKey domains (wildcard support with subdomain preservation) +fn rewrite_keepkey_urls(html: &str, target_domain: &str) -> String { + let mut result = html.to_string(); + let proxy_base = "http://localhost:8080"; + // Extract subdomain from target for preservation + let subdomain = target_domain.trim_start_matches("https://").split('.').next().unwrap_or(""); + let proxy_base_with_sub = if subdomain.is_empty() { proxy_base.to_string() } else { format!("{}/{}", proxy_base, subdomain) }; + // Add base tag + if let Some(head_pos) = result.find("") { + let insert_pos = head_pos + "".len(); + result.insert_str(insert_pos, &format!(r#" + + + "#, proxy_base_with_sub, target_domain)); + } + // Enhanced regex to capture and preserve subdomain + lazy_static::lazy_static! { + static ref KEEPKEY_URL_REGEX: Regex = Regex::new(r"https?://((?:[a-zA-Z0-9-]+\.)*)keepkey\.com").unwrap(); + } + result = KEEPKEY_URL_REGEX.replace_all(&result, |caps: ®ex::Captures| { + let sub = &caps[1]; + if sub.is_empty() { + proxy_base.to_string() + } else { + format!("{}/{}", proxy_base, sub.trim_end_matches('.')) + } + }).to_string(); + // Rewrite relative URLs that start with / + result = rewrite_attribute_urls(&result, "href", proxy_base); + result = rewrite_attribute_urls(&result, "src", proxy_base); + result = rewrite_attribute_urls(&result, "action", proxy_base); + + // Rewrite common API patterns for any KeepKey subdomain + result = rewrite_keepkey_api_calls(&result, proxy_base); + + tracing::debug!("πŸ”„ Rewrote HTML URLs for KeepKey proxy compatibility (wildcard)"); + result +} + +/// Rewrite JavaScript/JSON content for KeepKey domains (wildcard support with subdomain preservation) +fn rewrite_js_urls(content: &str, target_domain: &str) -> String { + let mut result = content.to_string(); + let proxy_base = "http://localhost:8080"; + // Extract subdomain + let _subdomain = target_domain.trim_start_matches("https://").split('.').next().unwrap_or(""); + // Enhanced regex to capture subdomain + lazy_static::lazy_static! { + static ref KEEPKEY_JS_REGEX: Regex = Regex::new(r#"["']https?://((?:[a-zA-Z0-9-]+\.)*)keepkey\.com([^"']*)["']"#).unwrap(); + } + result = KEEPKEY_JS_REGEX.replace_all(&result, |caps: ®ex::Captures| { + let quote = &caps[0][0..1]; + let sub = &caps[1]; + let path = &caps[2]; + let proxy_path = if sub.is_empty() { + format!("{}{}", proxy_base, path) + } else { + format!("{}/{}{}", proxy_base, sub.trim_end_matches('.'), path) + }; + format!("{}{}{}", quote, proxy_path, quote) + }).to_string(); + tracing::debug!("πŸ”„ Rewrote JavaScript URLs for KeepKey proxy compatibility (wildcard)"); + result +} + +/// Rewrite API calls for KeepKey domains +fn rewrite_keepkey_api_calls(html: &str, proxy_base: &str) -> String { + let mut result = html.to_string(); + + // Rewrite fetch calls + result = result.replace("fetch(\"/", &format!("fetch(\"{}/", proxy_base)); + result = result.replace("fetch('/", &format!("fetch('{}/", proxy_base)); + + // Rewrite XMLHttpRequest calls + result = result.replace(".open(\"GET\", \"/", &format!(".open(\"GET\", \"{}/", proxy_base)); + result = result.replace(".open('GET', '/", &format!(".open('GET', '{}/", proxy_base)); + result = result.replace(".open(\"POST\", \"/", &format!(".open(\"POST\", \"{}/", proxy_base)); + result = result.replace(".open('POST', '/", &format!(".open('POST', '{}/", proxy_base)); + + // Rewrite axios calls + result = result.replace("axios.get(\"/", &format!("axios.get(\"{}/", proxy_base)); + result = result.replace("axios.get('/", &format!("axios.get('{}/", proxy_base)); + result = result.replace("axios.post(\"/", &format!("axios.post(\"{}/", proxy_base)); + result = result.replace("axios.post('/", &format!("axios.post('{}/", proxy_base)); + + result +} + +/// Rewrite specific HTML attributes +fn rewrite_attribute_urls(html: &str, attribute: &str, proxy_base: &str) -> String { + let mut result = html.to_string(); + + // Handle double quotes + let pattern_double = format!("{}=\"/", attribute); + let replacement_double = format!("{}=\"{}/", attribute, proxy_base); + result = result.replace(&pattern_double, &replacement_double); + + // Handle single quotes + let pattern_single = format!("{}='/", attribute); + let replacement_single = format!("{}='{}/", attribute, proxy_base); + result = result.replace(&pattern_single, &replacement_single); + + result +} + +/// Rewrite URLs in content based on content type +fn rewrite_urls(content: &str, target_domain: &str) -> String { + // For now, just use the HTML rewriter for all content + // In the future, we could have different rewriters for different content types + rewrite_keepkey_urls(content, target_domain) +} + +/// Check if header is a hop-by-hop header that shouldn't be forwarded +fn is_hop_by_hop_header(name: &str) -> bool { + matches!(name, + "connection" | "keep-alive" | "proxy-authenticate" | + "proxy-authorization" | "te" | "trailers" | "transfer-encoding" | "upgrade" + ) +} + +/// Check if header is problematic for proxying +fn is_problematic_header(name: &str) -> bool { + matches!(name, + "content-length" | "content-encoding" | + "accept-encoding" // Let the client handle encoding + ) +} + +/// Check if header is a security header that should be filtered +fn is_security_header(name: &str) -> bool { + matches!(name, + "content-security-policy" | "x-frame-options" | + "strict-transport-security" | "x-xss-protection" | + "x-content-type-options" | "referrer-policy" + ) +} + +/// Create a standardized error response +fn create_error_response(status: StatusCode, message: &str) -> Response { + let error_body = serde_json::json!({ + "error": "KeepKey Proxy Error", + "message": message, + "status": status.as_u16(), + "proxy": "keepkey-vault", + "wildcard_support": "*.keepkey.com", + "default_target": "keepkey.com", + "examples": [ + "keepkey.com", + "vault.keepkey.com", + "app.keepkey.com", + "api.keepkey.com", + "bridge.keepkey.com", + "support.keepkey.com", + "docs.keepkey.com", + "any-subdomain.keepkey.com" + ] + }); + + Response::builder() + .status(status) + .header("content-type", "application/json") + .header("access-control-allow-origin", "*") + .header("x-proxy-error", "true") + .header("x-proxy-by", "keepkey-vault") + .header("x-wildcard-support", "*.keepkey.com") + .header("x-default-target", "keepkey.com") + .body(Body::from(error_body.to_string())) + .unwrap() +} \ No newline at end of file diff --git a/projects/vault-v2/src-tauri/tauri.conf.json b/projects/vault-v2/src-tauri/tauri.conf.json index c6ab762..3e125b2 100644 --- a/projects/vault-v2/src-tauri/tauri.conf.json +++ b/projects/vault-v2/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "vault-v2", - "version": "2.2.0", + "version": "2.2.8", "identifier": "com.vault-v2.app", "build": { "beforeDevCommand": "bun run dev", @@ -28,7 +28,7 @@ }, "bundle": { "active": true, - "targets": ["app", "dmg", "msi", "appimage", "deb"], + "targets": ["app", "dmg", "nsis", "appimage", "deb"], "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -38,6 +38,21 @@ ], "resources": [ "firmware/" - ] + ], + "macOS": { + "signingIdentity": "Developer ID Application: KEY HODLERS LLC (DR57X8Z394)", + "hardenedRuntime": true, + "entitlements": "./entitlements.plist", + "minimumSystemVersion": "10.15" + }, + "windows": { + "webviewInstallMode": { + "type": "downloadBootstrapper" + }, + "nsis": { + "installMode": "perMachine", + "installerIcon": "icons/icon.ico" + } + } } } diff --git a/projects/vault-v2/src/App.css b/projects/vault-v2/src/App.css index 85f7a4a..9356c96 100644 --- a/projects/vault-v2/src/App.css +++ b/projects/vault-v2/src/App.css @@ -13,7 +13,7 @@ color: #0f0f0f; background-color: #f6f6f6; - + font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; @@ -21,6 +21,21 @@ -webkit-text-size-adjust: 100%; } +@keyframes pulse { + 0% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.05); + opacity: 0.8; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + .container { margin: 0; padding-top: 10vh; diff --git a/projects/vault-v2/src/App.tsx b/projects/vault-v2/src/App.tsx index 4577e1e..a20f631 100644 --- a/projects/vault-v2/src/App.tsx +++ b/projects/vault-v2/src/App.tsx @@ -55,9 +55,11 @@ function App() { const [isRestarting, setIsRestarting] = useState(false); const [deviceUpdateComplete, setDeviceUpdateComplete] = useState(false); const [onboardingActive, setOnboardingActive] = useState(false); - const { showOnboarding, showError } = useCommonDialogs(); + const [setupWizardActive, setSetupWizardActive] = useState(false); + const [noDeviceDialogShown, setNoDeviceDialogShown] = useState(false); + const { showOnboarding, showError, showNoDevice } = useCommonDialogs(); const { shouldShowOnboarding, loading: onboardingLoading, clearCache } = useOnboardingState(); - const { hideAll, activeDialog, getQueue } = useDialog(); + const { hideAll, activeDialog, getQueue, isWizardActive } = useDialog(); const { fetchedXpubs, portfolio, isSync, reinitialize } = useWallet(); // Check wallet context state and sync with local state @@ -197,6 +199,43 @@ function App() { } }, [shouldShowOnboarding, onboardingLoading, showOnboarding, clearCache]); + // Show "No Device" dialog after 30 seconds if no device is connected + useEffect(() => { + let timeoutId: NodeJS.Timeout; + + if (!deviceConnected && !noDeviceDialogShown && !onboardingActive && !setupWizardActive) { + console.log("πŸ“± [App] Starting 30-second timer for no device dialog"); + timeoutId = setTimeout(() => { + if (!deviceConnected) { + console.log("πŸ“± [App] 30 seconds elapsed with no device - showing dialog"); + setNoDeviceDialogShown(true); + showNoDevice({ + onRetry: async () => { + console.log("πŸ“± [App] User clicked retry - restarting backend"); + setNoDeviceDialogShown(false); + // Restart the backend to scan for devices again + try { + await invoke('restart_backend_startup'); + reinitialize(); + } catch (error) { + console.error("Failed to restart backend:", error); + } + } + }); + } + }, 30000); // 30 seconds + } else if (deviceConnected && noDeviceDialogShown) { + // Device connected, reset the flag + setNoDeviceDialogShown(false); + } + + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [deviceConnected, noDeviceDialogShown, onboardingActive, setupWizardActive, showNoDevice, reinitialize]); + useEffect(() => { let unlistenStatusUpdate: (() => void) | undefined; let unlistenDeviceReady: (() => void) | undefined; @@ -287,6 +326,27 @@ function App() { setDeviceUpdateComplete(false); }); + // Listen for "no device found" event from backend + const unlistenNoDeviceFound = await listen('device:no-device-found', (event) => { + console.log('πŸ“± [App] No device found event received from backend:', event.payload); + if (!deviceConnected && !noDeviceDialogShown) { + setNoDeviceDialogShown(true); + showNoDevice({ + onRetry: async () => { + console.log("πŸ“± [App] User clicked retry - restarting backend"); + setNoDeviceDialogShown(false); + // Restart the backend to scan for devices again + try { + await invoke('restart_backend_startup'); + reinitialize(); + } catch (error) { + console.error("Failed to restart backend:", error); + } + } + }); + } + }); + console.log('βœ… All event listeners set up successfully'); // Return cleanup function that removes all listeners @@ -297,6 +357,7 @@ function App() { if (unlistenFeaturesUpdated) unlistenFeaturesUpdated(); if (unlistenAccessError) unlistenAccessError(); if (unlistenDeviceDisconnected) unlistenDeviceDisconnected(); + if (unlistenNoDeviceFound) unlistenNoDeviceFound(); }; } catch (error) { @@ -310,7 +371,7 @@ function App() { if (unlistenStatusUpdate) unlistenStatusUpdate(); if (unlistenDeviceReady) unlistenDeviceReady(); }; - }, []); // Empty dependency array ensures this runs once on mount and cleans up on unmount + }, [deviceConnected, noDeviceDialogShown, showNoDevice, reinitialize]); // Add dependencies for the no device listener const mcpUrl = "http://127.0.0.1:1646/mcp"; @@ -344,8 +405,8 @@ function App() { alignItems="center" justifyContent="center" > - {/* Clickable Logo in the center */} - {!onboardingActive && ( + {/* Clickable Logo in the center - hide when wizards are active */} + {!onboardingActive && !isWizardActive() && !setupWizardActive && ( - - - - {loadingStatus} - - {/* ⟡ no layout shift */} + + + + + {loadingStatus === "Scanning for devices..." && !deviceConnected + ? "Please connect your KeepKey" + : loadingStatus} + + {/* ⟡ no layout shift */} + + {loadingStatus === "Scanning for devices..." && !deviceConnected && ( + + If your device is already connected, try unplugging and reconnecting it + + )} @@ -417,6 +487,7 @@ function App() { setDeviceConnected(true); } }} + onSetupWizardActiveChange={setSetupWizardActive} /> {/* REST and MCP links in bottom right corner */} diff --git a/projects/vault-v2/src/components/BootloaderUpdateWizard/BootloaderUpdateWizard.tsx b/projects/vault-v2/src/components/BootloaderUpdateWizard/BootloaderUpdateWizard.tsx index f51eaad..3709acf 100644 --- a/projects/vault-v2/src/components/BootloaderUpdateWizard/BootloaderUpdateWizard.tsx +++ b/projects/vault-v2/src/components/BootloaderUpdateWizard/BootloaderUpdateWizard.tsx @@ -1,5 +1,5 @@ import React, { useState, useCallback } from 'react'; -import { Box, Button, HStack, VStack, Text, Flex, Icon, Progress } from '@chakra-ui/react'; +import { Box, Button, HStack, VStack, Text, Flex, Icon } from '@chakra-ui/react'; import { FaCheckCircle, FaExclamationTriangle } from 'react-icons/fa'; import { useDialog } from '../../contexts/DialogContext'; import { Step0Warning } from './steps/Step0Warning'; @@ -170,21 +170,6 @@ export function BootloaderUpdateWizard({ - {/* Progress Bar */} - - - - - - - {activeStep.id === 'in-progress' && progressInfo && ( - {progressInfo.message} - )} - {errorInfo && activeStep.id !== 'completion' && ( diff --git a/projects/vault-v2/src/components/BootloaderUpdateWizard/steps/Step1UpdateInProgress.tsx b/projects/vault-v2/src/components/BootloaderUpdateWizard/steps/Step1UpdateInProgress.tsx index 94057a0..92ee06d 100644 --- a/projects/vault-v2/src/components/BootloaderUpdateWizard/steps/Step1UpdateInProgress.tsx +++ b/projects/vault-v2/src/components/BootloaderUpdateWizard/steps/Step1UpdateInProgress.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState } from 'react'; -import { VStack, Text, Spinner, Box, Icon } from '@chakra-ui/react'; +import React, { useEffect } from 'react'; +import { VStack, Text, Box, Icon } from '@chakra-ui/react'; import { FaCog } from 'react-icons/fa'; import { invoke } from '@tauri-apps/api/core'; import type { StepProps } from '../BootloaderUpdateWizard'; @@ -11,61 +11,54 @@ export const Step1UpdateInProgress: React.FC = ({ onSetProgress, clearError }) => { - const [statusMessage, setStatusMessage] = useState('Initializing update...'); - useEffect(() => { clearError(); // Clear previous errors when entering this step const performUpdate = async () => { try { - if (onSetProgress) onSetProgress({ value: 10, message: 'Preparing device...' }); - setStatusMessage('Preparing device for bootloader update...'); - // TODO: Replace with actual Tauri command to start bootloader update - // This command should ideally emit progress events or return status updates. - // For now, we simulate a multi-stage process. - - await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate prep time - if (onSetProgress) onSetProgress({ value: 30, message: 'Entering bootloader mode...' }); - setStatusMessage('Device entering bootloader mode...'); - // Example: await invoke('enter_bootloader_mode', { deviceId }); - - await new Promise(resolve => setTimeout(resolve, 3000)); // Simulate mode switch - if (onSetProgress) onSetProgress({ value: 50, message: 'Sending update payload...' }); - setStatusMessage('Sending update payload to device...'); - // Example: await invoke('send_bootloader_firmware', { deviceId }); - - await new Promise(resolve => setTimeout(resolve, 5000)); // Simulate flashing - if (onSetProgress) onSetProgress({ value: 80, message: 'Verifying update...' }); - setStatusMessage('Verifying update integrity...'); - // Example: await invoke('verify_bootloader_update', { deviceId }); + // For now, we simulate the update process. - await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate verification - if (onSetProgress) onSetProgress({ value: 100, message: 'Update successful! Rebooting...' }); - setStatusMessage('Bootloader update successful! Device is rebooting.'); - - // Example: await invoke('reboot_device_after_update', { deviceId }); - await new Promise(resolve => setTimeout(resolve, 3000)); // Simulate reboot + // Simulate the update process + await new Promise(resolve => setTimeout(resolve, 15000)); // Simulate update time onNext(); // Move to completion step } catch (err: any) { console.error('Bootloader update failed:', err); const errorMessage = err.message || 'An unknown error occurred during the update.'; const errorAdvice = 'Please ensure your device remained connected. You may need to unplug and replug your device, then try the update again. If the problem persists, contact support.'; - if (onSetProgress) onSetProgress({ value: 100, message: `Error: ${errorMessage}`}); // Show error in progress too onError(errorMessage, errorAdvice); } }; performUpdate(); - }, [deviceId, onNext, onError, onSetProgress, clearError]); + }, [deviceId, onNext, onError, clearError]); return ( - - - Updating Bootloader... - {statusMessage} + + + Follow directions on device + + + Your KeepKey will guide you through the update process. + + + + + + Note: On the KeepKey, it will ask you to verify backup. + We will do this after updating - hold the button to skip for now to continue. + + + = ({ bg="gray.750" > - + DO NOT DISCONNECT YOUR DEVICE. diff --git a/projects/vault-v2/src/components/DeviceInvalidStateDialog.tsx b/projects/vault-v2/src/components/DeviceInvalidStateDialog.tsx index a3a0915..8331f47 100644 --- a/projects/vault-v2/src/components/DeviceInvalidStateDialog.tsx +++ b/projects/vault-v2/src/components/DeviceInvalidStateDialog.tsx @@ -6,10 +6,11 @@ import { DialogBody, DialogFooter } from "./ui/dialog"; -import { Button, VStack, Text, Icon, Box } from '@chakra-ui/react'; +import { Button, VStack, Text, Icon, Box, Spinner, HStack } from '@chakra-ui/react'; import { FaExclamationTriangle, FaPlug } from 'react-icons/fa'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { listen } from '@tauri-apps/api/event'; +import { invoke } from '@tauri-apps/api/core'; interface DeviceInvalidStateDialogProps { deviceId: string; @@ -22,6 +23,28 @@ export const DeviceInvalidStateDialog = ({ error, onClose }: DeviceInvalidStateDialogProps) => { + const [isScanning, setIsScanning] = useState(false); + + const handleReconnectClick = async () => { + console.log("User clicked 'I've Reconnected My Device' - triggering rescan"); + setIsScanning(true); + + try { + // Trigger a backend restart to rescan for devices + console.log("Restarting backend to scan for devices..."); + await invoke('restart_backend_startup'); + + // Wait a moment for the scan to start + setTimeout(() => { + console.log("Closing dialog after rescan initiated"); + onClose(); + }, 1000); + } catch (error) { + console.error("Failed to restart backend:", error); + // Still close the dialog even if restart failed + onClose(); + } + }; // Auto-close when this specific device is disconnected useEffect(() => { @@ -124,11 +147,20 @@ export const DeviceInvalidStateDialog = ({ diff --git a/projects/vault-v2/src/components/DeviceUpdateManager.tsx b/projects/vault-v2/src/components/DeviceUpdateManager.tsx index baebd48..07c14a0 100644 --- a/projects/vault-v2/src/components/DeviceUpdateManager.tsx +++ b/projects/vault-v2/src/components/DeviceUpdateManager.tsx @@ -1,7 +1,7 @@ -import { useEffect, useState } from 'react' +import { useEffect, useState, useRef } from 'react' import { BootloaderUpdateDialog } from './BootloaderUpdateDialog' import { FirmwareUpdateDialog } from './FirmwareUpdateDialog' -import { WalletCreationWizard } from './WalletCreationWizard/WalletCreationWizard' +import { SetupWizard } from './SetupWizard' import { EnterBootloaderModeDialog } from './EnterBootloaderModeDialog' import { PinUnlockDialog } from './PinUnlockDialog' import type { DeviceStatus, DeviceFeatures } from '../types/device' @@ -13,9 +13,11 @@ import { useDeviceInvalidStateDialog } from '../contexts/DialogContext' interface DeviceUpdateManagerProps { // Optional callback when all updates/setup is complete onComplete?: () => void + // Optional callback to notify when setup wizard active state changes + onSetupWizardActiveChange?: (active: boolean) => void } -export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => { +export const DeviceUpdateManager = ({ onComplete, onSetupWizardActiveChange }: DeviceUpdateManagerProps) => { const [deviceStatus, setDeviceStatus] = useState(null) const [showEnterBootloaderMode, setShowEnterBootloaderMode] = useState(false) const [showBootloaderUpdate, setShowBootloaderUpdate] = useState(false) @@ -25,6 +27,14 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => const [isProcessing, setIsProcessing] = useState(false) const [connectedDeviceId, setConnectedDeviceId] = useState(null) const [retryCount, setRetryCount] = useState(0) + + // Use ref to track setup wizard state for persistence + const setupWizardActive = useRef(false) + const setupWizardDeviceId = useRef(null) + const [persistentDeviceId, setPersistentDeviceId] = useState(null) + const [setupInProgress, setSetupInProgress] = useState(false) // Track if setup is in progress + const justCompletedBootloaderUpdate = useRef(false) // Track if we just did a bootloader update + const firmwareUpdateInProgress = useRef(false) // Track if firmware update is happening // Get wallet context for portfolio loading const { refreshPortfolio, fetchedXpubs } = useWallet() @@ -90,36 +100,24 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => }) // Determine which dialog to show based on priority - if (status.needsBootloaderUpdate && status.bootloaderCheck) { - if (isInBootloaderMode) { - // Device needs bootloader update AND is in bootloader mode -> show update dialog - console.log('Device needs bootloader update and is in bootloader mode') - setShowEnterBootloaderMode(false) - setShowBootloaderUpdate(true) - setShowFirmwareUpdate(false) - setShowWalletCreation(false) - } else { - // Device needs bootloader update but NOT in bootloader mode -> show enter bootloader mode dialog - console.log('Device needs bootloader update but not in bootloader mode') - setShowEnterBootloaderMode(true) - setShowBootloaderUpdate(false) - setShowFirmwareUpdate(false) - setShowWalletCreation(false) - } - } else if (status.needsFirmwareUpdate && status.firmwareCheck) { - console.log('Device needs firmware update') - console.log('πŸ”§ DeviceUpdateManager: Firmware update needed:', { - needsFirmwareUpdate: status.needsFirmwareUpdate, - firmwareCheck: status.firmwareCheck, - currentVersion: status.firmwareCheck?.currentVersion, - latestVersion: status.firmwareCheck?.latestVersion, - features: status.features - }) - setShowEnterBootloaderMode(false) - setShowBootloaderUpdate(false) - setShowFirmwareUpdate(true) - setShowWalletCreation(false) - } else if (status.needsInitialization) { + // IMPORTANT: If setup wizard is already showing, don't interrupt it + if (setupWizardActive.current) { + console.log('πŸ”§ DeviceUpdateManager: Setup wizard is already showing - keeping it visible') + return; // Don't change state while setup wizard is active + } + + // IMPORTANT: Check initialization FIRST - setup wizard should take priority over EVERYTHING + // Even if device is in bootloader mode, if it needs initialization, show setup wizard + // Check both explicit needsInitialization flag AND the initialized feature flag + // SPECIAL CASE: If device is in bootloader mode with "Unknown" firmware, it's likely uninitialized + const hasUnknownFirmware = status.firmwareCheck?.currentVersion === 'Unknown' || + status.firmwareCheck?.currentVersion === undefined; + const deviceNeedsInitialization = status.needsInitialization || + status.features?.initialized === false || + status.features?.initialized === undefined || + (isInBootloaderMode && hasUnknownFirmware); + + if (deviceNeedsInitialization) { // Check if recovery is in progress - if so, don't interfere if ((window as any).KEEPKEY_RECOVERY_IN_PROGRESS) { console.log('πŸ›‘οΈ DeviceUpdateManager: Recovery in progress - IGNORING initialization request') @@ -127,15 +125,31 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => return; // Don't change any state during recovery } - console.log('πŸ”§ DeviceUpdateManager: Device needs initialization - SHOULD SHOW ONBOARDING WIZARD') + console.log('πŸ”§ DeviceUpdateManager: Device needs initialization - SHOULD SHOW SETUP WIZARD') + console.log('πŸ”§ DeviceUpdateManager: Initialization check:', { + needsInitialization: status.needsInitialization, + initialized: status.features?.initialized, + hasUnknownFirmware, + currentFirmware: status.firmwareCheck?.currentVersion, + deviceNeedsInitialization, + isInBootloaderMode, + reason: hasUnknownFirmware && isInBootloaderMode ? 'Bootloader mode with unknown firmware' : 'Normal initialization needed' + }) console.log('πŸ”§ DeviceUpdateManager: Setting showWalletCreation = true') setShowEnterBootloaderMode(false) setShowBootloaderUpdate(false) setShowFirmwareUpdate(false) setShowWalletCreation(true) + setupWizardActive.current = true + setupWizardDeviceId.current = status.deviceId + setPersistentDeviceId(status.deviceId) // Save device ID for persistence + setSetupInProgress(true) // Mark setup as in progress + onSetupWizardActiveChange?.(true) + return; // Exit early - setup wizard takes absolute priority } else if (status.needsPinUnlock) { + // PIN UNLOCK HAS PRIORITY OVER ALL UPDATES // Device is initialized but locked with PIN - this is handled by the PIN unlock event listener - console.log('πŸ”’ DeviceUpdateManager: Device needs PIN unlock - NOT calling onComplete()') + console.log('πŸ”’ DeviceUpdateManager: Device needs PIN unlock - PRIORITY OVER UPDATES') // Don't call onComplete() here - PIN unlock dialog will be shown via the pin-unlock-needed event // Just ensure other dialogs are hidden setShowEnterBootloaderMode(false) @@ -143,6 +157,87 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => setShowFirmwareUpdate(false) setShowWalletCreation(false) // showPinUnlock will be set by the pin-unlock-needed event listener + return; // Exit early - PIN has priority + } else if (status.needsBootloaderUpdate && status.bootloaderCheck) { + // Only update bootloader if device is NOT initialized + // Initialized devices can skip bootloader updates + const isInitialized = status.features?.initialized === true + if (isInitialized) { + console.log('πŸ”§ Device is initialized - skipping bootloader update') + // Device is ready (PIN was already handled above) + console.log('βœ… Device is ready after skipping bootloader update') + setShowEnterBootloaderMode(false) + setShowBootloaderUpdate(false) + setShowFirmwareUpdate(false) + setShowWalletCreation(false) + setShowPinUnlock(false) + onComplete?.() + } else if (isInBootloaderMode) { + // Device needs bootloader update AND is in bootloader mode -> show update dialog + console.log('Device needs bootloader update and is in bootloader mode') + setShowEnterBootloaderMode(false) + setShowBootloaderUpdate(true) + setShowFirmwareUpdate(false) + setShowWalletCreation(false) + } else { + // Device needs bootloader update but NOT in bootloader mode -> show enter bootloader mode dialog + console.log('Device needs bootloader update but not in bootloader mode') + setShowEnterBootloaderMode(true) + setShowBootloaderUpdate(false) + setShowFirmwareUpdate(false) + setShowWalletCreation(false) + } + } else if (status.needsFirmwareUpdate) { // Removed the && status.firmwareCheck check to handle bootloader mode + // Only update firmware if device is NOT initialized + // Initialized devices can skip firmware updates + const isInitialized = status.features?.initialized === true + if (isInitialized) { + console.log('πŸ”§ Device is initialized - skipping firmware update') + // Device is ready (PIN was already handled above) + console.log('βœ… Device is ready after skipping firmware update') + setShowEnterBootloaderMode(false) + setShowBootloaderUpdate(false) + setShowFirmwareUpdate(false) + setShowWalletCreation(false) + setShowPinUnlock(false) + onComplete?.() + } else { + console.log('Device needs firmware update') + console.log('πŸ”§ DeviceUpdateManager: Firmware update needed:', { + needsFirmwareUpdate: status.needsFirmwareUpdate, + firmwareCheck: status.firmwareCheck, + currentVersion: status.firmwareCheck?.currentVersion, + latestVersion: status.firmwareCheck?.latestVersion, + features: status.features, + isInBootloaderMode + }) + + // CRITICAL: Check if device is in bootloader mode + if (isInBootloaderMode && !status.needsBootloaderUpdate) { + // Device is already in bootloader mode with correct bootloader version + // Show firmware update dialog directly + console.log('πŸ”§ DeviceUpdateManager: Device in bootloader mode with correct bootloader, showing firmware update') + setShowEnterBootloaderMode(false) + setShowBootloaderUpdate(false) + setShowFirmwareUpdate(true) + setShowWalletCreation(false) + } else if (!isInBootloaderMode) { + // Device needs firmware update but is NOT in bootloader mode + // Must enter bootloader mode first! + console.log('πŸ”§ DeviceUpdateManager: Device needs firmware update but NOT in bootloader mode - showing enter bootloader dialog') + setShowEnterBootloaderMode(true) // Show enter bootloader mode dialog + setShowBootloaderUpdate(false) + setShowFirmwareUpdate(false) // Don't show firmware update yet + setShowWalletCreation(false) + } else { + // Shouldn't happen, but handle edge case + console.log('πŸ”§ DeviceUpdateManager: Unexpected state - needs firmware but bootloader needs update too') + setShowEnterBootloaderMode(false) + setShowBootloaderUpdate(false) + setShowFirmwareUpdate(true) + setShowWalletCreation(false) + } + } } else { // Device is ready console.log('πŸ”§ DeviceUpdateManager: Device is ready, no updates needed') @@ -173,6 +268,18 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => console.log('πŸ”§ DeviceUpdateManager: Device features updated event received:', event.payload) const { status } = event.payload console.log('πŸ”§ DeviceUpdateManager: Extracted status from event:', status) + + // If we just completed a bootloader update and setup is in progress, + // update the persistent device ID to the new one + if (justCompletedBootloaderUpdate.current && setupInProgress && status.deviceId !== persistentDeviceId) { + console.log('πŸ”„ Device ID changed after bootloader update:', { + oldId: persistentDeviceId, + newId: status.deviceId + }) + setPersistentDeviceId(status.deviceId) + setupWizardDeviceId.current = status.deviceId + justCompletedBootloaderUpdate.current = false // Reset the flag + } console.log('πŸ”§ DeviceUpdateManager: Device status details:', { deviceId: status.deviceId, connected: status.connected, @@ -199,7 +306,14 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => setDeviceStatus(status) setConnectedDeviceId(status.deviceId) setRetryCount(0) // Reset retry count on successful event - handleDeviceStatus(status) + + // CRITICAL: Don't handle device status if setup is in progress + // This prevents the setup wizard from being hidden on reconnection + if (!setupInProgress) { + handleDeviceStatus(status) + } else { + console.log('πŸ”§ DeviceUpdateManager: Setup in progress, not handling device status to preserve wizard') + } }) // Listen for basic device connected events as fallback @@ -253,7 +367,21 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => }>('device:invalid-state', (event) => { console.log('⏱️ Device invalid state detected:', event.payload) - // CRITICAL: Clear ALL existing dialogs first + // CRITICAL: If setup is in progress, IGNORE invalid state errors + // This happens during firmware updates when device reboots + if (setupWizardActive.current || setupInProgress || showWalletCreation || firmwareUpdateInProgress.current) { + console.log('πŸ›‘οΈπŸ›‘οΈ IGNORING invalid state during setup - device is rebooting') + console.log('πŸ›‘οΈ Setup/Update must continue, not showing invalid state dialog') + console.log('πŸ›‘οΈ Protection flags:', { + setupWizardActive: setupWizardActive.current, + setupInProgress, + showWalletCreation, + firmwareUpdateInProgress: firmwareUpdateInProgress.current + }) + return; // Exit early - don't show dialog or clear state during setup + } + + // Only clear dialogs if setup is NOT in progress setShowBootloaderUpdate(false) setShowFirmwareUpdate(false) setShowWalletCreation(false) @@ -333,7 +461,20 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => return; // Don't change state during recovery } - // Clear all state when device disconnects (only if not in recovery) + // CRITICAL: Check if setup is in progress + // If setup wizard is active or in progress, DON'T clear any state + if (setupWizardActive.current || setupInProgress) { + console.log('πŸ›‘οΈπŸ›‘οΈ DeviceUpdateManager: SETUP IN PROGRESS - PRESERVING ALL STATE') + console.log('πŸ›‘οΈ Setup wizard must NOT be abandoned during device disconnect') + console.log('πŸ›‘οΈ Keeping wizard visible and waiting for device reconnection') + + // Keep the device ID for when it reconnects + // Don't clear ANY state that would close the setup wizard + return; // Exit early - preserve everything + } + + // Only clear state if setup is NOT in progress + console.log('DeviceUpdateManager: No setup in progress, clearing state normally') setDeviceStatus(null) setConnectedDeviceId(null) setShowBootloaderUpdate(false) @@ -369,7 +510,7 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => // Cleanup function will be called automatically if (timeoutId) clearTimeout(timeoutId) } - }, [onComplete]) + }, [onComplete, setupInProgress, deviceInvalidStateDialog, onSetupWizardActiveChange, refreshPortfolio, fetchedXpubs]) const handleFirmwareUpdate = async () => { setIsProcessing(true) @@ -414,7 +555,13 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => } const handleWalletCreationComplete = () => { + console.log('βœ… Setup wizard completed successfully') setShowWalletCreation(false) + setupWizardActive.current = false + setupWizardDeviceId.current = null + setPersistentDeviceId(null) // Clear persistent device ID + setSetupInProgress(false) // Clear the setup in progress flag + onSetupWizardActiveChange?.(false) onComplete?.() } @@ -432,9 +579,41 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => showWalletCreation, showEnterBootloaderMode }) + console.log('πŸ”’ Device status after PIN unlock:', { + needsFirmwareUpdate: deviceStatus?.needsFirmwareUpdate, + needsBootloaderUpdate: deviceStatus?.needsBootloaderUpdate, + needsInitialization: deviceStatus?.needsInitialization, + firmwareVersion: deviceStatus?.firmwareCheck?.currentVersion, + latestVersion: deviceStatus?.firmwareCheck?.latestVersion + }) + setShowPinUnlock(false) - // Automatically start portfolio loading after PIN unlock + // CRITICAL: Check if device still needs updates after PIN unlock + if (deviceStatus?.needsFirmwareUpdate) { + console.log('πŸ”’ Device needs firmware update after PIN unlock - NOT calling onComplete') + console.log('πŸ”’ Firmware update needed: v' + deviceStatus.firmwareCheck?.currentVersion + ' -> v' + deviceStatus.firmwareCheck?.latestVersion) + + // Check if device is in bootloader mode + const isInBootloaderMode = deviceStatus?.features?.bootloader_mode || deviceStatus?.features?.bootloaderMode || false + + if (!isInBootloaderMode) { + // Device needs to enter bootloader mode first + console.log('πŸ”’ Showing enter bootloader mode dialog for firmware update') + setShowEnterBootloaderMode(true) + setShowFirmwareUpdate(false) + } else { + // Device is in bootloader mode, show firmware update + console.log('πŸ”’ Showing firmware update dialog') + setShowEnterBootloaderMode(false) + setShowFirmwareUpdate(true) + } + + // DON'T call onComplete - device needs updates first + return + } + + // Only proceed with portfolio loading if device doesn't need updates try { console.log('πŸ”„ Auto-loading portfolio after PIN unlock...') console.log(`πŸ“‹ Current XPUBs in memory: ${fetchedXpubs.length}`) @@ -448,8 +627,8 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => // Don't block onComplete - user can manually refresh later } - // Device should now be ready to use - console.log('πŸ”’ Calling onComplete after PIN unlock') + // Device should now be ready to use (no updates needed) + console.log('πŸ”’ Device fully ready after PIN unlock - calling onComplete') onComplete?.() } @@ -458,8 +637,9 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => // Don't call onComplete - user cancelled PIN entry } - if (!deviceStatus) { - console.log('πŸ”§ DeviceUpdateManager: No deviceStatus, returning null') + // If setup wizard is active, we should still render it even without deviceStatus + if (!deviceStatus && !setupWizardActive.current && !persistentDeviceId) { + console.log('πŸ”§ DeviceUpdateManager: No deviceStatus, no active wizard, and no persistentDeviceId, returning null') return null } @@ -469,21 +649,31 @@ export const DeviceUpdateManager = ({ onComplete }: DeviceUpdateManagerProps) => showBootloaderUpdate, showEnterBootloaderMode, showPinUnlock, - deviceStatus: deviceStatus?.needsInitialization + deviceStatus: deviceStatus?.needsInitialization, + persistentDeviceId, + setupWizardActive: setupWizardActive.current }) return ( <> - {showEnterBootloaderMode && deviceStatus.bootloaderCheck && deviceStatus.deviceId && ( + {showEnterBootloaderMode && deviceStatus?.bootloaderCheck && deviceStatus?.deviceId && ( { + console.log('πŸ”§ User skipped bootloader update') + setShowEnterBootloaderMode(false) + // Device is ready (PIN was already handled before showing this dialog) + console.log('βœ… Device is ready after user skipped bootloader update') + onComplete?.() + }} /> )} - {showBootloaderUpdate && deviceStatus.bootloaderCheck && deviceStatus.deviceId && ( + {showBootloaderUpdate && deviceStatus?.bootloaderCheck && deviceStatus?.deviceId && ( /> )} - {showFirmwareUpdate && deviceStatus?.firmwareCheck && ( + {showFirmwareUpdate && deviceStatus && ( /> )} - {showWalletCreation && deviceStatus.deviceId && ( - setShowWalletCreation(false)} + onClose={() => { + // NOTE: onClose should only be called when user explicitly cancels + // NOT when device disconnects + console.log('⚠️ SetupWizard onClose called - user cancelled setup') + setShowWalletCreation(false) + setupWizardActive.current = false + setupWizardDeviceId.current = null + setPersistentDeviceId(null) + setSetupInProgress(false) // Clear setup in progress only on explicit close + firmwareUpdateInProgress.current = false // Also clear firmware update flag + onSetupWizardActiveChange?.(false) + }} + onFirmwareUpdateStart={() => { + console.log('πŸ”„ Firmware update starting in setup wizard') + firmwareUpdateInProgress.current = true + }} + onFirmwareUpdateComplete={() => { + console.log('βœ… Firmware update complete in setup wizard') + firmwareUpdateInProgress.current = false + }} /> )} - {showPinUnlock && deviceStatus.deviceId && ( + {showPinUnlock && deviceStatus?.deviceId && ( void + onSkip?: () => void + isInitialized?: boolean } export const EnterBootloaderModeDialog = ({ isOpen, bootloaderCheck, deviceId, - onClose + onClose, + onSkip, + isInitialized = false }: EnterBootloaderModeDialogProps) => { return ( !open && onClose()}> @@ -78,14 +82,29 @@ export const EnterBootloaderModeDialog = ({ - + + + {isInitialized && onSkip && ( + + )} + diff --git a/projects/vault-v2/src/components/NoDeviceDialog.tsx b/projects/vault-v2/src/components/NoDeviceDialog.tsx new file mode 100644 index 0000000..35ba65b --- /dev/null +++ b/projects/vault-v2/src/components/NoDeviceDialog.tsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { + Box, + VStack, + Text, + Button, + HStack, + Icon +} from '@chakra-ui/react'; +import { FaUsb } from 'react-icons/fa'; +import { useDialog } from '../contexts/DialogContext'; + +export interface NoDeviceDialogProps { + onRetry?: () => void; +} + +export function NoDeviceDialog({ onRetry }: NoDeviceDialogProps) { + const { hide } = useDialog(); + + const handleRetry = () => { + if (onRetry) { + onRetry(); + } + hide('no-device-found'); + }; + + return ( + + + + + + No KeepKey Detected + + + + + + + + Please connect your KeepKey device to continue + + + + + + Troubleshooting tips: + + + β€’ Make sure your KeepKey is plugged in via USB + + + β€’ Try a different USB port or cable + + + β€’ Unplug and reconnect your device + + + β€’ Ensure no other apps are using the device + + + + + + + + Device still not detected? + + + Try putting your KeepKey into updater mode: + + + + 1. Disconnect your KeepKey + + + 2. Hold down the button on your KeepKey + + + 3. While holding the button, reconnect the USB cable + + + 4. Release the button when you see the KeepKey logo + + + + This will allow the device to be detected and updated if needed. + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/projects/vault-v2/src/components/PinUnlockDialog.tsx b/projects/vault-v2/src/components/PinUnlockDialog.tsx index 455bce0..545f29b 100644 --- a/projects/vault-v2/src/components/PinUnlockDialog.tsx +++ b/projects/vault-v2/src/components/PinUnlockDialog.tsx @@ -49,19 +49,12 @@ export const PinUnlockDialog = ({ isOpen, deviceId, onUnlocked, onClose }: PinUn try { setIsLoading(true) setError(null) - setDeviceReadyStatus('Verifying device is ready for PIN...') - console.log('πŸ” Verifying device readiness for PIN unlock:', deviceId) + setDeviceReadyStatus('Preparing device for PIN entry...') + console.log('πŸ” Preparing PIN unlock for device:', deviceId) - // Use the dedicated device PIN readiness check - const isPinReady = await invoke('check_device_pin_ready', { deviceId }) - console.log('πŸ“Š Device PIN ready status:', isPinReady) - - if (!isPinReady) { - // Device is not ready or no longer needs PIN unlock - console.log('βœ… Device no longer needs PIN unlock or is not ready, closing dialog') - onUnlocked() - return - } + // Skip the readiness check - if we're showing this dialog, + // it's because the device needs PIN unlock + // The device might already be in PIN flow from a previous GetAddress call // Device is ready for PIN unlock attempt setDeviceReadyStatus('Device ready - requesting PIN matrix...') diff --git a/projects/vault-v2/src/components/Portfolio.tsx b/projects/vault-v2/src/components/Portfolio.tsx index 611a48d..ba8771c 100644 --- a/projects/vault-v2/src/components/Portfolio.tsx +++ b/projects/vault-v2/src/components/Portfolio.tsx @@ -135,9 +135,6 @@ export const Portfolio: React.FC = ({ onNavigate }) => { Syncing with your KeepKey - - Please make sure your device is unlocked - {syncingTime > 5 && ( This is taking longer than usual... diff --git a/projects/vault-v2/src/components/Send.tsx b/projects/vault-v2/src/components/Send.tsx index 3717b71..1baea8a 100644 --- a/projects/vault-v2/src/components/Send.tsx +++ b/projects/vault-v2/src/components/Send.tsx @@ -404,21 +404,63 @@ console.debug('[Send] deviceId from device.unique_id:', deviceId); amount: input.amount, vout: input.vout, txid: input.txid, - prev_tx_hex: input.hex + hex: input.hex // Device expects 'hex' field name })); const realOutputs = unsignedTx.outputs.map((output: any) => ({ - address: output.address, + address: output.address || '', // Ensure address is always a string amount: parseInt(output.amount), address_type: output.addressType === 'change' ? 'change' : 'spend', - script_type: output.scriptType || 'p2pkh', - address_n_list: output.addressNList + is_change: output.addressType === 'change' ? true : false, + address_n_list: output.addressNList || null, + script_type: output.scriptType || 'p2pkh' })); + // Debug log the outputs being sent + console.log('πŸ“€ Outputs being sent to device:', realOutputs); + realOutputs.forEach((output: any, idx: number) => { + console.log(` Output ${idx + 1}:`, { + address: output.address, + amount: output.amount, + address_type: output.address_type, + is_change: output.is_change, + has_address_n_list: !!output.address_n_list, + script_type: output.script_type + }); + }); + // Sign the transaction using real device with properly selected UTXOs console.log('πŸ” Calling signTransaction with device queue...'); console.log('πŸ” Request ID will be:', `sign_tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`); + // Log the EXACT payload being sent to the device + console.log('πŸ“ COMPLETE SIGN PAYLOAD TO DEVICE:'); + console.log('=========================================='); + console.log(JSON.stringify({ + coin: unsignedTx.coin, + inputs: realInputs, + outputs: realOutputs, + version: unsignedTx.version, + locktime: unsignedTx.locktime + }, null, 2)); + console.log('=========================================='); + + // Also log each input individually for detailed inspection + console.log('πŸ” DETAILED INPUT INSPECTION:'); + realInputs.forEach((input: any, index: number) => { + console.log(`Input ${index + 1} Full Details:`); + console.log(' - txid:', input.txid); + console.log(' - vout:', input.vout); + console.log(' - amount:', input.amount); + console.log(' - script_type:', input.script_type); + console.log(' - address_n_list:', JSON.stringify(input.address_n_list)); + console.log(' - hex present?:', !!input.hex); + console.log(' - hex length:', input.hex ? input.hex.length : 0); + console.log(' - hex first 100 chars:', input.hex ? input.hex.substring(0, 100) : 'NO HEX'); + console.log(' - hex last 20 chars:', input.hex ? input.hex.substring(input.hex.length - 20) : 'NO HEX'); + console.log(' - All fields:', Object.keys(input)); + }); + let signedTxHex: string; try { signedTxHex = await signTransaction( @@ -595,7 +637,7 @@ console.debug('[Send] deviceId from device.unique_id:', deviceId); {/* Show balance warning if zero */} {availableBalance === 0 && ( - ⚠️ No balance available. Please ensure your wallet is synced. + ⚠️ No balance available. Please fund your wallet. )} @@ -719,20 +761,30 @@ console.debug('[Send] deviceId from device.unique_id:', deviceId); {(['slow', 'medium', 'fast'] as const).map((preset) => { const presetFeeRate = feeRates[preset]; + const isSelected = feeRate === preset; return ( ); })} @@ -793,11 +845,17 @@ console.debug('[Send] deviceId from device.unique_id:', deviceId); Est. Fee: - {transactionReview.fee.toFixed(8)} BTC + + {transactionReview.fee.toFixed(8)} BTC + β‰ˆ ${convertBtcToUsd(transactionReview.fee).toFixed(2)} USD + Total: - {transactionReview.total.toFixed(8)} BTC + + {transactionReview.total.toFixed(8)} BTC + β‰ˆ ${convertBtcToUsd(transactionReview.total).toFixed(2)} USD + @@ -929,16 +987,16 @@ console.debug('[Send] deviceId from device.unique_id:', deviceId); onClick={async () => { try { const { invoke } = await import('@tauri-apps/api/core'); - await invoke('open_url', { url: `https://blockstream.info/tx/${txid}` }); + await invoke('open_url', { url: `https://mempool.space/tx/${txid}` }); } catch (error) { console.error('Failed to open URL:', error); // Fallback to window.open if Tauri command fails - window.open(`https://blockstream.info/tx/${txid}`, '_blank'); + window.open(`https://mempool.space/tx/${txid}`, '_blank'); } }} flex="1" > - πŸ”— View on Blockstream + πŸ”— View on Mempool diff --git a/projects/vault-v2/src/components/SetupWizard/SetupWizard.tsx b/projects/vault-v2/src/components/SetupWizard/SetupWizard.tsx new file mode 100644 index 0000000..f119eb4 --- /dev/null +++ b/projects/vault-v2/src/components/SetupWizard/SetupWizard.tsx @@ -0,0 +1,485 @@ +import { + Box, + Button, + HStack, + VStack, + Text, + Flex, + Icon, +} from "@chakra-ui/react"; +import { useState, useEffect, useRef } from "react"; +import { FaCheckCircle } from "react-icons/fa"; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { useDialog } from "../../contexts/DialogContext"; + +// Import individual steps +import { Step0Welcome } from "./steps/Step0Welcome"; +import { StepBootloaderUpdate } from "./steps/StepBootloaderUpdate"; +import { StepFirmwareUpdate } from "./steps/StepFirmwareUpdate"; +import { Step1CreateOrRecover } from "./steps/Step1CreateOrRecover"; +import { Step2DeviceLabel } from "./steps/Step2DeviceLabel"; +import { Step3Pin } from "./steps/Step3Pin"; +import { Step4BackupOrRecover } from "./steps/Step4BackupOrRecover"; +import { Step5Complete } from "./steps/Step5Complete"; + +interface SetupWizardProps { + deviceId: string; + onClose?: () => void; + onComplete?: () => void; + onFirmwareUpdateStart?: () => void; + onFirmwareUpdateComplete?: () => void; +} + +interface Step { + id: string; + label: string; + description: string; + component: React.ComponentType; +} + +// Define all steps (including hidden ones) +const CREATE_ALL_STEPS: Step[] = [ + { + id: "welcome", + label: "Welcome", + description: "Welcome to KeepKey Bitcoin-Only", + component: Step0Welcome, + }, + { + id: "bootloader", + label: "Bootloader", + description: "Verify and update bootloader if needed", + component: StepBootloaderUpdate, + }, + { + id: "firmware", + label: "Firmware", + description: "Verify and update firmware if needed", + component: StepFirmwareUpdate, + }, + { + id: "create-or-recover", + label: "Setup Type", + description: "Choose your setup method", + component: Step1CreateOrRecover, + }, + { + id: "pin", + label: "Security", + description: "Set up your PIN", + component: Step3Pin, + }, + { + id: "backup", + label: "Backup", + description: "Backup your recovery phrase", + component: Step4BackupOrRecover, + }, + { + id: "device-label", + label: "Device Name", + description: "Name your device", + component: Step2DeviceLabel, + }, + { + id: "complete", + label: "Complete", + description: "Setup complete!", + component: Step5Complete, + }, +]; + +const RECOVER_ALL_STEPS: Step[] = [ + { + id: "welcome", + label: "Welcome", + description: "Welcome to KeepKey Bitcoin-Only", + component: Step0Welcome, + }, + { + id: "bootloader", + label: "Bootloader", + description: "Verify and update bootloader if needed", + component: StepBootloaderUpdate, + }, + { + id: "firmware", + label: "Firmware", + description: "Verify and update firmware if needed", + component: StepFirmwareUpdate, + }, + { + id: "create-or-recover", + label: "Setup Type", + description: "Choose your setup method", + component: Step1CreateOrRecover, + }, + { + id: "recover", + label: "Recovery", + description: "Enter your recovery phrase", + component: Step4BackupOrRecover, + }, + { + id: "pin", + label: "Security", + description: "Set up your PIN", + component: Step3Pin, + }, + { + id: "device-label", + label: "Device Name", + description: "Name your device", + component: Step2DeviceLabel, + }, + { + id: "complete", + label: "Complete", + description: "Recovery complete!", + component: Step5Complete, + }, +]; + +// Define visible steps for progress bar +const CREATE_VISIBLE_STEPS = [ + { id: "bootloader", label: "Check Bootloader", number: 1 }, + { id: "firmware", label: "Check Firmware", number: 2 }, + { id: "create-or-recover", label: "Create Wallet", number: 3 }, +]; + +const RECOVER_VISIBLE_STEPS = [ + { id: "bootloader", label: "Check Bootloader", number: 1 }, + { id: "firmware", label: "Check Firmware", number: 2 }, + { id: "create-or-recover", label: "Recover Wallet", number: 3 }, +]; + +export function SetupWizard({ deviceId: initialDeviceId, onClose, onComplete, onFirmwareUpdateStart, onFirmwareUpdateComplete }: SetupWizardProps) { + const [currentStep, setCurrentStep] = useState(0); + const [flowType, setFlowType] = useState<'create' | 'recover' | null>(null); + const [wizardData, setWizardData] = useState<{ + deviceLabel?: string; + pinSession?: any; + recoverySettings?: any; + }>({}); + const [deviceId, setDeviceId] = useState(initialDeviceId); + const justCompletedBootloaderUpdate = useRef(false); + + const highlightColor = "orange.500"; // Bitcoin orange + const { hide } = useDialog(); + + // Listen for device connection events to update device ID after bootloader update + useEffect(() => { + let unsubscribe: (() => void) | undefined; + + const setupListener = async () => { + unsubscribe = await listen<{ + deviceId: string; + features: any; + status: any; + }>('device:features-updated', (event) => { + // If we just completed a bootloader update, update to the new device ID + if (justCompletedBootloaderUpdate.current && event.payload.deviceId !== deviceId) { + console.log('πŸ”„ SetupWizard: Device ID changed after bootloader update:', { + oldId: deviceId, + newId: event.payload.deviceId + }); + setDeviceId(event.payload.deviceId); + justCompletedBootloaderUpdate.current = false; + } + }); + }; + + setupListener(); + + return () => { + if (unsubscribe) { + unsubscribe(); + } + }; + }, [deviceId]); + + // Determine which steps to use based on flow type + const ALL_STEPS = flowType === 'recover' ? RECOVER_ALL_STEPS : CREATE_ALL_STEPS; + const VISIBLE_STEPS = flowType === 'recover' ? RECOVER_VISIBLE_STEPS : CREATE_VISIBLE_STEPS; + + const handleNext = () => { + console.log("=== SetupWizard handleNext called ==="); + console.log("Current step:", currentStep); + console.log("Current step ID:", ALL_STEPS[currentStep].id); + console.log("Total steps:", ALL_STEPS.length); + console.log("Flow type:", flowType); + + if (currentStep < ALL_STEPS.length - 1) { + const nextStep = currentStep + 1; + const nextStepId = ALL_STEPS[nextStep].id; + console.log("Moving to next step:", nextStep, "which is:", nextStepId); + setCurrentStep(nextStep); + console.log("setCurrentStep called with:", nextStep); + } else { + console.log("At final step, calling handleComplete"); + handleComplete(); + } + console.log("=== handleNext completed ==="); + }; + + const handlePrevious = () => { + if (currentStep > 0) { + // If going back from a step after flow type is chosen, reset flow type + const createOrRecoverIndex = ALL_STEPS.findIndex(step => step.id === 'create-or-recover'); + if (currentStep > createOrRecoverIndex && ALL_STEPS[currentStep].id !== 'create-or-recover') { + // Going back to or before the create-or-recover step + if (currentStep - 1 <= createOrRecoverIndex) { + setFlowType(null); + } + } + setCurrentStep(currentStep - 1); + } + }; + + const handleComplete = async () => { + console.log("=== Starting setup completion ==="); + try { + // Mark setup as completed + console.log("Calling set_onboarding_completed..."); + await invoke("set_onboarding_completed"); + console.log("set_onboarding_completed completed successfully"); + + // Call the completion callback if provided + if (onComplete) { + console.log("Calling onComplete callback"); + onComplete(); + } + + // Use multiple methods to ensure the dialog closes + if (onClose) { + console.log("Calling onClose callback"); + onClose(); + } + + // Use the dialog context directly to force close after a short delay + setTimeout(() => { + hide('setup-wizard'); + console.log('Forced setup wizard closure via DialogContext'); + }, 100); + } catch (error) { + console.error("Failed to mark setup as completed:", error); + } + }; + + const handleFlowTypeSelection = (type: 'create' | 'recover') => { + setFlowType(type); + handleNext(); + }; + + const updateWizardData = (data: Partial) => { + setWizardData(prev => ({ ...prev, ...data })); + }; + + const handleBootloaderUpdateComplete = () => { + console.log('πŸ”„ SetupWizard: Bootloader update completed, expecting device ID change'); + justCompletedBootloaderUpdate.current = true; + }; + + const StepComponent = ALL_STEPS[currentStep].component; + + // Debug current step + console.log("SetupWizard render - currentStep:", currentStep, "stepId:", ALL_STEPS[currentStep].id, "component:", StepComponent.name); + + // Calculate progress based on visible steps + const currentStepId = ALL_STEPS[currentStep].id; + const visibleStepIndex = VISIBLE_STEPS.findIndex(step => step.id === currentStepId); + + // If we're past all visible steps, show 100% progress + let actualProgress = 0; + if (visibleStepIndex >= 0) { + actualProgress = ((visibleStepIndex + 1) / VISIBLE_STEPS.length) * 100; + } else { + // Check if we're past all visible steps + const lastVisibleStepId = VISIBLE_STEPS[VISIBLE_STEPS.length - 1].id; + const lastVisibleStepIndex = ALL_STEPS.findIndex(step => step.id === lastVisibleStepId); + if (currentStep > lastVisibleStepIndex) { + actualProgress = 100; + } + } + + // Props to pass to step components + const stepProps = { + deviceId, + wizardData, + updateWizardData, + onNext: handleNext, + onBack: handlePrevious, + onFlowTypeSelect: handleFlowTypeSelection, + flowType, + onBootloaderUpdateComplete: handleBootloaderUpdateComplete, + onFirmwareUpdateStart, + onFirmwareUpdateComplete, + }; + + return ( + + {/* Header */} + + + + KeepKey Bitcoin Setup + + + {ALL_STEPS[currentStep].description} + + + + + {/* Progress */} + + + 0 ? "green.500" : highlightColor} + borderRadius="full" + transition="width 0.3s, background-color 0.3s" + w={`${actualProgress}%`} + /> + + + + {/* Step indicators with improved responsive layout */} + + + {VISIBLE_STEPS.map((step, index) => { + const stepIndex = ALL_STEPS.findIndex(s => s.id === step.id); + const lastVisibleStepId = VISIBLE_STEPS[VISIBLE_STEPS.length - 1].id; + const lastVisibleStepIndex = ALL_STEPS.findIndex(s => s.id === lastVisibleStepId); + const isPastAllVisible = currentStep > lastVisibleStepIndex; + + const isCompleted = isPastAllVisible || (stepIndex !== -1 && stepIndex < currentStep); + const isCurrent = !isPastAllVisible && ALL_STEPS[currentStep]?.id === step.id; + const isActive = isCompleted || isCurrent; + + return ( + + + {isCompleted ? ( + + ) : ( + + {step.number} + + )} + + + {step.label} + + {index < VISIBLE_STEPS.length - 1 && ( + + )} + + ); + })} + + + + {/* Content */} + + + + + + + {/* Footer */} + + + + {visibleStepIndex >= 0 + ? `Step ${visibleStepIndex + 1} of ${VISIBLE_STEPS.length}` + : (currentStep > 0 ? 'Setting up wallet...' : '') + } + + + + {/* Only show Next button if not on flow selection step or if flow is selected */} + {(ALL_STEPS[currentStep].id !== 'create-or-recover' || flowType) && ( + + )} + + + + + ); +} \ No newline at end of file diff --git a/projects/vault-v2/src/components/SetupWizard/index.ts b/projects/vault-v2/src/components/SetupWizard/index.ts new file mode 100644 index 0000000..342f9c6 --- /dev/null +++ b/projects/vault-v2/src/components/SetupWizard/index.ts @@ -0,0 +1 @@ +export { SetupWizard } from './SetupWizard'; \ No newline at end of file diff --git a/projects/vault-v2/src/components/SetupWizard/steps/Step0Welcome.tsx b/projects/vault-v2/src/components/SetupWizard/steps/Step0Welcome.tsx new file mode 100644 index 0000000..2f0d25f --- /dev/null +++ b/projects/vault-v2/src/components/SetupWizard/steps/Step0Welcome.tsx @@ -0,0 +1,47 @@ +import { VStack, Text, Icon, Box } from "@chakra-ui/react"; +import { FaBitcoin } from "react-icons/fa"; +import { useEffect } from "react"; + +interface Step0WelcomeProps { + onNext: () => void; +} + +export function Step0Welcome({ onNext }: Step0WelcomeProps) { + // Auto-advance after 3 seconds + useEffect(() => { + const timer = setTimeout(() => { + onNext(); + }, 3000); + + return () => clearTimeout(timer); + }, [onNext]); + + return ( + + + + + + + + Welcome to KeepKey + + + Bitcoin-Only Edition + + + The secure hardware wallet focused exclusively on Bitcoin + + + + + Setting up your device... + + + ); +} \ No newline at end of file diff --git a/projects/vault-v2/src/components/SetupWizard/steps/Step1CreateOrRecover.tsx b/projects/vault-v2/src/components/SetupWizard/steps/Step1CreateOrRecover.tsx new file mode 100644 index 0000000..be2744e --- /dev/null +++ b/projects/vault-v2/src/components/SetupWizard/steps/Step1CreateOrRecover.tsx @@ -0,0 +1,128 @@ +import { VStack, Text, Button, Box, Icon, HStack } from "@chakra-ui/react"; +import { FaPlus, FaKey } from "react-icons/fa"; + +interface Step1CreateOrRecoverProps { + onFlowTypeSelect: (type: 'create' | 'recover') => void; +} + +export function Step1CreateOrRecover({ onFlowTypeSelect }: Step1CreateOrRecoverProps) { + return ( + + + + Choose Setup Method + + + Create a new wallet or restore an existing one + + + + + {/* Create New Wallet */} + onFlowTypeSelect('create')} + > + + + + + + + Create New Wallet + + + Generate a new recovery phrase and set up a fresh wallet + + + + + + + {/* Recover Existing Wallet */} + onFlowTypeSelect('recover')} + > + + + + + + + Recover Wallet + + + Restore your wallet using an existing recovery phrase + + + + + + + + ); +} \ No newline at end of file diff --git a/projects/vault-v2/src/components/SetupWizard/steps/Step2DeviceLabel.tsx b/projects/vault-v2/src/components/SetupWizard/steps/Step2DeviceLabel.tsx new file mode 100644 index 0000000..a1667ec --- /dev/null +++ b/projects/vault-v2/src/components/SetupWizard/steps/Step2DeviceLabel.tsx @@ -0,0 +1,126 @@ +import { VStack, Text, Input, Button, HStack, Box } from "@chakra-ui/react"; +import React, { useState } from "react"; +import { invoke } from "@tauri-apps/api/core"; + +interface Step2DeviceLabelProps { + deviceId: string; + wizardData: any; + updateWizardData: (data: any) => void; + onNext: () => void; + onBack: () => void; +} + +export function Step2DeviceLabel({ + deviceId, + wizardData, + updateWizardData, + onNext, + onBack +}: Step2DeviceLabelProps) { + const [label, setLabel] = useState(wizardData.deviceLabel || ""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Skip this step if we should skip device label (after backup completion) + React.useEffect(() => { + if (wizardData.skipDeviceLabel) { + console.log("Skipping device label step, moving to completion"); + onNext(); + } + }, [wizardData.skipDeviceLabel, onNext]); + + const handleSubmit = async () => { + setIsLoading(true); + setError(null); + + try { + if (label.trim()) { + await invoke('set_device_label', { deviceId, label: label.trim() }); + } + updateWizardData({ deviceLabel: label.trim() || 'KeepKey' }); + onNext(); + } catch (err: any) { + console.error("Failed to set device label:", err); + + // If the error is about PIN, skip setting the label for now + if (err.toString().includes('PIN')) { + console.warn("Device requires PIN for label setting, skipping for now"); + updateWizardData({ deviceLabel: label.trim() || 'KeepKey', labelPending: true }); + onNext(); + } else { + setError(`Failed to set device label: ${err}`); + } + } finally { + setIsLoading(false); + } + }; + + const handleSkip = () => { + updateWizardData({ deviceLabel: 'KeepKey' }); + onNext(); + }; + + return ( + + + + Name Your Device + + + Give your KeepKey a friendly name to identify it easily + + + + + setLabel(e.target.value)} + size="lg" + bg="gray.700" + borderColor="gray.600" + _hover={{ borderColor: "gray.500" }} + _focus={{ borderColor: "orange.500", boxShadow: "0 0 0 1px orange.500" }} + color="white" + disabled={isLoading} + onKeyPress={(e) => { + if (e.key === 'Enter' && label.trim()) { + handleSubmit(); + } + }} + /> + {error && ( + + {error} + + )} + + + + + + + + + ); +} \ No newline at end of file diff --git a/projects/vault-v2/src/components/SetupWizard/steps/Step3Pin.tsx b/projects/vault-v2/src/components/SetupWizard/steps/Step3Pin.tsx new file mode 100644 index 0000000..4ea4b55 --- /dev/null +++ b/projects/vault-v2/src/components/SetupWizard/steps/Step3Pin.tsx @@ -0,0 +1,48 @@ +import { Box } from "@chakra-ui/react"; +import { DevicePinHorizontal } from "../../WalletCreationWizard/DevicePinHorizontal"; + +interface Step3PinProps { + deviceId: string; + wizardData: any; + updateWizardData: (data: any) => void; + onNext: () => void; + onBack: () => void; +} + +export function Step3Pin({ + deviceId, + wizardData, + updateWizardData, + onNext, + onBack +}: Step3PinProps) { + + const handlePinComplete = (pinSession: any) => { + console.log("Step3Pin: handlePinComplete called!"); + console.log("Step3Pin: PIN completed, session:", pinSession); + console.log("Step3Pin: Session details:", JSON.stringify(pinSession, null, 2)); + + // Update wizard data + updateWizardData({ pinSession }); + console.log("Step3Pin: Updated wizard data with pinSession"); + + // Call onNext immediately + console.log("Step3Pin: About to call onNext()..."); + onNext(); + console.log("Step3Pin: onNext() has been called"); + }; + + return ( + + + + ); +} \ No newline at end of file diff --git a/projects/vault-v2/src/components/SetupWizard/steps/Step4BackupOrRecover.tsx b/projects/vault-v2/src/components/SetupWizard/steps/Step4BackupOrRecover.tsx new file mode 100644 index 0000000..2e970ca --- /dev/null +++ b/projects/vault-v2/src/components/SetupWizard/steps/Step4BackupOrRecover.tsx @@ -0,0 +1,176 @@ +import { Box, VStack, Text, Button } from "@chakra-ui/react"; +import { RecoveryFlow } from "../../WalletCreationWizard/RecoveryFlow"; +import { RecoverySettings } from "../../WalletCreationWizard/RecoverySettings"; +import { invoke } from "@tauri-apps/api/core"; +import { useState } from "react"; + +interface Step4BackupOrRecoverProps { + deviceId: string; + wizardData: any; + updateWizardData: (data: any) => void; + onNext: () => void; + onBack: () => void; + flowType: 'create' | 'recover' | null; +} + +export function Step4BackupOrRecover({ + deviceId, + wizardData, + updateWizardData, + onNext, + onBack, + flowType +}: Step4BackupOrRecoverProps) { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [showRecoverySettings, setShowRecoverySettings] = useState(true); + + console.log("Step4BackupOrRecover rendered with flowType:", flowType); + + // Create flow - show backup phrase + if (flowType === 'create') { + const handleBackupComplete = async () => { + setIsLoading(true); + try { + await invoke('complete_wallet_creation', { deviceId }); + // Update wizard data to indicate we should skip device label + updateWizardData({ skipDeviceLabel: true }); + onNext(); + } catch (err) { + console.error("Failed to complete wallet creation:", err); + setError(`Failed to complete wallet creation: ${err}`); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + Look at Your Device + + + + + + Your recovery phrase is displayed on your KeepKey screen + + + Write down each word exactly as shown on the device + + + + + + + ⚠️ YOU WILL ONLY SEE THIS ONCE + + + This is by design - the phrase cannot be retrieved later + + + + + + Take your time to write down all words carefully + + + The device will wait for your confirmation + + + + + {error && ( + + {error} + + )} + + + + ); + } + + // Recover flow - show recovery options then recovery flow + if (flowType === 'recover') { + if (showRecoverySettings && !wizardData.recoverySettings) { + const handleRecoverySettingsComplete = (settings: any) => { + updateWizardData({ recoverySettings: settings }); + setShowRecoverySettings(false); + }; + + return ( + + ); + } + + const handleRecoveryComplete = async () => { + setIsLoading(true); + try { + await invoke('complete_recovery', { deviceId }); + onNext(); + } catch (err) { + console.error("Failed to complete recovery:", err); + setError(`Failed to complete recovery: ${err}`); + } finally { + setIsLoading(false); + } + }; + + const handleRecoveryError = (error: string) => { + setError(error); + }; + + return ( + { + setShowRecoverySettings(true); + updateWizardData({ recoverySettings: null }); + }} + /> + ); + } + + return null; +} \ No newline at end of file diff --git a/projects/vault-v2/src/components/SetupWizard/steps/Step5Complete.tsx b/projects/vault-v2/src/components/SetupWizard/steps/Step5Complete.tsx new file mode 100644 index 0000000..8d509f9 --- /dev/null +++ b/projects/vault-v2/src/components/SetupWizard/steps/Step5Complete.tsx @@ -0,0 +1,99 @@ +import { VStack, Text, Icon, Box, Button } from "@chakra-ui/react"; +import { keyframes } from "@emotion/react"; +import { FaCheckCircle } from "react-icons/fa"; +import { useEffect } from "react"; + +interface Step5CompleteProps { + wizardData: any; + flowType: 'create' | 'recover' | null; + onNext: () => void; +} + +const confettiFall = keyframes` + 0% { transform: translateY(-100vh) rotate(0deg); opacity: 1; } + 100% { transform: translateY(100vh) rotate(720deg); opacity: 0; } +`; + +const pulse = keyframes` + 0% { transform: scale(1); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } +`; + +export function Step5Complete({ wizardData, flowType, onNext }: Step5CompleteProps) { + // Auto-complete after 5 seconds + useEffect(() => { + const timer = setTimeout(() => { + onNext(); + }, 5000); + + return () => clearTimeout(timer); + }, [onNext]); + + const isRecovery = flowType === 'recover'; + + // Generate confetti pieces + const confettiColors = ['#10b981', '#f59e0b', '#ef4444', '#3b82f6', '#8b5cf6', '#ec4899']; + const confettiPieces = Array.from({ length: 50 }, (_, i) => ({ + id: i, + color: confettiColors[i % confettiColors.length], + left: `${Math.random() * 100}%`, + delay: `${Math.random() * 3}s`, + duration: `${3 + Math.random() * 2}s` + })); + + return ( + + {/* Confetti animation */} + {confettiPieces.map(piece => ( + + ))} + + + + + + + + + πŸŽ‰ {isRecovery ? 'Wallet Recovered!' : 'Wallet Created!'} + + + Your KeepKey {wizardData.deviceLabel && `"${wizardData.deviceLabel}"`} is ready + + + {isRecovery + ? 'Your wallet has been successfully restored from your recovery phrase' + : 'Your new Bitcoin wallet is now secure and ready to use' + } + + + + + + + ); +} \ No newline at end of file diff --git a/projects/vault-v2/src/components/SetupWizard/steps/StepBootloaderUpdate.tsx b/projects/vault-v2/src/components/SetupWizard/steps/StepBootloaderUpdate.tsx new file mode 100644 index 0000000..8a25fe7 --- /dev/null +++ b/projects/vault-v2/src/components/SetupWizard/steps/StepBootloaderUpdate.tsx @@ -0,0 +1,350 @@ +import { VStack, HStack, Text, Button, Box, Icon, Image, Spinner } from "@chakra-ui/react"; +import { FaShieldAlt, FaExclamationTriangle } from "react-icons/fa"; +import { useState, useEffect } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import holdAndConnectSvg from '../../../assets/svg/hold-and-connect.svg'; + +interface StepBootloaderUpdateProps { + deviceId: string; + onNext: () => void; + onBack: () => void; + onBootloaderUpdateComplete?: () => void; +} + + +export function StepBootloaderUpdate({ deviceId, onNext, onBack, onBootloaderUpdateComplete }: StepBootloaderUpdateProps) { + const [deviceStatus, setDeviceStatus] = useState(null); + const [isUpdating, setIsUpdating] = useState(false); + const [showBootloaderInstructions, setShowBootloaderInstructions] = useState(false); + + useEffect(() => { + // Only check device status if we have a deviceId + if (deviceId) { + checkDeviceStatus(); + } + + // If showing bootloader instructions, periodically check if device entered bootloader mode + let intervalId: NodeJS.Timeout | null = null; + if (showBootloaderInstructions && deviceId) { + intervalId = setInterval(() => { + checkDeviceStatus(); + }, 2000); // Check every 2 seconds + } + + return () => { + if (intervalId) { + clearInterval(intervalId); + } + }; + }, [deviceId, showBootloaderInstructions]); + + const checkDeviceStatus = async () => { + try { + const status = await invoke('get_device_status', { deviceId }); + + // Check if status is null or undefined + if (!status) { + console.error("Device status is null or undefined"); + // Don't show error - this is normal during setup + return; + } + + setDeviceStatus(status); + + // Check if device is in bootloader mode + const isInBootloaderMode = status?.features?.bootloader_mode || status?.features?.bootloaderMode || false; + console.log('Bootloader mode check:', { + hasFeatures: !!status?.features, + bootloader_mode: status?.features?.bootloader_mode, + bootloaderMode: status?.features?.bootloaderMode, + isInBootloaderMode, + needsBootloaderUpdate: status?.needsBootloaderUpdate + }); + + // Simple bootloader check - needs to be 2.1.5 + const currentBootloaderVersion = status?.bootloaderCheck?.currentVersion; + + console.log("Bootloader check:", { + currentVersion: currentBootloaderVersion, + needsUpdate: status?.needsBootloaderUpdate + }); + + // Check if bootloader needs update (anything not 2.1.5) + const needsBootloaderUpdate = currentBootloaderVersion !== "2.1.5" && currentBootloaderVersion !== "2.1.4"; + + if (needsBootloaderUpdate && currentBootloaderVersion) { + console.log("Bootloader update needed: v" + currentBootloaderVersion + " β†’ v2.1.5"); + // Show update UI + if (!isInBootloaderMode) { + console.log("Device needs bootloader update but not in bootloader mode"); + setShowBootloaderInstructions(true); + } else if (isInBootloaderMode && showBootloaderInstructions) { + console.log("Device is now in bootloader mode for update"); + setShowBootloaderInstructions(false); + } + return; // Stay on this step + } + + // Bootloader is good, move to next step + if (currentBootloaderVersion === "2.1.5" || currentBootloaderVersion === "2.1.4") { + console.log("Bootloader is up to date, proceeding to next step"); + onNext(); + return; + } else if (!isInBootloaderMode) { + // Device needs bootloader update but is not in bootloader mode + console.log("Device needs bootloader update but not in bootloader mode"); + setShowBootloaderInstructions(true); + } else if (isInBootloaderMode && showBootloaderInstructions) { + // Device has entered bootloader mode, hide instructions + console.log("Device is now in bootloader mode"); + setShowBootloaderInstructions(false); + } + } catch (err) { + console.error("Failed to get device status:", err); + // Don't show error - this is normal during setup + } + }; + + const handleBootloaderUpdate = async () => { + // First check if device is in bootloader mode + const isInBootloaderMode = deviceStatus?.features?.bootloader_mode || deviceStatus?.features?.bootloaderMode || false; + + if (!isInBootloaderMode) { + setShowBootloaderInstructions(true); + return; + } + + setIsUpdating(true); + + try { + // Start bootloader update + const success = await invoke('update_device_bootloader', { + deviceId, + targetVersion: deviceStatus.bootloaderCheck?.latestVersion || '' + }); + + // Notify that bootloader update completed + if (onBootloaderUpdateComplete) { + onBootloaderUpdateComplete(); + } + + // Wait longer for device to reconnect after bootloader update + console.log('πŸ”„ Bootloader update complete, waiting for device to reconnect...'); + setTimeout(() => { + onNext(); + }, 5000); // Increased to 5 seconds + + } catch (err) { + console.error("Failed to update bootloader:", err); + const errorMsg = String(err); + + // Check if the error is because device is not in bootloader mode + if (errorMsg.includes('bootloader mode') || errorMsg.includes('Bootloader mode')) { + setShowBootloaderInstructions(true); + } + // Don't show any errors to the user + setIsUpdating(false); + } + }; + + + if (!deviceStatus) { + return ( + + + + Follow Directions on device... + + + ); + } + + // If showing bootloader instructions + if (showBootloaderInstructions) { + return ( + + + {/* Left side - Instructions and image */} + + + + + Enter Firmware Update Mode + + + + + To update the firmware, your device must be in Update Mode + + + + Hold button while connecting device + + + + {/* Right side - Steps and actions */} + + + Quick Steps: + 1. Unplug your KeepKey device + 2. Hold the button and plug it back in + 3. Follow directions on device + 4. Release the button + + + + + + ); + } + + // Check if this is an old bootloader that MUST be updated + const currentBootloaderVersion = deviceStatus?.bootloaderCheck?.currentVersion || "unknown"; + const isOldBootloader = currentBootloaderVersion === "2.0.0" || + currentBootloaderVersion.startsWith("1.") || + currentBootloaderVersion === "2.0.1" || + currentBootloaderVersion === "2.0.2" || + currentBootloaderVersion === "2.0.3" || + currentBootloaderVersion === "2.0.4"; + + return ( + + + {/* Left side - Icon and status */} + + + + + + Firmware Updater + + {isOldBootloader ? ( + <> + + Update available for bootloader v{currentBootloaderVersion} + + + Let's update to the latest version for the best experience + + + ) : deviceStatus.needsBootloaderUpdate ? ( + + Your KeepKey bootloader needs to be updated! + + ) : ( + + Your bootloader is up to date! + + )} + + + {/* Update info box for old bootloaders */} + {isOldBootloader && ( + + + + Update Required: + + + β€’ Your device has bootloader v{currentBootloaderVersion} + + + β€’ We'll update it to v2.1.5 for improved security + + + β€’ This is a required step in the setup process + + + + )} + + + {/* Right side - Details and actions */} + + {deviceStatus.bootloaderCheck && ( + + + + + Current Version + + + v{deviceStatus.bootloaderCheck.currentVersion} + + + + + Latest Version + + + v{deviceStatus.bootloaderCheck.latestVersion} + + + + + )} + + + {isUpdating && ( + + + + + + On the KeepKey, it will ask you to verify backup. We will do this after updating, hold the button to skip this for now to continue. + + + + + + Follow directions on device + + + Your KeepKey will guide you through the update process. + + + Do not disconnect your device during the update. + + + + )} + + + {(deviceStatus.needsBootloaderUpdate || isOldBootloader) && !isUpdating && ( + + )} + {!deviceStatus.needsBootloaderUpdate && !isOldBootloader && ( + + βœ… Bootloader verified - v{currentBootloaderVersion} + + )} + + + + + ); +} \ No newline at end of file diff --git a/projects/vault-v2/src/components/SetupWizard/steps/StepFirmwareUpdate.tsx b/projects/vault-v2/src/components/SetupWizard/steps/StepFirmwareUpdate.tsx new file mode 100644 index 0000000..74ec904 --- /dev/null +++ b/projects/vault-v2/src/components/SetupWizard/steps/StepFirmwareUpdate.tsx @@ -0,0 +1,589 @@ +import { VStack, HStack, Text, Button, Box, Icon, Progress, Badge, Alert, Spinner } from "@chakra-ui/react"; +import { FaDownload, FaExclamationTriangle } from "react-icons/fa"; +import { useState, useEffect, useRef } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; + +interface StepFirmwareUpdateProps { + deviceId: string; + onNext: () => void; + onBack: () => void; + onFirmwareUpdateStart?: () => void; + onFirmwareUpdateComplete?: () => void; +} + +type UpdateState = 'idle' | 'loading_firmware' | 'erasing' | 'waiting_confirmation' | 'uploading' | 'complete'; + +// CSS animation for striped progress bar +const stripeAnimationStyle = ` + @keyframes stripeAnimation { + 0% { background-position: 0 0; } + 100% { background-position: 40px 0; } + } +`; + +export function StepFirmwareUpdate({ deviceId, onNext, onBack, onFirmwareUpdateStart, onFirmwareUpdateComplete }: StepFirmwareUpdateProps) { + const [deviceStatus, setDeviceStatus] = useState(null); + const [isUpdating, setIsUpdating] = useState(false); + const [error, setError] = useState(null); + const [updateState, setUpdateState] = useState('idle'); + const [updateProgress, setUpdateProgress] = useState(0); + const [isWaitingForReboot, setIsWaitingForReboot] = useState(false); + const progressIntervalRef = useRef(null); + const unlistenRef = useRef<(() => void) | null>(null); + const rebootPollRef = useRef(null); + + useEffect(() => { + checkDeviceStatus(); + }, [deviceId]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (rebootPollRef.current) { + clearInterval(rebootPollRef.current); + } + }; + }, []); + + // Set up event listener for firmware update events + useEffect(() => { + if (!isUpdating) return; + + const setupListener = async () => { + unlistenRef.current = await listen('firmware:update-status', (event) => { + const { status, progress } = event.payload as { status: string; progress?: number }; + console.log('Firmware update status:', status, progress); + + switch (status) { + case 'loading_firmware': + setUpdateState('loading_firmware'); + break; + case 'firmware_erase': + setUpdateState('erasing'); + break; + case 'button_request': + setUpdateState('waiting_confirmation'); + break; + case 'firmware_upload': + setUpdateState('uploading'); + // Start progress animation when upload begins + if (!progressIntervalRef.current) { + let prog = 0; + progressIntervalRef.current = setInterval(() => { + prog += (100 / 60); // 100% over 60 seconds + if (prog >= 100) { + prog = 100; + if (progressIntervalRef.current) { + clearInterval(progressIntervalRef.current); + } + } + setUpdateProgress(prog); + }, 1000); + } + break; + case 'complete': + setUpdateState('complete'); + if (progressIntervalRef.current) { + clearInterval(progressIntervalRef.current); + } + setUpdateProgress(100); + break; + } + }); + }; + + setupListener(); + + return () => { + if (unlistenRef.current) { + unlistenRef.current(); + } + if (progressIntervalRef.current) { + clearInterval(progressIntervalRef.current); + } + }; + }, [isUpdating]); + + const checkDeviceStatus = async () => { + try { + const status = await invoke('get_device_status', { deviceId }); + setDeviceStatus(status); + + // Simple check: if firmware is 7.10.0, we're good to go + const currentVersion = status?.firmwareCheck?.currentVersion; + + console.log("Firmware check:", { + currentVersion, + needsUpdate: status?.needsFirmwareUpdate + }); + + // If firmware is already 7.10.0, skip to next step + if (currentVersion === "7.10.0") { + console.log("Firmware is 7.10.0, skipping to next step"); + onNext(); + } + // Otherwise show the update screen + } catch (err) { + console.error("Failed to get device status:", err); + // Don't show scary red error, just log it + } + }; + + const handleFirmwareUpdate = async () => { + setIsUpdating(true); + setError(null); + setUpdateProgress(0); + // Start with loading state - the event listener will update based on actual events + setUpdateState('loading_firmware'); + + // Notify parent that firmware update is starting + if (onFirmwareUpdateStart) { + onFirmwareUpdateStart(); + } + + try { + // Actually invoke the firmware update + // The event listener will handle updating the UI based on actual device events + await invoke('update_device_firmware', { + deviceId, + targetVersion: deviceStatus.firmwareCheck?.latestVersion || '' + }); + + // If we get here, the update was successful + setUpdateState('complete'); + + // Notify parent that firmware update is complete + if (onFirmwareUpdateComplete) { + onFirmwareUpdateComplete(); + } + + // After firmware update, device will reboot - wait longer and let the backend handle reconnection + console.log("Firmware update complete - device will reboot, waiting for reconnection..."); + setIsWaitingForReboot(true); + + // Wait 10 seconds before starting to poll - device needs time to fully reboot + console.log("Waiting 10 seconds for device to reboot..."); + setTimeout(() => { + console.log("Starting to poll for device reconnection..."); + + // Start polling for device reconnection after reboot + let pollAttempts = 0; + const maxPollAttempts = 20; // 20 attempts over 10 seconds after initial wait + + rebootPollRef.current = setInterval(async () => { + pollAttempts++; + console.log(`Polling for device after reboot (attempt ${pollAttempts}/${maxPollAttempts})...`); + + try { + // First try with the original device ID + const status = await invoke('get_device_status', { deviceId }); + + if (status?.firmwareCheck?.currentVersion === "7.10.0") { + console.log("βœ… Device reconnected with firmware 7.10.0!"); + + // Clear the polling interval + if (rebootPollRef.current) { + clearInterval(rebootPollRef.current); + rebootPollRef.current = null; + } + + setIsWaitingForReboot(false); + setIsUpdating(false); + setDeviceStatus(status); + + // Move to next step + onNext(); + } + } catch (err) { + // If the original device ID fails, try to find any connected device + try { + console.log("Original device ID failed, looking for any connected device..."); + const devices = await invoke('list_devices'); + + if (devices && devices.length > 0) { + // Take the first device found + const newDeviceId = devices[0].unique_id || devices[0].id; + console.log(`Found device with ID: ${newDeviceId}`); + + // Try to get status with the new device ID + const status = await invoke('get_device_status', { deviceId: newDeviceId }); + + if (status?.firmwareCheck?.currentVersion === "7.10.0") { + console.log("βœ… Device reconnected with firmware 7.10.0 (new ID)!"); + + // Clear the polling interval + if (rebootPollRef.current) { + clearInterval(rebootPollRef.current); + rebootPollRef.current = null; + } + + setIsWaitingForReboot(false); + setIsUpdating(false); + setDeviceStatus(status); + + // Move to next step + onNext(); + } + } else { + console.log("No devices found yet, continuing to poll..."); + } + } catch (listErr) { + console.log("Device not ready yet, continuing to poll..."); + } + } + + if (pollAttempts >= maxPollAttempts) { + console.error("Device did not reconnect after firmware update"); + if (rebootPollRef.current) { + clearInterval(rebootPollRef.current); + rebootPollRef.current = null; + } + setIsWaitingForReboot(false); + setIsUpdating(false); + setError("Device did not reconnect after update. Please unplug and reconnect your device."); + } + }, 500); // Poll every 500ms + }, 10000); // Wait 10 seconds before starting to poll + + } catch (err) { + console.error("Failed to update firmware:", err); + setError(`Failed to update firmware: ${err}`); + setIsUpdating(false); + setUpdateState('idle'); + // Clear any running progress interval + if (progressIntervalRef.current) { + clearInterval(progressIntervalRef.current); + } + } + }; + + + if (!deviceStatus) { + return ( + + + + Follow directions on device... + + + ); + } + + const isOOBDevice = deviceStatus.firmwareCheck?.currentVersion === "4.0.0"; + + return ( + + + {/* Left side - Icon and status */} + + + + + + Firmware Update + + {!deviceStatus.firmwareCheck ? ( + + + + Checking firmware version... + + + ) : deviceStatus.needsFirmwareUpdate ? ( + <> + + A new firmware version is available for your KeepKey + + {isOOBDevice && ( + + Critical Update Required + + )} + + ) : ( + + βœ… Firmware verified - v{deviceStatus.firmwareCheck?.currentVersion} + + )} + + + {/* Important Instructions */} + + + + ⚠️ Important Instructions: + + + β€’ Do not disconnect your device during the update + + + β€’ You may need to re-enter your PIN after the update + + + β€’ Your funds and settings will remain safe + + + + + + {/* Right side - Details and actions */} + + {!deviceStatus.firmwareCheck ? ( + + + + + Detecting firmware version... + + + + ) : ( + + + + + Current Version + + + v{deviceStatus.firmwareCheck.currentVersion} + + + + + Latest Version + + + v{deviceStatus.firmwareCheck.latestVersion} + + + + {isOOBDevice && ( + + ⚠️ Your device has factory firmware. Update is highly recommended. + + )} + + )} + + {error && ( + + + {String(error)} + + + )} + + {isUpdating && ( + + + + {/* Status Messages */} + + {/* Loading Firmware */} + {['loading_firmware', 'erasing', 'waiting_confirmation', 'uploading', 'complete'].includes(updateState) && ( + + + + πŸ“¦ Loaded firmware binary: 577,720 bytes + + + )} + + {/* Device in bootloader mode */} + {['erasing', 'waiting_confirmation', 'uploading', 'complete'].includes(updateState) && ( + + + + βœ… Device confirmed in bootloader mode + + + )} + + {/* Firmware Erase */} + {['erasing', 'waiting_confirmation', 'uploading', 'complete'].includes(updateState) && ( + + + + {updateState === 'erasing' ? 'πŸ”„' : 'βœ…'} Firmware Erase + + + )} + + {/* Waiting for confirmation */} + {updateState === 'waiting_confirmation' && ( + + + + + + + Confirm action on device! + + + + Look at your KeepKey screen and press the button to confirm. + + + Note: If your device is not set up, you can safely ignore any "verify backup" screen. + + + + )} + + + {/* Progress Bar - Only show during actual upload */} + {updateState === 'uploading' && ( + <> + + Uploading firmware... Do not disconnect your device + + + + + + {Math.round(updateProgress)}% - Estimated time remaining: {Math.max(0, 60 - Math.round(updateProgress * 0.6))}s + + + Your device will restart when complete. + + + )} + + {updateState === 'complete' && !isWaitingForReboot && ( + + + βœ… Firmware update complete! + + + )} + + {isWaitingForReboot && ( + + + + + + Device is rebooting... + + + + Your KeepKey is restarting with the new firmware. + + + This may take a few seconds. Please wait... + + + + )} + + )} + + + {/* Show verification status */} + {deviceStatus.firmwareCheck && ( + + + Status: + {!deviceStatus.firmwareCheck.currentVersion || !deviceStatus.firmwareCheck.latestVersion ? ( + Checking... + ) : deviceStatus.firmwareCheck.currentVersion === deviceStatus.firmwareCheck.latestVersion ? ( + βœ“ Up to date + ) : ( + Update Available + )} + + + )} + + {!deviceStatus.firmwareCheck ? ( + + ) : deviceStatus.needsFirmwareUpdate && !isUpdating ? ( + + ) : deviceStatus.firmwareCheck?.currentVersion === "7.10.0" && !isUpdating ? ( + + ) : isUpdating ? null : ( + + )} + + + + + ); +} \ No newline at end of file diff --git a/projects/vault-v2/src/components/VaultInterface.tsx b/projects/vault-v2/src/components/VaultInterface.tsx index d2ffd06..febaa97 100644 --- a/projects/vault-v2/src/components/VaultInterface.tsx +++ b/projects/vault-v2/src/components/VaultInterface.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { Box, Flex, Button, Text, HStack, useDisclosure } from '@chakra-ui/react'; import { FaTh, FaGlobe, FaWallet, FaCog, FaQuestionCircle } from 'react-icons/fa'; -import { listen } from '@tauri-apps/api/event'; +import { listen, emit } from '@tauri-apps/api/event'; import { invoke } from '@tauri-apps/api/core'; import splashBg from '../assets/splash-bg.png'; import { SettingsDialog } from './SettingsDialog'; @@ -10,6 +10,7 @@ import { WalletProvider, useWallet } from '../contexts/WalletContext'; import Send from './Send'; import Receive from './Receive'; import { useDialog } from '../contexts/DialogContext'; +import packageJson from '../../package.json'; // import { AppHeader } from './AppHeader'; type ViewType = 'apps' | 'browser' | 'pairings' | 'vault' | 'assets' | 'send' | 'receive' | 'portfolio'; @@ -46,18 +47,21 @@ export const VaultInterface = () => { const handleSupportClick = async () => { try { + // First try to open in integrated browser await invoke('vault_open_support'); + console.log('Opening support in integrated browser'); } catch (error) { - console.error('Failed to open support via backend:', error); - // Fallback to direct open - try { - const { invoke } = await import('@tauri-apps/api/core'); - await invoke('open_url', { url: 'https://support.keepkey.com' }); - } catch (error) { - console.error('Failed to open URL:', error); - // Fallback to window.open if Tauri command fails - window.open('https://support.keepkey.com', '_blank'); - } + console.error('Failed to open support via integrated browser:', error); + // Fallback to opening in external browser + try { + await invoke('open_url', { url: 'https://support.keepkey.com' }); + console.log('Opening support in external browser'); + } catch (fallbackError) { + console.error('Failed to open URL via Tauri:', fallbackError); + // Last resort: use window.open + window.open('https://support.keepkey.com', '_blank'); + console.log('Opening support via window.open'); + } } }; @@ -73,12 +77,18 @@ export const VaultInterface = () => { icon: , onClick: () => handleViewChange('vault'), }, - { - id: 'browser', - label: 'Browser', - icon: , - onClick: () => handleViewChange('browser'), - }, + // { + // id: 'browser', + // label: 'Browser', + // icon: , + // onClick: async () => { + // handleViewChange('browser'); + // // Always navigate to keepkey.com when browser button is clicked + // setTimeout(async () => { + // await emit('browser:navigate', { url: 'http://localhost:8080' }); + // }, 100); + // }, + // }, { id: 'settings', label: 'Settings', @@ -157,52 +167,92 @@ export const VaultInterface = () => { {/* Main Vault Interface - Hidden when settings is open */} {!isSettingsOpen && ( - {/* Main Content Area */} - - {renderCurrentView()} - - - {/* Bottom Navigation */} + {/* Top Navigation Bar */} - - {navItems.map((item) => ( - - ))} + + {/* Left side - Main navigation items */} + + {navItems.slice(0, 1).map((item) => ( + + ))} + + + {/* Center - Logo/Title with Version */} + + + KeepKey Vault + + + v{packageJson.version} + + + + {/* Right side - Settings and Support */} + + {navItems.slice(1).map((item) => ( + + ))} + + + {/* Main Content Area */} + + {renderCurrentView()} + )} diff --git a/projects/vault-v2/src/components/WalletCreationWizard/DevicePinHorizontal.tsx b/projects/vault-v2/src/components/WalletCreationWizard/DevicePinHorizontal.tsx new file mode 100644 index 0000000..bd89f99 --- /dev/null +++ b/projects/vault-v2/src/components/WalletCreationWizard/DevicePinHorizontal.tsx @@ -0,0 +1,473 @@ +import { + Button, + Text, + VStack, + Box, + HStack, + SimpleGrid, + Input, + Icon, + Heading, + Image, + Flex, + Spinner, + Center, +} from "@chakra-ui/react"; +import { useState, useCallback, useRef, useEffect } from "react"; +import { FaCircle, FaChevronDown, FaChevronRight, FaInfoCircle } from "react-icons/fa"; +import cipherImage from "../../assets/onboarding/cipher.png"; +import { PinService } from "../../services/pinService"; +import { PinCreationSession, PinStep, PinPosition, PIN_MATRIX_LAYOUT } from "../../types/pin"; + +interface DevicePinHorizontalProps { + deviceId: string; + deviceLabel?: string; + mode: 'create' | 'confirm'; + onComplete: (session: PinCreationSession) => void; + onBack?: () => void; + isLoading?: boolean; + error?: string | null; +} + +export function DevicePinHorizontal({ + deviceId, + deviceLabel, + mode, + onComplete, + onBack, + isLoading = false, + error +}: DevicePinHorizontalProps) { + const [positions, setPositions] = useState([]); + const [showMoreInfo, setShowMoreInfo] = useState(false); + const [session, setSession] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [stepError, setStepError] = useState(null); + const [isInitializing, setIsInitializing] = useState(true); + const [isTransitioning, setIsTransitioning] = useState(false); + const inputRef = useRef(null); + + // Dynamic title and description based on session state + const getTitle = () => { + if (!session) return 'Initializing PIN Setup...'; + console.log("getTitle - session.current_step:", session.current_step, "PinStep.AwaitingSecond:", PinStep.AwaitingSecond); + switch (session.current_step) { + case PinStep.AwaitingFirst: + return 'Create Your PIN'; + case PinStep.AwaitingSecond: + return 'Confirm Your PIN'; + default: + return 'PIN Setup'; + } + }; + + const getDescription = () => { + if (!session) return 'Starting PIN creation session with your KeepKey device...'; + switch (session.current_step) { + case PinStep.AwaitingFirst: + return 'Use PIN layout shown on your device to find the location to press on the PIN pad.'; + case PinStep.AwaitingSecond: + return 'Re-enter your PIN to confirm it matches.'; + default: + return 'Processing PIN setup...'; + } + }; + + // Initialize PIN creation session with delay + useEffect(() => { + const initializeSession = async () => { + if (!session && mode === 'create') { + setIsInitializing(true); + try { + // Add 2 second delay to ensure device is ready and user is ready + console.log("DevicePinHorizontal: Waiting 2 seconds before starting PIN creation..."); + await new Promise(resolve => setTimeout(resolve, 2000)); + + console.log("DevicePinHorizontal: Starting PIN creation for device:", deviceId); + const newSession = await PinService.startPinCreation(deviceId, deviceLabel); + setSession(newSession); + setStepError(null); + } catch (error) { + console.error("PIN creation initialization error:", error); + setStepError(`Failed to start PIN creation: ${error}`); + } finally { + setIsInitializing(false); + } + } else { + setIsInitializing(false); + } + }; + + initializeSession(); + }, [deviceId, mode, session, deviceLabel]); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + // Track session state changes + useEffect(() => { + console.log("Session state changed:", session); + if (session) { + console.log("Current step:", session.current_step); + console.log("Is active:", session.is_active); + console.log("Session ID:", session.session_id); + } + }, [session]); + + const handlePinPress = useCallback((position: PinPosition) => { + console.log("handlePinPress called, current positions:", positions.length, "session step:", session?.current_step); + if (positions.length < 9 && !isLoading && !isSubmitting) { + setPositions(prev => [...prev, position]); + console.log("Added position, new length will be:", positions.length + 1); + } else { + console.log("PIN press blocked - length:", positions.length, "isLoading:", isLoading, "isSubmitting:", isSubmitting); + } + }, [positions.length, isLoading, isSubmitting, session]); + + const handleBackspace = useCallback(() => { + if (!isLoading && !isSubmitting) { + setPositions(prev => prev.slice(0, -1)); + } + }, [isLoading, isSubmitting]); + + const handleSubmit = useCallback(async () => { + console.log("handleSubmit called with positions:", positions.length, "session step:", session?.current_step); + if (positions.length > 0 && !isLoading && !isSubmitting && session) { + console.log("Proceeding with PIN submission for step:", session.current_step); + + // If this is the second PIN, show transition immediately + if (session.current_step === PinStep.AwaitingSecond) { + console.log("Second PIN submission - showing transition immediately!"); + setIsTransitioning(true); + // Still submit the PIN but don't wait for response + PinService.sendPinResponse(session.session_id, positions).then(() => { + console.log("Second PIN sent successfully"); + }).catch(err => { + console.error("Error sending second PIN:", err); + }); + + // Call onComplete after a brief delay + setTimeout(() => { + onComplete(session); + }, 1000); + return; + } + + setIsSubmitting(true); + setStepError(null); + + try { + // Validate positions first + const validation = PinService.validatePositions(positions); + if (!validation.valid) { + setStepError(validation.error!); + setIsSubmitting(false); + return; + } + + // Send PIN response + console.log("About to send PIN response for step:", session.current_step); + console.log("Sending positions:", positions); + + let result; + try { + result = await PinService.sendPinResponse(session.session_id, positions); + console.log("PIN response result:", result); + console.log("Current session step before processing:", session.current_step); + } catch (pinError) { + console.error("Error sending PIN response:", pinError); + setStepError(`Failed to send PIN: ${pinError}`); + setIsSubmitting(false); + return; + } + + if (result.success) { + // The AwaitingSecond case is already handled above with early return + + // Check if PIN creation is complete based on result + if (result.next_step === 'complete') { + console.log("PIN creation complete! Calling onComplete..."); + console.log("Setting isTransitioning to true"); + setIsTransitioning(true); + // PIN creation is complete, get final session status + const finalSession = await PinService.getSessionStatus(session.session_id); + console.log("Final session status:", finalSession); + console.log("About to call onComplete callback"); + onComplete(finalSession || session); + console.log("onComplete callback called"); + } else if (result.next_step === 'confirm') { + console.log("PIN needs confirmation, updating UI..."); + // Need to confirm PIN, get updated session + const updatedSession = await PinService.getSessionStatus(session.session_id); + console.log("Updated session after first PIN:", updatedSession); + console.log("Raw session JSON:", JSON.stringify(updatedSession, null, 2)); + if (updatedSession) { + setSession(updatedSession); + setPositions([]); + console.log("Reset positions, ready for second PIN"); + console.log("Current step should now be:", updatedSession.current_step); + console.log("Session state updated, component should re-render"); + // Force focus back to input after state update + setTimeout(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, 100); + } + } else { + console.log("Checking session status for other cases..."); + console.log("result.next_step value:", result.next_step); + console.log("result object:", JSON.stringify(result, null, 2)); + + // Check if the result indicates completion without explicit 'complete' value + if (!result.next_step || result.next_step === '') { + console.log("No next_step specified, checking session status..."); + const finalSession = await PinService.getSessionStatus(session.session_id); + console.log("Session status check result:", finalSession); + if (finalSession && finalSession.current_step === PinStep.Completed) { + console.log("Session is completed! Triggering completion flow..."); + setIsTransitioning(true); + onComplete(finalSession); + return; + } + } + + // Get updated session status for other cases + const updatedSession = await PinService.getSessionStatus(session.session_id); + console.log("Updated session for other cases:", updatedSession); + if (updatedSession) { + if (updatedSession.current_step === PinStep.Completed) { + console.log("Session shows completed, calling onComplete..."); + setIsTransitioning(true); + onComplete(updatedSession); + } else if (updatedSession.current_step === PinStep.AwaitingSecond) { + console.log("Session shows awaiting second PIN..."); + setSession(updatedSession); + setPositions([]); + } else { + console.log("Unexpected session state:", updatedSession.current_step); + setStepError(`Unexpected state: ${updatedSession.current_step}`); + } + } + } + } else { + setStepError(result.error || 'Failed to process PIN'); + } + } catch (error) { + console.error("PIN submission error:", error); + setStepError(`Failed to submit PIN: ${error}`); + } finally { + setIsSubmitting(false); + } + } + }, [positions, isLoading, isSubmitting, session, deviceId, onComplete]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (isLoading || isSubmitting) return; + + if (e.key === 'Backspace') { + handleBackspace(); + } else if (e.key === 'Enter') { + handleSubmit(); + } else if (PIN_MATRIX_LAYOUT.includes(Number(e.key) as any)) { + handlePinPress(Number(e.key) as PinPosition); + } + }, [handleBackspace, handleSubmit, handlePinPress, isLoading, isSubmitting]); + + // Generate PIN dots for display + const maxDotsToShow = Math.max(4, positions.length + (positions.length < 8 ? 1 : 0)); + const pinDots = Array.from({ length: Math.min(maxDotsToShow, 8) }, (_, i) => ( + + )); + + // Show loading spinner during initialization + if (isInitializing) { + return ( +
+ + + + Initializing PIN setup on device... + + +
+ ); + } + + // Show transition state when PIN is complete + if (isTransitioning) { + return ( +
+ + + + PIN setup complete! Preparing recovery phrase... + + +
+ ); + } + + return ( + + + {/* Left side - PIN Entry */} + + + + {getTitle()} + + + {getDescription()} + + + + {/* PIN Dots Display */} + + + {pinDots} + + + + {/* PIN Length Hints */} + + {positions.length === 0 && ( + + πŸ’‘ We recommend using 4 digits for optimal security + + )} + {positions.length > 0 && positions.length < 4 && ( + + {4 - positions.length} more digit{4 - positions.length !== 1 ? 's' : ''} recommended + + )} + {positions.length === 4 && ( + + βœ… Perfect! 4 digits provides great security + + )} + + + + {/* Right side - PIN Pad */} + + {/* Scrambled PIN Grid */} + + + {PIN_MATRIX_LAYOUT.map((position) => ( + + ))} + + + + {/* Action Buttons */} + + + + + + + + {/* Hidden input for keyboard support */} + {}} + onKeyDown={handleKeyDown} + position="absolute" + opacity={0} + pointerEvents="none" + aria-hidden="true" + /> + + {/* Error display */} + {(stepError || error) && ( + + + {stepError || error} + + + )} + + ); +} \ No newline at end of file diff --git a/projects/vault-v2/src/components/views/BrowserView.tsx b/projects/vault-v2/src/components/views/BrowserView.tsx index d0fbfca..644a43c 100644 --- a/projects/vault-v2/src/components/views/BrowserView.tsx +++ b/projects/vault-v2/src/components/views/BrowserView.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Box, Input, @@ -9,25 +9,57 @@ import { IconButton, Spinner } from '@chakra-ui/react'; -import { FaArrowLeft, FaArrowRight, FaRedo, FaHome, FaSearch } from 'react-icons/fa'; +import { FaArrowLeft, FaArrowRight, FaRedo, FaHome, FaSearch, FaExternalLinkAlt } from 'react-icons/fa'; import { listen } from '@tauri-apps/api/event'; import { invoke } from '@tauri-apps/api/core'; export const BrowserView = () => { - const [url, setUrl] = useState('https://keepkey.com'); - const [inputUrl, setInputUrl] = useState('https://keepkey.com'); + // Use proxy server for keepkey.com to avoid CORS issues + const [url, setUrl] = useState('http://localhost:8080'); + const [inputUrl, setInputUrl] = useState('keepkey.com'); const [isLoading, setIsLoading] = useState(true); const [canGoBack, setCanGoBack] = useState(false); const [canGoForward, setCanGoForward] = useState(false); const [showControls, setShowControls] = useState(false); const [hoverTimer, setHoverTimer] = useState(null); + const [apiStatus, setApiStatus] = useState<'checking' | 'connected' | 'error'>('checking'); + const [pendingNavigation, setPendingNavigation] = useState(null); + const [externalLinkMessage, setExternalLinkMessage] = useState(null); + const iframeRef = useRef(null); + + // Check API connectivity on mount + useEffect(() => { + checkApiConnection(); + }, []); + + const checkApiConnection = async () => { + try { + const response = await fetch('http://localhost:1646/spec/swagger.json'); + if (response.ok) { + setApiStatus('connected'); + console.log('βœ… API server is connected at localhost:1646'); + } else { + setApiStatus('error'); + console.error('❌ API server returned error:', response.status); + } + } catch (error) { + setApiStatus('error'); + console.error('❌ Failed to connect to API server:', error); + } + }; const handleNavigate = async () => { if (!inputUrl) return; - // Simple URL validation and formatting + // Use proxy server for KeepKey domains let formattedUrl = inputUrl; - if (!inputUrl.startsWith('http://') && !inputUrl.startsWith('https://')) { + + // Check if it's a KeepKey domain + if (inputUrl.includes('keepkey.com') || inputUrl === 'vault' || inputUrl === 'vault.keepkey.com') { + // Always use proxy for KeepKey domains + formattedUrl = 'http://localhost:8080'; + } else if (!inputUrl.startsWith('http://') && !inputUrl.startsWith('https://')) { + // For non-KeepKey domains, add https:// formattedUrl = `https://${inputUrl}`; } @@ -36,16 +68,17 @@ export const BrowserView = () => { await invoke('browser_navigate', { url: formattedUrl }); } catch (error) { console.error('Failed to notify backend of navigation:', error); - // Still navigate even if backend call fails - setUrl(formattedUrl); - setInputUrl(formattedUrl); - setIsLoading(true); } + + // Update the UI + setUrl(formattedUrl); + setIsLoading(true); }; const handleHome = () => { - const homeUrl = 'https://keepkey.com'; - setInputUrl(homeUrl); + // Use proxy for keepkey.com + const homeUrl = 'http://localhost:8080'; + setInputUrl('keepkey.com'); setUrl(homeUrl); setIsLoading(true); }; @@ -53,7 +86,7 @@ export const BrowserView = () => { const handleRefresh = () => { setIsLoading(true); // Force iframe reload by changing src - const iframe = document.getElementById('browser-iframe') as HTMLIFrameElement; + const iframe = iframeRef.current; if (iframe) { iframe.src = iframe.src; } @@ -78,17 +111,57 @@ export const BrowserView = () => { const handleIframeLoad = () => { setIsLoading(false); // Try to get the actual URL from iframe (limited by CORS) - const iframe = document.getElementById('browser-iframe') as HTMLIFrameElement; + const iframe = iframeRef.current; if (iframe) { try { + // Try to inject a script to handle link clicks (may be blocked by CORS) + const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; + if (iframeDoc && iframe.contentWindow?.location.origin === window.location.origin) { + // Only works for same-origin content + const links = iframeDoc.getElementsByTagName('a'); + for (let i = 0; i < links.length; i++) { + const link = links[i]; + if (link.target === '_blank' || link.target === '_new') { + link.addEventListener('click', (e) => { + e.preventDefault(); + handleExternalLink(link.href); + }); + } + } + } setInputUrl(iframe.contentWindow?.location.href || url); } catch (e) { - // Cross-origin restrictions prevent accessing iframe URL + // Cross-origin restrictions prevent accessing iframe content + console.log('Cross-origin iframe, cannot access content'); setInputUrl(url); } } }; + const handleExternalLink = async (linkUrl: string) => { + console.log('External link clicked:', linkUrl); + + // Check if it's a documentation or external link + if (linkUrl.includes('docs') || linkUrl.includes('github') || !linkUrl.includes('keepkey.com')) { + // Open in external browser + try { + await invoke('open_url', { url: linkUrl }); + // Show a temporary message + setExternalLinkMessage(`Opening ${linkUrl} in external browser...`); + setTimeout(() => setExternalLinkMessage(null), 3000); + } catch (error) { + console.error('Failed to open external link:', error); + // Fallback: navigate in iframe + setUrl(linkUrl); + setInputUrl(linkUrl); + } + } else { + // Navigate within the iframe + setUrl(linkUrl); + setInputUrl(linkUrl); + } + }; + const handleMouseEnter = () => { // Clear any existing timer if (hoverTimer) { @@ -124,9 +197,26 @@ export const BrowserView = () => { unlisten = await listen('browser:navigate', (event) => { const { url: newUrl } = event.payload as { url: string }; console.log('Received backend navigation command:', newUrl); - setUrl(newUrl); - setInputUrl(newUrl); - setIsLoading(true); + + // Handle special case for support.keepkey.com + if (newUrl.includes('support.keepkey.com')) { + // Navigate directly to support URL + setUrl(newUrl); + setInputUrl('support.keepkey.com'); + setIsLoading(true); + + // Also update the iframe directly to ensure navigation + setTimeout(() => { + const iframe = iframeRef.current; + if (iframe && iframe.src !== newUrl) { + iframe.src = newUrl; + } + }, 50); + } else { + setUrl(newUrl); + setInputUrl(newUrl); + setIsLoading(true); + } }); } catch (error) { console.error('Failed to set up browser event listeners:', error); @@ -139,6 +229,17 @@ export const BrowserView = () => { if (unlisten) unlisten(); }; }, []); + + // Handle pending navigation once component is ready + useEffect(() => { + if (pendingNavigation) { + console.log('Processing pending navigation to:', pendingNavigation); + setUrl(pendingNavigation); + setInputUrl(pendingNavigation.includes('support.keepkey.com') ? 'support.keepkey.com' : pendingNavigation); + setIsLoading(true); + setPendingNavigation(null); + } + }, [pendingNavigation]); return ( @@ -194,6 +295,16 @@ export const BrowserView = () => { > + handleExternalLink(url)} + title="Open current page in external browser" + > + +
{/* Address Bar */} @@ -259,8 +370,28 @@ export const BrowserView = () => { )} + {externalLinkMessage && ( + + + {externalLinkMessage} + + )} + {/* Embedded Website */}