[
  {
    "path": ".dockerignore",
    "content": "node_modules\n.gitignore\n.github\n*.md\ndist\njustfile\n.env.local\n"
  },
  {
    "path": ".eslintrc.cjs",
    "content": "module.exports = {\n    env: {\n        browser: true,\n        es2021: true,\n        node: true\n    },\n    extends: [\n        \"eslint:recommended\",\n        \"plugin:@typescript-eslint/recommended\",\n        \"plugin:solid/typescript\",\n        \"plugin:import/typescript\",\n        \"plugin:import/recommended\"\n    ],\n    overrides: [\n        {\n            files: [\"**/*.cjs\"], // Specify the file pattern for CJS files\n            parserOptions: {\n                sourceType: \"script\" // Treat the CJS files as CommonJS modules\n            },\n            rules: {\n                \"@typescript-eslint/no-var-requires\": \"off\" // Disable this specific rule for CJS files\n            }\n        }\n    ],\n    parser: \"@typescript-eslint/parser\",\n    parserOptions: {\n        tsconfigRootDir: \"./\",\n        project: \"tsconfig.json\",\n        ecmaVersion: \"latest\",\n        sourceType: \"module\",\n        ecmaFeatures: {\n            jsx: true\n        }\n    },\n    plugins: [\"@typescript-eslint\", \"solid\", \"import\"],\n    rules: {\n        \"@typescript-eslint/no-unused-vars\": [\n            \"warn\",\n            {\n                argsIgnorePattern: \"^_\",\n                destructuredArrayIgnorePattern: \"^_\",\n                varsIgnorePattern: \"^_\"\n            }\n        ],\n        \"solid/reactivity\": \"warn\",\n        \"solid/no-destructure\": \"warn\",\n        \"solid/jsx-no-undef\": \"error\",\n        \"@typescript-eslint/no-non-null-assertion\": \"off\"\n    },\n    settings: {\n        \"import/parsers\": {\n            \"@typescript-eslint/parser\": [\".ts\", \".tsx\"]\n        },\n        \"import/resolver\": {\n            typescript: {\n                project: [\"./tsconfig.json\"],\n                alwaysTryTypes: true\n            }\n        }\n    }\n};\n"
  },
  {
    "path": ".github/workflows/android-build.yml",
    "content": "name: Build Android\n\non:\n  push:\n    branches:\n      - master\n      - prod\n  pull_request:\n    branches:\n      - '*'\n\njobs:\n  build:\n    name: Build APK\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Setup java\n        uses: actions/setup-java@v3\n        with:\n          distribution: 'zulu'\n          java-version: '17'\n\n      - name: Cache gradle\n        uses: actions/cache@v1\n        with:\n          path: ~/.gradle/caches\n          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}\n          restore-keys: |\n            ${{ runner.os }}-gradle-\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18.x\n\n      - uses: pnpm/action-setup@v4\n        name: Install pnpm\n        id: pnpm-install\n        with:\n          run_install: false\n\n      - name: Get pnpm store directory\n        id: pnpm-cache\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path)\" >> $GITHUB_OUTPUT\n\n      - uses: actions/cache@v3\n        name: Setup pnpm cache\n        with:\n          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n\n      - name: Install app dependencies\n        run: pnpm install\n\n      - name: Build SolidJS app\n        env:\n          VITE_NETWORK: signet\n          VITE_PROXY: wss://p.mutinywallet.com\n          VITE_ESPLORA: https://mutinynet.com/api\n          VITE_LSP: https://mutinynet-flow.lnolymp.us\n          VITE_RGS: https://rgs.mutinynet.com/snapshot/\n          VITE_AUTH: https://auth-staging.mutinywallet.com\n          VITE_SUBSCRIPTIONS: https://subscriptions-staging.mutinywallet.com\n          VITE_STORAGE: https://storage-staging.mutinywallet.com/v2\n          VITE_FEEDBACK: https://feedback-staging.mutinywallet.com\n          VITE_SCORER: https://scorer-staging.mutinywallet.com\n          VITE_PRIMAL: https://primal-cache.mutinywallet.com/api\n        run: pnpm build\n\n      - name: Capacitor sync\n        run: npx cap sync\n\n      - name: Build APK (gradle)\n        working-directory: android\n        run: ./gradlew assembleDebug --no-daemon\n\n      - name: Upload APK\n        uses: actions/upload-artifact@v2\n        with:\n          name: Debug APK\n          path: android/app/build/outputs/apk/fdroid/debug/app-fdroid-universal-debug.apk\n"
  },
  {
    "path": ".github/workflows/android-prod.yml",
    "content": "name: Release Android Prod\n\non:\n  push:\n    tags:\n    - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10\n\njobs:\n  build:\n    name: Build APK\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Setup java\n        uses: actions/setup-java@v3\n        with:\n          distribution: 'zulu'\n          java-version: '17'\n\n      - name: Cache gradle\n        uses: actions/cache@v1\n        with:\n          path: ~/.gradle/caches\n          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}\n          restore-keys: |\n            ${{ runner.os }}-gradle-\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18.x\n\n      - uses: pnpm/action-setup@v4\n        name: Install pnpm\n        id: pnpm-install\n        with:\n          run_install: false\n\n      - name: Get pnpm store directory\n        id: pnpm-cache\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path)\" >> $GITHUB_OUTPUT\n\n      - uses: actions/cache@v3\n        name: Setup pnpm cache\n        with:\n          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n\n      - name: Install app dependencies\n        run: pnpm install\n\n      - name: Build SolidJS app\n        env:\n          VITE_NETWORK: bitcoin\n          VITE_PROXY: wss://p.mutinywallet.com\n          VITE_ESPLORA: https://mutiny.mempool.space/api\n          VITE_LSP: https://0conf.lnolymp.us\n          VITE_RGS: https://scorer.mutinywallet.com/v1/rgs/snapshot/\n          VITE_AUTH: https://auth.mutinywallet.com\n          VITE_SUBSCRIPTIONS: https://subscriptions.mutinywallet.com\n          VITE_STORAGE: https://storage.mutinywallet.com/v2\n          VITE_FEEDBACK: https://feedback.mutinywallet.com\n          VITE_SCORER: https://scorer.mutinywallet.com\n          VITE_PRIMAL: https://primal-cache.mutinywallet.com/api\n          VITE_BLIND_AUTH: https://blind-auth.mutinywallet.com\n          VITE_HERMES: https://mutiny.plus\n        run: pnpm build\n\n      - name: Capacitor sync\n        run: npx cap sync\n\n      - name: Build AAB\n        working-directory: android\n        run: ./gradlew clean bundleRelease --stacktrace\n\n      - name: Sign AAB (F-Droid)\n        uses: r0adkll/sign-android-release@v1\n        with:\n          releaseDirectory: android/app/build/outputs/bundle/fdroidRelease\n          signingKeyBase64: ${{ secrets.SIGNING_KEY }}\n          alias: ${{ secrets.KEY_ALIAS }}\n          keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}\n          keyPassword: ${{ secrets.KEY_PASSWORD }}\n        env:\n          BUILD_TOOLS_VERSION: \"33.0.0\"\n\n      - name: Build APK\n        working-directory: android\n        run: ./gradlew assembleRelease --stacktrace\n\n      - name: Sign APK (F-Droid)\n        uses: r0adkll/sign-android-release@v1\n        with:\n          releaseDirectory: android/app/build/outputs/apk/fdroid/release\n          signingKeyBase64: ${{ secrets.SIGNING_KEY }}\n          alias: ${{ secrets.KEY_ALIAS }}\n          keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}\n          keyPassword: ${{ secrets.KEY_PASSWORD }}\n        env:\n          BUILD_TOOLS_VERSION: \"33.0.0\"\n\n      - name: Create Release\n        id: create_release\n        uses: actions/create-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: Android Release ${{ github.ref }}\n          draft: false\n          prerelease: false \n\n      # F-Droid APK\n      - name: Upload F-Droid APK Universal Asset\n        id: upload-release-asset-fdroid-universal-apk\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: android/app/build/outputs/apk/fdroid/release/app-fdroid-universal-release-unsigned-signed.apk\n          asset_name: mutiny-wallet-fdroid-universal-${{ github.ref_name }}.apk\n          asset_content_type: application/zip\n\n      - name: Upload F-Droid APK x86 Asset\n        id: upload-release-asset-fdroid-x86-apk\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: android/app/build/outputs/apk/fdroid/release/app-fdroid-x86-release-unsigned-signed.apk\n          asset_name: mutiny-wallet-fdroid-x86-${{ github.ref_name }}.apk\n          asset_content_type: application/zip\n\n      - name: Upload F-Droid APK x86_64 Asset\n        id: upload-release-asset-fdroid-x86-64-apk\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: android/app/build/outputs/apk/fdroid/release/app-fdroid-x86_64-release-unsigned-signed.apk\n          asset_name: mutiny-wallet-fdroid-x86_64-${{ github.ref_name }}.apk\n          asset_content_type: application/zip\n\n      # FDroid AAB\n      - name: Upload F-Droid AAB Asset\n        id: upload-release-asset-fdroid-aab\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: android/app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab\n          asset_name: mutiny-wallet-fdroid-${{ github.ref_name }}.aab\n          asset_content_type: application/zip\n"
  },
  {
    "path": ".github/workflows/android-staging.yml",
    "content": "name: Release Android Staging\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - master\n\njobs:\n  build:\n    name: Build APK\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Setup java\n        uses: actions/setup-java@v3\n        with:\n          distribution: 'zulu'\n          java-version: '17'\n\n      - name: Cache gradle\n        uses: actions/cache@v1\n        with:\n          path: ~/.gradle/caches\n          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}\n          restore-keys: |\n            ${{ runner.os }}-gradle-\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18.x\n\n      - uses: pnpm/action-setup@v4\n        name: Install pnpm\n        id: pnpm-install\n        with:\n          run_install: false\n\n      - name: Get pnpm store directory\n        id: pnpm-cache\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path)\" >> $GITHUB_OUTPUT\n\n      - uses: actions/cache@v3\n        name: Setup pnpm cache\n        with:\n          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n\n      - name: Install app dependencies\n        run: pnpm install\n\n      - name: Build SolidJS app\n        env:\n          VITE_NETWORK: signet\n          VITE_PROXY: wss://p.mutinywallet.com\n          VITE_ESPLORA: https://mutinynet.com/api\n          VITE_LSP: https://mutinynet-flow.lnolymp.us\n          VITE_RGS: https://rgs.mutinynet.com/snapshot/\n          VITE_AUTH: https://auth-staging.mutinywallet.com\n          VITE_SUBSCRIPTIONS: https://subscriptions-staging.mutinywallet.com\n          VITE_STORAGE: https://storage-staging.mutinywallet.com/v2\n          VITE_FEEDBACK: https://feedback-staging.mutinywallet.com\n          VITE_SCORER: https://scorer-staging.mutinywallet.com\n          VITE_PRIMAL: https://primal-cache.mutinywallet.com/api\n          VITE_BLIND_AUTH: https://blind-auth-staging.mutinywallet.com\n          VITE_HERMES: https://signet.mutiny.plus\n        run: pnpm build\n\n      - name: Capacitor sync\n        run: npx cap sync\n\n      - name: Build AAB\n        working-directory: android\n        run: ./gradlew clean bundleRelease --stacktrace\n\n      - name: Sign AAB (F-Droid)\n        uses: r0adkll/sign-android-release@v1\n        with:\n          releaseDirectory: android/app/build/outputs/bundle/fdroidRelease\n          signingKeyBase64: ${{ secrets.SIGNING_KEY }}\n          alias: ${{ secrets.KEY_ALIAS }}\n          keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}\n          keyPassword: ${{ secrets.KEY_PASSWORD }}\n        env:\n          BUILD_TOOLS_VERSION: \"33.0.0\"\n\n      - name: Build APK\n        working-directory: android\n        run: ./gradlew assembleRelease --stacktrace\n\n      - name: Sign APK (F-Droid)\n        uses: r0adkll/sign-android-release@v1\n        with:\n          releaseDirectory: android/app/build/outputs/apk/fdroid/release\n          signingKeyBase64: ${{ secrets.SIGNING_KEY }}\n          alias: ${{ secrets.KEY_ALIAS }}\n          keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}\n          keyPassword: ${{ secrets.KEY_PASSWORD }}\n        env:\n          BUILD_TOOLS_VERSION: \"33.0.0\"\n\n      # F-Droid APK\n      - name: Upload F-Droid APK Universal Asset\n        id: upload-release-asset-fdroid-universal-apk\n        uses: actions/upload-artifact@v3\n        with:\n          path: android/app/build/outputs/apk/fdroid/release/app-fdroid-universal-release-unsigned-signed.apk\n          name: mutiny-wallet-fdroid-universal-master.apk\n\n      - name: Upload F-Droid APK x86 Asset\n        id: upload-release-asset-fdroid-x86-apk\n        uses: actions/upload-artifact@v3\n        with:\n          path: android/app/build/outputs/apk/fdroid/release/app-fdroid-x86-release-unsigned-signed.apk\n          name: mutiny-wallet-fdroid-x86-master.apk\n\n      - name: Upload F-Droid APK x86_64 Asset\n        id: upload-release-asset-fdroid-x86-64-apk\n        uses: actions/upload-artifact@v3\n        with:\n          path: android/app/build/outputs/apk/fdroid/release/app-fdroid-x86_64-release-unsigned-signed.apk\n          name: mutiny-wallet-fdroid-x86_64-master.apk\n\n      # FDroid AAB\n      - name: Upload F-Droid AAB Asset\n        uses: actions/upload-artifact@v3\n        with:\n          path: android/app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab\n          name: mutiny-wallet-fdroid-master.aab\n"
  },
  {
    "path": ".github/workflows/code-quality.yml",
    "content": "name: Code Quality Check\non:\n  push:\n    branches:\n      - master\n      - prod\n  pull_request:\n    branches:\n      - '*'\n\njobs:\n  code_quality:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18.x\n\n      - uses: pnpm/action-setup@v4\n        name: Install pnpm\n        id: pnpm-install\n        with:\n          run_install: false\n\n      - name: Get pnpm store directory\n        id: pnpm-cache\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path)\" >> $GITHUB_OUTPUT\n\n      - uses: actions/cache@v3\n        name: Setup pnpm cache\n        with:\n          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n\n      - name: Step Zero - Install Dependencies\n        run: pnpm install\n\n      - name: Step One - Check Code Formatting\n        run: pnpm run check-format\n\n      - name: Step Two - Check Lint Errors\n        run: pnpm run eslint\n\n      - name: Step Three - Check Type Check Errors\n        run: pnpm run check-types\n\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: Create and publish Docker images\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - master\n  release:\n    types: [ published ]\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  build-and-push-image:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: mutinywallet\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v3\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm64\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/ios-prod.yml",
    "content": "name: Release iOS Prod\n\non:\n    workflow_dispatch:\n    push:\n        branches:\n        - master\n\njobs:\n  build:\n    name: Build iOS\n    runs-on: macos-14\n    timeout-minutes: 180\n    steps:\n        - name: Checkout source\n          uses: actions/checkout@v3\n\n        - uses: pnpm/action-setup@v4\n          name: Install pnpm\n          id: pnpm-install\n          with:\n            run_install: false\n\n        - name: Setup Node.js\n          uses: actions/setup-node@v3\n          with:\n            node-version: 18.x\n            cache: 'pnpm'\n\n        # Install dependencies using pnpm\n        - name: Install Dependencies\n          run: pnpm install\n\n        - name: Build SolidJS app\n          env:\n            VITE_NETWORK: bitcoin\n            VITE_PROXY: wss://p.mutinywallet.com\n            VITE_ESPLORA: https://mutiny.mempool.space/api\n            VITE_LSP: https://0conf.lnolymp.us\n            VITE_RGS: https://scorer.mutinywallet.com/v1/rgs/snapshot/\n            VITE_AUTH: https://auth.mutinywallet.com\n            VITE_SUBSCRIPTIONS: https://subscriptions.mutinywallet.com\n            VITE_STORAGE: https://storage.mutinywallet.com/v2\n            VITE_FEEDBACK: https://feedback.mutinywallet.com\n            VITE_SCORER: https://scorer.mutinywallet.com\n            VITE_PRIMAL: https://primal-cache.mutinywallet.com/api\n            VITE_BLIND_AUTH: https://blind-auth.mutinywallet.com\n            VITE_HERMES: https://mutiny.plus\n          run: pnpm build\n\n        - uses: actions/cache@v3\n          with:\n            path: ios/App/Pods\n            key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}\n            restore-keys: |\n              ${{ runner.os }}-pods-\n\n        - name: Capacitor sync\n          run: npx cap sync\n        \n        - uses: ruby/setup-ruby@v1\n          with:\n            ruby-version: '3.3'\n            bundler-cache: true\n            working-directory: 'ios/App'\n\n        - uses: maxim-lobanov/setup-xcode@v1\n          with:\n            xcode-version: '15.2'\n\n        - uses: maierj/fastlane-action@v3.0.0\n          env:\n            DEVELOPER_APP_IDENTIFIER: ${{ secrets.DEVELOPER_APP_IDENTIFIER }}\n            KEYCHAIN_NAME: ${{ secrets.KEYCHAIN_NAME }}\n            KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}\n            APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}\n            APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}\n            APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}\n            CERTIFICATE_STORE_URL: ${{ secrets.CERTIFICATE_STORE_URL }}\n            GIT_USERNAME: ${{ secrets.GIT_USERNAME }}\n            GIT_TOKEN: ${{ secrets.GIT_TOKEN }}\n            FASTLANE_APPLE_ID: ${{ secrets.FASTLANE_APPLE_ID }}\n            MATCH_USERNAME: ${{ secrets.FASTLANE_APPLE_ID }}\n            MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}\n            APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}\n            DEVELOPER_PORTAL_TEAM_ID: ${{ secrets.DEVELOPER_PORTAL_TEAM_ID }}\n          with:\n            lane: 'beta'\n            subdirectory: 'ios/App'\n\n\n"
  },
  {
    "path": ".github/workflows/ios-staging.yml",
    "content": "name: Build iOS Staging\n\non:\n  workflow_dispatch:\n  pull_request:\n    branches:\n      - '*'\n\njobs:\n  build:\n    name: Build iOS\n    runs-on: macos-14\n    timeout-minutes: 180\n    steps:\n        - name: Checkout source\n          uses: actions/checkout@v3\n\n        - uses: pnpm/action-setup@v4\n          name: Install pnpm\n          id: pnpm-install\n          with:\n            run_install: false\n\n        - name: Setup Node.js\n          uses: actions/setup-node@v3\n          with:\n            node-version: 18.x\n            cache: 'pnpm'\n\n        # Install dependencies using pnpm\n        - name: Install Dependencies\n          run: pnpm install\n\n        - name: Build SolidJS app\n          env:\n            VITE_NETWORK: signet\n            VITE_PROXY: wss://p.mutinywallet.com\n            VITE_ESPLORA: https://mutinynet.com/api\n            VITE_LSP: https://mutinynet-flow.lnolymp.us\n            VITE_RGS: https://rgs.mutinynet.com/snapshot/\n            VITE_AUTH: https://auth-staging.mutinywallet.com\n            VITE_SUBSCRIPTIONS: https://subscriptions-staging.mutinywallet.com\n            VITE_STORAGE: https://storage-staging.mutinywallet.com/v2\n            VITE_FEEDBACK: https://feedback-staging.mutinywallet.com\n            VITE_SCORER: https://scorer-staging.mutinywallet.com\n            VITE_PRIMAL: https://primal-cache.mutinywallet.com/api\n            VITE_BLIND_AUTH: https://blind-auth-staging.mutinywallet.com\n            VITE_HERMES: https://signet.mutiny.plus\n          run: pnpm build\n\n        - uses: actions/cache@v3\n          with:\n            path: ios/App/Pods\n            key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}\n            restore-keys: |\n              ${{ runner.os }}-pods-\n\n        - name: Capacitor sync\n          run: npx cap sync\n        \n        - uses: ruby/setup-ruby@v1\n          with:\n            ruby-version: '3.3'\n            bundler-cache: true\n            working-directory: 'ios/App'\n\n        - uses: maxim-lobanov/setup-xcode@v1\n          with:\n            xcode-version: '15.2'\n\n        - uses: maierj/fastlane-action@v3.0.0\n          env:\n            DEVELOPER_APP_IDENTIFIER: ${{ secrets.DEVELOPER_APP_IDENTIFIER }}\n            # PROVISIONING_PROFILE_SPECIFIER: match AppStore ${{ secrets.DEVELOPER_APP_IDENTIFIER }}\n            KEYCHAIN_NAME: ${{ secrets.KEYCHAIN_NAME }}\n            KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}\n            APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}\n            APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}\n            APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}\n            CERTIFICATE_STORE_URL: ${{ secrets.CERTIFICATE_STORE_URL }}\n            GIT_USERNAME: ${{ secrets.GIT_USERNAME }}\n            GIT_TOKEN: ${{ secrets.GIT_TOKEN }}\n            FASTLANE_APPLE_ID: ${{ secrets.FASTLANE_APPLE_ID }}\n            MATCH_USERNAME: ${{ secrets.FASTLANE_APPLE_ID }}\n            MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}\n            APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}\n            DEVELOPER_PORTAL_TEAM_ID: ${{ secrets.DEVELOPER_PORTAL_TEAM_ID }}\n          with:\n            lane: 'build'\n            subdirectory: 'ios/App'\n\n\n"
  },
  {
    "path": ".github/workflows/playwright.yml",
    "content": "name: Playwright Tests\n\n# Only run one at a time per branch\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  pull_request:\n    branches:\n      - '*'\n      \njobs:\n  test:\n    timeout-minutes: 60\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout source\n        uses: actions/checkout@v3\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18.x\n\n      - uses: pnpm/action-setup@v4\n        name: Install pnpm\n        id: pnpm-install\n        with:\n          run_install: false\n\n      - name: Get pnpm store directory\n        id: pnpm-cache\n        shell: bash\n        run: |\n          echo \"STORE_PATH=$(pnpm store path)\" >> $GITHUB_OUTPUT\n\n      - uses: actions/cache@v3\n        name: Setup pnpm cache\n        with:\n          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}\n          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n          restore-keys: |\n            ${{ runner.os }}-pnpm-store-\n\n      - name: Install app dependencies\n        run: pnpm install\n\n      - run: npx playwright install --with-deps\n        if: steps.playwright-cache.outputs.cache-hit != 'true'\n      - run: npx playwright install-deps\n        if: steps.playwright-cache.outputs.cache-hit != 'true'\n\n      - name: Run Playwright tests\n        env:\n          VITE_NETWORK: signet\n          VITE_PROXY: wss://p.mutinywallet.com\n          VITE_ESPLORA: https://mutinynet.com/api\n          VITE_LSP: https://mutinynet-flow.lnolymp.us\n          VITE_RGS: https://rgs.mutinynet.com/snapshot/\n          VITE_AUTH: https://auth-staging.mutinywallet.com\n          VITE_SUBSCRIPTIONS: https://subscriptions-staging.mutinywallet.com\n          VITE_STORAGE: https://storage-staging.mutinywallet.com/v2\n          VITE_FEEDBACK: https://feedback-staging.mutinywallet.com\n          VITE_SCORER: https://scorer-staging.mutinywallet.com\n          VITE_PRIMAL: https://primal-cache.mutinywallet.com/api\n        run: pnpm exec playwright test --grep-invert @slow\n      - uses: actions/upload-artifact@v3\n        if: always()\n        with:\n          name: playwright-report\n          path: playwright-report/\n          retention-days: 30\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\nandroid/app/play/*\nandroid/app/fdroid/*\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n.turbo\n\n# PWA dev stuff\ndev-dist\n.solid\n/test-results/\n/tests-examples/\n/playwright-report/\n/playwright/.cache/\n\n.env\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Use Node.js as a base image for building the site\nFROM node:20-slim AS builder\nENV PNPM_HOME=\"/pnpm\"\nENV PATH=\"$PNPM_HOME:$PATH\"\n\n# This is the cooler way to run pnpm these days (no need to npm install it)\nRUN corepack enable\nCOPY . /app\nWORKDIR /app\n\n# I think we need git be here because the vite build wants to look up the commit hash\nRUN apt update && apt install -y git python3 make build-essential\n\n# Add the ARG directives for build-time environment variables\nARG VITE_NETWORK=\"bitcoin\"\nARG VITE_PROXY=\"/_services/proxy\"\nARG VITE_PRIMAL=\"https://primal-cache.mutinywallet.com/api\"\nARG VITE_ESPLORA\nARG VITE_SCORER=\"https://scorer.mutinywallet.com\"\nARG VITE_LSP=\"https://0conf.lnolymp.us\"\nARG VITE_RGS\nARG VITE_AUTH\nARG VITE_STORAGE=\"/_services/vss/v2\"\nARG VITE_SELFHOSTED=\"true\"\n\n# Install dependencies\nRUN pnpm install --frozen-lockfile\n\n# IDK why but it gets mad if you don't do this\nRUN git config --global --add safe.directory /app\n\n# Build the static site\nRUN pnpm run build\n\n# Now, use Nginx as a base image for serving the site\nFROM nginx:alpine\n\n# Copy the static assets from the builder stage to the Nginx default static serve directory\nCOPY --from=builder /app/dist/public /usr/share/nginx/html\n\n# Copy the custom Nginx configuration file into the container\nCOPY default.conf /etc/nginx/conf.d/default.conf\n\n# Expose the default Nginx port\nEXPOSE 80\n\n# Start Nginx when the container starts\nCMD [\"nginx\", \"-g\", \"daemon off;\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022-2023 Mutiny Wallet Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "### Running Mutiny Web\n\n### Dependencies\n\n-   pnpm > 8\n\n```\npnpm install\npnpm run dev\n```\n\n### Env\n\nThe easiest way to get start with development is to create a file called `.env.local` and copy the contents of `.env.example` into it. This is basically identical to the env that `signet-app.mutinywallet.com` uses.\n\n### Testing\n\nWe have a couple Playwright e2e tests in the e2e folder. You can run these with:\n\n```\njust test\n```\n\nOr get a visual look into what's happening:\n\n```\njust test-ui\n```\n\n### Formatting\n\nHopefully your editor picks up on the `prettier.config.mjs` file and auto formats accordingly. If you want to format everything in the project run `pnpm run format`.\n\n### Deploying Web\n\nCreate a PR from `master` to `prod`, and once it does CI and gets approvals, do this from the command line:\n\n```\ngit checkout master && git pull && git checkout prod && git pull && git merge --ff-only origin/master && git push\n```\n\n## Contributing\n\nBefore committing make sure to run `pnpm run pre-commit`. This will typecheck, lint, and format everything so CI won't hassle you. (Shortcut: `just pre`).\n\n### Local\n\nIf you want to develop against a local version of [the node manager](https://github.com/MutinyWallet/mutiny-node), you may want to `pnpm link` it.\n\nDue to how [Vite's dev server works](https://vitejs.dev/config/server-options.html#server-fs-allow), the linked `mutiny-node` project folder should be a sibling of this `mutiny-web` folder. Alternatively you can change the allow path in `vite.config.ts`.\n\nIn your `mutiny-node` local repo:\n\n```\njust link\n```\n\n(on a Mac you might need to prefix `just link` with these flags: `AR=/opt/homebrew/opt/llvm/bin/llvm-ar CC=/opt/homebrew/opt/llvm/bin/clang`)\n\nNow in this repo, link them.\n\n```\njust local\n```\n\nTo revert back and use the remote version of mutiny-wasm:\n\n```\njust remote\n```\n\n## Android\n\n### How to test locally\n\n```\njust native\n```\n\nNow open up the `android` directory in android studio and run the build\n\n### Deploying\n\n#### Pull Requests\n\nEach pull request will build the debug signet app and upload it internally to the github actions run.\n\n#### Master\n\nEach push to master will build a signed release version running in signet mode. The build process is almost identical to the release version.\n\nPrereleased tags will be created for master.\n\n#### Release\n\n##### Android\n\nFirst bump up the `versionCode` and `versionName` in `./andriod/app/build.gradle`. The `versionCode` must always go up by one when making a release. The `versionName` can mimic `package.json` with an extra build number like `0.4.3-1` to make it easier to keep things looking like they are in sync when android only releases go out.\n\nPublish a new tag like `0.4.3-1` in order to trigger a signed release version running in mainnet mode.\n\n##### iOS\n\nIn `ios/App/App.xcodeproj/project.pbxproj` bump `MARKETING_VERSION` and then do whatever needs to be done in testflight to get it released.\n\n### Creating keys for the first time\n\n1. Generate a new signing key\n\n```\nkeytool -genkey -v -keystore <my-release-key.keystore> -alias <alias_name> -keyalg RSA -keysize 2048 -validity 10000\nopenssl base64 < <my-release-key.keystore> | tr -d '\\n' | tee some_signing_key.jks.base64.txt\n```\n\n2. Create 3 Secret Key variables on your GitHub repository and fill in with the signing key information\n    - `KEY_ALIAS` <- `<alias_name>`\n    - `KEY_STORE_PASSWORD` <- `<your key store password>`\n    - `SIGNING_KEY` <- the data from `<my-release-key.keystore>`\n3. Change the `versionCode` and `versionName` on `app/build.gradle`\n4. Commit and push.\n\n## Translating\n\n### Testing language keys\n\nTo check what keys are missing from your desired language:\n\n```\njust i18n $lang\n```\n\n### Adding new languages or keys\n\n1. In `public/i18n/` locate your desired language .json file or create one if one does not exist\n\n    - When creating a new language file ensure it follows the ISO 639 2-letter standard\n\n2. Populate your translation file with a translation object where all of the keys will be located\n\nIf you want to add Japanese you will create a file `/public/i18n/jp.json` and populate it with keys like so:\n\n```\n{\n  \"common\": {\n        \"continue\": \"続ける\",\n        ...\n    }\n}\n```\n\n(You should compare your translations against the English language as all other languages are not the master and are likely deprecated)\n\nIf you're using VS Code there are some nice extensions that can make this easier like i18n-ally and i18n-json-editor\n\n3. Add your language to the `Language` object in `/src/utils/languages.ts`. This will allow you to select the language via the language selector in the UI. If your desired language is set as your primary language in your browser it will be selected automatically\n\n```\nexport const LANGUAGE_OPTIONS: Language[] = [\n    {\n        value: \"日本語\",\n        shortName: \"jp\"\n    },\n```\n\n4. That's it! You should now be able to see your translation keys populating the app in your desired language. When youre ready go ahead and open a PR to have you language merged for others!\n"
  },
  {
    "path": "android/.gitignore",
    "content": "# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore\n\n# Built application files\n*.apk\n*.aar\n*.ap_\n*.aab\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n#  Uncomment the following line in case you need and you don't have the release build type files in your app\n# release/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# IntelliJ\n*.iml\n.idea/workspace.xml\n.idea/tasks.xml\n.idea/gradle.xml\n.idea/assetWizardSettings.xml\n.idea/dictionaries\n.idea/libraries\n# Android Studio 3 in .gitignore file.\n.idea/caches\n.idea/modules.xml\n# Comment next line if keeping position of elements in Navigation Editor is relevant for you\n.idea/navEditor.xml\n\n# Keystore files\n# Uncomment the following lines if you do not want to check your keystore files in.\n#*.jks\n#*.keystore\n\n# External native build folder generated in Android Studio 2.2 and later\n.externalNativeBuild\n.cxx/\n\n# Google Services (e.g. APIs or Firebase)\n# google-services.json\n\n# Freeline\nfreeline.py\nfreeline/\nfreeline_project_description.json\n\n# fastlane\nfastlane/report.xml\nfastlane/Preview.html\nfastlane/screenshots\nfastlane/test_output\nfastlane/readme.md\n\n# Version control\nvcs.xml\n\n# lint\nlint/intermediates/\nlint/generated/\nlint/outputs/\nlint/tmp/\n# lint/reports/\n\n# Android Profiling\n*.hprof\n\n# Cordova plugins for Capacitor\ncapacitor-cordova-android-plugins\n\n# Copied web assets\napp/src/main/assets/public\n\n# Generated Config files\napp/src/main/assets/capacitor.config.json\napp/src/main/assets/capacitor.plugins.json\napp/src/main/res/xml/config.xml\n"
  },
  {
    "path": "android/app/.gitignore",
    "content": "/build/*\n!/build/.npmkeep\n"
  },
  {
    "path": "android/app/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n    namespace \"com.mutinywallet.mutinywallet\"\n    compileSdk rootProject.ext.compileSdkVersion\n    defaultConfig {\n        applicationId \"com.mutinywallet.mutinywallet\"\n        minSdkVersion rootProject.ext.minSdkVersion\n        targetSdkVersion rootProject.ext.targetSdkVersion\n        versionCode 76\n        versionName \"1.8.0\"\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        aaptOptions {\n             // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.\n             // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61\n            ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'\n        }\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n    flavorDimensions \"channel\"\n    productFlavors {\n        play {\n            dimension \"channel\"\n        }\n\n        fdroid {\n            getIsDefault().set(true)\n            dimension \"channel\"\n        }\n    }\n    splits {\n        abi {\n            enable true\n            reset()\n            include \"x86\", \"x86_64\"\n            universalApk true\n        }\n    }\n}\n\nrepositories {\n    flatDir{\n        dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'\n    }\n}\n\ndependencies {\n    implementation fileTree(include: ['*.jar'], dir: 'libs')\n    implementation \"androidx.appcompat:appcompat:$androidxAppCompatVersion\"\n    implementation \"androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion\"\n    implementation \"androidx.core:core-splashscreen:$coreSplashScreenVersion\"\n    implementation project(':capacitor-android')\n    testImplementation \"junit:junit:$junitVersion\"\n    androidTestImplementation \"androidx.test.ext:junit:$androidxJunitVersion\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion\"\n    implementation project(':capacitor-cordova-android-plugins')\n}\n\napply from: 'capacitor.build.gradle'\n\ntry {\n    def servicesJSON = file('google-services.json')\n    if (servicesJSON.text) {\n        apply plugin: 'com.google.gms.google-services'\n    }\n} catch(Exception e) {\n    logger.info(\"google-services.json not found, google-services plugin not applied. Push Notifications won't work\")\n}\n"
  },
  {
    "path": "android/app/capacitor.build.gradle",
    "content": "// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME \"capacitor update\" IS RUN\n\nandroid {\n  compileOptions {\n      sourceCompatibility JavaVersion.VERSION_17\n      targetCompatibility JavaVersion.VERSION_17\n  }\n}\n\napply from: \"../capacitor-cordova-android-plugins/cordova.variables.gradle\"\ndependencies {\n    implementation project(':capacitor-mlkit-barcode-scanning')\n    implementation project(':capacitor-app')\n    implementation project(':capacitor-app-launcher')\n    implementation project(':capacitor-clipboard')\n    implementation project(':capacitor-filesystem')\n    implementation project(':capacitor-haptics')\n    implementation project(':capacitor-network')\n    implementation project(':capacitor-share')\n    implementation project(':capacitor-status-bar')\n    implementation project(':capacitor-toast')\n    implementation project(':capacitor-secure-storage-plugin')\n\n}\n\n\nif (hasProperty('postBuildExtras')) {\n  postBuildExtras()\n}\n"
  },
  {
    "path": "android/app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java",
    "content": "package com.getcapacitor.myapp;\n\nimport static org.junit.Assert.*;\n\nimport android.content.Context;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport androidx.test.platform.app.InstrumentationRegistry;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\n@RunWith(AndroidJUnit4.class)\npublic class ExampleInstrumentedTest {\n\n    @Test\n    public void useAppContext() throws Exception {\n        // Context of the app under test.\n        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();\n\n        assertEquals(\"com.getcapacitor.app\", appContext.getPackageName());\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:hardwareAccelerated=\"true\"\n        android:enableOnBackInvokedCallback=\"true\"\n        android:theme=\"@style/AppTheme\">\n\n        <activity\n            android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode\"\n            android:name=\".MainActivity\"\n            android:label=\"@string/title_activity_main\"\n            android:theme=\"@style/AppTheme.NoActionBarLaunch\"\n            android:launchMode=\"singleTask\"\n            android:exported=\"true\">\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n            <intent-filter android:autoVerify=\"true\">\n                <action android:name=\"android.intent.action.VIEW\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n                <data android:scheme=\"http\" />\n                <data android:scheme=\"https\" />\n                <data android:host=\"app.mutinywallet.com\" />\n            </intent-filter>\n\n        </activity>\n\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.fileprovider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/file_paths\"></meta-data>\n        </provider>\n    </application>\n\n    <!-- Permissions -->\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.CAMERA\" />\n    <uses-permission android:name=\"android.permission.VIBRATE\" />\n\n    <uses-sdk tools:overrideLibrary=\"com.google.zxing.client.android\" />\n    <uses-feature android:name=\"android.hardware.camera\" />\n\n    <uses-permission android:name=\"android.permission.READ_CLIPBOARD\" />\n    <uses-permission android:name=\"android.permission.WRITE_CLIPBOARD\" />\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/java/com/mutinywallet/mutinywallet/MainActivity.java",
    "content": "package com.mutinywallet.mutinywallet;\n\nimport android.os.Bundle;\nimport android.webkit.WebView;\n\nimport com.getcapacitor.BridgeActivity;\n\npublic class MainActivity extends BridgeActivity {\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n    }\n\n    @Override\n    public void onStart() {\n        super.onStart();\n        WebView webview = getBridge().getWebView();\n        webview.setOverScrollMode(WebView.OVER_SCROLL_NEVER);\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector\n    android:height=\"108dp\"\n    android:width=\"108dp\"\n    android:viewportHeight=\"108\"\n    android:viewportWidth=\"108\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#3DDC84\"\n          android:pathData=\"M0,0h108v108h-108z\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M9,0L9,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,0L19,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M29,0L29,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M39,0L39,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M49,0L49,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M59,0L59,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M69,0L69,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M79,0L79,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M89,0L89,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M99,0L99,108\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,9L108,9\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,19L108,19\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,29L108,29\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,39L108,39\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,49L108,49\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,59L108,59\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,69L108,69\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,79L108,79\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,89L108,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M0,99L108,99\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,29L89,29\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,39L89,39\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,49L89,49\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,59L89,59\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,69L89,69\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M19,79L89,79\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M29,19L29,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M39,19L39,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M49,19L49,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M59,19L59,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M69,19L69,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n    <path android:fillColor=\"#00000000\" android:pathData=\"M79,19L79,89\"\n          android:strokeColor=\"#33FFFFFF\" android:strokeWidth=\"0.8\"/>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportHeight=\"108\"\n    android:viewportWidth=\"108\">\n    <path\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z\"\n        android:strokeColor=\"#00000000\"\n        android:strokeWidth=\"1\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"78.5885\"\n                android:endY=\"90.9159\"\n                android:startX=\"48.7653\"\n                android:startY=\"61.0927\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z\"\n        android:strokeColor=\"#00000000\"\n        android:strokeWidth=\"1\" />\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\".MainActivity\">\n\n    <WebView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/app/src/main/res/values/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#FFFFFF</color>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values/strings.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<resources>\n    <string name=\"app_name\">Mutiny Wallet</string>\n    <string name=\"title_activity_main\">Mutiny Wallet</string>\n    <string name=\"package_name\">com.mutinywallet.mutinywallet</string>\n    <string name=\"custom_url_scheme\">mutiny</string>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n    <style name=\"AppTheme.NoActionBar\" parent=\"Theme.AppCompat.DayNight.NoActionBar\">\n        <item name=\"windowActionBar\">false</item>\n        <item name=\"windowNoTitle\">true</item>\n        <item name=\"android:background\">@null</item>\n    </style>\n\n\n    <style name=\"AppTheme.NoActionBarLaunch\" parent=\"Theme.SplashScreen\">\n        <item name=\"android:background\">@drawable/splash</item>\n    </style>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/xml/file_paths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <external-path name=\"my_images\" path=\".\" />\n    <cache-path name=\"my_cache_images\" path=\".\" />\n</paths>"
  },
  {
    "path": "android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java",
    "content": "package com.getcapacitor.myapp;\n\nimport static org.junit.Assert.*;\n\nimport org.junit.Test;\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\npublic class ExampleUnitTest {\n\n    @Test\n    public void addition_isCorrect() throws Exception {\n        assertEquals(4, 2 + 2);\n    }\n}\n"
  },
  {
    "path": "android/build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    \n    repositories {\n        google()\n        mavenCentral()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:8.2.2'\n        classpath 'com.google.gms:google-services:4.4.0'\n\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\napply from: \"variables.gradle\"\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "android/capacitor.settings.gradle",
    "content": "// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME \"capacitor update\" IS RUN\ninclude ':capacitor-android'\nproject(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/android/capacitor')\n\ninclude ':capacitor-mlkit-barcode-scanning'\nproject(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor-mlkit/barcode-scanning/android')\n\ninclude ':capacitor-app'\nproject(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/app/android')\n\ninclude ':capacitor-app-launcher'\nproject(':capacitor-app-launcher').projectDir = new File('../node_modules/.pnpm/@capacitor+app-launcher@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/app-launcher/android')\n\ninclude ':capacitor-clipboard'\nproject(':capacitor-clipboard').projectDir = new File('../node_modules/.pnpm/@capacitor+clipboard@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/clipboard/android')\n\ninclude ':capacitor-filesystem'\nproject(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/filesystem/android')\n\ninclude ':capacitor-haptics'\nproject(':capacitor-haptics').projectDir = new File('../node_modules/.pnpm/@capacitor+haptics@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/haptics/android')\n\ninclude ':capacitor-network'\nproject(':capacitor-network').projectDir = new File('../node_modules/.pnpm/@capacitor+network@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/network/android')\n\ninclude ':capacitor-share'\nproject(':capacitor-share').projectDir = new File('../node_modules/.pnpm/@capacitor+share@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/share/android')\n\ninclude ':capacitor-status-bar'\nproject(':capacitor-status-bar').projectDir = new File('../node_modules/.pnpm/@capacitor+status-bar@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/status-bar/android')\n\ninclude ':capacitor-toast'\nproject(':capacitor-toast').projectDir = new File('../node_modules/.pnpm/@capacitor+toast@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/toast/android')\n\ninclude ':capacitor-secure-storage-plugin'\nproject(':capacitor-secure-storage-plugin').projectDir = new File('../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@6.0.0/node_modules/capacitor-secure-storage-plugin/android')\n"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.2.1-all.zip\nnetworkTimeout=10000\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx1536m\n\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app's APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n"
  },
  {
    "path": "android/gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\nAPP_HOME=$( cd \"${APP_HOME:-./}\" && pwd -P ) || exit\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n# Collect all arguments for the java command;\n#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of\n#     shell script including quotes and variable substitutions, so put them in\n#     double quotes to make sure that they get re-expanded; and\n#   * put everything else in single quotes, so that it's not re-expanded.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "android/gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "android/settings.gradle",
    "content": "include ':app'\ninclude ':capacitor-cordova-android-plugins'\nproject(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')\n\napply from: 'capacitor.settings.gradle'"
  },
  {
    "path": "android/variables.gradle",
    "content": "ext {\n    minSdkVersion = 22\n    compileSdkVersion = 34\n    targetSdkVersion = 34\n    androidxActivityVersion = '1.8.0'\n    androidxAppCompatVersion = '1.6.1'\n    androidxCoordinatorLayoutVersion = '1.2.0'\n    androidxCoreVersion = '1.12.0'\n    androidxFragmentVersion = '1.6.2'\n    coreSplashScreenVersion = '1.0.1'\n    androidxWebkitVersion = '1.9.0'\n    junitVersion = '4.13.2'\n    androidxJunitVersion = '1.1.5'\n    androidxEspressoCoreVersion = '3.5.1'\n    cordovaAndroidVersion = '10.1.1'\n}"
  },
  {
    "path": "capacitor.config.ts",
    "content": "import { CapacitorConfig } from \"@capacitor/cli\";\n\nconst config: CapacitorConfig = {\n    appId: \"com.mutinywallet.mutinywallet\",\n    backgroundColor: \"171717\",\n    appName: \"Mutiny Wallet\",\n    webDir: \"dist/public\",\n    server: {\n        androidScheme: \"https\"\n    }\n};\n\nexport default config;\n"
  },
  {
    "path": "default.conf",
    "content": "server {\n    listen 80;\n\n    location / {\n        root   /usr/share/nginx/html;\n        index  index.html index.htm;\n        try_files $uri $uri/ /index.html;\n    }\n}\n"
  },
  {
    "path": "e2e/encrypt.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\n\nimport { loadHome, visitSettings } from \"./utils\";\n\ntest.beforeEach(async ({ page }) => {\n    await page.goto(\"http://localhost:3420/\");\n});\n\ntest(\"test local encrypt\", async ({ page }) => {\n    await loadHome(page);\n    await visitSettings(page);\n\n    // Click the \"Backup\" link\n    await page.click(\"text=Backup\");\n\n    // Click the \"Tap to reveal seed words\" button\n    await page.click(\"text=Tap to reveal seed words\");\n\n    // Click all three checkboxes\n    await page.getByLabel(\"I wrote down the words\").click({ force: true });\n    await page\n        .getByLabel(\"I understand that my funds are my responsibility\")\n        .click({ force: true });\n    await page\n        .getByLabel(\"I'm not lying just to get this over with\")\n        .click({ force: true });\n\n    // The \"I wrote down the words\" button should not be disabled\n    const wroteDownButton = await page.locator(\"button\", {\n        hasText: \"I wrote down the words\"\n    });\n    await expect(wroteDownButton).not.toBeDisabled();\n\n    // Click the \"I wrote down the words\" button\n    await wroteDownButton.click();\n\n    // Make sure the balance box ready light is on\n    await page.locator(\"title=READY\");\n\n    // Go back to settings / change password\n    await visitSettings(page);\n    await page.click(\"text=Security\");\n\n    // The header should now say \"Encrypt your seed words\"\n    await expect(page.locator(\"h1\")).toContainText([\"Encrypt your seed words\"]);\n\n    // Set a password\n    // 1. Find the input field with the name \"password\"\n    const passwordInput = await page.locator(`input[name='password']`);\n\n    // 2. Type the password into the input field\n    await passwordInput.fill(\"test\");\n\n    // 3. Find the input field with the name \"confirmPassword\"\n    const confirmPasswordInput = await page.locator(\n        `input[name='confirmPassword']`\n    );\n\n    // 4. Type the password into the input field\n    await confirmPasswordInput.fill(\"test\");\n\n    // The \"Encrypt\" button should not be disabled\n    const encryptButton = await page.locator(\"button\", { hasText: \"Encrypt\" });\n    await expect(encryptButton).toBeEnabled();\n\n    // wait 5 seconds for no reason (SADLY THIS IS IMPORTANT FOR THE TEST TO PASS)\n    await page.waitForTimeout(5000);\n\n    // Click the \"Encrypt\" button\n    await encryptButton.click();\n\n    // Wait for a modal with the text \"Enter your password\"\n    await page.getByText(\"Enter your password\").waitFor();\n\n    // Find the input field with the name \"password\"\n    const passwordInput2 = await page.locator(`input[name='password']`);\n\n    // Type the password into the input field\n    await passwordInput2.fill(\"test\");\n\n    // Click the \"Decrypt Wallet\" button\n    await page.click(\"text=Decrypt Wallet\");\n\n    // Should have a balance up top now\n    await page.locator(`text=0 sats`).first().waitFor();\n\n    const shutdownPopup = page.getByText(\"Mutiny Wallet is Shutting Down\");\n    if (await shutdownPopup.isVisible()) {\n        // Click the close button\n        await page.getByRole(\"button\").nth(1).click();\n    }\n});\n"
  },
  {
    "path": "e2e/fedimint.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\n\nimport { loadHome } from \"./utils\";\n\nconst SIGNET_INVITE_CODE =\n    \"fed11qgqzc2nhwden5te0vejkg6tdd9h8gepwvejkg6tdd9h8garhduhx6at5d9h8jmn9wshxxmmd9uqqzgxg6s3evnr6m9zdxr6hxkdkukexpcs3mn7mj3g5pc5dfh63l4tj6g9zk4er\";\n\ntest.beforeEach(async ({ page }) => {\n    await page.goto(\"http://localhost:3420/\");\n});\n\ntest(\"fedmint join, receive, send\", async ({ page }) => {\n    await loadHome(page);\n\n    // Click the top left button (it's the profile button), a child of header\n    // TODO: better ARIA stuff\n    await page.locator(`header button`).first().click();\n\n    // Click \"Join a federation\" cta\n    await page.click(\"text=Join a federation\");\n\n    // Fill the input with the federation code\n    await page.fill(\"input[name='federation_code']\", SIGNET_INVITE_CODE);\n\n    const addButton = await page.getByRole(\"button\", { name: \"Add\" }).first();\n\n    // Click the \"Add\" button\n    await addButton.click();\n\n    // Wait for a header to appear with the text \"MutinySignetFederation\"\n    await page.getByText(\"MutinySignetFederation\").waitFor();\n\n    // Navigate back to profile\n    await page.goBack();\n\n    // Navigate back home\n    await page.goBack();\n\n    // Click the top left button (it's the profile button), a child of header\n    // TODO: better ARIA stuff\n    await page.locator(`header button`).first().click();\n\n    // Make sure there's text that says \"fedimint\"\n    await page.locator(\"text=fedimint\").first();\n\n    // Navigate back home\n    await page.goBack();\n\n    // Click the fab button\n    await page.locator(\"#fab\").click();\n    // Click the receive button in the fab\n    await page.locator(\"text=Receive\").last().click();\n\n    // Expect the url to conain receive\n    await expect(page).toHaveURL(/.*receive/);\n\n    // At least one h1 should show \"0 sats\"\n    await expect(page.locator(\"h1\")).toContainText([\"0 SATS\"]);\n\n    // Type 100 into the input\n    await page.locator(\"#sats-input\").pressSequentially(\"100\");\n\n    // Now the h1 should show \"100,000 sats\"\n    await expect(page.locator(\"h1\")).toContainText([\"100 SATS\"]);\n\n    // There should be a button with the text \"Continue\" and it should not be disabled\n    const continueButton = await page.locator(\"button\", {\n        hasText: \"Continue\"\n    });\n    await expect(continueButton).not.toBeDisabled();\n\n    await continueButton.click();\n\n    await expect(\n        page.getByText(\"Keep Mutiny open to complete the payment.\")\n    ).toBeVisible();\n\n    // Locate an SVG inside a div with id \"qr\"\n    const qrCode = await page.locator(\"#qr > svg\");\n\n    await expect(qrCode).toBeVisible();\n\n    const value = await qrCode.getAttribute(\"value\");\n\n    // The SVG's value property includes \"bitcoin:l\"\n    expect(value).toContain(\"lightning:l\");\n\n    const lightningInvoice = value?.split(\"lightning:\")[1];\n\n    // Post the lightning invoice to the server\n    const _response = await fetch(\n        \"https://faucet.mutinynet.com/api/lightning\",\n        {\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\"\n            },\n            body: JSON.stringify({\n                bolt11: lightningInvoice\n            })\n        }\n    );\n\n    // Wait for an h1 to appear in the dom that says \"Payment Received\"\n    await page.waitForSelector(\"text=Payment Received\");\n\n    // Click the \"Nice\" button\n    await page.click(\"text=Nice\");\n\n    // Make sure we have 100 sats in the top balance\n    await page.waitForSelector(\"text=100 SATS\");\n\n    // Now we send\n    await page.locator(\"#fab\").click();\n    await page.locator(\"text=Send\").last().click();\n\n    // type refund@lnurl-staging.mutinywallet.com\n    const sendInput = await page.locator(\"input\");\n    await sendInput.fill(\"refund@lnurl-staging.mutinywallet.com\");\n\n    await page.click(\"text=Continue\");\n\n    // Wait for the destination to show up\n    await page.waitForSelector(\"text=LIGHTNING\");\n\n    // Type 90 into the input\n    await page.locator(\"#sats-input\").fill(\"90\");\n\n    // Now the h1 should show \"90 sats\"\n    await expect(page.locator(\"h1\")).toContainText([\"90 SATS\"]);\n\n    // There should be a button with the text \"Confirm Send\" and it should not be disabled\n    const confirmButton = await page.locator(\"button\", {\n        hasText: \"Confirm Send\"\n    });\n    await expect(confirmButton).not.toBeDisabled();\n\n    await confirmButton.click();\n\n    // Wait for an h1 to appear in the dom that says \"Payment Sent\"\n    await page.waitForSelector(\"text=Payment Sent\");\n});\n"
  },
  {
    "path": "e2e/load.spec.ts",
    "content": "import { test } from \"@playwright/test\";\n\nimport { loadHome } from \"./utils\";\n\ntest.beforeEach(async ({ page }) => {\n    await page.goto(\"http://localhost:3420/\");\n});\n\ntest(\"initial load\", async ({ page }) => {\n    await loadHome(page);\n});\n"
  },
  {
    "path": "e2e/restore.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\n\nimport { visitSettings } from \"./utils\";\n\ntest.beforeEach(async ({ page }) => {\n    await page.goto(\"http://localhost:3420/\");\n});\n\ntest(\"restore from seed @slow\", async ({ page }) => {\n    // Start on the home page\n    await expect(page).toHaveTitle(/Mutiny Wallet/);\n    await page.waitForSelector(\"text=Welcome to the Mutiny!\");\n\n    console.log(\"Waiting for new wallet to be created...\");\n\n    await page.locator(`button:has-text('Import Existing')`).click();\n\n    // should have 100k sats on-chain\n    const TEST_SEED_WORDS =\n        \"rival hood review write spoon tide orange ill opera enrich clip acoustic\";\n\n    // There should be some warning text: \"This will replace your existing wallet\"\n    await expect(page.locator(\"p\")).toContainText([\n        \"This will replace your existing wallet\"\n    ]);\n\n    const seedWords = TEST_SEED_WORDS.split(\" \");\n\n    // Find the input field with the name \"words.0\"\n    for (let i = 0; i < 12; i++) {\n        const wordInput = await page.locator(`input[name='words.${i}']`);\n\n        // Type the seed words into the input field\n        await wordInput.fill(seedWords[i]);\n    }\n\n    // There should be a button with the text \"Restore\" and it should not be disabled\n    const restoreButton = await page.locator(\"button\", { hasText: \"Restore\" });\n    await expect(restoreButton).not.toBeDisabled();\n\n    restoreButton.click();\n\n    // A modal should pop up, click the \"Confirm\" button\n    const confirmButton = await page.locator(\"button\", { hasText: \"Confirm\" });\n    await confirmButton.click();\n\n    // Eventually we should have a balance of 100k sats\n    await page.locator(\"text=100,000 SATS\");\n\n    // Now we should clean up after ourselves and delete the wallet\n    await visitSettings(page);\n\n    // Click the \"Restore\" link\n    await page.click(\"text=Admin Page\");\n\n    // Clicke the Delete Everything button\n    await page.click(\"text=Delete Everything\");\n\n    // A modal should pop up, click the \"Confirm\" button\n    const confirmDeleteButton = await page.locator(\"button\", {\n        hasText: \"Confirm\"\n    });\n\n    // wait 5 seconds for no reason\n    await page.waitForTimeout(5000);\n\n    await confirmDeleteButton.click();\n\n    await page.locator(\"text=Welcome to the Mutiny!\");\n});\n"
  },
  {
    "path": "e2e/roundtrip.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\n\nimport { loadHome, visitSettings } from \"./utils\";\n\ntest.beforeEach(async ({ page }) => {\n    await page.goto(\"http://localhost:3420/\");\n});\n\ntest(\"rountrip receive and send\", async ({ page }) => {\n    test.slow(); // tell playwright that this test is slow\n\n    await loadHome(page);\n\n    await page.locator(\"#fab\").click();\n    await page.locator(\"text=Receive\").last().click();\n\n    // Expect the url to contain receive\n    await expect(page).toHaveURL(/.*receive/);\n\n    // At least one h1 should show \"0 sats\"\n    await expect(page.locator(\"h1\")).toContainText([\"0 SATS\"]);\n\n    // At least one h2 should show \"0 USD\"\n    // await expect(page.locator(\"h2\")).toContainText([\"$0 USD\"]);\n    await page.waitForSelector(\"text=$0 USD\");\n\n    // Type 100000 into the input\n    await page.locator(\"#sats-input\").pressSequentially(\"100000\");\n\n    // Now the h1 should show \"100,000 sats\"\n    await expect(page.locator(\"h1\")).toContainText([\"100,000 SATS\"]);\n\n    // There should be a button with the text \"Continue\" and it should not be disabled\n    const continueButton = await page.locator(\"button\", {\n        hasText: \"Continue\"\n    });\n    await expect(continueButton).not.toBeDisabled();\n\n    // Wait one second\n    // TODO: figure out how to not get an error without waiting\n    await page.waitForTimeout(1000);\n\n    continueButton.click();\n\n    await expect(\n        page.getByText(\"Keep Mutiny open to complete the payment.\")\n    ).toBeVisible();\n\n    // Locate an SVG inside a div with id \"qr\"\n    const qrCode = await page.locator(\"#qr > svg\");\n\n    await expect(qrCode).toBeVisible();\n\n    const value = await qrCode.getAttribute(\"value\");\n\n    // The SVG's value property includes \"lightning:l\"\n    expect(value).toContain(\"lightning:l\");\n\n    // Post the lightning invoice to the server\n    const response = await fetch(\"https://faucet.mutinynet.com/api/lightning\", {\n        method: \"POST\",\n        mode: \"cors\",\n        headers: {\n            \"Content-Type\": \"application/json\"\n        },\n        body: JSON.stringify({\n            bolt11: value\n        })\n    });\n\n    if (!response.ok) {\n        response.text().then((text) => {\n            throw new Error(\"failed to post invoice to faucet: \" + text);\n        });\n    }\n\n    // Wait for an h1 to appear in the dom that says \"Payment Received\"\n    await page.waitForSelector(\"text=Payment Received\", { timeout: 30000 });\n\n    // Click the \"Nice\" button\n    await page.click(\"text=Nice\");\n\n    // Now we send\n    await page.locator(\"#fab\").click();\n    await page.locator(\"text=Send\").click();\n\n    // In the textarea with the placeholder \"bitcoin:...\" type refund@lnurl-staging.mutinywallet.com\n    const sendInput = await page.locator(\"input\");\n    await sendInput.fill(\"refund@lnurl-staging.mutinywallet.com\");\n\n    await page.click(\"text=Continue\");\n\n    // Wait two seconds (the destination doesn't show up immediately)\n    // TODO: figure out how to not get an error without waiting\n    await page.waitForTimeout(2000);\n\n    // Type 10000 into the input\n    await page.locator(\"#sats-input\").fill(\"10000\");\n\n    // Now the h1 should show \"100,000 sats\"\n    await expect(page.locator(\"h1\")).toContainText([\"10,000 SATS\"]);\n\n    // There should be a button with the text \"Confirm Send\" and it should not be disabled\n    const confirmButton = await page.locator(\"button\", {\n        hasText: \"Confirm Send\"\n    });\n    await expect(confirmButton).not.toBeDisabled();\n\n    confirmButton.click();\n\n    // Wait for an h1 to appear in the dom that says \"Payment Sent\"\n    await page.waitForSelector(\"text=Payment Sent\", { timeout: 30000 });\n\n    // Click the \"Nice\" button to go home\n    await page.click(\"text=Nice\");\n\n    // Click settings\n    await visitSettings(page);\n\n    // Click \"lightning channels\"\n    await page.click(\"text=Lightning Channels\");\n\n    // Close the channel\n    await page.getByText(\"You have 1 lightning channel.\").waitFor();\n\n    await page.click(\"text=Online Channels\");\n\n    // Idk why the node isn't ready to close channels right away\n    await page.waitForTimeout(5000);\n\n    await page.click(\"text=Close\");\n\n    await page.click(\"text=Confirm\");\n\n    // wait for the channel to close\n    await page.waitForTimeout(5000);\n\n    await page\n        .getByText(\n            \"It looks like you don't have any channels yet. To get started, receive some sats over lightning, or swap some on-chain funds into a channel. Get your hands dirty!\"\n        )\n        .waitFor();\n});\n"
  },
  {
    "path": "e2e/routes.spec.ts",
    "content": "import { expect, Page, test } from \"@playwright/test\";\n\nimport { loadHome, visitSettings } from \"./utils\";\n\nconst routes = [\n    \"/\",\n    \"/feedback\",\n    \"/receive\",\n    \"/scanner\",\n    \"/search\",\n    \"/send\",\n    \"/settings\"\n];\n\nconst settingsRoutes = [\n    \"/admin\",\n    \"/backup\",\n    \"/channels\",\n    \"/connections\",\n    \"/currency\",\n    \"/emergencykit\",\n    \"/restore\",\n    \"/servers\",\n    \"/nostrkeys\"\n];\n\nconst settingsRoutesPrefixed = settingsRoutes.map((route) => {\n    return \"/settings\" + route;\n});\n\nconst allRoutes = routes.concat(settingsRoutesPrefixed);\n\n// Create a JS Map of all routes so we can check them off one by one\nconst checklist = new Map();\nallRoutes.forEach((route) => {\n    checklist.set(route, false);\n});\n\n// Only works if there's a link to the route on the page\nasync function checkRoute(\n    page: Page,\n    route: string,\n    expectedHeader: string,\n    checklist: Map<string, boolean>\n) {\n    await page.locator(`a[href='${route}']`).first().click();\n    await expect(page.locator(\"h1\").first()).toHaveText(expectedHeader);\n    checklist.set(route, true);\n}\n\ntest.beforeEach(async ({ page }) => {\n    await page.goto(\"http://localhost:3420/\");\n});\n\ntest(\"visit each route\", async ({ page }) => {\n    await loadHome(page);\n\n    checklist.set(\"/\", true);\n\n    await visitSettings(page);\n\n    checklist.set(\"/settings\", true);\n\n    // Lightning Channels\n    await checkRoute(\n        page,\n        \"/settings/channels\",\n        \"Lightning Channels\",\n        checklist\n    );\n    await page.goBack();\n\n    // Backup\n    await checkRoute(page, \"/settings/backup\", \"Backup\", checklist);\n    await page.goBack();\n\n    // Restore\n    await checkRoute(page, \"/settings/restore\", \"Restore\", checklist);\n    await page.goBack();\n\n    // Currency\n    await checkRoute(page, \"/settings/currency\", \"Currency\", checklist);\n    await page.goBack();\n\n    // Servers\n    await checkRoute(page, \"/settings/servers\", \"Servers\", checklist);\n    await page.goBack();\n\n    // Nostr Keys\n    await checkRoute(page, \"/settings/nostrkeys\", \"Nostr Keys\", checklist);\n    await page.goBack();\n\n    // Emergency Kit\n    await checkRoute(\n        page,\n        \"/settings/emergencykit\",\n        \"Emergency Kit\",\n        checklist\n    );\n    await page.goBack();\n\n    // Admin\n    await checkRoute(page, \"/settings/admin\", \"Secret Debug Tools\", checklist);\n    await page.goBack();\n\n    // Feedback\n    await checkRoute(page, \"/feedback\", \"Give us feedback!\", checklist);\n    await page.goBack();\n\n    // Go back home\n    await page.goBack();\n\n    // Try the fab button\n    await page.locator(\"#fab\").click();\n    await page.locator(\"text=Send\").click();\n    await expect(page.locator(\"input\").first()).toBeFocused();\n\n    // Send is covered in another test\n    checklist.set(\"/send\", true);\n\n    await page.goBack();\n\n    // Try the fab button again\n    await page.locator(\"#fab\").click();\n    // (There are actually two buttons with the \"Receive text on first run)\n    await page.locator(\"text=Receive\").last().click();\n\n    await expect(page.locator(\"h1\").first()).toHaveText(\"Receive Bitcoin\");\n\n    // Actual receive is covered in another test\n    checklist.set(\"/receive\", true);\n\n    await page.goBack();\n\n    // Try the fab button again\n    await page.locator(\"#fab\").click();\n    await page.locator(\"text=Scan\").click();\n\n    // Scanner\n    await expect(\n        page.locator(\"button:has-text('Paste Something')\")\n    ).toBeVisible();\n    checklist.set(\"/scanner\", true);\n\n    // Visit connections nwa params\n    const nwaParams =\n        \"/settings/connections?nwa=nostr%2Bwalletauth%3A%2F%2Fe552dec5821ef94dc1b9138a347b4b1d8dcb595e31f5c89352e50dc11255e0f4%3Frelay%3Dwss%253A%252F%252Frelay.damus.io%252F%26secret%3D0bfe616c5e126a7c%26required_commands%3Dpay_invoice%26budget%3D21%252Fday%26identity%3D32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\";\n    await page.goto(\"http://localhost:3420\" + nwaParams);\n    await expect(page.locator('[role=\"dialog\"] h2 header').first()).toHaveText(\n        \"Add Connection\"\n    );\n\n    // print how many routes we've visited\n    checklist.forEach((value, key) => {\n        console.log(`${key}: ${value}`);\n    });\n});\n"
  },
  {
    "path": "e2e/utils.ts",
    "content": "import { expect, Page } from \"@playwright/test\";\n\nexport async function loadHome(page: Page) {\n    // Start on the home page\n    await expect(page).toHaveTitle(/Mutiny Wallet/);\n    await page.waitForSelector(\"text=Welcome to the Mutiny!\");\n\n    console.log(\"Waiting for new wallet to be created...\");\n\n    await page.locator(`button:has-text('New Wallet')`).click();\n\n    await page.locator(\"text=Create your profile\").first();\n\n    await page.locator(\"button:has-text('Skip for now')\").click();\n\n    // await page.getByText(\"Pick a Federation\").waitFor();\n\n    // await page.locator(\"button:has-text('Skip for now')\").click();\n\n    // await page.locator(`button:has-text('Confirm')`).click();\n\n    // Should have a balance up top now\n    await page.locator(`text=0 sats`).first().waitFor();\n\n    const shutdownPopup = page.getByText(\"Mutiny Wallet is Shutting Down\");\n    if (await shutdownPopup.isVisible()) {\n        // Click the close button\n        await page.getByRole(\"button\").nth(1).click();\n    }\n}\n\nexport async function visitSettings(page: Page) {\n    // Find an image with an alt text of \"mutiny\" and click it\n    // TODO: probably should have better ARIA stuff for this\n    await page.locator(\"img[alt='mutiny']\").first().click();\n    await expect(page.locator(\"h1\").first()).toHaveText(\"Settings\");\n}\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixpkgs-unstable\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs = { self, nixpkgs, flake-utils, ... }:\n    flake-utils.lib.eachDefaultSystem (system:\n      let\n        pkgs = import nixpkgs {\n          inherit system;\n        };\n\n      in\n      {\n        devShells.default = pkgs.mkShell {\n          buildInputs = [\n            pkgs.corepack_20\n            pkgs.nodejs_20\n            pkgs.python3\n            pkgs.just\n          ];\n          shellHook = ''\n            corepack prepare pnpm@8.15.5 --activate\n          '';\n        };\n      });\n}\n"
  },
  {
    "path": "index.html",
    "content": "<!doctype html>\n<html>\n    <head>\n        <title data-sm=\"\">Mutiny Wallet</title>\n        <meta charset=\"utf-8\" />\n        <meta\n            name=\"viewport\"\n            content=\"width=device-width, initial-scale=1.0 height=device-height viewport-fit=cover user-scalable=no\"\n        />\n        <link rel=\"manifest\" href=\"/manifest.webmanifest\" />\n        <meta name=\"theme-color\" content=\"#171717\" />\n        <meta\n            name=\"description\"\n            content=\"Mutiny is a self-custodial lightning wallet that runs in the browser.\"\n        />\n        <link rel=\"icon\" href=\"/favicon.ico\" />\n        <meta name=\"twitter:card\" content=\"summary_large_image\" />\n        <meta name=\"twitter:title\" content=\"Mutiny Wallet\" />\n        <meta\n            name=\"twitter:description\"\n            content=\"Mutiny is a self-custodial lightning wallet that runs in the browser.\"\n        />\n        <meta name=\"twitter:site\" content=\"https://app.mutinywallet.com/\" />\n        <meta\n            name=\"twitter:image\"\n            content=\"https://app.mutinywallet.com/images/twitter_card_image.png\"\n        />\n        <meta property=\"og:type\" content=\"website\" />\n        <meta property=\"og:title\" content=\"Mutiny Wallet\" />\n        <meta\n            property=\"og:description\"\n            content=\"Mutiny is a self-custodial lightning wallet that runs in the browser.\"\n        />\n        <meta property=\"og:url\" content=\"https://app.mutinywallet.com/\" />\n        <meta\n            property=\"og:image\"\n            content=\"https://app.mutinywallet.com/images/twitter_card_image.png\"\n        />\n        <link rel=\"apple-touch-icon\" href=\"/images/icon.png\" sizes=\"512x512\" />\n        <link rel=\"mask-icon\" href=\"/mutiny_logo_mask.svg\" color=\"#000\" />\n        <style>\n            #no-wasm {\n                display: none;\n            }\n\n            #no-wasm,\n            #no-script {\n                margin: 1rem;\n            }\n            body {\n                background-color: hsla(0, 0%, 5%, 1);\n            }\n        </style>\n    </head>\n    <body>\n        <div id=\"no-wasm\">\n            <p>\n                <img\n                    src=\"/mutiny-pixel-logo.png\"\n                    width=\"75px\"\n                    alt=\"Mutiny Wallet\"\n                />\n            </p>\n            <p>\n                Your browser does not support WebAssembly, or it's disabled.\n                Please update or enable WebAssembly to run this app.\n            </p>\n            <p>\n                If you're running iOS in lockdown mode you'll need to\n                <a href=\"https://support.apple.com/en-us/HT212650\"\n                    >add an exception for Mutiny Wallet.</a\n                >\n            </p>\n        </div>\n        <noscript>\n            <div id=\"no-script\">\n                <p>\n                    <img\n                        src=\"/mutiny-pixel-logo.png\"\n                        width=\"75px\"\n                        alt=\"Mutiny Wallet\"\n                    />\n                </p>\n                <p>\n                    Mutiny is a self-custodial wallet that runs in the browser.\n                    JavaScript must be enabled.\n                </p>\n            </div>\n        </noscript>\n        <div id=\"root\"></div>\n        <script type=\"module\" src=\"/src/index.tsx\"></script>\n        <script>\n            // Check for WebAssembly support\n            if (\n                typeof WebAssembly === \"object\" &&\n                typeof WebAssembly.instantiate === \"function\"\n            ) {\n                var module = new WebAssembly.Module(\n                    new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0])\n                ); // This is an empty wasm module\n                if (\n                    WebAssembly.Module instanceof Function &&\n                    module instanceof WebAssembly.Module\n                )\n                    console.log(\"WebAssembly is supported\");\n                else document.getElementById(\"no-wasm\").style.display = \"block\";\n            } else {\n                document.getElementById(\"no-wasm\").style.display = \"block\";\n            }\n        </script>\n    </body>\n</html>\n"
  },
  {
    "path": "ios/.gitignore",
    "content": "App/build\nApp/Pods\nApp/output\nApp/App/public\nDerivedData\nxcuserdata\n\n# Cordova plugins for Capacitor\ncapacitor-cordova-ios-plugins\n\n# Generated Config files\nApp/App/capacitor.config.json\nApp/App/config.xml\n\nApp/App.app.dSYM.zip\nApp/App.ipa"
  },
  {
    "path": "ios/App/App/App.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.developer.associated-domains</key>\n\t<array>\n\t\t<string>applinks:app.mutinywallet.com</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/App/App/AppDelegate.swift",
    "content": "import UIKit\nimport Capacitor\n\n@UIApplicationMain\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n\n    var window: UIWindow?\n\n    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {\n        // Override point for customization after application launch.\n        if #available(iOS 16.4, *) {\n            DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {\n                if let vc = self.window?.rootViewController as? CAPBridgeViewController {\n                    vc.bridgedWebView?.isInspectable = true;\n                }\n            }\n        }\n        \n        return true\n    }\n\n    func applicationWillResignActive(_ application: UIApplication) {\n        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.\n        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.\n    }\n\n    func applicationDidEnterBackground(_ application: UIApplication) {\n        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.\n        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.\n    }\n\n    func applicationWillEnterForeground(_ application: UIApplication) {\n        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.\n    }\n\n    func applicationDidBecomeActive(_ application: UIApplication) {\n        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.\n    }\n\n    func applicationWillTerminate(_ application: UIApplication) {\n        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.\n    }\n\n    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {\n        // Called when the app was launched with a url. Feel free to add additional processing here,\n        // but if you want the App API to support tracking app url opens, make sure to keep this call\n        return ApplicationDelegateProxy.shared.application(app, open: url, options: options)\n    }\n\n    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {\n        // Called when the app was launched with an activity, including Universal Links.\n        // Feel free to add additional processing here, but if you want the App API to support\n        // tracking app url opens, make sure to keep this call\n        return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)\n    }\n\n}\n"
  },
  {
    "path": "ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\": [\n    {\n      \"filename\": \"AppIcon-512@2x.png\",\n      \"idiom\": \"universal\",\n      \"platform\": \"ios\",\n      \"size\": \"1024x1024\"\n    },\n    {\n      \"idiom\": \"ipad\",\n      \"size\": \"20x20\",\n      \"scale\": \"1x\",\n      \"filename\": \"AppIcon-20x20@1x.png\"\n    },\n    {\n      \"idiom\": \"iphone\",\n      \"size\": \"20x20\",\n      \"scale\": \"2x\",\n      \"filename\": \"AppIcon-20x20@2x.png\"\n    },\n    {\n      \"idiom\": \"ipad\",\n      \"size\": \"20x20\",\n      \"scale\": \"2x\",\n      \"filename\": \"AppIcon-20x20@2x-1.png\"\n    },\n    {\n      \"idiom\": \"iphone\",\n      \"size\": \"20x20\",\n      \"scale\": \"3x\",\n      \"filename\": \"AppIcon-20x20@3x.png\"\n    },\n    {\n      \"idiom\": \"ipad\",\n      \"size\": \"29x29\",\n      \"scale\": \"1x\",\n      \"filename\": \"AppIcon-29x29@1x.png\"\n    },\n    {\n      \"idiom\": \"iphone\",\n      \"size\": \"29x29\",\n      \"scale\": \"2x\",\n      \"filename\": \"AppIcon-29x29@2x.png\"\n    },\n    {\n      \"idiom\": \"ipad\",\n      \"size\": \"29x29\",\n      \"scale\": \"2x\",\n      \"filename\": \"AppIcon-29x29@2x-1.png\"\n    },\n    {\n      \"idiom\": \"iphone\",\n      \"size\": \"29x29\",\n      \"scale\": \"3x\",\n      \"filename\": \"AppIcon-29x29@3x.png\"\n    },\n    {\n      \"idiom\": \"ipad\",\n      \"size\": \"40x40\",\n      \"scale\": \"1x\",\n      \"filename\": \"AppIcon-40x40@1x.png\"\n    },\n    {\n      \"idiom\": \"iphone\",\n      \"size\": \"40x40\",\n      \"scale\": \"2x\",\n      \"filename\": \"AppIcon-40x40@2x.png\"\n    },\n    {\n      \"idiom\": \"ipad\",\n      \"size\": \"40x40\",\n      \"scale\": \"2x\",\n      \"filename\": \"AppIcon-40x40@2x-1.png\"\n    },\n    {\n      \"idiom\": \"iphone\",\n      \"size\": \"40x40\",\n      \"scale\": \"3x\",\n      \"filename\": \"AppIcon-40x40@3x.png\"\n    },\n    {\n      \"idiom\": \"iphone\",\n      \"size\": \"60x60\",\n      \"scale\": \"2x\",\n      \"filename\": \"AppIcon-60x60@2x.png\"\n    },\n    {\n      \"idiom\": \"iphone\",\n      \"size\": \"60x60\",\n      \"scale\": \"3x\",\n      \"filename\": \"AppIcon-60x60@3x.png\"\n    },\n    {\n      \"idiom\": \"ipad\",\n      \"size\": \"76x76\",\n      \"scale\": \"1x\",\n      \"filename\": \"AppIcon-76x76@1x.png\"\n    },\n    {\n      \"idiom\": \"ipad\",\n      \"size\": \"76x76\",\n      \"scale\": \"2x\",\n      \"filename\": \"AppIcon-76x76@2x.png\"\n    },\n    {\n      \"idiom\": \"ipad\",\n      \"size\": \"83.5x83.5\",\n      \"scale\": \"2x\",\n      \"filename\": \"AppIcon-83.5x83.5@2x.png\"\n    },\n    {\n      \"idiom\": \"ios-marketing\",\n      \"size\": \"1024x1024\",\n      \"scale\": \"1x\",\n      \"filename\": \"AppIcon-512@2x.png\"\n    }\n  ],\n  \"info\": {\n    \"author\": \"xcode\",\n    \"version\": 1\n  }\n}"
  },
  {
    "path": "ios/App/App/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/App/App/Assets.xcassets/Splash.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"Default@1x~universal~anyany.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"Default@1x~universal~anyany-dark.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"filename\" : \"Default@2x~universal~anyany.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"Default@2x~universal~anyany-dark.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"filename\" : \"Default@3x~universal~anyany.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"appearances\" : [\n        {\n          \"appearance\" : \"luminosity\",\n          \"value\" : \"dark\"\n        }\n      ],\n      \"filename\" : \"Default@3x~universal~anyany-dark.png\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/App/App/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"17132\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" useTraitCollections=\"YES\" useSafeAreas=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <device id=\"retina4_7\" orientation=\"portrait\" appearance=\"light\"/>\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"17105\"/>\n        <capability name=\"System colors in document resources\" minToolsVersion=\"11.0\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <imageView key=\"view\" userInteractionEnabled=\"NO\" contentMode=\"scaleAspectFill\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" image=\"Splash\" id=\"snD-IY-ifK\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"375\" height=\"667\"/>\n                        <autoresizingMask key=\"autoresizingMask\"/>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\"/>\n                    </imageView>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"Splash\" width=\"1366\" height=\"1366\"/>\n        <systemColor name=\"systemBackgroundColor\">\n            <color white=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"genericGamma22GrayColorSpace\"/>\n        </systemColor>\n    </resources>\n</document>\n"
  },
  {
    "path": "ios/App/App/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"21701\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" colorMatched=\"YES\" initialViewController=\"BYZ-38-t0r\">\n    <device id=\"retina4_7\" orientation=\"portrait\" appearance=\"light\"/>\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"21679\"/>\n    </dependencies>\n    <scenes>\n        <!--My Custom View Controller-->\n        <scene sceneID=\"tne-QT-ifu\">\n            <objects>\n                <viewController id=\"BYZ-38-t0r\" customClass=\"MyCustomViewController\" customModule=\"App\" customModuleProvider=\"target\" sceneMemberID=\"viewController\"/>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"dkx-z0-nzr\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"-41\" y=\"-69\"/>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "ios/App/App/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>en</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>Mutiny Wallet</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(MARKETING_VERSION)</string>\n\t<key>CFBundleVersion</key>\n\t<string>13</string>\n\t<key>ITSAppUsesNonExemptEncryption</key>\n\t<false/>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>NSCameraUsageDescription</key>\n\t<string>Mutiny uses the camera to scan QR codes of bitcoin addresses and lightning invoices.</string>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>UIRequiredDeviceCapabilities</key>\n\t<array>\n\t\t<string>armv7</string>\n\t</array>\n\t<key>UIStatusBarStyle</key>\n\t<string>UIStatusBarStyleDarkContent</string>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t</array>\n\t<key>UIViewControllerBasedStatusBarAppearance</key>\n\t<true/>\n\t<key>LSApplicationQueriesSchemes</key>\n\t<array>\n\t\t<string>nostr+walletconnect</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/App/App.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 48;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };\n\t\t50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };\n\t\t504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };\n\t\t504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };\n\t\t504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };\n\t\t504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };\n\t\t50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };\n\t\t6E62BA8F2AA9491700710026 /* MyCustomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E62BA8E2AA9491700710026 /* MyCustomViewController.swift */; };\n\t\tA084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXFileReference section */\n\t\t2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = \"<group>\"; };\n\t\t50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = \"<group>\"; };\n\t\t504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = \"<group>\"; };\n\t\t6E62BA8E2AA9491700710026 /* MyCustomViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyCustomViewController.swift; sourceTree = \"<group>\"; };\n\t\t6EE2E6442B02B59700B1E443 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = \"<group>\"; };\n\t\tAF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tAF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-App.release.xcconfig\"; path = \"Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\tFC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-App.debug.xcconfig\"; path = \"Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig\"; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t504EC3011FED79650016851F /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tA084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tAF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t504EC2FB1FED79650016851F = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t6E62BA8E2AA9491700710026 /* MyCustomViewController.swift */,\n\t\t\t\t504EC3061FED79650016851F /* App */,\n\t\t\t\t504EC3051FED79650016851F /* Products */,\n\t\t\t\t7F8756D8B27F46E3366F6CEA /* Pods */,\n\t\t\t\t27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t504EC3051FED79650016851F /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t504EC3041FED79650016851F /* App.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t504EC3061FED79650016851F /* App */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t6EE2E6442B02B59700B1E443 /* App.entitlements */,\n\t\t\t\t50379B222058CBB4000EE86E /* capacitor.config.json */,\n\t\t\t\t504EC3071FED79650016851F /* AppDelegate.swift */,\n\t\t\t\t504EC30B1FED79650016851F /* Main.storyboard */,\n\t\t\t\t504EC30E1FED79650016851F /* Assets.xcassets */,\n\t\t\t\t504EC3101FED79650016851F /* LaunchScreen.storyboard */,\n\t\t\t\t504EC3131FED79650016851F /* Info.plist */,\n\t\t\t\t2FAD9762203C412B000D30F8 /* config.xml */,\n\t\t\t\t50B271D01FEDC1A000F3C39B /* public */,\n\t\t\t);\n\t\t\tpath = App;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t7F8756D8B27F46E3366F6CEA /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tFC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,\n\t\t\t\tAF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,\n\t\t\t);\n\t\t\tname = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t504EC3031FED79650016851F /* App */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget \"App\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t504EC3001FED79650016851F /* Sources */,\n\t\t\t\t504EC3011FED79650016851F /* Frameworks */,\n\t\t\t\t504EC3021FED79650016851F /* Resources */,\n\t\t\t\t9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = App;\n\t\t\tproductName = App;\n\t\t\tproductReference = 504EC3041FED79650016851F /* App.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t504EC2FC1FED79650016851F /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastSwiftUpdateCheck = 0920;\n\t\t\t\tLastUpgradeCheck = 0920;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t504EC3031FED79650016851F = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject \"App\" */;\n\t\t\tcompatibilityVersion = \"Xcode 8.0\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 504EC2FB1FED79650016851F;\n\t\t\tproductRefGroup = 504EC3051FED79650016851F /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t504EC3031FED79650016851F /* App */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t504EC3021FED79650016851F /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t50B271D11FEDC1A000F3C39B /* public in Resources */,\n\t\t\t\t504EC30F1FED79650016851F /* Assets.xcassets in Resources */,\n\t\t\t\t50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,\n\t\t\t\t504EC30D1FED79650016851F /* Main.storyboard in Resources */,\n\t\t\t\t2FAD9763203C412B000D30F8 /* config.xml in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t504EC3001FED79650016851F /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t504EC3081FED79650016851F /* AppDelegate.swift in Sources */,\n\t\t\t\t6E62BA8F2AA9491700710026 /* MyCustomViewController.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXVariantGroup section */\n\t\t504EC30B1FED79650016851F /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t504EC30C1FED79650016851F /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t504EC3111FED79650016851F /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t504EC3141FED79650016851F /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t504EC3151FED79650016851F /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Owholemodule\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t504EC3171FED79650016851F /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = App/App.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1434;\n\t\t\t\tDEVELOPMENT_TEAM = X773Y823TN;\n\t\t\t\tINFOPLIST_FILE = App/Info.plist;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.finance\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 14.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = \"$(inherited) @executable_path/Frameworks\";\n\t\t\t\tMARKETING_VERSION = 1.8.0;\n\t\t\t\tOTHER_SWIFT_FLAGS = \"$(inherited) \\\"-D\\\" \\\"COCOAPODS\\\" \\\"-DDEBUG\\\"\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"com.mutinywallet.mutiny-test\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t504EC3181FED79650016851F /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = App/App.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Distribution\";\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1434;\n\t\t\t\tDEVELOPMENT_TEAM = \"\";\n\t\t\t\t\"DEVELOPMENT_TEAM[sdk=iphoneos*]\" = X773Y823TN;\n\t\t\t\tINFOPLIST_FILE = App/Info.plist;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.finance\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 14.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = \"$(inherited) @executable_path/Frameworks\";\n\t\t\t\tMARKETING_VERSION = 1.8.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.mutinywallet.mutiny;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\t\"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]\" = \"match AppStore com.mutinywallet.mutiny\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t504EC2FF1FED79650016851F /* Build configuration list for PBXProject \"App\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t504EC3141FED79650016851F /* Debug */,\n\t\t\t\t504EC3151FED79650016851F /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget \"App\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t504EC3171FED79650016851F /* Debug */,\n\t\t\t\t504EC3181FED79650016851F /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 504EC2FC1FED79650016851F /* Project object */;\n}\n"
  },
  {
    "path": "ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:App.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1530\"\n   version = \"1.7\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\"\n      buildArchitectures = \"Automatic\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"504EC3031FED79650016851F\"\n               BuildableName = \"App.app\"\n               BlueprintName = \"App\"\n               ReferencedContainer = \"container:App.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      shouldAutocreateTestPlan = \"YES\">\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"504EC3031FED79650016851F\"\n            BuildableName = \"App.app\"\n            BlueprintName = \"App\"\n            ReferencedContainer = \"container:App.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n      <CommandLineArguments>\n         <CommandLineArgument\n            argument = \"\"\n            isEnabled = \"YES\">\n         </CommandLineArgument>\n      </CommandLineArguments>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"504EC3031FED79650016851F\"\n            BuildableName = \"App.app\"\n            BlueprintName = \"App\"\n            ReferencedContainer = \"container:App.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "ios/App/App.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:App.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/App/Gemfile",
    "content": "source \"https://rubygems.org\"\n\ngem \"fastlane\"\n"
  },
  {
    "path": "ios/App/MyCustomViewController.swift",
    "content": "//\n//  MyCustomViewController.swift\n//  App\n//\n//  Created by Paul Miller on 9/6/23.\n//\n\nimport UIKit\nimport Capacitor\n\nclass MyCustomViewController: CAPBridgeViewController {\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        webView!.allowsBackForwardNavigationGestures = true\n        webView!.scrollView.bounces = false\n        webView!.scrollView.alwaysBounceVertical = false\n        webView!.scrollView.showsVerticalScrollIndicator = false\n    }\n}\n"
  },
  {
    "path": "ios/App/Podfile",
    "content": "require_relative '../../node_modules/.pnpm/@capacitor+ios@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/ios/scripts/pods_helpers'\n\nplatform :ios, '13.0'\nuse_frameworks!\n\n# workaround to avoid Xcode caching of Pods that requires\n# Product -> Clean Build Folder after new Cordova plugins installed\n# Requires CocoaPods 1.6 or newer\ninstall! 'cocoapods', :disable_input_output_paths => true\n\ndef capacitor_pods\n  pod 'Capacitor', :path => '../../node_modules/.pnpm/@capacitor+ios@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/ios'\n  pod 'CapacitorCordova', :path => '../../node_modules/.pnpm/@capacitor+ios@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/ios'\n  pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor-mlkit/barcode-scanning'\n  pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/app'\n  pod 'CapacitorAppLauncher', :path => '../../node_modules/.pnpm/@capacitor+app-launcher@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/app-launcher'\n  pod 'CapacitorClipboard', :path => '../../node_modules/.pnpm/@capacitor+clipboard@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/clipboard'\n  pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/filesystem'\n  pod 'CapacitorHaptics', :path => '../../node_modules/.pnpm/@capacitor+haptics@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/haptics'\n  pod 'CapacitorNetwork', :path => '../../node_modules/.pnpm/@capacitor+network@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/network'\n  pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/share'\n  pod 'CapacitorStatusBar', :path => '../../node_modules/.pnpm/@capacitor+status-bar@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/status-bar'\n  pod 'CapacitorToast', :path => '../../node_modules/.pnpm/@capacitor+toast@6.0.0_@capacitor+core@6.0.0/node_modules/@capacitor/toast'\n  pod 'CapacitorSecureStoragePlugin', :path => '../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@6.0.0/node_modules/capacitor-secure-storage-plugin'\nend\n\ntarget 'App' do\n  capacitor_pods\n  # Add your Pods here\nend\n\npost_install do |installer|\n  assertDeploymentTarget(installer)\nend\n"
  },
  {
    "path": "ios/App/fastlane/.gitignore",
    "content": "report.xml"
  },
  {
    "path": "ios/App/fastlane/Appfile",
    "content": "app_identifier(ENV[\"DEVELOPER_APP_IDENTIFIER\"]) # The bundle identifier of your app\napple_id(ENV[\"FASTLANE_APPLE_ID\"]) # Your Apple Developer Portal username\nitc_team_id(ENV[\"APP_STORE_CONNECT_TEAM_ID\"]) # App Store Connect Team ID\nteam_id(ENV[\"DEVELOPER_PORTAL_TEAM_ID\"]) # Developer Portal Team ID\n\n# For more information about the Appfile, see:\n#     https://docs.fastlane.tools/advanced/#appfile\n"
  },
  {
    "path": "ios/App/fastlane/Fastfile",
    "content": "# This file contains the fastlane.tools configuration\n# You can find the documentation at https://docs.fastlane.tools\n#\n# For a list of all available actions, check out\n#\n#     https://docs.fastlane.tools/actions\n#\n# For a list of all available plugins, check out\n#\n#     https://docs.fastlane.tools/plugins/available-plugins\n#\n\n# Uncomment the line if you want fastlane to automatically update itself\n# update_fastlane\n\ndefault_platform(:ios)\n\nISSUER_ID = ENV[\"APPLE_ISSUER_ID\"]\nKEY_ID = ENV[\"APPLE_KEY_ID\"]\nKEY_CONTENT = ENV[\"APPLE_KEY_CONTENT\"]\n\nKEYCHAIN_NAME = ENV[\"KEYCHAIN_NAME\"]\nKEYCHAIN_PASSWORD = ENV[\"KEYCHAIN_PASSWORD\"]\nAPP_INDENTIFIER = ENV[\"DEVELOPER_APP_INDENTIFIER\"]\n\ndef delete_temp_keychain(name)\n  delete_keychain(\n    name: name\n  ) if File.exist? File.expand_path(\"~/Library/Keychains/#{name}-db\")\nend\n\ndef create_temp_keychain(name, password)\n  create_keychain(\n    name: name,\n    password: password,\n    unlock: false,\n    timeout: 0\n  )\nend\n\ndef ensure_temp_keychain(name, password)\n  delete_temp_keychain(name)\n  create_temp_keychain(name, password)\nend\n\nplatform :ios do\n  desc \"Push a new beta build to TestFlight\"\n  lane :beta do\n    api_key = app_store_connect_api_key(\n      key_id: KEY_ID,\n      issuer_id: ISSUER_ID,\n      key_content: KEY_CONTENT\n    )\n\n    # In CI we don't want to be prompted for a password\n    ensure_temp_keychain(KEYCHAIN_NAME, KEYCHAIN_PASSWORD)\n\n    match(\n      type: \"appstore\",\n      keychain_name: KEYCHAIN_NAME,\n      keychain_password: KEYCHAIN_PASSWORD,\n      readonly: is_ci\n    )\n\n    testflight_build_num = app_store_build_number(\n      live: false,\n      api_key: api_key,\n      app_identifier: APP_INDENTIFIER\n    )\n\n    increment_build_number(\n      build_number: testflight_build_num + 1,\n      xcodeproj: \"App.xcodeproj\"\n    )\n\n    build_app(\n      workspace: \"App.xcworkspace\",\n      scheme: \"App\",\n      xcargs: \"-allowProvisioningUpdates\"\n    )\n    upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: true)\n  end\n\n  lane :build do\n    api_key = app_store_connect_api_key(\n      key_id: KEY_ID,\n      issuer_id: ISSUER_ID,\n      key_content: KEY_CONTENT\n    )\n\n    # In CI we don't want to be prompted for a password\n    ensure_temp_keychain(KEYCHAIN_NAME, KEYCHAIN_PASSWORD)\n\n    match(\n      type: \"appstore\",\n      keychain_name: KEYCHAIN_NAME,\n      keychain_password: KEYCHAIN_PASSWORD,\n      readonly: is_ci\n    )\n\n    build_app(\n      workspace: \"App.xcworkspace\",\n      scheme: \"App\",\n      xcargs: \"-allowProvisioningUpdates\"\n    )\n  end\nend\n"
  },
  {
    "path": "ios/App/fastlane/Matchfile",
    "content": "CERTIFICATE_STORE_URL = ENV[\"CERTIFICATE_STORE_URL\"]\nGIT_USERNAME = ENV[\"GIT_USERNAME\"]\nGIT_TOKEN = ENV[\"GIT_TOKEN\"]\nFASTLANE_APPLE_ID = ENV[\"FASTLANE_APPLE_ID\"]\n\ngit_url(CERTIFICATE_STORE_URL)\n\nstorage_mode(\"git\")\ntype(\"appstore\") # The default type, can be: appstore, adhoc, enterprise or development\n\ngit_basic_authorization(Base64.strict_encode64(\"#{GIT_USERNAME}:#{GIT_TOKEN}\"))\nusername(FASTLANE_APPLE_ID)\n\n# app_identifier([\"tools.fastlane.app\", \"tools.fastlane.app2\"])\n# username(\"user@fastlane.tools\") # Your Apple Developer Portal username\n\n# For all available options run `fastlane match --help`\n# Remove the # in the beginning of the line to enable the other options\n\n# The docs are available on https://docs.fastlane.tools/actions/match\n"
  },
  {
    "path": "ios/App/fastlane/README.md",
    "content": "fastlane documentation\n----\n\n# Installation\n\nMake sure you have the latest version of the Xcode command line tools installed:\n\n```sh\nxcode-select --install\n```\n\nFor _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)\n\n# Available Actions\n\n## iOS\n\n### ios beta\n\n```sh\n[bundle exec] fastlane ios beta\n```\n\nPush a new beta build to TestFlight\n\n### ios build\n\n```sh\n[bundle exec] fastlane ios build\n```\n\n\n\n----\n\nThis README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.\n\nMore information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).\n\nThe documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).\n"
  },
  {
    "path": "justfile",
    "content": "set dotenv-load := false\n\ndev:\n    pnpm run dev\n\npre:\n    pnpm run pre-commit\n\nlocal:\n    pnpm install && pnpm link --global \"@mutinywallet/mutiny-wasm\"\n\nremote:\n    pnpm unlink --filter \"@mutinywallet/mutiny-wasm\" && pnpm install\n\nnative:\n    pnpm install && pnpm build && npx cap sync\n\ntest:\n    pnpm exec playwright test\n    \ntest-ui:\n    pnpm exec playwright test --ui\n\nmainnet:\n    cp .env.mainnet .env.local\n\nsignet:\n    cp .env.signet .env.local\n"
  },
  {
    "path": "manifest.ts",
    "content": "import { ManifestOptions } from \"vite-plugin-pwa\";\n\nconst manifest: Partial<ManifestOptions> = {\n    name: \"Mutiny Wallet\",\n    orientation: \"portrait\",\n    short_name: \"Mutiny\",\n    description: \"A lightning wallet\",\n    theme_color: \"#000\",\n    display: \"standalone\",\n    categories: [\"finance\", \"social\"],\n    icons: [\n        {\n            src: \"192.png\",\n            sizes: \"192x192\",\n            type: \"image/png\"\n        },\n        {\n            src: \"512.png\",\n            sizes: \"512x512\",\n            type: \"image/png\"\n        },\n        {\n            src: \"maskable_icon.png\",\n            sizes: \"512x512\",\n            type: \"image/png\",\n            purpose: \"maskable\"\n        },\n        {\n            src: \"maskable_icon.png\",\n            sizes: \"512x512\",\n            type: \"image/png\",\n            purpose: \"any\"\n        },\n        {\n            src: \"windows11/SmallTile.scale-100.png\",\n            sizes: \"71x71\"\n        },\n        {\n            src: \"windows11/SmallTile.scale-125.png\",\n            sizes: \"89x89\"\n        },\n        {\n            src: \"windows11/SmallTile.scale-150.png\",\n            sizes: \"107x107\"\n        },\n        {\n            src: \"windows11/SmallTile.scale-200.png\",\n            sizes: \"142x142\"\n        },\n        {\n            src: \"windows11/SmallTile.scale-400.png\",\n            sizes: \"284x284\"\n        },\n        {\n            src: \"windows11/Square150x150Logo.scale-100.png\",\n            sizes: \"150x150\"\n        },\n        {\n            src: \"windows11/Square150x150Logo.scale-125.png\",\n            sizes: \"188x188\"\n        },\n        {\n            src: \"windows11/Square150x150Logo.scale-150.png\",\n            sizes: \"225x225\"\n        },\n        {\n            src: \"windows11/Square150x150Logo.scale-200.png\",\n            sizes: \"300x300\"\n        },\n        {\n            src: \"windows11/Square150x150Logo.scale-400.png\",\n            sizes: \"600x600\"\n        },\n        {\n            src: \"windows11/Wide310x150Logo.scale-100.png\",\n            sizes: \"310x150\"\n        },\n        {\n            src: \"windows11/Wide310x150Logo.scale-125.png\",\n            sizes: \"388x188\"\n        },\n        {\n            src: \"windows11/Wide310x150Logo.scale-150.png\",\n            sizes: \"465x225\"\n        },\n        {\n            src: \"windows11/Wide310x150Logo.scale-200.png\",\n            sizes: \"620x300\"\n        },\n        {\n            src: \"windows11/Wide310x150Logo.scale-400.png\",\n            sizes: \"1240x600\"\n        },\n        {\n            src: \"windows11/LargeTile.scale-100.png\",\n            sizes: \"310x310\"\n        },\n        {\n            src: \"windows11/LargeTile.scale-125.png\",\n            sizes: \"388x388\"\n        },\n        {\n            src: \"windows11/LargeTile.scale-150.png\",\n            sizes: \"465x465\"\n        },\n        {\n            src: \"windows11/LargeTile.scale-200.png\",\n            sizes: \"620x620\"\n        },\n        {\n            src: \"windows11/LargeTile.scale-400.png\",\n            sizes: \"1240x1240\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.scale-100.png\",\n            sizes: \"44x44\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.scale-125.png\",\n            sizes: \"55x55\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.scale-150.png\",\n            sizes: \"66x66\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.scale-200.png\",\n            sizes: \"88x88\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.scale-400.png\",\n            sizes: \"176x176\"\n        },\n        {\n            src: \"windows11/StoreLogo.scale-100.png\",\n            sizes: \"50x50\"\n        },\n        {\n            src: \"windows11/StoreLogo.scale-125.png\",\n            sizes: \"63x63\"\n        },\n        {\n            src: \"windows11/StoreLogo.scale-150.png\",\n            sizes: \"75x75\"\n        },\n        {\n            src: \"windows11/StoreLogo.scale-200.png\",\n            sizes: \"100x100\"\n        },\n        {\n            src: \"windows11/StoreLogo.scale-400.png\",\n            sizes: \"200x200\"\n        },\n        {\n            src: \"windows11/SplashScreen.scale-100.png\",\n            sizes: \"620x300\"\n        },\n        {\n            src: \"windows11/SplashScreen.scale-125.png\",\n            sizes: \"775x375\"\n        },\n        {\n            src: \"windows11/SplashScreen.scale-150.png\",\n            sizes: \"930x450\"\n        },\n        {\n            src: \"windows11/SplashScreen.scale-200.png\",\n            sizes: \"1240x600\"\n        },\n        {\n            src: \"windows11/SplashScreen.scale-400.png\",\n            sizes: \"2480x1200\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-16.png\",\n            sizes: \"16x16\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-20.png\",\n            sizes: \"20x20\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-24.png\",\n            sizes: \"24x24\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-30.png\",\n            sizes: \"30x30\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-32.png\",\n            sizes: \"32x32\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-36.png\",\n            sizes: \"36x36\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-40.png\",\n            sizes: \"40x40\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-44.png\",\n            sizes: \"44x44\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-48.png\",\n            sizes: \"48x48\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-60.png\",\n            sizes: \"60x60\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-64.png\",\n            sizes: \"64x64\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-72.png\",\n            sizes: \"72x72\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-80.png\",\n            sizes: \"80x80\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-96.png\",\n            sizes: \"96x96\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.targetsize-256.png\",\n            sizes: \"256x256\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-16.png\",\n            sizes: \"16x16\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-20.png\",\n            sizes: \"20x20\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-24.png\",\n            sizes: \"24x24\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-30.png\",\n            sizes: \"30x30\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-32.png\",\n            sizes: \"32x32\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-36.png\",\n            sizes: \"36x36\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-40.png\",\n            sizes: \"40x40\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-44.png\",\n            sizes: \"44x44\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-48.png\",\n            sizes: \"48x48\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-60.png\",\n            sizes: \"60x60\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-64.png\",\n            sizes: \"64x64\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-72.png\",\n            sizes: \"72x72\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-80.png\",\n            sizes: \"80x80\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-96.png\",\n            sizes: \"96x96\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-unplated_targetsize-256.png\",\n            sizes: \"256x256\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png\",\n            sizes: \"16x16\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png\",\n            sizes: \"20x20\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png\",\n            sizes: \"24x24\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png\",\n            sizes: \"30x30\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png\",\n            sizes: \"32x32\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png\",\n            sizes: \"36x36\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png\",\n            sizes: \"40x40\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png\",\n            sizes: \"44x44\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png\",\n            sizes: \"48x48\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png\",\n            sizes: \"60x60\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png\",\n            sizes: \"64x64\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png\",\n            sizes: \"72x72\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png\",\n            sizes: \"80x80\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png\",\n            sizes: \"96x96\"\n        },\n        {\n            src: \"windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png\",\n            sizes: \"256x256\"\n        },\n        {\n            src: \"android/android-launchericon-512-512.png\",\n            sizes: \"512x512\"\n        },\n        {\n            src: \"android/android-launchericon-192-192.png\",\n            sizes: \"192x192\"\n        },\n        {\n            src: \"android/android-launchericon-144-144.png\",\n            sizes: \"144x144\"\n        },\n        {\n            src: \"android/android-launchericon-96-96.png\",\n            sizes: \"96x96\"\n        },\n        {\n            src: \"android/android-launchericon-72-72.png\",\n            sizes: \"72x72\"\n        },\n        {\n            src: \"android/android-launchericon-48-48.png\",\n            sizes: \"48x48\"\n        },\n        {\n            src: \"ios/16.png\",\n            sizes: \"16x16\"\n        },\n        {\n            src: \"ios/20.png\",\n            sizes: \"20x20\"\n        },\n        {\n            src: \"ios/29.png\",\n            sizes: \"29x29\"\n        },\n        {\n            src: \"ios/32.png\",\n            sizes: \"32x32\"\n        },\n        {\n            src: \"ios/40.png\",\n            sizes: \"40x40\"\n        },\n        {\n            src: \"ios/50.png\",\n            sizes: \"50x50\"\n        },\n        {\n            src: \"ios/57.png\",\n            sizes: \"57x57\"\n        },\n        {\n            src: \"ios/58.png\",\n            sizes: \"58x58\"\n        },\n        {\n            src: \"ios/60.png\",\n            sizes: \"60x60\"\n        },\n        {\n            src: \"ios/64.png\",\n            sizes: \"64x64\"\n        },\n        {\n            src: \"ios/72.png\",\n            sizes: \"72x72\"\n        },\n        {\n            src: \"ios/76.png\",\n            sizes: \"76x76\"\n        },\n        {\n            src: \"ios/80.png\",\n            sizes: \"80x80\"\n        },\n        {\n            src: \"ios/87.png\",\n            sizes: \"87x87\"\n        },\n        {\n            src: \"ios/100.png\",\n            sizes: \"100x100\"\n        },\n        {\n            src: \"ios/114.png\",\n            sizes: \"114x114\"\n        },\n        {\n            src: \"ios/120.png\",\n            sizes: \"120x120\"\n        },\n        {\n            src: \"ios/128.png\",\n            sizes: \"128x128\"\n        },\n        {\n            src: \"ios/144.png\",\n            sizes: \"144x144\"\n        },\n        {\n            src: \"ios/152.png\",\n            sizes: \"152x152\"\n        },\n        {\n            src: \"ios/167.png\",\n            sizes: \"167x167\"\n        },\n        {\n            src: \"ios/180.png\",\n            sizes: \"180x180\"\n        },\n        {\n            src: \"ios/192.png\",\n            sizes: \"192x192\"\n        },\n        {\n            src: \"ios/256.png\",\n            sizes: \"256x256\"\n        },\n        {\n            src: \"ios/512.png\",\n            sizes: \"512x512\"\n        },\n        {\n            src: \"ios/1024.png\",\n            sizes: \"1024x1024\"\n        }\n    ],\n    shortcuts: [\n        {\n            name: \"Send\",\n            short_name: \"Send\",\n            url: \"/send\",\n            icons: [\n                {\n                    src: \"/images/send.png\",\n                    sizes: \"192x192\",\n                    type: \"image/png\"\n                }\n            ]\n        },\n        {\n            name: \"Receive\",\n            short_name: \"Receive\",\n            url: \"/receive\",\n            icons: [\n                {\n                    src: \"/images/receive.png\",\n                    sizes: \"192x192\",\n                    type: \"image/png\"\n                }\n            ]\n        },\n        {\n            name: \"Activity\",\n            short_name: \"Activity\",\n            url: \"/activity\",\n            icons: [\n                {\n                    src: \"/images/activity.png\",\n                    sizes: \"192x192\",\n                    type: \"image/png\"\n                }\n            ]\n        }\n    ]\n};\n\nexport default manifest;\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"mutiny-wallet\",\n    \"version\": \"1.8.0\",\n    \"license\": \"MIT\",\n    \"packageManager\": \"pnpm@8.6.6\",\n    \"scripts\": {\n        \"dev\": \"vite\",\n        \"build\": \"tsc && vite build\",\n        \"check-types\": \"tsc --noemit\",\n        \"eslint\": \"eslint src\",\n        \"eslint-path\": \"eslint\",\n        \"pre-commit\": \"pnpm eslint && pnpm run check-types && pnpm run format\",\n        \"format\": \"prettier --write \\\"{.,src/**,e2e/**}/*.{ts,tsx,js,jsx,json,css,scss,md}\\\"\",\n        \"check-format\": \"prettier --check \\\"src/**/*.{ts,tsx,js,jsx,json,css,scss,md}\\\"\"\n    },\n    \"type\": \"module\",\n    \"devDependencies\": {\n        \"@capacitor/assets\": \"^3.0.5\",\n        \"@capacitor/cli\": \"6.0.0\",\n        \"@ianvs/prettier-plugin-sort-imports\": \"^4.2.1\",\n        \"@playwright/test\": \"^1.42.1\",\n        \"@typescript-eslint/eslint-plugin\": \"^7.4.0\",\n        \"@typescript-eslint/parser\": \"^7.4.0\",\n        \"autoprefixer\": \"^10.4.19\",\n        \"eslint\": \"^8.57.0\",\n        \"eslint-import-resolver-typescript\": \"3.6.1\",\n        \"eslint-plugin-import\": \"2.29.1\",\n        \"eslint-plugin-prettier\": \"5.1.3\",\n        \"eslint-plugin-solid\": \"0.13.1\",\n        \"postcss\": \"^8.4.38\",\n        \"prettier\": \"^3.2.5\",\n        \"prettier-plugin-tailwindcss\": \"^0.5.12\",\n        \"tailwindcss\": \"^3.4.1\",\n        \"typescript\": \"^5.4.5\",\n        \"vite\": \"^5.2.10\",\n        \"vite-plugin-comlink\": \"^4.0.3\",\n        \"vite-plugin-pwa\": \"^0.19.8\",\n        \"vite-plugin-solid\": \"^2.10.2\",\n        \"vite-plugin-wasm\": \"^3.3.0\",\n        \"workbox-window\": \"^7.0.0\"\n    },\n    \"dependencies\": {\n        \"@capacitor-mlkit/barcode-scanning\": \"^6.0.0\",\n        \"@capacitor/android\": \"^6.0.0\",\n        \"@capacitor/app\": \"^6.0.0\",\n        \"@capacitor/app-launcher\": \"^6.0.0\",\n        \"@capacitor/clipboard\": \"^6.0.0\",\n        \"@capacitor/core\": \"^6.0.0\",\n        \"@capacitor/filesystem\": \"^6.0.0\",\n        \"@capacitor/haptics\": \"^6.0.0\",\n        \"@capacitor/ios\": \"^6.0.0\",\n        \"@capacitor/network\": \"^6.0.0\",\n        \"@capacitor/share\": \"^6.0.0\",\n        \"@capacitor/status-bar\": \"^6.0.0\",\n        \"@capacitor/toast\": \"^6.0.0\",\n        \"@kobalte/core\": \"^0.12.6\",\n        \"@kobalte/tailwindcss\": \"^0.9.0\",\n        \"@modular-forms/solid\": \"^0.20.0\",\n        \"@mutinywallet/mutiny-wasm\": \"1.7.13\",\n        \"@solid-primitives/upload\": \"^0.0.117\",\n        \"@solidjs/meta\": \"^0.29.3\",\n        \"@solidjs/router\": \"^0.13.1\",\n        \"capacitor-secure-storage-plugin\": \"^0.9.0\",\n        \"comlink\": \"^4.4.1\",\n        \"i18next\": \"^23.10.1\",\n        \"i18next-browser-languagedetector\": \"^7.2.0\",\n        \"i18next-http-backend\": \"^2.5.0\",\n        \"lucide-solid\": \"^0.363.0\",\n        \"qr-scanner\": \"^1.4.2\",\n        \"solid-js\": \"^1.8.16\",\n        \"solid-qr-code\": \"^0.0.8\",\n        \"solid-transition-group\": \"^0.2.3\"\n    },\n    \"engines\": {\n        \"node\": \">=18\"\n    }\n}\n"
  },
  {
    "path": "playwright.config.ts",
    "content": "import { defineConfig, devices } from \"@playwright/test\";\n\n/**\n * Read environment variables from file.\n * https://github.com/motdotla/dotenv\n */\n// require('dotenv').config();\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\nexport default defineConfig({\n    testDir: \"./e2e\",\n    /* Run tests in files in parallel */\n    fullyParallel: true,\n    /* Fail the build on CI if you accidentally left test.only in the source code. */\n    forbidOnly: !!process.env.CI,\n    /* Retry on CI only */\n    retries: process.env.CI ? 2 : 0,\n    /* Opt out of parallel tests on CI. */\n    workers: process.env.CI ? 1 : undefined,\n    /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n    reporter: \"html\",\n    /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n    use: {\n        /* Base URL to use in actions like `await page.goto('/')`. */\n        // baseURL: 'http://127.0.0.1:3000',\n\n        /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n        trace: \"on-first-retry\"\n    },\n\n    /* Configure projects for major browsers */\n    projects: [\n        // {\n        //   name: \"chromium\",\n        //   use: { ...devices[\"Desktop Chrome\"] }\n        // },\n\n        // {\n        //     name: \"firefox\",\n        //     use: { ...devices[\"Desktop Firefox\"] }\n        // },\n\n        // {\n        //   name: \"webkit\",\n        //   use: { ...devices[\"Desktop Safari\"] }\n        // },\n\n        /* Test against mobile viewports. */\n        {\n            name: \"Mobile Chrome\",\n            use: { ...devices[\"Pixel 5\"] }\n        }\n        // {\n        //     name: \"Mobile Safari\",\n        //     use: { ...devices[\"iPhone 12\"] }\n        // }\n\n        /* Test against branded browsers. */\n        // {\n        //   name: 'Microsoft Edge',\n        //   use: { ...devices['Desktop Edge'], channel: 'msedge' },\n        // },\n        // {\n        //   name: 'Google Chrome',\n        //   use: { ..devices['Desktop Chrome'], channel: 'chrome' },\n        // },\n    ],\n\n    /* Run your local dev server before starting the tests */\n    webServer: {\n        command: \"pnpm run dev\",\n        url: \"http://localhost:3420\",\n        reuseExistingServer: !process.env.CI\n    }\n});\n"
  },
  {
    "path": "postcss.config.cjs",
    "content": "module.exports = {\n    plugins: {\n      tailwindcss: {},\n      autoprefixer: {},\n    },\n  };\n  "
  },
  {
    "path": "prettier.config.mjs",
    "content": "/** @type {import(\"prettier\").Options} */\nexport default {\n    trailingComma: \"none\",\n    tabWidth: 4,\n    semi: true,\n    singleQuote: false,\n    arrowParens: \"always\",\n    printWidth: 80,\n    useTabs: false,\n\n    plugins: [\n        \"@ianvs/prettier-plugin-sort-imports\",\n        \"prettier-plugin-tailwindcss\" // MUST come last\n    ],\n\n    tailwindFunctions: [\"classList\"],\n\n    importOrder: [\"<THIRD_PARTY_MODULES>\", \"\", \"^[~/]\", \"\", \"^[./]\"]\n};\n"
  },
  {
    "path": "public/.well-known/apple-app-site-association",
    "content": "{\n  \"applinks\": {\n    \"apps\": [],\n    \"details\": [\n      {\n        \"appID\": \"X773Y823TN.com.mutinywallet.mutiny\",\n        \"paths\": [\"*\"]\n      }\n    ]\n  }\n}"
  },
  {
    "path": "public/.well-known/assetlinks.json",
    "content": "[{\n  \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n  \"target\": {\n    \"namespace\": \"android_app\",\n    \"package_name\": \"com.mutinywallet.mutinywallet\",\n    \"sha256_cert_fingerprints\":\n    [\"F8:20:46:16:9B:79:AA:D1:2A:77:CA:D6:1F:0E:F6:A4:D7:72:3A:5E:DE:C6:70:18:E9:9E:B4:DA:0B:44:17:60\"]\n  }\n}]\n"
  },
  {
    "path": "public/i18n/de.json",
    "content": "{\n  \"common\": {\n    \"title\": \"Mutiny Wallet\",\n    \"mutiny\": \"Mutiny\",\n    \"nice\": \"Schön\",\n    \"home\": \"Home\",\n    \"e_sats\": \"eSATS\",\n    \"e_sat\": \"eSAT\",\n    \"sats\": \"SATS\",\n    \"sat\": \"SAT\",\n    \"fee\": \"Gebühren\",\n    \"send\": \"Senden\",\n    \"receive\": \"Erhalten\",\n    \"dangit\": \"Mist\",\n    \"back\": \"Zurück\",\n    \"coming_soon\": \"(demnächst)\",\n    \"copy\": \"Kopieren\",\n    \"copied\": \"Kopiert\",\n    \"continue\": \"Weiter\",\n    \"error_unimplemented\": \"Nicht implementiert\",\n    \"why\": \"Warum?\",\n    \"private_tags\": \"Private Schlagwörter\",\n    \"view_transaction\": \"Transaction anzeigen\",\n    \"view_payment_details\": \"Zahlungsdetails anzeigen\",\n    \"pending\": \"Ausstehend\",\n    \"error_safe_mode\": \"Mutiny läuft im Safemode. Lightning ist deaktiviert.\",\n    \"self_hosted\": \"Selbst gehostet\"\n  },\n  \"contacts\": {\n    \"new\": \"neu\",\n    \"add_contact\": \"Kontakt hinzufügen\",\n    \"new_contact\": \"Neuer Kontakt\",\n    \"create_contact\": \"Kontakt hinzufügen\",\n    \"edit_contact\": \"Kontakt bearbeiten\",\n    \"save_contact\": \"Kontakt speichern\",\n    \"delete\": \"Löschen\",\n    \"confirm_delete\": \"Bist du sicher, dass der Kontakt gelöscht werden soll?\",\n    \"payment_history\": \"Zahlungshistorie\",\n    \"no_payments\": \"Bisher keine Zahlungen mit\",\n    \"edit\": \"Ändern\",\n    \"pay\": \"Bezahlen\",\n    \"name\": \"Name\",\n    \"ln_address\": \"Lightning Adresse\",\n    \"placeholder\": \"Satoshi\",\n    \"lightning_address\": \"Lightning Adresse\",\n    \"unimplemented\": \"Nicht implementiert\",\n    \"not_available\": \"Noch nicht verfügbar\",\n    \"error_name\": \"Wir benötigen mindestens einen Namen\",\n    \"email_error\": \"Das sieht nicht nach einer Lightning Adresse aus\",\n    \"npub_error\": \"Das sieht nicht nach einem Nostr npub aus\",\n    \"error_ln_address_missing\": \"Neue Kontakte benötigen eine Lightning Adresse\",\n    \"npub\": \"Nostr Npub\",\n    \"link_to_nostr_sync\": \"Nostr Kontakte importieren\"\n  },\n  \"redeem\": {\n    \"redeem_bitcoin\": \"Bitcoin einlösen\",\n    \"lnurl_amount_message\": \"Auszubezahlender Betrag zwischen {{min}} und {{max}} Sats eingeben\",\n    \"lnurl_redeem_failed\": \"Auszahlung abgebrochen\",\n    \"lnurl_redeem_success\": \"Zahlung erhalten\"\n  },\n  \"receive\": {\n    \"receive_bitcoin\": \"Bitcoin erhalten\",\n    \"edit\": \"Bearbeiten\",\n    \"checking\": \"Überprüfung\",\n    \"choose_format\": \"Format wählen\",\n    \"payment_received\": \"Zahlung erhalten\",\n    \"payment_initiated\": \"Zahlung initiiert\",\n    \"receive_add_the_sender\": \"Absenderinformationen hinzufügen\",\n    \"keep_mutiny_open\": \"Bitte Mutiny geöffnet lassen, um die Zahlung abzuschliessen.\",\n    \"choose_payment_format\": \"Zahlungsformat auswählen\",\n    \"unified_label\": \"Kombiniert\",\n    \"unified_caption\": \"Kombiniert eine Bitcoin Adresse mit einer Lightning Rechnung. Der Sender wählt die Zahlungsmethode.\",\n    \"lightning_label\": \"Lightning Rechnung\",\n    \"lightning_caption\": \"Ideal für kleine Beträge. In der Regel mit geringeren Gebühren als eine On-chain Transaktion.\",\n    \"onchain_label\": \"Bitcoin Adresse\",\n    \"onchain_caption\": \"On-chain Transaktion, wie es Satoshi getan hat. Ideal für sehr grosse Beträge.\",\n    \"unified_setup_fee\": \"Bei einer Zahlung über Lightning wird eine Einrichtungsgebühr von {{amount}} SATS verrechnet.\",\n    \"lightning_setup_fee\": \"Beim Erhalten einer Zahlung wird eine Lightning Einrichtungsgebühr von {{amount}} SATS verrechnet.\",\n    \"amount\": \"Betrag\",\n    \"fee\": \"+ Gebühr\",\n    \"total\": \"Total\",\n    \"spendable\": \"Verfügbar\",\n    \"channel_size\": \"Kanalgrösse\",\n    \"channel_reserve\": \"- Kanalreserve\",\n    \"error_under_min_lightning\": \"Standardmässig über On-chain. Betrag ist zu klein für den Empfang deiner initiale Lightningzahlung.\",\n    \"error_creating_unified\": \"Standardmässig über On-chain. Etwas ist schief gelaufen bei der Erstellung einer einheitlichen Adresse\",\n    \"error_creating_address\": \"Etwas ist schief gelaufen bei der Erstellung einer On-chain Adresse\",\n    \"amount_editable\": {\n      \"receive_too_small\": \"Eine Lightning Einrichtungsgebühr wird möglicherweise vom angeforderten Betrag abgezogen.\",\n      \"setup_fee_lightning\": \"Beim Bezahlen über Lightning wird eine Einrichtungsgebühr fällig.\",\n      \"more_than_21m\": \"Es gibt nur 21 Millionen Bitcoin.\",\n      \"set_amount\": \"Bestimme den Betrag\",\n      \"max\": \"MAX\",\n      \"fix_amounts\": {\n        \"ten_k\": \"10k\",\n        \"one_hundred_k\": \"100k\",\n        \"one_million\": \"1m\"\n      },\n      \"del\": \"LÖSCHEN\",\n      \"balance\": \"Kontostand\"\n    },\n    \"integrated_qr\": {\n      \"onchain\": \"On-chain\",\n      \"lightning\": \"Lightning\",\n      \"unified\": \"Kombiniert\",\n      \"gift\": \"Lightning Geschenk\"\n    },\n    \"remember_choice\": \"Auswahl merken\",\n    \"what_for\": \"Wozu?\"\n  },\n  \"send\": {\n    \"search\": {\n      \"placeholder\": \"Name, Adresse, Rechnung\",\n      \"paste\": \"Einfügen\",\n      \"contacts\": \"Kontakte\",\n      \"global_search\": \"Globale Suche\",\n      \"no_results\": \"Keine Resultate für\"\n    },\n    \"sending\": \"Senden...\",\n    \"confirm_send\": \"Sendung bestätigt\",\n    \"contact_placeholder\": \"Empfänger speichern\",\n    \"start_over\": \"Start über\",\n    \"send_bitcoin\": \"Bitcoin senden\",\n    \"paste\": \"Einfügen\",\n    \"scan_qr\": \"QR scannen\",\n    \"payment_initiated\": \"Zahlung initiiert\",\n    \"payment_sent\": \"Zahlung gesendet\",\n    \"destination\": \"Ziel\",\n    \"no_payment_info\": \"Keine Zahlungsinfo\",\n    \"progress_bar\": {\n      \"of\": \"von\",\n      \"sats_sent\": \"Sats gesendet\"\n    },\n    \"what_for\": \"Wozu?\",\n    \"zap_note\": \"Zap Notiz\",\n    \"error_low_balance\": \"Der Kontostand ist zu niedrig, um den Betrag zu überweisen.\",\n    \"error_invoice_match\": \"Angeforderter Betrag, {{amount}} SATS, ist nicht identisch mit dem gesetzten Betrag.\",\n    \"error_channel_reserves\": \"Kontostand zu niedrig.\",\n    \"error_address\": \"Ungültige Lightning Adresse\",\n    \"error_channel_reserves_explained\": \"Ein Teil deines Kontostands ist reserviert für Gebühren. Versuche kleinere Beträge zu senden oder den Kontostand zu erhöhen.\",\n    \"error_clipboard\": \"Zwischenablage wird nicht unterstützt\",\n    \"error_keysend\": \"Sendung fehlgeschlagen\",\n    \"error_LNURL\": \"LNURL Bezahlung fehlgeschlagen\",\n    \"error_expired\": \"Rechnung ist abgelaufen\",\n    \"payjoin_send\": \"Das ist ein Payjoin! Die Meuterei wird so lange andauern, bis sich die Privatsphäre verbessert\",\n    \"payment_pending\": \"Zahlung ausstehend\",\n    \"payment_pending_description\": \"Es dauert eine Weile, aber es ist immer noch möglich, dass die Zahlung durchgeht. Bitte bei 'Aktivitäten' den aktuellen Status prüfen.\",\n    \"hodl_invoice_warning\": \"Das ist eine Hodl Rechnung. Zahlungen zu Hodl Rechnungen kann zu Schliessungen von Kanälen mit hohen On-chain Gebühren führen. Bezahlung auf eigenes Risiko!\",\n    \"private\": \"Privat\",\n    \"anonzap\": \"Anon Zap\"\n  },\n  \"feedback\": {\n    \"header\": \"Lasse uns dein Feedback da!\",\n    \"received\": \"Feedback erhalten!\",\n    \"thanks\": \"Danke, dass du uns wissen lässt, was läuft.\",\n    \"more\": \"Hast du noch mehr zu sagen?\",\n    \"tracking\": \"Mutiny verfolgt oder spioniert dein Verhalten nicht aus, daher ist dein Feedback unglaublich hilfreich.\",\n    \"github\": \"Wir werden auf dein Feedback nicht antworten. Wenn du Unterstützung benötigst, bitte melden\",\n    \"create_issue\": \"Ein GitHub-Problem erstellen.\",\n    \"link\": \"Feedback?\",\n    \"feedback_placeholder\": \"Fehler, Funktionswünsche, Feedback, etc.\",\n    \"info_label\": \"Kontaktinformationen angeben\",\n    \"info_caption\": \"Wenn du möchtest, dass wir dieses Problem weiterverfolgen\",\n    \"email\": \"Email\",\n    \"email_caption\": \"Burners willkommen\",\n    \"nostr\": \"Nostr\",\n    \"nostr_caption\": \"Deine neuste npub\",\n    \"nostr_label\": \"Nostr npub oder NIP-05\",\n    \"send_feedback\": \"Feedback senden\",\n    \"invalid_feedback\": \"Bitte sage etwas!\",\n    \"need_contact\": \"Wir benötigen eine Möglichkeit, um dich zu kontaktieren\",\n    \"invalid_email\": \"Das sieht nicht nach einer Email-Adresse aus\",\n    \"error\": \"Fehler beim Senden des Feedbacks {{error}}\",\n    \"try_again\": \"Bitte versuche es später nochmals.\"\n  },\n  \"activity\": {\n    \"title\": \"Aktivität\",\n    \"mutiny\": \"Mutiny\",\n    \"wallet\": \"Wallet\",\n    \"nostr\": \"Nostr\",\n    \"view_all\": \"Alle anzeigen\",\n    \"receive_some_sats_to_get_started\": \"Sats erhalten, um zu starten\",\n    \"channel_open\": \"Kanal öffnen\",\n    \"channel_close\": \"Kanal schliessen\",\n    \"unknown\": \"Unbekannt\",\n    \"import_contacts\": \"Kontakte von Nostr importieren, um zu sehen wer dir etwas zappt.\",\n    \"coming_soon\": \"Demnächst\",\n    \"private\": \"Privat\",\n    \"anonymous\": \"Anonym\",\n    \"from\": \"Von:\",\n    \"transaction_details\": {\n      \"lightning_receive\": \"Erhalten via Lightning\",\n      \"lightning_send\": \"Gesendet via Lightning\",\n      \"channel_open\": \"Kanal öffnen\",\n      \"channel_close\": \"Kanal schliessen\",\n      \"onchain_receive\": \"On-chain erhalten\",\n      \"onchain_send\": \"On-chain gesendet\",\n      \"paid\": \"Bezahlt\",\n      \"unpaid\": \"Unbezahlt\",\n      \"status\": \"Status\",\n      \"date\": \"Datum\",\n      \"tagged_to\": \"Markiert mit\",\n      \"description\": \"Beschreibung\",\n      \"fee\": \"Gebühr\",\n      \"onchain_fee\": \"On-chain Gebühr\",\n      \"invoice\": \"Rechnung\",\n      \"payment_hash\": \"Zahlungs-Hash\",\n      \"payment_preimage\": \"Vorbild\",\n      \"txid\": \"Txid\",\n      \"total\": \"Betrag angefordert\",\n      \"balance\": \"Kontostand\",\n      \"reserve\": \"Reserve\",\n      \"peer\": \"Peer\",\n      \"channel_id\": \"Kanal ID\",\n      \"reason\": \"Grund\",\n      \"confirmed\": \"Bestätigt\",\n      \"unconfirmed\": \"Unbestätigt\",\n      \"sweep_delay\": \"Es kann einige Tage dauern, bis dein Geld wieder in der Wallet verfügbar sind\",\n      \"no_details\": \"Keine Kanaldetails gefunden, was bedeutet, dass dieser Kanal wahrscheinlich geschlossen wurde.\",\n      \"back_home\": \"zurück zur Startseite\"\n    }\n  },\n  \"scanner\": {\n    \"paste\": \"Füge etwas ein\",\n    \"cancel\": \"Abbrechen\"\n  },\n  \"settings\": {\n    \"header\": \"Einstellungen\",\n    \"support\": \"Lerne wie du Mutiny unterstützen kannst\",\n    \"experimental_features\": \"Experimente\",\n    \"debug_tools\": \"DEBUG TOOLS\",\n    \"danger_zone\": \"Gefahrenzone\",\n    \"general\": \"Allgemein\",\n    \"version\": \"Version:\",\n    \"admin\": {\n      \"title\": \"Admin Seite\",\n      \"caption\": \"Unsere internen Debug-Tools. Verwende sie mit Bedacht!\",\n      \"header\": \"Geheime Debug Tools\",\n      \"warning_one\": \"Wenn du weisst was du tust, bist du am richtigen Ort.\",\n      \"warning_two\": \"Das sind interne Tools, die wir fürs Debuggen und Testen der App verwenden. Bitte vorsichtig sein!\",\n      \"kitchen_sink\": {\n        \"disconnect\": \"Trennen\",\n        \"peers\": \"Peers\",\n        \"no_peers\": \"Keine Peers\",\n        \"refresh_peers\": \"Peers aktualisieren\",\n        \"connect_peer\": \"Peer verbinden\",\n        \"expect_a_value\": \"Ein Wert wird erwartet...\",\n        \"connect\": \"Verbinden\",\n        \"close_channel\": \"Kanal schliessen\",\n        \"force_close\": \"Kanal schliessen erzwingen\",\n        \"abandon_channel\": \"Kanal verlassen\",\n        \"confirm_close_channel\": \"Bist du sicher, dass du den Kanal schliessen willst?\",\n        \"confirm_force_close\": \"Bist du sicher, dass du die Kanalschliessung erzwingen willst? Deine Einlagen benötigen ein paar Tage, bis sie On-chain verfügbar sind.\",\n        \"confirm_abandon_channel\": \"Bist du sicher, dass du diesen Kanal verlassen willst? Typischerweise wird diese Aktion nur ausgeführt, wenn die eröffnende Transaktion nicht bestätigt werden kann. Andernfalls wirst du deine Einlagen verlieren.\",\n        \"channels\": \"Kanäle\",\n        \"no_channels\": \"Keine Kanäle\",\n        \"refresh_channels\": \"Kanäle aktualisieren\",\n        \"pubkey\": \"Pubkey\",\n        \"amount\": \"Betrag\",\n        \"open_channel\": \"Kanal eröffnen\",\n        \"nodes\": \"Nodes\",\n        \"no_nodes\": \"Keine Nodes\",\n        \"enable_zaps_to_hodl\": \"Zaps nach Hodl Invoices erlauben?\",\n        \"zaps_to_hodl_desc\": \"Zaps nach Hodl Invoices kann zu erzwungenen Kanalschliessungen und hohen On-chain Gebühren führen. Benutzung auf eigene Gefahr!\",\n        \"zaps_to_hodl_enable\": \"Hodl Zaps aktivieren\",\n        \"zaps_to_hodl_disable\": \"Hodl Zaps deaktivieren\"\n      }\n    },\n    \"backup\": {\n      \"title\": \"Backup\",\n      \"secure_funds\": \"Sorgen wir dafür, dass deine Einlagen gesichert werden.\",\n      \"twelve_words_tip\": \"Wir werden dir 12 Wörter zeigen. Du schreibst die 12 Wörter auf.\",\n      \"warning_one\": \"Wenn du deine Browser-Historie löschst oder dein Gerät verlierst sind diese 12 Wörter der einzige Weg, um dein Wallet wiederherzustellen.\",\n      \"warning_two\": \"Mutiny ist eine selbst verwahrende Wallet. Du allein bist dafür verantwortlich...\",\n      \"confirm\": \"Ich habe die Wörter aufgeschrieben\",\n      \"responsibility\": \"Ich bin mir bewusst, dass meine Einlagen in meiner Verantwortung sind\",\n      \"liar\": \"Ich lüge nicht, nur um das hinter mich zu bringen\",\n      \"seed_words\": {\n        \"reveal\": \"TIPPE, UM DIE SEED-WÖRTER ZU ENTDECKEN\",\n        \"hide\": \"VERSTECKEN\",\n        \"copy\": \"Gefährliches Kopieren in die Zwischenablage\",\n        \"copied\": \"Kopiert!\"\n      }\n    },\n    \"channels\": {\n      \"title\": \"Lightning Kanäle\",\n      \"outbound\": \"Outbound\",\n      \"inbound\": \"Inbound\",\n      \"reserve\": \"Reserve\",\n      \"have_channels\": \"Du hast\",\n      \"have_channels_one\": \"Lightning Kanal.\",\n      \"have_channels_many\": \"lightning Kanäle.\",\n      \"inbound_outbound_tip\": \"Outbound ist der Betrag, den du über Lightning ausgeben kannst. Inbound ist der Betrag, den du über Lightning empfangen kannst ohne eine Lightning Servicegebühr zu bezahlen.\",\n      \"reserve_tip\": \"Um die 1% deines Kontostandes ist reserviert, um Lightning Gebühren zu bezahlen. Zusätzliche Reserven sind nötig, um Kanäle via Swap zu öffnen.\",\n      \"no_channels\": \"Anscheinend hast du noch keine Kanäle. Erhalte ein paar Sats über Lightning, um zu starten. Oder transferiere ein paar on-chain Bitcoin in einen Lightning Kanal. Mache ruhig deine Hände schmutzig!\",\n      \"close_channel\": \"Schliessen\",\n      \"online_channels\": \"Online Kanäle\",\n      \"offline_channels\": \"Offline Kanäle\",\n      \"close_channel_confirm\": \"Wenn der Kanal geschlossen wird, werden die Einlagen nach on-chain transferiert. Dies führt zu einer on-chain Gebühr.\"\n    },\n    \"connections\": {\n      \"title\": \"Wallet Verbindungen\",\n      \"error_name\": \"Name kann nicht leer sein\",\n      \"error_connection\": \"Wallet Verbindung konnte nicht erstellt werden\",\n      \"error_budget_zero\": \"Der Budgetrahmen muss grösser sein als 0\",\n      \"add_connection\": \"Verbindung hinzufügen\",\n      \"manage_connections\": \"Verbindungen verwalten\",\n      \"manage_gifts\": \"Geschenke verwalten\",\n      \"delete_connection\": \"Löschen\",\n      \"new_connection\": \"Neue Verbindung\",\n      \"edit_connection\": \"Verbindung bearbeiten\",\n      \"new_connection_label\": \"Name\",\n      \"new_connection_placeholder\": \"Mein Lieblings-Nostr-Client...\",\n      \"create_connection\": \"Verbindung erstellen\",\n      \"save_connection\": \"Änderungen speichern\",\n      \"edit_budget\": \"Budgetrahmen bearbeiten\",\n      \"open_app\": \"App öffnen\",\n      \"open_in_nostr_client\": \"In Nostr-Client öffnen\",\n      \"open_in_primal\": \"In Primal öffnen\",\n      \"nostr_client_not_found\": \"Nostr-Client konnte nicht gefunden werden\",\n      \"client_not_found_description\": \"Installiere einen Nostr-Client wie Primal, Amethyst oder Damus, um den Link zu öffnen.\",\n      \"relay\": \"Relais\",\n      \"authorize\": \"Autorisiere externe Dienste, um Zahlungen von deinem Wallet anzufordern. Passt hervorragend zu Nostr-Clients.\",\n      \"pending_nwc\": {\n        \"title\": \"Offene Anfragen\",\n        \"approve_all\": \"Alle genehmigen\",\n        \"deny_all\": \"Alle ablehnen\"\n      },\n      \"careful\": \"Vorsicht beim Teilen dieser Verbindung! Anfragen innerhalb des Budgetrahmens werden automatisch bezahlt.\",\n      \"spent\": \"Ausgegeben\",\n      \"remaining\": \"Übrig\",\n      \"confirm_delete\": \"Bist du sicher, dass du diese Verbindung löschen willst?\",\n      \"budget\": \"Budgetrahmen\",\n      \"resets_every\": \"Alles zurücksetzen\",\n      \"resubscribe_date\": \"Erneut abonnieren\"\n    },\n    \"emergency_kit\": {\n      \"title\": \"Notfallbausatz\",\n      \"caption\": \"Probleme im Wallet diagnostizieren und lösen.\",\n      \"emergency_tip\": \"Wenn deine Wallet nicht mehr läuft, findest du hier ein paar Werkzeuge, um es zu debuggen und reparieren.\",\n      \"questions\": \"Bitte melde dich, wenn du Fragen dazu hast, was diese Buttons tun\",\n      \"link\": \"melde dich bei uns für Unterstützung.\",\n      \"import_export\": {\n        \"title\": \"Wallet-Status exportieren\",\n        \"error_password\": \"Passwort wird benötigt\",\n        \"error_read_file\": \"Fehler beim Lesen der Datei\",\n        \"error_no_text\": \"Keinen Text in der Datei gefunden\",\n        \"tip\": \"Du kannst deinen gesamten Status aus der Mutiniy-Wallet als Datei exportieren and sie in einem neuen Browser importieren. Normalerweise funktioniert es!\",\n        \"caveat_header\": \"Wichtige Vorbehalte:\",\n        \"caveat\": \"Nach dem Exportieren bitte keine Operationen mehr im originalen Browser vornehmen. Wenn doch, musst du den Export nochmals durchführen. Nach einem erfolgreichen Import wird empfohlen, den Status im originalen Browser zu löschen. So wird sichergestellt, dass es keine Konflikte gibt.\",\n        \"save_state\": \"Status als Datei speichern\",\n        \"import_state\": \"Status aus einer Datei importieren\",\n        \"confirm_replace\": \"Möchtest du deinen Status ersetzen mit\",\n        \"password\": \"Passwort eingeben, um zu entschlüsseln\",\n        \"decrypt_wallet\": \"Wallet entschlüsseln\"\n      },\n      \"logs\": {\n        \"title\": \"Debug-Protokolle herunterladen\",\n        \"something_screwy\": \"Irgendetwas Verrücktes ist im Gange? Bitte schaue das Protokoll an!\",\n        \"download_logs\": \"Protokolle herunterladen\",\n        \"password\": \"Passwort eingeben, um zu entschlüsseln\",\n        \"confirm_password_label\": \"Passwort bestätigen\"\n      },\n      \"delete_everything\": {\n        \"delete\": \"Alles löschen\",\n        \"confirm\": \"Damit löschst du den Status deiner Node. Das kann nicht rückgängig gemacht werden!\",\n        \"deleted\": \"Gelöscht\",\n        \"deleted_description\": \"Alle Daten gelöscht\"\n      }\n    },\n    \"encrypt\": {\n      \"title\": \"Passwort ändern\",\n      \"caption\": \"Bitte zuerst ein Backup erstellen, um die Verschlüsselung zu entsperren\",\n      \"header\": \"Deine Seed-Wörter entschlüsseln\",\n      \"hot_wallet_warning\": \"Mutiny ist eine \\\"Hot Wallet\\\". Um zu funktionieren, benötigt es deine Seed-Wörter. Du kannst aber optional deine Seed-Wörter mit einem Passwort verschlüsseln.\",\n      \"password_tip\": \"Auf diese Weise hat jemand, der Zugriff auf deinen Browser erhält, immer noch keinen Zugriff auf dein Geld.\",\n      \"optional\": \"(optional)\",\n      \"existing_password\": \"Bestehendes Passwort\",\n      \"existing_password_caption\": \"Leer lassen, wenn du noch kein Passwort gesetzt hast.\",\n      \"new_password_label\": \"Passwort\",\n      \"new_password_placeholder\": \"Ein Passwort eingeben\",\n      \"new_password_caption\": \"Dieses Passwort wird genutzt, um deine Seed-Wörter zu verschlüsseln. Wenn du es vergisst, musst du deine Seed-Wörter wieder eingeben, um an deine Einlagen zu gelangen. Du hast deine Seed-Wörter aufgeschrieben, oder?\",\n      \"confirm_password_label\": \"Passwort bestätigen\",\n      \"confirm_password_placeholder\": \"Passwort wiederholen\",\n      \"encrypt\": \"Verschlüsseln\",\n      \"skip\": \"Überspringen\",\n      \"error_match\": \"Passwort stimmt nicht überein\",\n      \"error_same_as_existingpassword\": \"Das neue Passwort darf nicht mit dem bestehenden Passwort übereinstimmen\"\n    },\n    \"decrypt\": {\n      \"title\": \"Passwort eingeben\",\n      \"decrypt_wallet\": \"Wallet entschlüsseln\",\n      \"forgot_password_link\": \"Passwort vergessen?\",\n      \"error_wrong_password\": \"Passwort ungültig\"\n    },\n    \"currency\": {\n      \"title\": \"Währung\",\n      \"caption\": \"Wähle dein bevorzugtes Währungspaar\",\n      \"select_currency\": \"Währung auswählen\",\n      \"select_currency_label\": \"Währungspaar\",\n      \"select_currency_caption\": \"Wenn du eine neue Währung auswählst, wird das Wallet neu synchronisiert und die Preise aktualisiert\",\n      \"request_currency_support_link\": \"Unterstützung anfordern für weitere Währungen\",\n      \"error_unsupported_currency\": \"Bitte eine unterstützte Währung auswählen.\"\n    },\n    \"language\": {\n      \"title\": \"Sprache\",\n      \"caption\": \"Wähle deine bevorzugte Sprache\",\n      \"select_language\": \"Sprache auswählen\",\n      \"select_language_label\": \"Sprache\",\n      \"select_language_caption\": \"Wenn du eine neue Sprache auswählst, wird die Sprache in der Wallet geändert, unabhängig davon welche Sprache dein Browser verwendet\",\n      \"request_language_support_link\": \"Fordere Unterstützung für weitere Sprachen an\",\n      \"error_unsupported_language\": \"Bitte eine unterstützte Sprache auswählen.\"\n    },\n    \"lnurl_auth\": {\n      \"title\": \"LNURL Auth\",\n      \"auth\": \"Auth\",\n      \"expected\": \"Erwarte etwas wie LNURL...\"\n    },\n    \"plus\": {\n      \"title\": \"Mutiny+\",\n      \"join\": \"Mitmachen\",\n      \"sats_per_month\": \"für {{amount}} Sats im Monat.\",\n      \"lightning_balance\": \"Du benötigst mindestens {{amount}} Sats in deiner Lightning Wallet, um zu starten. Probiere es aus bevor du kaufst!\",\n      \"restore\": \"Abonnement wiederherstellen\",\n      \"ready_to_join\": \"Bereit zum Mitmachen\",\n      \"click_confirm\": \"Klicke auf bestätigen, um deinen ersten Monat zu bezahlen.\",\n      \"open_source\": \"Mutiny ist Open Source und selbstverwahrend.\",\n      \"optional_pay\": \"Aber du kannst auch dafür bezahlen.\",\n      \"paying_for\": \"Dafür bezahlen\",\n      \"supports_dev\": \"unterstützt die laufende Entwicklung und ermöglicht den frühzeitigen Zugriff auf neue Features und Premium-Funktionalitäten:\",\n      \"thanks\": \"Du bist Teil von Mutiny! Genieße die folgenden Vorteile:\",\n      \"renewal_time\": \"Du erhältst eine Zahlungsaufforderung für die Verlängerung\",\n      \"cancel\": \"Um dein Abonnement zu beenden, bezahle einfach nicht mehr. Du kannst Mutiny+ auch deaktivieren\",\n      \"wallet_connection\": \"Wallet Verbindung.\",\n      \"subscribe\": \"Einschreiben\",\n      \"error_no_plan\": \"Kein Abonnement gefunden\",\n      \"error_failure\": \"Konnte nicht abonniert werden\",\n      \"error_no_subscription\": \"Kein bestehendes Abonnement gefunden\",\n      \"error_expired_subscription\": \"Dein Abonnement ist abgelaufen, klicke auf Mitmachen, um es zu erneuern\",\n      \"satisfaction\": \"Selbstgefällige Zufriedenheit\",\n      \"gifting\": \"Schenken\",\n      \"multi_device\": \"Zugriff mit mehreren Geräten\",\n      \"ios_testflight\": \"Zugang zu iOS TestFlight\",\n      \"more\": \"... und es wird noch mehr kommen\",\n      \"cta_description\": \"Genieße frühzeitigen Zugriff auf neue Features und Premium-Funktionalitäten.\",\n      \"cta_but_already_plus\": \"Danke für deinen Support!\"\n    },\n    \"restore\": {\n      \"title\": \"Wiederherstellen\",\n      \"all_twelve\": \"Du musst alle 12 Wörter eingeben\",\n      \"wrong_word\": \"Falsches Wort\",\n      \"paste\": \"Gefährliches Eingeben von der Zwischenablage\",\n      \"confirm_text\": \"Bist du sicher, dass du diese Wallet wiederherstellen willst? Deine bestehende Wallet wird gelöscht!\",\n      \"restore_tip\": \"Du kannst eine bestehende Mutiny Wallet mit deinen 12 Wörtern wiederherstellen. Das wird deine bestehende Wallet ersetzen, sei dir also sicher was du tust!\",\n      \"multi_browser_warning\": \"Bitte nicht verschiedene Browser gleichzeitig nutzen.\",\n      \"error_clipboard\": \"Zwischenablage wird nicht unterstützt\",\n      \"error_word_number\": \"Falsche Anzahl Wörter\",\n      \"error_invalid_seed\": \"Ungültige Seed Phrase\"\n    },\n    \"servers\": {\n      \"title\": \"Server\",\n      \"caption\": \"Vertraue uns nicht! Nutze deine eigenen Server, um Mutiny zu verwenden.\",\n      \"link\": \"Lerne mehr über die Selbstverwahrung\",\n      \"proxy_label\": \"Websockets Proxy\",\n      \"proxy_caption\": \"Wie deine Lightning Node mit dem rest des Netzwerks kommuniziert.\",\n      \"error_proxy\": \"Sollte eine URL sein, die mit wss:// beginnt\",\n      \"esplora_label\": \"Esplora\",\n      \"esplora_caption\": \"Block-Daten für On-chain Informationen.\",\n      \"error_esplora\": \"Das sieht nicht nach einer gültigen URL aus\",\n      \"rgs_label\": \"RGS\",\n      \"rgs_caption\": \"Rapid Gossip Sync. Netzwerkdaten über das Lightning Netzwerk benutzt für das Routing.\",\n      \"error_rgs\": \"Das sieht nicht nach einer gültigen URL aus\",\n      \"lsp_label\": \"LSP\",\n      \"lsp_caption\": \"Lightning Service Provider. Automatisches Eröffnen von Kanälen, um deine Inbound Liquidität sicherzustellen. Verpackt auch Rechnungen, um den Datenschutz zu gewähren.\",\n      \"lsps_connection_string_label\": \"LSPS Connection String\",\n      \"lsps_connection_string_caption\": \"Lightning Service Provider. Automatisches Eröffnen von Kanälen, um deine Inbound Liquidität sicherzustellen. Genutzt werden LSP Spezifikationen.\",\n      \"error_lsps_connection_string\": \"Das sieht nicht nach einem Node Connection String aus\",\n      \"lsps_token_label\": \"LSPS Token\",\n      \"lsps_token_caption\": \"LSPS Token.  Wird benutzt, um zu identifizieren welche Wallet sich mit dem LSP verbindet\",\n      \"lsps_valid_error\": \"Du kannst entweder ein LSP-Set oder LSPS Connection String und LSPS Token-Set haben, nicht beides.\",\n      \"error_lsps_token\": \"Das sieht nicht nach einem validen Token aus\",\n      \"storage_label\": \"Lagerung\",\n      \"storage_caption\": \"Encrypted VSS Backup Service.\",\n      \"error_lsp\": \"Das sieht nicht nach einer gültigen URL aus\",\n      \"save\": \"Speichern\"\n    },\n    \"nostr_contacts\": {\n      \"title\": \"Sync Nostr Kontakte\",\n      \"npub_label\": \"Nostr npub\",\n      \"npub_required\": \"Npub darf nicht leer sein\",\n      \"sync\": \"Sync\",\n      \"resync\": \"Resync\",\n      \"remove\": \"Entfernen\"\n    },\n    \"manage_federations\": {\n      \"title\": \"Federationen verwalten\",\n      \"federation_code_label\": \"Federation Code\",\n      \"federation_code_required\": \"Federation Code darf nicht leer sein\",\n      \"federation_added_success\": \"Federation erfolgreich hinzugefügt\",\n      \"federation_remove_confirm\": \"Bist du sicher, dass du diese Federation enfernen willst? Stelle zuerst sicher, dass all deine Einlagen auf deine Lightning Wallet transferiert wurden.\",\n      \"add\": \"Hinzufügen\",\n      \"remove\": \"Entfernen\",\n      \"expires\": \"Läuft ab\",\n      \"federation_id\": \"Federation ID\",\n      \"description\": \"Mutiny unterstützt experimentell das Fedimint Protocoll. Du benötigst einen Einladungscode einer Federation, damit du diese Funktion nutzen kannst. Diese Einlagen sind zur Zeit aus der Ferne nicht gesichert. Das Speichern von Geld in einer Federation geschieht auf eigene Gefahr!\",\n      \"learn_more\": \"Lerne mehr über Fedimint.\"\n    },\n    \"gift\": {\n      \"give_sats_link\": \"Verschenke Sats als Geschenk\",\n      \"title\": \"Verschenken\",\n      \"no_plus_caption\": \"Upgrade auf Mutiny+ um Verschenken zu aktivieren\",\n      \"receive_too_small\": \"Deine erste Einzahlung muss mindestens {{amount}} SATS oder grösser sein.\",\n      \"setup_fee_lightning\": \"Eine Lightning Einrichtungsgebühr wird beim Erhalten des Geschenks verrechnet.\",\n      \"already_claimed\": \"Dieses Geschenk wurde bereits eingelöst\",\n      \"sender_is_poor\": \"Der Kontostand des Senders ist nicht gross genug, um das Geschenk zu bezahlen.\",\n      \"sender_timed_out\": \"Bezahlung des Geschenks ist abgelaufen. Der Sender ist vielleicht Offline, oder das Geschenk wurde bereits eingelöst.\",\n      \"sender_generic_error\": \"Sender hat folgenden Fehler gesendet: {{error}}\",\n      \"receive_header\": \"Dir hat jemand ein paar Sats geschenkt!\",\n      \"receive_description\": \"Du musst etwas Spezielles sein. Um das Geld einzulösen, musst du nur den grossen Knopf drücken. Das Geld wird deiner Wallet hinzugefügt, sobald der Schenkende das nächste Mal online ist.\",\n      \"receive_claimed\": \"Geschenk eingelöst! Du solltest das Geschenk demnächst in deinem Kontostand sehen.\",\n      \"receive_cta\": \"Geschenk einlösen\",\n      \"receive_try_again\": \"Nochmals versuchen\",\n      \"send_header\": \"Geschenk erstellen\",\n      \"send_explainer\": \"Übergebe das Geschenk gefüllt mit Sats. Erstelle eine Mutiny Geschenk-URL, die von jedem über einen Web-Browser eingelöst werden kann.\",\n      \"send_name_required\": \"Das ist für deine Historie\",\n      \"send_name_label\": \"Name des Empfängers\",\n      \"send_header_claimed\": \"Geschenk erhalten!\",\n      \"send_claimed\": \"Dein Geschenk wurde eingelöst. Danke fürs Teilen.\",\n      \"send_sharable_header\": \"URL zum Teilen\",\n      \"send_instructions\": \"Bitte diese Geschenk-URL deinem Empfänger senden, oder frage ihn, ob er diesen QR-Code mit seiner Wallet scannen kann.\",\n      \"send_another\": \"Einen Weiteren erstellen\",\n      \"send_small_warning\": \"Ein brandneuer Mutiny-Benutzer wird nicht fähig sein weniger als 100k Sats einzulösen.\",\n      \"send_cta\": \"Ein Geschenk erstellen\",\n      \"send_delete_button\": \"Geschenk löschen\",\n      \"send_delete_confirm\": \"Bist du sicher, dass du dieses Geschenk löschen willst? Ist das dein Rugpull-Moment?\",\n      \"send_tip\": \"Damit das Geschenk eingelöst werden kann, muss deine Mutiny Wallet geöffnet sein.\",\n      \"need_plus\": \"Upgrade auf Mutiny+, um Schenken zu aktivieren. Durch Schenken kannst du eine Mutiny Geschenk-URL erstellen, die von jedem mit einem Webbrowser eingelöst werden kann.\"\n    }\n  },\n  \"swap\": {\n    \"peer_not_found\": \"Peer nicht gefunden\",\n    \"channel_too_small\": \"Es ist nicht klug einen Kanal zu eröffnen, der kleiner als {{amount}} Sats ist\",\n    \"insufficient_funds\": \"Du hast nicht genügend Geld, um den Kanal zu eröffnen\",\n    \"header\": \"Tausche in Lightning\",\n    \"initiated\": \"Wechsel initialisiert\",\n    \"sats_added\": \"+{{amount}} Sats werden deiner Lightning Wallet hinzugefügt\",\n    \"use_existing\": \"Bitte den bestehenden Peer nutzen\",\n    \"choose_peer\": \"Einen Peer wählen\",\n    \"peer_connect_label\": \"Zu einem neuen Peer verbinden\",\n    \"peer_connect_placeholder\": \"Peer Verbindgungs-String\",\n    \"connect\": \"Verbinden\",\n    \"connecting\": \"wird verbunden...\",\n    \"confirm_swap\": \"Tausch bestätigen\"\n  },\n  \"swap_lightning\": {\n    \"insufficient_funds\": \"Du hast nicht genug Geld, um nach Lightning zu wechseln\",\n    \"header\": \"Tausche nach Lightning\",\n    \"header_preview\": \"Vorschau des Tausches\",\n    \"completed\": \"Tausch ausgeführt\",\n    \"too_small\": \"Ungültiger Betrag eingegeben. Zum Tauschen benötigst du mindestens 100k Sats.\",\n    \"sats_added\": \"+{{amount}} Sats wurden deiner Lightning Wallet hinzugefügt\",\n    \"sats_fee\": \"+{{amount}} Sats Gebühren\",\n    \"confirm_swap\": \"Tausch bestätigen\",\n    \"preview_swap\": \"Vorschau der Tauschgebühr\"\n  },\n  \"reload\": {\n    \"mutiny_update\": \"Mutiny Update\",\n    \"new_version_description\": \"Neue Version von Mutiny wurde zwischengespeichert, Lade die App neu, um sie zu verwenden.\",\n    \"reload\": \"Neu laden\"\n  },\n  \"error\": {\n    \"title\": \"Fehler\",\n    \"emergency_link\": \"Notfall-Kit.\",\n    \"reload\": \"Neu laden\",\n    \"restart\": {\n      \"title\": \"Es ist etwas *besonders* Verrücktes im Gange? Stoppe die Nodes!\",\n      \"start\": \"Start\",\n      \"stop\": \"Stop\"\n    },\n    \"general\": {\n      \"oh_no\": \"Oh nein!\",\n      \"never_should_happen\": \"Das hätte nicht passieren dürfen\",\n      \"try_reloading\": \"Versuche diese Seite neu zu laden oder klicke den \\\"Dangit\\\"-Knopf. Wenn du weiterhin Probleme hast,\",\n      \"support_link\": \"kontaktiere uns für Unterstützung.\",\n      \"getting_desperate\": \"Bist du verzweifelt? Versuche den\"\n    },\n    \"load_time\": {\n      \"stuck\": \"Steckst du auf diesem Bildschirm fest? Versuche die Seite neu zu laden. Wann das nicht hilft, versuche den\"\n    },\n    \"not_found\": {\n      \"title\": \"Nicht gefunden\",\n      \"wtf_paul\": \"Das ist vielleicht Paul's Fehler.\"\n    },\n    \"reset_router\": {\n      \"payments_failing\": \"Werden Zahlungen nicht durchgeführt? Versuche den Lightning-Router zu resetten.\",\n      \"reset_router\": \"Reset Router\"\n    },\n    \"resync\": {\n      \"incorrect_balance\": \"On-chain Kontostand scheint inkorrekt? Versuche die On-chain Wallet neu zu synchen.\",\n      \"resync_wallet\": \"Resync Wallet\"\n    },\n    \"on_boot\": {\n      \"existing_tab\": {\n        \"title\": \"Mehrere Tabs erkannt\",\n        \"description\": \"Mutiny kann nur in einem Tab genutzt werden. Es sieht so aus, als ob du weitere Tabs offen hast, in denen Mutiny läuft. Bitte schliesse den Tab und lade diese Seite neu, oder schliesse diesen Tab und lade den anderen neu.\"\n      },\n      \"already_running\": {\n        \"title\": \"Vielleicht läuft Mutiny auf einem anderen Gerät\",\n        \"description\": \"Mutiny kann nur mit einem Gerät genutzt werden. Es sieht so aus, als ob du ein anderes Gerät oder Browser für diese Wallet nutzst. Wenn du Mutiny kürzlich auf einem anderen Gerät geschlossen hast, warte bitte ein paar Minuten und versuche es nochmals.\",\n        \"retry_again_in\": \"Versuche es nochmals in\",\n        \"seconds\": \"Sekunden\"\n      },\n      \"incompatible_browser\": {\n        \"title\": \"Nicht kompatibler Browser\",\n        \"header\": \"Nicht kompatibler Browser erkannt\",\n        \"description\": \"Mutiny benötigt einen modernen Browser, der WebAssembly, LocalStorage und IndexedDB unterstützt. Einige Browser deaktivieren diese Features im privaten Modus.\",\n        \"try_different_browser\": \"Bitte stelle sicher, dass dein Browser alle diese Features unterstützt oder versuche es mit einem anderen Browser. Vielleicht möchtest du auch ausgewählte Erweiterungen oder \\\"Schields\\\" deaktivieren, die diese Features blockieren.\",\n        \"browser_storage\": \"(Wir würden gerne mehr Browser unterstützen, aber wir müssen deine Wallet-Daten im Browserspeicher sichern, sonst verlierst du dein Geld.)\",\n        \"browsers_link\": \"Unterstützte Browser\"\n      },\n      \"loading_failed\": {\n        \"title\": \"Konnte nicht geladen werden\",\n        \"header\": \"Mutiny konnte nicht geladen werden\",\n        \"description\": \"Etwas ist schief gelaufen, während Mutiny Wallet hochgefahren wurde.\",\n        \"repair_options\": \"Wenn deine Wallet kaputt zu sein scheint, findest du hier ein paar Werkzeuge. Damit kannst du versuchen deine Wallet zu debuggen und zu reparieren.\",\n        \"questions\": \"Wenn du Fragen zur Funktionsweise dieser Schaltflächen hast, wende dich bitte an uns\",\n        \"support_link\": \"kontaktiere uns für Unterstützung.\",\n        \"services_down\": \"Es sieht so aus, als ob einer der Dienste von Mutiny ausgefallen ist. Bitte versuche es später noch einmal.\",\n        \"in_the_meantime\": \"Wenn du in der Zwischenzeit auf deine On-Chain-Gelder zugreifen möchtest, kannst du Mutiny laden\",\n        \"safe_mode\": \"Sicherheitsmodus\"\n      }\n    }\n  },\n  \"modals\": {\n    \"share\": \"Teilen\",\n    \"details\": \"Details\",\n    \"loading\": {\n      \"loading\": \"Laden: {{stage}}\",\n      \"default\": \"Ich fange jetzt an\",\n      \"double_checking\": \"etwas noch einmal überprüfen\",\n      \"downloading\": \"Herunterladen\",\n      \"setup\": \"Einrichten\",\n      \"done\": \"Erledigt\"\n    },\n    \"onboarding\": {\n      \"welcome\": \"Willkommen!\",\n      \"restore_from_backup\": \"Wenn du Mutiny bereits verwendet hast, kannst du dein Backup wiederherstellen. Andernfalls kannst du dies überspringen und dich über dein neues Wallet freuen!\",\n      \"not_available\": \"Das machen wir noch nicht\",\n      \"secure_your_funds\": \"Sichere deine Einlagen\"\n    },\n    \"more_info\": {\n      \"whats_with_the_fees\": \"Was ist mit den Gebühren?\",\n      \"self_custodial\": \"Mutiny ist ein selbst-verwahrendes Wallet. Um eine Lightning-Zahlung zu initiieren, musst du einen Lightning-Kanal öffnen. Dazu ist ein Minimalbetrag und eine Einrichtungsgebühr erforderlich.\",\n      \"future_payments\": \"Für zukünftige Zahlungen, egal ob senden oder empfangen, fallen nur noch die normalen Netzwerkgebühren und eine geringe Servicegebühr an. Es sei denn, dein Kanal hat keine eingehende Liquidität mehr.\",\n      \"liquidity\": \"Lerne mehr über Liquidität\"\n    },\n    \"confirm_dialog\": {\n      \"are_you_sure\": \"Bist du sicher?\",\n      \"cancel\": \"Abbrechen\",\n      \"confirm\": \"Bestätigen\"\n    },\n    \"lnurl_auth\": {\n      \"auth_request\": \"Authentifizierungsanfrage\",\n      \"login\": \"Login\",\n      \"decline\": \"Rückgang\",\n      \"error\": \"Das hat irgendwie nicht funktioniert.\",\n      \"authenticated\": \"Authentifiziert!\"\n    }\n  }\n}"
  },
  {
    "path": "public/i18n/en.json",
    "content": "{\n  \"common\": {\n    \"title\": \"Mutiny Wallet\",\n    \"mutiny\": \"Mutiny\",\n    \"nice\": \"Nice\",\n    \"home\": \"Home\",\n    \"e_sats\": \"eSATS\",\n    \"e_sat\": \"eSAT\",\n    \"sats\": \"SATS\",\n    \"sat\": \"SAT\",\n    \"scan\": \"Scan\",\n    \"fee\": \"Fee\",\n    \"send\": \"Send\",\n    \"receive\": \"Receive\",\n    \"request\": \"Request\",\n    \"dangit\": \"Dangit\",\n    \"back\": \"Back\",\n    \"coming_soon\": \"(coming soon)\",\n    \"copy\": \"Copy\",\n    \"copied\": \"Copied\",\n    \"continue\": \"Continue\",\n    \"error_unimplemented\": \"Unimplemented\",\n    \"why\": \"Why?\",\n    \"private_tags\": \"Private tags\",\n    \"view_transaction\": \"View transaction\",\n    \"view_payment_details\": \"View payment details\",\n    \"pending\": \"Pending\",\n    \"error_safe_mode\": \"Mutiny is running in safe mode. Lightning is disabled.\",\n    \"self_hosted\": \"Self-hosted\",\n    \"expires\": \"Expires in {{time}}\"\n  },\n  \"home\": {\n    \"receive\": \"Receive your first sats\",\n    \"find\": \"Find your friends on nostr\",\n    \"backup\": \"Secure your funds!\",\n    \"connection\": \"Create a wallet connection\",\n    \"connection_edit\": \"Edit wallet connections\",\n    \"federation\": \"Join a federation\",\n    \"subnav\": {\n      \"just_me\": \"Just Me\",\n      \"friends\": \"Friends\",\n      \"requests\": \"Requests\"\n    },\n    \"shutdown\": {\n      \"title\": \"Mutiny Wallet is Shutting Down\",\n      \"message\": \"We are shutting down the hosted version of Mutiny Wallet. To learn about moving your funds, or self-hosting your wallet, please visit our blog.\"\n    }\n  },\n  \"profile\": {\n    \"profile\": \"Profile\",\n    \"nostr_identity\": \"Nostr Identity\",\n    \"add_lightning_address\": \"Add Lightning Address\",\n    \"edit_profile\": \"Edit Profile\",\n    \"join_federation\": \"Join a federation\",\n    \"manage_federation\": \"Manage Federations\",\n    \"federated_custody\": \"Federated Custody\",\n    \"self_custody\": \"Self Custody\",\n    \"social\": \"Social\",\n    \"edit\": {\n      \"nym\": \"Nym\"\n    },\n    \"deleted\": \"Your nostr profile has been deleted.\",\n    \"no_lightning_address\": \"No lightning address set\",\n    \"pay_me\": \"Send me sats\"\n  },\n  \"chat\": {\n    \"prompt\": \"This is a new conversation. Try asking for money!\",\n    \"placeholder\": \"Message\"\n  },\n  \"contacts\": {\n    \"new\": \"new\",\n    \"add_contact\": \"Add Contact\",\n    \"new_contact\": \"New Contact\",\n    \"create_contact\": \"Create contact\",\n    \"edit_contact\": \"Edit contact\",\n    \"save_contact\": \"Save contact\",\n    \"delete\": \"Delete\",\n    \"confirm_delete\": \"Are you sure you want to delete this contact?\",\n    \"payment_history\": \"Payment history\",\n    \"no_payments\": \"No payments yet with\",\n    \"edit\": \"Edit\",\n    \"pay\": \"Pay\",\n    \"name\": \"Name\",\n    \"ln_address\": \"Lightning Address\",\n    \"placeholder\": \"Satoshi\",\n    \"lightning_address\": \"Lightning Address\",\n    \"unimplemented\": \"Unimplemented\",\n    \"not_available\": \"We don't do that yet\",\n    \"error_name\": \"We at least need a name\",\n    \"email_error\": \"That doesn't look like a lightning address\",\n    \"npub_error\": \"That doesn't look like a nostr npub\",\n    \"error_ln_address_missing\": \"New contacts need a lightning address\",\n    \"npub\": \"Nostr Npub\"\n  },\n  \"redeem\": {\n    \"redeem_bitcoin\": \"Redeem Bitcoin\",\n    \"lnurl_amount_message\": \"Enter withdrawal amount between {{min}} and {{max}} sats\",\n    \"lnurl_redeem_failed\": \"Withdrawal Failed\",\n    \"lnurl_redeem_success\": \"Payment Received\"\n  },\n  \"request\": {\n    \"request_bitcoin\": \"Request Bitcoin\",\n    \"request\": \"Request\"\n  },\n  \"receive\": {\n    \"receive_bitcoin\": \"Receive Bitcoin\",\n    \"edit\": \"Edit\",\n    \"checking\": \"Checking\",\n    \"choose_format\": \"Choose format\",\n    \"payment_received\": \"Payment Received\",\n    \"payment_initiated\": \"Payment Initiated\",\n    \"receive_add_the_sender\": \"Add the sender for your records\",\n    \"keep_mutiny_open\": \"Keep Mutiny open to complete the payment.\",\n    \"choose_payment_format\": \"Choose payment format\",\n    \"lightning_label\": \"Lightning invoice\",\n    \"lightning_caption\": \"Ideal for small transactions. Usually lower fees than on-chain.\",\n    \"onchain_label\": \"Bitcoin address\",\n    \"onchain_caption\": \"On-chain, just like Satoshi did it. Ideal for very large transactions.\",\n    \"lightning_setup_fee\": \"A lightning setup fee of {{amount}} SATS will be charged for this receive.\",\n    \"amount\": \"Amount\",\n    \"fee\": \"+ Fee\",\n    \"total\": \"Total\",\n    \"spendable\": \"Spendable\",\n    \"channel_size\": \"Channel size\",\n    \"channel_reserve\": \"- Channel reserve\",\n    \"error_under_min_lightning\": \"Defaulting to On-chain. Amount is too small for your initial Lightning receive.\",\n    \"error_creating_unified\": \"Defaulting to On-chain. Something went wrong when creating the Lightning invoice.\",\n    \"error_creating_address\": \"Something went wrong when creating the on-chain address.\",\n    \"amount_editable\": {\n      \"receive_too_small\": \"Your first receive needs to be {{amount}} SATS or greater.\",\n      \"setup_fee_lightning\": \"A lightning setup fee will be charged.\",\n      \"more_than_21m\": \"There are only 21 million bitcoin.\",\n      \"set_amount\": \"Set amount\",\n      \"max\": \"MAX\",\n      \"fix_amounts\": {\n        \"ten_k\": \"10k\",\n        \"one_hundred_k\": \"100k\",\n        \"one_million\": \"1m\"\n      },\n      \"del\": \"DEL\",\n      \"balance\": \"Balance\"\n    },\n    \"integrated_qr\": {\n      \"onchain\": \"On-chain\",\n      \"lightning\": \"Lightning\",\n      \"gift\": \"Lightning Gift\"\n    },\n    \"remember_choice\": \"Remember my choice next time\",\n    \"what_for\": \"What's this for?\",\n    \"method_help\": {\n      \"title\": \"Receive Method\",\n      \"body\": \"Lightning receives will automatically go into your chosen federation. You can swap to self-custodial later if you want.\"\n    },\n    \"receive_strings_error\": \"Something went wrong generating an invoice or on-chain address.\",\n    \"error_under_min_onchain\": \"That's under the dust limit! On-chain transactions should be much bigger.\",\n    \"warning_on_chain_fedi\": \"On-chain fedimint deposits require 10 confirmations. RBF not supported yet.\",\n    \"warning_address_reuse\": \"Only send to this address once!\"\n  },\n  \"send\": {\n    \"search\": {\n      \"placeholder\": \"Name, address, invoice\",\n      \"paste\": \"Paste\",\n      \"contacts\": \"Contacts\",\n      \"global_search\": \"Global search\",\n      \"no_results\": \"No results found for\"\n    },\n    \"sending\": \"Sending...\",\n    \"confirm_send\": \"Confirm Send\",\n    \"contact_placeholder\": \"Add the receiver for your records\",\n    \"start_over\": \"Start Over\",\n    \"send_bitcoin\": \"Send Bitcoin\",\n    \"paste\": \"Paste\",\n    \"scan_qr\": \"Scan QR\",\n    \"payment_initiated\": \"Payment Initiated\",\n    \"payment_sent\": \"Payment Sent\",\n    \"destination\": \"Destination\",\n    \"no_payment_info\": \"No payment info\",\n    \"progress_bar\": {\n      \"of\": \"of\",\n      \"sats_sent\": \"sats sent\"\n    },\n    \"what_for\": \"What's this for?\",\n    \"zap_note\": \"Zap note\",\n    \"error_low_balance\": \"We do not have enough balance to pay the given amount.\",\n    \"error_invoice_match\": \"Amount requested, {{amount}} SATS, does not equal amount set.\",\n    \"error_channel_reserves\": \"Not enough available funds.\",\n    \"error_address\": \"Invalid Lightning Address\",\n    \"error_channel_reserves_explained\": \"A portion of your channel balance is reserved for fees. Try sending a smaller amount or adding funds.\",\n    \"error_clipboard\": \"Clipboard not supported\",\n    \"error_keysend\": \"Keysend failed\",\n    \"error_LNURL\": \"LNURL Pay failed\",\n    \"error_expired\": \"Invoice is expired\",\n    \"payjoin_send\": \"This is a payjoin! The Mutiny will continue until privacy improves\",\n    \"payment_pending\": \"Payment pending\",\n    \"payment_pending_description\": \"It's taking a while, but it's possible this payment may still go through. Please check 'Activity' for the current status.\",\n    \"hodl_invoice_warning\": \"This is a hodl invoice. Payments to hodl invoices can cause channel force closes, which results in high on-chain fees. Pay at your own risk!\",\n    \"private\": \"Private\",\n    \"publiczap\": \"Public Zap\",\n    \"privatezap\": \"Private Zap\"\n  },\n  \"feedback\": {\n    \"header\": \"Give us feedback!\",\n    \"received\": \"Feedback received!\",\n    \"thanks\": \"Thank you for letting us know what's going on.\",\n    \"more\": \"Got more to say?\",\n    \"tracking\": \"Mutiny doesn't track or spy on your behavior, so your feedback is incredibly helpful.\",\n    \"github\": \"We will not respond to this feedback. If you'd like support please\",\n    \"create_issue\": \"create a GitHub issue.\",\n    \"link\": \"Feedback?\",\n    \"feedback_placeholder\": \"Bugs, feature requests, feedback, etc.\",\n    \"info_label\": \"Include contact info\",\n    \"info_caption\": \"If you need us to follow-up on this issue\",\n    \"email\": \"Email\",\n    \"email_caption\": \"Burners welcome\",\n    \"nostr\": \"Nostr\",\n    \"nostr_caption\": \"Your freshest npub\",\n    \"nostr_label\": \"Nostr npub or NIP-05\",\n    \"send_feedback\": \"Send Feedback\",\n    \"invalid_feedback\": \"Please say something!\",\n    \"need_contact\": \"We need some way to contact you\",\n    \"invalid_email\": \"That doesn't look like an email address to me\",\n    \"error\": \"Error submitting feedback {{error}}\",\n    \"try_again\": \"Please try again later.\"\n  },\n  \"activity\": {\n    \"title\": \"Activity\",\n    \"mutiny\": \"Mutiny\",\n    \"wallet\": \"Wallet\",\n    \"nostr\": \"Nostr\",\n    \"view_all\": \"View all\",\n    \"receive_some_sats_to_get_started\": \"Receive some sats to get started\",\n    \"channel_open\": \"Channel Open\",\n    \"channel_close\": \"Channel Close\",\n    \"unknown\": \"Unknown\",\n    \"import_contacts\": \"Import your contacts from nostr to see who they're zapping.\",\n    \"coming_soon\": \"Coming soon\",\n    \"private\": \"Private\",\n    \"anonymous\": \"Anonymous\",\n    \"from\": \"From:\",\n    \"transaction_details\": {\n      \"lightning_receive\": \"Received via Lightning\",\n      \"lightning_send\": \"Sent via Lightning\",\n      \"channel_open\": \"Channel open\",\n      \"channel_close\": \"Channel close\",\n      \"onchain_receive\": \"On-chain receive\",\n      \"onchain_send\": \"On-chain send\",\n      \"paid\": \"Paid\",\n      \"unpaid\": \"Unpaid\",\n      \"status\": \"Status\",\n      \"date\": \"Date\",\n      \"tagged_to\": \"Tagged to\",\n      \"description\": \"Description\",\n      \"fee\": \"Fee\",\n      \"onchain_fee\": \"On-chain Fee\",\n      \"invoice\": \"Invoice\",\n      \"payment_hash\": \"Payment Hash\",\n      \"payment_preimage\": \"Preimage\",\n      \"txid\": \"Txid\",\n      \"total\": \"Amount Requested\",\n      \"balance\": \"Balance\",\n      \"reserve\": \"Reserve\",\n      \"peer\": \"Peer\",\n      \"channel_id\": \"Channel ID\",\n      \"reason\": \"Reason\",\n      \"confirmed\": \"Confirmed\",\n      \"unconfirmed\": \"Unconfirmed\",\n      \"sweep_delay\": \"Funds may take a few days to be swept back into the wallet\",\n      \"no_details\": \"No channel details found, which means this channel has likely been closed.\",\n      \"back_home\": \"back home\"\n    },\n    \"start_a_chat\": \"Start a chat?\",\n    \"start_a_chat_are_you_sure\": \"This user isn't in your contact list.\",\n    \"federation_message\": \"Federation Message\"\n  },\n  \"scanner\": {\n    \"paste\": \"Paste Something\",\n    \"cancel\": \"Cancel\"\n  },\n  \"settings\": {\n    \"header\": \"Settings\",\n    \"support\": \"Learn how to support Mutiny\",\n    \"experimental_features\": \"Experiments\",\n    \"debug_tools\": \"DEBUG TOOLS\",\n    \"danger_zone\": \"Danger zone\",\n    \"general\": \"General\",\n    \"version\": \"Version:\",\n    \"admin\": {\n      \"title\": \"Admin Page\",\n      \"caption\": \"Our internal debug tools. Use wisely!\",\n      \"header\": \"Secret Debug Tools\",\n      \"warning_one\": \"If you know what you're doing you're in the right place.\",\n      \"warning_two\": \"These are internal tools we use to debug and test the app. Please be careful!\",\n      \"kitchen_sink\": {\n        \"disconnect\": \"Disconnect\",\n        \"peers\": \"Peers\",\n        \"no_peers\": \"No peers\",\n        \"refresh_peers\": \"Refresh Peers\",\n        \"connect_peer\": \"Connect Peer\",\n        \"expect_a_value\": \"Expecting a value...\",\n        \"connect\": \"Connect\",\n        \"close_channel\": \"Close Channel\",\n        \"force_close\": \"Force close Channel\",\n        \"abandon_channel\": \"Abandon Channel\",\n        \"confirm_close_channel\": \"Are you sure you want to close this channel?\",\n        \"confirm_force_close\": \"Are you sure you want to force close this channel? Your funds will take a few days to redeem on chain.\",\n        \"confirm_abandon_channel\": \"Are you sure you want to abandon this channel? Typically only do this if the opening transaction will never confirm. Otherwise, you will lose funds.\",\n        \"channels\": \"Channels\",\n        \"no_channels\": \"No Channels\",\n        \"refresh_channels\": \"Refresh Channels\",\n        \"pubkey\": \"Pubkey\",\n        \"amount\": \"Amount\",\n        \"open_channel\": \"Open Channel\",\n        \"nodes\": \"Nodes\",\n        \"no_nodes\": \"No nodes\",\n        \"enable_zaps_to_hodl\": \"Enable zaps to hodl invoices?\",\n        \"zaps_to_hodl_desc\": \"Zaps to hodl invoices can result in channel force closes, which results in high on-chain fees. Use at your own risk!\",\n        \"zaps_to_hodl_enable\": \"Enable hodl zaps\",\n        \"zaps_to_hodl_disable\": \"Disable hodl zaps\"\n      }\n    },\n    \"backup\": {\n      \"title\": \"Backup\",\n      \"secure_funds\": \"Let's get these funds secured.\",\n      \"twelve_words_tip\": \"We'll show you 12 words. You write down the 12 words.\",\n      \"warning_one\": \"If you clear your browser history, or lose your device, these 12 words are the only way you can restore your wallet.\",\n      \"warning_two\": \"Mutiny is self-custodial. It's all up to you...\",\n      \"confirm\": \"I wrote down the words\",\n      \"responsibility\": \"I understand that my funds are my responsibility\",\n      \"liar\": \"I'm not lying just to get this over with\",\n      \"seed_words\": {\n        \"reveal\": \"TAP TO REVEAL SEED WORDS\",\n        \"hide\": \"HIDE\",\n        \"copy\": \"Dangerously Copy to Clipboard\",\n        \"copied\": \"Copied!\"\n      }\n    },\n    \"channels\": {\n      \"title\": \"Lightning Channels\",\n      \"outbound\": \"Outbound\",\n      \"inbound\": \"Inbound\",\n      \"reserve\": \"Reserve\",\n      \"have_channels\": \"You have\",\n      \"have_channels_one\": \"lightning channel.\",\n      \"have_channels_many\": \"lightning channels.\",\n      \"inbound_outbound_tip\": \"Outbound is the amount of money you can spend on lightning. Inbound is the amount you can receive without incurring a lightning service fee.\",\n      \"reserve_tip\": \"About 1% of your channel balance is reserved on lightning for fees. Additional reserves are required for channels you opened via swap.\",\n      \"no_channels\": \"It looks like you don't have any channels yet. To get started, receive some sats over lightning, or swap some on-chain funds into a channel. Get your hands dirty!\",\n      \"close_channel\": \"Close\",\n      \"online_channels\": \"Online Channels\",\n      \"offline_channels\": \"Offline Channels\",\n      \"close_channel_confirm\": \"Closing this channel will move the balance on-chain and incur an on-chain fee.\",\n      \"force_close_channel_confirm\": \"This channel is offline. Force closing this channel will move the balance on-chain, incur an on-chain fee, and may take a few days to settle.\"\n    },\n    \"connections\": {\n      \"title\": \"Wallet Connections\",\n      \"error_name\": \"Name cannot be empty\",\n      \"error_connection\": \"Failed to create Wallet Connection\",\n      \"error_budget_zero\": \"Budget must be greater than zero\",\n      \"add_connection\": \"Add Connection\",\n      \"manage_connections\": \"Manage Connections\",\n      \"manage_gifts\": \"Manage Gifts\",\n      \"delete_connection\": \"Delete\",\n      \"new_connection\": \"New Connection\",\n      \"edit_connection\": \"Edit Connection\",\n      \"new_connection_label\": \"Name\",\n      \"new_connection_placeholder\": \"My favorite nostr client...\",\n      \"create_connection\": \"Create Connection\",\n      \"save_connection\": \"Save Changes\",\n      \"edit_budget\": \"Edit Budget\",\n      \"open_app\": \"Open App\",\n      \"open_in_nostr_client\": \"Open in Nostr Client\",\n      \"open_in_primal\": \"Open in Primal\",\n      \"nostr_client_not_found\": \"Nostr client not found\",\n      \"client_not_found_description\": \"Install a nostr client like Primal, Amethyst, or Damus to open this link.\",\n      \"relay\": \"Relay\",\n      \"authorize\": \"Authorize external services to request payments from your wallet. Pairs great with Nostr clients.\",\n      \"pending_nwc\": {\n        \"title\": \"Pending Requests\",\n        \"approve_all\": \"Approve All\",\n        \"deny_all\": \"Deny All\"\n      },\n      \"careful\": \"Be careful where you share this connection! Requests within budget will paid automatically.\",\n      \"spent\": \"Spent\",\n      \"remaining\": \"Remaining\",\n      \"confirm_delete\": \"Are you sure you want to delete this connection?\",\n      \"budget\": \"Budget\",\n      \"resets_every\": \"Resets every\",\n      \"resubscribe_date\": \"Resubscribe on\",\n      \"disable_connection\": \"Disable\",\n      \"disabled\": \"This subscription is disabled. Currently you can't re-enable subscriptions, but we're working to fix that. Check back soon!\",\n      \"disable_connection_confirm\": \"Are you sure you want to disable your subscription? Currently subscriptions can't be re-enabled, we're working to fix that.\"\n    },\n    \"emergency_kit\": {\n      \"title\": \"Emergency Kit\",\n      \"caption\": \"Diagnose and solve problems with your wallet.\",\n      \"emergency_tip\": \"If your wallet seems broken, here are some tools to try to debug and repair it.\",\n      \"questions\": \"If you have any questions on what these buttons do, please\",\n      \"link\": \"reach out to us for support.\",\n      \"import_export\": {\n        \"title\": \"Export wallet state\",\n        \"error_password\": \"Password is required\",\n        \"error_read_file\": \"File read error\",\n        \"error_no_text\": \"No text found in file\",\n        \"tip\": \"You can export your entire Mutiny Wallet state to a file and import it into a new browser. It usually works!\",\n        \"caveat_header\": \"Important caveats:\",\n        \"caveat\": \"after exporting don't do any operations in the original browser. If you do, you'll need to export again. After a successful import, a best practice is to clear the state of the original browser just to make sure you don't create conflicts.\",\n        \"save_state\": \"Save State As File\",\n        \"import_state\": \"Import State From File\",\n        \"confirm_replace\": \"Do you want to replace your state with\",\n        \"password\": \"Enter your password to decrypt\",\n        \"decrypt_wallet\": \"Decrypt Wallet\",\n        \"decrypt_export\": \"Enter your password to save\"\n      },\n      \"logs\": {\n        \"title\": \"Download debug logs\",\n        \"something_screwy\": \"Something screwy going on? Check out the logs!\",\n        \"download_logs\": \"Download Logs\",\n        \"password\": \"Enter your password to decrypt\",\n        \"confirm_password_label\": \"Confirm Password\"\n      },\n      \"delete_everything\": {\n        \"delete\": \"Delete Everything\",\n        \"confirm\": \"This will delete your node's state. This can't be undone!\",\n        \"deleted\": \"Deleted\",\n        \"deleted_description\": \"Deleted all data\"\n      }\n    },\n    \"encrypt\": {\n      \"title\": \"Security\",\n      \"caption\": \"Backup first to unlock encryption\",\n      \"header\": \"Encrypt your seed words\",\n      \"hot_wallet_warning\": \"Mutiny is a \\\"hot wallet\\\" so it needs your seed word to operate, but you can optionally encrypt those words with a password.\",\n      \"password_tip\": \"That way, if someone gets access to your browser, they still won't have access to your funds.\",\n      \"optional\": \"(optional)\",\n      \"existing_password\": \"Existing password\",\n      \"existing_password_caption\": \"Leave blank if you haven't set a password yet.\",\n      \"new_password_label\": \"Password\",\n      \"new_password_placeholder\": \"Enter a password\",\n      \"new_password_caption\": \"This password will be used to encrypt your seed words. If you forget it, you will need to re-enter your seed words to access your funds. You did write down your seed words, right?\",\n      \"confirm_password_label\": \"Confirm Password\",\n      \"confirm_password_placeholder\": \"Enter the same password\",\n      \"encrypt\": \"Encrypt\",\n      \"skip\": \"Skip\",\n      \"error_match\": \"Passwords do not match\",\n      \"error_same_as_existingpassword\": \"New password must not match existing password\"\n    },\n    \"decrypt\": {\n      \"title\": \"Enter your password\",\n      \"decrypt_wallet\": \"Decrypt Wallet\",\n      \"forgot_password_link\": \"Forgot Password?\",\n      \"error_wrong_password\": \"Invalid Password\"\n    },\n    \"currency\": {\n      \"title\": \"Currency\",\n      \"caption\": \"Choose your preferred currency pair\",\n      \"select_currency\": \"Select Currency\",\n      \"select_currency_label\": \"Currency Pair\",\n      \"select_currency_caption\": \"Choosing a new currency will resync the wallet to fetch a price update\",\n      \"request_currency_support_link\": \"Request support for more currencies\",\n      \"error_unsupported_currency\": \"Please Select a supported currency.\"\n    },\n    \"language\": {\n      \"title\": \"Language\",\n      \"caption\": \"Choose your preferred language\",\n      \"select_language\": \"Select Language\",\n      \"select_language_label\": \"Language\",\n      \"select_language_caption\": \"Choosing a new currency will change the wallet language, ignoring current browser language\",\n      \"request_language_support_link\": \"Request support for more languages\",\n      \"error_unsupported_language\": \"Please Select a supported language.\"\n    },\n    \"lnurl_auth\": {\n      \"title\": \"LNURL Auth\",\n      \"auth\": \"Auth\",\n      \"expected\": \"Expecting something like LNURL...\"\n    },\n    \"plus\": {\n      \"title\": \"Mutiny+\",\n      \"join\": \"Join\",\n      \"sats_per_month\": \"for {{amount}} sats a month.\",\n      \"lightning_balance\": \"You'll need at least {{amount}} sats in your lightning balance to get started. Try before you buy!\",\n      \"restore\": \"Restore Subscription\",\n      \"ready_to_join\": \"Ready to join\",\n      \"click_confirm\": \"Click confirm to pay for your first month.\",\n      \"open_source\": \"Mutiny is open source and self-hostable.\",\n      \"optional_pay\": \"But also you can pay for it.\",\n      \"paying_for\": \"Paying for\",\n      \"supports_dev\": \"helps support ongoing development and unlocks early access to new features and premium functionality:\",\n      \"thanks\": \"You're part of the mutiny! Enjoy the following perks:\",\n      \"renewal_time\": \"You'll get a renewal payment request around\",\n      \"cancel\": \"To cancel your subscription just don't pay. You can also disable the Mutiny+\",\n      \"wallet_connection\": \"Wallet Connection.\",\n      \"subscribe\": \"Subscribe\",\n      \"error_no_plan\": \"No plans found\",\n      \"error_failure\": \"Couldn't subscribe\",\n      \"error_no_subscription\": \"No existing subscription found\",\n      \"error_expired_subscription\": \"Your subscription has expired, click join to renew\",\n      \"satisfaction\": \"Smug satisfaction\",\n      \"gifting\": \"Gifting\",\n      \"multi_device\": \"Multi-device access\",\n      \"ios_testflight\": \"iOS TestFlight access\",\n      \"more\": \"... and more to come\",\n      \"cta_description\": \"Enjoy early access to new features and premium functionality.\",\n      \"cta_but_already_plus\": \"Thank you for your support!\",\n      \"lightning_address\": \"Your own lightning address\"\n    },\n    \"restore\": {\n      \"title\": \"Restore\",\n      \"all_twelve\": \"You need to enter all 12 words\",\n      \"wrong_word\": \"Wrong word\",\n      \"paste\": \"Dangerously Paste from Clipboard\",\n      \"confirm_text\": \"Are you sure you want to restore to this wallet? Your existing wallet will be deleted!\",\n      \"restore_tip\": \"You can restore an existing Mutiny Wallet from your 12 word seed phrase. This will replace your existing wallet, so make sure you know what you're doing!\",\n      \"multi_browser_warning\": \"Do not use on multiple browsers at the same time.\",\n      \"error_clipboard\": \"Clipboard not supported\",\n      \"error_word_number\": \"Wrong number of words\",\n      \"error_invalid_seed\": \"Invalid seed phrase\"\n    },\n    \"servers\": {\n      \"title\": \"Servers\",\n      \"caption\": \"Don't trust us! Use your own servers to back Mutiny.\",\n      \"link\": \"Learn more about self-hosting\",\n      \"proxy_label\": \"Websockets Proxy\",\n      \"proxy_caption\": \"How your lightning node communicates with the rest of the network.\",\n      \"error_proxy\": \"Should be a url starting with wss://\",\n      \"esplora_label\": \"Esplora\",\n      \"esplora_caption\": \"Block data for on-chain information.\",\n      \"error_esplora\": \"That doesn't look like a URL\",\n      \"rgs_label\": \"RGS\",\n      \"rgs_caption\": \"Rapid Gossip Sync. Network data about the lightning network used for routing.\",\n      \"error_rgs\": \"That doesn't look like a URL\",\n      \"lsp_label\": \"LSP\",\n      \"lsp_caption\": \"Lightning Service Provider. Automatically opens channels to you for inbound liquidity. Also wraps invoices for privacy.\",\n      \"lsps_connection_string_label\": \"LSPS Connection String\",\n      \"lsps_connection_string_caption\": \"Lightning Service Provider. Automatically opens channels to you for inbound liquidity. Using LSP specification.\",\n      \"error_lsps_connection_string\": \"That doesn't look like node connection string\",\n      \"lsps_token_label\": \"LSPS Token\",\n      \"lsps_token_caption\": \"LSPS Token.  Used to identify what wallet is connecting to the LSP\",\n      \"lsps_valid_error\": \"You can either have just an LSP set or LSPS Connection String and LSPS Token set, not both.\",\n      \"error_lsps_token\": \"That doesn't look like a valid token\",\n      \"storage_label\": \"Storage\",\n      \"storage_caption\": \"Encrypted VSS backup service.\",\n      \"error_lsp\": \"That doesn't look like a URL\",\n      \"error_tor\": \"Tor URLs are not currently supported\",\n      \"save\": \"Save\",\n      \"error_lsp_change_failed\": \"Unable to change LSP. Maybe you have open channels?\"\n    },\n    \"nostr_contacts\": {\n      \"title\": \"Sync Nostr Contacts\",\n      \"npub_label\": \"Nostr npub\",\n      \"npub_required\": \"Npub can't be blank\",\n      \"sync\": \"Sync\",\n      \"resync\": \"Resync\",\n      \"remove\": \"Remove\"\n    },\n    \"manage_federations\": {\n      \"title\": \"Manage Federations\",\n      \"federation_code_label\": \"Federation code\",\n      \"federation_code_required\": \"Federation code can't be blank\",\n      \"federation_added_success\": \"Federation added successfully\",\n      \"federation_remove_confirm\": \"Are you sure you want to remove this federation? Make sure any funds you have are transferred to your lightning balance or another wallet first.\",\n      \"add\": \"Add\",\n      \"remove\": \"Leave federation\",\n      \"expires\": \"Expires\",\n      \"federation_id\": \"Federation ID\",\n      \"description\": \"Federations are bitcoin-based networks that make it cheaper, quicker, and easier to use bitcoin.\",\n      \"learn_more\": \"Learn more\",\n      \"discover\": \"Discover Federations\",\n      \"manual\": \"Invite Code\",\n      \"created_at\": \"Created At\",\n      \"recommended_by\": \"Recommended By\",\n      \"already_in_fed\": \"You're already in a federation!\",\n      \"descriptionpart2\": \"Each one is run by a group of different individuals or companies. Discover one that you or your friends might trust below.\",\n      \"join_me\": \"Join me\",\n      \"recommend\": \"Recommend federation\",\n      \"recommended_by_you\": \"Recommended by you\",\n      \"transfer_funds\": \"Transfer funds\",\n      \"transfer_funds_message\": \"Add a second federation to enable transfers.\"\n    },\n    \"gift\": {\n      \"give_sats_link\": \"Give sats as a gift\",\n      \"something_went_wrong\": \"Something went wrong\",\n      \"title\": \"Gifting\",\n      \"no_plus_caption\": \"Upgrade to Mutiny+ to enable gifting\",\n      \"receive_too_small\": \"Your first receive needs to be {{amount}} SATS or greater.\",\n      \"setup_fee_lightning\": \"A lightning setup fee will be charged to receive this gift.\",\n      \"already_claimed\": \"This gift has already been claimed\",\n      \"sender_is_poor\": \"The sender doesn't have enough balance to pay this gift.\",\n      \"sender_timed_out\": \"Gift payment timed out. The sender may be offline, or this gift has already been claimed.\",\n      \"sender_generic_error\": \"Sender sent error: {{error}}\",\n      \"receive_header\": \"You've been gifted some sats!\",\n      \"receive_description\": \"You must be pretty special. To claim your money just hit the big button. Funds will be added to this wallet the next time your gifter is online.\",\n      \"receive_claimed\": \"Gift claimed! You should see the gift hit your balance shortly.\",\n      \"receive_cta\": \"Claim Gift\",\n      \"receive_try_again\": \"Try Again\",\n      \"send_header\": \"Create Gift\",\n      \"send_explainer\": \"Give the gift of sats. Create a Mutiny gift URL that can be claimed by anyone with a web browser.\",\n      \"send_name_required\": \"This is for your records\",\n      \"send_name_label\": \"Recipient Name\",\n      \"send_header_claimed\": \"Gift Received!\",\n      \"send_claimed\": \"Your gift has been claimed. Thanks for sharing.\",\n      \"send_sharable_header\": \"Sharable URL\",\n      \"send_instructions\": \"Copy this gift URL to your recipient, or ask them to scan this QR code with their wallet.\",\n      \"send_another\": \"Create Another\",\n      \"send_small_warning\": \"A brand new Mutiny user won't be able to redeem fewer than 100k sats.\",\n      \"send_cta\": \"Create a gift\",\n      \"send_delete_button\": \"Delete Gift\",\n      \"send_delete_confirm\": \"Are you sure you want to delete this gift? Is this your rugpull moment?\",\n      \"send_tip\": \"Your copy of Mutiny Wallet needs to be open for the gift to be redeemed.\",\n      \"need_plus\": \"Upgrade to Mutiny+ to enable gifting. Gifting allows you to create a Mutiny gift URL that can be claimed by anyone with a web browser.\"\n    },\n    \"appearance\": \"Appearance\",\n    \"social\": \"Social\",\n    \"nostr_keys\": {\n      \"title\": \"Nostr Keys\",\n      \"caption\": \"Not your keys not your notes\",\n      \"description\": \"You can use the same nostr profile across multiple apps.\",\n      \"warning\": \"Be careful where you share your private nostr key!\",\n      \"learn_more\": \"Learn more about nostr\",\n      \"import_profile\": \"Import a nostr profile\",\n      \"delete_account\": \"Delete account\",\n      \"delete_account_confirm\": \"Deleting a nostr profile cannot be undone and will disable some social features of the wallet.\",\n      \"unlink_account\": \"Unlink nostr profile\",\n      \"unlink_account_confirm\": \"Unlinking your nostr profile will reset your wallet to its default nostr profile.\",\n      \"delete_account_confirm_scary\": \"THIS PROFILE WILL BE DELETED ACROSS ALL NOSTR APPS.\"\n    },\n    \"lightning_address\": {\n      \"title\": \"Mutiny Address\",\n      \"description\": \"Get zaps and payments to your wallet with your own lightning address.\",\n      \"create\": \"Create Mutiny Address\",\n      \"add_a_federation\": \"To enable this feature you need to be a member of a federation.\",\n      \"ios_warning\": \"Mutiny Address requires Mutiny+ which cannot be purchased with this app.\"\n    }\n  },\n  \"swap\": {\n    \"peer_not_found\": \"Peer not found\",\n    \"channel_too_small\": \"It's just silly to make a channel smaller than {{amount}} sats\",\n    \"insufficient_funds\": \"You don't have enough funds to make this channel\",\n    \"header\": \"Swap to Lightning\",\n    \"initiated\": \"Swap Initiated\",\n    \"sats_added\": \"+{{amount}} sats will be added to your Lightning balance\",\n    \"use_existing\": \"Use existing peer\",\n    \"choose_peer\": \"Choose a peer\",\n    \"peer_connect_label\": \"Connect to new peer\",\n    \"peer_connect_placeholder\": \"Peer connect string\",\n    \"connect\": \"Connect\",\n    \"connecting\": \"Connecting...\",\n    \"confirm_swap\": \"Confirm Swap\"\n  },\n  \"swap_lightning\": {\n    \"insufficient_funds\": \"You don't have enough funds to swap to lightning\",\n    \"header\": \"Swap to Lightning\",\n    \"header_preview\": \"Preview Swap\",\n    \"completed\": \"Swap Completed\",\n    \"too_small\": \"Invalid amount entered. You need to swap at least 100k sats.\",\n    \"sats_added\": \"+{{amount}} sats have been added to your Lightning balance\",\n    \"sats_fee\": \"+{{amount}} sats fee\",\n    \"confirm_swap\": \"Confirm Swap\",\n    \"preview_swap\": \"Preview Swap Fee\"\n  },\n  \"reload\": {\n    \"mutiny_update\": \"Mutiny Update\",\n    \"new_version_description\": \"New version of Mutiny has been cached, reload to start using it.\",\n    \"reload\": \"Reload\"\n  },\n  \"error\": {\n    \"title\": \"Error\",\n    \"emergency_link\": \"emergency kit.\",\n    \"reload\": \"Reload\",\n    \"restart\": {\n      \"title\": \"Something *extra* screwy going on? Stop the nodes!\",\n      \"start\": \"Start\",\n      \"stop\": \"Stop\"\n    },\n    \"general\": {\n      \"oh_no\": \"Oh no!\",\n      \"never_should_happen\": \"This never should've happened\",\n      \"try_reloading\": \"Try reloading this page or clicking the \\\"Dangit\\\" button. If you keep having problems,\",\n      \"support_link\": \"reach out to us for support.\",\n      \"getting_desperate\": \"Getting desperate? Try the\"\n    },\n    \"load_time\": {\n      \"stuck\": \"Stuck on this screen? Try reloading. If that doesn't work, check out the\"\n    },\n    \"not_found\": {\n      \"title\": \"Not Found\",\n      \"wtf_paul\": \"This is probably Paul's fault.\"\n    },\n    \"resync\": {\n      \"incorrect_balance\": \"On-chain balance seems incorrect? Try re-syncing the on-chain wallet.\",\n      \"resync_wallet\": \"Resync wallet\"\n    },\n    \"on_boot\": {\n      \"existing_tab\": {\n        \"title\": \"Multiple tabs detected\",\n        \"description\": \"Mutiny can only be used in one tab at a time. It looks like you have another tab open with Mutiny running. Please close that tab and refresh this page, or close this tab and refresh the other one.\"\n      },\n      \"already_running\": {\n        \"title\": \"Mutiny may be running on another device\",\n        \"description\": \"Mutiny can only be used in one place at a time. It looks like you have another device or browser using this wallet. If you've recently closed Mutiny on another device, please wait a few minutes and try again.\",\n        \"retry_again_in\": \"Retry again in\",\n        \"seconds\": \"seconds\"\n      },\n      \"incompatible_browser\": {\n        \"title\": \"Incompatible browser\",\n        \"header\": \"Incompatible browser detected\",\n        \"description\": \"Mutiny requires a modern browser that supports WebAssembly, LocalStorage, and IndexedDB. Some browsers disable these features in private mode.\",\n        \"try_different_browser\": \"Please make sure your browser supports all these features, or consider trying another browser. You might also try disabling certain extensions or \\\"shields\\\" that block these features.\",\n        \"browser_storage\": \"(We'd love to support more private browsers, but we have to save your wallet data to browser storage or else you will lose funds.)\",\n        \"browsers_link\": \"Supported Browsers\"\n      },\n      \"loading_failed\": {\n        \"title\": \"Failed to load\",\n        \"header\": \"Failed to load Mutiny\",\n        \"description\": \"Something went wrong while booting up Mutiny Wallet.\",\n        \"repair_options\": \"If your wallet seems broken, here are some tools to try to debug and repair it.\",\n        \"questions\": \"If you have any questions on what these buttons do, please\",\n        \"support_link\": \"reach out to us for support.\",\n        \"services_down\": \"It looks like one of Mutiny's services is down. Please try again later.\",\n        \"in_the_meantime\": \"In the meantime if you want to access your on-chain funds you can load Mutiny in\",\n        \"safe_mode\": \"Safe Mode\"\n      }\n    }\n  },\n  \"modals\": {\n    \"share\": \"Share\",\n    \"details\": \"Details\",\n    \"loading\": {\n      \"loading\": \"Loading: {{stage}}\",\n      \"default\": \"Just getting started\",\n      \"double_checking\": \"Double checking something\",\n      \"existing_wallet\": \"Checking for existing wallet\",\n      \"downloading\": \"Downloading\",\n      \"setup\": \"Setup\",\n      \"done\": \"Done\"\n    },\n    \"onboarding\": {\n      \"welcome\": \"Welcome!\",\n      \"setup\": \"Setup\",\n      \"restore_from_backup\": \"If you've used Mutiny before you can restore from a backup. Otherwise you can skip this and enjoy your new wallet!\",\n      \"not_available\": \"We don't do that yet\",\n      \"secure_your_funds\": \"Secure your funds\"\n    },\n    \"more_info\": {\n      \"whats_with_the_fees\": \"What's with the fees?\",\n      \"self_custodial\": \"Mutiny is a self-custodial wallet. To initiate a lightning payment we must open a lightning channel, which requires a minimum amount and a setup fee.\",\n      \"future_payments\": \"Future payments, both send and receive, will only incur normal network fees and a nominal service fee unless your channel runs out of inbound capacity.\",\n      \"liquidity\": \"Learn more about liquidity\",\n      \"fedimint\": \"If you join a federation, no setup fee or minimum is required.\"\n    },\n    \"confirm_dialog\": {\n      \"are_you_sure\": \"Are you sure?\",\n      \"cancel\": \"Cancel\",\n      \"confirm\": \"Confirm\"\n    },\n    \"lnurl_auth\": {\n      \"auth_request\": \"Authentication Request\",\n      \"login\": \"Login\",\n      \"decline\": \"Decline\",\n      \"error\": \"That didn't work for some reason.\",\n      \"authenticated\": \"Authenticated!\"\n    }\n  },\n  \"setup\": {\n    \"initial\": {\n      \"welcome\": \"Welcome to the Mutiny!\",\n      \"new_wallet\": \"New Wallet\",\n      \"import_existing\": \"Import Existing\",\n      \"reporting\": \"Allow anonymous error reporting to help us improve the app. You can opt out at any time.\"\n    },\n    \"new_profile\": {\n      \"description\": \"Mutiny makes payments social.\",\n      \"title\": \"Create your profile\"\n    },\n    \"skip\": \"Skip for now\",\n    \"import_profile\": \"Import existing nostr profile\",\n    \"import\": {\n      \"title\": \"Import nostr profile\",\n      \"description\": \"Login with an existing nostr account.\"\n    },\n    \"federation\": {\n      \"pick\": \"Pick a federation\",\n      \"skip_confirm\": \"You'll need to pay chain and channel fees to use Mutiny, and miss out on advanced features such as lightning address. You can change your mind later in the settings.\"\n    }\n  },\n  \"utils\": {\n    \"hours_future_one\": \"One hour from now\",\n    \"hours_future_other\": \"{{count}} hours from now\",\n    \"hours_past_one\": \"One hour ago\",\n    \"hours_past_other\": \"{{count}} hours ago\",\n    \"hours_short\": \"{{count}}h\",\n    \"days_future_one\": \"One day from now\",\n    \"days_future_other\": \"{{count}} days from now\",\n    \"days_past_one\": \"One day ago\",\n    \"days_past_other\": \"{{count}} days ago\",\n    \"days_short\": \"{{count}}d\",\n    \"minutes_future_one\": \"One minute from now\",\n    \"minutes_future_other\": \"{{count}} minutes from now\",\n    \"minutes_past_one\": \"One minute ago\",\n    \"minutes_past_other\": \"{{count}} minutes ago\",\n    \"minutes_short\": \"{{count}}m\",\n    \"nowish\": \"Nowish\",\n    \"seconds_future\": \"Seconds from now\",\n    \"seconds_past\": \"Just now\"\n  },\n  \"transfer\": {\n    \"completed\": \"Transfer Completed\",\n    \"sats_moved\": \"+{{amount}} sats have been moved to {{federation_name}}\",\n    \"confirm\": \"Confirm Transfer\",\n    \"title\": \"Transfer funds\"\n  }\n}\n"
  },
  {
    "path": "public/i18n/es.json",
    "content": "{\n  \"common\": {\n    \"title\": \"Billetera Mutiny\",\n    \"mutiny\": \"Mutiny\",\n    \"nice\": \"Bien\",\n    \"home\": \"Inicio\",\n    \"e_sats\": \"eSATS\",\n    \"e_sat\": \"eSAT\",\n    \"sats\": \"SATS\",\n    \"sat\": \"SAT\",\n    \"fee\": \"Comisión\",\n    \"send\": \"Enviar\",\n    \"receive\": \"Recibir\",\n    \"dangit\": \"¡Qué pena!\",\n    \"back\": \"Atrás\",\n    \"coming_soon\": \"(muy pronto)\",\n    \"copy\": \"Copiar\",\n    \"copied\": \"Copiado\",\n    \"continue\": \"Continuar\",\n    \"error_unimplemented\": \"Sin implementar\",\n    \"why\": \"¿Por qué?\",\n    \"private_tags\": \"Etiquetas privadas\",\n    \"view_transaction\": \"Ver transacción\",\n    \"view_payment_details\": \"Ver detalles del pago\",\n    \"pending\": \"Pendiente\",\n    \"error_safe_mode\": \"Mutiny se está ejecutando en modo seguro. Lightning esta deshabilitado.\",\n    \"self_hosted\": \"Auto-hospedado\"\n  },\n  \"contacts\": {\n    \"new\": \"nuevo\",\n    \"add_contact\": \"Añadir Contacto\",\n    \"new_contact\": \"Nuevo Contacto\",\n    \"create_contact\": \"Crear contacto\",\n    \"edit_contact\": \"Editar contacto\",\n    \"save_contact\": \"Guardar contacto\",\n    \"delete\": \"Elminar\",\n    \"confirm_delete\": \"¿Está seguro de querer eliminar este contacto?\",\n    \"payment_history\": \"Historial de pagos\",\n    \"no_payments\": \"Todavía no hay pagos con\",\n    \"edit\": \"Editar\",\n    \"pay\": \"Pagar\",\n    \"name\": \"Nombre\",\n    \"ln_address\": \"Dirección Lightning\",\n    \"placeholder\": \"Satoshi\",\n    \"lightning_address\": \"Dirección Lightning\",\n    \"unimplemented\": \"Sin implementar\",\n    \"not_available\": \"No hacemos eso todavía\",\n    \"error_name\": \"Necesitamos por lo menos un nombre\",\n    \"email_error\": \"Eso no parece una dirección lightning\",\n    \"npub_error\": \"Eso no parece un npub de nostr\",\n    \"error_ln_address_missing\": \"Los contactos nuevos necesitan una dirección lightning\",\n    \"npub\": \"Npub Nostr\"\n  },\n  \"receive\": {\n    \"receive_bitcoin\": \"Recibir Bitcoin\",\n    \"edit\": \"Editar\",\n    \"checking\": \"Verificando\",\n    \"choose_format\": \"Escoja formato\",\n    \"payment_received\": \"Pago Recibido\",\n    \"payment_initiated\": \"Pago Iniciado\",\n    \"receive_add_the_sender\": \"Agregue el remitente para sus registros\",\n    \"keep_mutiny_open\": \"Deje Mutiny abierto para completar el pago.\",\n    \"choose_payment_format\": \"Escoja el formato de pago\",\n    \"unified_label\": \"Unificado\",\n    \"unified_caption\": \"Combina una dirección de bitcoin y una factura lightning. El remitente escoge el método de pago.\",\n    \"lightning_label\": \"Factura Lightning\",\n    \"lightning_caption\": \"Ideal para transacciones pequeñas. Usualmente comisiones más bajas que en-cadena.\",\n    \"onchain_label\": \"Dirección bitcoin\",\n    \"onchain_caption\": \"En-cadena, tal como lo hizo Satoshi. Ideal para transacciones muy grandes.\",\n    \"unified_setup_fee\": \"Una comisión de instalación de lightning por {{amount}} SATS se cobrará si pagado a través de lightning.\",\n    \"lightning_setup_fee\": \"Una comisión de instalación de lightning por {{amount}} SATS se cobrará para esta recepción.\",\n    \"amount\": \"Monto\",\n    \"fee\": \"+ Comisión\",\n    \"total\": \"Total\",\n    \"spendable\": \"Gastable\",\n    \"channel_size\": \"Tamaño canal\",\n    \"channel_reserve\": \"- Reserva canal\",\n    \"error_under_min_lightning\": \"En-cadena por defecto. El monto es demasiado pequeño para su recepción en Lightning.\",\n    \"error_creating_unified\": \"En-cadena por defecto. Algo salió mal al crear la dirección unificada\",\n    \"error_creating_address\": \"Algo salió mal al crear la dirección en-cadena\",\n    \"amount_editable\": {\n      \"receive_too_small\": \"Una comisión de instalación de lightning puede ser deducida del monto solicitado.\",\n      \"setup_fee_lightning\": \"Se cargará una comisión de instalación de lightning si se paga por lightning.\",\n      \"more_than_21m\": \"Hay solo 21 millones bitcoins.\",\n      \"set_amount\": \"Establecer monto\",\n      \"max\": \"MAX\",\n      \"fix_amounts\": {\n        \"ten_k\": \"10k\",\n        \"one_hundred_k\": \"100k\",\n        \"one_million\": \"1m\"\n      },\n      \"del\": \"BORR\",\n      \"balance\": \"Balance\"\n    },\n    \"integrated_qr\": {\n      \"onchain\": \"En-cadena\",\n      \"lightning\": \"Lightning\",\n      \"unified\": \"Unificado\",\n      \"gift\": \"Regalo Lightning\"\n    },\n    \"remember_choice\": \"Recordar my selección para la próxima vez\",\n    \"what_for\": \"¿Para qué es esto?\"\n  },\n  \"send\": {\n    \"search\": {\n      \"placeholder\": \"Nombre, dirección, factura\",\n      \"paste\": \"Pegar\",\n      \"contacts\": \"Contactos\",\n      \"global_search\": \"Búsqueda global\",\n      \"no_results\": \"No hay resultados para\"\n    },\n    \"sending\": \"Enviando...\",\n    \"confirm_send\": \"Confirmar Envío\",\n    \"contact_placeholder\": \"Aggregar el recipiente para su registro\",\n    \"start_over\": \"Comenzar de Nuevo\",\n    \"send_bitcoin\": \"Enviar Bitcoin\",\n    \"paste\": \"Pegar\",\n    \"scan_qr\": \"Escanear QR\",\n    \"payment_initiated\": \"Pago Iniciado\",\n    \"payment_sent\": \"Pago Enviado\",\n    \"destination\": \"Destino\",\n    \"no_payment_info\": \"No hay información de pago\",\n    \"progress_bar\": {\n      \"of\": \"de\",\n      \"sats_sent\": \"sats enviados\"\n    },\n    \"what_for\": \"¿Para qué es esto?\",\n    \"zap_note\": \"Zapear nota\",\n    \"error_low_balance\": \"No tenemos saldo suficiente para pagar el monto indicada.\",\n    \"error_invoice_match\": \"Monto solicitado, {{amount}} SATS, no es igual al monto establecido.\",\n    \"error_channel_reserves\": \"No hay suficientes fondos disponibles.\",\n    \"error_address\": \"Dirección Lightning Inválida\",\n    \"error_channel_reserves_explained\": \"Una porción del balance de su canal es reservada para comisiones. Intente enviar un monto más pequeño o agregue fondos.\",\n    \"error_clipboard\": \"Portapapeles no soportado\",\n    \"error_keysend\": \"Keysend falló\",\n    \"error_LNURL\": \"Pago LNURL falló\",\n    \"error_expired\": \"La factura está expirada\",\n    \"payjoin_send\": \"Esto es un payjoin! El Mutiny continuará hasta que la privacidad mejore\",\n    \"payment_pending\": \"Pago pendiente\",\n    \"payment_pending_description\": \"Se esta tomando un tiempo, pero es posible que este pago todavía se realice. Por favor verifique 'Actividad' para el estado actual.\",\n    \"hodl_invoice_warning\": \"Esta es una factura hodl. Los pagos a facturas hodl pueden causar cierres forzosos del canal, lo que resulta en comisiones altas en-cadena ¡Pague bajo su propio riesgo!\",\n    \"private\": \"Privado\",\n    \"anonzap\": \"Zap Anon\"\n  },\n  \"feedback\": {\n    \"header\": \"¡Denos retroalimentación!\",\n    \"received\": \"¡Retroalimentacion recibida!\",\n    \"thanks\": \"Gracias por decirnos lo que está sucediendo.\",\n    \"more\": \"¿Tiene algo más que decir?\",\n    \"tracking\": \"Mutiny no rastrea n espía su comportamiento, por lo que su retroalimentación es increíblemente útil.\",\n    \"github\": \"No responderemos a esta retroalimentación. Si desea soporte por favor\",\n    \"create_issue\": \"cree una issue en GitHub.\",\n    \"link\": \"¿Retroalimentación?\",\n    \"feedback_placeholder\": \"Bugs, solicitud de funcionalidad, retroalimentación, etc.\",\n    \"info_label\": \"Incluir información de contacto\",\n    \"info_caption\": \"Si necesita que le demos seguimiento a este problema\",\n    \"email\": \"Correo electrónico\",\n    \"email_caption\": \"Direcciones desechables son bienvenidas\",\n    \"nostr\": \"Nostr\",\n    \"nostr_caption\": \"Su npub más fresco\",\n    \"nostr_label\": \"npub de Nostr o NIP-05\",\n    \"send_feedback\": \"Enviar Retroalimentación\",\n    \"invalid_feedback\": \"¡Por favor diga algo!\",\n    \"need_contact\": \"Necesitamos alguna forma de contactarlo\",\n    \"invalid_email\": \"Eso no me parece una dirección de correo electrónico\",\n    \"error\": \"Error enviando retroalimentación {{error}}\",\n    \"try_again\": \"Por favor intente de nuevo más tarde.\"\n  },\n  \"activity\": {\n    \"title\": \"Actividad\",\n    \"mutiny\": \"Mutiny\",\n    \"wallet\": \"Billetera\",\n    \"nostr\": \"Nostr\",\n    \"view_all\": \"Ver todo\",\n    \"receive_some_sats_to_get_started\": \"Reciba algunos sats para comenzar\",\n    \"channel_open\": \"Canal Abierto\",\n    \"channel_close\": \"Canal Cerrado\",\n    \"unknown\": \"Desconocido\",\n    \"import_contacts\": \"Importe sus contactos desde nostr para ver a quién están zapeando.\",\n    \"coming_soon\": \"Muy pronto\",\n    \"private\": \"Privado\",\n    \"anonymous\": \"Anónimo\",\n    \"from\": \"De:\",\n    \"transaction_details\": {\n      \"lightning_receive\": \"Recibido via Lightning\",\n      \"lightning_send\": \"Enviado via Lightning\",\n      \"channel_open\": \"Canal abierto\",\n      \"channel_close\": \"Canal cerrado\",\n      \"onchain_receive\": \"Recibido en-cadena\",\n      \"onchain_send\": \"Enviado en-cadena\",\n      \"paid\": \"Pagado\",\n      \"unpaid\": \"Sin pagar\",\n      \"status\": \"Estado\",\n      \"date\": \"Fecha\",\n      \"tagged_to\": \"Etiquetado a\",\n      \"description\": \"Descripción\",\n      \"fee\": \"Comisión\",\n      \"onchain_fee\": \"Comisión En-cadena\",\n      \"invoice\": \"Factura\",\n      \"payment_hash\": \"Hash de Pago\",\n      \"payment_preimage\": \"Preimagen\",\n      \"txid\": \"Txid\",\n      \"total\": \"Monto Solicitado\",\n      \"balance\": \"Balance\",\n      \"reserve\": \"Reserva\",\n      \"peer\": \"Par\",\n      \"channel_id\": \"ID Canal\",\n      \"reason\": \"Razón\",\n      \"confirmed\": \"Confirmado\",\n      \"unconfirmed\": \"Sin Confirmar\",\n      \"sweep_delay\": \"Los fondos pueden tomar algunos días en regresar a la billetera\",\n      \"no_details\": \"No se encontraron detalles del canal, lo que significa que probablemente el canal ha sido cerrado.\",\n      \"back_home\": \"regresar a inicio\"\n    }\n  },\n  \"scanner\": {\n    \"paste\": \"Pegar Algo\",\n    \"cancel\": \"Cancelar\"\n  },\n  \"settings\": {\n    \"header\": \"Ajustes\",\n    \"support\": \"Entérese como apoyar a Mutiny\",\n    \"experimental_features\": \"Experimentos\",\n    \"debug_tools\": \"HERRAMIENTAS DE DEPURACIÓN\",\n    \"danger_zone\": \"Zona de peligro\",\n    \"general\": \"General\",\n    \"version\": \"Versión:\",\n    \"admin\": {\n      \"title\": \"Página de Administración\",\n      \"caption\": \"Nuestras herramientas internas de depuración ¡Úselas sabiamente!\",\n      \"header\": \"Herramientas Secretas de Depuración\",\n      \"warning_one\": \"Si sabe lo que está haciendo está en el lugar adecuado.\",\n      \"warning_two\": \"Estas son herramientas internas que usamos para depurar y probar la aplicación ¡Por favor tenga cuidado!\",\n      \"kitchen_sink\": {\n        \"disconnect\": \"Desconectar\",\n        \"peers\": \"Pares\",\n        \"no_peers\": \"No hay pares\",\n        \"refresh_peers\": \"Refrescar Pares\",\n        \"connect_peer\": \"Conectar a Par\",\n        \"expect_a_value\": \"Esperando un valor...\",\n        \"connect\": \"Conectar\",\n        \"close_channel\": \"Cerrar Canal\",\n        \"force_close\": \"Forzar cierre de canal\",\n        \"abandon_channel\": \"Abandonar Canal\",\n        \"confirm_close_channel\": \"¿Está seguro de querer cerrar este canal?\",\n        \"confirm_force_close\": \"¿Está seguro de querer forzar el cierre de este canal? Sus fondos tardarán unos días en ser redimidos en la cadena.\",\n        \"confirm_abandon_channel\": \"¿Está seguro de querer abandonar este canal? Típicamente haga esto solo si la transacción de apertura no se confirma nunca. De lo contrario, perderá fondos.\",\n        \"channels\": \"Canales\",\n        \"no_channels\": \"No hay Canales\",\n        \"refresh_channels\": \"Refrescar Canales\",\n        \"pubkey\": \"Pubkey\",\n        \"amount\": \"Monto\",\n        \"open_channel\": \"Abrir Canal\",\n        \"nodes\": \"Nodos\",\n        \"no_nodes\": \"No hay nodos\",\n        \"enable_zaps_to_hodl\": \"¿Habilitar zaps a facturas hodl?\",\n        \"zaps_to_hodl_desc\": \"Zaps a facturas hodl pueden resultar en el cierre forzoso de canales, lo que resulta en altas comisiones en-cadena ¡Use bajo su propio riesgo!\",\n        \"zaps_to_hodl_enable\": \"Habilitar zaps hodl\",\n        \"zaps_to_hodl_disable\": \"Deshabilitar zaps hodl\"\n      }\n    },\n    \"backup\": {\n      \"title\": \"Respaldo\",\n      \"secure_funds\": \"Aseguremos estos fondos.\",\n      \"twelve_words_tip\": \"Le mostraremos 12 palabras. Usted escribe esas 12 palabras.\",\n      \"warning_one\": \"Si limpia el historial de su navegador, o pierde su dispositivo, estas 12 palabras son la única manera de restaurar su billetera.\",\n      \"warning_two\": \"Mutiny es auto-custodial. Todo depende de usted...\",\n      \"confirm\": \"Escribí las palabras\",\n      \"responsibility\": \"Entiendo que mis fondos son mi responsabilidad\",\n      \"liar\": \"No estoy mintiendo solo para terminar con esto\",\n      \"seed_words\": {\n        \"reveal\": \"TOQUE PARA REVELAR LAS PALABRAS SEMILLA\",\n        \"hide\": \"ESCONDER\",\n        \"copy\": \"Peligrosamente Copiar al Portapapeles\",\n        \"copied\": \"Copiado!\"\n      }\n    },\n    \"channels\": {\n      \"title\": \"Canales Lightning\",\n      \"outbound\": \"Saliente\",\n      \"inbound\": \"Entrante\",\n      \"reserve\": \"Reserva\",\n      \"have_channels\": \"Tiene\",\n      \"have_channels_one\": \"canal lightning.\",\n      \"have_channels_many\": \"canales lightning.\",\n      \"inbound_outbound_tip\": \"Saliente es el monto de dinero que puede gastar en lightning. Entrante es el monto que puede recibir sin incurrir en una comisión de servicio de lightning.\",\n      \"reserve_tip\": \"Alrededor del 1% del balance de su canal está reservado en lightning para comisiones. Reservas adicionales son requeridas para canales que abrió usando swap.\",\n      \"no_channels\": \"Parece que todavía no tiene ningún canal. Para empezar, reciba algunos sats port lightning, o haga un swap para mover fondos en-cadena hacia un canal ¡Manos a la obra!\",\n      \"close_channel\": \"Cerrar\",\n      \"online_channels\": \"Canales en Línea\",\n      \"offline_channels\": \"Canales Fuera de Línea\",\n      \"close_channel_confirm\": \"Cerrar este canal moverá el saldo en-cadena e incurrirá en una comisión en-cadena.\"\n    },\n    \"connections\": {\n      \"title\": \"Conexiones Billetera\",\n      \"error_name\": \"El nombre no puede estar vacío\",\n      \"error_connection\": \"Fallo al crear Conexión Billetera\",\n      \"error_budget_zero\": \"El presupuesto debe ser mayor a cero\",\n      \"add_connection\": \"Agregar Conexión\",\n      \"manage_connections\": \"Manejar Conexiones\",\n      \"manage_gifts\": \"Manejar Regalos\",\n      \"delete_connection\": \"Eliminar\",\n      \"new_connection\": \"Nueva Conexión\",\n      \"edit_connection\": \"Editar Conexión\",\n      \"new_connection_label\": \"Nombre\",\n      \"new_connection_placeholder\": \"Mi cliente nostr favorito...\",\n      \"create_connection\": \"Crear Conexión\",\n      \"save_connection\": \"Guardar Cambios\",\n      \"edit_budget\": \"Editar Presupuesto\",\n      \"open_app\": \"Abrir Aplicación\",\n      \"open_in_nostr_client\": \"Abrir en Cliente Nostr\",\n      \"open_in_primal\": \"Abrir en Primal\",\n      \"nostr_client_not_found\": \"Cliente nostr no encontrado\",\n      \"client_not_found_description\": \"Instale un cliente nostr como Primal, Amethyst, o Damus para abrir este enlace.\",\n      \"relay\": \"Relé\",\n      \"authorize\": \"Autoriza servicios externos para solicitar pagos desde su billetera. Combina muy bien con clientes Nostr.\",\n      \"pending_nwc\": {\n        \"title\": \"Solicitudes Pendientes\",\n        \"approve_all\": \"Aprovar Todo\",\n        \"deny_all\": \"Denegar Todo\"\n      },\n      \"careful\": \"¡Tenga cuidado dónde comparte esta conexión! Solicitudes dentro del presupuesto serán pagadas automáticamente.\",\n      \"spent\": \"Gastado\",\n      \"remaining\": \"Restante\",\n      \"confirm_delete\": \"¿Está seguro de querer eliminar esta conexión?\",\n      \"budget\": \"Presupuesto\",\n      \"resets_every\": \"Se restablece cada\",\n      \"resubscribe_date\": \"Suscribirse de nuevo en\"\n    },\n    \"emergency_kit\": {\n      \"title\": \"Kit de Emergencia\",\n      \"caption\": \"Diagnostique y resuelva problemas con su billetera.\",\n      \"emergency_tip\": \"Si su billetera parece dañada, aquí hay algunas herramientas para tratar depurarla y repararla.\",\n      \"questions\": \"Si tiene preguntas acerca de qué hacen estos botones, por favor\",\n      \"link\": \"contáctenos para recibir soporte.\",\n      \"import_export\": {\n        \"title\": \"Exportar estado de la billetera\",\n        \"error_password\": \"Contraseña requerida\",\n        \"error_read_file\": \"Error al leer archivo\",\n        \"error_no_text\": \"No se encontró texto en el archivo\",\n        \"tip\": \"Puede exportar todo el estado de su Billetera Mutiny a un archivo e importarlo a otro navegador ¡Usualmente funciona!\",\n        \"caveat_header\": \"Advertencias importantes:\",\n        \"caveat\": \"después de exportar no realice ninguna operación en el navegador original. Si lo hace, tendrá que exportar de nuevo. Después de una importación exitosa, una buena práctica es borrar el estado del navegador original solo para asegurarse de no crear conflictos.\",\n        \"save_state\": \"Guardar Estado a Archivo\",\n        \"import_state\": \"Importar Estado desde Archivo\",\n        \"confirm_replace\": \"¿Desea reemplazar su estado con\",\n        \"password\": \"Ingrese su contraseña para descifrar\",\n        \"decrypt_wallet\": \"Descifrar Billetera\"\n      },\n      \"logs\": {\n        \"title\": \"Descargar logs de depuración\",\n        \"something_screwy\": \"Algo raro está sucediendo? ¡Verifique los logs!\",\n        \"download_logs\": \"Descargar logs\",\n        \"password\": \"Ingrese su contraseña para cifrar\",\n        \"confirm_password_label\": \"Confirme la Contraseña\"\n      },\n      \"delete_everything\": {\n        \"delete\": \"Eliminar Todo\",\n        \"confirm\": \"Esto eliminará el estado de su nodo ¡Esto no puede ser deshecho!\",\n        \"deleted\": \"Eliminado\",\n        \"deleted_description\": \"Eliminados todos los datos\"\n      }\n    },\n    \"encrypt\": {\n      \"title\": \"Seguridad\",\n      \"caption\": \"Haga un respaldo primero para desbloquear el cifrado\",\n      \"header\": \"Cifre sus palabras semilla\",\n      \"hot_wallet_warning\": \"Mutiny es una \\\"billetera caliente\\\" por lo que necesita sus palabras semilla para operar, pero usted puede opcionalmente cifrar esas palabras con una contraseña.\",\n      \"password_tip\": \"De esa manera, si alguien consigue acceder a su navegador, de todas maneras no tendrá acceso a sus fondos.\",\n      \"optional\": \"(opcional)\",\n      \"existing_password\": \"Constraseña existente\",\n      \"existing_password_caption\": \"Deje vacío si no ha establecido una contraseña todavía.\",\n      \"new_password_label\": \"Contraseña\",\n      \"new_password_placeholder\": \"Ingrese una contraseña\",\n      \"new_password_caption\": \"Esta contraseña será usada para cifrar sus palabras semilla. Si se le olvida, necesitará reingresar sus palabras semilla para acceder a sus fondos. Usted si escribió sus palabras semilla ¿correcto?\",\n      \"confirm_password_label\": \"Confirme Contraseña\",\n      \"confirm_password_placeholder\": \"Ingrese la misma contraseña\",\n      \"encrypt\": \"Cifrar\",\n      \"skip\": \"Saltar\",\n      \"error_match\": \"Las contraseñas no coinciden\",\n      \"error_same_as_existingpassword\": \"La nueva contraseña no debe coincidir con la contraseña existente\"\n    },\n    \"decrypt\": {\n      \"title\": \"Ingrese su contraseña\",\n      \"decrypt_wallet\": \"Descifrar Billetera\",\n      \"forgot_password_link\": \"¿Olvidó la Contraseña?\",\n      \"error_wrong_password\": \"Contraseña Inválida\"\n    },\n    \"currency\": {\n      \"title\": \"Moneda\",\n      \"caption\": \"Escoja su par de monedas preferida\",\n      \"select_currency\": \"Seleccione Moneda\",\n      \"select_currency_label\": \"Par de Moneda\",\n      \"select_currency_caption\": \"Al escoger una nueva moneda se resincronizará la billetera para obtener una actualización del precio\",\n      \"request_currency_support_link\": \"Solicite soporte para más monedas\",\n      \"error_unsupported_currency\": \"Por favor seleccione una moneda soportada.\"\n    },\n    \"language\": {\n      \"title\": \"Idioma\",\n      \"caption\": \"Escoja su idioma preferido\",\n      \"select_language\": \"Seleccione Idioma\",\n      \"select_language_label\": \"Idioma\",\n      \"select_language_caption\": \"Al escoger un nuevo idioma se cambiará el lenguaje de la billetera, ignorando el idioma actual del navegador\",\n      \"request_language_support_link\": \"Solicite soporte para más idiomas\",\n      \"error_unsupported_language\": \"Por favor seleccione un idioma soportado.\"\n    },\n    \"lnurl_auth\": {\n      \"title\": \"LNURL Auth\",\n      \"auth\": \"Auth\",\n      \"expected\": \"Esperando algo como LNURL...\"\n    },\n    \"plus\": {\n      \"title\": \"Mutiny+\",\n      \"join\": \"Unirse\",\n      \"sats_per_month\": \"por {{amount}} sats por mes.\",\n      \"lightning_balance\": \"Necesitará por lo menos {{amount}} sats en su saldo lightning para empezar ¡Intente antes de comprar!\",\n      \"restore\": \"Restaurar Suscripción\",\n      \"ready_to_join\": \"Listo para unirse\",\n      \"click_confirm\": \"Haga clic en confirmar para pagar su primer mes.\",\n      \"open_source\": \"Mutiny es código abierto y auto-hospedable.\",\n      \"optional_pay\": \"Pero también puede pagar por él.\",\n      \"paying_for\": \"Pagando por\",\n      \"supports_dev\": \"ayuda a soportar el desarrollo continuo y desbloquea el acceso temprano a nuevas características y funcionalidad premium:\",\n      \"thanks\": \"¡Usted hace parte del motín! Disfrute de las siguientes ventajas:\",\n      \"renewal_time\": \"Recibirá una solicitud de renovación de pago hacia\",\n      \"cancel\": \"Para cancelar su suscripción simplemente no pague. También puede deshabilitar el Mutiny+\",\n      \"wallet_connection\": \"Conexión Billetera.\",\n      \"subscribe\": \"Suscribirse\",\n      \"error_no_plan\": \"No se encontraron planes\",\n      \"error_failure\": \"No se pudo suscribir\",\n      \"error_no_subscription\": \"No se encontró ninguna suscripción\",\n      \"error_expired_subscription\": \"Su suscripción ha expirado, haga clic en unirse para renovar\",\n      \"satisfaction\": \"Satisfacción engreída\",\n      \"gifting\": \"Regalos\",\n      \"multi_device\": \"Acceso multi-dispositivo\",\n      \"ios_testflight\": \"Acceso a iOS TestFlight\",\n      \"more\": \"... y más por venir\",\n      \"cta_description\": \"Disfrute acceso temprano a nuevas características y funcionalidad premium.\",\n      \"cta_but_already_plus\": \"¡Gracias por su apoyo!\"\n    },\n    \"restore\": {\n      \"title\": \"Restaurar\",\n      \"all_twelve\": \"Necesita ingresar todas las 12 palabras\",\n      \"wrong_word\": \"Palabra equivocada\",\n      \"paste\": \"Peligrosamente Pegar del Portapapeles\",\n      \"confirm_text\": \"¿Está seguro de querer restaurar a esta billetera? ¡Su billetera existente será eliminada!\",\n      \"restore_tip\": \"Podrá restaurar una Billetera Mutiny existente desde su frase de 12 palabras semilla. Esto reemplazará su billetera existente, ¡por lo tanto esté seguro de saber lo que está haciendo!\",\n      \"multi_browser_warning\": \"No use múltiples navegadores al mismo tiempo.\",\n      \"error_clipboard\": \"Portapapeles no soportado\",\n      \"error_word_number\": \"Número equivocado de palabras\",\n      \"error_invalid_seed\": \"Frase semilla inválida\"\n    },\n    \"servers\": {\n      \"title\": \"Servidores\",\n      \"caption\": \"¡No confíe en nosotros! Use sus propios servidores para respaldar Mutiny.\",\n      \"link\": \"Aprenda más acerca de auto-hospedaje\",\n      \"proxy_label\": \"Proxy de Websockets\",\n      \"proxy_caption\": \"Cómo su nodo de lightning se comunica con el resto de la red.\",\n      \"error_proxy\": \"Debe ser una url comenzando con wss://\",\n      \"esplora_label\": \"Esplora\",\n      \"esplora_caption\": \"Datos de bloques para información en-cadena.\",\n      \"error_esplora\": \"Eso no parece una URL\",\n      \"rgs_label\": \"RGS\",\n      \"rgs_caption\": \"Rapid Gossip Sync. Datos de red sobre la red de lightning usados para enrutamiento.\",\n      \"error_rgs\": \"Eso no parece una URL\",\n      \"lsp_label\": \"LSP\",\n      \"lsp_caption\": \"Lightning Service Provider. Automáticamente abre canales hacia usted para liquidez entrante. También envuelve facturas para privacidad.\",\n      \"lsps_connection_string_label\": \"Cadena de Caracteres de Conexión LSPS\",\n      \"lsps_connection_string_caption\": \"Lightning Service Provider. Automáticamente abre canales hacia usted para liquidez entrante. Usando la especificación LSP.\",\n      \"error_lsps_connection_string\": \"Eso no parece una cadena de caracteres de conexión\",\n      \"lsps_token_label\": \"Token LSPS\",\n      \"lsps_token_caption\": \"Token LSPS.  Usado para identificar qué billetera está conectando al LSP\",\n      \"lsps_valid_error\": \"Puede tener tan solo un LSP establecido o una Cadena de Caracteres de Conexión LSPS y Token LSPS establecidos, no ambos.\",\n      \"error_lsps_token\": \"Eso no parece un token válido\",\n      \"storage_label\": \"Almacenamiento\",\n      \"storage_caption\": \"Servicio de respaldo VSS cifrado.\",\n      \"error_lsp\": \"Eso no parece un URL\",\n      \"save\": \"Guardar\"\n    },\n    \"nostr_contacts\": {\n      \"title\": \"Sincronizar Contactos Nostr\",\n      \"npub_label\": \"npub nostr\",\n      \"npub_required\": \"Npub no puede estar vacío\",\n      \"sync\": \"Sincronizar\",\n      \"resync\": \"Resincronizar\",\n      \"remove\": \"Eliminar\"\n    },\n    \"manage_federations\": {\n      \"title\": \"Manejar Federaciones\",\n      \"federation_code_label\": \"Código federación\",\n      \"federation_code_required\": \"Código federación no puede estar vacío\",\n      \"federation_added_success\": \"Federación agregada exitosamente\",\n      \"federation_remove_confirm\": \"¿Esta seguro de querer eliminar esta federación? Asegúrese primero de que sus fondos sean transferidos a su balance lightning u otra billetera.\",\n      \"add\": \"Agregar\",\n      \"remove\": \"Eliminar\",\n      \"expires\": \"Expira\",\n      \"federation_id\": \"ID federación\",\n      \"description\": \"\",\n      \"learn_more\": \"Aprenda más\",\n      \"created_at\": \"Creado En\",\n      \"recommended_by\": \"Recomendado por\"\n    },\n    \"gift\": {\n      \"give_sats_link\": \"Dar sats de regalo\",\n      \"title\": \"Regalo\",\n      \"no_plus_caption\": \"Actualice a Mutiny+ para habilitar regalos\",\n      \"receive_too_small\": \"Su primera recepción debe ser de {{amount}} SATS o más.\",\n      \"setup_fee_lightning\": \"Una comisión de instalación de lightning será cargada para recibir este regalo.\",\n      \"already_claimed\": \"Este regalo ya ha sido reclamado\",\n      \"sender_is_poor\": \"El remitente no tiene suficiente saldo para pagar este regalo.\",\n      \"sender_timed_out\": \"Se agotó el tiempo para el pago del regalo. El remitente puede estar desconectado, o este regalo ya ha sido reclamado.\",\n      \"sender_generic_error\": \"Remitente envió error: {{error}}\",\n      \"receive_header\": \"¡Le han regalado algunos sats!\",\n      \"receive_description\": \"Usted debe ser bastante especial. Para reclamar su dinero simplemente presione el botón grande. Los fondos serán agregados a esta billetera la próxima vez que el remitente se conecte.\",\n      \"receive_claimed\": \"¡Regalo reclamado! Deberá ver el regalo reflejado en el balance en breve.\",\n      \"receive_cta\": \"Reclamar Regalo\",\n      \"receive_try_again\": \"Intente de Nuevo\",\n      \"send_header\": \"Crear Regalo\",\n      \"send_explainer\": \"Regale sats. Cree una URL de regalo de Mutiny que pueda ser reclamado por cualquiera con un navegador.\",\n      \"send_name_required\": \"Esto es para sus registros\",\n      \"send_name_label\": \"Nombre del Recipiente\",\n      \"send_header_claimed\": \"¡Regalo Recibido!\",\n      \"send_claimed\": \"Su regalo ha sido reclamado. Gracias por compartir.\",\n      \"send_sharable_header\": \"URL compartible\",\n      \"send_instructions\": \"Copie este URL de regalo a su recipiente, o pídale que escanee este código QR con su billetera.\",\n      \"send_another\": \"Crear Otro\",\n      \"send_small_warning\": \"Un usuario nuevo de Mutiny no podrá redimir menos de 100k sats.\",\n      \"send_cta\": \"Crear un regalo\",\n      \"send_delete_button\": \"Eliminar Regalo\",\n      \"send_delete_confirm\": \"¿Está seguro de querer eliminar este regalo? ¿Es este su momento de tirar de la alfombra?\",\n      \"send_tip\": \"Su copia de la Billetera Mutiny necesita estar abierta para que el regalo sea reclamado.\",\n      \"need_plus\": \"Actualice a Mutiny+ para habilitar regalos. La funcionalidad de regalos le permite crear una URL de regalo de Mutiny que puede ser reclamado por cualquiera con un navegador.\"\n    }\n  },\n  \"swap\": {\n    \"peer_not_found\": \"Par no encontrado\",\n    \"channel_too_small\": \"Es simplemente tonto crear un canal más pequeño que {{amount}} sats\",\n    \"insufficient_funds\": \"No tiene suficientes fondos para crear este canal\",\n    \"header\": \"Hacer un Swap a Lightning\",\n    \"initiated\": \"Swap Iniciado\",\n    \"sats_added\": \"+{{amount}} sats serán agregados a su balance lightning\",\n    \"use_existing\": \"Usar par existente\",\n    \"choose_peer\": \"Escoja un par\",\n    \"peer_connect_label\": \"Conectar a un par nuevo\",\n    \"peer_connect_placeholder\": \"Cadena de caracteres de conexión a par\",\n    \"connect\": \"Conectar\",\n    \"connecting\": \"Conectando...\",\n    \"confirm_swap\": \"Confirmar Swap\"\n  },\n  \"swap_lightning\": {\n    \"insufficient_funds\": \"No tiene fondos suficientes para hacer swap a lightning\",\n    \"header\": \"Hacer Swap a Lightning\",\n    \"header_preview\": \"Previsualizar Swap\",\n    \"completed\": \"Swap Completado\",\n    \"too_small\": \"Monto invalido ingresado. Tiene que hacer un swap de por lo menos 100k sats.\",\n    \"sats_added\": \"+{{amount}} sats han sido agregados a su balance de Lightning\",\n    \"sats_fee\": \"+{{amount}} sats de comisión\",\n    \"confirm_swap\": \"Confirmar Swap\",\n    \"preview_swap\": \"Previsualizar Comisión de Swap\"\n  },\n  \"reload\": {\n    \"mutiny_update\": \"Actualización de Mutiny\",\n    \"new_version_description\": \"Una nueva versión de Mutiny ha sido almacenada en el caché, recargue para empezar a usarla.\",\n    \"reload\": \"Recargar\"\n  },\n  \"error\": {\n    \"title\": \"Error\",\n    \"emergency_link\": \"kit de emergencia.\",\n    \"reload\": \"Recargar\",\n    \"restart\": {\n      \"title\": \"¿Algo *extra* absurdo sucediendo? ¡Detenga los nodos!\",\n      \"start\": \"Iniciar\",\n      \"stop\": \"Parar\"\n    },\n    \"general\": {\n      \"oh_no\": \"¡Oh no!\",\n      \"never_should_happen\": \"Esto nunca debió suceder\",\n      \"try_reloading\": \"Intente recargar esta página o haga clic en el botón \\\"Qué Pena\\\". Si continua teniendo problemas,\",\n      \"support_link\": \"contáctenos para recibir soporte.\",\n      \"getting_desperate\": \"¿Desesperado? Intente\"\n    },\n    \"load_time\": {\n      \"stuck\": \"¿Atascado en esta pantalla? Intente recargar. Si eso no funciona, intente\"\n    },\n    \"not_found\": {\n      \"title\": \"No Encontrado\",\n      \"wtf_paul\": \"Esto es probablemente culpa de Paul.\"\n    },\n    \"reset_router\": {\n      \"payments_failing\": \"¿No puede hacer pagos? Intente reiniciar el enrutador de lightning.\",\n      \"reset_router\": \"Reiniciar Enrutador\"\n    },\n    \"resync\": {\n      \"incorrect_balance\": \"¿El balance en-cadena parece incorrecto? Intente resincronizar la billetera en-cadena.\",\n      \"resync_wallet\": \"Resincronizar billetera\"\n    },\n    \"on_boot\": {\n      \"existing_tab\": {\n        \"title\": \"Múltiples pestañas detectadas\",\n        \"description\": \"Mutiny solo puede ser usado en una pestaña a la vez. Parece que tiene otra pestaña abierta con Mutiny ejecutándose. Por favor cierre esa pestaña y refresque esta página, o cierre esta pestaña y refresque la otra.\"\n      },\n      \"already_running\": {\n        \"title\": \"Mutiny puede estar ejecutándose en otro dispositivo\",\n        \"description\": \"Mutiny solo puede ser usado en un lugar a la vez. Parece que tiene otro dispositivo o navegador usando esta billetera. Si ha cerrado Mutiny recientemente en otro dispositivo, por favor espere unos minutos e intente de nuevo.\",\n        \"retry_again_in\": \"Intente de nuevo en\",\n        \"seconds\": \"segundos\"\n      },\n      \"incompatible_browser\": {\n        \"title\": \"Navegador incompatible\",\n        \"header\": \"Navegador incompatible detectado\",\n        \"description\": \"Mutiny requiere un navegador moderno que soporte WebAssembly, LocalStorage, e IndexedDB. Algunos navegadores deshabilitan estas funcionalidades en modo privado.\",\n        \"try_different_browser\": \"Por favor asegúrese de que su navegador soporte todas estas funcionalidades, o considere intentar con otro navegador. También puede intentar deshabilitar ciertas extensiones o \\\"escudos\\\" que puedan bloquear estas funcionalidades.\",\n        \"browser_storage\": \"(Nos encantaría soportar más navegadores privados, pero tenemos que guardar los datos de su billetera en el almacenamiento del navegador o de lo contrario perdería esos fondos.)\",\n        \"browsers_link\": \"Navegadores Soportados\"\n      },\n      \"loading_failed\": {\n        \"title\": \"Fallo al cargar\",\n        \"header\": \"Fallo al cargar Mutiny\",\n        \"description\": \"Algo no funcionó al iniciar la Billetera Mutiny.\",\n        \"repair_options\": \"Si su billetera parece dañada, aquí hay algunas herramientas para tratar depurarla y repararla.\",\n        \"questions\": \"Si tiene alguna pregunta acerca de qué hacen estos botones, por favor\",\n        \"support_link\": \"contáctenos para recibir soporte.\",\n        \"services_down\": \"Parece que uno de los servicios de Mutiny está abajo. Por favor intente de nuevo más tarde.\",\n        \"in_the_meantime\": \"Mientras tanto si desea acceder a sus fondos en-cadena puede cargar Mutiny en\",\n        \"safe_mode\": \"Modo Seguro\"\n      }\n    }\n  },\n  \"modals\": {\n    \"share\": \"Compartir\",\n    \"details\": \"Detalles\",\n    \"loading\": {\n      \"loading\": \"Cargando: {{stage}}\",\n      \"default\": \"Apenas empezando\",\n      \"double_checking\": \"Verificando de nuevo algo\",\n      \"downloading\": \"Descargando\",\n      \"setup\": \"Configuración\",\n      \"done\": \"Hecho\"\n    },\n    \"onboarding\": {\n      \"welcome\": \"¡Bienvenido!\",\n      \"restore_from_backup\": \"Si ha usado Mutiny antes puede restaurar desde un respaldo ¡De lo contrario puede saltarse esto y disfrutar su nueva billetera!\",\n      \"not_available\": \"Todavía no hacemos eso\",\n      \"secure_your_funds\": \"Asegure sus fondos\"\n    },\n    \"more_info\": {\n      \"whats_with_the_fees\": \"¿Cuál es el asunto con las comisiones?\",\n      \"self_custodial\": \"Mutiny es una billetera auto-custodial. Para iniciar un pago lightning debemos abrir un canal lightning, lo que requiere un monto mínimo y una comisión de instalación.\",\n      \"future_payments\": \"Pagos futuros, tanto envíos como recepciones, solamente incurrirán en comisiones de red y una comisión nominal de servicio a menos de que su canal se quede sin capacidad entrante.\",\n      \"liquidity\": \"Aprenda más sobre liquidez\"\n    },\n    \"confirm_dialog\": {\n      \"are_you_sure\": \"¿Está seguro?\",\n      \"cancel\": \"Cancelar\",\n      \"confirm\": \"Confirmar\"\n    },\n    \"lnurl_auth\": {\n      \"auth_request\": \"Solicitud de autenticación\",\n      \"login\": \"Acceder\",\n      \"decline\": \"Declinar\",\n      \"error\": \"Eso no funcionó por alguna razón.\",\n      \"authenticated\": \"¡Autenticado!\"\n    }\n  },\n  \"profile\": {\n    \"manage_federation\": \"Manejar Federaciones\"\n  },\n  \"utils\": {\n    \"hours_future_one\": \"En una hora\",\n    \"hours_future_other\": \"En {{count}} horas\",\n    \"hours_past_one\": \"Hace una hora\",\n    \"hours_past_other\": \"Hace {{count}} horas\",\n    \"hours_short\": \"{{count}}h\",\n    \"days_future_one\": \"En un día\",\n    \"days_future_other\": \"En {{count}} días\",\n    \"days_past_one\": \"Hace un día\",\n    \"days_past_other\": \"Hace {{count}} días\",\n    \"days_short\": \"{{count}}d\",\n    \"minutes_future_one\": \"En un minuto\",\n    \"minutes_future_other\": \"En {{count}} minutos\",\n    \"minutes_past_one\": \"Hace un minuto\",\n    \"minutes_past_other\": \"Hace {{count}} minutos\",\n    \"minutes_short\": \"{{count}}m\",\n    \"nowish\": \"Casi ahora\",\n    \"seconds_future\": \"En algunos segundos\",\n    \"seconds_past\": \"Justo ahora\"\n  }\n}\n"
  },
  {
    "path": "public/i18n/fr.json",
    "content": "{\n  \"common\": {\n    \"title\": \"Mutiny Wallet\",\n    \"mutiny\": \"Mutiny\",\n    \"nice\": \"C'est bien\",\n    \"home\": \"Accueil\",\n    \"e_sats\": \"eSATS\",\n    \"e_sat\": \"eSAT\",\n    \"sats\": \"SATS\",\n    \"sat\": \"SAT\",\n    \"scan\": \"Scan\",\n    \"fee\": \"Frais\",\n    \"send\": \"Envoyer\",\n    \"receive\": \"Recevoir\",\n    \"request\": \"Demander\",\n    \"dangit\": \"Zut\",\n    \"back\": \"Retour\",\n    \"coming_soon\": \"(à venir)\",\n    \"copy\": \"Copier\",\n    \"copied\": \"Copié\",\n    \"continue\": \"Continue\",\n    \"error_unimplemented\": \"Pas de mise en œuvre\",\n    \"why\": \"Pourquoi ?\",\n    \"private_tags\": \"Tags privés\",\n    \"view_transaction\": \"Visualiser la transaction\",\n    \"view_payment_details\": \"Afficher les détails du paiement\",\n    \"pending\": \"En cours\",\n    \"error_safe_mode\": \"Mutiny fonctionne en mode sécurisé. Le réseau Lightning est désactivé.\",\n    \"self_hosted\": \"Auto-hébergé\",\n    \"expires\": \"Expire dans {{time}}\"\n  },\n  \"home\": {\n    \"receive\": \"Recevoir tes premiers sats\",\n    \"find\": \"Trouver tes amis sur nostr\",\n    \"backup\": \"Sécurise tes fonds !\",\n    \"connection\": \"Créer une connexion de portefeuille\",\n    \"connection_edit\": \"Modifier les connexions de portefeuille\",\n    \"federation\": \"Rejoindre une fédération\",\n    \"subnav\": {\n      \"just_me\": \"Juste moi\",\n      \"friends\": \"Amis\",\n      \"requests\": \"Demandes\"\n    }\n  },\n  \"profile\": {\n    \"profile\": \"Profil\",\n    \"nostr_identity\": \"Identité Nostr\",\n    \"add_lightning_address\": \"Ajouter une adresse Lightning\",\n    \"edit_profile\": \"Modifier Profil\",\n    \"join_federation\": \"Rejoindre une fédération\",\n    \"manage_federation\": \"Gérer les fédérations\",\n    \"federated_custody\": \"Garde fédérée\",\n    \"self_custody\": \"Auto-garde\",\n    \"social\": \"Social\",\n    \"edit\": {\n      \"nym\": \"Nym\"\n    },\n    \"deleted\": \"Ton profil nostr a été supprimé.\",\n    \"no_lightning_address\": \"Pas d'adresse de Lightning définie\",\n    \"pay_me\": \"Envoie-moi des sats\"\n  },\n  \"chat\": {\n    \"prompt\": \"Voici une nouvelle conversation. Essaie de demander de l'argent !\",\n    \"placeholder\": \"Message\"\n  },\n  \"contacts\": {\n    \"new\": \"nouveau\",\n    \"add_contact\": \"Ajouter Contact\",\n    \"new_contact\": \"Nouveau Contact\",\n    \"create_contact\": \"Créer contact\",\n    \"edit_contact\": \"Modifier contact\",\n    \"save_contact\": \"Sauvegarder contact\",\n    \"delete\": \"Supprimer\",\n    \"confirm_delete\": \"Veux-tu vraiment supprimer ce contact ?\",\n    \"payment_history\": \"Historique des paiements\",\n    \"no_payments\": \"Aucun paiement n'a encore été effectué avec\",\n    \"edit\": \"Modifier\",\n    \"pay\": \"Payer\",\n    \"name\": \"Nom\",\n    \"ln_address\": \"Adresse de Lightning\",\n    \"placeholder\": \"Satoshi\",\n    \"lightning_address\": \"Adresse de Lightning\",\n    \"unimplemented\": \"Pas de mise en œuvre\",\n    \"not_available\": \"Nous ne faisons pas ça encore\",\n    \"error_name\": \"Nous avons besoin d'un nom\",\n    \"email_error\": \"Cela ne ressemble pas à une adresse de Lightning\",\n    \"npub_error\": \"Cela ne ressemble pas à un npub de nostr\",\n    \"error_ln_address_missing\": \"Les nouveaux contacts ont besoin d'une adresse de Lightning\",\n    \"npub\": \"Nostr Npub\"\n  },\n  \"redeem\": {\n    \"redeem_bitcoin\": \"Réclamer Bitcoin\",\n    \"lnurl_amount_message\": \"Saisir le montant du retrait entre {{min}} et {{max}}.\",\n    \"lnurl_redeem_failed\": \"Retrait échoué\",\n    \"lnurl_redeem_success\": \"Paiement reçu\"\n  },\n  \"request\": {\n    \"request_bitcoin\": \"Demander Bitcoin\",\n    \"request\": \"Demander\"\n  },\n  \"receive\": {\n    \"receive_bitcoin\": \"Demander Bitcoin\",\n    \"edit\": \"Modifier\",\n    \"checking\": \"Vérification\",\n    \"choose_format\": \"Sélectionne le format\",\n    \"payment_received\": \"Paiement reçu\",\n    \"payment_initiated\": \"Payment Initiated\",\n    \"receive_add_the_sender\": \"Ajouter l'expéditeur pour tes archives\",\n    \"keep_mutiny_open\": \"Garde Mutiny ouvert pour finaliser le paiement.\",\n    \"choose_payment_format\": \"Sélectionne le format de paiement\",\n    \"lightning_label\": \"Facture de Lightning\",\n    \"lightning_caption\": \"Idéal pour les petites transactions. Les frais sont généralement inférieurs à ceux On-chain.\",\n    \"onchain_label\": \"Adresse de Bitcoin\",\n    \"onchain_caption\": \"On-chain, comme Satoshi l'a fait. Idéal pour les plus grandes transactions.\",\n    \"lightning_setup_fee\": \"Des frais d'installation de {{amount}} SATS seront facturés pour cette réception.\",\n    \"amount\": \"Montant\",\n    \"fee\": \"+ Frais\",\n    \"total\": \"Total\",\n    \"spendable\": \"Dépensable\",\n    \"channel_size\": \"Taille du canal\",\n    \"channel_reserve\": \"- Réserve de canal\",\n    \"error_under_min_lightning\": \"Passage par défaut à On-chain. Le montant est trop petit pour votre réception initiale de Lightning.\",\n    \"error_creating_unified\": \"Passons à On-chain. Un problème s'est produit lors de la création de la facture Lightning.\",\n    \"error_creating_address\": \"Un problème s'est produit lors de la création de l'adresse on-chain.\",\n    \"amount_editable\": {\n      \"receive_too_small\": \"Ta première réception doit être au moins égale à {{amount}} SATS ou plus.\",\n      \"setup_fee_lightning\": \"Des frais d'installation de Lightning seront facturés.\",\n      \"more_than_21m\": \"Il n'y a que 21 millions de bitcoin.\",\n      \"set_amount\": \"Fixer le montant\",\n      \"max\": \"MAX\",\n      \"fix_amounts\": {\n        \"ten_k\": \"10k\",\n        \"one_hundred_k\": \"100k\",\n        \"one_million\": \"1m\"\n      },\n      \"del\": \"SUPPRIMER\",\n      \"balance\": \"Balance\"\n    },\n    \"integrated_qr\": {\n      \"onchain\": \"On-chain\",\n      \"lightning\": \"Lightning\",\n      \"gift\": \"Cadeau Lightning\"\n    },\n    \"remember_choice\": \"Souvenir de mon choix pour la prochaine fois\",\n    \"what_for\": \"Ça sert à quoi ?\",\n    \"method_help\": {\n      \"title\": \"Méthode de réception\",\n      \"body\": \"Lightning reçoit automatiquement dans la fédération que t'as choisie. Tu peux passer à l'auto-garde plus tard si tu veux.\"\n    },\n    \"receive_strings_error\": \"Un problème s'est produit lors de la génération d'une facture ou d'une adresse on-chain.\",\n    \"error_under_min_onchain\": \"C'est sous la limite de poussière ! Les transactions on-chain devraient être beaucoup plus grandes.\",\n    \"warning_on_chain_fedi\": \"Les dépôts on-chain au fedimint nécessitent 10 confirmations. Le RBF n'est pas encore disponible.\",\n    \"warning_address_reuse\": \"N'envoies qu'une seule fois à cette adresse !\"\n  },\n  \"send\": {\n    \"search\": {\n      \"placeholder\": \"Nom, adresse, facture\",\n      \"paste\": \"Coller\",\n      \"contacts\": \"Contacts\",\n      \"global_search\": \"Recherche globale\",\n      \"no_results\": \"Aucun résultat pour\"\n    },\n    \"sending\": \"En cours...\",\n    \"confirm_send\": \"Confirme l'envoi\",\n    \"contact_placeholder\": \"Ajouter le destinataire pour tes archives\",\n    \"start_over\": \"Recommencer\",\n    \"send_bitcoin\": \"Envoyer Bitcoin\",\n    \"paste\": \"Coller\",\n    \"scan_qr\": \"Scanner le QR\",\n    \"payment_initiated\": \"Paiement lancé\",\n    \"payment_sent\": \"Paiement envoyé\",\n    \"destination\": \"Destination\",\n    \"no_payment_info\": \"Aucune information sur le paiement\",\n    \"progress_bar\": {\n      \"of\": \"de\",\n      \"sats_sent\": \"sats envoyé\"\n    },\n    \"what_for\": \"Ça sert à quoi ?\",\n    \"zap_note\": \"Zap note\",\n    \"error_low_balance\": \"Tu n'as pas de solde suffisant pour payer le montant donné.\",\n    \"error_invoice_match\": \"Le montant demandé, {{amount}} SATS, ne correspond pas au montant fixé.\",\n    \"error_channel_reserves\": \"Pas assez de fonds disponibles.\",\n    \"error_address\": \"Adresse de Lightning invalide\",\n    \"error_channel_reserves_explained\": \"Une partie du solde de ton canal est réservée aux frais. Essayie d'envoyer un montant inférieur ou d'ajouter des fonds.\",\n    \"error_clipboard\": \"Le presse-papiers n'est pas pris en charge\",\n    \"error_keysend\": \"Keysend a échoué\",\n    \"error_LNURL\": \"LNURL Pay a échoué\",\n    \"error_expired\": \"La facture est expirée\",\n    \"payjoin_send\": \"Voici un payjoin ! La mutinerie se poursuivra jusqu'à ce que le respect de la vie privée s'améliore.\",\n    \"payment_pending\": \"Paiement en cours\",\n    \"payment_pending_description\": \"Cela prend du temps, mais il est possible que ce paiement soit encore effectué. Consulte la rubrique « Activité » afin de confirmer le statut actuel.\",\n    \"hodl_invoice_warning\": \"Voici une facture hodl. Les paiements effectués sur des factures hodl peuvent entraîner des fermetures forcées de canaux, ce qui se traduit par des frais élevés on-chain. Paie à tes risques et périls !\",\n    \"private\": \"Privé\",\n    \"publiczap\": \"Zap Publique\",\n    \"privatezap\": \"Zap Privé\"\n  },\n  \"feedback\": {\n    \"header\": \"Donne ton avis !\",\n    \"received\": \"Tes commentaires ont été reçus!\",\n    \"thanks\": \"Merci de nous informer.\",\n    \"more\": \"Autres choses à dire ?\",\n    \"tracking\": \"Mutiny ne suit pas tes activités et ne les espionne pas, c'est pourquoi tes commentaires sont incroyablement utiles.\",\n    \"github\": \"Nous ne répondrons pas à tes commentaires. Si tu as besoin d'aide, merci de nous le faire savoir\",\n    \"create_issue\": \"Créer un GitHub issue.\",\n    \"link\": \"Commentaires ?\",\n    \"feedback_placeholder\": \"Bogues, demandes de fonctionnalités, commentaires, etc.\",\n    \"info_label\": \"Inclure des informations de contact\",\n    \"info_caption\": \"Si tu veux que nous assurions le suivi de cette question\",\n    \"email\": \"Email\",\n    \"email_caption\": \"Les adresses jetables sont les bienvenues\",\n    \"nostr\": \"Nostr\",\n    \"nostr_caption\": \"Ton nouveau npub\",\n    \"nostr_label\": \"Nostr npub ou NIP-05\",\n    \"send_feedback\": \"Envoyer commentaire\",\n    \"invalid_feedback\": \"Dis quelque chose !\",\n    \"need_contact\": \"Nous avons besoin d'un moyen de te contacter\",\n    \"invalid_email\": \"Cela ne ressemble pas à une adresse d'email\",\n    \"error\": \"Erreur dans l'envoi du commentaire {{error}}\",\n    \"try_again\": \"Réessaie plus tard.\"\n  },\n  \"activity\": {\n    \"title\": \"Activité\",\n    \"mutiny\": \"Mutiny\",\n    \"wallet\": \"Wallet\",\n    \"nostr\": \"Nostr\",\n    \"view_all\": \"Tout voir\",\n    \"receive_some_sats_to_get_started\": \"Recevoir des sats pour commencer\",\n    \"channel_open\": \"Ouvrir Canal\",\n    \"channel_close\": \"Fermer Canal\",\n    \"unknown\": \"Inconnu\",\n    \"import_contacts\": \"Importez tes contacts de nostr pour voir qui ils zappent.\",\n    \"coming_soon\": \"À venir\",\n    \"private\": \"Privé\",\n    \"anonymous\": \"Anonyme\",\n    \"from\": \"De:\",\n    \"transaction_details\": {\n      \"lightning_receive\": \"Reçu via Lightning\",\n      \"lightning_send\": \"Envoyé via Lightning\",\n      \"channel_open\": \"Ouvrir canal\",\n      \"channel_close\": \"Fermer canal\",\n      \"onchain_receive\": \"Recevoir nn-chain\",\n      \"onchain_send\": \"Envoyer on-chain\",\n      \"paid\": \"Payé\",\n      \"unpaid\": \"Non payé\",\n      \"status\": \"Statu\",\n      \"date\": \"Date\",\n      \"tagged_to\": \"Étiqueté à.\",\n      \"description\": \"Description\",\n      \"fee\": \"Frais\",\n      \"onchain_fee\": \"Frais on-chain\",\n      \"invoice\": \"Facture\",\n      \"payment_hash\": \"Hash de paiement\",\n      \"payment_preimage\": \"Préimage\",\n      \"txid\": \"Txid\",\n      \"total\": \"Montant demandé\",\n      \"balance\": \"Balance\",\n      \"reserve\": \"Réserve\",\n      \"peer\": \"Pair\",\n      \"channel_id\": \"ID de Canal\",\n      \"reason\": \"Motif\",\n      \"confirmed\": \"Confirmé\",\n      \"unconfirmed\": \"Non confirmé\",\n      \"sweep_delay\": \"Les fonds peuvent prendre quelques jours avant d'être reversés dans le portefeuille.\",\n      \"no_details\": \"Aucune information sur le canal n'a été trouvée, ce qui signifie que ce canal a probablement été fermé.\",\n      \"back_home\": \"retour à l'accueil\"\n    },\n    \"start_a_chat\": \"Commencer une discussion ?\",\n    \"start_a_chat_are_you_sure\": \"Cet utilisateur ne figure pas dans ta liste de contacts.\",\n    \"federation_message\": \"Message de la fédération\"\n  },\n  \"scanner\": {\n    \"paste\": \"Coller quelque chose\",\n    \"cancel\": \"Annuler\"\n  },\n  \"settings\": {\n    \"header\": \"Paramètres\",\n    \"support\": \"Comment soutenir Mutiny\",\n    \"experimental_features\": \"Expériences\",\n    \"debug_tools\": \"DEBUG TOOLS\",\n    \"danger_zone\": \"Zone de danger\",\n    \"general\": \"General\",\n    \"version\": \"Version:\",\n    \"admin\": {\n      \"title\": \"Page d'administration\",\n      \"caption\": \"Nos outils de debug internes. Utilise-les à bon escient !\",\n      \"header\": \"Les outils secrets de débogage\",\n      \"warning_one\": \"Si tu sais ce que tu fais, t'es au bon endroit.\",\n      \"warning_two\": \"Il s'agit d'outils internes que nous utilisons pour déboguer et tester l'application. Sois prudent !\",\n      \"kitchen_sink\": {\n        \"disconnect\": \"Déconnecter\",\n        \"peers\": \"Pairs\",\n        \"no_peers\": \"Aucun de pairs\",\n        \"refresh_peers\": \"Actualiser les pairs.\",\n        \"connect_peer\": \"Connecter à un pair\",\n        \"expect_a_value\": \"Attendre une valeure...\",\n        \"connect\": \"Connecter\",\n        \"close_channel\": \"Fermer Canal\",\n        \"force_close\": \"Fermeture forcée d'un canal\",\n        \"abandon_channel\": \"Abandonner ce canal\",\n        \"confirm_close_channel\": \"Es-tu sûr de vouloir fermer ce canal ?\",\n        \"confirm_force_close\": \"Es-tu sûr de vouloir forcer la fermature ce canal ? Tes fonds mettront quelques jours à être remboursés on-chain.\",\n        \"confirm_abandon_channel\": \"Es-tu sûr de vouloir abandonner ce canal ? En général, tu ne le pas que si la transaction d'ouverture n'est jamais confirmée. Sinon, te perds des fonds.\",\n        \"channels\": \"Canaux\",\n        \"no_channels\": \"Aucuns canaux\",\n        \"refresh_channels\": \"Actualiser les canaux\",\n        \"pubkey\": \"Pubkey\",\n        \"amount\": \"Montant\",\n        \"open_channel\": \"Ouvrir canal\",\n        \"nodes\": \"Nœuds\",\n        \"no_nodes\": \"Aucuns nœuds\",\n        \"enable_zaps_to_hodl\": \"Activer les zaps pour les factures de hodl ?\",\n        \"zaps_to_hodl_desc\": \"Les zaps vers les factures hodl peuvent entraîner des fermetures forcées des canaux, ce qui se traduit par des frais élevés on-chain. Utilise-les à tes risques et périls !\",\n        \"zaps_to_hodl_enable\": \"Activer hodl zaps\",\n        \"zaps_to_hodl_disable\": \"Désactiver hodl zaps\"\n      }\n    },\n    \"backup\": {\n      \"title\": \"Backup\",\n      \"secure_funds\": \"Assurons la sécurité de ces fonds.\",\n      \"twelve_words_tip\": \"Nous te montrerons 12 mots. Fais une note des 12 mots quelque part.\",\n      \"warning_one\": \"Si t'effaces l'historique de ton navigateur ou si tu perds ton appareil, ces 12 mots sont le seul moyen de restaurer ton portefeuille.\",\n      \"warning_two\": \"Mutiny est auto-garde. Tout dépend de toi...\",\n      \"confirm\": \"J'ai écrit les mots\",\n      \"responsibility\": \"Je comprends que mes fonds sont sous ma responsabilité\",\n      \"liar\": \"Je ne mens pas juste pour en finir avec ça\",\n      \"seed_words\": {\n        \"reveal\": \"TAPE POUR RÉVÉLER LES MOTS DE GRAINE\",\n        \"hide\": \"CACHER\",\n        \"copy\": \"Copier dangereusement dans le presse-papiers\",\n        \"copied\": \"Copié !\"\n      }\n    },\n    \"channels\": {\n      \"title\": \"Canaux de Lightning\",\n      \"outbound\": \"Sortie\",\n      \"inbound\": \"Entrant\",\n      \"reserve\": \"Réserve\",\n      \"have_channels\": \"Tu as\",\n      \"have_channels_one\": \"canal de lightning.\",\n      \"have_channels_many\": \"canaux de lightning.\",\n      \"inbound_outbound_tip\": \"Sortie est la somme d'argent que tu peux dépenser sur Lightning. Entrant est le montant que tu peux recevoir sans avoir à payer de frais de service pour Lightning.\",\n      \"reserve_tip\": \"Environ 1 % du solde de ton canal est réservé aux frais de Lightning. Des réserves supplémentaires sont nécessaires pour les chaînes que tu as ouvertes via un swap\",\n      \"no_channels\": \"Il semble que tu n'aies pas encore de canaux. Pour commencer, reçois des sats par Lightning, ou échange à on-chain afin de financer un canal. Mets la main à la pâte !\",\n      \"close_channel\": \"Fermer\",\n      \"online_channels\": \"Canaux en ligne\",\n      \"offline_channels\": \"Canaux hors ligne\",\n      \"close_channel_confirm\": \"La fermeture de ce canal déplacera le solde on-chain et entraînera des frais sur la chaîne.\",\n      \"force_close_channel_confirm\": \"Ce canal est hors ligne. La fermeture forcée de ce canal déplacera le solde sur la chaîne, entraînera des frais sur la chaîne et peut prendre quelques jours pour être réglé.\"\n    },\n    \"connections\": {\n      \"title\": \"Connexions de Portefeuilles\",\n      \"error_name\": \"Le nom ne peut être vide\",\n      \"error_connection\": \"Échec de la création de la connexion au portefeuille\",\n      \"error_budget_zero\": \"Le budget doit être supérieur à zéro\",\n      \"add_connection\": \"Ajouter Connexion\",\n      \"manage_connections\": \"Gérer Connections\",\n      \"manage_gifts\": \"Gérer Cadeaux\",\n      \"delete_connection\": \"Supprimer\",\n      \"new_connection\": \"Nouvelle Connexion\",\n      \"edit_connection\": \"Modifier Connexion\",\n      \"new_connection_label\": \"Nom\",\n      \"new_connection_placeholder\": \"Mon client de nostr préféré...\",\n      \"create_connection\": \"Créer Connexion\",\n      \"save_connection\": \"Enregistrer les modifications\",\n      \"edit_budget\": \"Modifier le budget\",\n      \"open_app\": \"Ourvir App\",\n      \"open_in_nostr_client\": \"Ouvrir dans un client de Nostr\",\n      \"open_in_primal\": \"Ouvrir dans Primal\",\n      \"nostr_client_not_found\": \"Client de Nostr n'a pas été trouvé\",\n      \"client_not_found_description\": \"Installe un client de nostr comme Primal, Amethyst ou Damus afin d'ouvrir ce lien.\",\n      \"relay\": \"Relai\",\n      \"authorize\": \"Autorise des services externes à demander des paiements à partir de ton portefeuille. S'associe parfaitement avec les clients de Nostr.\",\n      \"pending_nwc\": {\n        \"title\": \"Demandes en cours\",\n        \"approve_all\": \"Tout approuver\",\n        \"deny_all\": \"Tous refuser\"\n      },\n      \"careful\": \"Fais attention où tu partages cette connexion ! Les demandes qui ne dépassent pas le budget seront payées automatiquement.\",\n      \"spent\": \"Dépensé\",\n      \"remaining\": \"Restant\",\n      \"confirm_delete\": \"Es-tu sûr de vouloir supprimer cette connexion ?\",\n      \"budget\": \"Budget\",\n      \"resets_every\": \"Réinitialise chaque\",\n      \"resubscribe_date\": \"Réinscrire sur\",\n      \"disable_connection\": \"Désactiver\",\n      \"disabled\": \"Cet abonnement est désactivé. Actuellement, tu ne peux pas réactiver les abonnements, mais nous travaillons à résoudre ce problème. Reviens bientôt !\",\n      \"disable_connection_confirm\": \"Es-tu sûr de vouloir désactiver ton abonnement ? Actuellement, les abonnements ne peuvent pas être réactivés. Nous y travaillons à la résolution.\"\n    },\n    \"emergency_kit\": {\n      \"title\": \"Kit d'urgence\",\n      \"caption\": \"Diagnostiquer et résoudre les problèmes liés à ton portefeuille.\",\n      \"emergency_tip\": \"Si ton portefeuille semble ne pas fonctionner, voici quelques outils pour essayer de le déboguer et de le réparer.\",\n      \"questions\": \"Si tu as des questions sur les fonctions de ces boutons, fais nous signe.\",\n      \"link\": \"pour le service.\",\n      \"import_export\": {\n        \"title\": \"Exporter l'état du portefeuille\",\n        \"error_password\": \"Le mot de passe est requis\",\n        \"error_read_file\": \"Erreur de lecture\",\n        \"error_no_text\": \"Aucun texte n'a été trouvé dans le fichier\",\n        \"tip\": \"Tu peux exporter l'état complet de ton Mutiny Wallet dans un fichier et l'importer dans un nouveau navigateur. Cela devrait fonctionner !\",\n        \"caveat_header\": \"Des mises en garde importantes :\",\n        \"caveat\": \"Après l'exportation, ne réalisez plus d'opérations dans le navigateur d'origine. Si vous le faites, vous devrez réexporter. Après une importation réussie, la meilleure pratique est d'effacer l'historique du navigateur d'origine pour éviter tout risque de conflit.\",\n        \"save_state\": \"Enregistrer l'état sous forme de fichier\",\n        \"import_state\": \"Importer un état à partir d'un fichier\",\n        \"confirm_replace\": \"Veux-tu remplacer ton état par\",\n        \"password\": \"Entre ton mot de passe pour décrypter\",\n        \"decrypt_wallet\": \"Décrypter Portfeuille\",\n        \"decrypt_export\": \"Entre ton mot de passe pour enregistrer\"\n      },\n      \"logs\": {\n        \"title\": \"Télécharger les journaux de débogage\",\n        \"something_screwy\": \"Il y a de quoi de bizarre ? Consulte les journaux !\",\n        \"download_logs\": \"Télécharger les journaux\",\n        \"password\": \"Entre ton mot de passe pour décrypter\",\n        \"confirm_password_label\": \"Confirmer le mot de passe\"\n      },\n      \"delete_everything\": {\n        \"delete\": \"Tout supprimer\",\n        \"confirm\": \"Cette opération supprime l'état de ton nœud. Cette opération ne peut pas être annulée !\",\n        \"deleted\": \"Supprimé\",\n        \"deleted_description\": \"Suppression de toutes les données\"\n      }\n    },\n    \"encrypt\": {\n      \"title\": \"Sécurité\",\n      \"caption\": \"Sauvegarder d'abord pour déverrouiller le cryptage\",\n      \"header\": \"Cryptage des mots de graine\",\n      \"hot_wallet_warning\": \"Mutiny est un \\\"hot wallet\\\", il faut donc que ton mot de départ fonctionne, mais tu peux éventuellement crypter ces mots avec un mot de passe.\",\n      \"password_tip\": \"Ainsi, si quelqu'un accède à ton navigateur, il n'aura toujours pas accès à tes fonds.\",\n      \"optional\": \"(optionnel)\",\n      \"existing_password\": \"Mot de passe existant\",\n      \"existing_password_caption\": \"Laisse en blanc si tu n’as pas encore défini de mot de passe.\",\n      \"new_password_label\": \"Mot de passe\",\n      \"new_password_placeholder\": \"Entrer un mot de passe\",\n      \"new_password_caption\": \"Ce mot de passe sera utilisé pour crypter tes mots de graine. Si tu l’as oublié, tu devrais saisir à nouveau tes mots de graine pour accéder à tes fonds. Mais tu as bien noté des mots de graine, n'est-ce pas ?\",\n      \"confirm_password_label\": \"Confirmer mot de passe\",\n      \"confirm_password_placeholder\": \"Entrer le même mot de passe\",\n      \"encrypt\": \"Crypter\",\n      \"skip\": \"Ignorer\",\n      \"error_match\": \"Les mots de passe ne correspondent pas\",\n      \"error_same_as_existingpassword\": \"Le nouveau mot de passe ne doit pas correspondre au mot de passe existant\"\n    },\n    \"decrypt\": {\n      \"title\": \"Entrer ton mot de passe\",\n      \"decrypt_wallet\": \"Décrypter le portefeuille\",\n      \"forgot_password_link\": \"Mot de passe oublié ?\",\n      \"error_wrong_password\": \"Mot de passe invalide\"\n    },\n    \"currency\": {\n      \"title\": \"Monnaie\",\n      \"caption\": \"Choisir ta paire de devises préférée\",\n      \"select_currency\": \"Sélectionner monnaie\",\n      \"select_currency_label\": \"Paire de devises\",\n      \"select_currency_caption\": \"Le choix d'une nouvelle devise entraîne la resynchronisation du portefeuille pour obtenir une mise à jour des prix\",\n      \"request_currency_support_link\": \"Demande de soutien pour d'autres devises\",\n      \"error_unsupported_currency\": \"Sélectionner une devise prise en charge.\"\n    },\n    \"language\": {\n      \"title\": \"Langue\",\n      \"caption\": \"Choisir ta langue préférée\",\n      \"select_language\": \"Choisir Langue\",\n      \"select_language_label\": \"Langue\",\n      \"select_language_caption\": \"Le choix d'une nouvelle devise modifie la langue du portefeuille, sans tenir compte de la langue actuelle du navigateur\",\n      \"request_language_support_link\": \"Demande pour d'autres langues\",\n      \"error_unsupported_language\": \"Sélectionner une langue prise en charge.\"\n    },\n    \"lnurl_auth\": {\n      \"title\": \"LNURL Auth\",\n      \"auth\": \"Auth\",\n      \"expected\": \"Attendre quelque chose comme LNURL...\"\n    },\n    \"plus\": {\n      \"title\": \"Mutiny+\",\n      \"join\": \"Joindre\",\n      \"sats_per_month\": \"pour {{amount}} sats par mois.\",\n      \"lightning_balance\": \"T'auras besoin d'au moins {{amount}} sats dans ton montant de Lightning pour commencer. Essaie avant d'acheter !\",\n      \"restore\": \"Restaurer l'abonnement\",\n      \"ready_to_join\": \"Prêt à rejoindre\",\n      \"click_confirm\": \"Cliquer sur confirmer pour payer ton premier mois.\",\n      \"open_source\": \"Mutiny est open source et auto-hébergeable.\",\n      \"optional_pay\": \"Mais tu peux aussi payer pour cela.\",\n      \"paying_for\": \"Payer pour\",\n      \"supports_dev\": \"permet de soutenir le développement en cours et de bénéficier d'un accès anticipé aux nouvelles fonctionnalités et aux fonctionnalités premium :\",\n      \"thanks\": \"Tu fais partie de la mutinerie ! Profite des avantages suivants :\",\n      \"renewal_time\": \"Tu recevras une demande de paiement de renouvellement vers\",\n      \"cancel\": \"Pour annuler l'abonnement, il suffit de ne pas payer. Tu peux également désactiver le service Mutiny+\",\n      \"wallet_connection\": \"Connexion de portefeuille.\",\n      \"subscribe\": \"S'abonner\",\n      \"error_no_plan\": \"Aucun plan n'a été trouvé\",\n      \"error_failure\": \"Impossible de s'abonner\",\n      \"error_no_subscription\": \"Aucun abonnement existant n'a été trouvé\",\n      \"error_expired_subscription\": \"Ton abonnement a expiré, clique sur rejoindre pour le renouveler\",\n      \"satisfaction\": \"Satisfaction arrogante\",\n      \"gifting\": \"L'art du don\",\n      \"multi_device\": \"Accès multi-appareils\",\n      \"ios_testflight\": \"l'accès à iOS TestFlight\",\n      \"more\": \"... et plus encore à venir\",\n      \"cta_description\": \"Bénéficier d'un accès anticipé aux nouvelles fonctionnalités et aux fonctionnalités premium.\",\n      \"cta_but_already_plus\": \"Merci pour ton soutien!\",\n      \"lightning_address\": \"Ton propre adresse Lightning\"\n    },\n    \"restore\": {\n      \"title\": \"Restaurer\",\n      \"all_twelve\": \"Tu dois saisir les 12 mots\",\n      \"wrong_word\": \"Mot erroné\",\n      \"paste\": \"Copier dangereusement dans le presse-papiers\",\n      \"confirm_text\": \"Es-tu sûr de vouloir restaurer ce portefeuille ? Ton portefeuille actuel sera supprimé !\",\n      \"restore_tip\": \"Tu peux restaurer un Mutiny Wallet existant à partir de tes 12 mots de graine. Cela remplacera ton portefeuille existant, alors assure-toi de savoir ce que tu fais !\",\n      \"multi_browser_warning\": \"Ne pas utiliser plusieurs navigateurs en même temps.\",\n      \"error_clipboard\": \"Le presse-papiers n'est pas pris en charge\",\n      \"error_word_number\": \"Nombre de mots incorrect\",\n      \"error_invalid_seed\": \"Mots de graine non valides\"\n    },\n    \"servers\": {\n      \"title\": \"Serveurs\",\n      \"caption\": \"Ne nous fais pas confiance ! Utilise tes propres serveurs pour soutenir Mutiny.\",\n      \"link\": \"En savoir plus sur l'auto-hébergement\",\n      \"proxy_label\": \"Websockets Proxy\",\n      \"proxy_caption\": \"Comment ton nœud Lightning communique avec le reste du réseau.\",\n      \"error_proxy\": \"Il doit s'agir d'une url commençant par wss://\",\n      \"esplora_label\": \"Esplora\",\n      \"esplora_caption\": \"Données de bloc pour les informations on-chain.\",\n      \"error_esplora\": \"Cela ne ressemble pas à un URL\",\n      \"rgs_label\": \"RGS\",\n      \"rgs_caption\": \"Rapid Gossip Sync. Données de réseau sur le réseau de Lightning utilisées pour le routage.\",\n      \"error_rgs\": \"Cela ne ressemble pas à un URL\",\n      \"lsp_label\": \"LSP\",\n      \"lsp_caption\": \"Lightning Service Provider. Ouvre automatiquement des canaux vers toi pour les liquidités entrantes. Enveloppe également les factures pour plus de confidentialité.\",\n      \"lsps_connection_string_label\": \"LSPS Connection String\",\n      \"lsps_connection_string_caption\": \"Lightning Service Provider. Ouvre automatiquement des canaux vers toi pour les liquidités entrantes. Utilisation de la spécification LSP.\",\n      \"error_lsps_connection_string\": \"Cela ne ressemble pas à une chaîne de connexion de nœud\",\n      \"lsps_token_label\": \"LSPS Token\",\n      \"lsps_token_caption\": \"LSPS Token.  Utilisé pour identifier le portefeuille qui se connecte au LSP\",\n      \"lsps_valid_error\": \"Tu peux avoir soit un jeu de LSP, soit un jeu de chaîne de connexion LSPS et un jeu de jeton LSPS, mais pas les deux.\",\n      \"error_lsps_token\": \"Cela ne ressemble pas à un jeton valide\",\n      \"storage_label\": \"Stockage\",\n      \"storage_caption\": \"Service de sauvegarde VSS crypté.\",\n      \"error_lsp\": \"Cela ne ressemble pas à un URLL\",\n      \"error_tor\": \"Les URL Tor ne sont pas pris en charge actuellement\",\n      \"save\": \"Enregistrer\"\n    },\n    \"nostr_contacts\": {\n      \"title\": \"Synchroniser les contacts Nostr\",\n      \"npub_label\": \"Nostr npub\",\n      \"npub_required\": \"Npub ne peut pas être vide\",\n      \"sync\": \"Synchroniser\",\n      \"resync\": \"Resynchroniser\",\n      \"remove\": \"Retirer\"\n    },\n    \"manage_federations\": {\n      \"title\": \"Gérer Fédérations\",\n      \"federation_code_label\": \"Code de Fédération\",\n      \"federation_code_required\": \"Le code de la fédération ne peut pas être vide\",\n      \"federation_added_success\": \"Fédération ajoutée avec succès\",\n      \"federation_remove_confirm\": \"Es-tu sûr de vouloir supprimer cette fédération ? Assuret-toi d'abord que les fonds dont tu disposes sont transférés sur ton solde Lightning ou sur un autre portefeuille.\",\n      \"add\": \"Ajouter\",\n      \"remove\": \"Quitter fédération\",\n      \"expires\": \"Expire\",\n      \"federation_id\": \"ID de Fédération\",\n      \"description\": \"Les fédérations sont des réseaux basés sur le bitcoin qui rendent l'utilisation de bitcoin moins coûteuse, plus rapide et plus facile.\",\n      \"learn_more\": \"En savoir plus\",\n      \"discover\": \"Découvrir les fédérations\",\n      \"manual\": \"Code d'invitation\",\n      \"created_at\": \"Créé à\",\n      \"recommended_by\": \"Recommandé par\",\n      \"already_in_fed\": \"T'es déjà dans une fédération !\",\n      \"descriptionpart2\": \"Chacun d'entre eux est géré par un groupe de personnes ou d'entreprises différentes. Découvre ci-dessous celui auquel toi ou tes amis pourriez faire confiance.\",\n      \"join_me\": \"Rejoigndre à moi.\",\n      \"recommend\": \"Recommander une fédération\",\n      \"recommended_by_you\": \"Recommandé par toi\",\n      \"transfer_funds\": \"Transfert de fonds\",\n      \"transfer_funds_message\": \"Ajouter une deuxième fédération pour permettre les transferts.\"\n    },\n    \"gift\": {\n      \"give_sats_link\": \"Offrir sats comme cadeau\",\n      \"something_went_wrong\": \"Quelque chose n'a pas fonctionné\",\n      \"title\": \"L'art du don\",\n      \"no_plus_caption\": \"Passez à Mutiny+ pour permettre les cadeaux\",\n      \"receive_too_small\": \"Ta première réception doit être au moins égale à {{amount}} SATS ou plus.\",\n      \"setup_fee_lightning\": \"Des frais d'installation de Lightning seront facturés pour recevoir ce cadeau.\",\n      \"already_claimed\": \"Ce cadeau a déjà été réclamé\",\n      \"sender_is_poor\": \"L'expéditeur ne dispose pas d'un solde suffisant pour payer ce cadeau.\",\n      \"sender_timed_out\": \"Le paiement du cadeau a expiré. L'expéditeur est peut-être hors ligne ou ce cadeau a déjà été réclamé.\",\n      \"sender_generic_error\": \"Erreur envoyée par l'expéditeur: {{error}}\",\n      \"receive_header\": \"Tu as reçu des sats comme cadeau !\",\n      \"receive_description\": \"Tu dois être assez spécial. Pour réclamer ton argent, il te suffit d'appuyer sur le gros bouton. Les fonds seront ajoutés à ce porte-monnaie la prochaine fois que ton destinataire sera en ligne.\",\n      \"receive_claimed\": \"Cadeau réclamé ! Tu devrais voir le cadeau apparaître sur ton solde dans peu de temps.\",\n      \"receive_cta\": \"Réclamer un cadeau\",\n      \"receive_try_again\": \"Essayer à nouveau\",\n      \"send_header\": \"Créer un cadeau\",\n      \"send_explainer\": \"Offre le cadeau des sats. Créer un cadeau Mutiny URL qui peut être réclamé par n'importe qui avec un navigateur web.\",\n      \"send_name_required\": \"Ceci est pour ton information\",\n      \"send_name_label\": \"Nom du bénéficiaire\",\n      \"send_header_claimed\": \"Cadeau reçu!\",\n      \"send_claimed\": \"YTon cadeau a été réclamé. Merci d’avoir partagé.\",\n      \"send_sharable_header\": \"URL à partager\",\n      \"send_instructions\": \"Copie l'URL de ce cadeau à ton destinataire ou demande-lui de scanner ce code QR avec son portefeuille.\",\n      \"send_another\": \"Créer un autre\",\n      \"send_small_warning\": \"Un nouvel utilisateur de Mutiny ne pourra pas échanger moins de 100k sats.\",\n      \"send_cta\": \"Créer un cadeau\",\n      \"send_delete_button\": \"Supprimer cadeau\",\n      \"send_delete_confirm\": \"Es-tu sûr de vouloir supprimer ce cadeau ? C'est ton moment de rugpull ?\",\n      \"send_tip\": \"Ton exemplaire de Mutiny Wallet doit être ouvert pour que le cadeau puisse être échangé.\",\n      \"need_plus\": \"Passer à Mutiny+ pour activer le don. Le don tepermet de créer un cadeau Mutiny URL qui peut être réclamé par n'importe qui avec un navigateur web.\"\n    },\n    \"appearance\": \"Présentation\",\n    \"social\": \"Social\",\n    \"nostr_keys\": {\n      \"title\": \"Clés de Nostr\",\n      \"caption\": \"Pas tes clés, pas tes notes\",\n      \"description\": \"Tu peux utiliser le même profil de nostr dans plusieurs applications.\",\n      \"warning\": \"Fais attention à l'endroit où tu partages ta clé privée de nostr !\",\n      \"learn_more\": \"En savoir plus sur nostr\",\n      \"import_profile\": \"Importer un profil nostr\",\n      \"delete_account\": \"Supprimer le compte\",\n      \"delete_account_confirm\": \"La suppression d'un profil nostr ne peut être annulée et désactive certaines fonctionnalités sociales du portefeuille.\",\n      \"unlink_account\": \"Déconnecter le profil de nostr\",\n      \"unlink_account_confirm\": \"La déconnexion de ton profil nostr réinitialisera ton portefeuille à son profil de nostr par défaut.\",\n      \"delete_account_confirm_scary\": \"CE PROFIL SERA SUPPRIMÉ DANS TOUTES LES APPLICATIONS DE NOSTR.\"\n    },\n    \"lightning_address\": {\n      \"title\": \"Adresse de Mutiny\",\n      \"description\": \"Recevoir des zaps et des paiements sur ton portefeuille avec ton propre adresse Lightning.\",\n      \"create\": \"Créer une adresse de Mutiny\",\n      \"add_a_federation\": \"Pour activer cette fonctionnalité, tu devrais être membre d'une fédération.\",\n      \"ios_warning\": \"Mutiny Address nécessite Mutiny+ qui ne peut pas être acheté avec cette application.\"\n    }\n  },\n  \"swap\": {\n    \"peer_not_found\": \"Pair non trouvé\",\n    \"channel_too_small\": \"C’est un peu fou créer un canal plus petit que {{amount}} sats\",\n    \"insufficient_funds\": \"Tu n’as pas de fonds suffisants pour créer ce canal\",\n    \"header\": \"Échange à Lightning\",\n    \"initiated\": \"Échange initié\",\n    \"sats_added\": \"+{{amount}} sats seront ajouté à ton solde de Lightning\",\n    \"use_existing\": \"Utiliser un pair existant\",\n    \"choose_peer\": \"Choisir un pair\",\n    \"peer_connect_label\": \"Connexion à un nouveau pair\",\n    \"peer_connect_placeholder\": \"Chaîne de connexion de pair\",\n    \"connect\": \"Connexion\",\n    \"connecting\": \"En cours...\",\n    \"confirm_swap\": \"Confirmer l'échange\"\n  },\n  \"swap_lightning\": {\n    \"insufficient_funds\": \"Tu n'as pas assez de fonds pour échanger à Lightning\",\n    \"header\": \"Échanger à Lightning\",\n    \"header_preview\": \"Prévisualiser l'échange\",\n    \"completed\": \"L'échange est complet\",\n    \"too_small\": \"Le montant saisi n'est pas valide. Tu devrais échanger au moins 100k sats.\",\n    \"sats_added\": \"+{{amount}} sats sont ajoutés à ton solde de Lightning\",\n    \"sats_fee\": \"+{{amount}} frais de sats\",\n    \"confirm_swap\": \"Confirmer l'échange\",\n    \"preview_swap\": \"Prévisualiser les frais d'échange\"\n  },\n  \"reload\": {\n    \"mutiny_update\": \"Mise à jour de Mutiny\",\n    \"new_version_description\": \"La nouvelle version de Mutiny a été mise en cache, recharge-la pour commencer à l'utiliser.\",\n    \"reload\": \"Recharger\"\n  },\n  \"error\": {\n    \"title\": \"Erreur\",\n    \"emergency_link\": \"kit d'urgence.\",\n    \"reload\": \"Recharger\",\n    \"restart\": {\n      \"title\": \"Il se passe quelque chose de bizarre ? Arrête les nœuds !\",\n      \"start\": \"Démarrer\",\n      \"stop\": \"Arrêter\"\n    },\n    \"general\": {\n      \"oh_no\": \"Zut !\",\n      \"never_should_happen\": \"Cela n'aurait jamais dû arriver\",\n      \"try_reloading\": \"Essaie de recharger cette page ou de cliquer sur le bouton  \\”Zut\\\". Si tu rencontres toujours des problèmes,\",\n      \"support_link\": \"contacte-nous pour obtenir un soutien.\",\n      \"getting_desperate\": \"Désespéré ? Essaie le\"\n    },\n    \"load_time\": {\n      \"stuck\": \"Bloqué sur cet écran ? Essaie de recharger. Si cela ne fonctionne pas, consulte la\"\n    },\n    \"not_found\": {\n      \"title\": \"Non trouvé.\",\n      \"wtf_paul\": \"C'est probablement la faute de Paul.\"\n    },\n    \"resync\": {\n      \"incorrect_balance\": \"Le solde deon-chain semble incorrect ? Essaie de resynchroniser le portefeuille d’on-chain.\",\n      \"resync_wallet\": \"Resynchroniser le portefeuille\"\n    },\n    \"on_boot\": {\n      \"existing_tab\": {\n        \"title\": \"Plusieurs onglets détectés\",\n        \"description\": \"Mutiny ne peut être utilisé que dans un seul onglet à la fois. Il semble que t'aies un autre onglet ouvert avec Mutiny en cours d'exécution. Ferme cet onglet et actualise cette page, ou ferme cet onglet et fait actualiser l'autre.\"\n      },\n      \"already_running\": {\n        \"title\": \"Mutiny fonctionne peut-être sur un autre appareil\",\n        \"description\": \"Mutiny ne peut être utilisé qu'à un seul endroit à la fois. Il semble qu'un autre appareil ou navigateur utilise ce portefeuille. Si tu as récemment fermé Mutiny sur un autre appareil, attend quelques minutes et réessaie-le.\",\n        \"retry_again_in\": \"Réessaie dans\",\n        \"seconds\": \"seconds\"\n      },\n      \"incompatible_browser\": {\n        \"title\": \"Navigateur incompatible\",\n        \"header\": \"Navigateur incompatible détecté\",\n        \"description\": \"Mutiny exige un navigateur moderne qui soutient WebAssembly, LocalStorage, et IndexedDB. Certains navigateurs désactivent ces fonctionnalités en mode privé.\",\n        \"try_different_browser\": \"Assure-toi que ton navigateur prend en charge toutes ces fonctionnalités ou essaie d'en changer. Tu peux également essayer de désactiver certaines extensions ou certains \\\"shields\\\" qui bloquent ces fonctionnalités..\",\n        \"browser_storage\": \"(Nous aimerions prendre en charge davantage de navigateurs privés, mais nous devons enregistrer les données de ton portefeuille dans le stockage du navigateur, faute de quoi tu perdras des fonds.)\",\n        \"browsers_link\": \"Les navigateurs supportés\"\n      },\n      \"loading_failed\": {\n        \"title\": \"Échec du chargement\",\n        \"header\": \"Échec du chargement de Mutiny\",\n        \"description\": \"Un problème s'est produit lors du démarrage de Mutiny Wallet.\",\n        \"repair_options\": \"Si ton portefeuille semble cassé, voici quelques outils pour essayer de le déboguer et de le réparer.\",\n        \"questions\": \"Si tu as des questions sur les fonctions de ces boutons,\",\n        \"support_link\": \"fais nous signe.\",\n        \"services_down\": \"Il semble que l'un des services de Mutiny soit en panne. Veuillez réessayer plus tard.\",\n        \"in_the_meantime\": \"En attendant, si tu veux accéder à tes fonds on-chain, tu peux charger Mutiny dans\",\n        \"safe_mode\": \"Mode Sécurisé\"\n      }\n    }\n  },\n  \"modals\": {\n    \"share\": \"Partager\",\n    \"details\": \"Détails\",\n    \"loading\": {\n      \"loading\": \"Chargement: {{stage}}\",\n      \"default\": \"Ça commence\",\n      \"double_checking\": \"Nous vérifions quelque chose\",\n      \"existing_wallet\": \"Vérification de l'existence d'un portefeuille\",\n      \"downloading\": \"Téléchargement\",\n      \"setup\": \"Configuration\",\n      \"done\": \"Fini\"\n    },\n    \"onboarding\": {\n      \"welcome\": \"Bienvenue !\",\n      \"setup\": \"Configuration\",\n      \"restore_from_backup\": \"Si tu as déjà utilisé Mutiny, tu peux le restaurer à partir d'une sauvegarde. Sinon, tu peux sauter cette étape et profiter de ton nouveau portefeuille !\",\n      \"not_available\": \"Nous ne le faisons pas encore\",\n      \"secure_your_funds\": \"Sécuriser tes fonds\"\n    },\n    \"more_info\": {\n      \"whats_with_the_fees\": \"Pourquoi ces frais ?\",\n      \"self_custodial\": \"Mutiny est un portefeuille auto-garde. Pour initier un paiement lightning, nous devons ouvrir un canal Lightning, ce qui nécessite un montant minimum et des frais d'installation.\",\n      \"future_payments\": \"Les paiements futurs, qu'ils soient envoyés ou reçus, ne donneront lieu qu'à des frais de réseau normaux et à des frais de service nominaux, à moins que ton canal ne soit à court de capacité d'arrivée.\",\n      \"liquidity\": \"En savoir plus sur la liquidité\",\n      \"fedimint\": \"Si tu t'adhères à une fédération, aucun frais de configuration ou minimum n'est requis.\"\n    },\n    \"confirm_dialog\": {\n      \"are_you_sure\": \"Es-tu sûr ?\",\n      \"cancel\": \"Annuler\",\n      \"confirm\": \"Confirmer\"\n    },\n    \"lnurl_auth\": {\n      \"auth_request\": \"Demande d'authentification\",\n      \"login\": \"Connexion\",\n      \"decline\": \"Refuser\",\n      \"error\": \"Cela n'a pas fonctionné pour une raison quelconque.\",\n      \"authenticated\": \"Authentifié !\"\n    }\n  },\n  \"setup\": {\n    \"new_profile\": {\n      \"description\": \"Mutiny rend les paiements sociaux.\",\n      \"title\": \"Créer ton profil\"\n    },\n    \"skip\": \"Passer pour l'instant\",\n    \"import_profile\": \"Importer un profil de nostr existant\",\n    \"import\": {\n      \"title\": \"Importer profil de  nostr\",\n      \"description\": \"Se connecter avec un compte de nostr existant.\"\n    },\n    \"federation\": {\n      \"pick\": \"Choisir une fédération\",\n      \"skip_confirm\": \"Tu devrais payer des frais de chaîne et de canal pour utiliser Mutiny, et ne pourras pas bénéficier de fonctions avancées telles que l'adresse Lightning. Tu peux changer d'avis plus tard dans les paramètres.\"\n    }\n  },\n  \"utils\": {\n    \"hours_future_one\": \"Dans une heure.\",\n    \"hours_future_other\": \"D'ici {{count}} heures\",\n    \"hours_past_one\": \"Il y a une heure\",\n    \"hours_past_other\": \"Il y a {{count}} heures\",\n    \"hours_short\": \"{{count}}h\",\n    \"days_future_one\": \"Dans un jour\",\n    \"days_future_other\": \"D'ici {{count}} jours\",\n    \"days_past_one\": \"Il y a un jour\",\n    \"days_past_other\": \"Il y a {{count}} jours\",\n    \"days_short\": \"{{count}}j\",\n    \"minutes_future_one\": \"Dans une minute\",\n    \"minutes_future_other\": \"D'ici {{count}} minutes\",\n    \"minutes_past_one\": \"Il y a une minute\",\n    \"minutes_past_other\": \"Il y a {{count}} minutes\",\n    \"minutes_short\": \"{{count}}m\",\n    \"nowish\": \"Comme là\",\n    \"seconds_future\": \"Dans quelques secondes\",\n    \"seconds_past\": \"A l'instant\"\n  },\n  \"transfer\": {\n    \"completed\": \"Transfert réalisé\",\n    \"sats_moved\": \"+{{amount}} sats ont été transférés vers {{federation_name}}\",\n    \"confirm\": \"Confirmer le transfert\",\n    \"title\": \"Transfert de fonds\"\n  }\n}\n"
  },
  {
    "path": "public/i18n/hans.json",
    "content": "{\n    \"common\": {\n        \"title\": \"Mutiny 钱包\",\n        \"mutiny\": \"Mutiny\",\n        \"nice\": \"不错\",\n        \"home\": \"首页\",\n        \"e_sats\": \"e聪\",\n        \"e_sat\": \"e聪\",\n        \"sats\": \"聪\",\n        \"sat\": \"聪\",\n        \"scan\": \"扫描\",\n        \"fee\": \"费用\",\n        \"send\": \"发送\",\n        \"receive\": \"接收\",\n        \"request\": \"要求\",\n        \"dangit\": \"该死\",\n        \"back\": \"返回\",\n        \"coming_soon\": \"（即将推出）\",\n        \"copy\": \"复制\",\n        \"copied\": \"已复制\",\n        \"continue\": \"继续\",\n        \"error_unimplemented\": \"尚未实施\",\n        \"why\": \"为什么？\",\n        \"private_tags\": \"私人标签\",\n        \"view_transaction\": \"查看交易\",\n        \"view_payment_details\": \"查看付款详情\",\n        \"pending\": \"待定\",\n        \"error_safe_mode\": \"Mutiny 正在安全模式下运行。闪电已禁用。\",\n        \"self_hosted\": \"自托管\",\n        \"expires\": \"过期时间为 {{time}}\"\n    },\n    \"home\": {\n        \"receive\": \"首次收款\",\n        \"find\": \"在 nostr 上查找你的朋友\",\n        \"backup\": \"确保你的资金安全！\",\n        \"connection\": \"创建钱包连接\",\n        \"connection_edit\": \"编辑钱包连接\",\n        \"federation\": \"加入联合会\",\n        \"subnav\": {\n            \"just_me\": \"本人\",\n            \"friends\": \"朋友\",\n            \"requests\": \"要求\"\n        }\n    },\n    \"profile\": {\n        \"profile\": \"个人档案\",\n        \"nostr_identity\": \"Nostr 身份\",\n        \"add_lightning_address\": \"添加闪电地址\",\n        \"edit_profile\": \"编辑个人档案\",\n        \"join_federation\": \"加入联合会\",\n        \"manage_federation\": \"管理联合会\",\n        \"federated_custody\": \"联合式保管\",\n        \"self_custody\": \"自行保管\",\n        \"social\": \"社交\",\n        \"edit\": {\n            \"nym\": \"昵称\"\n        },\n        \"deleted\": \"你的 nostr 个人档案已被删除。\",\n        \"no_lightning_address\": \"尚未设置闪电地址\",\n        \"pay_me\": \"给我发聪\"\n    },\n    \"chat\": {\n        \"prompt\": \"这是一次新的对话。试试要钱吧！\",\n        \"placeholder\": \"消息\"\n    },\n    \"contacts\": {\n        \"new\": \"新增\",\n        \"add_contact\": \"添加联系人\",\n        \"new_contact\": \"新联系人\",\n        \"create_contact\": \"创建联系人\",\n        \"edit_contact\": \"编辑联系人\",\n        \"save_contact\": \"保存联系人\",\n        \"delete\": \"删除\",\n        \"confirm_delete\": \"你确定要删除这个联系人吗？\",\n        \"payment_history\": \"付款历史\",\n        \"no_payments\": \"尚未付款\",\n        \"edit\": \"编辑\",\n        \"pay\": \"支付\",\n        \"name\": \"名称\",\n        \"ln_address\": \"闪电地址\",\n        \"placeholder\": \"中本聪\",\n        \"lightning_address\": \"闪电地址\",\n        \"unimplemented\": \"尚未实施\",\n        \"not_available\": \"我们还没做到这一点\",\n        \"error_name\": \"我们至少需要一个名称\",\n        \"email_error\": \"这看起来不像是个闪电地址\",\n        \"npub_error\": \"这看起来不像是个 nostr npub\",\n        \"error_ln_address_missing\": \"新联系人需要闪电地址\",\n        \"npub\": \"Nostr Npub\"\n    },\n    \"redeem\": {\n        \"redeem_bitcoin\": \"兑现比特币\",\n        \"lnurl_amount_message\": \"输入 {{min}} 至 {{max}} 聪之间的取款金额\",\n        \"lnurl_redeem_failed\": \"取款失败\",\n        \"lnurl_redeem_success\": \"已收到付款\"\n    },\n    \"request\": {\n        \"request_bitcoin\": \"要求比特币\",\n        \"request\": \"要求\"\n    },\n    \"receive\": {\n        \"receive_bitcoin\": \"接收比特币\",\n        \"edit\": \"编辑\",\n        \"checking\": \"正在检查\",\n        \"choose_format\": \"选择格式\",\n        \"payment_received\": \"已收到付款\",\n        \"payment_initiated\": \"已启动付款\",\n        \"receive_add_the_sender\": \"将付款方添加到通讯录\",\n        \"keep_mutiny_open\": \"保持 Mutiny 开启以完成付款。\",\n        \"choose_payment_format\": \"选择付款方式\",\n        \"lightning_label\": \"闪电发票\",\n        \"lightning_caption\": \"适合小额交易。费用通常低于链上交易。\",\n        \"onchain_label\": \"比特币地址\",\n        \"onchain_caption\": \"链上交易，就像中本聪所做的那样。适合大额交易。\",\n        \"lightning_setup_fee\": \"此接收将收取 {{amount}} 聪的闪电设置费。\",\n        \"amount\": \"金额\",\n        \"fee\": \"+ 费用\",\n        \"total\": \"总额\",\n        \"spendable\": \"可支出\",\n        \"channel_size\": \"通道大小\",\n        \"channel_reserve\": \"- 通道储备\",\n        \"error_under_min_lightning\": \"默认为链上。首次闪电接收的金额太小。\",\n        \"error_creating_unified\": \"默认为链上。创建闪电发票时出了问题。\",\n        \"error_creating_address\": \"创建链上地址时出了问题。\",\n        \"amount_editable\": {\n            \"receive_too_small\": \"你的首次接收必须是 {{amount}} 聪或更高。\",\n            \"setup_fee_lightning\": \"将收取闪电设置费。\",\n            \"more_than_21m\": \"比特币只有 2100 万个。\",\n            \"set_amount\": \"设定金额\",\n            \"max\": \"最高\",\n            \"fix_amounts\": {\n                \"ten_k\": \"1万\",\n                \"one_hundred_k\": \"10万\",\n                \"one_million\": \"1百万\"\n            },\n            \"del\": \"DEL\",\n            \"balance\": \"余额\"\n        },\n        \"integrated_qr\": {\n            \"onchain\": \"链上\",\n            \"lightning\": \"闪电\",\n            \"gift\": \"闪电红包\"\n        },\n        \"remember_choice\": \"下次记住我的选择\",\n        \"what_for\": \"这是派什么用的？\",\n        \"method_help\": {\n            \"title\": \"接收方式\",\n            \"body\": \"闪电接收将自动存入你选择的联合会。如果你想要的话，可以以后兑换成自我保管。\"\n        },\n        \"receive_strings_error\": \"生成发票或链上地址时出了问题。\",\n        \"error_under_min_onchain\": \"这还不到灰尘的上限！链上交易应该更大。\",\n        \"warning_on_chain_fedi\": \"链上 fedimint 存款需要 10 次确认。尚未支持 RBF。\",\n        \"warning_address_reuse\": \"只能向该地址发送一次！\"\n    },\n    \"send\": {\n        \"search\": {\n            \"placeholder\": \"名称、地址、发票\",\n            \"paste\": \"粘贴\",\n            \"contacts\": \"通讯录\",\n            \"global_search\": \"全球搜索\",\n            \"no_results\": \"未找到相关结果\"\n        },\n        \"sending\": \"正在发送...\",\n        \"confirm_send\": \"确认发送\",\n        \"contact_placeholder\": \"将收款方添加到通讯录\",\n        \"start_over\": \"重新开始\",\n        \"send_bitcoin\": \"发送比特币\",\n        \"paste\": \"粘贴\",\n        \"scan_qr\": \"扫描二维码\",\n        \"payment_initiated\": \"已启动付款\",\n        \"payment_sent\": \"付款已发送\",\n        \"destination\": \"目的地\",\n        \"no_payment_info\": \"无付款信息\",\n        \"progress_bar\": {\n            \"of\": \"中的\",\n            \"sats_sent\": \"聪已支付\"\n        },\n        \"what_for\": \"这是派什么用的？\",\n        \"zap_note\": \"打闪笔记\",\n        \"error_low_balance\": \"我们没有足够的余额来支付指定金额。\",\n        \"error_invoice_match\": \"要求的 {{amount}} 聪金额不等于设定的金额。\",\n        \"error_channel_reserves\": \"可用资金不足。\",\n        \"error_address\": \"无效闪电地址\",\n        \"error_channel_reserves_explained\": \"你的通道余额中有一部分是为支付费用预留的储备。尝试发送较小的金额或添加资金。\",\n        \"error_clipboard\": \"不支持剪贴板\",\n        \"error_keysend\": \"Keysend 失败了\",\n        \"error_LNURL\": \"LNURL Pay 失败了\",\n        \"error_expired\": \"发票已过期\",\n        \"payjoin_send\": \"这是一个 payjoin！在隐私权得到改善之前，Mutiny 仍将继续\",\n        \"payment_pending\": \"付款待定\",\n        \"payment_pending_description\": \"虽然需要一些时间，但这笔付款仍有可能通过。请查看“动态”，了解当前状态。\",\n        \"hodl_invoice_warning\": \"这是一张 hodl 发票。向 hodl 发票付款可能会导致通道强制关闭，从而产生高额链上费用。请自行承担支付风险！\",\n        \"private\": \"私人\",\n        \"publiczap\": \"公开打闪\",\n        \"privatezap\": \"私人打闪\"\n    },\n    \"feedback\": {\n        \"header\": \"给我们反馈！\",\n        \"received\": \"已收到反馈意见！\",\n        \"thanks\": \"谢谢你让我们知道发生了什么。\",\n        \"more\": \"还有什么要说的吗？\",\n        \"tracking\": \"Mutiny 不会跟踪或监视你的行为，因此你的反馈会非常有用。\",\n        \"github\": \"我们不会对这些反馈做出回应。如果你需要支持，请\",\n        \"create_issue\": \"创建一个 GitHub issue。\",\n        \"link\": \"反馈？\",\n        \"feedback_placeholder\": \"错误、要求功能、反馈等。\",\n        \"info_label\": \"包括联系信息\",\n        \"info_caption\": \"如果你需要我们跟进这个问题\",\n        \"email\": \"电子邮箱\",\n        \"email_caption\": \"欢迎使用一次性邮箱\",\n        \"nostr\": \"Nostr\",\n        \"nostr_caption\": \"你最新的 npub\",\n        \"nostr_label\": \"Nostr npub 或 NIP-05\",\n        \"send_feedback\": \"发送反馈\",\n        \"invalid_feedback\": \"请说点什么吧！\",\n        \"need_contact\": \"我们需要与你联系的方式\",\n        \"invalid_email\": \"我看这不像是电子邮箱地址\",\n        \"error\": \"提交反馈时出错 {{error}}\",\n        \"try_again\": \"请稍后重试。\"\n    },\n    \"activity\": {\n        \"title\": \"动态\",\n        \"mutiny\": \"Mutiny\",\n        \"wallet\": \"钱包\",\n        \"nostr\": \"Nostr\",\n        \"view_all\": \"查看所有\",\n        \"receive_some_sats_to_get_started\": \"接收一点聪即可开始\",\n        \"channel_open\": \"通道开通\",\n        \"channel_close\": \"通道关闭\",\n        \"unknown\": \"未知\",\n        \"import_contacts\": \"从 nostr 导入通讯录，看看他们在打闪谁。\",\n        \"coming_soon\": \"即将推出\",\n        \"private\": \"私人\",\n        \"anonymous\": \"匿名\",\n        \"from\": \"来自：\",\n        \"transaction_details\": {\n            \"lightning_receive\": \"通过闪电接收\",\n            \"lightning_send\": \"通过闪电发送\",\n            \"channel_open\": \"通道开通\",\n            \"channel_close\": \"通道关闭\",\n            \"onchain_receive\": \"链上接收\",\n            \"onchain_send\": \"链上发送\",\n            \"paid\": \"已支付\",\n            \"unpaid\": \"未支付\",\n            \"status\": \"状态\",\n            \"date\": \"日期\",\n            \"tagged_to\": \"标签\",\n            \"description\": \"描述\",\n            \"fee\": \"费用\",\n            \"onchain_fee\": \"链上费用\",\n            \"invoice\": \"发票\",\n            \"payment_hash\": \"付款哈希值\",\n            \"payment_preimage\": \"原像\",\n            \"txid\": \"Txid\",\n            \"total\": \"要求金额\",\n            \"balance\": \"余额\",\n            \"reserve\": \"储备\",\n            \"peer\": \"对等方\",\n            \"channel_id\": \"通道 ID\",\n            \"reason\": \"原因\",\n            \"confirmed\": \"已确认\",\n            \"unconfirmed\": \"未确认\",\n            \"sweep_delay\": \"资金可能需要几天时间才能回到钱包中\",\n            \"no_details\": \"未找到通道详细信息，这意味着该通道可能已关闭。\",\n            \"back_home\": \"返回首页\"\n        },\n        \"start_a_chat\": \"开始聊天？\",\n        \"start_a_chat_are_you_sure\": \"该用户不在你的通讯录中。\",\n        \"federation_message\": \"联合会消息\"\n    },\n    \"scanner\": {\n        \"paste\": \"粘贴些什么\",\n        \"cancel\": \"取消\"\n    },\n    \"settings\": {\n        \"header\": \"设置\",\n        \"support\": \"了解如何支持 Mutiny\",\n        \"experimental_features\": \"试验\",\n        \"debug_tools\": \"调试工具\",\n        \"danger_zone\": \"危险地带\",\n        \"general\": \"一般\",\n        \"version\": \"版本：\",\n        \"admin\": {\n            \"title\": \"管理页面\",\n            \"caption\": \"我们的内部调试工具。明智使用！\",\n            \"header\": \"秘密调试工具\",\n            \"warning_one\": \"如果你知道自己在做什么，那你就来对地方了。\",\n            \"warning_two\": \"这些是我们用来调试和测试应用程序的内部工具。请谨慎使用！\",\n            \"kitchen_sink\": {\n                \"disconnect\": \"断连\",\n                \"peers\": \"对等方\",\n                \"no_peers\": \"没有对等方\",\n                \"refresh_peers\": \"刷新对等方\",\n                \"connect_peer\": \"连接对等方\",\n                \"expect_a_value\": \"期望值...\",\n                \"connect\": \"连接\",\n                \"close_channel\": \"关闭通道\",\n                \"force_close\": \"强制关闭通道\",\n                \"abandon_channel\": \"弃用通道\",\n                \"confirm_close_channel\": \"你确定要关闭Z这条通道吗？\",\n                \"confirm_force_close\": \"你确定要强制关闭Z这条通道吗？你的资金需要几天才能在链上赎回。\",\n                \"confirm_abandon_channel\": \"你确定要放弃这个通道吗？通常只有在开通交易永远无法确认的情况下才会这样做。否则，你将损失资金。\",\n                \"channels\": \"通道\",\n                \"no_channels\": \"没有通道\",\n                \"refresh_channels\": \"刷新通道\",\n                \"pubkey\": \"公钥\",\n                \"amount\": \"金额\",\n                \"open_channel\": \"开通通道\",\n                \"nodes\": \"节点\",\n                \"no_nodes\": \"没有节点\",\n                \"enable_zaps_to_hodl\": \"启用对 hodl 发票打闪？\",\n                \"zaps_to_hodl_desc\": \"对 hodl 发票打闪可能会导致通道强制关闭，从而产生高额链上费用。使用风险自负！\",\n                \"zaps_to_hodl_enable\": \"启用 hodl 打闪\",\n                \"zaps_to_hodl_disable\": \"禁用 hodl 打闪\"\n            }\n        },\n        \"backup\": {\n            \"title\": \"备份\",\n            \"secure_funds\": \"让我们确保这些资金的安全。\",\n            \"twelve_words_tip\": \"我们会给你看 12 个单词。请你写下这 12 个单词。\",\n            \"warning_one\": \"如果你清除了浏览器历史记录，或者丢失了设备，这 12 个字是你恢复钱包的唯一方法。\",\n            \"warning_two\": \"Mutiny 是自我保管钱包。都看你自己...\",\n            \"confirm\": \"我已经写下了这几个字\",\n            \"responsibility\": \"我明白我的资金由我自己负责\",\n            \"liar\": \"我不是为了了结此事而撒谎\",\n            \"seed_words\": {\n                \"reveal\": \"轻点显示种子单词\",\n                \"hide\": \"隐藏\",\n                \"copy\": \"危险地复制到剪贴板\",\n                \"copied\": \"已复制！\"\n            }\n        },\n        \"channels\": {\n            \"title\": \"闪电通道\",\n            \"outbound\": \"出账\",\n            \"inbound\": \"入账\",\n            \"reserve\": \"储备\",\n            \"have_channels\": \"你有\",\n            \"have_channels_one\": \"条闪电通道。\",\n            \"have_channels_many\": \"条闪电通道。\",\n            \"inbound_outbound_tip\": \"出账是你可以在闪电上花费的金额。入账是指在不产生闪电服务费的情况下，你可以收到的金额。\",\n            \"reserve_tip\": \"你的闪电通道余额中约有1%是为支付费用预留的储备。通过兑换开通的通道需要额外储备。\",\n            \"no_channels\": \"看起来你还没有任何通道。要开始使用，可以通过闪电接收一些聪，或将一些链上资金换成一条通道。开始动手吧！\",\n            \"close_channel\": \"关闭\",\n            \"online_channels\": \"在线通道\",\n            \"offline_channels\": \"离线通道\",\n            \"close_channel_confirm\": \"关闭该通道会将余额转移到链上，并产生链上费用。\",\n            \"force_close_channel_confirm\": \"此通道已下线。强制关闭此通道会将余额转移到链上，产生链上费用，并可能需要几天时间结算。\"\n        },\n        \"connections\": {\n            \"title\": \"钱包连接\",\n            \"error_name\": \"名称不能为空\",\n            \"error_connection\": \"创建钱包连接失败\",\n            \"error_budget_zero\": \"预算必须大于零\",\n            \"add_connection\": \"添加连接\",\n            \"manage_connections\": \"管理连接\",\n            \"manage_gifts\": \"管理红包\",\n            \"delete_connection\": \"删除\",\n            \"new_connection\": \"新增连接\",\n            \"edit_connection\": \"编辑连接\",\n            \"new_connection_label\": \"名称\",\n            \"new_connection_placeholder\": \"我最爱的 nostr 客户端...\",\n            \"create_connection\": \"创建连接\",\n            \"save_connection\": \"保存更改\",\n            \"edit_budget\": \"编辑预算\",\n            \"open_app\": \"打开应用程序\",\n            \"open_in_nostr_client\": \"在 Nostr 客户端中打开\",\n            \"open_in_primal\": \"在 Primal 中打开\",\n            \"nostr_client_not_found\": \"没找到 Nostr 客户端\",\n            \"client_not_found_description\": \"安装 Primal、Amethyst 或达摩等 nostr 客户端打开此链接。\",\n            \"relay\": \"中继器\",\n            \"authorize\": \"授权外部服务从你的钱包中要求付款。与 Nostr 客户端完美搭配。\",\n            \"pending_nwc\": {\n                \"title\": \"待处理要求\",\n                \"approve_all\": \"全部批准\",\n                \"deny_all\": \"全部拒绝\"\n            },\n            \"careful\": \"请小心共享此连接！预算范围内的要求将自动支付。\",\n            \"spent\": \"支出\",\n            \"remaining\": \"剩余\",\n            \"confirm_delete\": \"你确定要删除此连接吗？\",\n            \"budget\": \"预算\",\n            \"resets_every\": \"重置于每\",\n            \"resubscribe_date\": \"重新订阅于\",\n            \"disable_connection\": \"停用\",\n            \"disabled\": \"此订阅已停用。目前你还无法重新启用订阅，但我们正在努力解决这个问题。请尽快回来查看！\",\n            \"disable_connection_confirm\": \"你确定要停用订阅吗？目前还无法重新启用订阅，我们正在努力解决这个问题。\"\n        },\n        \"emergency_kit\": {\n            \"title\": \"应急箱\",\n            \"caption\": \"诊断并解决钱包问题。\",\n            \"emergency_tip\": \"如果你的钱包似乎坏了，这里有一些工具可以尝试调试和修复。\",\n            \"questions\": \"如果你对这些按钮的作用有任何疑问，请\",\n            \"link\": \"向我们寻求支持。\",\n            \"import_export\": {\n                \"title\": \"导出钱包状态\",\n                \"error_password\": \"需要密码\",\n                \"error_read_file\": \"文件读取错误\",\n                \"error_no_text\": \"文件中没有文本\",\n                \"tip\": \"你可以将整个 Mutiny 钱包状态导出到文件，然后导入到新的浏览器中。通常都能成功！\",\n                \"caveat_header\": \"重要注意事项：\",\n                \"caveat\": \"导出后不要在原浏览器中进行任何操作。如果进行了操作，就需要重新导出。导入成功后，最佳做法是清除原浏览器的状态，以确保不会产生冲突。\",\n                \"save_state\": \"将状态保存为文件\",\n                \"import_state\": \"从文件导入状态\",\n                \"confirm_replace\": \"你是否想用这个来代替你的钱包状态？\",\n                \"password\": \"输入密码解密\",\n                \"decrypt_wallet\": \"解密钱包\",\n                \"decrypt_export\": \"输入密码保存\"\n            },\n            \"logs\": {\n                \"title\": \"下载调试日志\",\n                \"something_screwy\": \"有什么不对劲的地方吗？查看日志！\",\n                \"download_logs\": \"下载日志\",\n                \"password\": \"输入密码解密\",\n                \"confirm_password_label\": \"确认密码\"\n            },\n            \"delete_everything\": {\n                \"delete\": \"删除一切\",\n                \"confirm\": \"这将删除你的节点的状态。此操作无法撤销！\",\n                \"deleted\": \"已删除\",\n                \"deleted_description\": \"删除所有数据\"\n            }\n        },\n        \"encrypt\": {\n            \"title\": \"安全\",\n            \"caption\": \"先备份才能解锁加密\",\n            \"header\": \"加密你的种子词\",\n            \"hot_wallet_warning\": \"Mutiny 是一个“热钱包”，因此它需要你的种子单词才能运行，但你也可以选择用密码来加密这些单词。\",\n            \"password_tip\": \"这样，即使有人访问了你的浏览器，也无法访问你的资金。\",\n            \"optional\": \"（可选）\",\n            \"existing_password\": \"现有密码\",\n            \"existing_password_caption\": \"如果尚未设置密码，请留空。\",\n            \"new_password_label\": \"密码\",\n            \"new_password_placeholder\": \"输入密码\",\n            \"new_password_caption\": \"该密码将用于加密你的种子词。如果你忘记了密码，就需要重新输入你的种子词才能使用你的资金。你写下了你的种子词，对吗？\",\n            \"confirm_password_label\": \"确认密码\",\n            \"confirm_password_placeholder\": \"输入相同的密码\",\n            \"encrypt\": \"加密\",\n            \"skip\": \"跳过\",\n            \"error_match\": \"密码不符\",\n            \"error_same_as_existingpassword\": \"新密码必须与现有密码不一致\"\n        },\n        \"decrypt\": {\n            \"title\": \"输入你的密码\",\n            \"decrypt_wallet\": \"解密钱包\",\n            \"forgot_password_link\": \"忘记密码？\",\n            \"error_wrong_password\": \"密码无效\"\n        },\n        \"currency\": {\n            \"title\": \"货币\",\n            \"caption\": \"选择首选货币对\",\n            \"select_currency\": \"选择货币\",\n            \"select_currency_label\": \"货币对\",\n            \"select_currency_caption\": \"选择新货币将重新同步钱包以获取价格更新\",\n            \"request_currency_support_link\": \"要求支持更多货币\",\n            \"error_unsupported_currency\": \"请选择一种支持的货币。\"\n        },\n        \"language\": {\n            \"title\": \"语言\",\n            \"caption\": \"选择你的语言\",\n            \"select_language\": \"选择语言\",\n            \"select_language_label\": \"语言\",\n            \"select_language_caption\": \"选择新货币将更改钱包语言，并忽略当前浏览器语言\",\n            \"request_language_support_link\": \"要求支持更多语言\",\n            \"error_unsupported_language\": \"请选择一个已支持的语言。\"\n        },\n        \"lnurl_auth\": {\n            \"title\": \"LNURL 认证\",\n            \"auth\": \"认证\",\n            \"expected\": \"期待类似 LNURL 的东西...\"\n        },\n        \"plus\": {\n            \"title\": \"Mutiny+\",\n            \"join\": \"加入\",\n            \"sats_per_month\": \"，每月 {{amount}} 聪。\",\n            \"lightning_balance\": \"你的闪电余额中至少需要 {{amount}} 聪才能开始使用。先试后买！\",\n            \"restore\": \"恢复订阅\",\n            \"ready_to_join\": \"准备好了加入\",\n            \"click_confirm\": \"点击确认支付第一个月的费用。\",\n            \"open_source\": \"Mutiny 是开放源代码，可自行托管。\",\n            \"optional_pay\": \"但你也可以付费。\",\n            \"paying_for\": \"订阅\",\n            \"supports_dev\": \"有助于支持正在进行的开发工作，并能让你尽早享受新功能和高级功能：\",\n            \"thanks\": \"你已成为 Mutiny 的一员！享受以下福利:\",\n            \"renewal_time\": \"你将收到一份续订付款申请于\",\n            \"cancel\": \"要取消订阅，只需不付费即可。你还可以停用 Mutiny+ 的\",\n            \"wallet_connection\": \"钱包连接。\",\n            \"subscribe\": \"订阅\",\n            \"error_no_plan\": \"未找到订阅计划\",\n            \"error_failure\": \"无法订阅\",\n            \"error_no_subscription\": \"未找到现有订阅\",\n            \"error_expired_subscription\": \"你的订阅已过期，请单击“加入”续订\",\n            \"satisfaction\": \"自鸣得意的满足感\",\n            \"gifting\": \"红包功能\",\n            \"multi_device\": \"多设备访问\",\n            \"ios_testflight\": \"iOS TestFlight 访问权限\",\n            \"more\": \"... 还有更多\",\n            \"cta_description\": \"提前享受新功能和高级功能。\",\n            \"cta_but_already_plus\": \"感谢你的支持！\",\n            \"lightning_address\": \"你自己的闪电地址\"\n        },\n        \"restore\": {\n            \"title\": \"恢复\",\n            \"all_twelve\": \"你必须输入全部 12 个单词\",\n            \"wrong_word\": \"错别字\",\n            \"paste\": \"危险地从剪贴板粘贴\",\n            \"confirm_text\": \"你确定要恢复到此钱包吗？你现有的钱包将被删除！\",\n            \"restore_tip\": \"你可以使用 12 个单词的种子短语恢复现有的 Mutiny 钱包。这将代替你现有的钱包，因此请确保你知道自己在做什么！\",\n            \"multi_browser_warning\": \"请勿同时在多个浏览器上使用。\",\n            \"error_clipboard\": \"不支持剪贴板\",\n            \"error_word_number\": \"字数不对\",\n            \"error_invalid_seed\": \"种子短语无效\"\n        },\n        \"servers\": {\n            \"title\": \"服务器\",\n            \"caption\": \"不要信赖我们！使用你自己的服务器运行 Mutiny。\",\n            \"link\": \"了解有关自助托管的更多信息\",\n            \"proxy_label\": \"Websockets 代理\",\n            \"proxy_caption\": \"你的闪电节点与网络其他部分的通信方式。\",\n            \"error_proxy\": \"应该是以 wss:// 开头的网址\",\n            \"esplora_label\": \"Esplora\",\n            \"esplora_caption\": \"链上信息的区块数据。\",\n            \"error_esplora\": \"这看起来不像是一个 URL\",\n            \"rgs_label\": \"RGS\",\n            \"rgs_caption\": \"快速流言同步（Rapid Gossip Sync）。用于路由选择的闪电网络数据。\",\n            \"error_rgs\": \"这看起来不像是一个 URL\",\n            \"lsp_label\": \"LSP\",\n            \"lsp_caption\": \"闪电服务提供商（Lightning Service Provider）。自动为你打开入账流动性通道。还可保护发票隐私。\",\n            \"lsps_connection_string_label\": \"LSPS 连接字符串\",\n            \"lsps_connection_string_caption\": \"闪电服务提供商(Lightning Service Provider)。自动为你打开入账流动性通道。使用 LSP 规范。\",\n            \"error_lsps_connection_string\": \"这看起来不像是节点连接字符串\",\n            \"lsps_token_label\": \"LSPS 令牌\",\n            \"lsps_token_caption\": \"LSPS 令牌。 用于识别连接到 LSP 的钱包\",\n            \"lsps_valid_error\": \"你可以只使用 LSP 设置，也可以使用 LSPS 连接字符串和 LSPS 令牌设置，但不能同时使用两种设置。\",\n            \"error_lsps_token\": \"这看起来不像是一个有效的令牌\",\n            \"storage_label\": \"存储\",\n            \"storage_caption\": \"加密 VSS 备份服务。\",\n            \"error_lsp\": \"这看起来不像是一个 URL\",\n            \"error_tor\": \"目前不支持 Tor URL\",\n            \"save\": \"保存\"\n        },\n        \"nostr_contacts\": {\n            \"title\": \"同步 Nostr 通讯录\",\n            \"npub_label\": \"Nostr npub\",\n            \"npub_required\": \"Npub 不能为空\",\n            \"sync\": \"同步\",\n            \"resync\": \"重新同步\",\n            \"remove\": \"移除\"\n        },\n        \"manage_federations\": {\n            \"title\": \"管理联合会\",\n            \"federation_code_label\": \"联合会代码\",\n            \"federation_code_required\": \"联合会代码不能为空\",\n            \"federation_added_success\": \"已成功添加联合会\",\n            \"federation_remove_confirm\": \"你是否确定要移除这个联合会？请先确保你的资金已转入闪电余额或其他钱包。\",\n            \"add\": \"添加\",\n            \"remove\": \"离开联合会\",\n            \"expires\": \"到期\",\n            \"federation_id\": \"联合会 ID\",\n            \"description\": \"联合会是基于比特币的网络，使比特币的使用变得更便宜、更快捷、更方便。\",\n            \"learn_more\": \"了解更多\",\n            \"discover\": \"发现联合会\",\n            \"manual\": \"邀请码\",\n            \"created_at\": \"创建于\",\n            \"recommended_by\": \"推荐人\",\n            \"already_in_fed\": \"你已经加入了一个联合会！\",\n            \"descriptionpart2\": \"每个联合会都由一组不同的个人或公司运营。请在下面查找一个你或你的朋友们可能信任的联合会。\",\n            \"join_me\": \"加入我\",\n            \"recommend\": \"推荐联合会\",\n            \"recommended_by_you\": \"你的推荐\",\n            \"transfer_funds\": \"转账资金\",\n            \"transfer_funds_message\": \"添加第二个联合会以实现转账。\"\n        },\n        \"gift\": {\n            \"give_sats_link\": \"赠送红包\",\n            \"something_went_wrong\": \"出了问题\",\n            \"title\": \"红包\",\n            \"no_plus_caption\": \"升级到 Mutiny+，即可赠送红包\",\n            \"receive_too_small\": \"你的首次接收必须是 {{amount}} 聪或更高。\",\n            \"setup_fee_lightning\": \"接收此红包将收取闪电设置费。\",\n            \"already_claimed\": \"该红包已被认领\",\n            \"sender_is_poor\": \"付款方没有足够的余额来支付这份红包。\",\n            \"sender_timed_out\": \"红包付款超时。送礼人可能不在线，或该红包已被领取。\",\n            \"sender_generic_error\": \"付款方发送错误：{{error}}\",\n            \"receive_header\": \"你收到了一个红包！\",\n            \"receive_description\": \"你一定很特别。只需点击大按钮，即可领取你的红包。下次送礼人在线时，资金就会纳入这个钱包。\",\n            \"receive_claimed\": \"红包已领取！你应该很快就能看到红包存入你的余额。\",\n            \"receive_cta\": \"领取红包\",\n            \"receive_try_again\": \"重试\",\n            \"send_header\": \"创建红包\",\n            \"send_explainer\": \"赠送红包。创建一个 Mutiny 红包链接，任何人都可以通过网络浏览器领取。\",\n            \"send_name_required\": \"供你存档\",\n            \"send_name_label\": \"收款方名称\",\n            \"send_header_claimed\": \"红包已收到！\",\n            \"send_claimed\": \"你的红包已被认领。感谢你的分享。\",\n            \"send_sharable_header\": \"可分享的链接\",\n            \"send_instructions\": \"将这个红包链接复制给收款方，或请他们用钱包扫描这个二维码。\",\n            \"send_another\": \"再创一个\",\n            \"send_small_warning\": \"全新的 Mutiny 用户无法兑换少于10万聪。\",\n            \"send_cta\": \"创建红包\",\n            \"send_delete_button\": \"删除红包\",\n            \"send_delete_confirm\": \"你确定要删除这份红包吗？这是你的“拉毯时刻”吗？\",\n            \"send_tip\": \"你的 Mutiny 钱包需要打开才能兑换红包。\",\n            \"need_plus\": \"升级到 Mutiny+ 以启用红包功能。红包允许你创建一个 Mutiny 红包链接，任何人都可以通过网络浏览器领取。\"\n        },\n        \"appearance\": \"外观\",\n        \"social\": \"社交\",\n        \"nostr_keys\": {\n            \"title\": \"Nostr 密钥\",\n            \"caption\": \"不是你的密钥，就不是你的笔记\",\n            \"description\": \"你可以在多个应用程序中使用同一个 nostr 个人档案。\",\n            \"warning\": \"谨慎分享 nostr 私人密钥！\",\n            \"learn_more\": \"了解更多关于 nostr 的信息\",\n            \"import_profile\": \"导入 nostr 个人档案\",\n            \"delete_account\": \"删除帐户\",\n            \"delete_account_confirm\": \"删除 nostr 个人档案无法撤销，并且会禁用钱包的某些社交功能。\",\n            \"unlink_account\": \"断连 nostr 档案\",\n            \"unlink_account_confirm\": \"断开对 nostr 档案的连接会将钱包重置为默认的 nostr 个人档案。\",\n            \"delete_account_confirm_scary\": \"此个人档案将在所有 nostr 应用程序中被删除。\"\n        },\n        \"lightning_address\": {\n            \"title\": \"Mutiny 地址\",\n            \"description\": \"使用自己的闪电地址向钱包获取打闪和付款。\",\n            \"create\": \"创建 Mutiny 地址\",\n            \"add_a_federation\": \"要启用此功能，你必须是一个联合会的成员。\",\n            \"ios_warning\": \"Mutiny 地址需要 Mutiny+ 订阅，该应用无法购买。\"\n        }\n    },\n    \"swap\": {\n        \"peer_not_found\": \"未找到对等方\",\n        \"channel_too_small\": \"让通道小于 {{amount}} 聪实在是太傻了。\",\n        \"insufficient_funds\": \"你没有足够的资金来创建这条通道\",\n        \"header\": \"兑换到闪电\",\n        \"initiated\": \"已启动兑换\",\n        \"sats_added\": \"+{{amount}} 聪将被添加到你的闪电余额中\",\n        \"use_existing\": \"使用现有对等方\",\n        \"choose_peer\": \"选择对等方\",\n        \"peer_connect_label\": \"连接新的对等方\",\n        \"peer_connect_placeholder\": \"对等方连接字符串\",\n        \"connect\": \"连接\",\n        \"connecting\": \"正在连接...\",\n        \"confirm_swap\": \"确认兑换\"\n    },\n    \"swap_lightning\": {\n        \"insufficient_funds\": \"你没有足够的资金兑换成闪电\",\n        \"header\": \"兑换到闪电\",\n        \"header_preview\": \"预览兑换\",\n        \"completed\": \"交换已完成\",\n        \"too_small\": \"输入的金额无效。你必须交换至少10万聪。\",\n        \"sats_added\": \"+{{amount}} 聪已被添加到你的闪电余额中\",\n        \"sats_fee\": \"+{{amount}} 聪费用\",\n        \"confirm_swap\": \"确认兑换\",\n        \"preview_swap\": \"预览兑换费用\"\n    },\n    \"reload\": {\n        \"mutiny_update\": \"Mutiny 更新\",\n        \"new_version_description\": \"新版 Mutiny 已缓存，重新加载即可开始使用。\",\n        \"reload\": \"重新加载\"\n    },\n    \"error\": {\n        \"title\": \"错误\",\n        \"emergency_link\": \"应急箱。\",\n        \"reload\": \"重新加载\",\n        \"restart\": {\n            \"title\": \"发生了什么*额外*糟糕的事情？停止节点！\",\n            \"start\": \"开始\",\n            \"stop\": \"停止\"\n        },\n        \"general\": {\n            \"oh_no\": \"哦，不！\",\n            \"never_should_happen\": \"这本不该发生\",\n            \"try_reloading\": \"请尝试重新加载此页面或点击“该死”按钮。如果你仍然遇到问题，\",\n            \"support_link\": \"向我们寻求支持。\",\n            \"getting_desperate\": \"绝望了吗？试试\"\n        },\n        \"load_time\": {\n            \"stuck\": \"卡在这个屏幕上？尝试重新加载。如果还不行，请查看\"\n        },\n        \"not_found\": {\n            \"title\": \"找不到\",\n            \"wtf_paul\": \"这大概是 Paul 的错。\"\n        },\n        \"resync\": {\n            \"incorrect_balance\": \"链上余额似乎不正确？尝试重新同步链上钱包。\",\n            \"resync_wallet\": \"重新同步钱包\"\n        },\n        \"on_boot\": {\n            \"existing_tab\": {\n                \"title\": \"检测到多个选项卡\",\n                \"description\": \"Mutiny 一次只能在一个选项卡中使用。看起来你打开了另一个运行 Mutiny 的选项卡。请关闭该选项卡并刷新此页面，或关闭此选项卡并刷新另一个选项卡。\"\n            },\n            \"already_running\": {\n                \"title\": \"Mutiny 可能在其他设备上运行\",\n                \"description\": \"Mutiny 一次只能在一个地方使用。看起来你有其他设备或浏览器在使用这个钱包。如果你最近在其他设备上关闭了 Mutiny，请稍等片刻再试。\",\n                \"retry_again_in\": \"重试于\",\n                \"seconds\": \"秒后\"\n            },\n            \"incompatible_browser\": {\n                \"title\": \"浏览器不兼容\",\n                \"header\": \"检测到浏览器不兼容\",\n                \"description\": \"Mutiny 需要一个支持 WebAssembly、LocalStorage 和 IndexedDB 的现代浏览器。有些浏览器在私人模式下会禁用这些功能。\",\n                \"try_different_browser\": \"请确保你的浏览器支持所有这些功能，或者考虑尝试其他浏览器。你也可以尝试禁用某些阻止这些功能的扩展或 “屏蔽”。\",\n                \"browser_storage\": \"(我们很乐意支持更多的隐私性浏览器，但我们必须将你的钱包数据保存到浏览器存储中，否则你将丢失资金。）\",\n                \"browsers_link\": \"支持的浏览器\"\n            },\n            \"loading_failed\": {\n                \"title\": \"加载失败\",\n                \"header\": \"Mutiny 加载失败\",\n                \"description\": \"启动 Mutiny 钱包时出了问题。\",\n                \"repair_options\": \"如果你的钱包似乎坏了，这里有一些工具可以尝试调试和修复它。\",\n                \"questions\": \"如果你对这些按钮的作用有任何疑问，请\",\n                \"support_link\": \"向我们寻求支持。\",\n                \"services_down\": \"看来 Mutiny 的某项服务出现故障。请稍后再试。\",\n                \"in_the_meantime\": \"在此期间，如果你想使用你的链上资金，可以将 Mutiny 载入\",\n                \"safe_mode\": \"安全模式\"\n            }\n        }\n    },\n    \"modals\": {\n        \"share\": \"分享\",\n        \"details\": \"详情\",\n        \"loading\": {\n            \"loading\": \"正在加载：{{stage}}\",\n            \"default\": \"刚刚起步\",\n            \"double_checking\": \"正在重复检查\",\n            \"existing_wallet\": \"检查现有钱包\",\n            \"downloading\": \"正在下载\",\n            \"setup\": \"设置\",\n            \"done\": \"完成\"\n        },\n        \"onboarding\": {\n            \"welcome\": \"欢迎！\",\n            \"setup\": \"设置\",\n            \"restore_from_backup\": \"如果你以前使用过 Mutiny，可以从备份中恢复。否则，你可以跳过这一步，尽情享受你的新钱包！\",\n            \"not_available\": \"我们还没做到这一点\",\n            \"secure_your_funds\": \"确保你的资金安全\"\n        },\n        \"more_info\": {\n            \"whats_with_the_fees\": \"这些费用是怎么回事？\",\n            \"self_custodial\": \"Mutiny 是一个自我保管钱包。要启动闪电支付，我们必须开通闪电通道，这需要最低金额和设置费。\",\n            \"future_payments\": \"未来的付款（包括发送和接收）将只产生正常的网络费用和象征性的服务费，除非你的通道已耗尽入账容量。\",\n            \"liquidity\": \"进一步了解流动性\",\n            \"fedimint\": \"如果你加入了一个联合会，则不需要缴纳设置费或呈现最低金额限制。\"\n        },\n        \"confirm_dialog\": {\n            \"are_you_sure\": \"你确定吗？\",\n            \"cancel\": \"取消\",\n            \"confirm\": \"确认\"\n        },\n        \"lnurl_auth\": {\n            \"auth_request\": \"认证要求\",\n            \"login\": \"登录\",\n            \"decline\": \"拒绝\",\n            \"error\": \"不知道为什么，这招不管用。\",\n            \"authenticated\": \"已认证！\"\n        }\n    },\n    \"setup\": {\n        \"new_profile\": {\n            \"description\": \"Mutiny 使支付成为一种社交。\",\n            \"title\": \"创建你的个人档案\"\n        },\n        \"skip\": \"暂时跳过\",\n        \"import_profile\": \"导入现有的 nostr 档案\",\n        \"import\": {\n            \"title\": \"导入 nostr 档案\",\n            \"description\": \"使用现有的 nostr 帐户登录。\"\n        },\n        \"federation\": {\n            \"pick\": \"选择一个联合会\",\n            \"skip_confirm\": \"使用 Mutiny 需要支付链上费和通道费，而且会错过闪电地址等高级功能。你可以稍后在设置中改变主意。\"\n        }\n    },\n    \"utils\": {\n        \"hours_future_one\": \"一小时后\",\n        \"hours_future_other\": \"{{count}} 小时后\",\n        \"hours_past_one\": \"一小时前\",\n        \"hours_past_other\": \"{{count}} 小时前\",\n        \"hours_short\": \"{{count}}h\",\n        \"days_future_one\": \"一天后\",\n        \"days_future_other\": \"{{count}} 天后\",\n        \"days_past_one\": \"一天前\",\n        \"days_past_other\": \"{{count}} 天前\",\n        \"days_short\": \"{{count}}d\",\n        \"minutes_future_one\": \"一分钟后\",\n        \"minutes_future_other\": \"{{count}} 分钟后\",\n        \"minutes_past_one\": \"一分钟前\",\n        \"minutes_past_other\": \"{{count}} 分钟前\",\n        \"minutes_short\": \"{{count}}m\",\n        \"nowish\": \"现在\",\n        \"seconds_future\": \"几秒钟后\",\n        \"seconds_past\": \"刚刚\"\n    },\n    \"transfer\": {\n        \"completed\": \"转账已完成\",\n        \"sats_moved\": \"+{{amount}} 聪已被转账至 {{federation_name}}\",\n        \"confirm\": \"确认转账\",\n        \"title\": \"转账资金\"\n    }\n}\n"
  },
  {
    "path": "public/i18n/hant.json",
    "content": "{\n    \"common\": {\n        \"title\": \"Mutiny 錢包\",\n        \"mutiny\": \"Mutiny\",\n        \"nice\": \"不錯\",\n        \"home\": \"首頁\",\n        \"e_sats\": \"e聰\",\n        \"e_sat\": \"e聰\",\n        \"sats\": \"聰\",\n        \"sat\": \"聰\",\n        \"scan\": \"掃描\",\n        \"fee\": \"費用\",\n        \"send\": \"發送\",\n        \"receive\": \"接收\",\n        \"request\": \"要求\",\n        \"dangit\": \"該死\",\n        \"back\": \"返回\",\n        \"coming_soon\": \"（即將推出）\",\n        \"copy\": \"複製\",\n        \"copied\": \"已複製\",\n        \"continue\": \"繼續\",\n        \"error_unimplemented\": \"尚未實施\",\n        \"why\": \"爲什麼？\",\n        \"private_tags\": \"私人標籤\",\n        \"view_transaction\": \"查看交易\",\n        \"view_payment_details\": \"查看付款詳情\",\n        \"pending\": \"待定\",\n        \"error_safe_mode\": \"Mutiny 正在安全模式下運行。閃電已禁用。\",\n        \"self_hosted\": \"自託管\",\n        \"expires\": \"過期時間爲 {{time}}\"\n    },\n    \"home\": {\n        \"receive\": \"首次收款\",\n        \"find\": \"在 nostr 上查找你的朋友\",\n        \"backup\": \"確保你的資金安全！\",\n        \"connection\": \"創建錢包連接\",\n        \"connection_edit\": \"編輯錢包連接\",\n        \"federation\": \"加入聯合會\",\n        \"subnav\": {\n            \"just_me\": \"本人\",\n            \"friends\": \"朋友\",\n            \"requests\": \"要求\"\n        }\n    },\n    \"profile\": {\n        \"profile\": \"個人檔案\",\n        \"nostr_identity\": \"Nostr 身份\",\n        \"add_lightning_address\": \"添加閃電地址\",\n        \"edit_profile\": \"編輯個人檔案\",\n        \"join_federation\": \"加入聯合會\",\n        \"manage_federation\": \"管理聯合會\",\n        \"federated_custody\": \"聯合式保管\",\n        \"self_custody\": \"自行保管\",\n        \"social\": \"社交\",\n        \"edit\": {\n            \"nym\": \"暱稱\"\n        },\n        \"deleted\": \"你的 nostr 個人檔案已被刪除。\",\n        \"no_lightning_address\": \"尚未設置閃電地址\",\n        \"pay_me\": \"給我發聰\"\n    },\n    \"chat\": {\n        \"prompt\": \"這是一次新的對話。試試要錢吧！\",\n        \"placeholder\": \"消息\"\n    },\n    \"contacts\": {\n        \"new\": \"新增\",\n        \"add_contact\": \"添加聯繫人\",\n        \"new_contact\": \"新聯繫人\",\n        \"create_contact\": \"創建聯繫人\",\n        \"edit_contact\": \"編輯聯繫人\",\n        \"save_contact\": \"保存聯繫人\",\n        \"delete\": \"刪除\",\n        \"confirm_delete\": \"你確定要刪除這個聯繫人嗎？\",\n        \"payment_history\": \"付款歷史\",\n        \"no_payments\": \"尚未付款\",\n        \"edit\": \"編輯\",\n        \"pay\": \"支付\",\n        \"name\": \"名稱\",\n        \"ln_address\": \"閃電地址\",\n        \"placeholder\": \"中本聰\",\n        \"lightning_address\": \"閃電地址\",\n        \"unimplemented\": \"尚未實施\",\n        \"not_available\": \"我們還沒做到這一點\",\n        \"error_name\": \"我們至少需要一個名稱\",\n        \"email_error\": \"這看起來不像是個閃電地址\",\n        \"npub_error\": \"這看起來不像是個 nostr npub\",\n        \"error_ln_address_missing\": \"新聯繫人需要閃電地址\",\n        \"npub\": \"Nostr Npub\"\n    },\n    \"redeem\": {\n        \"redeem_bitcoin\": \"兌現比特幣\",\n        \"lnurl_amount_message\": \"輸入 {{min}} 至 {{max}} 聰之間的取款金額\",\n        \"lnurl_redeem_failed\": \"取款失敗\",\n        \"lnurl_redeem_success\": \"已收到付款\"\n    },\n    \"request\": {\n        \"request_bitcoin\": \"要求比特幣\",\n        \"request\": \"要求\"\n    },\n    \"receive\": {\n        \"receive_bitcoin\": \"接收比特幣\",\n        \"edit\": \"編輯\",\n        \"checking\": \"正在檢查\",\n        \"choose_format\": \"選擇格式\",\n        \"payment_received\": \"已收到付款\",\n        \"payment_initiated\": \"已啓動付款\",\n        \"receive_add_the_sender\": \"將付款方添加到通訊錄\",\n        \"keep_mutiny_open\": \"保持 Mutiny 開啓以完成付款。\",\n        \"choose_payment_format\": \"選擇付款方式\",\n        \"lightning_label\": \"閃電發票\",\n        \"lightning_caption\": \"適合小額交易。費用通常低於鏈上交易。\",\n        \"onchain_label\": \"比特幣地址\",\n        \"onchain_caption\": \"鏈上交易，就像中本聰所做的那樣。適合大額交易。\",\n        \"lightning_setup_fee\": \"此接收將收取 {{amount}} 聰的閃電設置費。\",\n        \"amount\": \"金額\",\n        \"fee\": \"+ 費用\",\n        \"total\": \"總額\",\n        \"spendable\": \"可支出\",\n        \"channel_size\": \"通道大小\",\n        \"channel_reserve\": \"- 通道儲備\",\n        \"error_under_min_lightning\": \"默認爲鏈上。首次閃電接收的金額太小。\",\n        \"error_creating_unified\": \"默認爲鏈上。創建閃電發票時出了問題。\",\n        \"error_creating_address\": \"創建鏈上地址時出了問題。\",\n        \"amount_editable\": {\n            \"receive_too_small\": \"你的首次接收必須是 {{amount}} 聰或更高。\",\n            \"setup_fee_lightning\": \"將收取閃電設置費。\",\n            \"more_than_21m\": \"比特幣只有 2100 萬個。\",\n            \"set_amount\": \"設置金額\",\n            \"max\": \"最高\",\n            \"fix_amounts\": {\n                \"ten_k\": \"1萬\",\n                \"one_hundred_k\": \"10萬\",\n                \"one_million\": \"1百萬\"\n            },\n            \"del\": \"DEL\",\n            \"balance\": \"餘額\"\n        },\n        \"integrated_qr\": {\n            \"onchain\": \"鏈上\",\n            \"lightning\": \"閃電\",\n            \"gift\": \"閃電紅包\"\n        },\n        \"remember_choice\": \"下次記住我的選擇\",\n        \"what_for\": \"這是派什麼用的？\",\n        \"method_help\": {\n            \"title\": \"接收方式\",\n            \"body\": \"閃電接收將自動存入你選擇的聯合會。如果你想要的話，可以以後兌換成自我保管。\"\n        },\n        \"receive_strings_error\": \"生成發票或鏈上地址時出了問題。\",\n        \"error_under_min_onchain\": \"這還不到灰塵的上限！鏈上交易應該更大。\",\n        \"warning_on_chain_fedi\": \"鏈上 fedimint 存款需要 10 次確認。尚未支持 RBF。\",\n        \"warning_address_reuse\": \"只能向該地址發送一次！\"\n    },\n    \"send\": {\n        \"search\": {\n            \"placeholder\": \"名稱、地址、發票\",\n            \"paste\": \"貼上\",\n            \"contacts\": \"通訊錄\",\n            \"global_search\": \"全球搜索\",\n            \"no_results\": \"未找到相關結果\"\n        },\n        \"sending\": \"正在發送...\",\n        \"confirm_send\": \"確認發送\",\n        \"contact_placeholder\": \"將收款方添加到通訊錄\",\n        \"start_over\": \"重新開始\",\n        \"send_bitcoin\": \"發送比特幣\",\n        \"paste\": \"貼上\",\n        \"scan_qr\": \"掃描二維碼\",\n        \"payment_initiated\": \"已啓動付款\",\n        \"payment_sent\": \"付款已發送\",\n        \"destination\": \"目的地\",\n        \"no_payment_info\": \"無付款信息\",\n        \"progress_bar\": {\n            \"of\": \"中的\",\n            \"sats_sent\": \"聰已支付\"\n        },\n        \"what_for\": \"這是派什麼用的？\",\n        \"zap_note\": \"打閃筆記\",\n        \"error_low_balance\": \"我們沒有足夠的餘額來支付指定金額。\",\n        \"error_invoice_match\": \"要求的 {{amount}} 聰金額不等於設定的金額。\",\n        \"error_channel_reserves\": \"可用資金不足。\",\n        \"error_address\": \"無效閃電地址\",\n        \"error_channel_reserves_explained\": \"你的通道餘額中有一部分是爲支付費用預留的儲備。嘗試發送較小的金額或添加資金。\",\n        \"error_clipboard\": \"不支持剪貼板\",\n        \"error_keysend\": \"Keysend 失敗了\",\n        \"error_LNURL\": \"LNURL Pay 失敗了\",\n        \"error_expired\": \"發票已過期\",\n        \"payjoin_send\": \"這是一個 payjoin！在隱私權得到改善之前，Mutiny 仍將繼續\",\n        \"payment_pending\": \"付款待定\",\n        \"payment_pending_description\": \"雖然需要一些時間，但這筆付款仍有可能通過。請查看『動態』，瞭解當前狀態。\",\n        \"hodl_invoice_warning\": \"這是一張 hodl 發票。向 hodl 發票付款可能會導致通道強制關閉。從而產生高額鏈上費用。請自行承擔支付風險！\",\n        \"private\": \"私人\",\n        \"publiczap\": \"公開打閃\",\n        \"privatezap\": \"私人打閃\"\n    },\n    \"feedback\": {\n        \"header\": \"給我們反饋！\",\n        \"received\": \"已收到反饋意見！\",\n        \"thanks\": \"謝謝你讓我們知道發生了什麼。\",\n        \"more\": \"還有什麼要說的嗎？\",\n        \"tracking\": \"Mutiny 不會跟蹤或監視你的行爲，因此你的反饋會非常有用。\",\n        \"github\": \"我們不會對這些反饋作出回應。如果你需要支持，請\",\n        \"create_issue\": \"創建一個 GitHub issue.\",\n        \"link\": \"反饋？\",\n        \"feedback_placeholder\": \"錯誤、要求功能、反饋等。\",\n        \"info_label\": \"包括聯繫信息\",\n        \"info_caption\": \"如果你需要我們跟進這個問題\",\n        \"email\": \"電子郵箱\",\n        \"email_caption\": \"歡迎使用一次性郵箱\",\n        \"nostr\": \"Nostr\",\n        \"nostr_caption\": \"你最新的 npub\",\n        \"nostr_label\": \"Nostr npub 或 NIP-05\",\n        \"send_feedback\": \"發送反饋\",\n        \"invalid_feedback\": \"請說點什麼吧！\",\n        \"need_contact\": \"我們需要於你聯繫的方式\",\n        \"invalid_email\": \"我看這不像是電子郵箱地址\",\n        \"error\": \"提交反饋時出錯 {{error}}\",\n        \"try_again\": \"請稍後重試。\"\n    },\n    \"activity\": {\n        \"title\": \"動態\",\n        \"mutiny\": \"Mutiny\",\n        \"wallet\": \"錢包\",\n        \"nostr\": \"Nostr\",\n        \"view_all\": \"查看所有\",\n        \"receive_some_sats_to_get_started\": \"接收一點聰即可開始\",\n        \"channel_open\": \"通道開通\",\n        \"channel_close\": \"通道關閉\",\n        \"unknown\": \"未知\",\n        \"import_contacts\": \"從 nostr 導入通訊錄，看看他們在打閃誰。\",\n        \"coming_soon\": \"即將推出\",\n        \"private\": \"私人\",\n        \"anonymous\": \"匿名\",\n        \"from\": \"來自：\",\n        \"transaction_details\": {\n            \"lightning_receive\": \"通過閃電接收\",\n            \"lightning_send\": \"通過閃電發送\",\n            \"channel_open\": \"通道開通\",\n            \"channel_close\": \"通道關閉\",\n            \"onchain_receive\": \"鏈上接收\",\n            \"onchain_send\": \"鏈上發送\",\n            \"paid\": \"已支付\",\n            \"unpaid\": \"爲支付\",\n            \"status\": \"狀態\",\n            \"date\": \"日期\",\n            \"tagged_to\": \"標籤\",\n            \"description\": \"描述\",\n            \"fee\": \"費用\",\n            \"onchain_fee\": \"鏈上費用\",\n            \"invoice\": \"發票\",\n            \"payment_hash\": \"付款哈希值\",\n            \"payment_preimage\": \"原像\",\n            \"txid\": \"Txid\",\n            \"total\": \"要求金額\",\n            \"balance\": \"餘額\",\n            \"reserve\": \"儲備\",\n            \"peer\": \"對等方\",\n            \"channel_id\": \"通道 ID\",\n            \"reason\": \"原因\",\n            \"confirmed\": \"已確認\",\n            \"unconfirmed\": \"未確認\",\n            \"sweep_delay\": \"資金可能需要幾天時間才能回到錢包中\",\n            \"no_details\": \"未找到通道詳細信息，這意味着該通道可能已關閉。\",\n            \"back_home\": \"返回首頁\"\n        },\n        \"start_a_chat\": \"開始聊天？\",\n        \"start_a_chat_are_you_sure\": \"該用戶不在你的通訊錄中。\",\n        \"federation_message\": \"聯合會消息\"\n    },\n    \"scanner\": {\n        \"paste\": \"貼上些什麼\",\n        \"cancel\": \"取消\"\n    },\n    \"settings\": {\n        \"header\": \"設置\",\n        \"support\": \"瞭解如何支持 Mutiny\",\n        \"experimental_features\": \"試驗\",\n        \"debug_tools\": \"調試工具\",\n        \"danger_zone\": \"危險地帶\",\n        \"general\": \"一般\",\n        \"version\": \"版本：\",\n        \"admin\": {\n            \"title\": \"管理頁面\",\n            \"caption\": \"我們的內部調試工具。明智使用！\",\n            \"header\": \"祕密調試工具\",\n            \"warning_one\": \"如果你知道自己在做什麼，那你就來對地方了。\",\n            \"warning_two\": \"這些是我們用來調試和測試應用程式的內部工具。請謹慎使用！\",\n            \"kitchen_sink\": {\n                \"disconnect\": \"斷連\",\n                \"peers\": \"對等方\",\n                \"no_peers\": \"沒有對等方\",\n                \"refresh_peers\": \"刷新對等方\",\n                \"connect_peer\": \"連接對等方\",\n                \"expect_a_value\": \"期望值...\",\n                \"connect\": \"連接\",\n                \"close_channel\": \"關閉通道\",\n                \"force_close\": \"強制關閉通道\",\n                \"abandon_channel\": \"棄用通道\",\n                \"confirm_close_channel\": \"你確定要關閉這條通道嗎？\",\n                \"confirm_force_close\": \"你確定要強制關閉這條通道嗎？你的資金需要幾天才能在鏈上贖回。\",\n                \"confirm_abandon_channel\": \"你確定要放棄這條通道嗎？通常只有在開通交易永遠無法確認的情況下才會這樣做。否則，你將損失資金。\",\n                \"channels\": \"通道\",\n                \"no_channels\": \"沒有通道\",\n                \"refresh_channels\": \"刷新通道\",\n                \"pubkey\": \"公鑰\",\n                \"amount\": \"金額\",\n                \"open_channel\": \"開通通道\",\n                \"nodes\": \"節點\",\n                \"no_nodes\": \"沒有節點\",\n                \"enable_zaps_to_hodl\": \"啓用對 hodl 發票打閃？\",\n                \"zaps_to_hodl_desc\": \"對 hodl 發票打閃可能會導致通道強制關閉，從而產生高額鏈上費用。使用風險自負！\",\n                \"zaps_to_hodl_enable\": \"啓用 hodl 打閃\",\n                \"zaps_to_hodl_disable\": \"禁用 hodl 打閃\"\n            }\n        },\n        \"backup\": {\n            \"title\": \"備份\",\n            \"secure_funds\": \"讓我們確保這些資金的安全。\",\n            \"twelve_words_tip\": \"我們會給你看 12 個單詞。請你寫下這 12 個單詞。\",\n            \"warning_one\": \"如果你清除了瀏覽器歷史記錄，或者丟失了設備，這 12 個字是你恢復錢包的唯一方法。\",\n            \"warning_two\": \"Mutiny 是自我保管錢包。都看你自己...\",\n            \"confirm\": \"我已經寫下了這幾個字\",\n            \"responsibility\": \"我明白我的資金由我自己負責\",\n            \"liar\": \"我不是爲了了結此事而撒謊\",\n            \"seed_words\": {\n                \"reveal\": \"輕點顯示種子單詞\",\n                \"hide\": \"隱藏\",\n                \"copy\": \"危險地複製到剪貼板\",\n                \"copied\": \"已複製！\"\n            }\n        },\n        \"channels\": {\n            \"title\": \"閃電通道\",\n            \"outbound\": \"出賬\",\n            \"inbound\": \"入賬\",\n            \"reserve\": \"儲備\",\n            \"have_channels\": \"你有\",\n            \"have_channels_one\": \"條閃電通道。\",\n            \"have_channels_many\": \"條閃電通道。\",\n            \"inbound_outbound_tip\": \"出賬是你可以在閃電上花費的金額。入賬是指在不產生閃電服務費的情況下，你可以收到的金額。\",\n            \"reserve_tip\": \"你的閃電通道餘額中約有1%是爲支付費用預留的儲備。通過兌換開通的通道需要額外儲備。\",\n            \"no_channels\": \"看起來你還沒有任何通道。要開始使用，可以通過閃電接收一些聰，或將一些鏈上資金換成一條通道。開始動手吧！\",\n            \"close_channel\": \"關閉\",\n            \"online_channels\": \"在線通道\",\n            \"offline_channels\": \"離線通道\",\n            \"close_channel_confirm\": \"關閉該通道會將餘額轉移到鏈上，並產生鏈上費用。\",\n            \"force_close_channel_confirm\": \"此通道已下線。強制關閉此通道會將餘額轉移到鏈上，產生鏈上費用，並可能需要幾天時間結算。\"\n        },\n        \"connections\": {\n            \"title\": \"錢包連接\",\n            \"error_name\": \"名稱不能爲空\",\n            \"error_connection\": \"創建錢包連接失敗\",\n            \"error_budget_zero\": \"預算必須大於零\",\n            \"add_connection\": \"添加連接\",\n            \"manage_connections\": \"管理連接\",\n            \"manage_gifts\": \"管理紅包\",\n            \"delete_connection\": \"刪除\",\n            \"new_connection\": \"新增連接\",\n            \"edit_connection\": \"編輯連接\",\n            \"new_connection_label\": \"名稱\",\n            \"new_connection_placeholder\": \"我最愛的 nostr 客戶端...\",\n            \"create_connection\": \"創建連接\",\n            \"save_connection\": \"保存更改\",\n            \"edit_budget\": \"編輯預算\",\n            \"open_app\": \"打開應用程式\",\n            \"open_in_nostr_client\": \"在 Nostr 客戶端中打開\",\n            \"open_in_primal\": \"在 Primal 中打開\",\n            \"nostr_client_not_found\": \"沒有找到 Nostr 客戶端\",\n            \"client_not_found_description\": \"安裝 Primal、Amethyst、或達摩等 nostr 客戶端打開此鏈接。\",\n            \"relay\": \"中繼器\",\n            \"authorize\": \"授權外部錢包從你的錢包中要求付款。與 Nostr 客戶端完美搭配。\",\n            \"pending_nwc\": {\n                \"title\": \"待處理要求\",\n                \"approve_all\": \"全部批准\",\n                \"deny_all\": \"全部拒絕\"\n            },\n            \"careful\": \"請小心分享此連接！預算範圍內的要求將自動支付。\",\n            \"spent\": \"支出\",\n            \"remaining\": \"剩餘\",\n            \"confirm_delete\": \"你確定要刪除此連接名嗎？\",\n            \"budget\": \"預算\",\n            \"resets_every\": \"重置於每\",\n            \"resubscribe_date\": \"重新訂閱於\",\n            \"disable_connection\": \"停用\",\n            \"disabled\": \"此訂閱已停用。目前你還無法重新啓用訂閱，但我們正在努力解決這個問題。請儘快回來查看！\",\n            \"disable_connection_confirm\": \"你確定要停用訂閱嗎？目前還無法重新啓用訂閱，我們正在努力解決這個問題。\"\n        },\n        \"emergency_kit\": {\n            \"title\": \"應急箱\",\n            \"caption\": \"診斷並解決錢包問題。\",\n            \"emergency_tip\": \"如果你的錢包似乎壞了，這裏有一些工具可以嘗試調試和修復。\",\n            \"questions\": \"如果你對這些按鈕的作用有任何疑問，請\",\n            \"link\": \"向我們尋求支持。\",\n            \"import_export\": {\n                \"title\": \"導出錢包狀態\",\n                \"error_password\": \"需要密碼\",\n                \"error_read_file\": \"文件讀取錯誤\",\n                \"error_no_text\": \"文件中沒有文本\",\n                \"tip\": \"你可以將整個 Mutiny 錢包狀態導出到文件，然後導入到新的瀏覽器中。通常都能成功！\",\n                \"caveat_header\": \"重要注意事項：\",\n                \"caveat\": \"導出後不要再原瀏覽器中進行任何操作。如果進行了操作，就需要重新導出。導入成功後，最佳做法是清除瀏覽器的狀態，以確保不會產生衝突。\",\n                \"save_state\": \"將狀態保存爲文件\",\n                \"import_state\": \"從文件導入狀態\",\n                \"confirm_replace\": \"你是否想要用這個來代替你的錢包狀態？\",\n                \"password\": \"輸入密碼解密\",\n                \"decrypt_wallet\": \"解密錢包\",\n                \"decrypt_export\": \"輸入密碼保存\"\n            },\n            \"logs\": {\n                \"title\": \"下載調試日誌\",\n                \"something_screwy\": \"有什麼不對勁的地方嗎？查看日誌！\",\n                \"download_logs\": \"下載日誌\",\n                \"password\": \"輸入密碼解密\",\n                \"confirm_password_label\": \"確認密碼\"\n            },\n            \"delete_everything\": {\n                \"delete\": \"刪除一切\",\n                \"confirm\": \"這將刪除你的節點的狀態。此操作無法撤銷！\",\n                \"deleted\": \"已刪除\",\n                \"deleted_description\": \"刪除所有數據\"\n            }\n        },\n        \"encrypt\": {\n            \"title\": \"安全\",\n            \"caption\": \"先備份才能解鎖加密\",\n            \"header\": \"加密你的種子詞\",\n            \"hot_wallet_warning\": \"Mutiny 是一個『熱錢包』，因此它需要你的種子單詞才能運行，但你也可以選擇密碼來加密這些單詞。\",\n            \"password_tip\": \"這樣，即使有人訪問了你的瀏覽器，也無法訪問你的資金。\",\n            \"optional\": \"（可選）\",\n            \"existing_password\": \"現有密碼\",\n            \"existing_password_caption\": \"如果尚未設置密碼，請留空。\",\n            \"new_password_label\": \"密碼\",\n            \"new_password_placeholder\": \"輸入密碼\",\n            \"new_password_caption\": \"該密碼將用於加密你的種子詞。如果你忘記了密碼，就需要重新輸入你的種子詞才能使用你的資金。你寫下了你的種子詞，對吧？\",\n            \"confirm_password_label\": \"確認密碼\",\n            \"confirm_password_placeholder\": \"輸入同樣的密碼\",\n            \"encrypt\": \"加密\",\n            \"skip\": \"跳過\",\n            \"error_match\": \"密碼不符\",\n            \"error_same_as_existingpassword\": \"新密碼必須與現有密碼不一致\"\n        },\n        \"decrypt\": {\n            \"title\": \"輸入你的密碼\",\n            \"decrypt_wallet\": \"解密錢包\",\n            \"forgot_password_link\": \"忘記密碼？\",\n            \"error_wrong_password\": \"密碼無效\"\n        },\n        \"currency\": {\n            \"title\": \"貨幣\",\n            \"caption\": \"選擇首選貨幣對\",\n            \"select_currency\": \"選擇貨幣\",\n            \"select_currency_label\": \"貨幣對\",\n            \"select_currency_caption\": \"選擇新貨幣將重新同步錢包以獲取價格更新\",\n            \"request_currency_support_link\": \"要求支持更多貨幣\",\n            \"error_unsupported_currency\": \"請選擇一種支持的貨幣。\"\n        },\n        \"language\": {\n            \"title\": \"語言\",\n            \"caption\": \"選擇你的語言\",\n            \"select_language\": \"選擇語言\",\n            \"select_language_label\": \"語言\",\n            \"select_language_caption\": \"選擇新貨幣將更改錢包語言，並忽略當前瀏覽器語言\",\n            \"request_language_support_link\": \"要求支持更多語言\",\n            \"error_unsupported_language\": \"請選擇一個已支持的語言。\"\n        },\n        \"lnurl_auth\": {\n            \"title\": \"LNURL 認證\",\n            \"auth\": \"認證\",\n            \"expected\": \"期待類似 LNURL 的東西...\"\n        },\n        \"plus\": {\n            \"title\": \"Mutiny+\",\n            \"join\": \"加入\",\n            \"sats_per_month\": \"，每月 {{amount}} 聰。\",\n            \"lightning_balance\": \"你的閃電餘額中至少需要 {{amount}} 聰才能開始使用。先試後買！\",\n            \"restore\": \"恢復訂閱\",\n            \"ready_to_join\": \"準備好了加入\",\n            \"click_confirm\": \"點擊確認支付第一個月的費用。\",\n            \"open_source\": \"Mutiny 是開放代碼，可自行託管。\",\n            \"optional_pay\": \"但你也可以付費。\",\n            \"paying_for\": \"訂閱\",\n            \"supports_dev\": \"有助於支持正在進行的開發工作，並能讓你儘早享受新功能和高級功能：\",\n            \"thanks\": \"你已成爲 Mutiny 的一員！享受以下福利：\",\n            \"renewal_time\": \"你將收到一份續訂付款申請於\",\n            \"cancel\": \"要取消訂閱，只需不付費即。你還可以停用 Mutiny+ 的\",\n            \"wallet_connection\": \"錢包連接。\",\n            \"subscribe\": \"訂閱\",\n            \"error_no_plan\": \"未找到現有訂閱\",\n            \"error_failure\": \"無法訂閱\",\n            \"error_no_subscription\": \"未找到現有訂閱\",\n            \"error_expired_subscription\": \"你的訂閱已過期，請單擊『加入』續訂\",\n            \"satisfaction\": \"自鳴得意的滿足感\",\n            \"gifting\": \"紅包功能\",\n            \"multi_device\": \"多設備訪問\",\n            \"ios_testflight\": \"iOS TestFlight 訪問權限\",\n            \"more\": \"... 還有更多\",\n            \"cta_description\": \"提前享受新功能和高級功能。\",\n            \"cta_but_already_plus\": \"感謝你的支持！\",\n            \"lightning_address\": \"你自己的閃電地址\"\n        },\n        \"restore\": {\n            \"title\": \"恢復\",\n            \"all_twelve\": \"你必須輸入全部 12 個單詞\",\n            \"wrong_word\": \"錯別字\",\n            \"paste\": \"危險地從剪貼板貼上\",\n            \"confirm_text\": \"你確定要回覆到此錢包嗎？你現有的錢包將被刪除！\",\n            \"restore_tip\": \"你可以使用 12 個單詞的種子短語恢復現有的 Mutiny 錢包。這將代替你現有的錢包，因此請確保你知道自己在做什麼！\",\n            \"multi_browser_warning\": \"請勿同時在多個瀏覽器上使用。\",\n            \"error_clipboard\": \"不支持剪貼板\",\n            \"error_word_number\": \"字數不對\",\n            \"error_invalid_seed\": \"種子短語無效\"\n        },\n        \"servers\": {\n            \"title\": \"服務器\",\n            \"caption\": \"不要信賴我們！使用你自己的服務器運行 Mutiny。\",\n            \"link\": \"瞭解有關自助託管的更多信息\",\n            \"proxy_label\": \"Websockets 代理\",\n            \"proxy_caption\": \"你的閃電節點與網絡其他部分的通信方式。\",\n            \"error_proxy\": \"應該是以 wss:// 開頭的網址\",\n            \"esplora_label\": \"Esplora\",\n            \"esplora_caption\": \"鏈上信息的區塊數據。\",\n            \"error_esplora\": \"這看起來不像是個 URL\",\n            \"rgs_label\": \"RGS\",\n            \"rgs_caption\": \"快速流言同步（Rapid Gossip Sync）。用於路由選擇的閃電網絡數據。\",\n            \"error_rgs\": \"這看起來不像是個 URL\",\n            \"lsp_label\": \"LSP\",\n            \"lsp_caption\": \"閃電服務提供商（Lightning Service Provider）。自動爲你打開入賬流動性通道。還可以保護發票隱私。\",\n            \"lsps_connection_string_label\": \"LSPS 連接字符串\",\n            \"lsps_connection_string_caption\": \"閃電服務提供商（Lightning Service Provider）。自動爲你打開入賬流動性通道。使用 LSP 規範。\",\n            \"error_lsps_connection_string\": \"這看起來不像是節點連接字符串\",\n            \"lsps_token_label\": \"LSPS 令牌\",\n            \"lsps_token_caption\": \"LSPS 令牌。用於識別連接到 LSP 的錢包\",\n            \"lsps_valid_error\": \"你可以只使用 LSP 設置，也可以使用 LSPS 連接字符串和 LSPS 令牌設置，但不能同時使用兩種設置。\",\n            \"error_lsps_token\": \"這看到起來不像是一個有效的令牌\",\n            \"storage_label\": \"存儲\",\n            \"storage_caption\": \"加密 VSS 備份服務。\",\n            \"error_lsp\": \"這看起來不像是個 URL\",\n            \"error_tor\": \"目前還不支持 Tor URL\",\n            \"save\": \"保存\"\n        },\n        \"nostr_contacts\": {\n            \"title\": \"同步 Nostr 通訊錄\",\n            \"npub_label\": \"Nostr npub\",\n            \"npub_required\": \"Npub 不能爲空\",\n            \"sync\": \"同步\",\n            \"resync\": \"重新同步\",\n            \"remove\": \"移除\"\n        },\n        \"manage_federations\": {\n            \"title\": \"管理聯合會\",\n            \"federation_code_label\": \"聯合會代碼\",\n            \"federation_code_required\": \"聯合會代碼不能爲空\",\n            \"federation_added_success\": \"已成功添加聯合會\",\n            \"federation_remove_confirm\": \"你是否確定要移除這個聯合會？請先確保你的資金已轉入閃電餘額或其他錢包。\",\n            \"add\": \"添加\",\n            \"remove\": \"離開聯合會\",\n            \"expires\": \"到期\",\n            \"federation_id\": \"聯合會 ID\",\n            \"description\": \"聯合會是基於比特幣的網絡，使比特幣的使用變得更便宜、更快捷、更方便。\",\n            \"learn_more\": \"瞭解更多\",\n            \"discover\": \"發現聯合會\",\n            \"manual\": \"邀請碼\",\n            \"created_at\": \"創建於\",\n            \"recommended_by\": \"推薦人\",\n            \"already_in_fed\": \"你已經加入了一個聯合會！\",\n            \"descriptionpart2\": \"每個聯合會都由不同的個人或公司運營。請在下面查找一個你或你的朋友們可能信任的聯合會。\",\n            \"join_me\": \"加入我\",\n            \"recommend\": \"推薦聯合會\",\n            \"recommended_by_you\": \"你的推薦\",\n            \"transfer_funds\": \"轉賬資金\",\n            \"transfer_funds_message\": \"添加第二個聯合會以實現轉賬。\"\n        },\n        \"gift\": {\n            \"give_sats_link\": \"贈送紅包\",\n            \"something_went_wrong\": \"出了問題\",\n            \"title\": \"紅包\",\n            \"no_plus_caption\": \"升級到 Mutiny+，即可贈送紅包\",\n            \"receive_too_small\": \"你的首次接收必須是 {{amount}} 聰或更高。\",\n            \"setup_fee_lightning\": \"接收此紅包將收取閃電設置費。\",\n            \"already_claimed\": \"該紅包已被認領\",\n            \"sender_is_poor\": \"付款方沒有足夠的餘額來支付這份紅包。\",\n            \"sender_timed_out\": \"紅包付款超時。送禮人可能不在線，或該紅包已被領取。\",\n            \"sender_generic_error\": \"付款方發送錯誤：{{error}}\",\n            \"receive_header\": \"你收到了一個紅包！\",\n            \"receive_description\": \"你一定很特別。只需點擊大按鈕，即可領取你的紅包。下次送禮人在線時，資金就會納入這個錢包。\",\n            \"receive_claimed\": \"紅包已領取！你應該很快就能看到紅包存入你的餘額。\",\n            \"receive_cta\": \"領取紅包\",\n            \"receive_try_again\": \"重試\",\n            \"send_header\": \"創建紅包\",\n            \"send_explainer\": \"贈送紅包。創建一個 Mutiny 紅包鏈接，任何人都可以通過網絡瀏覽器領取。\",\n            \"send_name_required\": \"供你存檔\",\n            \"send_name_label\": \"收款方名稱\",\n            \"send_header_claimed\": \"紅包已收到！\",\n            \"send_claimed\": \"你的紅包已被認領。感謝你的分享。\",\n            \"send_sharable_header\": \"可分享的鏈接\",\n            \"send_instructions\": \"將這個紅包鏈接複製給收款方，或請他們用錢包掃描這個二維碼。\",\n            \"send_another\": \"再創一個\",\n            \"send_small_warning\": \"全新的 Mutiny 用戶無法兌換少於10萬聰\",\n            \"send_cta\": \"創建紅包\",\n            \"send_delete_button\": \"刪除紅包\",\n            \"send_delete_confirm\": \"你確定要刪除這份紅包嗎？這是你的『拉毯時刻』嗎？\",\n            \"send_tip\": \"你的 Mutiny 錢包需要打開才能兌換紅包。\",\n            \"need_plus\": \"升級到 Mutiny+ 以啓用紅包功能。紅包允許你創建一個 Mutiny 紅包鏈接，任何人都可以通過網絡瀏覽器領取。\"\n        },\n        \"appearance\": \"外觀\",\n        \"social\": \"社交\",\n        \"nostr_keys\": {\n            \"title\": \"Nostr 密鑰\",\n            \"caption\": \"不是你的密鑰，就不是你的筆記\",\n            \"description\": \"你可以在多個應用程式中使用同一個 nostr 個人檔案。\",\n            \"warning\": \"請謹慎分享 nostr 私人密鑰！\",\n            \"learn_more\": \"瞭解更多關於 nostr 的信息\",\n            \"import_profile\": \"導入 nostr 個人檔案\",\n            \"delete_account\": \"刪除帳戶\",\n            \"delete_account_confirm\": \"刪除 nostr 個人檔案無法撤銷，並且會禁用錢包的某些社交功能。\",\n            \"unlink_account\": \"斷連 nostr 檔案\",\n            \"unlink_account_confirm\": \"斷開對 nostr 檔案的連接會將錢包重置爲默認的 nostr 個人檔案。\",\n            \"delete_account_confirm_scary\": \"此個人檔案將在所有 nostr 應用程式中被刪除。\"\n        },\n        \"lightning_address\": {\n            \"title\": \"Mutiny 地址\",\n            \"description\": \"使用自己的閃電地址向錢包獲取打閃和付款。\",\n            \"create\": \"創建 Mutiny 地址\",\n            \"add_a_federation\": \"要器用此功能，你必須是一個聯合會的成員。\",\n            \"ios_warning\": \"Mutiny 地址需要 Mutiny+ 訂閱，該應用無法購買。\"\n        }\n    },\n    \"swap\": {\n        \"peer_not_found\": \"未找到對等方\",\n        \"channel_too_small\": \"讓通道小於  {{amount}} 聰實在太傻了。\",\n        \"insufficient_funds\": \"你沒有足夠的資金來創建這條通道\",\n        \"header\": \"兌換到閃電\",\n        \"initiated\": \"已啓動兌換\",\n        \"sats_added\": \"+{{amount}} 聰將被添加到你的閃電餘額中\",\n        \"use_existing\": \"使用現有對等方\",\n        \"choose_peer\": \"選擇對等方\",\n        \"peer_connect_label\": \"連接新的對等方\",\n        \"peer_connect_placeholder\": \"對等方連接字符串\",\n        \"connect\": \"連接\",\n        \"connecting\": \"正在連接...\",\n        \"confirm_swap\": \"確認兌換\"\n    },\n    \"swap_lightning\": {\n        \"insufficient_funds\": \"你沒有足夠的資金兌換成閃電\",\n        \"header\": \"兌換到閃電\",\n        \"header_preview\": \"預覽兌換\",\n        \"completed\": \"兌換已完成\",\n        \"too_small\": \"輸入的金額無效。你必須交換至少10萬聰。\",\n        \"sats_added\": \"+{{amount}} 聰已被添加到你的閃電餘額中\",\n        \"sats_fee\": \"+{{amount}} 聰費用\",\n        \"confirm_swap\": \"確認兌換\",\n        \"preview_swap\": \"預覽兌換費用\"\n    },\n    \"reload\": {\n        \"mutiny_update\": \"Mutiny 更新\",\n        \"new_version_description\": \"新版 Mutiny 已緩存，重新加載即可開始使用。\",\n        \"reload\": \"重新加載\"\n    },\n    \"error\": {\n        \"title\": \"錯誤\",\n        \"emergency_link\": \"應急箱\",\n        \"reload\": \"重新加載\",\n        \"restart\": {\n            \"title\": \"發生了什麼*額外*糟糕的事情？停止節點！\",\n            \"start\": \"開始\",\n            \"stop\": \"停止\"\n        },\n        \"general\": {\n            \"oh_no\": \"哦，不！\",\n            \"never_should_happen\": \"這本不該發生\",\n            \"try_reloading\": \"請嘗試重新加載此頁面或點擊『該死』按鈕。如果你仍然遇到問題，\",\n            \"support_link\": \"向我們尋求支持。\",\n            \"getting_desperate\": \"絕望了嗎？試試\"\n        },\n        \"load_time\": {\n            \"stuck\": \"卡在這個屏幕上？嘗試重新加載。如果還不行，請查看\"\n        },\n        \"not_found\": {\n            \"title\": \"找不到\",\n            \"wtf_paul\": \"這大概是 Paul 的錯。\"\n        },\n        \"resync\": {\n            \"incorrect_balance\": \"鏈上餘額似乎不正確？嘗試重新同步鏈上錢包。\",\n            \"resync_wallet\": \"重新同步錢包\"\n        },\n        \"on_boot\": {\n            \"existing_tab\": {\n                \"title\": \"檢測到多個選項卡\",\n                \"description\": \"Mutiny 一次只能在一個選項卡中使用。看起來你打開了另一個運行 Mutiny 的選項卡。請關閉該選項卡並刷新此頁面，或關閉此選項卡並刷新另一個選項卡。\"\n            },\n            \"already_running\": {\n                \"title\": \"Mutiny 可能在其他設備上運行\",\n                \"description\": \"Mutiny 一次只能在一個地方使用。看起來你有其他設備或瀏覽器在使用這個錢包。如果你最近在其他設備上關閉了 Mutiny，請稍等片刻再試。\",\n                \"retry_again_in\": \"重試於\",\n                \"seconds\": \"秒後\"\n            },\n            \"incompatible_browser\": {\n                \"title\": \"瀏覽器不兼容\",\n                \"header\": \"檢測到瀏覽器不兼容\",\n                \"description\": \"Mutiny 需要一個支持 WebAssembly、LocalStorage 和 IndexedDB 的現代瀏覽器。有些瀏覽器在私人模式下會禁用這些功能。\",\n                \"try_different_browser\": \"請確保你的瀏覽器支持所有這些功能，或者考慮嘗試其他瀏覽器。你也可以嘗試禁用某些阻止這些功能的擴展或『屏蔽』功能。\",\n                \"browser_storage\": \"（我們很樂意支持更多的隱私性瀏覽器，但我們必須將你的錢包數據保存到瀏覽器存儲中，否則你將丟失資金。）\",\n                \"browsers_link\": \"支持的瀏覽器\"\n            },\n            \"loading_failed\": {\n                \"title\": \"加載失敗\",\n                \"header\": \"Mutiny 加載失敗\",\n                \"description\": \"啓動 Mutiny 錢包時出了問題。\",\n                \"repair_options\": \"如果你的錢包似乎壞了，這裏有一些工具可以嘗試調試和修復它。\",\n                \"questions\": \"如果你對這些按鈕的作用有任何疑問，請\",\n                \"support_link\": \"向我們尋求支持。\",\n                \"services_down\": \"看來 Mutiny 的某項服務出現問題。請稍後再試。\",\n                \"in_the_meantime\": \"在此期間，如果你想使用你的鏈上資金，可以將 Mutiny 載入\",\n                \"safe_mode\": \"安全模式\"\n            }\n        }\n    },\n    \"modals\": {\n        \"share\": \"分享\",\n        \"details\": \"詳情\",\n        \"loading\": {\n            \"loading\": \"正在加載：{{stage}}\",\n            \"default\": \"剛剛起步\",\n            \"double_checking\": \"正在重複檢查\",\n            \"existing_wallet\": \"檢查現有錢包\",\n            \"downloading\": \"正在下載\",\n            \"setup\": \"設置\",\n            \"done\": \"完成\"\n        },\n        \"onboarding\": {\n            \"welcome\": \"歡迎!\",\n            \"setup\": \"設置\",\n            \"restore_from_backup\": \"如果你以前使用過 Mutiny，可以從備份中恢復。否則，你可以跳過這一步，盡情享受你的新錢包！\",\n            \"not_available\": \"我們還沒做到這一點\",\n            \"secure_your_funds\": \"確保你的資金安全\"\n        },\n        \"more_info\": {\n            \"whats_with_the_fees\": \"這些費用是怎麼回事？\",\n            \"self_custodial\": \"Mutiny 是一個自我保管錢包。要啓動閃電支付，我們必須閃電通道，這需要最低金額和設置費。\",\n            \"future_payments\": \"未來的付款（包括發送和接收）將只產生正常的網絡費用和象徵性的服務費，除非你的通道已耗盡入賬容量。\",\n            \"liquidity\": \"進一步瞭解流動性\",\n            \"fedimint\": \"如果你加入了一個聯合會，則不需要繳納設置費或呈現最低金額限制。\"\n        },\n        \"confirm_dialog\": {\n            \"are_you_sure\": \"你確定嗎？\",\n            \"cancel\": \"取消\",\n            \"confirm\": \"確認\"\n        },\n        \"lnurl_auth\": {\n            \"auth_request\": \"認證要求\",\n            \"login\": \"登錄\",\n            \"decline\": \"拒絕\",\n            \"error\": \"不知道爲什麼，這招不管用。\",\n            \"authenticated\": \"已認證！\"\n        }\n    },\n    \"setup\": {\n        \"new_profile\": {\n            \"description\": \"Mutiny 使支付成爲一種社交。\",\n            \"title\": \"創建你的個人檔案\"\n        },\n        \"skip\": \"暫時跳過\",\n        \"import_profile\": \"導入現有的 nostr 檔案\",\n        \"import\": {\n            \"title\": \"導入 nostr 檔案\",\n            \"description\": \"使用現有的 nostr 帳戶登錄。\"\n        },\n        \"federation\": {\n            \"pick\": \"選擇聯合會\",\n            \"skip_confirm\": \"使用 Mutiny 需要支付鏈上費和通道費，而且會錯過閃電地址等高級功能。你可以稍後在設置中改變主意。\"\n        }\n    },\n    \"utils\": {\n        \"hours_future_one\": \"一小時後\",\n        \"hours_future_other\": \"{{count}} 小時後\",\n        \"hours_past_one\": \"一小時前\",\n        \"hours_past_other\": \"{{count}} 小時前\",\n        \"hours_short\": \"{{count}}h\",\n        \"days_future_one\": \"一天後\",\n        \"days_future_other\": \"{{count}} 天後\",\n        \"days_past_one\": \"一天前\",\n        \"days_past_other\": \"{{count}} 天前\",\n        \"days_short\": \"{{count}}d\",\n        \"minutes_future_one\": \"一分鐘後\",\n        \"minutes_future_other\": \"{{count}} 分鐘後\",\n        \"minutes_past_one\": \"一分鐘前\",\n        \"minutes_past_other\": \"{{count}} 分鐘前\",\n        \"minutes_short\": \"{{count}}m\",\n        \"nowish\": \"現在\",\n        \"seconds_future\": \"幾秒鐘後\",\n        \"seconds_past\": \"剛剛\"\n    },\n    \"transfer\": {\n        \"completed\": \"轉賬已完成\",\n        \"sats_moved\": \"+{{amount}} 聰已被轉賬至 {{federation_name}}\",\n        \"confirm\": \"確認轉賬\",\n        \"title\": \"轉賬資金\"\n    }\n}\n"
  },
  {
    "path": "public/i18n/it.json",
    "content": "{\n  \"common\": {\n    \"title\": \"Mutiny Wallet\",\n    \"mutiny\": \"Mutiny\",\n    \"nice\": \"Figo\",\n    \"home\": \"Home\",\n    \"e_sats\": \"eSATS\",\n    \"e_sat\": \"eSAT\",\n    \"sats\": \"SATS\",\n    \"sat\": \"SAT\",\n    \"scan\": \"Scansione\",\n    \"fee\": \"Commissione\",\n    \"send\": \"Invia\",\n    \"receive\": \"Ricevi\",\n    \"request\": \"Richiedi\",\n    \"dangit\": \"Dannazione\",\n    \"back\": \"Ritorna\",\n    \"coming_soon\": \"(prossimamente)\",\n    \"copy\": \"Copia\",\n    \"copied\": \"Copiato\",\n    \"continue\": \"Continua\",\n    \"error_unimplemented\": \"Non implementato\",\n    \"why\": \"Perché?\",\n    \"private_tags\": \"Etichette private\",\n    \"view_transaction\": \"Vedi transazione\",\n    \"view_payment_details\": \"Vedi dettagli pagamento\",\n    \"pending\": \"In attesa\",\n    \"error_safe_mode\": \"Mutiny viene eseguito in modalità sicura. Lightning è disattivato.\",\n    \"self_hosted\": \"Auto-ospitato\",\n    \"expires\": \"Scade in {{tempo}}\"\n  },\n  \"home\": {\n    \"receive\": \"Ricevi i tuoi primi sats\",\n    \"find\": \"Trova i tuoi amici su Nostr\",\n    \"backup\": \"Mettete al sicuro i vostri fondi!\",\n    \"connection\": \"Create una connessione al portafoglio\",\n    \"connection_edit\": \"Modifica la connessione al portafoglio\",\n    \"federation\": \"Unisciti ad una federazione\",\n    \"subnav\": {\n      \"just_me\": \"Solo io\",\n      \"friends\": \"Amici\",\n      \"requests\": \"Richieste\"\n    }\n  },\n  \"profile\": {\n    \"profile\": \"Profilo\",\n    \"nostr_identity\": \"Identità Nostr\",\n    \"add_lightning_address\": \"Aggiungi Indirizzo Lightning\",\n    \"edit_profile\": \"Modifica Profilo\",\n    \"join_federation\": \"Unisciti ad una federazione\",\n    \"manage_federation\": \"Gestisci Federationi\",\n    \"federated_custody\": \"Custodia Federata\",\n    \"self_custody\": \"Auto Custodia\",\n    \"social\": \"Sociale\",\n    \"edit\": {\n      \"nym\": \"Nym\"\n    },\n    \"deleted\": \"Il tuo profilo nostr è stato cancellato.\",\n    \"no_lightning_address\": \"Nessun indirizzo lightning impostato\",\n    \"pay_me\": \"Inviami sats\"\n  },\n  \"chat\": {\n    \"prompt\": \"Questa è una nuova conversazione. Prova a chiedere soldi!\",\n    \"placeholder\": \"Messaggio\"\n  },\n  \"contacts\": {\n    \"new\": \"new\",\n    \"add_contact\": \"Aggiungi Contatto\",\n    \"new_contact\": \"Nuovo Contatto\",\n    \"create_contact\": \"Crea Contatto\",\n    \"edit_contact\": \"Modifica Contatto\",\n    \"save_contact\": \"Salva Contatto\",\n    \"delete\": \"Cancella\",\n    \"confirm_delete\": \"Sei sicuro di voler eliminare questo contatto?\",\n    \"payment_history\": \"Cronologia dei pagamenti\",\n    \"no_payments\": \"Ancora nessun pagamento\",\n    \"edit\": \"Modifica\",\n    \"pay\": \"Paga\",\n    \"name\": \"Nome\",\n    \"ln_address\": \"Indirzzo Lightning\",\n    \"placeholder\": \"Satoshi\",\n    \"lightning_address\": \"Indirizzo Lightning\",\n    \"unimplemented\": \"Non implementato\",\n    \"not_available\": \"Non lo facciamo ancora\",\n    \"error_name\": \"Ci serve almeno un nome\",\n    \"email_error\": \"Non sembra un indirizzo lightning\",\n    \"npub_error\": \"Non sembra un nostr npub\",\n    \"error_ln_address_missing\": \"I nuovi contatti hanno bisogno di un indirizzo lightning\",\n    \"npub\": \"Nostr Npub\"\n  },\n  \"redeem\": {\n    \"redeem_bitcoin\": \"Riscatta Bitcoin\",\n    \"lnurl_amount_message\": \"Inserire l'importo di ritiro tra {{min}} e {{max}} sats\",\n    \"lnurl_redeem_failed\": \"Ritiro Fallito\",\n    \"lnurl_redeem_success\": \"Pagamento Ricevuto\"\n  },\n  \"request\": {\n    \"request_bitcoin\": \"Richiesta Bitcoin\",\n    \"request\": \"Richiesta\"\n  },\n  \"receive\": {\n    \"receive_bitcoin\": \"Ricevi Bitcoin\",\n    \"edit\": \"Modifica\",\n    \"checking\": \"Controllando\",\n    \"choose_format\": \"Scegli un formato\",\n    \"payment_received\": \"Pagamento Ricevuto\",\n    \"payment_initiated\": \"Pagamento Avviato\",\n    \"receive_add_the_sender\": \"Aggiungete il mittente per il vostro archivio\",\n    \"keep_mutiny_open\": \"Mantenere Mutiny aperto per completare il pagamento.\",\n    \"choose_payment_format\": \"Scegliere il formato di pagamento\",\n    \"unified_label\": \"Unificato\",\n    \"unified_caption\": \"Combina un indirizzo bitcoin e una fattura lightning. Il mittente sceglie il metodo di pagamento.\",\n    \"lightning_label\": \"Fattura Lightning\",\n    \"lightning_caption\": \"Ideale per le piccole transazioni. Di solito le commissioni sono inferiori a quelle on-chain\",\n    \"onchain_label\": \"Indirizzo Bitcoin\",\n    \"onchain_caption\": \"On-chain, proprio come ha fatto Satoshi. Ideale per transazioni molto grandi.\",\n    \"unified_setup_fee\": \"Una commissione di configurazione di {{amount}} SATS sarà addebitata se il pagamento avviene in Lightning\",\n    \"lightning_setup_fee\": \"Una commissione di configurazione Lightning {{amount}} SATS sarà addebitata per ricevere questo\",\n    \"amount\": \"Importo\",\n    \"fee\": \"+ Commissione\",\n    \"total\": \"Totale\",\n    \"spendable\": \"Spendibile\",\n    \"channel_size\": \"Dimensione del Canale\",\n    \"channel_reserve\": \"- Riserva del Canale\",\n    \"error_under_min_lightning\": \"Predefinito a On-chain. L'importo è troppo basso per il vostro Lightning iniziale.\",\n    \"error_creating_unified\": \"Predefinito a On-chain. Qualcosa è andato storto durante la creazione dell'indirizzo unificato\",\n    \"error_creating_address\": \"Qualcosa è andato storto durante la creazione dell'indirizzo on-chain\",\n    \"amount_editable\": {\n      \"receive_too_small\": \"La prima ricezione deve essere {{amount}} SATS o maggiore.\",\n      \"setup_fee_lightning\": \"Una commissione di configurazione lightning sarà adeebitata se pagata sopra lightning\",\n      \"more_than_21m\": \"Ci sono solo 21 milioni bitcoin.\",\n      \"set_amount\": \"Imposta importo\",\n      \"max\": \"MAX\",\n      \"fix_amounts\": {\n        \"ten_k\": \"10k\",\n        \"one_hundred_k\": \"100k\",\n        \"one_million\": \"1m\"\n      },\n      \"del\": \"CANCELLA\",\n      \"balance\": \"Bilancio\"\n    },\n    \"integrated_qr\": {\n      \"onchain\": \"On-chain\",\n      \"lightning\": \"Lightning\",\n      \"unified\": \"Unificato\",\n      \"gift\": \"Regalo Lightning\"\n    },\n    \"remember_choice\": \"Ricorda la mia scelta la prossima volta\",\n    \"what_for\": \"A cosa serve questo?\",\n    \"method_help\": {\n      \"title\": \"Metodo di Ricezione\",\n      \"body\": \"I lightning ricevuti sotto 200.000 sats vanno nella propria federazione per impostazione predefinita. Tutto ciò che è superiore a questa cifra va in un canale lightning.\"\n    }\n  },\n  \"send\": {\n    \"search\": {\n      \"placeholder\": \"Nome, indirizzo, fattura\",\n      \"paste\": \"Incolla\",\n      \"contacts\": \"Contatti\",\n      \"global_search\": \"Ricerca globale\",\n      \"no_results\": \"Nessun risultato trovato per\"\n    },\n    \"sending\": \"Inviando...\",\n    \"confirm_send\": \"Conferma Invio\",\n    \"contact_placeholder\": \"Aggiungete il ricevitore per il vostro archivio\",\n    \"start_over\": \"Ricominciare\",\n    \"send_bitcoin\": \"Invia Bitcoin\",\n    \"paste\": \"Incolla\",\n    \"scan_qr\": \"Scansiona QR\",\n    \"payment_initiated\": \"Pagamento Avviato\",\n    \"payment_sent\": \"Pagamento Inviato\",\n    \"destination\": \"Destinazione\",\n    \"no_payment_info\": \"Nessuna informazione di pagamento\",\n    \"progress_bar\": {\n      \"of\": \"of\",\n      \"sats_sent\": \"sats inviati\"\n    },\n    \"what_for\": \"A cosa serve questo?\",\n    \"zap_note\": \"Zap nota\",\n    \"error_low_balance\": \"Non abbiamo un saldo sufficiente per pagare l'importo indicato.\",\n    \"error_invoice_match\": \"Importo richiesto, {{amount}} SATS, non è uguale all'importo impostato.\",\n    \"error_channel_reserves\": \"I fondi disponibili sono insufficienti.\",\n    \"error_address\": \"Indirizzo Lightning non valido\",\n    \"error_channel_reserves_explained\": \"Una parte del saldo del canale è riservata alle commissioni. Provate a inviare un importo inferiore o ad aggiungere fondi.\",\n    \"error_clipboard\": \"Appunti non supportati\",\n    \"error_keysend\": \"Keysend fallito\",\n    \"error_LNURL\": \"LNURL Pay fallito\",\n    \"error_expired\": \"Fattura scaduta\",\n    \"payjoin_send\": \"Questo è un payjoin! The Mutiny continuerà finché la privacy non migliorerà\",\n    \"payment_pending\": \"Pagamento in corso\",\n    \"payment_pending_description\": \"Ci vuole un po' di tempo, ma è possibile che il pagamento vada ancora a buon fine. Controllare la sezione \\\"Attività\\\" per conoscere lo stato attuale.\",\n    \"hodl_invoice_warning\": \"Questa è una fattura hodl. I pagamenti di fatture hodl possono causare la chiusura forzata del canale, con conseguenti commissioni elevate on-chain. Pagate a vostro rischio e pericolo!\",\n    \"private\": \"Privato\",\n    \"publiczap\": \"Zap Pubblico\",\n    \"privatezap\": \"Zap Privato\"\n  },\n  \"feedback\": {\n    \"header\": \"Dateci un riscontro!\",\n    \"received\": \"Riscontro ricevuto!\",\n    \"thanks\": \"Grazie per averci fatto sapere cosa succede.\",\n    \"more\": \"Avete altro da dire?\",\n    \"tracking\": \"Mutiny non traccia né spia il vostro comportamento, quindi il vostro riscontro è incredibilmente utile.\",\n    \"github\": \"Non risponderemo a questo riscontro. Se desiderate assistenza, vi preghiamo di\",\n    \"create_issue\": \"creare una questione su GitHub.\",\n    \"link\": \"Riscontro?\",\n    \"feedback_placeholder\": \"Bug, richieste di funzionalità, riscontro, ecc.\",\n    \"info_label\": \"Includere i dati di contatto\",\n    \"info_caption\": \"Se avete bisogno di un approfondimento su questo problema\",\n    \"email\": \"Email\",\n    \"email_caption\": \"Burners benvenuti\",\n    \"nostr\": \"Nostr\",\n    \"nostr_caption\": \"Il tuo npub più recente\",\n    \"nostr_label\": \"Nostr npub o NIP-05\",\n    \"send_feedback\": \"Invia Riscontro\",\n    \"invalid_feedback\": \"Per favore dicci qualcosa!\",\n    \"need_contact\": \"Ci serve un modo per contattarvi\",\n    \"invalid_email\": \"Non mi sembra un indirizzo email\",\n    \"error\": \"Errore nell'invio del riscontro {{error}}\",\n    \"try_again\": \"Riprova più tardi.\"\n  },\n  \"activity\": {\n    \"title\": \"Attività\",\n    \"mutiny\": \"Mutiny\",\n    \"wallet\": \"Portafoglio\",\n    \"nostr\": \"Nostr\",\n    \"view_all\": \"Vedi tutto\",\n    \"receive_some_sats_to_get_started\": \"Ricevi alcuni sats per iniziare\",\n    \"channel_open\": \"Canale Aperto\",\n    \"channel_close\": \"Canale Chiuso\",\n    \"unknown\": \"Sconosciuto\",\n    \"import_contacts\": \"Importa i tuoi contatti da nostr per vedere chi sta facendo zapping.\",\n    \"coming_soon\": \"Prossimamente\",\n    \"private\": \"Privato\",\n    \"anonymous\": \"Anonimo\",\n    \"from\": \"Da:\",\n    \"transaction_details\": {\n      \"lightning_receive\": \"Ricevuto via Lightning\",\n      \"lightning_send\": \"Inviato via Lightning\",\n      \"channel_open\": \"Canale aperto\",\n      \"channel_close\": \"Canale chiuso\",\n      \"onchain_receive\": \"Ricevi on-chain\",\n      \"onchain_send\": \"Invia on-chain\",\n      \"paid\": \"Pagato\",\n      \"unpaid\": \"Non pagato\",\n      \"status\": \"Stato\",\n      \"date\": \"Data\",\n      \"tagged_to\": \"Contrassegnato da\",\n      \"description\": \"Descrizione\",\n      \"fee\": \"Commissione\",\n      \"onchain_fee\": \"Commissione On-chain\",\n      \"invoice\": \"Fattura\",\n      \"payment_hash\": \"Hash di pagamento\",\n      \"payment_preimage\": \"Preimaggine\",\n      \"txid\": \"Txid\",\n      \"total\": \"Importo richiesto\",\n      \"balance\": \"Bilancio\",\n      \"reserve\": \"Riserva\",\n      \"peer\": \"Peer\",\n      \"channel_id\": \"ID canale\",\n      \"reason\": \"Motivo\",\n      \"confirmed\": \"Confermato\",\n      \"unconfirmed\": \"Non confermato\",\n      \"sweep_delay\": \"I fondi possono impiegare alcuni giorni per essere reimmessi nel portafoglio.\",\n      \"no_details\": \"Non sono stati trovati dettagli sul canale, il che significa che probabilmente questo canale è stato chiuso.\",\n      \"back_home\": \"Torna a home\"\n    },\n    \"start_a_chat\": \"Inzia una chat?\",\n    \"start_a_chat_are_you_sure\": \"Questo utente non è presente nell'elenco dei tuoi contatti.\"\n  },\n  \"scanner\": {\n    \"paste\": \"Incolla qualcosa\",\n    \"cancel\": \"Cancella\"\n  },\n  \"settings\": {\n    \"header\": \"Impostazioni\",\n    \"support\": \"Scopri come sostenere Mutiny\",\n    \"experimental_features\": \"Esperimenti\",\n    \"debug_tools\": \"STRUMENTI DI DEBUG\",\n    \"danger_zone\": \"Zona Pericolosa\",\n    \"general\": \"Generale\",\n    \"version\": \"Versione:\",\n    \"admin\": {\n      \"title\": \"Pagina Amministrativa\",\n      \"caption\": \"I nostri strumenti di debug interni. Utilizzateli con saggezza!\",\n      \"header\": \"Strumenti di Debug segreti\",\n      \"warning_one\": \"Se sai cosa stai facendo, sei nel posto giusto.\",\n      \"warning_two\": \"Si tratta di strumenti interni che utilizziamo per il debug e il test dell'applicazione. Si prega di prestare attenzione!\",\n      \"kitchen_sink\": {\n        \"disconnect\": \"Disconnettere\",\n        \"peers\": \"Peers\",\n        \"no_peers\": \"Nessun peers\",\n        \"refresh_peers\": \"Aggiornare Peers\",\n        \"connect_peer\": \"Connettere Peer\",\n        \"expect_a_value\": \"Aspettatevi un valore...\",\n        \"connect\": \"Connettere\",\n        \"close_channel\": \"Chiudi Canale\",\n        \"force_close\": \"Forza chiusura Canale\",\n        \"abandon_channel\": \"Abbandona Canale\",\n        \"confirm_close_channel\": \"Sei sicuro di voler chiudere questo canale?\",\n        \"confirm_force_close\": \"Sei sicuro di voler chiudere forzatamente questo canale? I fondi impiegheranno alcuni giorni per essere riscattati on-chain.\",\n        \"confirm_abandon_channel\": \"Siete sicuri di voler abbandonare questo canale? In genere lo si fa solo se la transazione di apertura non verrà mai confermata. In caso contrario, si perderanno fondi.\",\n        \"channels\": \"Canali\",\n        \"no_channels\": \"Nessun Canale\",\n        \"refresh_channels\": \"Aggiorna Canali\",\n        \"pubkey\": \"Pubkey\",\n        \"amount\": \"Importo\",\n        \"open_channel\": \"Apri Canale\",\n        \"nodes\": \"Nodi\",\n        \"no_nodes\": \"Nessun nodo\",\n        \"enable_zaps_to_hodl\": \"Abilitare gli zap per le fatture Hodl?\",\n        \"zaps_to_hodl_desc\": \"Gli zapping alle fatture hodl possono provocare la chiusura forzata del canale, con conseguenti costi elevati on-chain. Usatelo a vostro rischio e pericolo!\",\n        \"zaps_to_hodl_enable\": \"Abilitare hodl zaps\",\n        \"zaps_to_hodl_disable\": \"Disabilitare hodl zaps\"\n      }\n    },\n    \"backup\": {\n      \"title\": \"Backup\",\n      \"secure_funds\": \"Mettiamo questi fondi in sicurezza\",\n      \"twelve_words_tip\": \"Vi mostriamo 12 parole. Scrivi le 12 parole.\",\n      \"warning_one\": \"Se si cancella la cronologia del browser o si perde il dispositivo, queste 12 parole sono l'unico modo per ripristinare il portafoglio.\",\n      \"warning_two\": \"Mutiny è auto-custodia. Dipende tutto da voi...\",\n      \"confirm\": \"Ho scritto le parole\",\n      \"responsibility\": \"Sono consapevole che i miei fondi sono sotto la mia responsabilità\",\n      \"liar\": \"Non sto mentendo solo per farla finita con questa storia...\",\n      \"seed_words\": {\n        \"reveal\": \"TOCCA PER RIVELARE LE PAROLE DEL SEME\",\n        \"hide\": \"NASCONDI\",\n        \"copy\": \"È pericoloso copiare negli appunti\",\n        \"copied\": \"Copiato!\"\n      }\n    },\n    \"channels\": {\n      \"title\": \"Canali Lightning\",\n      \"outbound\": \"In uscita\",\n      \"inbound\": \"In entrata\",\n      \"reserve\": \"Riserva\",\n      \"have_channels\": \"Tu hai\",\n      \"have_channels_one\": \"canale lightning.\",\n      \"have_channels_many\": \"canali lightning.\",\n      \"inbound_outbound_tip\": \"In uscita è la quantità di denaro che si può spendere per i servizi lightning. In entrata è l'importo che si può ricevere senza incorrere in una commissione di servizio lightning.\",\n      \"reserve_tip\": \"Circa l'1% del saldo del vostro canale è riservato alle commissioni lightning. Per i canali aperti tramite swap sono necessarie ulteriori riserve.\",\n      \"no_channels\": \"Sembra che tu non abbia ancora nessun canale. Per iniziare, ricevete dei sats su lightning o scambiate dei fondi on-chain in un canale. Sporcatevi le mani!\",\n      \"close_channel\": \"Chiudi\",\n      \"online_channels\": \"Canali Online\",\n      \"offline_channels\": \"Canali Offline\",\n      \"close_channel_confirm\": \"La chiusura di questo canale sposterà il saldo on-chain e comporterà una commissione on-chain.\"\n    },\n    \"connections\": {\n      \"title\": \"Connessioni Portafoglio\",\n      \"error_name\": \"Il nome non può essere vuoto\",\n      \"error_connection\": \"Impossibile creare la Connessione al Portafoglio\",\n      \"error_budget_zero\": \"Il budget deve essere maggiore di zero\",\n      \"add_connection\": \"Aggiungi Connessione\",\n      \"manage_connections\": \"Gestione delle Connessioni\",\n      \"manage_gifts\": \"Gestione Regali\",\n      \"delete_connection\": \"Cancella\",\n      \"new_connection\": \"Nuova Connessione\",\n      \"edit_connection\": \"Modifica Connessione\",\n      \"new_connection_label\": \"Nome\",\n      \"new_connection_placeholder\": \"Il mio client nostr preferito...\",\n      \"create_connection\": \"Crea Connessione\",\n      \"save_connection\": \"Salva modifiche\",\n      \"edit_budget\": \"Modifica Budget\",\n      \"open_app\": \"Apri App\",\n      \"open_in_nostr_client\": \"Apri nel client Nostr\",\n      \"open_in_primal\": \"Apri in Primal\",\n      \"nostr_client_not_found\": \"App Nostr non trovata\",\n      \"client_not_found_description\": \"Installa un client Nostr come Primal, Amethyst, o Damus per aprire questo collegamento.\",\n      \"relay\": \"Relay\",\n      \"authorize\": \"Autorizza servizi esterni a richiedere pagamenti dal vostro portafoglio. Si abbina perfettamente ai client Nostr.\",\n      \"pending_nwc\": {\n        \"title\": \"Richieste in attesa\",\n        \"approve_all\": \"Approva Tutto\",\n        \"deny_all\": \"Rifiuta Tutto\"\n      },\n      \"careful\": \"Fate attenzione a dove condividete questa connessione! Le richieste che rientrano nel budget saranno pagate automaticamente.\",\n      \"spent\": \"Speso\",\n      \"remaining\": \"Rimanente\",\n      \"confirm_delete\": \"Sei sicuro di voler eliminare questo collegamento?\",\n      \"budget\": \"Budget\",\n      \"resets_every\": \"Resetta tutto\",\n      \"resubscribe_date\": \"Reiscriviti su\"\n    },\n    \"emergency_kit\": {\n      \"title\": \"Kit d'emergenza\",\n      \"caption\": \"Diagnostica e risolve i problemi del tuo portafoglio.\",\n      \"emergency_tip\": \"Se il vostro portafoglio sembra essere rotto, ecco alcuni strumenti per cercare di debuggarlo e ripararlo.\",\n      \"questions\": \"In caso di domande sulle funzioni di questi pulsanti, si prega di\",\n      \"link\": \"contattateci per ricevere assistenza.\",\n      \"import_export\": {\n        \"title\": \"Esportazione stato portafoglio\",\n        \"error_password\": \"È richiesta la password\",\n        \"error_read_file\": \"Errore di lettura del file\",\n        \"error_no_text\": \"Nessun testo trovato nel file\",\n        \"tip\": \"È possibile esportare l'intero stato del portafoglio Mutiny in un file e importarlo in un nuovo browser. Di solito funziona!\",\n        \"caveat_header\": \"Avvertenze importanti:\",\n        \"caveat\": \"Dopo l'esportazione, non eseguire alcuna operazione nel browser originale. In caso contrario, sarà necessario esportare di nuovo. Dopo un'importazione riuscita, è consigliabile cancellare lo stato del browser originale per essere sicuri di non creare conflitti.\",\n        \"save_state\": \"Salva stato come file\",\n        \"import_state\": \"Importare lo stato da un file\",\n        \"confirm_replace\": \"Vuoi sostituire il tuo stato con\",\n        \"password\": \"Inserire la password per decodificare\",\n        \"decrypt_wallet\": \"Decodifica Portafoglio\"\n      },\n      \"logs\": {\n        \"title\": \"Scarica i log di debug\",\n        \"something_screwy\": \"Sta succedendo qualcosa di strano? Controlla i log!\",\n        \"download_logs\": \"Scarica i log\",\n        \"password\": \"Immettere la password per decodificare\",\n        \"confirm_password_label\": \"Confirm Password\"\n      },\n      \"delete_everything\": {\n        \"delete\": \"Cancella Tutto\",\n        \"confirm\": \"Questo cancellerà lo stato del nodo. Non può essere annullata!\",\n        \"deleted\": \"Cancellato\",\n        \"deleted_description\": \"Cancellato tutti i dati\"\n      }\n    },\n    \"encrypt\": {\n      \"title\": \"Sicurezza\",\n      \"caption\": \"Eseguire prima il backup per sbloccare la crittografia\",\n      \"header\": \"Criptare le parole seme\",\n      \"hot_wallet_warning\": \"Mutiny è un \\\"hot wallet\\\" quindi ha bisogno delle tue parole seme per funzionare, ma è possibile crittografare queste parole con una password.\",\n      \"password_tip\": \"In questo modo, se qualcuno riesce ad accedere al tuo browser, non avrà comunque accesso ai tuoi fondi.\",\n      \"optional\": \"(opzionale)\",\n      \"existing_password\": \"Password esistente\",\n      \"existing_password_caption\": \"Lasciare vuoto se non si è ancora impostata una password.\",\n      \"new_password_label\": \"Password\",\n      \"new_password_placeholder\": \"Inserire una password\",\n      \"new_password_caption\": \"Questa password verrà utilizzata per criptare le parole seme. Se la dimentichi, dovrai reinserire le tue parole seme per accedere ai tuoi fondi. Hai scritto le tue parole seme, vero?\",\n      \"confirm_password_label\": \"Conferma Password\",\n      \"confirm_password_placeholder\": \"Inserisci la stessa password\",\n      \"encrypt\": \"Crittografa\",\n      \"skip\": \"Salta\",\n      \"error_match\": \"Le password non coincidono\",\n      \"error_same_as_existingpassword\": \"La nuova password non deve corrispondere a quella esistente\"\n    },\n    \"decrypt\": {\n      \"title\": \"Inserisci la tua password\",\n      \"decrypt_wallet\": \"Decodifica portafoglio\",\n      \"forgot_password_link\": \"Password dimenticata?\",\n      \"error_wrong_password\": \"Password non valida\"\n    },\n    \"currency\": {\n      \"title\": \"Valuta\",\n      \"caption\": \"Scegli la coppia di valute preferite\",\n      \"select_currency\": \"Seleziona Valuta\",\n      \"select_currency_label\": \"Coppia di valute\",\n      \"select_currency_caption\": \"Scegliendo una nuova valuta, il portafoglio verrà risincronizzato per ottenere un aggiornamento del prezzo.\",\n      \"request_currency_support_link\": \"Richiedi il supporto per altre valute\",\n      \"error_unsupported_currency\": \"Seleziona una valuta supportata.\"\n    },\n    \"language\": {\n      \"title\": \"Lingua\",\n      \"caption\": \"Scegli la lingua preferita\",\n      \"select_language\": \"Seleziona Lingua\",\n      \"select_language_label\": \"Lingua\",\n      \"select_language_caption\": \"La scelta di una nuova valuta cambierà la lingua del portafoglio, ignorando la lingua corrente del browser.\",\n      \"request_language_support_link\": \"Richiedere il supporto per altre lingue\",\n      \"error_unsupported_language\": \"Seleziona una lingua supportata.\"\n    },\n    \"lnurl_auth\": {\n      \"title\": \"LNURL Auth\",\n      \"auth\": \"Auth\",\n      \"expected\": \"Mi aspetto qualcosa come LNURL...\"\n    },\n    \"plus\": {\n      \"title\": \"Mutiny+\",\n      \"join\": \"Join\",\n      \"sats_per_month\": \"per {{amount}} sats al mese.\",\n      \"lightning_balance\": \"Sono necessari almeno {{amount}} sats nel tuo bilancio lightning per iniziare. Prova prima di acquistare!\",\n      \"restore\": \"Ripristino della Sottoscrizione\",\n      \"ready_to_join\": \"Pronti ad unirsi\",\n      \"click_confirm\": \"Fare clic su conferma per pagare il primo mese.\",\n      \"open_source\": \"Mutiny è open source e auto-ostabile.\",\n      \"optional_pay\": \"Ma si può anche pagare.\",\n      \"paying_for\": \"Pagare per\",\n      \"supports_dev\": \"contribuisce a sostenere lo sviluppo continuo e sblocca l'accesso anticipato a nuove caratteristiche e funzionalità premium:\",\n      \"thanks\": \"Sei parte di mutiny! Godetevi i seguenti vantaggi:\",\n      \"renewal_time\": \"Riceverete una richiesta di pagamento per il rinnovo intorno a\",\n      \"cancel\": \"Per annullare l'abbonamento è sufficiente non pagare. È anche possibile disattivare il Mutiny+\",\n      \"wallet_connection\": \"Connessione Portafoglio.\",\n      \"subscribe\": \"Sottoscrivi\",\n      \"error_no_plan\": \"Nessun piano trovato\",\n      \"error_failure\": \"Impossibile iscriversi\",\n      \"error_no_subscription\": \"Nessun abbonamento esistente trovato\",\n      \"error_expired_subscription\": \"L'abbonamento è scaduto, fare clic su Join per rinnovarlo\",\n      \"satisfaction\": \"Soddisfazione compiaciuta\",\n      \"gifting\": \"Regali\",\n      \"multi_device\": \"Accesso Multi-dispositivo\",\n      \"ios_testflight\": \"Accesso iOS TestFlight\",\n      \"more\": \"... ed altro in arrivo\",\n      \"cta_description\": \"Godete dell'accesso anticipato alle nuove caratteristiche e alle funzionalità premium.\",\n      \"cta_but_already_plus\": \"Grazie per il tuo sostegno!\",\n      \"lightning_address\": \"Il tuo indirizzo lightning personale\"\n    },\n    \"restore\": {\n      \"title\": \"Ripristina\",\n      \"all_twelve\": \"È necessario inserire tutte le 12 parole\",\n      \"wrong_word\": \"Parola sbagliata\",\n      \"paste\": \"Pericoloso incollare dagli Appunti\",\n      \"confirm_text\": \"Sei sicuro di voler ripristinare questo portafoglio? Il portafoglio esistente verrà cancellato!\",\n      \"restore_tip\": \"È possibile ripristinare un Portafoglio Mutiny esistente a partire dalle tua frase di 12 parole seme. Questa operazione sostituirà il portafoglio esistente, quindi assicuratevi di sapere cosa state facendo!\",\n      \"multi_browser_warning\": \"Non utilizzare contemporaneamente su più browser.\",\n      \"error_clipboard\": \"Appunti non supportati\",\n      \"error_word_number\": \"Numero di parole sbagliato\",\n      \"error_invalid_seed\": \"Frase seme non valida\"\n    },\n    \"servers\": {\n      \"title\": \"Servers\",\n      \"caption\": \"Non fidatevi di noi! Utilizzate i vostri server per puntare su Mutiny.\",\n      \"link\": \"Per saperne di più sull'hosting autonomo\",\n      \"proxy_label\": \"Websockets Proxy\",\n      \"proxy_caption\": \"Come il nodo Lightning comunica con il resto della rete.\",\n      \"error_proxy\": \"Dovrebbe essere un url che inizia con wss://\",\n      \"esplora_label\": \"Esplora\",\n      \"esplora_caption\": \"Dati del blocco per informazioni on-chain.\",\n      \"error_esplora\": \"Non mi sembra un URL\",\n      \"rgs_label\": \"RGS\",\n      \"rgs_caption\": \"Rapid Gossip Sync. Dati di rete sulla rete lightning utilizzati per l'instradamento.\",\n      \"error_rgs\": \"Non mi sembra un URL\",\n      \"lsp_label\": \"LSP\",\n      \"lsp_caption\": \"Lightning Service Provider. Apre automaticamente i canali per la liquidità in entrata. Inoltre, le fatture sono protette dalla privacy.\",\n      \"lsps_connection_string_label\": \"LSPS Stringa di collegamento\",\n      \"lsps_connection_string_caption\": \"Lightning Service Provider. Apre automaticamente i canali per la liquidità in entrata. Utilizzando le specifiche LSP.\",\n      \"error_lsps_connection_string\": \"Non sembra una stringa di connessione al nodo\",\n      \"lsps_token_label\": \"LSPS Token\",\n      \"lsps_token_caption\": \"LSPS Token.  Utilizzato per identificare il portafoglio che si connette all'LSP.\",\n      \"lsps_valid_error\": \"È possibile avere solo un set LSP o un set LSPS Connection String e LSPS Token, ma non entrambi.\",\n      \"error_lsps_token\": \"Non sembra un token valido.\",\n      \"storage_label\": \"Storage\",\n      \"storage_caption\": \"Encrypted VSS backup service.\",\n      \"error_lsp\": \"Non mi sembra un URL\",\n      \"error_tor\": \"Tor URL non sono attualmente supportati\",\n      \"save\": \"Salva\"\n    },\n    \"nostr_contacts\": {\n      \"title\": \"Sincronizzazione dei contatti Nostr\",\n      \"npub_label\": \"Nostr npub\",\n      \"npub_required\": \"Npub non può essere vuoto\",\n      \"sync\": \"Sincronizzazione\",\n      \"resync\": \"Risincronizzazione\",\n      \"remove\": \"Rimuovere\"\n    },\n    \"manage_federations\": {\n      \"title\": \"Gestione Federazioni\",\n      \"federation_code_label\": \"Codice Federazione\",\n      \"federation_code_required\": \"Il codice Federazione non può essere vuoto\",\n      \"federation_added_success\": \"Federazione aggiunta con successo\",\n      \"federation_remove_confirm\": \"Sei sicuro di voler rimuovere questa Federazione? Assicurati che i fondi in tuo possesso siano prima trasferiti al vostro saldo lightning o ad un altro portafoglio.\",\n      \"add\": \"Aggiungere\",\n      \"remove\": \"Rimuovere\",\n      \"expires\": \"Scadenza\",\n      \"federation_id\": \"ID Federazione\",\n      \"description\": \"Mutiny ha un supporto sperimentale per il protocollo Fedimint. Conservate i fondi in una Federazione a vostro rischio e pericolo!\",\n      \"learn_more\": \"Per saperne di più su Fedimint.\",\n      \"discover\": \"Scopri Federazioni\",\n      \"manual\": \"Codice di invito\",\n      \"created_at\": \"Creato presso\",\n      \"recommended_by\": \"Consigliato da\",\n      \"already_in_fed\": \"Sei già in una federazione!\"\n    },\n    \"gift\": {\n      \"give_sats_link\": \"Date sats come regalo\",\n      \"something_went_wrong\": \"Qualcosa è andato storto\",\n      \"title\": \"Regali\",\n      \"no_plus_caption\": \"Upgrade a Mutiny+ per abilitare i regali\",\n      \"receive_too_small\": \"La prima ricezione deve essere {{amount}} SATS o maggiore.\",\n      \"setup_fee_lightning\": \"Per ricevere questo regalo verrà addebitata una commisione lightning di configurazione.\",\n      \"already_claimed\": \"Questo regalo è già stato richiesto\",\n      \"sender_is_poor\": \"Il mittente non ha un saldo sufficiente per pagare questo regalo.\",\n      \"sender_timed_out\": \"Il pagamento del regalo è scaduto. Il mittente potrebbe essere offline, oppure il regalo è già stato richiesto.\",\n      \"sender_generic_error\": \"Errore inviato dal mittente: {{error}}\",\n      \"receive_header\": \"Ti sono stati donati alcuni sats!\",\n      \"receive_description\": \"Dovete essere piuttosto speciali. Per richiedere il denaro è sufficiente premere il pulsante grande. I fondi saranno aggiunti a questo portafoglio la prossima volta che il vostro donatore sarà online.\",\n      \"receive_claimed\": \"Regalo richiesto! Il regalo dovrebbe arrivare a breve sul vostro saldo.\",\n      \"receive_cta\": \"Richiesta Regalo\",\n      \"receive_try_again\": \"Riprova\",\n      \"send_header\": \"Crea Regalo\",\n      \"send_explainer\": \"Regala dei sats. Crea un URL regalo Mutiny che possa essere richiesto da chiunque abbia un browser web.\",\n      \"send_name_required\": \"Questo è per la vostra documentazione\",\n      \"send_name_label\": \"Nome del destinatario\",\n      \"send_header_claimed\": \"Regalo ricevuto!\",\n      \"send_claimed\": \"Il vostro dono è stato richiesto. Grazie per la condivisione.\",\n      \"send_sharable_header\": \"URL di condivisione\",\n      \"send_instructions\": \"Copiate l'URL del regalo al vostro destinatario o chiedetegli di scansionare questo codice QR con il suo portafoglio.\",\n      \"send_another\": \"Crea un altro\",\n      \"send_small_warning\": \"Un nuovo utente Mutiny non potrà riscattare meno di 100k sats.\",\n      \"send_cta\": \"Crea un regalo\",\n      \"send_delete_button\": \"Cancella regalo\",\n      \"send_delete_confirm\": \"Sei sicuro di voler cancellare questo regalo?\",\n      \"send_tip\": \"Il tuo Mutiny Wallet deve essere aperto per poter riscattare il regalo.\",\n      \"need_plus\": \"Passa a Mutiny+ per abilitare la creazione di regali. Questo consente di creare una URL di regalo Mutiny che può essere richiesto da chiunque abbia un browser web.\"\n    },\n    \"appearance\": \"Aspetto\",\n    \"social\": \"Social\",\n    \"nostr_keys\": {\n      \"title\": \"Chiavi Nostr\",\n      \"caption\": \"Non le tue chiavi, non le tue note\",\n      \"description\": \"È possibile utilizzare lo stesso profilo nostr in più app.\",\n      \"warning\": \"Fate attenzione a dove condividete la vostra chiave nostr privata!\",\n      \"learn_more\": \"Per saperne di più su Nostr\",\n      \"import_profile\": \"Importare un profilo Nostr\",\n      \"delete_account\": \"Cancellare l'account\",\n      \"delete_account_confirm\": \"L'eliminazione di un profilo nostr non può essere annullata e disabilita alcune funzioni sociali del portafoglio.\",\n      \"unlink_account\": \"Scollega il profilo Nostr\",\n      \"unlink_account_confirm\": \"Scollegando il tuo profilo Nostr, il portafoglio verrà ripristinato al profilo Nostr predefinito.\",\n      \"delete_account_confirm_scary\": \"QUESTO PROFILO VERRÀ ELIMINATO IN TUTTE LE APP NOSTR.\"\n    },\n    \"lightning_address\": {\n      \"title\": \"Indirizzo Mutiny\",\n      \"description\": \"Ricevi zap e pagamenti nel tuo portafoglio con il tuo indirizzo lightning.\",\n      \"create\": \"Crea indirizzo Mutiny\"\n    }\n  },\n  \"swap\": {\n    \"peer_not_found\": \"Peer non trovato\",\n    \"channel_too_small\": \"È abbastanza stupido creare un canale più piccolo di {{amount}} sats\",\n    \"insufficient_funds\": \"Non avete abbastanza fondi per realizzare questo canale\",\n    \"header\": \"Scambia a Lightning\",\n    \"initiated\": \"Scambio avviato\",\n    \"sats_added\": \"+{{amount}} sats verrà aggiunto al tuo saldo Lightning\",\n    \"use_existing\": \"Usare i peer esistenti\",\n    \"choose_peer\": \"Scegli un peer\",\n    \"peer_connect_label\": \"Connetti ad un nuovo peer\",\n    \"peer_connect_placeholder\": \"Stringa di connessione peer\",\n    \"connect\": \"Connetti\",\n    \"connecting\": \"Connettendo...\",\n    \"confirm_swap\": \"Conferma Scambio\"\n  },\n  \"swap_lightning\": {\n    \"insufficient_funds\": \"Non disponi di fondi sufficienti per scambiare a lightning\",\n    \"header\": \"Scambia a Lightning\",\n    \"header_preview\": \"Anteprima Scambio\",\n    \"completed\": \"Scambio Completato\",\n    \"too_small\": \"Quantità inserita non valida. È necessario scambiare almeno 100k sats.\",\n    \"sats_added\": \"+{{amount}} sats sono stati aggiunti al tuo saldo Lightning\",\n    \"sats_fee\": \"+{{amount}} commissione sats\",\n    \"confirm_swap\": \"Conferma Scambio\",\n    \"preview_swap\": \"Anteprima commissioni Scambio\"\n  },\n  \"reload\": {\n    \"mutiny_update\": \"Aggiornamento Mutiny\",\n    \"new_version_description\": \"La nuova versione di Mutiny è stata memorizzata nella cache, ricaricarla per iniziare a usarla.\",\n    \"reload\": \"Ricarica\"\n  },\n  \"error\": {\n    \"title\": \"Errore\",\n    \"emergency_link\": \"kit d'emergenza.\",\n    \"reload\": \"Ricarica\",\n    \"restart\": {\n      \"title\": \"C'è qualcosa di *extra* strano in atto? Ferma i nodi!\",\n      \"start\": \"Avvia\",\n      \"stop\": \"Stop\"\n    },\n    \"general\": {\n      \"oh_no\": \"Oh no!\",\n      \"never_should_happen\": \"Questo non sarebbe mai dovuto accadere\",\n      \"try_reloading\": \"Provare a ricaricare la pagina o a fare clic sul  \\\"Dangit\\\" bottone. Se continui ad avere problemi,\",\n      \"support_link\": \"contattateci per ricevere assistenza.\",\n      \"getting_desperate\": \"Siete disperati? Provate il\"\n    },\n    \"load_time\": {\n      \"stuck\": \"Bloccato su questa schermata? Prova a ricaricare. Se non funziona, consulta la sezione\"\n    },\n    \"not_found\": {\n      \"title\": \"Non trovato\",\n      \"wtf_paul\": \"Probabilmente la colpa è di Paul.\"\n    },\n    \"resync\": {\n      \"incorrect_balance\": \"Il saldo on-chain non sembra corretto? Provare a risincronizzare il portafoglio on-chain.\",\n      \"resync_wallet\": \"Risincronizza il portafoglio\"\n    },\n    \"on_boot\": {\n      \"existing_tab\": {\n        \"title\": \"Rilevamento di schede multiple\",\n        \"description\": \"Mutiny può essere utilizzato solo in una scheda alla volta. Sembra che sia aperta un'altra scheda con Mutiny in esecuzione. Chiudere quella scheda e aggiornare questa pagina, oppure chiudere questa scheda e aggiornare l'altra.\"\n      },\n      \"already_running\": {\n        \"title\": \"Mutiny potrebbe essere in esecuzione su un altro dispositivo\",\n        \"description\": \"Mutiny può essere utilizzato solo da una postazione alla volta. Sembra che questo portafoglio sia utilizzato da un altro dispositivo o browser. Se recentemente hai chiuso Mutiny su un altro dispositivo, attendi qualche minuto e riprova.\",\n        \"retry_again_in\": \"Riprova in\",\n        \"seconds\": \"secondi\"\n      },\n      \"incompatible_browser\": {\n        \"title\": \"Browser non compatibile\",\n        \"header\": \"Rilevato browser non compatibile\",\n        \"description\": \"Mutiny richiede un browser moderno che supporti WebAssembly, LocalStorage e IndexedDB. Alcuni browser disabilitano queste funzionalità in modalità privata.\",\n        \"try_different_browser\": \"Assicuratevi che il vostro browser supporti tutte queste funzioni, oppure provate un altro browser. Potreste anche provare a disabilitare alcune estensioni o \\\"shields\\\" che bloccano queste funzionalità.\",\n        \"browser_storage\": \"(Ci piacerebbe supportare altri browser privati, ma dobbiamo salvare i dati del portafoglio nella memoria del browser, altrimenti i fondi andranno persi).\",\n        \"browsers_link\": \"Browser supportati\"\n      },\n      \"loading_failed\": {\n        \"title\": \"Caricamento fallito\",\n        \"header\": \"Impossibile caricare Mutiny\",\n        \"description\": \"Qualcosa è andato storto durante l'avvio di Mutiny Wallet.\",\n        \"repair_options\": \"Se il vostro portafoglio sembra essere rotto, ecco alcuni strumenti per cercare di debuggarlo e ripararlo.\",\n        \"questions\": \"In caso di domande sulle funzioni di questi pulsanti, si prega di\",\n        \"support_link\": \"contattarci per ricevere assistenza.\",\n        \"services_down\": \"Sembra che uno dei servizi di Mutiny sia inattivo. Si prega di riprovare più tardi.\",\n        \"in_the_meantime\": \"Nel frattempo, se volete accedere ai vostri fondi on-chain, potete caricare Mutiny in\",\n        \"safe_mode\": \"Modalità provvisoria\"\n      }\n    }\n  },\n  \"modals\": {\n    \"share\": \"Condividi\",\n    \"details\": \"Dettagli\",\n    \"loading\": {\n      \"loading\": \"Caricando: {{stage}}\",\n      \"default\": \"Solo iniziando l'avvio\",\n      \"double_checking\": \"Doppio controllo su qualcosa\",\n      \"existing_wallet\": \"Controllo del portafoglio esistente\",\n      \"downloading\": \"Scaricando\",\n      \"setup\": \"Impostazione\",\n      \"done\": \"Fatto\"\n    },\n    \"onboarding\": {\n      \"welcome\": \"Benvenuto!\",\n      \"setup\": \"Impostazione\",\n      \"restore_from_backup\": \"Se hai già utilizzato Mutiny in precedenza, puoi ripristinare da un backup. Altrimenti puoi saltare questo passaggio e goderti il tuo nuovo portafoglio!\",\n      \"not_available\": \"Non lo facciamo ancora\",\n      \"secure_your_funds\": \"Proteggete i tuoi fondi\"\n    },\n    \"more_info\": {\n      \"whats_with_the_fees\": \"Perché queste commissioni?\",\n      \"self_custodial\": \"Mutiny è un portafoglio auto-custodiale. Per avviare un pagamento lightning è necessario aprire un canale lightning, che richiede un importo minimo e una commissione di configurazione.\",\n      \"future_payments\": \"I pagamenti futuri, sia in invio che in ricezione, comporteranno solo le normali commissioni di rete e una commissione di servizio nominale, a meno che il vostro canale non esaurisca la capacità in entrata.\",\n      \"liquidity\": \"Maggiori informazioni sulla liquidità\",\n      \"fedimint\": \"Se si entra a far parte di una federazione, non è richiesta alcuna commissione di configurazione o minimo.\"\n    },\n    \"confirm_dialog\": {\n      \"are_you_sure\": \"Sei sicuro?\",\n      \"cancel\": \"Cancella\",\n      \"confirm\": \"Conferma\"\n    },\n    \"lnurl_auth\": {\n      \"auth_request\": \"Richiesta di autenticazione\",\n      \"login\": \"Accesso\",\n      \"decline\": \"Declina\",\n      \"error\": \"Per qualche motivo non ha funzionato.\",\n      \"authenticated\": \"Autenticato!\"\n    }\n  },\n  \"setup\": {\n    \"new_profile\": {\n      \"description\": \"Mutiny rende i pagamenti social.\"\n    },\n    \"skip\": \"Salta per adesso\",\n    \"import_profile\": \"Importare un profilo Nostr esistente\",\n    \"import\": {\n      \"title\": \"Importazione del profilo Nostr\",\n      \"description\": \"Accesso con un account Nostr esistente.\"\n    }\n  }\n}\n"
  },
  {
    "path": "public/i18n/ko.json",
    "content": "{\n  \"common\": {\n    \"title\": \"Mutiny 지갑\",\n    \"nice\": \"멋지다\",\n    \"home\": \"홈\",\n    \"sats\": \"SATS\",\n    \"sat\": \"SAT\",\n    \"usd\": \"USD\",\n    \"fee\": \"수수료\",\n    \"send\": \"보내기\",\n    \"receive\": \"받기\",\n    \"dangit\": \"땡글\",\n    \"back\": \"뒤로\",\n    \"coming_soon\": \"(곧 출시 예정)\",\n    \"copy\": \"복사\",\n    \"copied\": \"복사됨\",\n    \"continue\": \"계속\",\n    \"error_unimplemented\": \"미구현\",\n    \"why\": \"왜?\",\n    \"view_transaction\": \"거래 보기\",\n    \"private_tags\": \"비공개 태그\",\n    \"pending\": \"대기 중\"\n  },\n  \"contacts\": {\n    \"new\": \"새로 만들기\",\n    \"add_contact\": \"연락처 추가\",\n    \"new_contact\": \"새 연락처\",\n    \"create_contact\": \"연락처 생성\",\n    \"edit_contact\": \"연락처 수정\",\n    \"save_contact\": \"연락처 저장\",\n    \"payment_history\": \"결제 기록\",\n    \"no_payments\": \"아직 결제 기록이 없습니다.\",\n    \"edit\": \"수정\",\n    \"pay\": \"지불\",\n    \"name\": \"이름\",\n    \"placeholder\": \"사토시\",\n    \"unimplemented\": \"미구현\",\n    \"not_available\": \"아직 제공되지 않습니다.\",\n    \"error_name\": \"이름은 필수입니다.\"\n  },\n  \"receive\": {\n    \"receive_bitcoin\": \"비트코인 받기\",\n    \"edit\": \"수정\",\n    \"checking\": \"확인 중\",\n    \"choose_format\": \"포맷 선택\",\n    \"payment_received\": \"결제 완료\",\n    \"payment_initiated\": \"결제 시작됨\",\n    \"receive_add_the_sender\": \"송신자를 기록에 추가하세요.\",\n    \"choose_payment_format\": \"결제 포맷 선택\",\n    \"unified_label\": \"통합\",\n    \"unified_caption\": \"비트코인 주소와 라이트닝 인보이스를 결합합니다. 송신자가 결제 방법을 선택합니다.\",\n    \"lightning_label\": \"라이트닝 인보이스\",\n    \"lightning_caption\": \"작은 거래에 적합합니다. 보통 온체인 수수료보다 낮습니다.\",\n    \"onchain_label\": \"비트코인 주소\",\n    \"onchain_caption\": \"온체인, 사토시가 한 것처럼. 아주 큰 거래에 적합합니다.\",\n    \"unified_setup_fee\": \"라이트닝으로 지불하는 경우 {{amount}} SATS의 라이트닝 설치 비용이 부과됩니다.\",\n    \"lightning_setup_fee\": \"이 받기에는 {{amount}} SATS의 라이트닝 설치 비용이 부과됩니다.\",\n    \"amount\": \"금액\",\n    \"fee\": \"+ 수수료\",\n    \"total\": \"합계\",\n    \"spendable\": \"사용 가능\",\n    \"channel_size\": \"채널 크기\",\n    \"channel_reserve\": \"- 채널 예비금\",\n    \"amount_editable\": {\n      \"receive_too_small\": \"첫 라이트닝 받기는 {{amount}} SATS 이상이어야 합니다. 요청한 금액에서 설정 비용이 차감됩니다.\",\n      \"setup_fee_lightning\": \"라이트닝으로 지불하는 경우 라이트닝 설치 비용이 부과됩니다.\",\n      \"more_than_21m\": \"비트코인은 총 2,100만 개밖에 없습니다.\",\n      \"set_amount\": \"금액 설정\",\n      \"max\": \"최대\",\n      \"fix_amounts\": {\n        \"ten_k\": \"1만\",\n        \"one_hundred_k\": \"10만\",\n        \"one_million\": \"100만\"\n      },\n      \"del\": \"삭제\"\n    },\n    \"integrated_qr\": {\n      \"onchain\": \"온체인\",\n      \"lightning\": \"라이트닝\",\n      \"unified\": \"통합\"\n    }\n  },\n  \"send\": {\n    \"sending\": \"보내는 중...\",\n    \"confirm_send\": \"보내기 확인\",\n    \"contact_placeholder\": \"기록을 위해 수신자 추가\",\n    \"start_over\": \"다시 시작\",\n    \"paste\": \"붙여넣기\",\n    \"scan_qr\": \"QR 스캔\",\n    \"payment_initiated\": \"결제 시작됨\",\n    \"payment_sent\": \"결제 완료\",\n    \"destination\": \"수신처\",\n    \"progress_bar\": {\n      \"of\": \"/\",\n      \"sats_sent\": \"SATS 보냄\"\n    },\n    \"error_low_balance\": \"지불할 금액보다 충분한 잔액이 없습니다.\",\n    \"error_clipboard\": \"클립보드를 지원하지 않습니다.\",\n    \"error_keysend\": \"KeySend 실패\",\n    \"error_LNURL\": \"LNURL Pay 실패\"\n  },\n  \"feedback\": {\n    \"header\": \"피드백 주세요!\",\n    \"received\": \"피드백이 수신되었습니다!\",\n    \"thanks\": \"문제가 발생했음을 알려주셔서 감사합니다.\",\n    \"more\": \"더 하실 말씀이 있으신가요?\",\n    \"tracking\": \"Mutiny는 사용자 행동을 추적하거나 감시하지 않기 때문에 피드백이 매우 유용합니다.\",\n    \"github_one\": \"GitHub에 익숙하시다면\",\n    \"github_two\": \"를 사용하여\",\n    \"create_issue\": \"이슈를 생성하세요\",\n    \"link\": \"피드백?\",\n    \"feedback_placeholder\": \"버그, 기능 요청, 피드백 등\",\n    \"info_label\": \"연락처 정보 포함\",\n    \"info_caption\": \"문제에 대한 후속 조치를 필요로 하는 경우\",\n    \"email\": \"이메일\",\n    \"email_caption\": \"일회용 이메일 사용 가능\",\n    \"nostr\": \"Nostr\",\n    \"nostr_caption\": \"신선한 npub\",\n    \"nostr_label\": \"Nostr npub 또는 NIP-05\",\n    \"send_feedback\": \"피드백 보내기\",\n    \"invalid_feedback\": \"피드백을 입력하세요.\",\n    \"need_contact\": \"연락처 정보가 필요합니다.\",\n    \"invalid_email\": \"올바른 이메일 주소가 아닙니다.\",\n    \"error\": \"피드백 전송 오류\",\n    \"try_again\": \"나중에 다시 시도하세요.\"\n  },\n  \"activity\": {\n    \"title\": \"활동\",\n    \"mutiny\": \"Mutiny\",\n    \"nostr\": \"Nostr\",\n    \"view_all\": \"전체 보기\",\n    \"receive_some_sats_to_get_started\": \"시작하려면 일부 SATS를 받으세요\",\n    \"channel_open\": \"채널 오픈\",\n    \"channel_close\": \"채널 닫기\",\n    \"unknown\": \"알 수 없음\",\n    \"import_contacts\": \"Nostr에서 연락처를 가져와 누가 체널을 열고 있는지 확인하세요.\",\n    \"coming_soon\": \"곧 출시 예정\",\n    \"transaction_details\": {\n      \"lightning_receive\": \"라이트닝 입금\",\n      \"lightning_send\": \"라이트닝 송금\",\n      \"channel_open\": \"채널 개설\",\n      \"channel_close\": \"채널 종료\",\n      \"onchain_receive\": \"체인상 입금\",\n      \"onchain_send\": \"체인상 송금\",\n      \"paid\": \"지불 완료\",\n      \"unpaid\": \"미지불\",\n      \"status\": \"상태\",\n      \"when\": \"시간\",\n      \"description\": \"설명\",\n      \"fee\": \"수수료\",\n      \"fees\": \"수수료\",\n      \"bolt11\": \"Bolt11\",\n      \"payment_hash\": \"지불 해시\",\n      \"preimage\": \"사전 이미지\",\n      \"txid\": \"거래 ID\",\n      \"balance\": \"잔고\",\n      \"reserve\": \"리저브\",\n      \"peer\": \"피어\",\n      \"channel_id\": \"채널 ID\",\n      \"reason\": \"이유\",\n      \"confirmed\": \"확인됨\",\n      \"unconfirmed\": \"확인 대기\",\n      \"no_details\": \"채널 상세정보를 찾을 수 없습니다. 이는 해당 채널이 종료된 것으로 보입니다.\"\n    }\n  },\n  \"scanner\": {\n    \"paste\": \"붙여넣기\",\n    \"cancel\": \"취소\"\n  },\n  \"settings\": {\n    \"header\": \"설정\",\n    \"support\": \"Mutiny 지원 방법 알아보기\",\n    \"general\": \"일반\",\n    \"experimental_features\": \"실험\",\n    \"debug_tools\": \"디버그 도구\",\n    \"danger_zone\": \"위험 지역\",\n    \"admin\": {\n      \"title\": \"관리자 페이지\",\n      \"caption\": \"내부 디버그 도구입니다. 신중하게 사용하세요!\",\n      \"header\": \"비밀 디버그 도구\",\n      \"warning_one\": \"잘 알고 있는 경우 올바른 위치입니다.\",\n      \"warning_two\": \"디버그 및 테스트에 사용하는 내부 도구입니다. 주의하세요!\",\n      \"kitchen_sink\": {\n        \"disconnect\": \"연결 끊기\",\n        \"peers\": \"피어\",\n        \"no_peers\": \"피어 없음\",\n        \"refresh_peers\": \"피어 새로고침\",\n        \"connect_peer\": \"피어 연결\",\n        \"expect_a_value\": \"값을 입력하세요...\",\n        \"connect\": \"연결\",\n        \"close_channel\": \"채널 종료\",\n        \"force_close\": \"강제 종료\",\n        \"abandon_channel\": \"채널 포기\",\n        \"confirm_close_channel\": \"이 채널을 종료하시겠습니까?\",\n        \"confirm_force_close\": \"이 채널을 강제로 종료하시겠습니까? 자금은 몇 일 이후에 체인상에서 사용 가능해집니다.\",\n        \"confirm_abandon_channel\": \"이 채널을 포기하시겠습니까? 대개 개방 트랜잭션이 확인되지 않는다면 이렇게 하십시오. 그렇지 않으면 자금을 잃게 될 수 있습니다.\",\n        \"channels\": \"채널\",\n        \"no_channels\": \"채널 없음\",\n        \"refresh_channels\": \"채널 새로고침\",\n        \"pubkey\": \"퍼블릭 키\",\n        \"amount\": \"금액\",\n        \"open_channel\": \"채널 개설\",\n        \"nodes\": \"노드\",\n        \"no_nodes\": \"노드 없음\"\n      }\n    },\n    \"backup\": {\n      \"title\": \"백업\",\n      \"secure_funds\": \"자금을 안전하게 보호하세요.\",\n      \"twelve_words_tip\": \"12개의 단어를 보여드립니다. 12개의 단어를 기록하세요.\",\n      \"warning_one\": \"브라우저 기록을 지우거나 기기를 분실하면 이 12개의 단어만으로 지갑을 복원할 수 있습니다.\",\n      \"warning_two\": \"Mutiny는 사용자의 자산을 사용자 스스로 관리해야 합니다...\",\n      \"confirm\": \"12개의 단어를 기록했습니다.\",\n      \"responsibility\": \"자금이 사용자 스스로의 책임임을 이해합니다.\",\n      \"liar\": \"속이려는 것이 아닙니다.\",\n      \"seed_words\": {\n        \"reveal\": \"씨드 단어 공개\",\n        \"hide\": \"숨기기\",\n        \"copy\": \"클립보드에 복사\",\n        \"copied\": \"복사됨!\"\n      }\n    },\n    \"channels\": {\n      \"title\": \"라이트닝 채널\",\n      \"outbound\": \"송신\",\n      \"inbound\": \"수신\",\n      \"have_channels\": \"라이트닝 채널이\",\n      \"have_channels_one\": \"개 있습니다.\",\n      \"have_channels_many\": \"개 있습니다.\",\n      \"inbound_outbound_tip\": \"송신은 라이트닝으로 지출할 수 있는 금액을 나타냅니다. 수신은 수수료 없이 받을 수 있는 금액을 나타냅니다.\",\n      \"no_channels\": \"아직 채널이 없는 것 같습니다. 먼저 라이트닝으로 몇 sats를 받거나 체인상 자금을 채널로 바꾸세요. 시작해보세요!\"\n    },\n    \"connections\": {\n      \"title\": \"지갑 연결\",\n      \"error_name\": \"이름을 입력하세요.\",\n      \"error_connection\": \"지갑 연결 생성에 실패했습니다.\",\n      \"add_connection\": \"연결 추가\",\n      \"manage_connections\": \"연결 관리\",\n      \"disable_connection\": \"비활성화\",\n      \"enable_connection\": \"활성화\",\n      \"new_connection\": \"새로운 연결\",\n      \"new_connection_label\": \"이름\",\n      \"new_connection_placeholder\": \"내가 좋아하는 nostr 클라이언트...\",\n      \"create_connection\": \"연결 생성\",\n      \"open_app\": \"앱 열기\",\n      \"open_in_nostr_client\": \"Nostr 클라이언트에서 열기\",\n      \"open_in_primal\": \"Primal에서 열기\",\n      \"authorize\": \"외부 서비스가 지갑에서 결제를 요청할 수 있도록 인증합니다. nostr 클라이언트와 잘 맞습니다.\",\n      \"pending_nwc\": {\n        \"title\": \"대기 중인 요청\",\n        \"configure_link\": \"설정\"\n      }\n    },\n    \"emergency_kit\": {\n      \"title\": \"비상 키트\",\n      \"caption\": \"지갑 문제를 진단하고 해결하는 도구입니다.\",\n      \"emergency_tip\": \"지갑이 망가지는 것 같다면 이 도구를 사용하여 문제를 진단하고 해결하세요.\",\n      \"questions\": \"이 버튼들이 무엇을 하는지 궁금하다면, 지원을 받으시려면\",\n      \"link\": \"연락처를 통해 문의해주세요.\",\n      \"import_export\": {\n        \"title\": \"지갑 상태 내보내기\",\n        \"error_password\": \"비밀번호가 필요합니다.\",\n        \"error_read_file\": \"파일 읽기 오류\",\n        \"error_no_text\": \"파일에서 텍스트를 찾을 수 없습니다.\",\n        \"tip\": \"Mutiny 지갑 상태 전체를 파일로 내보내서 새 브라우저에 가져와서 복원할 수 있습니다. 보통 동작합니다!\",\n        \"caveat_header\": \"주의 사항:\",\n        \"caveat\": \"내보낸 후에는 원래 브라우저에서 아무 동작도 수행하지 마세요. 그렇게 하면 다시 내보내야 합니다. 성공적인 가져오기 후에는 원래 브라우저의 상태를 초기화하는 것이 좋습니다.\",\n        \"save_state\": \"상태를 파일로 저장\",\n        \"import_state\": \"파일에서 상태 가져오기\",\n        \"confirm_replace\": \"상태를 다음으로 대체하시겠습니까?\",\n        \"password\": \"복호화를 위해 비밀번호 입력\",\n        \"decrypt_wallet\": \"지갑 복호화\"\n      },\n      \"logs\": {\n        \"title\": \"디버그 로그 다운로드\",\n        \"something_screwy\": \"문제가 발생했나요? 로그를 확인하세요!\",\n        \"download_logs\": \"로그 다운로드\"\n      },\n      \"delete_everything\": {\n        \"delete\": \"모두 삭제\",\n        \"confirm\": \"노드 상태가 모두 삭제됩니다. 복구할 수 없습니다!\",\n        \"deleted\": \"삭제됨\",\n        \"deleted_description\": \"모든 데이터 삭제됨\"\n      }\n    },\n    \"encrypt\": {\n      \"header\": \"보안\",\n      \"hot_wallet_warning\": \"Mutiny는 &rdquo;핫 월렛&rdquo;이므로 시드 단어를 사용하여 작동하지만 선택적으로 비밀번호로 암호화할 수 있습니다.\",\n      \"password_tip\": \"이렇게 하면 다른 사람이 브라우저에 접근하더라도 자금에 접근할 수 없습니다.\",\n      \"optional\": \"(선택 사항)\",\n      \"existing_password\": \"기존 비밀번호\",\n      \"existing_password_caption\": \"비밀번호를 설정하지 않았다면 비워 두세요.\",\n      \"new_password_label\": \"비밀번호\",\n      \"new_password_placeholder\": \"비밀번호를 입력하세요\",\n      \"new_password_caption\": \"이 비밀번호는 시드 단어를 암호화하는 데 사용됩니다. 이를 잊어버리면 자금에 접근하려면 시드 단어를 다시 입력해야 합니다. 시드 단어를 기록해 두었나요?\",\n      \"confirm_password_label\": \"비밀번호 확인\",\n      \"confirm_password_placeholder\": \"동일한 비밀번호를 입력하세요\",\n      \"encrypt\": \"암호화\",\n      \"skip\": \"건너뛰기\",\n      \"error_match\": \"비밀번호가 일치하지 않습니다.\"\n    },\n    \"decrypt\": {\n      \"title\": \"비밀번호를 입력하세요\",\n      \"decrypt_wallet\": \"지갑 복호화\",\n      \"forgot_password_link\": \"비밀번호를 잊으셨나요?\",\n      \"error_wrong_password\": \"유효하지 않은 비밀번호\"\n    },\n    \"lnurl_auth\": {\n      \"title\": \"LNURL 인증\",\n      \"auth\": \"인증\",\n      \"expected\": \"LNURL과 같은 형식으로 입력해주세요.\"\n    },\n    \"plus\": {\n      \"title\": \"Mutiny+\",\n      \"join\": \"가입\",\n      \"for\": \"에 대해\",\n      \"sats_per_month\": \"sats 월별 비용입니다.\",\n      \"you_need\": \"적어도 다음 금액이 필요합니다.\",\n      \"lightning_balance\": \"라이트닝 잔액에서 sats를 지불하세요. 먼저 시험해보세요!\",\n      \"restore\": \"구독 복원\",\n      \"ready_to_join\": \"가입할 준비가 되었습니다.\",\n      \"click_confirm\": \"첫 달 비용을 지불하려면 확인을 클릭하세요.\",\n      \"open_source\": \"Mutiny는 오픈 소스이며 스스로 호스팅할 수 있습니다.\",\n      \"optional_pay\": \"또한 지불할 수도 있습니다.\",\n      \"paying_for\": \"지불 대상:\",\n      \"supports_dev\": \"는 지속적인 개발을 지원하고 새 기능과 프리미엄 기능의 조기 액세스를 제공합니다.\",\n      \"thanks\": \"Mutiny의 일원이 되셨습니다! 다음 혜택을 즐기세요:\",\n      \"renewal_time\": \"다음 시기에 갱신 요청이 도착합니다.\",\n      \"cancel\": \"구독을 취소하려면 결제하지 않으세요. 또는 Mutiny+ 기능을 비활성화할 수도 있습니다.\",\n      \"wallet_connection\": \"지갑 연결 기능.\",\n      \"subscribe\": \"구독하기\",\n      \"error_no_plan\": \"\",\n      \"error_failure\": \"\",\n      \"error_no_subscription\": \"기존 구독이 없습니다.\",\n      \"satisfaction\": \"만족함\",\n      \"gifting\": \"선물하기\",\n      \"multi_device\": \"다중 장치 접속\",\n      \"more\": \"... 그리고 더 많은 기능이 추가될 예정입니다.\"\n    },\n    \"restore\": {\n      \"title\": \"복원\",\n      \"all_twelve\": \"12개 단어를 모두 입력해야 합니다.\",\n      \"wrong_word\": \"잘못된 단어\",\n      \"paste\": \"클립보드에서 붙여넣기 (위험)\",\n      \"confirm_text\": \"이 지갑으로 복원하시겠습니까? 기존 지갑이 삭제됩니다!\",\n      \"restore_tip\": \"기존 Mutiny 지갑을 12개의 씨드 단어로 복원할 수 있습니다. 기존 지갑이 대체됩니다. 신중하게 사용하세요!\",\n      \"multi_browser_warning\": \"여러 브라우저에서 동시에 사용하지 마세요.\",\n      \"error_clipboard\": \"클립보드를 지원하지 않습니다.\",\n      \"error_word_number\": \"잘못된 단어 개수\",\n      \"error_invalid_seed\": \"잘못된 씨드 단어입니다.\"\n    },\n    \"servers\": {\n      \"title\": \"서버\",\n      \"caption\": \"우리를 믿지 마세요! Mutiny를 백업하기 위해 자체 서버를 사용하세요.\",\n      \"link\": \"자체 호스팅에 대해 자세히 알아보기\",\n      \"proxy_label\": \"웹소켓 프록시\",\n      \"proxy_caption\": \"라이트닝 노드가 네트워크와 통신하는 방법입니다.\",\n      \"error_proxy\": \"wss://로 시작하는 URL이어야 합니다.\",\n      \"esplora_label\": \"Esplora\",\n      \"esplora_caption\": \"온체인 정보를 위한 블록 데이터입니다.\",\n      \"error_esplora\": \"URL처럼 보이지 않습니다.\",\n      \"rgs_label\": \"RGS\",\n      \"rgs_caption\": \"Rapid Gossip Sync. 라우팅을 위해 사용되는 라이트닝 네트워크에 대한 네트워크 데이터입니다.\",\n      \"error_rgs\": \"URL처럼 보이지 않습니다.\",\n      \"lsp_label\": \"LSP\",\n      \"lsp_caption\": \"라이트닝 서비스 공급자. 인바운드 유동성을 위해 자동으로 채널을 열고, 개인 정보 보호를 위해 인보이스를 래핑합니다.\",\n      \"error_lsp\": \"URL처럼 보이지 않습니다.\",\n      \"save\": \"저장\"\n    }\n  },\n  \"swap\": {\n    \"peer_not_found\": \"피어를 찾을 수 없음\",\n    \"channel_too_small\": \"{{amount}} sats보다 작은 채널을 만드는 것은 그저 어리석은 짓입니다.\",\n    \"insufficient_funds\": \"이 채널을 만들기에 충분한 자금이 없습니다.\",\n    \"header\": \"라이트닝으로 스왑\",\n    \"initiated\": \"스왑 시작됨\",\n    \"sats_added\": \"sats가 라이트닝 잔액에 추가됩니다.\",\n    \"use_existing\": \"기존 피어 사용\",\n    \"choose_peer\": \"피어 선택\",\n    \"peer_connect_label\": \"새 피어 연결\",\n    \"peer_connect_placeholder\": \"피어 연결 문자열\",\n    \"connect\": \"연결\",\n    \"connecting\": \"연결 중...\",\n    \"confirm_swap\": \"스왑 확인\"\n  },\n  \"error\": {\n    \"title\": \"오류\",\n    \"emergency_link\": \"긴급 킷.\",\n    \"restart\": \"문제가 *더* 발생했나요? 노드를 중지하세요!\",\n    \"general\": {\n      \"oh_no\": \"앗!\",\n      \"never_should_happen\": \"이런 일은 일어나면 안 됩니다.\",\n      \"try_reloading\": \"이 페이지를 새로 고치거나 &rdquo;얘들아&rdquo; 버튼을 눌러보세요. 계속해서 문제가 발생하면\",\n      \"support_link\": \"지원을 요청하세요.\",\n      \"getting_desperate\": \"좀 답답하신가요? 다음을 시도해보세요.\"\n    },\n    \"load_time\": {\n      \"stuck\": \"이 화면에 멈춰있나요? 다시 로드해보세요. 그래도 동작하지 않으면 다음을 확인하세요.\"\n    },\n    \"not_found\": {\n      \"title\": \"찾을 수 없음\",\n      \"wtf_paul\": \"이건 아마 폴의 잘못입니다.\"\n    },\n    \"reset_router\": {\n      \"payments_failing\": \"결제 실패하고 있나요? 라이트닝 라우터를 초기화해보세요.\",\n      \"reset_router\": \"라우터 초기화\"\n    },\n    \"resync\": {\n      \"incorrect_balance\": \"온체인 잔액이 잘못된 것 같나요? 온체인 월렛을 다시 동기화해보세요.\",\n      \"resync_wallet\": \"월렛 다시 동기화\"\n    },\n    \"on_boot\": {\n      \"existing_tab\": {\n        \"title\": \"여러 탭 감지됨\",\n        \"description\": \"현재 Mutiny Wallet을 한 번에 한 탭에서만 사용할 수 있습니다. Mutiny가 실행 중인 다른 탭이 열려 있습니다. 해당 탭을 닫고 이 페이지를 새로 고치거나, 이 탭을 닫고 다른 탭을 새로 고치세요.\"\n      },\n      \"incompatible_browser\": {\n        \"title\": \"호환되지 않는 브라우저\",\n        \"header\": \"호환되지 않는 브라우저가 감지되었습니다.\",\n        \"description\": \"Mutiny Wallet은 WebAssembly, LocalStorage 및 IndexedDB를 지원하는 현대적인 브라우저를 필요로 합니다. 일부 브라우저는 이러한 기능을 비활성화하는 경우도 있습니다.\",\n        \"try_different_browser\": \"이러한 모든 기능을 지원하는 브라우저를 사용하는지 확인하거나 다른 브라우저를 시도하세요. 또는 이러한 기능을 차단하는 특정 확장 기능이나 &rdquo;보호 기능&rdquo;을 비활성화해보세요.\",\n        \"browser_storage\": \"(더 많은 프라이버시 브라우저를 지원하고 싶지만, 월렛 데이터를 브라우저 저장소에 저장해야 하므로 그렇게 할 수 없습니다. )\",\n        \"browsers_link\": \"지원되는 브라우저\"\n      },\n      \"loading_failed\": {\n        \"title\": \"로드 실패\",\n        \"header\": \"Mutiny 로드 실패\",\n        \"description\": \"Mutiny Wallet을 부팅하는 동안 문제가 발생했습니다.\",\n        \"repair_options\": \"월렛이 손상된 것 같다면, 디버그 및 복구를 시도하기 위한 몇 가지 도구입니다.\",\n        \"questions\": \"이러한 버튼이 무엇을 하는지 궁금하다면,\",\n        \"support_link\": \"지원을 요청하세요.\"\n      }\n    }\n  },\n  \"modals\": {\n    \"share\": \"공유\",\n    \"details\": \"상세정보\",\n    \"loading\": {\n      \"loading\": \"로딩 중:\",\n      \"default\": \"시작 중\",\n      \"double_checking\": \"검증 중\",\n      \"downloading\": \"다운로드 중\",\n      \"setup\": \"설정 중\",\n      \"done\": \"완료\"\n    },\n    \"onboarding\": {\n      \"welcome\": \"환영합니다!\",\n      \"restore_from_backup\": \"이미 Mutiny를 사용한 적이 있으시다면 백업에서 복원할 수 있습니다. 그렇지 않다면 이 단계를 건너뛰고 새로운 지갑을 즐기실 수 있습니다!\",\n      \"not_available\": \"아직 이 기능은 지원하지 않습니다\",\n      \"secure_your_funds\": \"자금을 안전하게 보호하세요\"\n    },\n    \"more_info\": {\n      \"whats_with_the_fees\": \"수수료는 어떻게 되나요?\",\n      \"self_custodial\": \"Mutiny는 자체 보관 월렛입니다. 라이트닝 지불을 시작하려면 라이트닝 채널을 개설해야 하며, 이는 최소 금액과 설정 비용이 필요합니다.\",\n      \"future_payments\": \"앞으로의 송금 및 입금은 일반 네트워크 수수료와 노말 서비스 수수료만 부과되며, 채널에 인바운드 용량이 부족한 경우에만 추가 수수료가 발생합니다.\",\n      \"liquidity\": \"유동성에 대해 자세히 알아보기\"\n    },\n    \"confirm_dialog\": {\n      \"are_you_sure\": \"확실합니까?\",\n      \"cancel\": \"취소\",\n      \"confirm\": \"확인\"\n    }\n  },\n  \"create_an_issue\": \"이슈 생성\",\n  \"send_bitcoin\": \"비트코인 전송\",\n  \"continue\": \"계속하기\",\n  \"keep_mutiny_open\": \"결제를 완료하기 위해 Mutiny를 열어두세요.\"\n}\n"
  },
  {
    "path": "public/i18n/pt.json",
    "content": "{\n  \"create_an_issue\": \"Crie uma issue\",\n  \"view_all\": \"Ver todas\",\n  \"receive_some_sats_to_get_started\": \"Receba alguns satoshis para começar\",\n  \"send_bitcoin\": \"Enviar Bitcoin\",\n  \"view_transaction\": \"Ver transação\",\n  \"amount_editable_first_payment_10k_or_greater\": \"Seu primeiro recebimento na lightning precisa ser de pelo menos 10.000 sats. Uma taxa de configuração será deduzida da quantidade requisitada.\",\n  \"why?\": \"Por que?\",\n  \"more_info_modal_p1\": \"Mutiny é uma carteira de auto custódia. Para iniciar um pagamento na lightning, nós precisamos abrir um canal que requer uma quantidade mínima e uma taxa de configuração.\",\n  \"more_info_modal_p2\": \"Transaçoēs futuras, como envios e recebimentos, terão somente taxas normais da rede e uma taxa de serviço nominal a não ser que seu canal fique sem capacidade de entrada.\",\n  \"learn_more_about_liquidity\": \"Aprenda mais sobre liquidez\",\n  \"set_amount\": \"Definir quantidade\",\n  \"whats_with_the_fees\": \"O que há com as taxas?\",\n  \"private_tags\": \"Tags privadas\",\n  \"receive_add_the_sender\": \"Marque quem o enviou para registro próprio\",\n  \"continue\": \"Continuar\",\n  \"receive_bitcoin\": \"Receber Bitcoin\",\n  \"settings\": {\n    \"header\": \"\"\n  }\n}\n"
  },
  {
    "path": "public/robots.txt",
    "content": "User-agent: *\nAllow: /\n"
  },
  {
    "path": "scripts/errorsToTs.cjs",
    "content": "const fs = require(\"fs\").promises;\n\n(async () => {\n    const filePath = process.argv[2]; // grab the file path from the command-line arguments\n\n    if (!filePath) {\n        console.error(\"Please provide a file path.\");\n        process.exit(1);\n    }\n\n    let file;\n\n    try {\n        file = await fs.readFile(filePath, \"utf-8\");\n    } catch (error) {\n        console.error(`Failed to read file at path: ${filePath}`);\n        console.error(error);\n        process.exit(1);\n    }\n\n    const regex = /#\\s*\\[error\\(\\s*\"([^\"]*)\"\\s*\\)\\]/g;\n\n    let matches = file.match(regex);\n\n    if (matches) {\n        let errors = matches.map((match) => match.match(/\"(.*?)\"/)[1]); // capture text within \"\"\n        let typeScriptTypeString = `type MutinyError = \"${errors.join(\n            '\"\\n\\t| \"'\n        )}\";`;\n\n        console.log(typeScriptTypeString);\n    } else {\n        console.error(\"No matches found in the provided file.\");\n    }\n})();\n"
  },
  {
    "path": "src/components/Activity.tsx",
    "content": "import { TagItem } from \"@mutinywallet/mutiny-wasm\";\nimport { cache, createAsync, useNavigate } from \"@solidjs/router\";\nimport { Plus, Save, Search, Shuffle } from \"lucide-solid\";\nimport {\n    createEffect,\n    createMemo,\n    createResource,\n    createSignal,\n    For,\n    Match,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    ActivityDetailsModal,\n    Button,\n    ButtonCard,\n    ContactButton,\n    FederationPopup,\n    LoadingShimmer,\n    NiceP,\n    ShutdownPopup,\n    SimpleDialog\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { PrivacyLevel } from \"~/routes\";\nimport { useMegaStore, WalletWorker } from \"~/state/megaStore\";\nimport {\n    actuallyFetchNostrProfile,\n    createDeepSignal,\n    hexpubFromNpub,\n    profileToPseudoContact,\n    PseudoContact,\n    timeAgo,\n    ZAPPLE_PAY_NPUB\n} from \"~/utils\";\n\nimport { GenericItem } from \"./GenericItem\";\n\nexport type HackActivityType =\n    | \"Lightning\"\n    | \"OnChain\"\n    | \"ChannelOpen\"\n    | \"ChannelClose\";\n\nexport interface IActivityItem {\n    kind: HackActivityType;\n    id: string;\n    amount_sats: number;\n    inbound: boolean;\n    labels: string[];\n    contacts: TagItem[];\n    last_updated: number;\n    privacy_level: PrivacyLevel;\n}\n\nasync function fetchContactForNpub(\n    sw: WalletWorker,\n    npub: string\n): Promise<PseudoContact | undefined> {\n    const hexpub = await hexpubFromNpub(sw, npub);\n    console.log(\"fetchContactForNpub\", hexpub);\n    if (!hexpub) {\n        return undefined;\n    }\n    const profile = await actuallyFetchNostrProfile(hexpub);\n    if (!profile) {\n        return undefined;\n    }\n    const pseudoContact = profileToPseudoContact(profile);\n    return pseudoContact;\n}\n\nexport function UnifiedActivityItem(props: {\n    item: IActivityItem;\n    onClick: (id: string, kind: HackActivityType) => void;\n    onNewContactClick: (profile: PseudoContact) => void;\n}) {\n    const [_state, _actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n\n    const click = () => {\n        props.onClick(\n            props.item.id,\n            props.item.kind as unknown as HackActivityType\n        );\n    };\n\n    const primaryContact = createMemo(() => {\n        if (props.item.contacts.length === 0) {\n            return undefined;\n        }\n        // if it's a zapple pay, don't show the contact, parse the label\n        const contact = props.item.contacts[0];\n        if (contact.npub && contact.npub === ZAPPLE_PAY_NPUB) {\n            return undefined;\n        }\n        return contact;\n    });\n\n    const getContact = cache(async (npub) => {\n        return await fetchContactForNpub(sw, npub);\n    }, \"profile\");\n\n    const profileFromNostr = createAsync(async () => {\n        if (props.item.contacts.length === 0) {\n            if (props.item.labels) {\n                const npub = props.item.labels.find((l) =>\n                    l.startsWith(\"npub\")\n                );\n                if (npub) {\n                    try {\n                        const newContact = await getContact(npub);\n                        return newContact;\n                    } catch (e) {\n                        console.error(e);\n                    }\n                }\n            }\n        } else if (\n            // if zapple pay, handle it specially\n            props.item.contacts[0].npub === ZAPPLE_PAY_NPUB\n        ) {\n            if (props.item.labels) {\n                const label = props.item.labels.find((l) =>\n                    l.startsWith(\"From: nostr:npub\")\n                );\n                if (label) {\n                    // get the npub from the label\n                    const npub = label.split(\"From: nostr:\")[1];\n                    try {\n                        return await getContact(npub.trim());\n                    } catch (e) {\n                        console.error(e);\n                    }\n                }\n            }\n        }\n        return undefined;\n    });\n\n    // TODO: figure out what other shit we should filter out\n    const message = () => {\n        const filtered = props.item.labels.filter(\n            (l) =>\n                l !== \"SWAP\" &&\n                !l.startsWith(\"LN Channel:\") &&\n                !l.startsWith(\"npub\") &&\n                !l.startsWith(\"From: nostr:npub\")\n        );\n        if (filtered.length === 0) {\n            return undefined;\n        }\n\n        return filtered[0];\n    };\n\n    const shouldShowShuffle = () => {\n        return (\n            props.item.kind === \"ChannelOpen\" ||\n            props.item.kind === \"ChannelClose\" ||\n            (props.item.labels.length > 0 && props.item.labels[0] === \"SWAP\")\n        );\n    };\n\n    const verb = () => {\n        if (props.item.kind === \"ChannelOpen\") {\n            return \"opened a\";\n        }\n        if (props.item.kind === \"ChannelClose\") {\n            return \"closed a\";\n        }\n        if (props.item.labels.length > 0 && props.item.labels[0] === \"SWAP\") {\n            return \"swapped to\";\n        }\n        if (\n            props.item.labels.length > 0 &&\n            props.item.labels[0] === \"Swept Force Close\"\n        ) {\n            return undefined;\n        }\n\n        return \"sent\";\n    };\n\n    const primaryName = () => {\n        return props.item.inbound ? primaryContact()?.name || \"Unknown\" : \"You\";\n    };\n\n    const secondaryName = () => {\n        if (props.item.labels.length > 0 && props.item.labels[0] === \"SWAP\") {\n            return \"Lightning\";\n        }\n        if (\n            props.item.kind === \"ChannelOpen\" ||\n            props.item.kind === \"ChannelClose\"\n        ) {\n            return \"Lightning channel\";\n        }\n        if (!props.item.inbound) {\n            return primaryContact()?.name || \"Unknown\";\n        }\n        return \"you\";\n    };\n\n    const shouldShowGeneric = () => {\n        if (props.item.inbound && primaryName() === \"Unknown\") {\n            return true;\n        }\n\n        if (!props.item.inbound && secondaryName() === \"Unknown\") {\n            return true;\n        }\n    };\n\n    function handlePrimaryOnClick() {\n        if (primaryContact()?.id) {\n            navigate(`/chat/${primaryContact()?.id}`);\n            return;\n        }\n\n        if (profileFromNostr()) {\n            props.onNewContactClick(profileFromNostr()!);\n            return;\n        }\n\n        if (\n            props.item.kind === \"ChannelOpen\" ||\n            props.item.kind === \"ChannelClose\"\n        ) {\n            click();\n        }\n    }\n\n    return (\n        <div class=\"pt-3 first-of-type:pt-0\">\n            <Suspense\n                fallback={\n                    <GenericItem\n                        amountOnClick={click}\n                        primaryName=\"Unknown\"\n                        genericAvatar={true}\n                        verb={verb()}\n                        message={message()}\n                        secondaryName={secondaryName()}\n                        amount={\n                            props.item.amount_sats\n                                ? BigInt(props.item.amount_sats || 0)\n                                : undefined\n                        }\n                        date={timeAgo(props.item.last_updated)}\n                        accent={props.item.inbound ? \"green\" : undefined}\n                        visibility={\n                            props.item.privacy_level === \"Public\"\n                                ? \"public\"\n                                : \"private\"\n                        }\n                    />\n                }\n            >\n                <GenericItem\n                    primaryAvatarUrl={\n                        primaryContact()?.image_url\n                            ? primaryContact()?.image_url\n                            : profileFromNostr()?.primal_image_url || \"\"\n                    }\n                    icon={shouldShowShuffle() ? <Shuffle /> : undefined}\n                    primaryOnClick={handlePrimaryOnClick}\n                    amountOnClick={click}\n                    primaryName={\n                        props.item.inbound\n                            ? primaryContact()?.name\n                                ? primaryContact()!.name\n                                : profileFromNostr()?.name || \"Unknown\"\n                            : \"You\"\n                    }\n                    genericAvatar={shouldShowGeneric()}\n                    verb={verb()}\n                    message={message()}\n                    secondaryName={secondaryName()}\n                    amount={\n                        props.item.amount_sats\n                            ? BigInt(props.item.amount_sats || 0)\n                            : undefined\n                    }\n                    date={timeAgo(props.item.last_updated)}\n                    accent={props.item.inbound ? \"green\" : undefined}\n                    visibility={\n                        props.item.privacy_level === \"Public\"\n                            ? \"public\"\n                            : \"private\"\n                    }\n                />\n            </Suspense>\n        </div>\n    );\n}\n\nfunction NewContactModal(props: { profile: PseudoContact; close: () => void }) {\n    const i18n = useI18n();\n    const navigate = useNavigate();\n\n    const [_state, _actions, sw] = useMegaStore();\n\n    async function createContact() {\n        try {\n            const existingContact = await sw.get_contact_for_npub(\n                props.profile.hexpub\n            );\n\n            if (existingContact) {\n                navigate(`/chat/${existingContact.id}`);\n                return;\n            }\n\n            const contactId = await sw.create_new_contact(\n                props.profile.name,\n                props.profile.hexpub,\n                props.profile.ln_address,\n                props.profile.lnurl,\n                props.profile.image_url\n            );\n\n            if (!contactId) {\n                throw new Error(\"no contact id returned\");\n            }\n\n            const tagItem = await sw.get_tag_item(contactId);\n\n            if (!tagItem) {\n                throw new Error(\"no contact returned\");\n            }\n\n            navigate(`/chat/${contactId}`);\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    return (\n        <SimpleDialog\n            title={i18n.t(\"activity.start_a_chat\")}\n            open\n            setOpen={() => {\n                props.close();\n            }}\n        >\n            <NiceP>{i18n.t(\"activity.start_a_chat_are_you_sure\")}</NiceP>\n            <ContactButton contact={props.profile} onClick={() => {}} />\n            <div class=\"flex-end flex w-full justify-end gap-2\">\n                <Button\n                    layout=\"small\"\n                    intent=\"red\"\n                    onClick={() => props.close()}\n                >\n                    {i18n.t(\"modals.confirm_dialog.cancel\")}\n                </Button>\n                <Button layout=\"small\" intent=\"blue\" onClick={createContact}>\n                    {i18n.t(\"common.continue\")}\n                </Button>\n            </div>\n        </SimpleDialog>\n    );\n}\n\nexport function CombinedActivity() {\n    const [state, _actions, sw] = useMegaStore();\n    const i18n = useI18n();\n\n    const [detailsOpen, setDetailsOpen] = createSignal(false);\n    const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();\n    const [detailsId, setDetailsId] = createSignal(\"\");\n    const navigate = useNavigate();\n\n    function openDetailsModal(id: string, kind: HackActivityType) {\n        console.log(\"Opening details modal: \", id, kind);\n\n        if (!id) {\n            console.warn(\"No id provided to openDetailsModal\");\n            return;\n        }\n\n        setDetailsId(id);\n        setDetailsKind(kind);\n        setDetailsOpen(true);\n    }\n\n    async function fetchActivity() {\n        try {\n            return await sw.get_activity(50, undefined);\n        } catch (e) {\n            console.error(e);\n            return [] as IActivityItem[];\n        }\n    }\n\n    const [activity, { refetch }] = createResource(fetchActivity, {\n        storage: createDeepSignal\n    });\n\n    createEffect(() => {\n        // Should re-run after every sync\n        if (!state.is_syncing) {\n            refetch();\n        }\n    });\n\n    const [newContact, setNewContact] = createSignal<PseudoContact>();\n\n    return (\n        <>\n            <Show when={detailsId() && detailsKind()}>\n                <ActivityDetailsModal\n                    open={detailsOpen()}\n                    kind={detailsKind()}\n                    id={detailsId()}\n                    setOpen={setDetailsOpen}\n                />\n            </Show>\n            <Show when={newContact()}>\n                <NewContactModal\n                    profile={newContact()!}\n                    close={() => setNewContact(undefined)}\n                />\n            </Show>\n            <Suspense fallback={<LoadingShimmer />}>\n                <Show when={state.expiration_warning}>\n                    <FederationPopup />\n                </Show>\n                <ShutdownPopup />\n                <Show when={!state.has_backed_up}>\n                    <ButtonCard\n                        red\n                        onClick={() => navigate(\"/settings/backup\")}\n                    >\n                        <div class=\"flex items-center gap-2\">\n                            <Save class=\"inline-block text-neutral-200\" />\n                            <NiceP>{i18n.t(\"home.backup\")}</NiceP>\n                        </div>\n                    </ButtonCard>\n                </Show>\n                <Switch>\n                    <Match when={activity.latest?.length === 0}>\n                        {/*<Show when={state.federations?.length === 0}>*/}\n                        {/*    <ButtonCard*/}\n                        {/*        onClick={() =>*/}\n                        {/*            navigate(\"/settings/federations\")*/}\n                        {/*        }*/}\n                        {/*    >*/}\n                        {/*        <div class=\"flex items-center gap-2\">*/}\n                        {/*            <Users class=\"inline-block text-m-red\" />*/}\n                        {/*            <NiceP>{i18n.t(\"home.federation\")}</NiceP>*/}\n                        {/*        </div>*/}\n                        {/*    </ButtonCard>*/}\n                        {/*</Show>*/}\n                        <ButtonCard onClick={() => navigate(\"/receive\")}>\n                            <div class=\"flex items-center gap-2\">\n                                <Plus class=\"inline-block text-m-red\" />\n                                <NiceP>{i18n.t(\"home.receive\")}</NiceP>\n                            </div>\n                        </ButtonCard>\n                        <ButtonCard onClick={() => navigate(\"/search\")}>\n                            <div class=\"flex items-center gap-2\">\n                                <Search class=\"inline-block text-m-red\" />\n                                <NiceP>{i18n.t(\"home.find\")}</NiceP>\n                            </div>\n                        </ButtonCard>\n                    </Match>\n                    <Match\n                        when={activity.latest && activity.latest!.length >= 0}\n                    >\n                        <div class=\"flex w-full flex-col divide-y divide-m-grey-800 overflow-x-clip\">\n                            <For each={activity.latest}>\n                                {(activityItem) => (\n                                    <UnifiedActivityItem\n                                        item={activityItem}\n                                        onClick={openDetailsModal}\n                                        onNewContactClick={setNewContact}\n                                    />\n                                )}\n                            </For>\n                        </div>\n                    </Match>\n                </Switch>\n            </Suspense>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/ActivityDetailsModal.tsx",
    "content": "import { Dialog } from \"@kobalte/core\";\nimport {\n    ActivityItem,\n    MutinyInvoice,\n    TagItem\n} from \"@mutinywallet/mutiny-wasm\";\nimport { createAsync } from \"@solidjs/router\";\nimport { Copy, Link, Shuffle, Zap } from \"lucide-solid\";\nimport {\n    createEffect,\n    createResource,\n    Match,\n    ParentComponent,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    AmountFiat,\n    AmountSats,\n    FancyCard,\n    HackActivityType,\n    Hr,\n    InfoBox,\n    KeyValue,\n    ModalCloseButton,\n    TinyButton,\n    TruncateMiddle,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { BalanceBar } from \"~/routes/settings/Channels\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { mempoolTxUrl, prettyPrintTime, useCopy } from \"~/utils\";\n\ninterface ChannelClosure {\n    channel_id: string;\n    node_id: string;\n    reason: string;\n    timestamp: number;\n}\n\ninterface OnChainTx {\n    txid: string;\n    received: number;\n    sent: number;\n    fee?: number;\n    confirmation_time?: {\n        Confirmed?: {\n            height: number;\n            time: number;\n        };\n    };\n    labels: string[];\n}\n\nconst ActivityAmount: ParentComponent<{\n    amount: string;\n    price: number;\n    positive?: boolean;\n    center?: boolean;\n}> = (props) => {\n    return (\n        <div\n            class=\"flex flex-col gap-1\"\n            classList={{\n                \"items-end\": !props.center,\n                \"items-center\": props.center\n            }}\n        >\n            <div\n                class=\"justify-end\"\n                classList={{ \"text-m-green\": props.positive }}\n            >\n                <AmountSats\n                    amountSats={Number(props.amount)}\n                    icon={props.positive ? \"plus\" : undefined}\n                />\n            </div>\n            <div class=\"text-sm text-white/70\">\n                <AmountFiat\n                    amountSats={Number(props.amount)}\n                    denominationSize=\"sm\"\n                />\n            </div>\n        </div>\n    );\n};\n\nexport const OVERLAY = \"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm\";\nexport const DIALOG_POSITIONER =\n    \"fixed inset-0 z-50 flex items-center justify-center\";\nexport const DIALOG_CONTENT =\n    \"max-w-[500px] w-[90vw] max-h-device overflow-y-scroll disable-scrollbars bg-neutral-900/80 backdrop-blur-md shadow-xl rounded-xl border border-white/10\";\n\nfunction LightningHeader(props: { info: MutinyInvoice }) {\n    const i18n = useI18n();\n\n    return (\n        <div class=\"flex flex-col items-center gap-4\">\n            <div class=\"flex flex-row items-center justify-center gap-[4px] font-normal\">\n                {props.info.inbound\n                    ? i18n.t(\"activity.transaction_details.lightning_receive\")\n                    : i18n.t(\"activity.transaction_details.lightning_send\")}\n                <Zap class=\"h-4 w-4\" />\n            </div>\n            <div class=\"flex flex-col items-center\">\n                <div\n                    class=\"text-2xl\"\n                    classList={{ \"text-m-green\": props.info.inbound }}\n                >\n                    <AmountSats\n                        amountSats={props.info.amount_sats}\n                        icon={props.info.inbound ? \"plus\" : undefined}\n                        denominationSize=\"lg\"\n                    />\n                </div>\n                <div class=\"text-lg text-white/70\">\n                    <AmountFiat\n                        amountSats={props.info.amount_sats}\n                        denominationSize=\"sm\"\n                    />\n                </div>\n            </div>\n        </div>\n    );\n}\n\nfunction OnchainHeader(props: { info: OnChainTx; kind?: HackActivityType }) {\n    const i18n = useI18n();\n\n    const isSend = () => {\n        return props.info.sent > props.info.received;\n    };\n\n    const amount = () => {\n        if (isSend()) {\n            return (props.info.sent - props.info.received).toString();\n        } else {\n            return (props.info.received - props.info.sent).toString();\n        }\n    };\n\n    return (\n        <div class=\"flex flex-col items-center gap-4\">\n            <div class=\"flex flex-row items-center justify-center gap-[4px] font-normal\">\n                {props.kind === \"ChannelOpen\"\n                    ? i18n.t(\"activity.transaction_details.channel_open\")\n                    : props.kind === \"ChannelClose\"\n                      ? i18n.t(\"activity.transaction_details.channel_close\")\n                      : isSend()\n                        ? i18n.t(\"activity.transaction_details.onchain_send\")\n                        : i18n.t(\n                              \"activity.transaction_details.onchain_receive\"\n                          )}\n                <Switch>\n                    <Match\n                        when={\n                            props.kind === \"ChannelOpen\" ||\n                            props.kind === \"ChannelClose\"\n                        }\n                    >\n                        <Shuffle class=\"h-4 w-4\" />\n                    </Match>\n                    <Match when={true}>\n                        <Link class=\"h-4 w-4\" />\n                    </Match>\n                </Switch>\n            </div>\n            <Show when={props.kind !== \"ChannelClose\" && Number(amount()) > 0}>\n                <div class=\"flex flex-col items-center\">\n                    <div\n                        class=\"text-2xl\"\n                        classList={{ \"text-m-green\": !isSend() }}\n                    >\n                        <AmountSats\n                            amountSats={Number(amount())}\n                            icon={!isSend() ? \"plus\" : undefined}\n                            denominationSize=\"lg\"\n                        />\n                    </div>\n                    <div class=\"text-lg text-white/70\">\n                        <AmountFiat\n                            amountSats={Number(amount())}\n                            denominationSize=\"sm\"\n                        />\n                    </div>\n                </div>\n            </Show>\n        </div>\n    );\n}\n\nexport function MiniStringShower(props: { text: string; hide?: boolean }) {\n    const [copy, copied] = useCopy({ copiedTimeout: 1000 });\n\n    return (\n        <div class=\"grid w-full grid-cols-[minmax(0,_1fr)_auto] gap-1\">\n            <Switch>\n                <Match when={props.hide}>\n                    <input\n                        type=\"password\"\n                        value={props.text}\n                        class=\"flex bg-transparent font-mono\"\n                        readonly\n                        disabled\n                    />\n                </Match>\n                <Match when={true}>\n                    <TruncateMiddle text={props.text} />\n                </Match>\n            </Switch>\n\n            <button\n                class=\"w-[1.5rem] p-1\"\n                classList={{ \"bg-m-red rounded\": copied() }}\n                onClick={() => copy(props.text)}\n            >\n                <Copy class=\"h-4 w-4\" />\n            </button>\n        </div>\n    );\n}\n\nfunction FormatPrettyPrint(props: { ts: number }) {\n    return (\n        <div>\n            {prettyPrintTime(props.ts).split(\",\", 2).join(\",\")}\n            <div class=\"text-right text-sm text-white/70\">\n                {prettyPrintTime(props.ts).split(\", \")[2]}\n            </div>\n        </div>\n    );\n}\n\nfunction LightningDetails(props: { info: MutinyInvoice; tags?: TagItem }) {\n    const i18n = useI18n();\n    const [state, _actions] = useMegaStore();\n    return (\n        <VStack>\n            <ul class=\"flex flex-col gap-4\">\n                <KeyValue key={i18n.t(\"activity.transaction_details.fee\")}>\n                    <ActivityAmount\n                        amount={props.info.fees_paid?.toString() || \"0\"}\n                        price={state.price}\n                    />\n                </KeyValue>\n                <Show when={props.tags || props.info.labels[0]}>\n                    <KeyValue\n                        key={i18n.t(\"activity.transaction_details.tagged_to\")}\n                    >\n                        <TinyButton\n                            tag={props.tags?.value ?? undefined}\n                            onClick={() => {\n                                // noop\n                            }}\n                        >\n                            {props.tags?.name || props.info.labels[0]}\n                        </TinyButton>\n                    </KeyValue>\n                </Show>\n                <Show when={!props.info.paid}>\n                    <KeyValue\n                        key={i18n.t(\"activity.transaction_details.status\")}\n                    >\n                        {i18n.t(\"activity.transaction_details.unpaid\")}\n                    </KeyValue>\n                </Show>\n                <KeyValue key={i18n.t(\"activity.transaction_details.date\")}>\n                    <FormatPrettyPrint ts={Number(props.info.last_updated)} />\n                </KeyValue>\n                <Show when={props.info.description}>\n                    <KeyValue\n                        key={i18n.t(\"activity.transaction_details.description\")}\n                    >\n                        <span class=\"pl-6\">{props.info.description}</span>\n                    </KeyValue>\n                </Show>\n                <KeyValue key={i18n.t(\"activity.transaction_details.invoice\")}>\n                    <MiniStringShower text={props.info.bolt11 ?? \"\"} />\n                </KeyValue>\n                <Show when={props.info.paid && !props.info.inbound}>\n                    <KeyValue\n                        key={i18n.t(\n                            \"activity.transaction_details.payment_preimage\"\n                        )}\n                    >\n                        <MiniStringShower text={props.info.preimage ?? \"\"} />\n                    </KeyValue>\n                </Show>\n            </ul>\n        </VStack>\n    );\n}\n\nfunction OnchainDetails(props: {\n    info: OnChainTx;\n    kind?: HackActivityType;\n    tags?: TagItem;\n}) {\n    const i18n = useI18n();\n    const [state, _actions, sw] = useMegaStore();\n    const [copy, copied] = useCopy({ copiedTimeout: 1000 });\n\n    const confirmationTime = () => {\n        return props.info.confirmation_time?.Confirmed?.time;\n    };\n\n    const network = state.network || \"signet\";\n\n    // Can return nothing if the channel is already closed\n    const [channelInfo] = createResource(async () => {\n        if (props.kind === \"ChannelOpen\") {\n            try {\n                const channels = await sw.list_channels();\n                const channel = channels?.find((channel) =>\n                    channel.outpoint?.startsWith(props.info.txid)\n                );\n                return channel;\n            } catch (e) {\n                console.error(e);\n            }\n        } else {\n            return undefined;\n        }\n    });\n\n    return (\n        <VStack>\n            {/* <pre>{JSON.stringify(channelInfo() || \"\", null, 2)}</pre> */}\n            <ul class=\"flex flex-col gap-4\">\n                <Switch>\n                    <Match when={props.kind === \"ChannelOpen\" && channelInfo()}>\n                        <BalanceBar\n                            inbound={\n                                Number(channelInfo()?.size) -\n                                    (Number(channelInfo()?.balance) +\n                                        Number(channelInfo()?.reserve)) || 0\n                            }\n                            reserve={Number(channelInfo()?.reserve) || 0}\n                            outbound={Number(channelInfo()?.balance) || 0}\n                        />\n                        <KeyValue\n                            key={i18n.t(\"activity.transaction_details.total\")}\n                        >\n                            <ActivityAmount\n                                amount={channelInfo()!.size.toString()}\n                                price={state.price}\n                            />\n                        </KeyValue>\n                        <KeyValue\n                            key={i18n.t(\n                                \"activity.transaction_details.onchain_fee\"\n                            )}\n                        >\n                            <ActivityAmount\n                                amount={props.info.fee!.toString()}\n                                price={state.price}\n                            />\n                        </KeyValue>\n                    </Match>\n                    <Match when={props.kind === \"ChannelOpen\"}>\n                        <InfoBox accent=\"blue\">\n                            {i18n.t(\"activity.transaction_details.no_details\")}\n                        </InfoBox>\n                    </Match>\n                </Switch>\n                <Show\n                    when={\n                        props.kind !== \"ChannelOpen\" &&\n                        props.info.fee &&\n                        props.info.fee > 0\n                    }\n                >\n                    <KeyValue\n                        key={i18n.t(\"activity.transaction_details.onchain_fee\")}\n                    >\n                        <ActivityAmount\n                            amount={props.info.fee!.toString()}\n                            price={state.price}\n                        />\n                    </KeyValue>\n                </Show>\n                <Show when={props.tags && props.kind === \"OnChain\"}>\n                    <KeyValue\n                        key={i18n.t(\"activity.transaction_details.tagged_to\")}\n                    >\n                        <TinyButton\n                            tag={props.tags?.value ?? undefined}\n                            onClick={() => {\n                                // noop\n                            }}\n                        >\n                            {props.tags?.name || props.info.labels[0]}\n                        </TinyButton>\n                    </KeyValue>\n                </Show>\n                <KeyValue key={i18n.t(\"activity.transaction_details.status\")}>\n                    {confirmationTime()\n                        ? i18n.t(\"activity.transaction_details.confirmed\")\n                        : i18n.t(\"activity.transaction_details.unconfirmed\")}\n                </KeyValue>\n                <KeyValue key={i18n.t(\"activity.transaction_details.date\")}>\n                    {confirmationTime() ? (\n                        <FormatPrettyPrint ts={Number(confirmationTime())} />\n                    ) : (\n                        \"Pending\"\n                    )}\n                </KeyValue>\n                <Show when={props.info.txid}>\n                    <KeyValue key={i18n.t(\"activity.transaction_details.txid\")}>\n                        <div class=\"flex gap-1\">\n                            {/* Have to do all these shenanigans because css / html is hard */}\n                            <div class=\"grid w-full grid-cols-[minmax(0,_1fr)_auto] gap-1\">\n                                <a\n                                    target=\"_blank\"\n                                    rel=\"noopener noreferrer\"\n                                    href={mempoolTxUrl(\n                                        props.info.txid,\n                                        network\n                                    )}\n                                >\n                                    <div class=\"flex flex-nowrap items-center font-mono text-white\">\n                                        <span class=\"truncate\">\n                                            {props.info.txid}\n                                        </span>\n                                        <span>\n                                            {props.info.txid.length > 32\n                                                ? props.info.txid.slice(-8)\n                                                : \"\"}\n                                        </span>\n                                        <svg\n                                            class=\"inline-block w-[16px] overflow-visible pl-0.5 text-white\"\n                                            width=\"16\"\n                                            height=\"16\"\n                                            fill=\"none\"\n                                            xmlns=\"http://www.w3.org/2000/svg\"\n                                        >\n                                            <path\n                                                d=\"M6.00002 3.33337v1.33334H10.39L2.66669 12.39l.94333.9434 7.72338-7.72336V10h1.3333V3.33337H6.00002Z\"\n                                                fill=\"currentColor\"\n                                            />\n                                        </svg>\n                                    </div>\n                                </a>\n                            </div>\n                            <button\n                                class=\"min-w-[1.5rem] p-1\"\n                                classList={{ \"bg-m-green rounded\": copied() }}\n                                onClick={() => copy(props.info.txid)}\n                            >\n                                <Copy class=\"h-4 w-4\" />\n                            </button>\n                        </div>\n                    </KeyValue>\n                </Show>\n            </ul>\n        </VStack>\n    );\n}\n\nfunction ChannelCloseDetails(props: { info: ChannelClosure }) {\n    const i18n = useI18n();\n    return (\n        <VStack>\n            {/* <pre>{JSON.stringify(props.info.value, null, 2)}</pre> */}\n            <ul class=\"flex flex-col gap-4\">\n                <InfoBox accent=\"blue\">\n                    <p>{i18n.t(\"activity.transaction_details.sweep_delay\")}</p>\n                </InfoBox>\n                <KeyValue\n                    key={i18n.t(\"activity.transaction_details.channel_id\")}\n                >\n                    <MiniStringShower text={props.info.channel_id ?? \"\"} />\n                </KeyValue>\n                <Show when={props.info.timestamp}>\n                    <KeyValue key={i18n.t(\"activity.transaction_details.date\")}>\n                        {props.info.timestamp ? (\n                            <FormatPrettyPrint\n                                ts={Number(props.info.timestamp)}\n                            />\n                        ) : (\n                            i18n.t(\"common.pending\")\n                        )}\n                    </KeyValue>\n                </Show>\n                <KeyValue key={i18n.t(\"activity.transaction_details.reason\")}>\n                    <p class=\"whitespace-normal text-right text-neutral-300\">\n                        {props.info.reason ?? \"\"}\n                    </p>\n                </KeyValue>\n            </ul>\n        </VStack>\n    );\n}\n\nexport function ActivityDetailsModal(props: {\n    open: boolean;\n    kind?: HackActivityType;\n    id: string;\n    setOpen: (open: boolean) => void;\n}) {\n    const [_state, _actions, sw] = useMegaStore();\n    const id = () => props.id;\n    const kind = () => props.kind;\n\n    const [data, { refetch }] = createResource(async () => {\n        try {\n            if (kind() === \"Lightning\") {\n                console.debug(\"reading invoice: \", id());\n                const invoice = await sw.get_invoice_by_hash(id());\n                return invoice;\n            } else if (kind() === \"ChannelClose\") {\n                console.debug(\"reading channel close: \", id());\n                const closeItem = await sw.get_channel_closure(id());\n\n                return closeItem;\n            } else {\n                console.debug(\"reading tx: \", id());\n                const tx = await sw.get_transaction(id());\n\n                return tx;\n            }\n        } catch (e) {\n            console.error(e);\n            return undefined;\n        }\n    });\n    const tags = createAsync(async () => {\n        if (\n            !!data() &&\n            // @ts-expect-error we're narrowing the type here\n            data()?.labels !== undefined &&\n            // @ts-expect-error we're narrowing the type here\n            typeof data()?.labels[0] === \"string\"\n        ) {\n            const typedData = data() as MutinyInvoice | ActivityItem;\n            try {\n                // find if there's just one for now\n                const tags = await sw.get_tag_item(typedData.labels[0]);\n                if (tags) {\n                    return tags;\n                } else {\n                    return;\n                }\n            } catch (e) {\n                console.error(e);\n            }\n        } else {\n            return;\n        }\n    });\n\n    createEffect(() => {\n        if (props.id && props.kind && props.open) {\n            refetch();\n        }\n    });\n\n    return (\n        <Dialog.Root open={props.open} onOpenChange={props.setOpen}>\n            <Dialog.Portal>\n                <Dialog.Overlay class={OVERLAY} />\n                <div class={DIALOG_POSITIONER}>\n                    <Dialog.Content class={DIALOG_CONTENT}>\n                        <Suspense>\n                            <div class=\"p-4\">\n                                <div class=\"flex justify-between\">\n                                    <div />\n                                    <Dialog.CloseButton>\n                                        <ModalCloseButton />\n                                    </Dialog.CloseButton>\n                                </div>\n                                <Dialog.Title>\n                                    <FancyCard>\n                                        <Show when={data.latest}>\n                                            <Switch>\n                                                <Match\n                                                    when={\n                                                        kind() === \"Lightning\"\n                                                    }\n                                                >\n                                                    <LightningHeader\n                                                        info={\n                                                            data() as MutinyInvoice\n                                                        }\n                                                    />\n                                                </Match>\n                                                <Match\n                                                    when={\n                                                        kind() === \"OnChain\" ||\n                                                        kind() ===\n                                                            \"ChannelOpen\" ||\n                                                        kind() ===\n                                                            \"ChannelClose\"\n                                                    }\n                                                >\n                                                    <OnchainHeader\n                                                        info={\n                                                            data() as unknown as OnChainTx\n                                                        }\n                                                        kind={kind()}\n                                                    />\n                                                </Match>\n                                            </Switch>\n                                        </Show>\n                                    </FancyCard>\n                                </Dialog.Title>\n                                <Hr />\n                                <Show when={data.latest}>\n                                    <Switch>\n                                        <Match when={kind() === \"Lightning\"}>\n                                            <LightningDetails\n                                                info={\n                                                    data() as unknown as MutinyInvoice\n                                                }\n                                                tags={tags()}\n                                            />\n                                        </Match>\n                                        <Match\n                                            when={\n                                                kind() === \"OnChain\" ||\n                                                kind() === \"ChannelOpen\"\n                                            }\n                                        >\n                                            <OnchainDetails\n                                                info={\n                                                    data() as unknown as OnChainTx\n                                                }\n                                                kind={kind()}\n                                                tags={tags()}\n                                            />\n                                        </Match>\n                                        <Match when={kind() === \"ChannelClose\"}>\n                                            <ChannelCloseDetails\n                                                info={\n                                                    data() as unknown as ChannelClosure\n                                                }\n                                            />\n                                        </Match>\n                                    </Switch>\n                                </Show>\n                            </div>\n                        </Suspense>\n                    </Dialog.Content>\n                </div>\n            </Dialog.Portal>\n        </Dialog.Root>\n    );\n}\n"
  },
  {
    "path": "src/components/Amount.tsx",
    "content": "import { createAsync } from \"@solidjs/router\";\nimport { Link, Users, Zap } from \"lucide-solid\";\nimport { Show } from \"solid-js\";\n\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { satsToFormattedFiat } from \"~/utils\";\n\nfunction prettyPrintAmount(n?: number | bigint): string {\n    if (!n || n.valueOf() === 0) {\n        return \"0\";\n    }\n    return n.toLocaleString(navigator.languages[0]);\n}\n\nexport function AmountSats(props: {\n    amountSats: bigint | number | undefined;\n    icon?: \"lightning\" | \"community\" | \"chain\" | \"plus\" | \"minus\";\n    denominationSize?: \"sm\" | \"lg\" | \"xl\";\n    isFederation?: boolean;\n}) {\n    const i18n = useI18n();\n    return (\n        <div class=\"flex items-center gap-2\">\n            <Show when={props.icon === \"lightning\"}>\n                <Zap class=\"w-[18px]\" />\n            </Show>\n            <Show when={props.icon === \"community\"}>\n                <Users class=\"w-[18px]\" />\n            </Show>\n            <Show when={props.icon === \"chain\"}>\n                <Link class=\"w-[18px]\" />\n            </Show>\n            <h1 class=\"whitespace-nowrap text-right font-light\">\n                <Show when={props.icon === \"plus\"}>\n                    <span>+</span>\n                </Show>\n                <Show when={props.icon === \"minus\"}>\n                    <span>-</span>\n                </Show>\n                {`${prettyPrintAmount(props.amountSats)} `}\n                <span\n                    class=\"text-base font-light\"\n                    classList={{\n                        \"text-sm\": props.denominationSize === \"sm\",\n                        \"text-lg\": props.denominationSize === \"lg\",\n                        \"text-xl\": props.denominationSize === \"xl\"\n                    }}\n                >\n                    <Show\n                        when={\n                            !props.amountSats ||\n                            Number(props.amountSats) > 1 ||\n                            Number(props.amountSats) === 0\n                        }\n                    >\n                        {props.isFederation\n                            ? i18n.t(\"common.e_sats\")\n                            : i18n.t(\"common.sats\")}\n                    </Show>\n                    <Show\n                        when={\n                            props.amountSats && Number(props.amountSats) === 1\n                        }\n                    >\n                        {props.isFederation\n                            ? i18n.t(\"common.e_sat\")\n                            : i18n.t(\"common.sat\")}\n                    </Show>\n                </span>\n            </h1>\n        </div>\n    );\n}\n\nexport function AmountFiat(props: {\n    amountSats: bigint | number | undefined;\n    denominationSize?: \"sm\" | \"lg\" | \"xl\";\n}) {\n    const [state, _actions, sw] = useMegaStore();\n\n    const amountInFiat = createAsync(async () => {\n        const formattedFiat = await satsToFormattedFiat(\n            state.price,\n            Number(props.amountSats) || 0,\n            state.fiat,\n            sw\n        );\n\n        return (state.fiat.value === \"BTC\" ? \"\" : \"~\") + formattedFiat;\n    });\n\n    return (\n        <h2 class=\"whitespace-nowrap font-light\">\n            {amountInFiat()}\n            <span\n                classList={{\n                    \"text-sm\": props.denominationSize === \"sm\",\n                    \"text-lg\": props.denominationSize === \"lg\",\n                    \"text-xl\": props.denominationSize === \"xl\"\n                }}\n            >\n                {` ${state.fiat.value} `}\n            </span>\n        </h2>\n    );\n}\n\nexport function AmountSmall(props: {\n    amountSats: bigint | number | undefined;\n}) {\n    const i18n = useI18n();\n    return (\n        <span class=\"whitespace-nowrap text-sm font-light md:text-base\">\n            {`${prettyPrintAmount(props.amountSats)} `}\n            <span class=\"text-xs md:text-sm\">\n                {props.amountSats === 1 || props.amountSats === 1n\n                    ? i18n.t(\"common.sat\")\n                    : i18n.t(\"common.sats\")}\n            </span>\n        </span>\n    );\n}\n"
  },
  {
    "path": "src/components/AmountEditable.tsx",
    "content": "import {\n    createEffect,\n    createSignal,\n    onCleanup,\n    onMount,\n    ParentComponent,\n    Show\n} from \"solid-js\";\n\nimport { AmountSats, BigMoney, SharpButton } from \"~/components\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport {\n    btcFloatRounding,\n    fiatInputSanitizer,\n    fiatToSats,\n    satsInputSanitizer,\n    satsToFiat,\n    toDisplayHandleNaN\n} from \"~/utils\";\n\nexport type MethodChoice = {\n    method: \"lightning\" | \"onchain\" | \"fedimint\";\n    maxAmountSats?: bigint;\n};\n\nfunction methodToIcon(method: MethodChoice[\"method\"]) {\n    switch (method) {\n        case \"lightning\":\n            return \"lightning\";\n        case \"onchain\":\n            return \"chain\";\n        case \"fedimint\":\n            return \"community\";\n    }\n}\n\nexport const AmountEditable: ParentComponent<{\n    initialAmountSats: string | bigint;\n    setAmountSats: (s: bigint) => void;\n    frozenAmount?: boolean;\n    onSubmit?: () => void;\n    activeMethod?: MethodChoice;\n    methods?: MethodChoice[];\n    setChosenMethod?: (method: MethodChoice) => void;\n}> = (props) => {\n    const [state, _actions, sw] = useMegaStore();\n    const [mode, setMode] = createSignal<\"fiat\" | \"sats\">(\"sats\");\n    const [localSats, setLocalSats] = createSignal(\n        props.initialAmountSats.toString() || \"0\"\n    );\n    const [rawFiatAmount, setRawFiatAmount] = createSignal(\n        props.initialAmountSats.toString() || \"0\"\n    );\n    const [localFiat, setLocalFiat] = createSignal(\"0\");\n\n    createEffect(() => {\n        if (rawFiatAmount()) {\n            satsToFiat(\n                state.price,\n                Number(rawFiatAmount()) || 0,\n                state.fiat,\n                sw\n            ).then((sats) => {\n                setLocalFiat(sats);\n            });\n        }\n    });\n\n    const displaySats = () => toDisplayHandleNaN(localSats());\n    const displayFiat = () =>\n        state.price !== 0 ? toDisplayHandleNaN(localFiat(), state.fiat) : \"…\";\n\n    let satsInputRef!: HTMLInputElement;\n    let fiatInputRef!: HTMLInputElement;\n\n    createEffect(() => {\n        if (focusState() === \"focused\") {\n            props.setAmountSats(BigInt(localSats()));\n        }\n    });\n\n    function handleSatsInput(e: InputEvent) {\n        const { value } = e.target as HTMLInputElement;\n        const sane = satsInputSanitizer(value);\n        setLocalSats(sane);\n        setRawFiatAmount(Number(sane).toString() || \"0\");\n    }\n\n    /** This behaves the same as handleCharacterInput but allows for the keyboard to be used instead of the virtual keypad\n     *\n     *  if state.fiat.value === \"BTC\"\n     *  tracking e.data is required as the string is not created from just normal sequencing numbers\n     *  input - 12345\n     *  result - 0.00012345\n     *\n     *  if state.fiat.value !== \"BTC\"\n     *  Otherwise we need to account for the user inputting decimals\n     *  input - 123,45\n     *  result - 123.45\n     */\n\n    async function handleFiatInput(e: InputEvent) {\n        const { value } = e.currentTarget as HTMLInputElement;\n        let sane;\n\n        if (state.fiat.value === \"BTC\") {\n            if (e.data !== null) {\n                sane = fiatInputSanitizer(\n                    Number(localFiat()).toFixed(8) + e.data,\n                    state.fiat.maxFractionalDigits\n                );\n            } else {\n                sane = fiatInputSanitizer(\n                    // This allows us to handle the backspace key and fight float rounding\n                    btcFloatRounding(localFiat() || \"0\"),\n                    state.fiat.maxFractionalDigits\n                );\n            }\n        } else {\n            sane = fiatInputSanitizer(\n                value.replace(\",\", \".\"),\n                state.fiat.maxFractionalDigits\n            );\n        }\n        setLocalFiat(sane);\n        const sats = await fiatToSats(\n            state.price,\n            Number(sane) || 0,\n            false,\n            sw\n        );\n        setLocalSats(sats);\n    }\n\n    function toggle(disabled: boolean) {\n        if (!disabled) {\n            setMode((m) => (m === \"sats\" ? \"fiat\" : \"sats\"));\n        }\n    }\n\n    onMount(() => {\n        focus();\n    });\n\n    function focus() {\n        // Make sure we actually have the inputs mounted before we try to focus them\n        if (satsInputRef && fiatInputRef && !props.frozenAmount) {\n            if (mode() === \"sats\") {\n                satsInputRef.focus();\n            } else {\n                fiatInputRef.focus();\n            }\n        }\n    }\n\n    let divRef: HTMLDivElement;\n\n    const [focusState, setFocusState] = createSignal<\"focused\" | \"unfocused\">(\n        \"focused\"\n    );\n\n    const handleMouseDown = (e: MouseEvent) => {\n        e.preventDefault();\n        // If it was already active, we'll need to toggle\n        if (focusState() === \"unfocused\") {\n            focus();\n            setFocusState(\"focused\");\n        } else {\n            toggle(state.price === 0);\n            focus();\n        }\n    };\n\n    const handleClickOutside = (e: MouseEvent) => {\n        if (e.target instanceof Element && !divRef.contains(e.target)) {\n            setFocusState(\"unfocused\");\n        }\n    };\n\n    // When the keyboard on mobile is shown / hidden we should update our \"focus\" state\n    // TODO: find a way so this doesn't fire on devices without a virtual keyboard\n    function handleResize(e: Event) {\n        const VIEWPORT_VS_CLIENT_HEIGHT_RATIO = 0.75;\n\n        const target = e.target as VisualViewport;\n\n        if (\n            (target.height * target.scale) / window.screen.height <\n            VIEWPORT_VS_CLIENT_HEIGHT_RATIO\n        ) {\n            console.log(\"keyboard is shown\");\n            setFocusState(\"focused\");\n        } else {\n            console.log(\"keyboard is hidden\");\n            setFocusState(\"unfocused\");\n        }\n    }\n\n    onMount(() => {\n        document.body.addEventListener(\"click\", handleClickOutside);\n        if (\"visualViewport\" in window) {\n            window?.visualViewport?.addEventListener(\"resize\", handleResize);\n        }\n    });\n\n    onCleanup(() => {\n        document.body.removeEventListener(\"click\", handleClickOutside);\n        if (\"visualViewport\" in window) {\n            window?.visualViewport?.removeEventListener(\"resize\", handleResize);\n        }\n    });\n\n    return (\n        <div class=\"mx-auto flex w-full max-w-[400px] flex-col items-center\">\n            <div ref={(el) => (divRef = el)} onMouseDown={handleMouseDown}>\n                <form\n                    class=\"absolute -z-10 opacity-0\"\n                    onSubmit={(e) => {\n                        e.preventDefault();\n                        props.onSubmit\n                            ? props.onSubmit()\n                            : setFocusState(\"unfocused\");\n                    }}\n                >\n                    <input type=\"submit\" style={{ display: \"none\" }} />\n                    <input\n                        id=\"sats-input\"\n                        ref={(el) => (satsInputRef = el)}\n                        disabled={mode() === \"fiat\" || props.frozenAmount}\n                        autofocus={mode() === \"sats\"}\n                        type=\"text\"\n                        value={localSats()}\n                        onInput={handleSatsInput}\n                        inputMode={\"decimal\"}\n                        autocomplete=\"off\"\n                    />\n                    <input\n                        id=\"fiat-input\"\n                        ref={(el) => (fiatInputRef = el)}\n                        disabled={mode() === \"sats\" || props.frozenAmount}\n                        autofocus={mode() === \"fiat\"}\n                        type=\"text\"\n                        value={localFiat()}\n                        onInput={handleFiatInput}\n                        inputMode={\"decimal\"}\n                        autocomplete=\"off\"\n                    />\n                </form>\n                <BigMoney\n                    mode={mode()}\n                    displayFiat={displayFiat()}\n                    displaySats={displaySats()}\n                    onToggle={() => toggle(state.price === 0)}\n                    inputFocused={\n                        focusState() === \"focused\" && !props.frozenAmount\n                    }\n                    onFocus={() => focus()}\n                />\n            </div>\n            <Show when={props.methods?.length && props.activeMethod}>\n                <MethodChooser\n                    methods={props.methods!}\n                    activeMethod={props.activeMethod!}\n                    setChosenMethod={props.setChosenMethod}\n                />\n            </Show>\n        </div>\n    );\n};\n\nfunction MethodChooser(props: {\n    activeMethod: MethodChoice;\n    methods: MethodChoice[];\n    setChosenMethod?: (method: MethodChoice) => void;\n}) {\n    function setNextMethod() {\n        const activeIndex = props.methods.findIndex(\n            (m) => m.method === props.activeMethod.method\n        );\n        const nextMethod =\n            props.methods[\n                activeIndex === props.methods.length - 1 ? 0 : activeIndex + 1\n            ];\n        props.setChosenMethod && props.setChosenMethod(nextMethod);\n    }\n    return (\n        <>\n            <SharpButton\n                onClick={setNextMethod}\n                disabled={props.methods.length === 1}\n            >\n                <AmountSats\n                    amountSats={props.activeMethod.maxAmountSats!}\n                    denominationSize=\"sm\"\n                    icon={methodToIcon(props.activeMethod.method)}\n                />\n            </SharpButton>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/BalanceBox.tsx",
    "content": "import { A, useNavigate } from \"@solidjs/router\";\nimport { Shuffle, Users } from \"lucide-solid\";\nimport { createMemo, Match, Show, Suspense, Switch } from \"solid-js\";\n\nimport {\n    AmountFiat,\n    AmountSats,\n    ButtonCard,\n    FancyCard,\n    Indicator,\n    InfoBox,\n    MediumHeader,\n    NiceP,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function LoadingShimmer(props: { center?: boolean; small?: boolean }) {\n    return (\n        <div class=\"flex animate-pulse flex-col gap-2\">\n            <h1\n                class=\"text-4xl font-light\"\n                classList={{ \"flex justify-center\": props.center }}\n            >\n                <div\n                    class=\"rounded bg-neutral-700\"\n                    classList={{\n                        \"h-[2.5rem] w-[12rem]\": !props.small,\n                        \"h-[1rem] w-[8rem]\": props.small\n                    }}\n                />\n            </h1>\n            <Show when={!props.small}>\n                <h2\n                    class=\"text-xl font-light text-white/70\"\n                    classList={{ \"flex justify-center\": props.center }}\n                >\n                    <div class=\"h-[1.75rem] w-[8rem] rounded bg-neutral-700\" />\n                </h2>\n            </Show>\n        </div>\n    );\n}\n\nconst STYLE =\n    \"px-2 py-1 rounded-xl text-sm flex gap-2 items-center font-semibold\";\n\nexport function BalanceBox(props: { loading?: boolean; small?: boolean }) {\n    const [state, _actions] = useMegaStore();\n    const navigate = useNavigate();\n    const i18n = useI18n();\n\n    const totalOnchain = createMemo(\n        () =>\n            (state.balance?.confirmed || 0n) +\n            (state.balance?.unconfirmed || 0n) +\n            (state.balance?.force_close || 0n)\n    );\n\n    const usableOnchain = createMemo(\n        () =>\n            (state.balance?.confirmed || 0n) +\n            (state.balance?.unconfirmed || 0n)\n    );\n\n    return (\n        <VStack>\n            <Switch>\n                <Match when={state.federations && state.federations.length}>\n                    <MediumHeader>Fedimint</MediumHeader>\n                    <FancyCard>\n                        <Show\n                            when={!props.loading}\n                            fallback={<LoadingShimmer />}\n                        >\n                            <div class=\"flex justify-between\">\n                                <div class=\"flex flex-col gap-1\">\n                                    <div class=\"text-2xl\">\n                                        <AmountSats\n                                            amountSats={\n                                                state.balance?.federation || 0n\n                                            }\n                                            icon=\"community\"\n                                            denominationSize=\"lg\"\n                                            isFederation\n                                        />\n                                    </div>\n                                    <div class=\"text-lg text-white/70\">\n                                        <Suspense>\n                                            <AmountFiat\n                                                amountSats={\n                                                    state.balance?.federation ||\n                                                    0n\n                                                }\n                                                denominationSize=\"sm\"\n                                            />\n                                        </Suspense>\n                                    </div>\n                                </div>\n                                <Show\n                                    when={state.balance?.federation || 0n > 0n}\n                                >\n                                    <div class=\"self-end justify-self-end\">\n                                        <A href=\"/swaplightning\" class={STYLE}>\n                                            <Shuffle class=\"h-6 w-6\" />\n                                        </A>\n                                    </div>\n                                </Show>\n                            </div>\n                        </Show>\n                    </FancyCard>\n                    <ButtonCard\n                        onClick={() => navigate(\"/settings/federations\")}\n                    >\n                        <div class=\"flex items-center gap-2\">\n                            <Users class=\"inline-block text-m-red\" />\n                            <NiceP>{i18n.t(\"profile.manage_federation\")}</NiceP>\n                        </div>\n                    </ButtonCard>\n                </Match>\n                <Match when={true}>\n                    <ButtonCard\n                        onClick={() => navigate(\"/settings/federations\")}\n                    >\n                        <div class=\"flex items-center gap-2\">\n                            <Users class=\"inline-block text-m-red\" />\n                            <NiceP>{i18n.t(\"profile.join_federation\")}</NiceP>\n                        </div>\n                    </ButtonCard>\n                </Match>\n            </Switch>\n            <MediumHeader>{i18n.t(\"profile.self_custody\")}</MediumHeader>\n            <FancyCard>\n                <Show when={!props.loading} fallback={<LoadingShimmer />}>\n                    <Switch>\n                        <Match when={state.safe_mode}>\n                            <div class=\"flex flex-col gap-1\">\n                                <InfoBox accent=\"red\">\n                                    {i18n.t(\"common.error_safe_mode\")}\n                                </InfoBox>\n                            </div>\n                        </Match>\n                        <Match when={true}>\n                            <div class=\"flex flex-col gap-1\">\n                                <div class=\"text-2xl\">\n                                    <AmountSats\n                                        amountSats={\n                                            state.balance?.lightning || 0\n                                        }\n                                        icon=\"lightning\"\n                                        denominationSize=\"lg\"\n                                    />\n                                </div>\n                                <div class=\"text-lg text-white/70\">\n                                    <Suspense>\n                                        <AmountFiat\n                                            amountSats={\n                                                state.balance?.lightning || 0\n                                            }\n                                            denominationSize=\"sm\"\n                                        />\n                                    </Suspense>\n                                </div>\n                            </div>\n                        </Match>\n                    </Switch>\n                </Show>\n                <hr class=\"my-2 border-m-grey-750\" />\n                <Show when={!props.loading} fallback={<LoadingShimmer />}>\n                    <div class=\"flex justify-between\">\n                        <div class=\"flex flex-col gap-1\">\n                            <div class=\"text-2xl\">\n                                <AmountSats\n                                    amountSats={totalOnchain()}\n                                    icon=\"chain\"\n                                    denominationSize=\"lg\"\n                                />\n                            </div>\n                            <div class=\"text-lg text-white/70\">\n                                <Suspense>\n                                    <AmountFiat\n                                        amountSats={totalOnchain()}\n                                        denominationSize=\"sm\"\n                                    />\n                                </Suspense>\n                            </div>\n                        </div>\n                        <div class=\"flex flex-col items-end justify-between gap-1\">\n                            <Show when={state.balance?.unconfirmed != 0n}>\n                                <Indicator>\n                                    {i18n.t(\"common.pending\")}\n                                </Indicator>\n                            </Show>\n                            <Show when={state.balance?.unconfirmed === 0n}>\n                                <div />\n                            </Show>\n                            <Show when={usableOnchain() > 0n}>\n                                <div class=\"self-end justify-self-end\">\n                                    <A href=\"/swap\" class={STYLE}>\n                                        <Shuffle class=\"h-6 w-6\" />\n                                    </A>\n                                </div>\n                            </Show>\n                        </div>\n                    </div>\n                </Show>\n            </FancyCard>\n        </VStack>\n    );\n}\n"
  },
  {
    "path": "src/components/BigMoney.tsx",
    "content": "import { ArrowDownUp } from \"lucide-solid\";\nimport { Show } from \"solid-js\";\n\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { Currency } from \"~/utils\";\n\nfunction BigScalingText(props: {\n    text: string;\n    fiat?: Currency;\n    mode: \"fiat\" | \"sats\";\n    loading: boolean;\n}) {\n    const chars = () => props.text.length;\n    const i18n = useI18n();\n\n    return (\n        <h1\n            class=\"whitespace-nowrap px-2 text-center text-4xl font-light transition-transform duration-300 ease-out\"\n            classList={{\n                \"scale-90\": chars() >= 11,\n                \"scale-95\": chars() === 10,\n                \"scale-100\": chars() === 9,\n                \"scale-105\": chars() === 7,\n                \"scale-110\": chars() === 6,\n                \"scale-125\": chars() === 5,\n                \"scale-150\": chars() <= 4\n            }}\n        >\n            <Show when={!props.loading || props.mode === \"sats\"} fallback=\"…\">\n                {!props.loading && props.mode === \"sats\"}\n                {props.mode === \"fiat\" &&\n                    //adds only the symbol\n                    props.fiat?.hasSymbol}\n                {`${props.text} `}\n                <span class=\"text-xl\">\n                    {props.fiat ? props.fiat.value : i18n.t(\"common.sats\")}\n                </span>\n            </Show>\n        </h1>\n    );\n}\n\nfunction SmallSubtleAmount(props: {\n    text: string;\n    fiat?: Currency;\n    mode: \"fiat\" | \"sats\";\n    loading: boolean;\n}) {\n    const i18n = useI18n();\n\n    return (\n        <h2\n            class=\"flex flex-row items-center whitespace-nowrap text-xl font-light text-m-grey-350\"\n            tabIndex={0}\n        >\n            <Show when={!props.loading || props.mode === \"fiat\"} fallback=\"…\">\n                {props.fiat?.value !== \"BTC\" && props.mode === \"sats\" && \"~\"}\n                {props.mode === \"sats\" &&\n                    //adds only the symbol\n                    props.fiat?.hasSymbol}\n                {`${props.text} `}\n                {/* IDK why a space doesn't work here */}\n                <span class=\"flex-0 w-1\">{\"\"}</span>\n                <span class=\"text-base\">\n                    {props.fiat ? props.fiat.value : i18n.t(\"common.sats\")}\n                </span>\n                <ArrowDownUp class=\"flex-0 inline-block h-6 w-6 pl-2 hover:cursor-pointer\" />\n            </Show>\n        </h2>\n    );\n}\n\nexport function BigMoney(props: {\n    mode: \"fiat\" | \"sats\";\n    displaySats: string;\n    displayFiat: string;\n    onToggle: () => void;\n    inputFocused: boolean;\n    onFocus: () => void;\n}) {\n    const [state, _actions] = useMegaStore();\n\n    return (\n        <div class=\"flex justify-center\">\n            <div class=\"flex w-max flex-col items-center justify-center p-4\">\n                <BigScalingText\n                    text={\n                        props.mode === \"fiat\"\n                            ? props.displayFiat\n                            : props.displaySats\n                    }\n                    fiat={props.mode === \"fiat\" ? state.fiat : undefined}\n                    mode={props.mode}\n                    loading={state.price === 0}\n                />\n                <div\n                    class=\"mb-2 mt-4 h-[2px] w-full rounded-full\"\n                    classList={{\n                        \"bg-m-blue\": props.inputFocused,\n                        \"bg-m-blue/0\": !props.inputFocused\n                    }}\n                />\n                <SmallSubtleAmount\n                    text={\n                        props.mode !== \"fiat\"\n                            ? props.displayFiat\n                            : props.displaySats\n                    }\n                    fiat={props.mode !== \"fiat\" ? state.fiat : undefined}\n                    mode={props.mode}\n                    loading={state.price === 0}\n                />\n            </div>\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/ChooseCurrency.tsx",
    "content": "import { createForm } from \"@modular-forms/solid\";\nimport { useNavigate } from \"@solidjs/router\";\nimport { createSignal, For, Show } from \"solid-js\";\n\nimport { Button, ExternalLink, InfoBox, NiceP, VStack } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport {\n    BTC_OPTION,\n    Currency,\n    eify,\n    FIAT_OPTIONS,\n    timeout,\n    USD_OPTION\n} from \"~/utils\";\n\ntype ChooseCurrencyForm = {\n    fiatCurrency: string;\n};\n\nconst COMBINED_OPTIONS: Currency[] = [USD_OPTION, BTC_OPTION, ...FIAT_OPTIONS];\n\nexport function ChooseCurrency() {\n    const i18n = useI18n();\n    const [error, setError] = createSignal<Error>();\n    const [state, actions] = useMegaStore();\n    const [loading, setLoading] = createSignal(false);\n    const navigate = useNavigate();\n\n    function findCurrencyByValue(value: string) {\n        return (\n            COMBINED_OPTIONS.find((currency) => currency.value === value) ??\n            USD_OPTION\n        );\n    }\n\n    const [_chooseCurrencyForm, { Form, Field }] =\n        createForm<ChooseCurrencyForm>({\n            initialValues: {\n                fiatCurrency: state.fiat.value\n            },\n            validate: (values) => {\n                const errors: Record<string, string> = {};\n                if (values.fiatCurrency === undefined) {\n                    errors.fiatCurrency = i18n.t(\n                        \"settings.currency.error_unsupported_currency\"\n                    );\n                }\n                return errors;\n            }\n        });\n\n    const handleFormSubmit = async (f: ChooseCurrencyForm) => {\n        setLoading(true);\n        try {\n            actions.saveFiat(findCurrencyByValue(f.fiatCurrency));\n\n            await timeout(1000);\n            navigate(\"/\");\n        } catch (e) {\n            console.error(e);\n            setError(eify(e));\n            setLoading(false);\n        }\n    };\n\n    return (\n        <VStack>\n            <Form onSubmit={handleFormSubmit} class=\"flex flex-col gap-4\">\n                <NiceP>{i18n.t(\"settings.currency.caption\")}</NiceP>\n                <ExternalLink href=\"https://github.com/MutinyWallet/mutiny-web/issues/new\">\n                    {i18n.t(\"settings.currency.request_currency_support_link\")}\n                </ExternalLink>\n                <div />\n                <VStack>\n                    <Field name=\"fiatCurrency\">\n                        {(field, props) => (\n                            <select\n                                {...props}\n                                value={field.value}\n                                class=\"w-full rounded-lg bg-m-grey-750 py-2 pl-4 pr-12 text-base font-normal text-white\"\n                            >\n                                <For each={COMBINED_OPTIONS}>\n                                    {({ value, label }) => (\n                                        <option\n                                            selected={field.value === value}\n                                            value={value}\n                                        >\n                                            {label}\n                                        </option>\n                                    )}\n                                </For>\n                            </select>\n                        )}\n                    </Field>\n                    <Show when={error()}>\n                        <InfoBox accent=\"red\">{error()?.message}</InfoBox>\n                    </Show>\n                    <div />\n                    <Button intent=\"blue\" loading={loading()}>\n                        {i18n.t(\"settings.currency.select_currency\")}\n                    </Button>\n                </VStack>\n            </Form>\n        </VStack>\n    );\n}\n"
  },
  {
    "path": "src/components/ChooseLanguage.tsx",
    "content": "import { createForm } from \"@modular-forms/solid\";\nimport { useNavigate } from \"@solidjs/router\";\nimport { createSignal, For, Show } from \"solid-js\";\n\nimport { Button, ExternalLink, InfoBox, NiceP, VStack } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify, EN_OPTION, Language, LANGUAGE_OPTIONS, timeout } from \"~/utils\";\n\ntype ChooseLanguageForm = {\n    selectedLanguage: string;\n};\n\nconst COMBINED_OPTIONS: Language[] = [EN_OPTION, ...LANGUAGE_OPTIONS];\n\nexport function ChooseLanguage() {\n    const i18n = useI18n();\n    const [error, setError] = createSignal<Error>();\n    const [state, actions] = useMegaStore();\n    const [loading, setLoading] = createSignal(false);\n    const navigate = useNavigate();\n\n    const [_chooseLanguageForm, { Form, Field }] =\n        createForm<ChooseLanguageForm>({\n            initialValues: {\n                selectedLanguage: state.lang ?? i18n.language\n            },\n            validate: (values) => {\n                const errors: Record<string, string> = {};\n                if (values.selectedLanguage === undefined) {\n                    errors.selectedLanguage = i18n.t(\n                        \"settings.language.error_unsupported_language\"\n                    );\n                }\n                return errors;\n            }\n        });\n\n    const handleFormSubmit = async (f: ChooseLanguageForm) => {\n        setLoading(true);\n        try {\n            actions.saveLanguage(f.selectedLanguage);\n\n            await i18n.changeLanguage(f.selectedLanguage);\n\n            await timeout(1000);\n            navigate(\"/\");\n        } catch (e) {\n            console.error(e);\n            setError(eify(e));\n            setLoading(false);\n        }\n    };\n\n    return (\n        <VStack>\n            <Form onSubmit={handleFormSubmit} class=\"flex flex-col gap-4\">\n                <NiceP>{i18n.t(\"settings.language.caption\")}</NiceP>\n                <ExternalLink href=\"https://github.com/MutinyWallet/mutiny-web/issues/new\">\n                    {i18n.t(\"settings.language.request_language_support_link\")}\n                </ExternalLink>\n                <div />\n                <VStack>\n                    <Field name=\"selectedLanguage\">\n                        {(field, props) => (\n                            <select\n                                {...props}\n                                value={field.value}\n                                class=\"w-full rounded-lg bg-m-grey-750 py-2 pl-4 pr-12 text-base font-normal text-white\"\n                            >\n                                <For each={COMBINED_OPTIONS}>\n                                    {({ value, shortName }) => (\n                                        <option\n                                            selected={field.value === shortName}\n                                            value={shortName}\n                                        >\n                                            {value}\n                                        </option>\n                                    )}\n                                </For>\n                            </select>\n                        )}\n                    </Field>\n                    <Show when={error()}>\n                        <InfoBox accent=\"red\">{error()?.message}</InfoBox>\n                    </Show>\n                    <div />\n                    <Button intent=\"blue\" loading={loading()}>\n                        {i18n.t(\"settings.language.select_language\")}\n                    </Button>\n                </VStack>\n            </Form>\n        </VStack>\n    );\n}\n"
  },
  {
    "path": "src/components/ContactButton.tsx",
    "content": "import { TagItem } from \"@mutinywallet/mutiny-wasm\";\n\nimport { LabelCircle } from \"~/components\";\nimport { PseudoContact } from \"~/utils\";\n\nexport function ContactButton(props: {\n    contact: PseudoContact | TagItem;\n    onClick: () => void;\n}) {\n    return (\n        <button\n            class=\"flex items-center gap-2 overflow-clip\"\n            onClick={() => props.onClick()}\n        >\n            <LabelCircle\n                name={props.contact.name}\n                image_url={props.contact.primal_image_url}\n                contact\n                label={false}\n            />\n            <div class=\"flex flex-1 flex-col items-start\">\n                <h2 class=\"overflow-hidden overflow-ellipsis text-base font-semibold\">\n                    {props.contact.name}\n                </h2>\n                <h3 class=\"overflow-hidden overflow-ellipsis text-left text-xs font-normal text-m-grey-400\">\n                    {props.contact.ln_address || \"\"}\n                </h3>\n            </div>\n        </button>\n    );\n}\n"
  },
  {
    "path": "src/components/ContactEditor.tsx",
    "content": "import { SubmitHandler } from \"@modular-forms/solid\";\nimport { createSignal, Match, Switch } from \"solid-js\";\n\nimport {\n    ContactForm,\n    ContactFormValues,\n    SimpleDialog,\n    SmallHeader\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nexport function ContactEditor(props: {\n    createContact: (contact: ContactFormValues) => void;\n    list?: boolean;\n}) {\n    const i18n = useI18n();\n    const [isOpen, setIsOpen] = createSignal(false);\n\n    // What we're all here for in the first place: returning a value\n    const handleSubmit: SubmitHandler<ContactFormValues> = (\n        c: ContactFormValues\n    ) => {\n        props.createContact(c);\n        setIsOpen(false);\n    };\n\n    return (\n        <>\n            <Switch>\n                <Match when={props.list}>\n                    <button\n                        onClick={() => setIsOpen(true)}\n                        class=\"flex flex-col items-center gap-2\"\n                    >\n                        <div class=\"flex h-16 w-16 flex-none items-center justify-center rounded-full bg-neutral-500 text-4xl uppercase \">\n                            <span class=\"leading-[4rem]\">+</span>\n                        </div>\n                        <SmallHeader class=\"overflow-ellipsis\">\n                            {i18n.t(\"contacts.new\")}\n                        </SmallHeader>\n                    </button>\n                </Match>\n                <Match when={!props.list}>\n                    <button\n                        onClick={() => setIsOpen(true)}\n                        class=\"flex w-full items-center gap-2 rounded-lg bg-neutral-700 p-2\"\n                    >\n                        <h2 class=\"overflow-hidden overflow-ellipsis text-base font-semibold\">\n                            + {i18n.t(\"contacts.add_contact\")}\n                        </h2>\n                    </button>\n                </Match>\n            </Switch>\n            <SimpleDialog\n                open={isOpen()}\n                setOpen={setIsOpen}\n                title={i18n.t(\"contacts.new_contact\")}\n            >\n                <ContactForm\n                    cta={i18n.t(\"contacts.create_contact\")}\n                    handleSubmit={handleSubmit}\n                />\n            </SimpleDialog>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/ContactForm.tsx",
    "content": "import {\n    createForm,\n    custom,\n    email,\n    required,\n    SubmitHandler\n} from \"@modular-forms/solid\";\n\nimport { Button, ContactFormValues, TextField, VStack } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore, WalletWorker } from \"~/state/megaStore\";\nimport { hexpubFromNpub } from \"~/utils\";\n\nconst validateNpub = async (sw: WalletWorker, value?: string) => {\n    if (!value) {\n        return false;\n    }\n    try {\n        const hexpub = await hexpubFromNpub(sw, value);\n        if (!hexpub) {\n            return false;\n        }\n        return true;\n    } catch (e) {\n        return false;\n    }\n};\n\nexport function ContactForm(props: {\n    handleSubmit: SubmitHandler<ContactFormValues>;\n    initialValues?: ContactFormValues;\n    cta: string;\n}) {\n    const [_state, _actions, sw] = useMegaStore();\n    const i18n = useI18n();\n    const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({\n        initialValues: props.initialValues\n    });\n\n    return (\n        <Form\n            onSubmit={props.handleSubmit}\n            class=\"mx-auto flex w-full flex-1 flex-col justify-around gap-4\"\n        >\n            <div>\n                <VStack>\n                    <Field\n                        name=\"name\"\n                        validate={[required(i18n.t(\"contacts.error_name\"))]}\n                    >\n                        {(field, props) => (\n                            <TextField\n                                {...props}\n                                placeholder={i18n.t(\"contacts.placeholder\")}\n                                value={field.value}\n                                error={field.error}\n                                label={i18n.t(\"contacts.name\")}\n                            />\n                        )}\n                    </Field>\n                    <Field\n                        name=\"ln_address\"\n                        validate={[\n                            required(\n                                i18n.t(\"contacts.error_ln_address_missing\")\n                            ),\n                            email(i18n.t(\"contacts.email_error\"))\n                        ]}\n                    >\n                        {(field, props) => (\n                            <TextField\n                                {...props}\n                                placeholder=\"example@example.com\"\n                                value={field.value}\n                                error={field.error}\n                                label={i18n.t(\"contacts.ln_address\")}\n                            />\n                        )}\n                    </Field>\n                    <Field\n                        name=\"npub\"\n                        validate={[\n                            custom(\n                                (v) => validateNpub(sw, v),\n                                i18n.t(\"contacts.npub_error\")\n                            )\n                        ]}\n                    >\n                        {(field, props) => (\n                            <TextField\n                                {...props}\n                                placeholder=\"npub1...\"\n                                value={field.value}\n                                error={field.error}\n                                label={i18n.t(\"contacts.npub\")}\n                            />\n                        )}\n                    </Field>\n                </VStack>\n            </div>\n            <VStack>\n                <Button type=\"submit\" intent=\"blue\">\n                    {props.cta}\n                </Button>\n            </VStack>\n        </Form>\n    );\n}\n"
  },
  {
    "path": "src/components/ContactViewer.tsx",
    "content": "import { SubmitHandler } from \"@modular-forms/solid\";\nimport { TagItem } from \"@mutinywallet/mutiny-wasm\";\nimport { useNavigate } from \"@solidjs/router\";\nimport { createSignal, JSX, Match, Show, Switch } from \"solid-js\";\n\nimport {\n    Button,\n    ConfirmDialog,\n    ContactForm,\n    KeyValue,\n    MiniStringShower,\n    showToast,\n    SimpleDialog,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { toParsedParams } from \"~/logic/waila\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport type ContactFormValues = {\n    name: string;\n    npub?: string;\n    ln_address?: string;\n};\n\nexport function ContactViewer(props: {\n    children: JSX.Element;\n    contact: TagItem;\n    saveContact: (id: string, contact: ContactFormValues) => void;\n    deleteContact: (id: string) => Promise<void>;\n}) {\n    const i18n = useI18n();\n    const [isOpen, setIsOpen] = createSignal(false);\n    const [isEditing, setIsEditing] = createSignal(false);\n    const [state, actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n    const [confirmOpen, setConfirmOpen] = createSignal(false);\n\n    const handleSubmit: SubmitHandler<ContactFormValues> = (\n        c: ContactFormValues\n    ) => {\n        const id = props.contact.id;\n\n        props.saveContact(id, c);\n        setIsEditing(false);\n    };\n\n    const handleDelete = async () => {\n        const id = props.contact.id;\n\n        await props.deleteContact(id);\n        setIsOpen(false);\n    };\n\n    const handlePay = async () => {\n        const network = state.network || \"signet\";\n\n        const lnurl = props.contact.lnurl || props.contact.ln_address || \"\";\n\n        if (lnurl) {\n            const result = await toParsedParams(lnurl, network, sw);\n            if (!result.ok) {\n                showToast(result.error);\n                return;\n            } else {\n                result.value.privateTag = props.contact.name;\n                if (\n                    result.value?.address ||\n                    result.value?.invoice ||\n                    result.value?.node_pubkey ||\n                    result.value?.lnurl\n                ) {\n                    actions.setScanResult(result.value);\n                    navigate(\"/send\");\n                }\n            }\n        }\n    };\n\n    return (\n        <>\n            <button onClick={() => setIsOpen(true)}>{props.children}</button>\n            <SimpleDialog\n                open={isOpen()}\n                setOpen={setIsOpen}\n                title={isEditing() ? i18n.t(\"contacts.edit_contact\") : \"\"}\n            >\n                <Switch>\n                    <Match when={isEditing()}>\n                        <ContactForm\n                            cta={i18n.t(\"contacts.save_contact\")}\n                            handleSubmit={handleSubmit}\n                            initialValues={props.contact}\n                        />\n                        <Button\n                            intent=\"red\"\n                            onClick={() => setConfirmOpen(true)}\n                        >\n                            {i18n.t(\"contacts.delete\")}\n                        </Button>\n                        <ConfirmDialog\n                            open={confirmOpen()}\n                            loading={false}\n                            onConfirm={handleDelete}\n                            onCancel={() => setConfirmOpen(false)}\n                        >\n                            {i18n.t(\"contacts.confirm_delete\")}\n                        </ConfirmDialog>\n                    </Match>\n                    <Match when={!isEditing()}>\n                        <div class=\"mx-auto flex w-full max-w-[400px] flex-1 flex-col items-center justify-around gap-4\">\n                            <div class=\"flex w-full flex-col items-center\">\n                                <div class=\"flex h-32 w-32 flex-none items-center justify-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 text-8xl uppercase\">\n                                    <Switch>\n                                        <Match when={props.contact.image_url}>\n                                            <img\n                                                src={props.contact.image_url}\n                                            />\n                                        </Match>\n                                        <Match when={true}>\n                                            {props.contact.name[0]}\n                                        </Match>\n                                    </Switch>\n                                </div>\n\n                                <h1 class=\"mb-4 mt-2 text-2xl font-semibold uppercase\">\n                                    {props.contact.name}\n                                </h1>\n\n                                <div class=\"flex flex-1 flex-col justify-center\">\n                                    <VStack>\n                                        <Show when={props.contact.npub}>\n                                            <KeyValue key={\"Npub\"}>\n                                                <MiniStringShower\n                                                    text={props.contact.npub!}\n                                                />\n                                            </KeyValue>\n                                        </Show>\n                                        <Show when={props.contact.ln_address}>\n                                            <KeyValue\n                                                key={i18n.t(\n                                                    \"contacts.lightning_address\"\n                                                )}\n                                            >\n                                                <MiniStringShower\n                                                    text={\n                                                        props.contact\n                                                            .ln_address!\n                                                    }\n                                                />\n                                            </KeyValue>\n                                        </Show>\n                                    </VStack>\n                                </div>\n                            </div>\n                            <div class=\"flex w-full gap-2\">\n                                <Button\n                                    layout=\"flex\"\n                                    intent=\"green\"\n                                    onClick={() => setIsEditing(true)}\n                                >\n                                    {i18n.t(\"contacts.edit\")}\n                                </Button>\n                                <Button\n                                    intent=\"blue\"\n                                    disabled={\n                                        !props.contact.lnurl &&\n                                        !props.contact.ln_address\n                                    }\n                                    onClick={handlePay}\n                                >\n                                    {i18n.t(\"contacts.pay\")}\n                                </Button>\n                            </div>\n                        </div>\n                    </Match>\n                </Switch>\n            </SimpleDialog>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/DecryptDialog.tsx",
    "content": "import { A } from \"@solidjs/router\";\nimport { createSignal, Show } from \"solid-js\";\n\nimport { Button, InfoBox, SimpleDialog, TextField } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify } from \"~/utils\";\n\nexport function DecryptDialog() {\n    const i18n = useI18n();\n    const [state, actions] = useMegaStore();\n\n    const [password, setPassword] = createSignal(\"\");\n    const [loading, setLoading] = createSignal(false);\n    const [error, setError] = createSignal(\"\");\n\n    async function decrypt(e: Event) {\n        e.preventDefault();\n        setLoading(true);\n        try {\n            await actions.setup(password());\n\n            // If we get this far and the state stills wants a password that means the password was wrong\n            if (state.needs_password) {\n                throw new Error(\"wrong\");\n            }\n        } catch (e) {\n            const err = eify(e);\n            console.error(e);\n            if (err.message === \"wrong\") {\n                setError(i18n.t(\"settings.decrypt.error_wrong_password\"));\n            } else {\n                throw e;\n            }\n        } finally {\n            setLoading(false);\n        }\n    }\n\n    function noop() {\n        // noop\n    }\n\n    return (\n        <SimpleDialog\n            title={i18n.t(\"settings.decrypt.title\")}\n            // Only show the dialog if we need a password and there's no setup error\n            open={state.needs_password && !state.setup_error}\n        >\n            <form onSubmit={decrypt}>\n                <div class=\"flex flex-col gap-4\">\n                    <TextField\n                        name=\"password\"\n                        type=\"password\"\n                        ref={noop}\n                        value={password()}\n                        onInput={(e) => setPassword(e.currentTarget.value)}\n                        error={\"\"}\n                        onBlur={noop}\n                        onChange={noop}\n                    />\n                    <Show when={error()}>\n                        <InfoBox accent=\"red\">{error()}</InfoBox>\n                    </Show>\n                    <Button intent=\"blue\" loading={loading()} onClick={decrypt}>\n                        {i18n.t(\"settings.decrypt.decrypt_wallet\")}\n                    </Button>\n                </div>\n            </form>\n            <A class=\"self-end text-m-grey-400\" href=\"/settings/restore\">\n                {i18n.t(\"settings.decrypt.forgot_password_link\")}\n            </A>\n        </SimpleDialog>\n    );\n}\n"
  },
  {
    "path": "src/components/DeleteEverything.tsx",
    "content": "import { SecureStoragePlugin } from \"capacitor-secure-storage-plugin\";\nimport { createSignal } from \"solid-js\";\n\nimport { Button, ConfirmDialog, showToast } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify } from \"~/utils\";\n\nexport function DeleteEverything(props: { emergency?: boolean }) {\n    const i18n = useI18n();\n    const [state, actions, sw] = useMegaStore();\n\n    async function confirmReset() {\n        setConfirmOpen(true);\n    }\n\n    const [confirmOpen, setConfirmOpen] = createSignal(false);\n    const [confirmLoading, setConfirmLoading] = createSignal(false);\n\n    async function resetNode() {\n        try {\n            setConfirmLoading(true);\n\n            localStorage.removeItem(\"profile_setup_stage\");\n\n            // Remove the nsec if it exists\n            await SecureStoragePlugin.clear();\n\n            // If we're in a context where the wallet is loaded we want to use the regular action to delete it\n            // Otherwise we just call the import_json method directly\n            if (state.load_stage === \"done\" && !props.emergency) {\n                try {\n                    await actions.deleteMutinyWallet();\n                } catch (e) {\n                    // If we can't stop we want to keep going\n                    console.error(e);\n                }\n            } else {\n                await sw.import_json(\"{}\");\n            }\n\n            showToast({\n                title: i18n.t(\n                    \"settings.emergency_kit.delete_everything.deleted\"\n                ),\n                description: i18n.t(\n                    \"settings.emergency_kit.delete_everything.deleted_description\"\n                )\n            });\n\n            setTimeout(() => {\n                window.location.href = \"/\";\n            }, 1000);\n        } catch (e) {\n            console.error(e);\n            showToast(eify(e));\n        } finally {\n            setConfirmOpen(false);\n            setConfirmLoading(false);\n        }\n    }\n\n    return (\n        <>\n            <Button onClick={confirmReset}>\n                {i18n.t(\"settings.emergency_kit.delete_everything.delete\")}\n            </Button>\n            <ConfirmDialog\n                loading={confirmLoading()}\n                open={confirmOpen()}\n                onConfirm={resetNode}\n                onCancel={() => setConfirmOpen(false)}\n            >\n                {i18n.t(\"settings.emergency_kit.delete_everything.confirm\")}\n            </ConfirmDialog>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/EditProfileForm.tsx",
    "content": "import { createForm, email } from \"@modular-forms/solid\";\nimport { createFileUploader } from \"@solid-primitives/upload\";\nimport { Pencil } from \"lucide-solid\";\nimport { createSignal, Match, Show, Switch } from \"solid-js\";\n\nimport { Button, InfoBox, TextField, VStack } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { blobToBase64, eify } from \"~/utils\";\n\nexport type EditableProfile = {\n    nym?: string;\n    lightningAddress?: string;\n    imageUrl?: string;\n};\n\nexport function EditProfileForm(props: {\n    initialProfile?: EditableProfile;\n    onSave: (profile: EditableProfile) => Promise<void>;\n    saving: boolean;\n    cta: string;\n}) {\n    const [_state, _actions, sw] = useMegaStore();\n    const [uploading, setUploading] = createSignal(false);\n    const [uploadError, setUploadError] = createSignal<Error>();\n\n    const { files, selectFiles } = createFileUploader({\n        multiple: false,\n        accept: \"image/*\"\n    });\n\n    async function uploadFile() {\n        console.log(\"uploadFile\");\n        await selectFiles(async (files) => {\n            if (files.length) {\n                return;\n            }\n        });\n    }\n\n    const i18n = useI18n();\n    const [profileForm, { Form, Field }] = createForm<EditableProfile>({\n        initialValues: { ...props.initialProfile }\n    });\n\n    async function handleSubmit(profile: EditableProfile) {\n        setUploading(true);\n        setUploadError(undefined);\n        try {\n            let imageUrl;\n            if (files() && files().length) {\n                const base64 = await blobToBase64(files()[0].file);\n                if (base64) {\n                    imageUrl = await sw.upload_profile_pic(base64);\n                }\n            }\n            await props.onSave({\n                nym: profile.nym,\n                lightningAddress: profile.lightningAddress,\n                imageUrl: imageUrl ? imageUrl : props.initialProfile?.imageUrl\n            });\n        } catch (e) {\n            setUploadError(eify(e));\n            console.error(e);\n        }\n        setUploading(false);\n    }\n\n    return (\n        <>\n            <VStack>\n                <button class=\"relative self-center\" onClick={uploadFile}>\n                    <div class=\"shiny-button flex h-[8rem] w-[8rem] flex-none items-center justify-center self-center overflow-clip rounded-full bg-m-grey-800 text-5xl uppercase\">\n                        <Switch>\n                            <Match when={files() && files().length}>\n                                <img src={files()[0].source} />\n                            </Match>\n                            <Match when={props.initialProfile?.imageUrl}>\n                                <img src={props.initialProfile?.imageUrl} />\n                            </Match>\n                        </Switch>\n                    </div>\n                    <div class=\"absolute top-0 flex h-[8rem] w-[8rem] items-center justify-center bg-m-grey-975/25\">\n                        <Pencil />\n                    </div>\n                </button>\n                <VStack>\n                    <Form\n                        onSubmit={handleSubmit}\n                        class=\"mx-auto flex w-full flex-1 flex-col justify-around gap-4\"\n                    >\n                        <Field name=\"nym\">\n                            {(field, props) => (\n                                <TextField\n                                    {...props}\n                                    placeholder={i18n.t(\"contacts.placeholder\")}\n                                    value={field.value}\n                                    error={field.error}\n                                    label={i18n.t(\"profile.edit.nym\")}\n                                />\n                            )}\n                        </Field>\n                        <Show when={props.cta !== \"Create\"}>\n                            <Field\n                                name=\"lightningAddress\"\n                                validate={[\n                                    email(i18n.t(\"contacts.email_error\"))\n                                ]}\n                            >\n                                {(field, props) => (\n                                    <TextField\n                                        {...props}\n                                        placeholder=\"example@example.com\"\n                                        value={field.value}\n                                        error={field.error}\n                                        label={i18n.t(\"contacts.ln_address\")}\n                                    />\n                                )}\n                            </Field>\n                        </Show>\n                        <Button\n                            layout=\"full\"\n                            type=\"submit\"\n                            loading={\n                                props.saving ||\n                                uploading() ||\n                                profileForm.submitting\n                            }\n                        >\n                            {props.cta}\n                        </Button>\n                    </Form>\n                    <Show when={uploadError()}>\n                        <InfoBox accent=\"red\">{uploadError()?.message}</InfoBox>\n                    </Show>\n                </VStack>\n            </VStack>\n            <div class=\"flex-1\" />\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/ErrorDisplay.tsx",
    "content": "import { Title } from \"@solidjs/meta\";\nimport { A } from \"@solidjs/router\";\nimport { onMount } from \"solid-js\";\n\nimport { ExternalLink } from \"~/components\";\nimport {\n    Button,\n    DefaultMain,\n    LargeHeader,\n    NiceP,\n    SmallHeader\n} from \"~/components/layout\";\nimport { useI18n } from \"~/i18n/context\";\n\nexport function SimpleErrorDisplay(props: { error: Error }) {\n    return (\n        <p class=\"rounded-xl bg-white/10 p-4 font-mono\">\n            <span class=\"font-bold\">{props.error.name}</span>:{\" \"}\n            {props.error.message}\n        </p>\n    );\n}\n\nexport function ErrorDisplay(props: { error: Error }) {\n    const i18n = useI18n();\n    onMount(() => {\n        console.error(props.error);\n    });\n    return (\n        <DefaultMain>\n            <Title>{i18n.t(\"error.general.oh_no\")}</Title>\n            <LargeHeader>{i18n.t(\"error.title\")}</LargeHeader>\n            <SmallHeader>\n                {i18n.t(\"error.general.never_should_happen\")}\n            </SmallHeader>\n            <SimpleErrorDisplay error={props.error} />\n            <NiceP>\n                {i18n.t(\"error.general.try_reloading\")}{\" \"}\n                <ExternalLink href=\"https://matrix.to/#/#mutiny-community:lightninghackers.com\">\n                    {i18n.t(\"error.general.support_link\")}\n                </ExternalLink>\n            </NiceP>\n            <Button onClick={() => window.location.reload()}>\n                {i18n.t(\"error.reload\")}\n            </Button>\n            <NiceP>\n                {i18n.t(\"error.general.getting_desperate\")}{\" \"}\n                <A href=\"/settings/emergencykit\">\n                    {i18n.t(\"error.emergency_link\")}\n                </A>\n            </NiceP>\n            <div class=\"h-full\" />\n            <Button onClick={() => (window.location.href = \"/\")} intent=\"red\">\n                {i18n.t(\"common.dangit\")}\n            </Button>\n        </DefaultMain>\n    );\n}\n"
  },
  {
    "path": "src/components/Fab.tsx",
    "content": "import { useNavigate } from \"@solidjs/router\";\nimport { ArrowDownLeft, ArrowUpRight, Plus, Scan } from \"lucide-solid\";\nimport { createSignal, JSX, onCleanup, onMount, Show } from \"solid-js\";\n\nimport { Circle } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nfunction FabMenuItem(props: {\n    onClick: () => void;\n    disabled?: boolean;\n    children: JSX.Element;\n}) {\n    return (\n        <button\n            class=\"flex gap-2 px-2 py-4 disabled:opacity-50\"\n            disabled={props.disabled}\n            onClick={() => props.onClick()}\n        >\n            {props.children}\n        </button>\n    );\n}\n\nexport function FabMenu(props: {\n    setOpen: (open: boolean) => void;\n    children: JSX.Element;\n    right?: boolean;\n    left?: boolean;\n}) {\n    let navRef: HTMLElement;\n\n    const handleClickOutside = (e: MouseEvent) => {\n        if (e.target instanceof Element && !navRef.contains(e.target)) {\n            e.stopPropagation();\n            props.setOpen(false);\n        }\n    };\n\n    onMount(() => {\n        document.body.addEventListener(\"click\", handleClickOutside);\n    });\n\n    onCleanup(() => {\n        document.body.removeEventListener(\"click\", handleClickOutside);\n    });\n\n    return (\n        <nav\n            ref={(el) => (navRef = el)}\n            class=\"fixed z-50 rounded-xl bg-m-grey-800/90 px-2 backdrop-blur-lg\"\n            classList={{\n                \"right-8 bottom-[calc(2rem+5rem)]\": props.right,\n                \"left-2 bottom-[calc(2rem+2rem)]\": props.left\n            }}\n        >\n            {props.children}\n        </nav>\n    );\n}\n\nexport function Fab(props: { onSearch: () => void; onScan: () => void }) {\n    const [open, setOpen] = createSignal(false);\n    const navigate = useNavigate();\n    const i18n = useI18n();\n\n    return (\n        <>\n            <Show when={open()}>\n                <FabMenu setOpen={setOpen} right>\n                    <ul class=\"flex flex-col divide-y divide-m-grey-400/25\">\n                        <li>\n                            <FabMenuItem\n                                onClick={() => {\n                                    props.onSearch();\n                                    setOpen(false);\n                                }}\n                            >\n                                <ArrowUpRight />\n                                {i18n.t(\"common.send\")}\n                            </FabMenuItem>\n                        </li>\n                        <li>\n                            <FabMenuItem onClick={() => navigate(\"/receive\")}>\n                                <ArrowDownLeft />\n                                {i18n.t(\"common.receive\")}\n                            </FabMenuItem>\n                        </li>\n\n                        <li>\n                            <FabMenuItem\n                                onClick={() => {\n                                    setOpen(false);\n                                    props.onScan();\n                                }}\n                            >\n                                <Scan />\n                                {i18n.t(\"common.scan\")}\n                            </FabMenuItem>\n                        </li>\n                    </ul>\n                </FabMenu>\n            </Show>\n            <div class=\"fixed bottom-8 right-8\">\n                <button id=\"fab\" onClick={() => setOpen(!open())}>\n                    <Circle size=\"large\" color=\"red\">\n                        <Plus class=\"h-8 w-8\" />\n                    </Circle>\n                </button>\n            </div>\n        </>\n    );\n}\n\nexport function MiniFab(props: {\n    onSend: () => void;\n    onRequest: () => void;\n    onScan: () => void;\n    sendDisabled?: boolean | undefined;\n}) {\n    const [open, setOpen] = createSignal(false);\n    const i18n = useI18n();\n\n    return (\n        <>\n            <Show when={open()}>\n                <FabMenu setOpen={setOpen} left>\n                    <ul class=\"flex flex-col divide-y divide-m-grey-400/25\">\n                        <li>\n                            <FabMenuItem\n                                disabled={props.sendDisabled || false}\n                                onClick={() => {\n                                    props.onSend();\n                                    setOpen(false);\n                                }}\n                            >\n                                <ArrowUpRight />\n                                {i18n.t(\"common.send\")}\n                            </FabMenuItem>\n                        </li>\n                        <li>\n                            <FabMenuItem\n                                onClick={() => {\n                                    props.onRequest();\n                                    setOpen(false);\n                                }}\n                            >\n                                <ArrowDownLeft />\n                                {i18n.t(\"common.request\")}\n                            </FabMenuItem>\n                        </li>\n\n                        <li>\n                            <FabMenuItem\n                                onClick={() => {\n                                    props.onScan();\n                                    setOpen(false);\n                                }}\n                            >\n                                <Scan />\n                                {i18n.t(\"common.scan\")}\n                            </FabMenuItem>\n                        </li>\n                    </ul>\n                </FabMenu>\n            </Show>\n            <button id=\"fab\" onClick={() => setOpen(true)}>\n                <Plus class=\"h-8 w-8 text-m-red\" />\n            </button>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/Failure.tsx",
    "content": "import { A } from \"@solidjs/router\";\nimport { Match, Switch } from \"solid-js\";\n\nimport { InfoBox, MegaClock, MegaEx } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nexport function Failure(props: { reason: string }) {\n    const i18n = useI18n();\n\n    return (\n        <Switch>\n            <Match when={props.reason === \"Payment timed out.\"}>\n                <MegaClock />\n                <h1 class=\"mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl\">\n                    {i18n.t(\"send.payment_pending\")}\n                </h1>\n                <InfoBox accent=\"white\">\n                    {i18n.t(\"send.payment_pending_description\")}\n                </InfoBox>\n            </Match>\n            <Match\n                when={props.reason === \"Channel reserve amount is too high.\"}\n            >\n                <MegaEx />\n                <h1 class=\"mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl\">\n                    {i18n.t(\"send.error_channel_reserves\")}\n                </h1>\n                <InfoBox accent=\"white\">\n                    {i18n.t(\"send.error_channel_reserves_explained\")}{\" \"}\n                    <A href=\"/settings/channels\">{i18n.t(\"common.why\")}</A>\n                </InfoBox>\n            </Match>\n            <Match when={true}>\n                <MegaEx />\n                <h1 class=\"mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl\">\n                    {props.reason}\n                </h1>\n            </Match>\n        </Switch>\n    );\n}\n"
  },
  {
    "path": "src/components/FederationInviteShower.tsx",
    "content": "import { Copy, QrCode } from \"lucide-solid\";\nimport { createSignal, Show } from \"solid-js\";\nimport { QRCodeSVG } from \"solid-qr-code\";\n\nimport { SimpleDialog } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useCopy } from \"~/utils\";\n\nexport function FederationInviteShower(props: {\n    name?: string;\n    inviteCode: string;\n}) {\n    const i18n = useI18n();\n    const [showQr, setShowQr] = createSignal(false);\n\n    const [copy, copied] = useCopy({ copiedTimeout: 1000 });\n\n    return (\n        <>\n            <div class=\"flex w-full justify-center gap-8\">\n                <button onClick={() => setShowQr(true)}>\n                    <QrCode class=\"inline-block h-4 w-4\" />\n                </button>\n                <button\n                    class=\"p-1\"\n                    classList={{\n                        \"bg-m-red rounded\": copied()\n                    }}\n                    onClick={() => copy(props.inviteCode)}\n                >\n                    <Copy class=\"inline-block h-4 w-4\" />\n                </button>\n            </div>{\" \"}\n            <SimpleDialog\n                open={showQr()}\n                setOpen={(open) => {\n                    setShowQr(open);\n                }}\n                title={i18n.t(\"settings.manage_federations.join_me\")}\n            >\n                <Show when={props.name}>\n                    <p class=\"break-all text-center font-system-mono text-base \">\n                        {props.name}\n                    </p>\n                </Show>\n                <div class=\"w-[15rem] self-center rounded bg-white p-[1rem]\">\n                    <QRCodeSVG\n                        value={props.inviteCode || \"\"}\n                        class=\"h-full max-h-[500px] w-full\"\n                    />\n                </div>\n            </SimpleDialog>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/FederationPopup.tsx",
    "content": "import { useNavigate } from \"@solidjs/router\";\nimport { Users } from \"lucide-solid\";\nimport { createSignal } from \"solid-js\";\n\nimport { ButtonCard, NiceP, SimpleDialog } from \"~/components/layout\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function FederationPopup() {\n    const [state, actions, _sw] = useMegaStore();\n    const [\n        showFederationExpirationWarning,\n        setShowFederationExpirationWarning\n    ] = createSignal(!state.expiration_warning_seen);\n\n    const i18n = useI18n();\n    const navigate = useNavigate();\n\n    return (\n        <SimpleDialog\n            title={`${i18n.t(\"activity.federation_message\")}: ${state.expiration_warning?.federationName}`}\n            open={showFederationExpirationWarning()}\n            setOpen={(open: boolean) => {\n                if (!open) {\n                    setShowFederationExpirationWarning(false);\n                    actions.clearExpirationWarning();\n                }\n            }}\n        >\n            <NiceP>{state.expiration_warning?.expiresMessage}</NiceP>\n            <ButtonCard\n                onClick={() => {\n                    actions.clearExpirationWarning();\n                    setShowFederationExpirationWarning(false);\n                    navigate(\"/settings/federations\");\n                }}\n            >\n                <div class=\"flex items-center gap-2\">\n                    <Users class=\"inline-block text-m-red\" />\n                    <NiceP>{i18n.t(\"profile.manage_federation\")}</NiceP>\n                </div>\n            </ButtonCard>\n        </SimpleDialog>\n    );\n}\n"
  },
  {
    "path": "src/components/Fee.tsx",
    "content": "import { AmountFiat, AmountSats, FeesModal } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nexport function Fee(props: { amountSats?: bigint | number }) {\n    const i18n = useI18n();\n\n    return (\n        <div class=\"flex gap-3\">\n            <p class=\"text-sm text-m-grey-400\">{i18n.t(\"common.fee\")}</p>\n            <div class=\"flex gap-1\">\n                <div class=\"flex flex-col gap-1\">\n                    <div class=\"text-right text-sm\">\n                        <AmountSats\n                            amountSats={props.amountSats}\n                            denominationSize=\"sm\"\n                        />\n                    </div>\n                    <div class=\"text-xs text-white/70\">\n                        <AmountFiat amountSats={props.amountSats} />\n                    </div>\n                </div>\n                <div>\n                    <FeesModal />\n                </div>\n            </div>\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/FeeDisplay.tsx",
    "content": "import { createAsync } from \"@solidjs/router\";\nimport { createMemo, ParentComponent, Show, Suspense } from \"solid-js\";\n\nimport { VStack } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { satsToFormattedFiat } from \"~/utils\";\n\nconst AmountKeyValue: ParentComponent<{ key: string; gray?: boolean }> = (\n    props\n) => {\n    return (\n        <div\n            class=\"flex items-center justify-between\"\n            classList={{ \"text-m-grey-350\": props.gray }}\n        >\n            <div class=\"font-semibold uppercase\">{props.key}</div>\n            <div class=\"font-light\">{props.children}</div>\n        </div>\n    );\n};\n\nfunction USDShower(props: { amountSats: string; fee?: string }) {\n    const [state, _actions, sw] = useMegaStore();\n    const amountInFiat = createAsync(async () => {\n        const formattedFiat = await satsToFormattedFiat(\n            state.price,\n            add(props.amountSats, props.fee),\n            state.fiat,\n            sw\n        );\n        return (state.fiat.value === \"BTC\" ? \"\" : \"~\") + formattedFiat;\n    });\n\n    return (\n        <Show when={!(props.amountSats === \"0\")}>\n            <AmountKeyValue gray key=\"\">\n                <div class=\"self-end whitespace-nowrap\">\n                    <Suspense>{`${amountInFiat()} `}</Suspense>\n                    <span class=\"text-sm\">{state.fiat.value}</span>\n                </div>\n            </AmountKeyValue>\n        </Show>\n    );\n}\n\nconst InlineAmount: ParentComponent<{\n    amount: string;\n    sign?: string;\n}> = (props) => {\n    const i18n = useI18n();\n    const prettyPrint = createMemo(() => {\n        const parsed = Number(props.amount);\n        if (isNaN(parsed)) {\n            return props.amount;\n        } else {\n            return parsed.toLocaleString(navigator.languages[0]);\n        }\n    });\n\n    return (\n        <div class=\"inline-block text-lg\">\n            {props.sign ? `${props.sign} ` : \"\"}\n            {prettyPrint()} <span class=\"text-sm\">{i18n.t(\"common.sats\")}</span>\n        </div>\n    );\n};\n\nfunction add(a: string, b?: string) {\n    return Number(a || 0) + Number(b || 0);\n}\n\nexport function FeeDisplay(props: {\n    amountSats: string;\n    fee: string;\n    maxAmountSats?: bigint;\n}) {\n    const i18n = useI18n();\n    // Normally we want to add the fee to the amount, but for max amount we just show the max\n    const totalOrTotalLessFee = () => {\n        if (\n            props.fee &&\n            props.maxAmountSats &&\n            props.amountSats === props.maxAmountSats?.toString()\n        ) {\n            return props.maxAmountSats.toLocaleString();\n        } else {\n            return add(props.amountSats, props.fee).toString();\n        }\n    };\n    return (\n        <div class=\"w-[20rem] self-center\">\n            <VStack>\n                <div class=\"flex flex-col gap-1\">\n                    <AmountKeyValue gray key={i18n.t(\"receive.fee\")}>\n                        <InlineAmount amount={props.fee || \"0\"} />\n                    </AmountKeyValue>\n                </div>\n                <hr class=\"border-white/20\" />\n                <div class=\"flex flex-col gap-1\">\n                    <AmountKeyValue key={i18n.t(\"receive.total\")}>\n                        <InlineAmount amount={totalOrTotalLessFee()} />\n                    </AmountKeyValue>\n                    <USDShower amountSats={props.amountSats} fee={props.fee} />\n                </div>\n            </VStack>\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/GenericItem.tsx",
    "content": "import { Check, Clock4, EyeOff, Globe, X, Zap } from \"lucide-solid\";\nimport { JSX, Match, Show, Switch } from \"solid-js\";\n\nimport { LabelCircle, LoadingSpinner } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nexport function GenericItem(props: {\n    primaryAvatarUrl?: string;\n    icon?: JSX.Element;\n    secondaryAvatarUrl?: string;\n    primaryName: string;\n    secondaryName?: string;\n    verb?: string;\n    amount?: bigint;\n    date?: string;\n    due?: string;\n    message?: string;\n    accent?: \"green\";\n    visibility?: \"public\" | \"private\";\n    showFiat?: boolean;\n    genericAvatar?: boolean;\n    forceSecondary?: boolean;\n    link?: string;\n    primaryOnClick?: () => void;\n    amountOnClick?: () => void;\n    secondaryOnClick?: () => void;\n    approveAction?: () => void;\n    rejectAction?: () => void;\n    shouldSpinny?: boolean;\n}) {\n    const i18n = useI18n();\n\n    return (\n        <div\n            class=\"grid w-full py-3 first-of-type:pt-0\"\n            classList={{\n                \"grid-cols-[auto_1fr_auto]\": true,\n                \"opacity-50\": props.shouldSpinny\n            }}\n        >\n            <div class=\"self-center\">\n                <Switch>\n                    <Match when={props.icon}>\n                        <button\n                            class=\"flex h-[3rem] w-[3rem] items-center justify-center\"\n                            onClick={() =>\n                                props.primaryOnClick && props.primaryOnClick()\n                            }\n                        >\n                            {props.icon}\n                        </button>\n                    </Match>\n                    <Match when={true}>\n                        <LabelCircle\n                            label={false}\n                            name={props.primaryName}\n                            contact\n                            image_url={props.primaryAvatarUrl}\n                            generic={props.genericAvatar}\n                            onClick={props.primaryOnClick}\n                        />\n                    </Match>\n                </Switch>\n            </div>\n            <div class=\"flex flex-col items-start justify-center gap-1 self-center px-2\">\n                {/* TITLE TEXT */}\n                <Show when={props.primaryName && props.verb}>\n                    <h2 class=\"text-sm\">\n                        <strong\n                            classList={{\n                                \"text-m-grey-400\": props.genericAvatar\n                            }}\n                        >\n                            {props.primaryName}\n                        </strong>\n                        <span class=\"font-light\">{` ${props.verb} `}</span>\n                        <Show when={props.secondaryName}>\n                            <strong>{props.secondaryName}</strong>\n                        </Show>\n                    </h2>\n                </Show>\n                <div class=\"flex flex-wrap gap-1\">\n                    {/* AMOUNT */}\n                    <Show when={props.amount}>\n                        <button\n                            onClick={() =>\n                                props.amountOnClick && props.amountOnClick()\n                            }\n                            class=\"flex items-center gap-1 rounded-full px-2 py-1 text-xs font-semibold text-white\"\n                            classList={{\n                                \"bg-m-grey-800\": !props.accent,\n                                \"bg-m-green/40 \": props.accent === \"green\"\n                            }}\n                        >\n                            {/* <img src={bolt} width={8} height={8} /> */}\n                            <Zap class=\"w-3\" fill=\"currentColor\" />\n                            {`${props.amount!.toLocaleString()} sats`}\n                        </button>\n                    </Show>\n                    {/* FIAT AMOUNT */}\n                    <Show when={props.showFiat}>\n                        <div class=\"flex items-center gap-1 rounded-full py-1 text-xs font-semibold text-m-grey-400\">\n                            {`~$42.00 USD`}\n                        </div>\n                    </Show>\n                    {/* OPTIONAL MESSAGE */}\n                    <Show when={props.message}>\n                        <div class=\"font-regular line-clamp-1 min-w-0 break-all rounded-full bg-m-grey-800 px-2 py-1 text-xs leading-6\">\n                            {props.message}\n                        </div>\n                    </Show>\n                    {/* DUE */}\n                    <Show when={props.due}>\n                        <div class=\"flex w-full items-center gap-1 text-m-grey-400\">\n                            <Clock4 class=\"w-3\" />\n                            <span class=\"text-xs text-m-grey-400\">\n                                {i18n.t(\"common.expires\", {\n                                    time: props.due,\n                                    interpolation: { escapeValue: false }\n                                })}\n                            </span>\n                        </div>\n                    </Show>\n                </div>\n                {/* DATE WITH SECOND AVATAR */}\n                <Show when={props.date}>\n                    <div class=\"flex items-center gap-1 text-m-grey-400\">\n                        <Show when={props.visibility === \"public\"}>\n                            {/* <img src={globe} width={12} height={12} /> */}\n                            <Globe class=\"w-3\" />\n                        </Show>\n                        <Show when={props.visibility === \"private\"}>\n                            <EyeOff class=\"w-3\" />\n                            {/* <img src={privateEye} width={12} height={12} /> */}\n                        </Show>\n                        <Show when={props.link && props.date}>\n                            <a\n                                href={props.link}\n                                class=\"text-xs text-m-grey-400\"\n                                target=\"_blank\"\n                                rel=\"noopener noreferrer\"\n                            >\n                                {props.date}\n                            </a>\n                        </Show>\n                        <Show when={!props.link && props.date}>\n                            <span class=\"text-xs text-m-grey-400\">\n                                {props.date}\n                            </span>\n                        </Show>\n                    </div>\n                </Show>\n            </div>\n            <Show when={props.secondaryAvatarUrl || props.forceSecondary}>\n                <div class=\"self-center\">\n                    <LabelCircle\n                        label={false}\n                        name={props.secondaryName}\n                        contact\n                        image_url={props.secondaryAvatarUrl}\n                        onClick={props.secondaryOnClick}\n                    />\n                </div>\n            </Show>\n            <Show when={!props.secondaryAvatarUrl && !props.forceSecondary}>\n                <div class=\"self-center\">\n                    {/* ACTIONS */}\n                    <Show\n                        when={\n                            props.approveAction &&\n                            props.rejectAction &&\n                            !props.shouldSpinny\n                        }\n                    >\n                        <div class=\"flex gap-4\">\n                            <button\n                                class=\"flex h-10 w-10 items-center justify-center rounded bg-m-grey-800 p-1 text-m-green active:-mb-[1px] active:mt-[1px]\"\n                                onClick={() =>\n                                    props.approveAction && props.approveAction()\n                                }\n                            >\n                                <Check />\n                            </button>\n                            <button\n                                class=\"flex h-10 w-10 items-center justify-center rounded bg-m-grey-800 p-1 text-m-red active:-mb-[1px] active:mt-[1px]\"\n                                onClick={() =>\n                                    props.rejectAction && props.rejectAction()\n                                }\n                            >\n                                <X />\n                            </button>\n                        </div>\n                    </Show>\n                    <Show when={props.shouldSpinny}>\n                        <LoadingSpinner wide small />\n                    </Show>\n                </div>\n            </Show>\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/HomeBalance.tsx",
    "content": "import { createMemo, Match, Suspense, Switch } from \"solid-js\";\n\nimport { AmountFiat, AmountSats, LoadingShimmer } from \"~/components\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function HomeBalance() {\n    const [state, actions] = useMegaStore();\n\n    const combinedBalance = createMemo(\n        () =>\n            (state.balance?.federation || 0n) +\n            (state.balance?.lightning || 0n) +\n            (state.balance?.confirmed || 0n) +\n            (state.balance?.unconfirmed || 0n)\n    );\n\n    // TODO: do some sort of status indicator\n    // const fullyReady = () => state.load_stage === \"done\" && state.price !== 0;\n\n    return (\n        <button\n            onClick={actions.cycleBalanceView}\n            class=\"flex h-12 items-center justify-center rounded-lg border-b border-t border-b-white/10 border-t-white/40 bg-black px-4 py-2\"\n        >\n            <h1 class=\"flex w-full justify-center whitespace-nowrap text-2xl font-light text-white\">\n                <Switch>\n                    <Match when={state.load_stage !== \"done\"}>\n                        <LoadingShimmer small />\n                    </Match>\n                    <Match when={state.balanceView === \"sats\"}>\n                        <AmountSats\n                            amountSats={combinedBalance()}\n                            denominationSize=\"lg\"\n                        />\n                    </Match>\n                    <Match when={state.balanceView === \"fiat\"}>\n                        <Suspense>\n                            <AmountFiat\n                                amountSats={combinedBalance()}\n                                denominationSize=\"lg\"\n                            />\n                        </Suspense>\n                    </Match>\n                    <Match when={state.balanceView === \"hidden\"}>\n                        <div class=\"flex items-center gap-2\">\n                            <span>*****</span>\n                        </div>\n                    </Match>\n                </Switch>\n            </h1>\n        </button>\n    );\n}\n"
  },
  {
    "path": "src/components/HomePrompt.tsx",
    "content": "import { useSearchParams } from \"@solidjs/router\";\nimport { createEffect, createSignal, onMount, Show } from \"solid-js\";\n\nimport {\n    Button,\n    InfoBox,\n    NiceP,\n    showToast,\n    SimpleDialog,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { setSettings } from \"~/logic/mutinyWalletSetup\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { bech32, bech32WordsToUrl, eify } from \"~/utils\";\n\nconst ImageWithFallback = (props: { src: string; alt: string }) => {\n    const [hasError, setHasError] = createSignal(false);\n\n    const handleError = () => {\n        setHasError(true);\n    };\n\n    return (\n        <img\n            src={props.src}\n            alt={props.alt}\n            onError={handleError}\n            class=\"h-4 w-4 rounded-sm\"\n            classList={{\n                hidden: hasError()\n            }}\n        />\n    );\n};\n\nexport function HomePrompt() {\n    const [_state, _actions, sw] = useMegaStore();\n    const i18n = useI18n();\n\n    const [params, setParams] = useSearchParams();\n\n    // When we have a nice result we can head over to the send screen\n    createEffect(() => {\n        if (params.lnurlauth) {\n            const lnurlauth = params.lnurlauth;\n            setParams({ lnurlauth: undefined });\n            setLnurlauthResult(lnurlauth);\n            try {\n                const decodedLnurl = bech32.decode(lnurlauth, 1023);\n                const url = bech32WordsToUrl(decodedLnurl.words);\n                const domain = new URL(url).hostname;\n                setLnurlDomain(domain);\n            } catch (e) {\n                // We care about this error the domain is just to make it pretty\n                console.error(e);\n            }\n            return;\n        }\n    });\n\n    // LSPS stuff\n    onMount(async () => {\n        if (params.lsps) {\n            const values = {\n                lsp: \"\",\n                lsps_connection_string: params.lsps,\n                lsps_token: params.token\n            };\n            try {\n                await sw.change_lsp(\n                    values.lsp ? values.lsp : undefined,\n                    values.lsps_connection_string\n                        ? values.lsps_connection_string\n                        : undefined,\n                    values.lsps_token ? values.lsps_token : undefined\n                );\n                await setSettings(values);\n                setParams({ lsps: undefined, token: undefined });\n                showToast({\n                    title: \"Success\",\n                    description: \"LSPS settings changed\"\n                });\n            } catch (e) {\n                console.error(\"Error changing lsp:\", e);\n                showToast(eify(e));\n            }\n        }\n    });\n\n    // Lnurl Auth stuff\n    const [lnurlauthResult, setLnurlauthResult] = createSignal<string>();\n    const [authLoading, setAuthLoading] = createSignal<boolean>(false);\n    const [isAuthenticated, setIsAuthenticated] = createSignal<boolean>(false);\n    const [authError, setAuthError] = createSignal<Error | undefined>(\n        undefined\n    );\n    const [lnurlDomain, setLnurlDomain] = createSignal<string>(\"\");\n\n    async function handleLnurlAuth() {\n        setAuthLoading(true);\n        try {\n            await sw.lnurl_auth(lnurlauthResult()!);\n            setIsAuthenticated(true);\n        } catch (e) {\n            console.error(e);\n            setAuthError(eify(e));\n        } finally {\n            setAuthLoading(false);\n        }\n    }\n\n    return (\n        <>\n            <SimpleDialog\n                title={i18n.t(\"modals.lnurl_auth.auth_request\")}\n                open={!!lnurlauthResult()}\n                setOpen={(open) => {\n                    if (!open) setLnurlauthResult(undefined);\n                }}\n            >\n                <Show when={lnurlDomain()}>\n                    <div class=\"flex w-full items-center justify-center gap-2 p-2\">\n                        <ImageWithFallback\n                            src={`https://${lnurlDomain()}/favicon.ico`}\n                            alt=\"Favicon\"\n                        />\n                        <pre class=\"text-center\">{lnurlDomain()}</pre>\n                    </div>\n                </Show>\n                <Show when={!isAuthenticated()}>\n                    <VStack>\n                        <Button\n                            loading={authLoading()}\n                            intent=\"blue\"\n                            onClick={handleLnurlAuth}\n                        >\n                            {i18n.t(\"modals.lnurl_auth.login\")}\n                        </Button>\n                        <Button\n                            intent=\"red\"\n                            onClick={() => setLnurlauthResult(undefined)}\n                        >\n                            {i18n.t(\"modals.lnurl_auth.decline\")}\n                        </Button>\n                    </VStack>\n                </Show>\n                <Show when={isAuthenticated()}>\n                    <div class=\"flex flex-col items-center\">\n                        <div class=\"rounded bg-m-grey-950 px-2 py-1 text-center\">\n                            <NiceP>\n                                {i18n.t(\"modals.lnurl_auth.authenticated\")}\n                            </NiceP>\n                        </div>\n                    </div>\n                </Show>\n                <Show when={authError()}>\n                    <InfoBox accent=\"red\">\n                        {i18n.t(\"modals.lnurl_auth.error\")}\n                    </InfoBox>\n                </Show>\n            </SimpleDialog>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/HomeSubnav.tsx",
    "content": "import { useSearchParams } from \"@solidjs/router\";\nimport {\n    createEffect,\n    createResource,\n    createSignal,\n    Show,\n    Suspense\n} from \"solid-js\";\n\nimport {\n    CombinedActivity,\n    LoadingShimmer,\n    NostrActivity,\n    PendingNwc,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function HomeSubnav() {\n    const [state, _actions, sw] = useMegaStore();\n\n    const i18n = useI18n();\n\n    const [params] = useSearchParams<{\n        tab: \"me\" | \"everybody\" | \"requests\";\n    }>();\n\n    const [activeView, setActiveView] = createSignal<\n        \"me\" | \"everybody\" | \"requests\"\n    >(params.tab || \"me\");\n\n    const [pending, { refetch }] = createResource(async () => {\n        try {\n            const pending = await sw.get_pending_nwc_invoices();\n            return pending?.length || 0;\n        } catch (e) {\n            console.error(e);\n            return 0;\n        }\n    });\n\n    createEffect(() => {\n        if (state.is_syncing) {\n            refetch();\n        }\n    });\n\n    return (\n        <>\n            <div class=\"flex gap-2\">\n                <button\n                    class=\"rounded px-2 py-1 text-sm\"\n                    classList={{\n                        \"bg-m-red\": activeView() === \"me\",\n                        \"bg-m-grey-800 text-m-grey-400\": activeView() !== \"me\"\n                    }}\n                    onClick={() => setActiveView(\"me\")}\n                >\n                    {i18n.t(\"home.subnav.just_me\")}\n                </button>\n                <button\n                    class=\"rounded px-2 py-1 text-sm\"\n                    classList={{\n                        \"bg-m-red\": activeView() === \"everybody\",\n                        \"bg-m-grey-800 text-m-grey-400\":\n                            activeView() !== \"everybody\"\n                    }}\n                    onClick={() => setActiveView(\"everybody\")}\n                >\n                    {i18n.t(\"home.subnav.friends\")}\n                </button>\n\n                <button\n                    class=\"flex items-center gap-1 rounded px-2 py-1 text-sm\"\n                    classList={{\n                        \"bg-m-red\": activeView() === \"requests\",\n                        \"bg-m-grey-800 text-m-grey-400\":\n                            activeView() !== \"requests\"\n                    }}\n                    onClick={() => setActiveView(\"requests\")}\n                >\n                    <span>{i18n.t(\"home.subnav.requests\")}</span>\n                    <Suspense fallback={<></>}>\n                        <Show when={pending.latest && pending.latest > 0}>\n                            <span\n                                class=\"inline-flex h-5 w-5 items-center justify-center rounded-full bg-white/20 text-xs\"\n                                classList={{\n                                    \"text-white\": !!((pending.latest || 0) > 0)\n                                }}\n                            >\n                                {pending.latest}\n                            </span>\n                        </Show>\n                    </Suspense>\n                </button>\n            </div>\n\n            <Show when={activeView() === \"me\"}>\n                <VStack>\n                    <Suspense>\n                        <Show\n                            when={!state.wallet_loading}\n                            fallback={<LoadingShimmer />}\n                        >\n                            <CombinedActivity />\n                        </Show>\n                    </Suspense>\n                </VStack>\n            </Show>\n            <Show when={activeView() === \"everybody\"}>\n                <Suspense fallback={<LoadingShimmer />}>\n                    <NostrActivity />\n                </Suspense>\n            </Show>\n            <Show when={activeView() === \"requests\"}>\n                <Suspense fallback={<LoadingShimmer />}>\n                    <Show when={!state.wallet_loading && !state.safe_mode}>\n                        <PendingNwc />\n                    </Show>\n                </Suspense>\n            </Show>\n            {/* spacer just so we can always scroll above the fab */}\n            <div class=\"h-[4rem]\" />\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/I18nProvider.tsx",
    "content": "import i18next from \"i18next\";\nimport { createResource, ParentComponent, Show } from \"solid-js\";\n\nimport i18nConfig from \"~/i18n/config\";\nimport { I18nContext } from \"~/i18n/context\";\n\nexport const I18nProvider: ParentComponent = (props) => {\n    const [i18nConfigured] = createResource(async () => {\n        console.log(\"about to get the config\");\n        try {\n            await i18nConfig;\n        } catch (e) {\n            console.error(\"Config error\", e);\n        }\n        return true;\n    });\n\n    return (\n        <Show when={i18nConfigured()}>\n            <I18nContext.Provider value={i18next}>\n                {props.children}\n            </I18nContext.Provider>\n        </Show>\n    );\n};\n"
  },
  {
    "path": "src/components/IOSbanner.tsx",
    "content": "import { X } from \"lucide-solid\";\nimport { Show } from \"solid-js\";\n\nimport { ButtonLink } from \"~/components\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function IOSbanner() {\n    const [state, actions] = useMegaStore();\n\n    function closeBanner() {\n        actions.setTestFlightPromptDismissed();\n    }\n\n    return (\n        <>\n            <Show when={true}>\n                <div class=\"grid grid-cols-[auto_minmax(0,_1fr)_auto] gap-4 rounded-xl bg-white p-4\">\n                    <div class=\"self-center\">\n                        <span class=\"h-8 w-8 text-3xl text-black\"></span>\n                    </div>\n                    <div class=\"flex flex-col justify-center text-black\">\n                        <header class={`text-sm font-semibold`}>\n                            iOS TESTFLIGHT FOR MUTINY\n                            <span class=\"text-m-red\">+</span> USERS\n                        </header>\n                    </div>\n                    <div class=\"flex items-center gap-4\">\n                        <ButtonLink\n                            intent=\"green\"\n                            layout=\"xs\"\n                            href={\n                                state.mutiny_plus\n                                    ? \"https://testflight.apple.com/join/9g23f0Mc\"\n                                    : \"/settings/plus\"\n                            }\n                        >\n                            Join\n                        </ButtonLink>\n                        <button\n                            onClick={closeBanner}\n                            class=\"self-center justify-self-center rounded-lg hover:bg-white/10 active:bg-m-blue\"\n                        >\n                            <X class=\"h-8 w-8 text-black\" />\n                        </button>{\" \"}\n                    </div>\n                </div>\n            </Show>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/ImportExport.tsx",
    "content": "import { createFileUploader } from \"@solid-primitives/upload\";\nimport { createSignal, Show } from \"solid-js\";\n\nimport {\n    Button,\n    ConfirmDialog,\n    InfoBox,\n    InnerCard,\n    NiceP,\n    showToast,\n    SimpleDialog,\n    TextField,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { downloadTextFile, eify } from \"~/utils\";\n\nexport function ImportExport(props: { emergency?: boolean }) {\n    const i18n = useI18n();\n    const [state, _actions, sw] = useMegaStore();\n\n    const [error, setError] = createSignal<Error>();\n    const [exportDecrypt, setExportDecrypt] = createSignal(false);\n    const [password, setPassword] = createSignal(\"\");\n\n    async function handleSave() {\n        try {\n            setError(undefined);\n            const json = await sw.export_json();\n            await downloadTextFile(json || \"\", \"mutiny-state.json\");\n        } catch (e) {\n            console.error(e);\n            const err = eify(e);\n            if (err.message === \"Incorrect password entered.\") {\n                setExportDecrypt(true);\n            } else {\n                setError(err);\n            }\n        }\n    }\n\n    async function savePassword(e: Event) {\n        e.preventDefault();\n        try {\n            setError(undefined);\n            if (!password()) {\n                throw new Error(\n                    i18n.t(\n                        \"settings.emergency_kit.import_export.error_password\"\n                    )\n                );\n            }\n            const json = await sw.export_json(password());\n            await downloadTextFile(json || \"\", \"mutiny-state.json\");\n        } catch (e) {\n            console.error(e);\n            setError(eify(e));\n        } finally {\n            setExportDecrypt(false);\n            setPassword(\"\");\n        }\n    }\n\n    function noop() {\n        // noop\n    }\n\n    const { files, selectFiles } = createFileUploader();\n\n    async function importJson() {\n        setConfirmLoading(true);\n        const fileReader = new FileReader();\n\n        try {\n            setError(undefined);\n            const file: File = files()[0].file;\n\n            const text = await new Promise<string | null>((resolve, reject) => {\n                fileReader.onload = (e) => {\n                    const result = e.target?.result?.toString();\n                    if (result) {\n                        resolve(result);\n                    } else {\n                        reject(\n                            new Error(\n                                i18n.t(\n                                    \"settings.emergency_kit.import_export.error_no_text\"\n                                )\n                            )\n                        );\n                    }\n                };\n                fileReader.onerror = (_e) =>\n                    reject(\n                        new Error(\n                            i18n.t(\n                                \"settings.emergency_kit.import_export.error_read_file\"\n                            )\n                        )\n                    );\n                fileReader.readAsText(file, \"UTF-8\");\n            });\n\n            if (state.load_stage === \"done\" && !props.emergency) {\n                console.log(\"Mutiny wallet loaded, stopping\");\n                try {\n                    await sw.stop();\n                } catch (e) {\n                    console.error(e);\n                    setError(eify(e));\n                }\n            } else {\n                await sw.initializeWasm();\n            }\n\n            // This should throw if there's a parse error, so we won't end up clearing\n            if (text) {\n                JSON.parse(text);\n                await sw.import_json(text);\n            }\n\n            setTimeout(() => {\n                window.location.href = \"/\";\n            }, 1000);\n        } catch (e) {\n            showToast(eify(e));\n        } finally {\n            setConfirmOpen(false);\n            setConfirmLoading(false);\n        }\n    }\n\n    async function uploadFile() {\n        selectFiles(async (files) => {\n            if (files.length) {\n                setConfirmOpen(true);\n                return;\n            }\n        });\n    }\n\n    const [confirmOpen, setConfirmOpen] = createSignal(false);\n    const [confirmLoading, setConfirmLoading] = createSignal(false);\n\n    return (\n        <>\n            <InnerCard\n                title={i18n.t(\"settings.emergency_kit.import_export.title\")}\n            >\n                <NiceP>\n                    {i18n.t(\"settings.emergency_kit.import_export.tip\")}\n                </NiceP>\n                <NiceP>\n                    <strong>\n                        {i18n.t(\n                            \"settings.emergency_kit.import_export.caveat_header\"\n                        )}\n                    </strong>{\" \"}\n                    {i18n.t(\"settings.emergency_kit.import_export.caveat\")}\n                </NiceP>\n                <div />\n                <Show when={error()}>\n                    <InfoBox accent=\"red\">{error()?.message}</InfoBox>\n                </Show>\n                <VStack>\n                    <Button onClick={handleSave}>\n                        {i18n.t(\n                            \"settings.emergency_kit.import_export.save_state\"\n                        )}\n                    </Button>\n                    <Button onClick={uploadFile}>\n                        {i18n.t(\n                            \"settings.emergency_kit.import_export.import_state\"\n                        )}\n                    </Button>\n                </VStack>\n            </InnerCard>\n            <ConfirmDialog\n                loading={confirmLoading()}\n                open={confirmOpen()}\n                onConfirm={importJson}\n                onCancel={() => setConfirmOpen(false)}\n            >\n                {i18n.t(\"settings.emergency_kit.import_export.confirm_replace\")}{\" \"}\n                {files()[0].name}?\n            </ConfirmDialog>\n            {/* TODO: this is pretty redundant with the DecryptDialog, could make a shared component */}\n            <SimpleDialog\n                title={i18n.t(\n                    \"settings.emergency_kit.import_export.decrypt_export\"\n                )}\n                open={exportDecrypt()}\n                setOpen={() => setExportDecrypt(false)}\n            >\n                <form onSubmit={savePassword}>\n                    <div class=\"flex flex-col gap-4\">\n                        <TextField\n                            name=\"password\"\n                            type=\"password\"\n                            ref={noop}\n                            value={password()}\n                            onInput={(e) => setPassword(e.currentTarget.value)}\n                            error={\"\"}\n                            onBlur={noop}\n                            onChange={noop}\n                        />\n                        <Show when={error()}>\n                            <InfoBox accent=\"red\">{error()?.message}</InfoBox>\n                        </Show>\n                        <Button intent=\"blue\" onClick={savePassword}>\n                            {i18n.t(\n                                \"settings.emergency_kit.import_export.decrypt_wallet\"\n                            )}\n                        </Button>\n                    </div>\n                </form>\n            </SimpleDialog>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/ImportNsecForm.tsx",
    "content": "import { useNavigate } from \"@solidjs/router\";\nimport { SecureStoragePlugin } from \"capacitor-secure-storage-plugin\";\nimport { createSignal, Show } from \"solid-js\";\n\nimport { Button, InfoBox, SimpleInput } from \"~/components\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function ImportNsecForm(props: { setup?: boolean }) {\n    const [_state, _actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n    const [nsec, setNsec] = createSignal(\"\");\n    const [saving, setSaving] = createSignal(false);\n    const [error, setError] = createSignal<string | undefined>();\n\n    async function saveNsec() {\n        setSaving(true);\n        setError(undefined);\n        const trimmedNsec = nsec().trim();\n        try {\n            const npub = await sw.nsec_to_npub(trimmedNsec);\n            if (!npub) {\n                throw new Error(\"Invalid nsec\");\n            }\n            await SecureStoragePlugin.set({ key: \"nsec\", value: trimmedNsec });\n\n            const new_npub = await sw.change_nostr_keys(trimmedNsec, undefined);\n            console.log(\"Changed to new npub: \", new_npub);\n            if (props.setup) {\n                navigate(\"/addfederation\");\n            } else {\n                navigate(\"/\");\n            }\n        } catch (e) {\n            console.error(e);\n            setError(\"Invalid nsec\");\n        }\n        setSaving(false);\n    }\n\n    return (\n        <>\n            <SimpleInput\n                value={nsec()}\n                type=\"password\"\n                onInput={(e) => setNsec(e.currentTarget.value)}\n                placeholder={`Nostr private key (starts with \"nsec\")`}\n            />\n            <Button layout=\"full\" onClick={saveNsec} loading={saving()}>\n                Import\n            </Button>\n            <Show when={error()}>\n                <InfoBox accent=\"red\">{error()}</InfoBox>\n            </Show>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/InfoBox.tsx",
    "content": "import { Info } from \"lucide-solid\";\nimport { ParentComponent } from \"solid-js\";\n\nexport const InfoBox: ParentComponent<{\n    accent: \"red\" | \"blue\" | \"green\" | \"white\";\n}> = (props) => {\n    return (\n        <div\n            class=\"grid grid-cols-[auto_minmax(0,_1fr)] gap-4 rounded-xl border bg-neutral-950/50 px-4 py-2 md:p-4\"\n            classList={{\n                \"border-m-red\": props.accent === \"red\",\n                \"border-m-blue bg-m-blue/10\": props.accent === \"blue\",\n                \"border-m-green\": props.accent === \"green\",\n                \"border-white\": props.accent === \"white\"\n            }}\n        >\n            <div class=\"self-center\">\n                <Info class=\"h-8 w-8\" />\n            </div>\n            <div class=\"flex items-center\">\n                <p class=\"text-sm font-light\">{props.children}</p>\n            </div>\n        </div>\n    );\n};\n"
  },
  {
    "path": "src/components/IntegratedQR.tsx",
    "content": "import { Copy, Link, Share, Zap } from \"lucide-solid\";\nimport { Match, Show, Switch } from \"solid-js\";\nimport { QRCodeSVG } from \"solid-qr-code\";\n\nimport { AmountFiat, AmountSats, TruncateMiddle } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { ReceiveFlavor } from \"~/routes/Receive\";\nimport { useCopy } from \"~/utils\";\n\nfunction KindIndicator(props: { kind: ReceiveFlavor | \"gift\" | \"lnAddress\" }) {\n    const i18n = useI18n();\n    return (\n        <div class=\"flex flex-col items-end text-black\">\n            <Switch>\n                <Match when={props.kind === \"onchain\"}>\n                    <h3 class=\"font-semibold\">\n                        {i18n.t(\"receive.integrated_qr.onchain\")}\n                    </h3>\n                    <Link class=\"h-4 w-4\" />\n                </Match>\n\n                <Match when={props.kind === \"lightning\"}>\n                    <h3 class=\"font-semibold\">\n                        {i18n.t(\"receive.integrated_qr.lightning\")}\n                    </h3>\n                    <Zap class=\"h-4 w-4\" />\n                </Match>\n\n                <Match when={props.kind === \"lnAddress\"}>\n                    <h3 class=\"font-semibold\">\n                        {i18n.t(\"contacts.ln_address\")}\n                    </h3>\n                    <Zap class=\"h-4 w-4\" />\n                </Match>\n\n                <Match when={props.kind === \"gift\"}>\n                    <h3 class=\"font-semibold\">\n                        {i18n.t(\"receive.integrated_qr.gift\")}\n                    </h3>\n                    <Zap class=\"h-4 w-4\" />\n                </Match>\n            </Switch>\n        </div>\n    );\n}\n\nasync function share(receiveString: string) {\n    // If the browser doesn't support share we can just copy the address\n    if (!navigator.share) {\n        console.error(\"Share not supported\");\n    }\n    const shareData: ShareData = {\n        title: \"Mutiny Wallet\",\n        text: receiveString\n    };\n    try {\n        await navigator.share(shareData);\n    } catch (e) {\n        console.error(e);\n    }\n}\n\nexport function IntegratedQr(props: {\n    value: string;\n    copyString?: string;\n    kind: ReceiveFlavor | \"gift\" | \"lnAddress\";\n    amountSats?: string;\n}) {\n    const i18n = useI18n();\n    const [copy, copied] = useCopy({ copiedTimeout: 1000 });\n    return (\n        <div\n            id=\"qr\"\n            class=\"relative flex w-full flex-col items-center rounded-xl bg-white px-4 text-black\"\n            onClick={() => copy(props.copyString ?? props.value)}\n        >\n            <Show when={copied()}>\n                <div class=\"absolute z-50 flex h-full w-full flex-col items-center justify-center rounded-xl bg-neutral-900/60 text-white transition-all\">\n                    <p class=\"text-xl font-bold\">{i18n.t(\"common.copied\")}</p>\n                </div>\n            </Show>\n            <Show when={props.kind !== \"lnAddress\"}>\n                <div\n                    class=\"flex w-full max-w-[256px] items-center py-4\"\n                    classList={{\n                        \"justify-between\": props.kind !== \"onchain\",\n                        \"justify-end\": props.kind === \"onchain\"\n                    }}\n                >\n                    <Show when={props.kind !== \"onchain\"}>\n                        <div class=\"flex flex-col gap-1\">\n                            <AmountSats amountSats={Number(props.amountSats)} />\n                            <div class=\"text-sm \">\n                                <AmountFiat\n                                    amountSats={Number(props.amountSats)}\n                                />\n                            </div>\n                        </div>\n                    </Show>\n                    <KindIndicator kind={props.kind} />\n                </div>\n            </Show>\n            <Show when={props.kind === \"lnAddress\"}>\n                <div class=\"py-4\" />\n            </Show>\n\n            <QRCodeSVG\n                value={props.value}\n                class=\"h-full max-h-[256px] w-full\"\n            />\n            <div\n                class=\"grid w-full max-w-[256px] gap-1 py-4 \"\n                classList={{\n                    \"grid-cols-[2rem_minmax(0,1fr)_2rem]\": !!navigator.share,\n                    \"grid-cols-[minmax(0,1fr)_2rem]\": !navigator.share\n                }}\n            >\n                <Show when={!!navigator.share}>\n                    <button\n                        class=\"justify-self-start\"\n                        onClick={(_) => share(props.value)}\n                    >\n                        <Share />\n                    </button>\n                </Show>\n                <Show when={props.kind !== \"lnAddress\"}>\n                    <div class=\"\">\n                        <TruncateMiddle text={props.value} whiteBg />\n                    </div>\n                    <button\n                        class=\" justify-self-end\"\n                        onClick={() => copy(props.value)}\n                    >\n                        <Copy />\n                    </button>\n                </Show>\n            </div>\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/JsonModal.tsx",
    "content": "import { createMemo, JSX } from \"solid-js\";\n\nimport { CopyButton, SimpleDialog } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nexport function JsonModal(props: {\n    title: string;\n    open: boolean;\n    plaintext?: string;\n    data?: unknown;\n    setOpen: (open: boolean) => void;\n    children?: JSX.Element;\n}) {\n    const i18n = useI18n();\n    const json = createMemo(() =>\n        props.plaintext ? props.plaintext : JSON.stringify(props.data, null, 2)\n    );\n\n    return (\n        <SimpleDialog\n            title={props.title}\n            open={props.open}\n            setOpen={props.setOpen}\n        >\n            <div class=\"max-h-[50vh] overflow-y-scroll rounded-xl bg-white/5 p-4 disable-scrollbars\">\n                <pre class=\"whitespace-pre-wrap break-all\">{json()}</pre>\n            </div>\n            {props.children}\n            <div class=\"self-center\">\n                <CopyButton title={i18n.t(\"common.copy\")} text={json()} />\n            </div>\n        </SimpleDialog>\n    );\n}\n"
  },
  {
    "path": "src/components/KitchenSink.tsx",
    "content": "import { Collapsible, TextField } from \"@kobalte/core\";\nimport { createForm, url } from \"@modular-forms/solid\";\nimport { MutinyChannel, MutinyPeer } from \"@mutinywallet/mutiny-wasm\";\nimport {\n    createResource,\n    createSignal,\n    For,\n    Match,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    Button,\n    ConfirmDialog,\n    ExternalLink,\n    TextField as FTextField,\n    Hr,\n    InnerCard,\n    MiniStringShower,\n    Restart,\n    ResyncOnchain,\n    showToast,\n    SimpleErrorDisplay,\n    ToggleHodl,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport {\n    getSettings,\n    MutinyWalletSettingStrings,\n    Network,\n    setSettings\n} from \"~/logic/mutinyWalletSetup\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify, mempoolTxUrl } from \"~/utils\";\n\n// TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise\ntype RefetchPeersType = (\n    info?: unknown\n) => MutinyPeer[] | Promise<MutinyPeer[] | undefined> | null | undefined;\n\nfunction PeerItem(props: { peer: MutinyPeer; refetchPeers: RefetchPeersType }) {\n    const i18n = useI18n();\n    const [_state, _actions, sw] = useMegaStore();\n\n    const handleDisconnectPeer = async () => {\n        if (props.peer.is_connected) {\n            await sw.disconnect_peer(props.peer.pubkey);\n        } else {\n            await sw.delete_peer(props.peer.pubkey);\n        }\n        await props.refetchPeers();\n    };\n\n    return (\n        <Collapsible.Root>\n            <Collapsible.Trigger class=\"w-full\">\n                <h2 class=\"truncate rounded bg-neutral-200 px-4 py-2 text-start font-mono text-lg text-black\">\n                    {\">\"}{\" \"}\n                    {props.peer.alias ? props.peer.alias : props.peer.pubkey}\n                </h2>\n            </Collapsible.Trigger>\n            <Collapsible.Content>\n                <VStack>\n                    <pre class=\"!select-text overflow-x-auto whitespace-pre-wrap break-all\">\n                        {JSON.stringify(props.peer, null, 2)}\n                    </pre>\n                    <Button\n                        intent=\"red\"\n                        layout=\"xs\"\n                        onClick={handleDisconnectPeer}\n                    >\n                        {i18n.t(\"settings.admin.kitchen_sink.disconnect\")}\n                    </Button>\n                </VStack>\n            </Collapsible.Content>\n        </Collapsible.Root>\n    );\n}\n\nfunction PeersList() {\n    const i18n = useI18n();\n    const [_state, _actions, sw] = useMegaStore();\n\n    const getPeers = async () => {\n        return await sw.list_peers();\n    };\n\n    const [peers, { refetch }] = createResource(getPeers);\n\n    return (\n        <>\n            <InnerCard title={i18n.t(\"settings.admin.kitchen_sink.peers\")}>\n                {/* By wrapping this in a suspense I don't cause the page to jump to the top */}\n                <Suspense>\n                    <VStack>\n                        <For\n                            each={peers.latest}\n                            fallback={\n                                <code>\n                                    {i18n.t(\n                                        \"settings.admin.kitchen_sink.no_peers\"\n                                    )}\n                                </code>\n                            }\n                        >\n                            {(peer) => (\n                                <PeerItem peer={peer} refetchPeers={refetch} />\n                            )}\n                        </For>\n                    </VStack>\n                </Suspense>\n                <Button layout=\"small\" onClick={refetch}>\n                    {i18n.t(\"settings.admin.kitchen_sink.refresh_peers\")}\n                </Button>\n            </InnerCard>\n            <ConnectPeer refetchPeers={refetch} />\n        </>\n    );\n}\n\nfunction ConnectPeer(props: { refetchPeers: RefetchPeersType }) {\n    const i18n = useI18n();\n    const [_state, _actions, sw] = useMegaStore();\n\n    const [value, setValue] = createSignal(\"\");\n\n    const onSubmit = async (e: SubmitEvent) => {\n        e.preventDefault();\n\n        const peerConnectString = value().trim();\n\n        await sw.connect_to_peer(peerConnectString);\n\n        await props.refetchPeers();\n\n        setValue(\"\");\n    };\n\n    return (\n        <InnerCard>\n            <form class=\"flex flex-col gap-4\" onSubmit={onSubmit}>\n                <TextField.Root\n                    value={value()}\n                    onChange={setValue}\n                    class=\"flex flex-col gap-4\"\n                >\n                    <TextField.Label class=\"text-sm font-semibold uppercase\">\n                        {i18n.t(\"settings.admin.kitchen_sink.connect_peer\")}\n                    </TextField.Label>\n                    <TextField.Input\n                        class=\"w-full rounded-lg p-2 text-black\"\n                        placeholder=\"028241...\"\n                    />\n                    <TextField.ErrorMessage class=\"text-red-500\">\n                        {i18n.t(\"settings.admin.kitchen_sink.expect_a_value\")}\n                    </TextField.ErrorMessage>\n                </TextField.Root>\n                <Button layout=\"small\" type=\"submit\">\n                    {i18n.t(\"settings.admin.kitchen_sink.connect\")}\n                </Button>\n            </form>\n        </InnerCard>\n    );\n}\n\ntype RefetchChannelsListType = (\n    info?: unknown\n) => MutinyChannel[] | Promise<MutinyChannel[] | undefined> | null | undefined;\n\ntype PendingChannelAction = \"close\" | \"force_close\" | \"abandon\";\n\nfunction ChannelItem(props: { channel: MutinyChannel; network?: Network }) {\n    const i18n = useI18n();\n    const [_state, _actions, sw] = useMegaStore();\n\n    const [pendingChannelAction, setPendingChannelAction] =\n        createSignal<PendingChannelAction>();\n    const [confirmLoading, setConfirmLoading] = createSignal(false);\n\n    async function confirmChannelAction() {\n        const action = pendingChannelAction();\n        if (!action) return;\n        setConfirmLoading(true);\n        try {\n            await sw.close_channel(\n                props.channel.outpoint as string,\n                action === \"force_close\",\n                action === \"abandon\"\n            );\n        } catch (e) {\n            console.error(e);\n            showToast(eify(e));\n        }\n        setConfirmLoading(false);\n        setPendingChannelAction(undefined);\n    }\n\n    return (\n        <Collapsible.Root>\n            <Collapsible.Trigger class=\"w-full\">\n                <h2 class=\"truncate rounded bg-neutral-200 px-4 py-2 text-start font-mono text-lg text-black\">\n                    {\">\"} {props.channel.peer}\n                </h2>\n            </Collapsible.Trigger>\n            <Collapsible.Content>\n                <VStack>\n                    <pre class=\"!select-text overflow-x-auto whitespace-pre-wrap break-all\">\n                        {JSON.stringify(props.channel, null, 2)}\n                    </pre>\n                    <ExternalLink\n                        href={mempoolTxUrl(\n                            props.channel.outpoint?.split(\":\")[0],\n                            props.network\n                        )}\n                    >\n                        {i18n.t(\"common.view_transaction\")}\n                    </ExternalLink>\n                    <Button\n                        intent=\"red\"\n                        layout=\"xs\"\n                        onClick={() => setPendingChannelAction(\"close\")}\n                    >\n                        {i18n.t(\"settings.admin.kitchen_sink.close_channel\")}\n                    </Button>\n                    <Button\n                        intent=\"red\"\n                        layout=\"xs\"\n                        onClick={() => setPendingChannelAction(\"force_close\")}\n                    >\n                        {i18n.t(\"settings.admin.kitchen_sink.force_close\")}\n                    </Button>\n                    <Button\n                        intent=\"red\"\n                        layout=\"xs\"\n                        onClick={() => setPendingChannelAction(\"abandon\")}\n                    >\n                        {i18n.t(\"settings.admin.kitchen_sink.abandon_channel\")}\n                    </Button>\n                </VStack>\n                <ConfirmDialog\n                    open={!!pendingChannelAction()}\n                    onConfirm={confirmChannelAction}\n                    onCancel={() => setPendingChannelAction(undefined)}\n                    loading={confirmLoading()}\n                >\n                    <Switch>\n                        <Match when={pendingChannelAction() === \"close\"}>\n                            <p>\n                                {i18n.t(\n                                    \"settings.admin.kitchen_sink.confirm_close_channel\"\n                                )}\n                            </p>\n                        </Match>\n                        <Match when={pendingChannelAction() === \"force_close\"}>\n                            <p>\n                                {i18n.t(\n                                    \"settings.admin.kitchen_sink.confirm_force_close\"\n                                )}\n                            </p>\n                        </Match>\n                        <Match when={pendingChannelAction() === \"abandon\"}>\n                            <p>\n                                {i18n.t(\n                                    \"settings.admin.kitchen_sink.confirm_abandon_channel\"\n                                )}\n                            </p>\n                        </Match>\n                    </Switch>\n                </ConfirmDialog>\n            </Collapsible.Content>\n        </Collapsible.Root>\n    );\n}\n\nfunction ChannelsList() {\n    const i18n = useI18n();\n    const [state, _actions, sw] = useMegaStore();\n\n    const getChannels = async () => {\n        return sw.list_channels();\n    };\n\n    const [channels, { refetch }] = createResource(getChannels);\n\n    const network = state.network;\n\n    return (\n        <>\n            <InnerCard title={i18n.t(\"settings.admin.kitchen_sink.channels\")}>\n                {/* By wrapping this in a suspense I don't cause the page to jump to the top */}\n                <Suspense>\n                    <For\n                        each={channels()}\n                        fallback={\n                            <code>\n                                {i18n.t(\n                                    \"settings.admin.kitchen_sink.no_channels\"\n                                )}\n                            </code>\n                        }\n                    >\n                        {(channel) => (\n                            <ChannelItem channel={channel} network={network} />\n                        )}\n                    </For>\n                </Suspense>\n                <Button\n                    type=\"button\"\n                    layout=\"small\"\n                    onClick={(e) => {\n                        e.preventDefault();\n                        refetch();\n                    }}\n                >\n                    {i18n.t(\"settings.admin.kitchen_sink.refresh_channels\")}\n                </Button>\n            </InnerCard>\n            <OpenChannel refetchChannels={refetch} />\n        </>\n    );\n}\n\nfunction OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {\n    const i18n = useI18n();\n    const [state, _actions, sw] = useMegaStore();\n\n    const [creationError, setCreationError] = createSignal<Error>();\n\n    const [amount, setAmount] = createSignal(\"\");\n    const [peerPubkey, setPeerPubkey] = createSignal(\"\");\n\n    const [newChannel, setNewChannel] = createSignal<MutinyChannel>();\n\n    const onSubmit = async (e: SubmitEvent) => {\n        e.preventDefault();\n\n        // TODO: figure out why this doesn't catch the rust error\n        // src/logging.rs:29\n        // ERROR: Could not create a signed transaction to open channel with: The invoice or address is on a different network.\n        try {\n            const pubkey = peerPubkey().trim();\n            const bigAmount = BigInt(amount());\n            console.log(\"pubkey\", pubkey);\n            console.log(\"bigAmount\", bigAmount);\n\n            const new_channel = await sw.open_channel(pubkey, bigAmount);\n            console.log(\"new_channel\", new_channel);\n\n            setNewChannel(new_channel);\n\n            await props.refetchChannels();\n\n            setAmount(\"\");\n            setPeerPubkey(\"\");\n        } catch (e) {\n            setCreationError(eify(e));\n        }\n    };\n\n    const network = state.network;\n\n    return (\n        <>\n            <InnerCard>\n                <form class=\"flex flex-col gap-4\" onSubmit={onSubmit}>\n                    <TextField.Root\n                        value={peerPubkey()}\n                        onChange={setPeerPubkey}\n                        class=\"flex flex-col gap-2\"\n                    >\n                        <TextField.Label class=\"text-sm font-semibold uppercase\">\n                            {i18n.t(\"settings.admin.kitchen_sink.pubkey\")}\n                        </TextField.Label>\n                        <TextField.Input class=\"w-full rounded-lg p-2 text-black\" />\n                    </TextField.Root>\n                    <TextField.Root\n                        value={amount()}\n                        onChange={setAmount}\n                        class=\"flex flex-col gap-2\"\n                    >\n                        <TextField.Label class=\"text-sm font-semibold uppercase\">\n                            {i18n.t(\"settings.admin.kitchen_sink.amount\")}\n                        </TextField.Label>\n                        <TextField.Input\n                            type=\"number\"\n                            class=\"w-full rounded-lg p-2 text-black\"\n                        />\n                    </TextField.Root>\n                    <Button layout=\"small\" type=\"submit\">\n                        {i18n.t(\"settings.admin.kitchen_sink.open_channel\")}\n                    </Button>\n                </form>\n            </InnerCard>\n            <Show when={newChannel()}>\n                <pre class=\"!select-text overflow-x-auto whitespace-pre-wrap break-all\">\n                    {JSON.stringify(newChannel()?.outpoint, null, 2)}\n                </pre>\n                <pre>{newChannel()?.outpoint}</pre>\n                <ExternalLink\n                    href={mempoolTxUrl(\n                        newChannel()?.outpoint?.split(\":\")[0],\n                        network\n                    )}\n                >\n                    {i18n.t(\"common.view_transaction\")}\n                </ExternalLink>\n            </Show>\n            <Show when={creationError()}>\n                <pre>{creationError()?.message}</pre>\n            </Show>\n        </>\n    );\n}\n\nfunction ListNodes() {\n    const i18n = useI18n();\n    const [_state, _actions, sw] = useMegaStore();\n\n    const getNodeIds = async () => {\n        const nodes = await sw.list_nodes();\n        return nodes as string[];\n    };\n\n    const [nodeIds] = createResource(getNodeIds);\n\n    return (\n        <InnerCard title={i18n.t(\"settings.admin.kitchen_sink.nodes\")}>\n            <Suspense>\n                <For\n                    each={nodeIds()}\n                    fallback={\n                        <code>\n                            {i18n.t(\"settings.admin.kitchen_sink.no_nodes\")}\n                        </code>\n                    }\n                >\n                    {(nodeId) => <MiniStringShower text={nodeId} />}\n                </For>\n            </Suspense>\n        </InnerCard>\n    );\n}\n\nfunction LSPS(props: { initialSettings: MutinyWalletSettingStrings }) {\n    const i18n = useI18n();\n    const [_state, _actions, sw] = useMegaStore();\n    const [lspSettingsForm, { Form, Field }] =\n        createForm<MutinyWalletSettingStrings>({\n            validate: (values) => {\n                const errors: Record<string, string> = {};\n                if (\n                    values.lsp &&\n                    (values.lsps_connection_string || values.lsps_token)\n                ) {\n                    errors.lsp = i18n.t(\"settings.servers.lsps_valid_error\");\n                }\n                return errors;\n            },\n            initialValues: props.initialSettings\n        });\n\n    async function handleSubmit(values: MutinyWalletSettingStrings) {\n        console.log(\"values\", values);\n        try {\n            await sw.change_lsp(\n                values.lsp ? values.lsp.trim() : undefined,\n                values.lsps_connection_string\n                    ? values.lsps_connection_string.trim()\n                    : undefined,\n                values.lsps_token ? values.lsps_token.trim() : undefined\n            );\n            await setSettings(values);\n            window.location.reload();\n        } catch (e) {\n            console.error(\"Error changing lsp:\", e);\n            showToast(eify(e));\n        }\n        console.log(values);\n    }\n\n    return (\n        <InnerCard>\n            <Form onSubmit={handleSubmit} class=\"flex flex-col gap-4\">\n                <Field\n                    name=\"lsp\"\n                    validate={[url(i18n.t(\"settings.servers.error_lsp\"))]}\n                >\n                    {(field, props) => (\n                        <FTextField\n                            {...props}\n                            value={field.value}\n                            error={field.error}\n                            label={i18n.t(\"settings.servers.lsp_label\")}\n                            caption={i18n.t(\"settings.servers.lsp_caption\")}\n                        />\n                    )}\n                </Field>\n                <Field name=\"lsps_connection_string\">\n                    {(field, props) => (\n                        <FTextField\n                            {...props}\n                            value={field.value}\n                            error={field.error}\n                            label={i18n.t(\n                                \"settings.servers.lsps_connection_string_label\"\n                            )}\n                            caption={i18n.t(\n                                \"settings.servers.lsps_connection_string_caption\"\n                            )}\n                        />\n                    )}\n                </Field>\n                <Field name=\"lsps_token\">\n                    {(field, props) => (\n                        <FTextField\n                            {...props}\n                            value={field.value}\n                            error={field.error}\n                            label={i18n.t(\"settings.servers.lsps_token_label\")}\n                            caption={i18n.t(\n                                \"settings.servers.lsps_token_caption\"\n                            )}\n                        />\n                    )}\n                </Field>\n                <Button\n                    type=\"submit\"\n                    disabled={!lspSettingsForm.dirty}\n                    intent=\"blue\"\n                >\n                    {i18n.t(\"settings.servers.save\")}\n                </Button>\n            </Form>\n        </InnerCard>\n    );\n}\n\nfunction AsyncLSPSEditor() {\n    const [settings] = createResource<MutinyWalletSettingStrings>(getSettings);\n\n    return (\n        <Switch>\n            <Match when={settings.error}>\n                <SimpleErrorDisplay error={settings.error} />\n            </Match>\n            <Match when={settings.latest}>\n                <LSPS initialSettings={settings()!} />\n            </Match>\n        </Switch>\n    );\n}\n\nexport function KitchenSink() {\n    return (\n        <>\n            <Suspense>\n                <AsyncLSPSEditor />\n            </Suspense>\n            <Hr />\n            <ListNodes />\n            <Hr />\n            <PeersList />\n            <Hr />\n            <ChannelsList />\n            <Hr />\n            <ToggleHodl />\n            <Hr />\n            <ResyncOnchain />\n            <Hr />\n            <Restart />\n            <Hr />\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/LabelCircle.tsx",
    "content": "import { createResource, createSignal, JSX, Match, Switch } from \"solid-js\";\nimport { Dynamic } from \"solid-js/web\";\n\nimport avatar from \"~/assets/generic-avatar.jpg\";\nimport { DEFAULT_NOSTR_NAME, generateGradient } from \"~/utils\";\n\nexport function Circle(props: {\n    children: JSX.Element;\n    color?: \"red\" | \"green\" | \"blue\";\n    size?: \"small\" | \"large\" | \"xl\";\n    background?: string;\n    onClick?: () => void;\n}) {\n    return (\n        <Dynamic\n            component={props.onClick ? \"button\" : \"div\"}\n            onClick={props.onClick}\n            class=\"flex flex-none items-center justify-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50  text-3xl uppercase\"\n            classList={{\n                \"bg-m-grey-800\": !props.color && !props.background,\n                \"bg-m-red\": props.color === \"red\" && !props.background,\n                \"bg-m-green\": props.color === \"green\" && !props.background,\n                \"h-[3rem] w-[3rem]\": !props.size,\n                \"h-[4rem] w-[4rem]\": props.size === \"large\",\n                \"h-[8rem] w-[8rem]\": props.size === \"xl\",\n                \"active:mt-[1px] active:-mb-[1px]\": !!props.onClick\n            }}\n            style={{\n                background: props.background\n            }}\n        >\n            {props.children}\n        </Dynamic>\n    );\n}\n\nexport function LabelCircle(props: {\n    name?: string;\n    image_url?: string;\n    contact: boolean;\n    label: boolean;\n    generic?: boolean;\n    size?: \"small\" | \"large\" | \"xl\";\n    onClick?: () => void;\n}) {\n    const [gradient] = createResource(async () => {\n        if (props.name && props.name !== DEFAULT_NOSTR_NAME && props.contact) {\n            return generateGradient(props.name || \"?\");\n        } else {\n            return undefined;\n        }\n    });\n\n    const text = () =>\n        props.contact &&\n        props.name &&\n        props.name.length &&\n        props.name !== DEFAULT_NOSTR_NAME\n            ? props.name[0]\n            : props.label\n              ? \"≡\"\n              : \"?\";\n    const bg = () => (props.name && props.contact ? gradient() : \"\");\n\n    const [errored, setErrored] = createSignal(false);\n\n    return (\n        <Circle\n            background={props.image_url && !errored() ? \"none\" : bg()}\n            onClick={\n                props.onClick\n                    ? () => props.onClick && props.onClick()\n                    : undefined\n            }\n            size={props.size}\n        >\n            <Switch>\n                <Match when={errored()}>{text()}</Match>\n                <Match when={props.image_url}>\n                    <img\n                        src={props.image_url}\n                        alt={\"image\"}\n                        onError={(e) => {\n                            // This doesn't stop the console errors from showing up\n                            e.stopPropagation();\n                            setErrored(true);\n                        }}\n                    />\n                </Match>\n                <Match when={text() === \"?\" || props.generic}>\n                    <img src={avatar} alt=\"avatar\" />\n                </Match>\n                <Match when={true}>{text()}</Match>\n            </Switch>\n        </Circle>\n    );\n}\n"
  },
  {
    "path": "src/components/LightningAddressShower.tsx",
    "content": "import { Copy, QrCode } from \"lucide-solid\";\nimport { createMemo, createSignal, Match, Show, Switch } from \"solid-js\";\nimport { QRCodeSVG } from \"solid-qr-code\";\n\nimport { FancyCard, LabelCircle, SimpleDialog } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { UserProfile } from \"~/routes\";\nimport { useCopy } from \"~/utils\";\n\nexport function LightningAddressShower(props: {\n    lud16?: string;\n    profile?: UserProfile;\n}) {\n    const i18n = useI18n();\n    const [showQr, setShowQr] = createSignal(false);\n\n    const [copy, copied] = useCopy({ copiedTimeout: 1000 });\n\n    const lud16 = createMemo(() => {\n        return props.lud16 || props.profile?.lud16 || \"\";\n    });\n\n    return (\n        <FancyCard>\n            <Switch>\n                <Match when={lud16()}>\n                    <p class=\"break-all text-center font-system-mono text-base \">\n                        {lud16()}\n                    </p>\n                    <div class=\"flex w-full justify-center gap-8\">\n                        <button onClick={() => setShowQr(true)}>\n                            <QrCode class=\"inline-block\" />\n                        </button>\n                        <button\n                            class=\"p-1\"\n                            classList={{\n                                \"bg-m-red rounded\": copied()\n                            }}\n                            onClick={() => copy(lud16())}\n                        >\n                            <Copy class=\"inline-block\" />\n                        </button>\n                    </div>{\" \"}\n                    <SimpleDialog\n                        open={showQr()}\n                        setOpen={(open) => {\n                            setShowQr(open);\n                        }}\n                        title={i18n.t(\"profile.pay_me\")}\n                    >\n                        <Show when={props.profile}>\n                            <div class=\"flex flex-col gap-2\">\n                                <div class=\"flex w-full justify-center\">\n                                    <LabelCircle\n                                        name={props.profile?.name}\n                                        image_url={props.profile?.picture}\n                                        contact\n                                        label={false}\n                                        size=\"large\"\n                                    />\n                                </div>\n                                <h2 class=\"text-center text-lg font-semibold\">\n                                    {props.profile?.name}\n                                </h2>\n                            </div>\n                            <p class=\"break-all text-center font-system-mono text-base \">\n                                {lud16()}\n                            </p>\n                        </Show>\n                        <div class=\"w-[10rem] self-center rounded bg-white p-[1rem]\">\n                            <QRCodeSVG\n                                value={\"lightning:\" + lud16() || \"\"}\n                                class=\"h-full max-h-[256px] w-full\"\n                            />\n                        </div>\n                    </SimpleDialog>\n                </Match>\n                <Match when={true}>\n                    <p class=\"text-center text-base italic text-m-grey-350\">\n                        {i18n.t(\"profile.no_lightning_address\")}\n                    </p>\n                </Match>\n            </Switch>\n        </FancyCard>\n    );\n}\n"
  },
  {
    "path": "src/components/LoadingIndicator.tsx",
    "content": "import { Progress } from \"@kobalte/core\";\nimport { Show } from \"solid-js\";\n\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nfunction LoadingBar(props: { value: number; max: number }) {\n    const i18n = useI18n();\n    function valueToStage(value: number) {\n        switch (value) {\n            case 0:\n                return i18n.t(\"modals.loading.default\");\n            case 1:\n                return i18n.t(\"modals.loading.double_checking\");\n            case 2:\n                return i18n.t(\"modals.loading.downloading\");\n            case 3:\n                return i18n.t(\"modals.loading.existing_wallet\");\n            case 4:\n                return i18n.t(\"modals.loading.setup\");\n            case 5:\n                return i18n.t(\"modals.loading.done\");\n            default:\n                return i18n.t(\"modals.loading.default\");\n        }\n    }\n    return (\n        <Progress.Root\n            value={props.value}\n            minValue={0}\n            maxValue={props.max}\n            getValueLabel={({ value }) =>\n                i18n.t(\"modals.loading.loading\", { stage: valueToStage(value) })\n            }\n            class=\"flex w-full flex-col gap-2\"\n        >\n            <Progress.ValueLabel class=\"text-sm text-m-grey-400\" />\n            <Progress.Track class=\"h-6  rounded bg-white/10\">\n                <Progress.Fill class=\"h-full w-[var(--kb-progress-fill-width)] rounded bg-m-blue transition-[width]\" />\n            </Progress.Track>\n        </Progress.Root>\n    );\n}\n\nexport function LoadingIndicator() {\n    const [state] = useMegaStore();\n\n    const loadStageValue = () => {\n        switch (state.load_stage) {\n            case \"fresh\":\n                return 0;\n            case \"checking_double_init\":\n                return 1;\n            case \"downloading\":\n                return 2;\n            case \"checking_for_existing_wallet\":\n                return 3;\n            case \"setup\":\n                return 4;\n            case \"done\":\n                return 5;\n            default:\n                return 0;\n        }\n    };\n\n    return (\n        <Show when={state.load_stage !== \"done\"}>\n            <LoadingBar value={loadStageValue()} max={5} />\n        </Show>\n    );\n}\n"
  },
  {
    "path": "src/components/Logo.tsx",
    "content": "import { Match, Switch } from \"solid-js\";\n\nimport pixelLogo from \"~/assets/mutiny-pixel-logo.png\";\nimport plusLogo from \"~/assets/mutiny-plus-logo.png\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function Logo() {\n    const [state, _actions] = useMegaStore();\n    return (\n        <Switch>\n            <Match when={state.mutiny_plus}>\n                <img\n                    id=\"mutiny-logo\"\n                    src={plusLogo}\n                    class=\"h-[25px] w-[86px]\"\n                    alt=\"Mutiny Plus logo\"\n                />\n            </Match>\n            <Match when={true}>\n                <img\n                    id=\"mutiny-logo\"\n                    src={pixelLogo}\n                    class=\"h-[25px] w-[75px]\"\n                    alt=\"Mutiny logo\"\n                />\n            </Match>\n        </Switch>\n    );\n}\n"
  },
  {
    "path": "src/components/Logs.tsx",
    "content": "import { createSignal, Show } from \"solid-js\";\n\nimport { Button, InfoBox, InnerCard, NiceP, VStack } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { downloadTextFile, eify } from \"~/utils\";\n\nexport function Logs() {\n    const i18n = useI18n();\n    const [_state, _actions, sw] = useMegaStore();\n\n    // Create state for errors, password and dialog visibility\n    const [error, setError] = createSignal<Error>();\n\n    async function handleSave() {\n        try {\n            setError(undefined);\n            const logs = await sw.get_logs();\n            await downloadTextFile(\n                logs.join(\"\") || \"\",\n                \"mutiny-logs.txt\",\n                \"text/plain\"\n            );\n        } catch (e) {\n            console.error(e);\n            setError(eify(e));\n        }\n    }\n\n    return (\n        <InnerCard title={i18n.t(\"settings.emergency_kit.logs.title\")}>\n            <VStack>\n                <NiceP>\n                    {i18n.t(\"settings.emergency_kit.logs.something_screwy\")}\n                </NiceP>\n                <Button intent=\"green\" onClick={handleSave}>\n                    {i18n.t(\"settings.emergency_kit.logs.download_logs\")}\n                </Button>\n            </VStack>\n            <Show when={error()}>\n                <InfoBox accent=\"red\">{error()?.message}</InfoBox>\n            </Show>\n        </InnerCard>\n    );\n}\n"
  },
  {
    "path": "src/components/MoreInfoModal.tsx",
    "content": "import { createSignal, JSXElement, ParentComponent } from \"solid-js\";\n\nimport { ExternalLink, SimpleDialog } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nexport function FeesModal() {\n    const i18n = useI18n();\n    return (\n        <MoreInfoModal\n            title={i18n.t(\"modals.more_info.whats_with_the_fees\")}\n            linkText={i18n.t(\"common.why\")}\n        >\n            <p>{i18n.t(\"modals.more_info.self_custodial\")}</p>\n            <p>{i18n.t(\"modals.more_info.future_payments\")}</p>\n            <p>{i18n.t(\"modals.more_info.fedimint\")}</p>\n            <p>\n                <ExternalLink href=\"https://github.com/MutinyWallet/mutiny-web/wiki/Understanding-liquidity\">\n                    {i18n.t(\"modals.more_info.liquidity\")}\n                </ExternalLink>\n            </p>\n        </MoreInfoModal>\n    );\n}\n\nconst MoreInfoModal: ParentComponent<{\n    linkText: string | JSXElement;\n    title: string;\n}> = (props) => {\n    const [open, setOpen] = createSignal(false);\n\n    return (\n        <>\n            <button\n                tabIndex={-1}\n                onClick={() => setOpen(true)}\n                class=\"font-semibold underline decoration-light-text hover:decoration-white\"\n            >\n                {props.linkText}\n            </button>\n            <SimpleDialog open={open()} setOpen={setOpen} title={props.title}>\n                {props.children}\n            </SimpleDialog>\n        </>\n    );\n};\n"
  },
  {
    "path": "src/components/MutinyPlusCta.tsx",
    "content": "import { A } from \"@solidjs/router\";\nimport { ChevronRight } from \"lucide-solid\";\nimport { ParentComponent, Show } from \"solid-js\";\n\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nconst CtaCard: ParentComponent = (props) => {\n    return (\n        <div class=\"w-full\">\n            <div class=\"relative\">\n                <div class=\"to-bg-m-grey-900/50 absolute inset-0 h-full w-full scale-[0.60] transform rounded-full bg-m-red/50 bg-gradient-to-r from-m-red/50 blur-2xl\" />\n                <div class=\"relative flex-col gap-2 rounded-xl border border-m-red bg-m-grey-800 py-4\">\n                    {props.children}{\" \"}\n                </div>\n            </div>\n        </div>\n    );\n};\n\nexport function MutinyPlusCta() {\n    const [state, _actions] = useMegaStore();\n    const i18n = useI18n();\n    return (\n        <CtaCard>\n            <A\n                href={\"/settings/plus\"}\n                class=\"flex w-full flex-col gap-1 px-4 py-2 no-underline hover:bg-m-grey-750 active:bg-m-grey-900\"\n            >\n                <div class=\"flex justify-between\">\n                    <span>\n                        {i18n.t(\"common.mutiny\")}\n                        <span class=\"text-m-red\">+</span>\n                    </span>\n                    <ChevronRight />\n                </div>\n                <div class=\"text-sm text-m-grey-400\">\n                    <Show\n                        when={state.mutiny_plus}\n                        fallback={i18n.t(\"settings.plus.cta_description\")}\n                    >\n                        {i18n.t(\"settings.plus.cta_but_already_plus\")}\n                    </Show>\n                </div>\n            </A>\n        </CtaCard>\n    );\n}\n"
  },
  {
    "path": "src/components/NWCEditor.tsx",
    "content": "import {\n    createForm,\n    getValue,\n    required,\n    setValue,\n    SubmitHandler\n} from \"@modular-forms/solid\";\nimport { BudgetPeriod, NwcProfile, TagItem } from \"@mutinywallet/mutiny-wasm\";\nimport { createAsync } from \"@solidjs/router\";\nimport {\n    createEffect,\n    createMemo,\n    createResource,\n    For,\n    Match,\n    ResourceFetcher,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    AmountEditable,\n    AmountSats,\n    Avatar,\n    Button,\n    Checkbox,\n    InfoBox,\n    KeyValue,\n    LoadingShimmer,\n    TextField,\n    TinyText,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { fetchNostrProfile } from \"~/utils\";\n\ntype BudgetInterval = \"Day\" | \"Week\" | \"Month\" | \"Year\";\ntype BudgetForm = {\n    connection_name: string;\n    auto_approve: boolean;\n    budget_amount: string; // modular forms doesn't like bigint\n    interval: BudgetInterval;\n    profileIndex?: number;\n    nwaString?: string;\n};\n\ntype FormMode = \"createnwa\" | \"createnwc\" | \"editnwc\";\ntype BudgetMode = \"fixed\" | \"editable\";\n\nfunction parseNWA(nwaString?: string) {\n    if (!nwaString) return undefined;\n    const nwa = decodeURI(nwaString);\n    if (nwa) {\n        // Examples:\n        // Mainnet\n        // nostr+walletauth://a957bc527d4b7cea5134308412719fa675671ed38eb313adcf89b96c6982480e?relay=wss%3A%2F%2Frelay.damus.io%2F&secret=a2522b4c6d6ae729&required_commands=pay_invoice&budget=21%2Fday&identity=04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9\n        // Signet\n        // nostr+walletauth://e9a09c45e3d412d694796041e45cb0ab8b92edbceec459ae76376b98111c9a3c?relay=wss%3A%2F%2Frelay.damus.io%2F&secret=5f894e2db96e0c63&required_commands=pay_invoice&budget=21%2Fday&identity=024f93e1890e9e470fb729ea24426766508c0e0c5618b5b475f2d027d0814d09\n        const url = new URL(nwa);\n        return {\n            budget: url.searchParams.get(\"budget\"),\n            identity: url.searchParams.get(\"identity\"),\n            required_commands: url.searchParams.get(\"required_commands\")\n        };\n    }\n}\n\n// nwa is \"day\" but waila parses it as \"daily\"\nfunction mapNwaInterval(interval: string): BudgetInterval | undefined {\n    switch (interval) {\n        case \"day\":\n        case \"daily\":\n            return \"Day\";\n        case \"week\":\n        case \"weekly\":\n            return \"Week\";\n        case \"month\":\n        case \"monthly\":\n            return \"Month\";\n        case \"year\":\n        case \"yearly\":\n            return \"Year\";\n        default:\n            return undefined;\n    }\n}\n\nfunction mapIntervalToBudgetPeriod(\n    interval: \"Day\" | \"Week\" | \"Month\" | \"Year\"\n): BudgetPeriod {\n    switch (interval) {\n        case \"Day\":\n            return 0;\n        case \"Week\":\n            return 1;\n        case \"Month\":\n            return 2;\n        case \"Year\":\n            return 3;\n    }\n}\n\nexport function NWCEditor(props: {\n    initialProfileIndex?: number;\n    initialNWA?: string;\n    onSave: (indexToOpen?: number, nwcUriForCallback?: string) => Promise<void>;\n}) {\n    const [_state, _actions, sw] = useMegaStore();\n    const i18n = useI18n();\n\n    const nwa = createMemo(() => parseNWA(props.initialNWA));\n\n    const formMode = createMemo(() => {\n        const mode: \"createnwa\" | \"createnwc\" | \"editnwc\" = nwa()\n            ? \"createnwa\"\n            : typeof props.initialProfileIndex === \"number\"\n              ? \"editnwc\"\n              : \"createnwc\";\n        return mode;\n    });\n\n    // NWA HANDLING SECTION\n\n    // for \"createNwa\" we need to parse the nwa and fetch the nostr profile if applicable\n    const [nostrProfile] = createResource(\n        () => nwa()?.identity,\n        fetchNostrProfile\n    );\n\n    const name = createMemo(() => {\n        if (!nostrProfile.latest) return;\n        const parsed = JSON.parse(nostrProfile.latest.content);\n        const name = parsed.display_name || parsed.name;\n        return name;\n    });\n\n    const image = createMemo(() => {\n        if (!nostrProfile.latest) return;\n        const parsed = JSON.parse(nostrProfile.latest.content);\n        const image_url = parsed.picture;\n        return image_url;\n    });\n\n    const parsedBudget = createMemo(() => {\n        if (!nwa()?.budget) return;\n        const [amount, interval] = nwa()!.budget!.split(\"/\");\n        return {\n            amount,\n            interval: mapNwaInterval(interval)\n        };\n    });\n\n    async function createNwa(f: BudgetForm) {\n        if (!f.nwaString) throw new Error(\"We lost the NWA string!\");\n        try {\n            await sw.approve_nostr_wallet_auth(\n                f.connection_name || \"Nostr Wallet Auth\",\n                // can we do better than ! here?\n                f.nwaString\n            );\n        } catch (e) {\n            console.error(e);\n        } finally {\n            props.onSave();\n        }\n    }\n    // END NWA HANDLING SECTION\n\n    // REGULAR NWC STUFF\n    // If the profile has a label we can fetch the contact for showing the profile image\n\n    const nwcProfileFetcher: ResourceFetcher<\n        number,\n        NwcProfile | undefined\n    > = async (index, _last) => {\n        console.log(\"fetching nwc profile\", index);\n        if (typeof index !== \"number\") return undefined;\n\n        try {\n            const profile: NwcProfile | undefined =\n                await sw.get_nwc_profile(index);\n            console.log(profile);\n            return profile;\n        } catch (e) {\n            console.error(e);\n            return undefined;\n        }\n    };\n\n    const [profile] = createResource(\n        props.initialProfileIndex,\n        nwcProfileFetcher\n    );\n\n    // TODO: this should get the contact so we can get the image, but not getting a contact tagged on the nwc right now\n    const contactFetcher: ResourceFetcher<string, TagItem | undefined> = async (\n        label,\n        _last\n    ) => {\n        console.log(\"fetching contact\", label);\n        if (!label) return undefined;\n\n        try {\n            const contact: TagItem | undefined = await sw.get_tag_item(label);\n            return contact;\n        } catch (e) {\n            console.error(e);\n            return undefined;\n        }\n    };\n\n    const [contact] = createResource(profile()?.label, contactFetcher);\n\n    async function saveConnection(f: BudgetForm) {\n        let newProfile: NwcProfile | undefined = undefined;\n        if (typeof f.profileIndex !== \"number\")\n            throw new Error(\"No profile index!\");\n        if (!f.auto_approve || f.budget_amount === \"0\") {\n            newProfile = await sw.set_nwc_profile_require_approval(\n                f.profileIndex\n            );\n        } else {\n            newProfile = await sw.set_nwc_profile_budget(\n                f.profileIndex,\n                BigInt(f.budget_amount),\n                mapIntervalToBudgetPeriod(f.interval)\n            );\n        }\n\n        if (!newProfile) {\n            // This will be caught by the form\n            throw new Error(i18n.t(\"settings.connections.error_connection\"));\n        } else {\n            // Remember the index so the collapser is open after creation\n            props.onSave(newProfile.index);\n        }\n    }\n\n    async function createConnection(f: BudgetForm) {\n        let newProfile: NwcProfile | undefined = undefined;\n\n        if (!f.auto_approve || f.budget_amount === \"0\") {\n            newProfile = await sw.create_nwc_profile(f.connection_name);\n        } else {\n            newProfile = await sw.create_budget_nwc_profile(\n                f.connection_name,\n                BigInt(f.budget_amount),\n                mapIntervalToBudgetPeriod(f.interval),\n                undefined\n            );\n        }\n\n        if (!newProfile) {\n            throw new Error(i18n.t(\"settings.connections.error_connection\"));\n        } else {\n            if (newProfile.nwc_uri) {\n                props.onSave(newProfile.index, newProfile.nwc_uri);\n            } else {\n                props.onSave(newProfile.index);\n            }\n        }\n    }\n\n    // TODO: refactor nwc editor so we don't need to do this\n    const initialBudget = createAsync(async () => {\n        if (profile()?.tag === \"Subscription\") {\n            try {\n                const plans = await sw.get_subscription_plans();\n                if (plans.length) {\n                    const returnValue = plans[0].amount_sat.toString() || \"0\";\n                    return returnValue;\n                } else {\n                    return \"0\";\n                }\n            } catch (e) {\n                console.error(e);\n                return \"0\";\n            }\n        }\n        if (profile()?.budget_amount) {\n            return profile()?.budget_amount?.toString() || \"0\";\n        }\n        return \"0\";\n    });\n\n    return (\n        <Switch>\n            <Match when={formMode() === \"createnwc\"}>\n                <NWCEditorForm\n                    values={{\n                        connection_name: \"\",\n                        auto_approve: false,\n                        budget_amount: \"0\",\n                        interval: \"Day\"\n                    }}\n                    formMode={formMode()}\n                    budgetMode=\"editable\"\n                    onSave={createConnection}\n                />\n            </Match>\n            <Match when={formMode() === \"createnwa\"}>\n                <Show\n                    when={nwa()?.identity ? nostrProfile()?.content : true}\n                    fallback={<LoadingShimmer />}\n                >\n                    <Avatar large image_url={image()} />\n                    <NWCEditorForm\n                        values={{\n                            connection_name: name(),\n                            auto_approve: nwa()?.budget ? true : false,\n                            budget_amount:\n                                nwa()?.budget && parsedBudget()?.amount\n                                    ? parsedBudget()!.amount\n                                    : \"0\",\n                            interval: parsedBudget()?.interval ?? \"Day\",\n                            nwaString: props.initialNWA\n                        }}\n                        formMode={formMode()}\n                        budgetMode={nwa()?.budget ? \"fixed\" : \"editable\"}\n                        onSave={createNwa}\n                    />\n                </Show>\n            </Match>\n            <Match when={formMode() === \"editnwc\"}>\n                {/* FIXME: not getting the contact rn */}\n                <Suspense>\n                    <Show when={profile()}>\n                        <Show when={profile()?.label && contact()?.image_url}>\n                            <Avatar large image_url={contact()?.image_url} />\n                            <pre>{JSON.stringify(contact(), null, 2)}</pre>\n                        </Show>\n                        <Show when={initialBudget()}>\n                            <NWCEditorForm\n                                values={{\n                                    connection_name: profile()?.name ?? \"\",\n                                    auto_approve: !profile()?.require_approval,\n                                    budget_amount: initialBudget()!,\n                                    interval:\n                                        profile()?.tag === \"Subscription\"\n                                            ? \"Month\"\n                                            : (profile()?.budget_period?.toString() as BudgetForm[\"interval\"]) ||\n                                              \"Day\",\n\n                                    profileIndex: profile()?.index\n                                }}\n                                formMode={formMode()}\n                                budgetMode={\n                                    profile()?.tag === \"Subscription\"\n                                        ? \"fixed\"\n                                        : \"editable\"\n                                }\n                                onSave={saveConnection}\n                            />\n                        </Show>\n                    </Show>\n                </Suspense>\n            </Match>\n        </Switch>\n    );\n}\n\nfunction NWCEditorForm(props: {\n    values: BudgetForm;\n    formMode: FormMode;\n    budgetMode: BudgetMode;\n    onSave: (f: BudgetForm) => Promise<void>;\n}) {\n    const i18n = useI18n();\n\n    const [budgetForm, { Form, Field }] = createForm<BudgetForm>({\n        initialValues: props.values,\n        validate: (values) => {\n            const errors: Record<string, string> = {};\n            if (values.auto_approve && values.budget_amount === \"0\") {\n                errors.budget_amount = i18n.t(\n                    \"settings.connections.error_budget_zero\"\n                );\n            }\n            return errors;\n        }\n    });\n\n    createEffect(() => {\n        if (props.values) {\n            setValue(budgetForm, \"budget_amount\", props.values.budget_amount);\n        }\n    });\n\n    const handleFormSubmit: SubmitHandler<BudgetForm> = async (\n        f: BudgetForm\n    ) => {\n        // If this throws the message will be caught by the form\n        await props.onSave({\n            ...f,\n            profileIndex: props.values.profileIndex,\n            nwaString: props.values.nwaString\n        });\n    };\n\n    return (\n        <Form onSubmit={handleFormSubmit}>\n            <VStack>\n                <Field\n                    name=\"connection_name\"\n                    validate={[\n                        required(i18n.t(\"settings.connections.error_name\"))\n                    ]}\n                >\n                    {(field, fieldProps) => (\n                        <TextField\n                            disabled={props.values?.connection_name !== \"\"}\n                            value={field.value}\n                            {...fieldProps}\n                            name=\"name\"\n                            error={field.error}\n                            placeholder={i18n.t(\n                                \"settings.connections.new_connection_placeholder\"\n                            )}\n                        />\n                    )}\n                </Field>\n                <Field name=\"auto_approve\" type=\"boolean\">\n                    {(field, _fieldProps) => (\n                        <Checkbox\n                            checked={field.value || false}\n                            label=\"Auto Approve\"\n                            onChange={(c) =>\n                                setValue(budgetForm, \"auto_approve\", c)\n                            }\n                        />\n                    )}\n                </Field>\n                <Show when={getValue(budgetForm, \"auto_approve\")}>\n                    <VStack>\n                        <TinyText>\n                            {i18n.t(\"settings.connections.careful\")}\n                        </TinyText>\n\n                        <Field name=\"budget_amount\">\n                            {(field, _fieldProps) => (\n                                <div class=\"flex flex-col items-end gap-2\">\n                                    <Show\n                                        when={props.budgetMode === \"editable\"}\n                                        fallback={\n                                            <AmountSats\n                                                amountSats={\n                                                    Number(field.value) || 0\n                                                }\n                                            />\n                                        }\n                                    >\n                                        <AmountEditable\n                                            initialAmountSats={\n                                                field.value || \"0\"\n                                            }\n                                            setAmountSats={(a) => {\n                                                setValue(\n                                                    budgetForm,\n                                                    \"budget_amount\",\n                                                    a.toString()\n                                                );\n                                            }}\n                                        />\n                                    </Show>\n                                    <p class=\"text-sm text-m-red\">\n                                        {field.error}\n                                    </p>\n                                </div>\n                            )}\n                        </Field>\n                        <KeyValue\n                            key={i18n.t(\"settings.connections.resets_every\")}\n                        >\n                            <Field name=\"interval\">\n                                {(field, fieldProps) => (\n                                    <Show\n                                        when={props.budgetMode === \"editable\"}\n                                        fallback={\n                                            budgetForm.internal.initialValues\n                                                .interval\n                                        }\n                                    >\n                                        <select\n                                            {...fieldProps}\n                                            class=\"w-full rounded-lg bg-m-grey-750 py-2 pl-4 pr-12 text-base font-normal text-white\"\n                                        >\n                                            <For\n                                                each={[\n                                                    {\n                                                        label: \"Day\",\n                                                        value: \"Day\"\n                                                    },\n                                                    {\n                                                        label: \"Week\",\n                                                        value: \"Week\"\n                                                    },\n                                                    {\n                                                        label: \"Month\",\n                                                        value: \"Month\"\n                                                    },\n                                                    {\n                                                        label: \"Year\",\n                                                        value: \"Year\"\n                                                    }\n                                                ]}\n                                            >\n                                                {({ label, value }) => (\n                                                    <option\n                                                        value={value}\n                                                        selected={\n                                                            field.value ===\n                                                            value\n                                                        }\n                                                    >\n                                                        {label}\n                                                    </option>\n                                                )}\n                                            </For>\n                                        </select>\n                                    </Show>\n                                )}\n                            </Field>\n                        </KeyValue>\n                    </VStack>\n                </Show>\n                <Show when={budgetForm.response.message}>\n                    <InfoBox accent=\"red\">\n                        {budgetForm.response.message}\n                    </InfoBox>\n                </Show>\n                <Button\n                    type=\"submit\"\n                    intent=\"blue\"\n                    loading={budgetForm.submitting}\n                >\n                    {props.formMode === \"editnwc\"\n                        ? i18n.t(\"settings.connections.save_connection\")\n                        : i18n.t(\"settings.connections.create_connection\")}\n                </Button>\n            </VStack>\n        </Form>\n    );\n}\n"
  },
  {
    "path": "src/components/NavBar.tsx",
    "content": "import { A } from \"@solidjs/router\";\nimport {\n    ArrowDownLeft,\n    ArrowUpRight,\n    Scan,\n    Settings,\n    User,\n    Wallet\n} from \"lucide-solid\";\nimport { JSX } from \"solid-js\";\n\ntype ActiveTab =\n    | \"home\"\n    | \"scan\"\n    | \"send\"\n    | \"receive\"\n    | \"settings\"\n    | \"profile\"\n    | \"none\";\n\nfunction NavBarItem(props: {\n    href: string;\n    icon: JSX.Element;\n    active: boolean;\n}) {\n    return (\n        <li>\n            <A\n                class=\"block rounded-lg p-2\"\n                href={props.href}\n                classList={{\n                    \"hover:bg-white/20 active:bg-white/10 active:mt-[2px] active:-mb-[2px]\":\n                        !props.active,\n                    \"bg-m-red\": props.active\n                }}\n            >\n                {props.icon}\n            </A>\n        </li>\n    );\n}\n\nexport function NavBar(props: { activeTab: ActiveTab }) {\n    return (\n        <nav class=\"fixed bottom-auto left-0 top-0 z-40 hidden h-full shadow-none safe-bottom md:block\">\n            <ul class=\"mt-4 flex flex-col justify-start gap-4 px-4\">\n                <NavBarItem\n                    href=\"/\"\n                    icon={<Wallet class=\"h-8 w-8\" />}\n                    active={props.activeTab === \"home\"}\n                />\n                <NavBarItem\n                    href=\"/search\"\n                    icon={<ArrowUpRight class=\"h-8 w-8\" />}\n                    active={props.activeTab === \"send\"}\n                />\n                <NavBarItem\n                    href=\"/receive\"\n                    icon={<ArrowDownLeft class=\"h-8 w-8\" />}\n                    active={props.activeTab === \"receive\"}\n                />\n                <NavBarItem\n                    href=\"/scanner\"\n                    icon={<Scan class=\"h-8 w-8\" />}\n                    active={false}\n                />\n                <NavBarItem\n                    href=\"/profile\"\n                    icon={<User class=\"h-8 w-8\" />}\n                    active={props.activeTab === \"profile\"}\n                />\n                <NavBarItem\n                    href=\"/settings\"\n                    icon={<Settings class=\"h-8 w-8\" />}\n                    active={props.activeTab === \"settings\"}\n                />\n            </ul>\n        </nav>\n    );\n}\n"
  },
  {
    "path": "src/components/NostrActivity.tsx",
    "content": "import { createAsync, useNavigate } from \"@solidjs/router\";\nimport { Search } from \"lucide-solid\";\nimport {\n    createEffect,\n    createResource,\n    For,\n    Match,\n    Show,\n    Switch\n} from \"solid-js\";\n\nimport { ButtonCard, NiceP } from \"~/components/layout\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { fetchZaps, getPrimalImageUrl } from \"~/utils\";\nimport { timeAgo } from \"~/utils/prettyPrintTime\";\n\nimport { GenericItem } from \"./GenericItem\";\n\nexport function Avatar(props: { image_url?: string; large?: boolean }) {\n    return (\n        <div\n            class=\"flex h-[3rem] w-[3rem] flex-none items-center justify-center self-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 bg-neutral-700 text-3xl uppercase\"\n            classList={{\n                \"h-[6rem] w-[6rem]\": props.large\n            }}\n        >\n            <Switch>\n                <Match when={props.image_url}>\n                    <img src={props.image_url} alt={\"image\"} />\n                </Match>\n                <Match when={true}>?</Match>\n            </Switch>\n        </div>\n    );\n}\n\nexport function NostrActivity() {\n    const i18n = useI18n();\n    const [state, _actions, sw] = useMegaStore();\n\n    const npub = createAsync(async () => await sw.get_npub());\n\n    const [data, { refetch }] = createResource(npub, fetchZaps);\n\n    function nameFromHexpub(hexpub: string): string {\n        const profile = data.latest?.profiles[hexpub];\n        if (!profile) return hexpub;\n        const parsed = JSON.parse(profile.content);\n        const name = parsed.display_name || parsed.name;\n        return name;\n    }\n\n    function imageFromHexpub(hexpub: string): string | undefined {\n        const profile = data.latest?.profiles[hexpub];\n        if (!profile) return;\n        const parsed = JSON.parse(profile.content);\n        const image_url = parsed.image || parsed.picture;\n        return getPrimalImageUrl(image_url);\n    }\n\n    createEffect(() => {\n        // Should re-run after every sync\n        if (!state.is_syncing) {\n            refetch();\n        }\n    });\n\n    const navigate = useNavigate();\n\n    // TODO: can this be part of mutiny wallet?\n    async function newContactFromHexpub(hexpub: string) {\n        try {\n            const npub = await sw.hexpub_to_npub(hexpub);\n            console.log(\"newContactFromHexpub\", npub);\n\n            if (!npub) {\n                throw new Error(\"No npub for that hexpub\");\n            }\n\n            const existingContact = await sw.get_contact_for_npub(npub);\n\n            if (existingContact) {\n                navigate(`/chat/${existingContact.id}`);\n                return;\n            }\n\n            const profile = data.latest?.profiles[hexpub];\n            if (!profile) return;\n            const parsed = JSON.parse(profile.content);\n            const name = parsed.display_name || parsed.name || profile.pubkey;\n            const image_url = parsed.image || parsed.picture || undefined;\n            const ln_address = parsed.lud16 || undefined;\n            const lnurl = parsed.lud06 || undefined;\n\n            const contactId = await sw.create_new_contact(\n                name,\n                npub,\n                ln_address,\n                lnurl,\n                image_url\n            );\n\n            if (!contactId) {\n                throw new Error(\"no contact id returned\");\n            }\n\n            const tagItem = await sw.get_tag_item(contactId);\n\n            if (!tagItem) {\n                throw new Error(\"no contact returned\");\n            }\n\n            navigate(`/chat/${contactId}`);\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    return (\n        <div class=\"flex w-full flex-col divide-y divide-m-grey-800 overflow-x-clip\">\n            <Show when={!data.latest || data.latest?.zaps.length === 0}>\n                <ButtonCard onClick={() => navigate(\"/search\")}>\n                    <div class=\"flex items-center gap-2\">\n                        <Search class=\"inline-block text-m-red\" />\n                        <NiceP>{i18n.t(\"home.find\")}</NiceP>\n                    </div>\n                </ButtonCard>\n            </Show>\n            <For each={data.latest?.zaps}>\n                {(zap) => (\n                    <>\n                        <GenericItem\n                            primaryAvatarUrl={\n                                imageFromHexpub(zap.from_hexpub) || \"\"\n                            }\n                            primaryName={\n                                zap.kind === \"anonymous\"\n                                    ? i18n.t(\"activity.anonymous\")\n                                    : zap.kind === \"private\"\n                                      ? i18n.t(\"activity.private\")\n                                      : nameFromHexpub(zap.from_hexpub)\n                            }\n                            primaryOnClick={() => {\n                                newContactFromHexpub(zap.from_hexpub);\n                            }}\n                            secondaryAvatarUrl={\n                                imageFromHexpub(zap.to_hexpub) || \"\"\n                            }\n                            secondaryName={nameFromHexpub(zap.to_hexpub)}\n                            secondaryOnClick={() => {\n                                newContactFromHexpub(zap.to_hexpub);\n                            }}\n                            verb={\"zapped\"}\n                            amount={zap.amount_sats}\n                            message={zap.content ? zap.content : undefined}\n                            date={timeAgo(zap.timestamp, data.latest?.until)}\n                            visibility={\n                                zap.kind === \"public\" ? \"public\" : \"private\"\n                            }\n                            genericAvatar={\n                                zap.kind === \"anonymous\" ||\n                                zap.kind === \"private\"\n                            }\n                            forceSecondary\n                            link={`https://njump.me/e/${zap.event_id}`}\n                        />\n                    </>\n                )}\n            </For>\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/PendingNwc.tsx",
    "content": "import { useNavigate } from \"@solidjs/router\";\nimport { Check, PlugZap, X } from \"lucide-solid\";\nimport {\n    createEffect,\n    createResource,\n    createSignal,\n    For,\n    Show\n} from \"solid-js\";\n\nimport { ButtonCard, GenericItem, InfoBox, NiceP } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport {\n    createDeepSignal,\n    eify,\n    veryShortTimeStamp,\n    vibrateSuccess\n} from \"~/utils\";\n\ntype PendingItem = {\n    id: string;\n    name_of_connection: string;\n    date?: bigint;\n    amount_sats?: bigint;\n    image?: string;\n};\n\nexport function PendingNwc() {\n    const i18n = useI18n();\n    const [state, _actions, sw] = useMegaStore();\n\n    const [error, setError] = createSignal<Error>();\n\n    const navigate = useNavigate();\n\n    const [hasPreConfiguredNWC, setHasPreConfiguredNWC] = createSignal(false);\n\n    async function fetchPendingRequests() {\n        const profiles = await sw.get_nwc_profiles();\n        setHasPreConfiguredNWC(!!profiles && profiles.length > 0);\n        if (!profiles) return [];\n\n        const contacts = await sw.get_contacts_sorted();\n        if (!contacts) return [];\n\n        const pending = await sw.get_pending_nwc_invoices();\n        if (!pending) return [];\n\n        const pendingItems: PendingItem[] = [];\n\n        for (const p of pending) {\n            const profile = profiles.find((pro) => pro.index === p.index);\n\n            if (profile) {\n                pendingItems.push({\n                    id: p.id,\n                    name_of_connection: profile.name,\n                    date: p.expiry,\n                    amount_sats: p.amount_sats\n                });\n            } else {\n                const contact = contacts.find((c) => c.npub === p.npub);\n                if (contact) {\n                    pendingItems.push({\n                        id: p.id,\n                        name_of_connection: contact.name,\n                        image: contact.image_url,\n                        date: p.expiry,\n                        amount_sats: p.amount_sats\n                    });\n                }\n            }\n        }\n        return pendingItems;\n    }\n\n    const [pendingRequests, { refetch }] = createResource(\n        fetchPendingRequests,\n        // Create deepsignal so we don't get flicker on refresh\n        { storage: createDeepSignal }\n    );\n\n    const [payList, setPayList] = createSignal<string[]>([]);\n\n    async function payItem(item: PendingItem) {\n        try {\n            // setPaying(item.id);\n            setPayList([...payList(), item.id]);\n            await sw.approve_invoice(item.id);\n            await vibrateSuccess();\n        } catch (e) {\n            const err = eify(e);\n            // If we've already paid this invoice, just ignore the error\n            // we just want to remove it from the list and continue\n            if (err.message === \"An invoice must not get payed twice.\") {\n                // wrap in try/catch so we don't crash if the invoice is already gone\n                try {\n                    await sw.deny_invoice(item.id);\n                } catch (_e) {\n                    // do nothing\n                }\n            } else {\n                setError(err);\n                console.error(e);\n            }\n        } finally {\n            setPayList(payList().filter((id) => id !== item.id));\n            refetch();\n        }\n    }\n\n    async function approveAll() {\n        // clone the list so it doesn't update in place\n        const toApprove = [...pendingRequests()!];\n        for (const item of toApprove) {\n            await payItem(item);\n        }\n    }\n\n    async function denyAll() {\n        try {\n            await sw.deny_all_pending_nwc();\n        } catch (e) {\n            setError(eify(e));\n            console.error(e);\n        } finally {\n            refetch();\n        }\n    }\n\n    async function rejectItem(item: PendingItem) {\n        try {\n            setPayList([...payList(), item.id]);\n            await sw.deny_invoice(item.id);\n        } catch (e) {\n            setError(eify(e));\n            console.error(e);\n        } finally {\n            setPayList(payList().filter((id) => id !== item.id));\n            refetch();\n        }\n    }\n\n    createEffect(() => {\n        // When there's an error wait five seconds and then clear it\n        if (error()) {\n            setTimeout(() => {\n                setError(undefined);\n            }, 5000);\n        }\n    });\n\n    createEffect(() => {\n        // Refetch on the sync interval\n        if (!state.is_syncing) {\n            refetch();\n        }\n    });\n\n    return (\n        <>\n            <ButtonCard onClick={() => navigate(\"/settings/connections\")}>\n                <div class=\"flex items-center gap-2\">\n                    <PlugZap class=\"inline-block text-m-red\" />\n                    <NiceP>\n                        {hasPreConfiguredNWC()\n                            ? i18n.t(\"home.connection_edit\")\n                            : i18n.t(\"home.connection\")}\n                    </NiceP>\n                </div>\n            </ButtonCard>\n            <Show\n                when={\n                    pendingRequests.latest && pendingRequests.latest!.length > 0\n                }\n            >\n                <div class=\"flex w-full justify-around\">\n                    <button\n                        class=\"flex items-center gap-1 font-semibold text-m-green active:-mb-[1px] active:mt-[1px] active:text-m-green/80\"\n                        onClick={approveAll}\n                    >\n                        <Check />\n                        <span>\n                            {i18n.t(\n                                \"settings.connections.pending_nwc.approve_all\"\n                            )}\n                        </span>\n                    </button>\n                    <button\n                        class=\"flex items-center gap-1 font-semibold text-m-red active:-mb-[1px] active:mt-[1px] active:text-m-red/80\"\n                        onClick={denyAll}\n                    >\n                        <X />\n                        <span>\n                            {i18n.t(\n                                \"settings.connections.pending_nwc.deny_all\"\n                            )}\n                        </span>\n                    </button>\n                </div>\n                <div class=\"flex w-full flex-col divide-y divide-m-grey-800 overflow-x-clip\">\n                    <Show when={error()}>\n                        <InfoBox accent=\"red\">{error()?.message}</InfoBox>\n                    </Show>\n                    <For each={pendingRequests.latest}>\n                        {(pendingItem) => (\n                            <GenericItem\n                                primaryAvatarUrl={pendingItem.image || \"\"}\n                                verb=\"requested\"\n                                amount={pendingItem.amount_sats || 0n}\n                                due={veryShortTimeStamp(pendingItem.date)}\n                                genericAvatar\n                                primaryName={pendingItem.name_of_connection}\n                                approveAction={() => payItem(pendingItem)}\n                                rejectAction={() => rejectItem(pendingItem)}\n                                shouldSpinny={payList().includes(\n                                    pendingItem.id\n                                )}\n                            />\n                        )}\n                    </For>\n                </div>\n            </Show>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/Reader.tsx",
    "content": "import {\n    BarcodeFormat,\n    BarcodeScannedEvent,\n    BarcodeScanner,\n    PermissionStatus\n} from \"@capacitor-mlkit/barcode-scanning\";\nimport { Capacitor } from \"@capacitor/core\";\nimport QrScanner from \"qr-scanner\";\nimport { onCleanup, onMount } from \"solid-js\";\n\nexport function Reader(props: { onResult: (result: string) => void }) {\n    let container: HTMLVideoElement | undefined;\n    let scanner: QrScanner | undefined;\n\n    const handleResult = ({ data }: { data: string }) => {\n        props.onResult(data);\n    };\n\n    const startScan = async () => {\n        // Check camera permission\n        const permissions: PermissionStatus =\n            await BarcodeScanner.checkPermissions();\n        if (permissions.camera === \"granted\") {\n            await BarcodeScanner.startScan({ formats: [BarcodeFormat.QrCode] });\n\n            // The camera gets mounted behind everything so we need to make the background transparent\n            document.querySelector(\"html\")?.classList.add(\"bg-transparent\");\n\n            const listener = await BarcodeScanner.addListener(\n                \"barcodeScanned\",\n                // eslint-disable-next-line\n                async (result: BarcodeScannedEvent) => {\n                    document\n                        .querySelector(\"html\")\n                        ?.classList.remove(\"bg-transparent\");\n\n                    await BarcodeScanner.stopScan();\n                    await listener.remove();\n\n                    // if the result has content\n                    if (result && result.barcode) {\n                        handleResult({ data: result.barcode.rawValue }); // pass the raw scanned content\n                    }\n                }\n            );\n        } else if (permissions.camera === \"prompt\") {\n            // Request permission if it has not been asked before\n            const requestedPermissions: PermissionStatus =\n                await BarcodeScanner.requestPermissions();\n            if (requestedPermissions.camera === \"granted\") {\n                // If user grants permission, start the scan\n                await startScan();\n            }\n        } else if (permissions.camera === \"denied\") {\n            // Handle the scenario when user denies the permission\n            // Maybe show a user friendly message here\n            console.log(\"Camera permission was denied\");\n        }\n    };\n\n    const stopScan = async () => {\n        // Restore the background first to minimize flicker\n        document.querySelector(\"html\")?.classList.remove(\"bg-transparent\");\n        await BarcodeScanner.stopScan();\n        await BarcodeScanner.removeAllListeners();\n    };\n\n    onMount(async () => {\n        if (Capacitor.isNativePlatform()) {\n            await startScan();\n        } else {\n            if (container) {\n                scanner = new QrScanner(container, handleResult, {\n                    returnDetailedScanResult: true\n                });\n                // Set the inversion mode to scan both dark on light and light on dark\n                // This should make us more flexible in scanning QR codes\n                scanner.setInversionMode(\"both\");\n                await scanner.start();\n            }\n        }\n    });\n\n    onCleanup(async () => {\n        if (Capacitor.isNativePlatform()) {\n            await stopScan();\n        } else {\n            scanner?.destroy();\n        }\n    });\n\n    return (\n        <>\n            <div id=\"video-container\">\n                <video\n                    ref={container}\n                    class=\"fixed h-full w-full bg-transparent object-cover\"\n                />\n            </div>\n        </>\n    );\n}\n"
  },
  {
    "path": "src/components/ReceiveWarnings.tsx",
    "content": "import { createMemo, createResource, Match, Switch } from \"solid-js\";\n\nimport { InfoBox } from \"~/components/InfoBox\";\nimport { FeesModal } from \"~/components/MoreInfoModal\";\nimport { useI18n } from \"~/i18n/context\";\nimport { ReceiveFlavor } from \"~/routes\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function ReceiveWarnings(props: {\n    amountSats: bigint;\n    from_fedi_to_ln?: boolean;\n    flavor?: ReceiveFlavor;\n}) {\n    const i18n = useI18n();\n    const [state, _actions, sw] = useMegaStore();\n\n    const [inboundCapacity] = createResource(async () => {\n        try {\n            const channels = await sw.list_channels();\n            if (!channels) return 0n;\n\n            let inbound = 0n;\n\n            // PAIN: mutiny-wasm types say these are bigints, but they're actually numbers\n            for (const channel of channels) {\n                inbound +=\n                    BigInt(channel.size) -\n                    BigInt(channel.balance + channel.reserve);\n            }\n\n            return inbound;\n        } catch (e) {\n            console.error(e);\n            return 0n;\n        }\n    });\n\n    const warningText = () => {\n        if (state.federations?.length !== 0 && props.from_fedi_to_ln !== true) {\n            return undefined;\n        }\n        if (props.flavor === \"lightning\") {\n            if (\n                (state.balance?.lightning || 0n) === 0n &&\n                !state.settings?.lsps_connection_string\n            ) {\n                return i18n.t(\"receive.amount_editable.receive_too_small\", {\n                    amount: \"100,000\"\n                });\n            }\n\n            if (props.amountSats > (inboundCapacity() || 0n)) {\n                return i18n.t(\"receive.amount_editable.setup_fee_lightning\");\n            }\n        }\n\n        return undefined;\n    };\n\n    const sillyAmountWarning = () => {\n        const parsed = Number(props.amountSats);\n        if (isNaN(parsed)) {\n            return undefined;\n        }\n\n        if (parsed >= 2099999997690000) {\n            // If over 21 million bitcoin, warn that too much\n            return i18n.t(\"receive.amount_editable.more_than_21m\");\n        }\n    };\n\n    const tooSmallWarning = () => {\n        if (\n            props.flavor === \"onchain\" &&\n            props.amountSats > 0n &&\n            props.amountSats < 546n\n        ) {\n            return i18n.t(\"receive.error_under_min_onchain\");\n        }\n    };\n\n    const onChainFedi = createMemo(() => {\n        if (props.flavor === \"onchain\" && state.federations?.length) {\n            return true;\n        }\n    });\n\n    return (\n        <Switch>\n            <Match when={tooSmallWarning()}>\n                <InfoBox accent=\"red\">{tooSmallWarning()}</InfoBox>\n            </Match>\n            <Match when={sillyAmountWarning()}>\n                <InfoBox accent=\"red\">{sillyAmountWarning()}</InfoBox>\n            </Match>\n            <Match when={warningText()}>\n                <InfoBox accent=\"blue\">\n                    {warningText()} <FeesModal />\n                </InfoBox>\n            </Match>\n            <Match when={onChainFedi()}>\n                <InfoBox accent=\"blue\">\n                    {i18n.t(\"receive.warning_on_chain_fedi\")}\n                </InfoBox>\n            </Match>\n        </Switch>\n    );\n}\n"
  },
  {
    "path": "src/components/Reload.tsx",
    "content": "import { RotateCw, X } from \"lucide-solid\";\nimport { Show } from \"solid-js\";\n// eslint-disable-next-line import/no-unresolved\nimport { useRegisterSW } from \"virtual:pwa-register/solid\";\n\nimport { useI18n } from \"~/i18n/context\";\n\nimport { SmallHeader } from \"./layout\";\nimport { Button } from \"./layout/Button\";\n\nexport function ReloadPrompt() {\n    const i18n = useI18n();\n\n    // useRegisterSW can also return an offlineReady thingy\n    const {\n        needRefresh: [needRefresh, setNeedRefresh],\n        updateServiceWorker\n    } = useRegisterSW({\n        immediate: true,\n        onRegisteredSW(swUrl, r) {\n            console.log(\"SW Registered: \" + r?.scope);\n        },\n        onRegisterError(error: Error) {\n            console.log(\"SW registration error\", error);\n        }\n    });\n\n    const dismissPrompt = () => {\n        setNeedRefresh(false);\n    };\n\n    async function updateSw() {\n        await updateServiceWorker();\n    }\n\n    return (\n        <Show when={needRefresh()}>\n            <div class=\"grid grid-cols-[auto_minmax(0,_1fr)_auto] gap-4 rounded-xl bg-neutral-950/50 p-4\">\n                <div class=\"self-center\">\n                    <RotateCw class=\"h-8 w-8\" />\n                </div>\n                <div class=\"flex flex-row justify-between gap-4 max-md:items-center\">\n                    <div class=\"flex flex-col\">\n                        <SmallHeader>\n                            {i18n.t(\"reload.mutiny_update\")}\n                        </SmallHeader>\n                        <p class=\"text-base font-light max-md:hidden\">\n                            {i18n.t(\"reload.new_version_description\")}\n                        </p>\n                    </div>\n                    <div class=\"flex items-center\">\n                        <Button\n                            intent=\"blue\"\n                            layout=\"xs\"\n                            class=\"self-auto\"\n                            onClick={updateSw}\n                        >\n                            {i18n.t(\"reload.reload\")}\n                        </Button>\n                    </div>\n                </div>\n                <button\n                    tabindex=\"-1\"\n                    onClick={dismissPrompt}\n                    class=\"w-8 self-center rounded-lg hover:bg-white/10 active:bg-m-blue\"\n                >\n                    <X class=\"h-8 w-8\" />\n                </button>\n            </div>\n        </Show>\n    );\n}\n"
  },
  {
    "path": "src/components/Restart.tsx",
    "content": "import { createSignal } from \"solid-js\";\n\nimport { Button, InnerCard, NiceP, VStack } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function Restart() {\n    const i18n = useI18n();\n    const [_state, _actions, sw] = useMegaStore();\n    const [hasStopped, setHasStopped] = createSignal(false);\n\n    async function toggle() {\n        try {\n            if (hasStopped()) {\n                await sw.start();\n                setHasStopped(false);\n            } else {\n                await sw.stop();\n                setHasStopped(true);\n            }\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    return (\n        <InnerCard>\n            <VStack>\n                <NiceP>{i18n.t(\"error.restart.title\")}</NiceP>\n                <Button\n                    intent={hasStopped() ? \"green\" : \"red\"}\n                    onClick={toggle}\n                >\n                    {hasStopped()\n                        ? i18n.t(\"error.restart.start\")\n                        : i18n.t(\"error.restart.stop\")}\n                </Button>\n            </VStack>\n        </InnerCard>\n    );\n}\n"
  },
  {
    "path": "src/components/ResyncOnchain.tsx",
    "content": "import { Button, InnerCard, NiceP, VStack } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function ResyncOnchain() {\n    const i18n = useI18n();\n    const [_state, _actions, sw] = useMegaStore();\n\n    async function reset() {\n        try {\n            await sw.reset_onchain_tracker();\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    return (\n        <InnerCard>\n            <VStack>\n                <NiceP>{i18n.t(\"error.resync.incorrect_balance\")}</NiceP>\n                <Button intent=\"red\" onClick={reset}>\n                    {i18n.t(\"error.resync.resync_wallet\")}\n                </Button>\n            </VStack>\n        </InnerCard>\n    );\n}\n"
  },
  {
    "path": "src/components/SeedWords.tsx",
    "content": "import { Copy } from \"lucide-solid\";\nimport { createMemo, createSignal, For, Match, Switch } from \"solid-js\";\n\nimport { useI18n } from \"~/i18n/context\";\nimport { useCopy } from \"~/utils\";\n\nexport function SeedWords(props: {\n    words: string;\n    setHasSeen?: (hasSeen: boolean) => void;\n}) {\n    const i18n = useI18n();\n    const [shouldShow, setShouldShow] = createSignal(false);\n    const [copy, copied] = useCopy({ copiedTimeout: 1000 });\n\n    function toggleShow() {\n        setShouldShow(!shouldShow());\n        if (shouldShow()) {\n            props.setHasSeen?.(true);\n        }\n    }\n\n    const splitWords = createMemo(() => props.words.split(\" \"));\n\n    function dangerouslyCopy() {\n        copy(props.words);\n    }\n\n    return (\n        <div class=\"flex flex-col gap-4 overflow-hidden rounded-xl bg-m-red p-4\">\n            <Switch>\n                <Match when={!shouldShow()}>\n                    <div\n                        class=\"flex w-full cursor-pointer justify-center\"\n                        onClick={toggleShow}\n                    >\n                        <code class=\"text-red\">\n                            {i18n.t(\"settings.backup.seed_words.reveal\")}\n                        </code>\n                    </div>\n                </Match>\n\n                <Match when={shouldShow()}>\n                    <>\n                        <div\n                            class=\"flex w-full cursor-pointer justify-center\"\n                            onClick={toggleShow}\n                        >\n                            <code class=\"text-red\">\n                                {i18n.t(\"settings.backup.seed_words.hide\")}\n                            </code>\n                        </div>\n                        <ol class=\"w-full list-inside list-decimal columns-2 overflow-hidden\">\n                            <For each={splitWords()}>\n                                {(word) => (\n                                    <li class=\"bg min-w-fit text-left font-mono\">\n                                        {word}\n                                    </li>\n                                )}\n                            </For>\n                        </ol>\n                        <div class=\"flex w-full justify-center\">\n                            <button\n                                onClick={dangerouslyCopy}\n                                class=\"rounded-lg bg-white/10 p-2 hover:bg-white/20\"\n                            >\n                                <div class=\"flex items-center gap-2\">\n                                    <span>\n                                        {copied()\n                                            ? i18n.t(\n                                                  \"settings.backup.seed_words.copied\"\n                                              )\n                                            : i18n.t(\n                                                  \"settings.backup.seed_words.copy\"\n                                              )}\n                                    </span>\n                                    <Copy class=\"h-4 w-4\" />\n                                </div>\n                            </button>\n                        </div>\n                    </>\n                </Match>\n            </Switch>\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/SetupErrorDisplay.tsx",
    "content": "import { Title } from \"@solidjs/meta\";\nimport { MonitorSmartphone } from \"lucide-solid\";\nimport { createResource, Match, Switch } from \"solid-js\";\n\nimport nodevice from \"~/assets/no-device.png\";\nimport {\n    Button,\n    DefaultMain,\n    DeleteEverything,\n    ExternalLink,\n    ImportExport,\n    LargeHeader,\n    Logs,\n    NiceP,\n    SmallHeader\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport {\n    getSettings,\n    MutinyWalletSettingStrings\n} from \"~/logic/mutinyWalletSetup\";\nimport { FeedbackLink } from \"~/routes/Feedback\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nfunction ErrorFooter() {\n    return (\n        <>\n            <div class=\"h-full\" />\n            <div class=\"mt-4 self-center\">\n                <FeedbackLink setupError={true} />\n            </div>\n        </>\n    );\n}\n\nexport function SetupErrorDisplay(props: {\n    initialError: Error;\n    password?: string;\n}) {\n    // Error shouldn't be reactive, so we assign to it so it just gets rendered with the first value\n    const [_state, _actions, sw] = useMegaStore();\n    const i18n = useI18n();\n    const error = props.initialError;\n\n    const [lockSeconds, { mutate }] = createResource(async () => {\n        if (error.message.startsWith(\"Mutiny is already running\")) {\n            const settings: MutinyWalletSettingStrings = await getSettings();\n            try {\n                const secs = await sw.get_device_lock_remaining_secs(\n                    props.password,\n                    settings.auth,\n                    settings.storage\n                );\n                return Number(secs) || 0;\n            } catch (e) {\n                console.error(e);\n                return 60; // set to 60 if we fail to get the lock time\n            }\n        } else {\n            return 0;\n        }\n    });\n\n    // Countdown every second if we are displaying the device lock error\n    if (error.message.startsWith(\"Mutiny is already running\")) {\n        setInterval(async () => {\n            const current = lockSeconds();\n            if (current !== undefined) {\n                if (current > 0) {\n                    mutate(current - 1);\n                } else {\n                    window.location.reload();\n                }\n            }\n        }, 1000);\n    }\n\n    return (\n        <DefaultMain>\n            <Switch>\n                <Match\n                    when={error.message.startsWith(\"Network connection closed\")}\n                >\n                    <LargeHeader>\n                        {i18n.t(\"error.on_boot.loading_failed.header\")}\n                    </LargeHeader>\n                    <p class=\"rounded-xl bg-white/10 p-4 font-mono\">\n                        <span class=\"font-bold\">{error.name}</span>:{\" \"}\n                        {error.message}\n                    </p>\n                    <NiceP>\n                        {i18n.t(\"error.on_boot.loading_failed.services_down\")}\n                    </NiceP>\n                    <NiceP>\n                        Follow us on{\" \"}\n                        <ExternalLink href=\"https://primal.net/p/npub1mutnyacc9uc4t5mmxvpprwsauj5p2qxq95v4a9j0jxl8wnkfvuyque23vg\">\n                            Nostr\n                        </ExternalLink>{\" \"}\n                        or{\" \"}\n                        <ExternalLink href=\"https://twitter.com/MutinyWallet\">\n                            Twitter\n                        </ExternalLink>{\" \"}\n                        for updates.\n                    </NiceP>\n                    <NiceP>\n                        {i18n.t(\"error.on_boot.loading_failed.in_the_meantime\")}{\" \"}\n                        <a href=\"/?safe_mode=true\">\n                            {\" \"}\n                            {i18n.t(\"error.on_boot.loading_failed.safe_mode\")}\n                        </a>\n                        .\n                    </NiceP>\n\n                    <ErrorFooter />\n                </Match>\n                <Match when={error.message.startsWith(\"Existing tab\")}>\n                    <Title>{i18n.t(\"error.on_boot.existing_tab.title\")}</Title>\n                    <LargeHeader>\n                        {i18n.t(\"error.on_boot.existing_tab.title\")}\n                    </LargeHeader>\n                    <MonitorSmartphone class=\"mx-auto h-1/4 w-1/4 max-w-[25vh]\" />\n                    <NiceP>\n                        {i18n.t(\"error.on_boot.existing_tab.description\")}\n                    </NiceP>\n                    <Button onClick={() => window.location.reload()}>\n                        {i18n.t(\"error.reload\")}\n                    </Button>\n                    <ErrorFooter />\n                </Match>\n                <Match\n                    when={error.message.startsWith(\"Mutiny is already running\")}\n                >\n                    <Title>\n                        {i18n.t(\"error.on_boot.already_running.title\")}\n                    </Title>\n                    <LargeHeader>\n                        {i18n.t(\"error.on_boot.already_running.title\")}\n                    </LargeHeader>\n                    <img\n                        src={nodevice}\n                        alt=\"no device\"\n                        class=\"mx-auto w-1/4 max-w-[25vh] flex-shrink\"\n                    />\n                    <p class=\"rounded-xl bg-white/10 p-4 font-mono\">\n                        <span class=\"font-bold\">{error.name}</span>:{\" \"}\n                        {error.message}\n                    </p>\n                    <NiceP>\n                        {i18n.t(\"error.on_boot.already_running.description\")}\n                    </NiceP>\n                    <p class=\"rounded-xl bg-white/10 p-4 font-mono\">\n                        {i18n.t(\"error.on_boot.already_running.retry_again_in\")}{\" \"}\n                        {lockSeconds()}{\" \"}\n                        {i18n.t(\"error.on_boot.already_running.seconds\")}\n                    </p>\n                    <Button onClick={() => window.location.reload()}>\n                        {i18n.t(\"error.reload\")}\n                    </Button>\n                    <ErrorFooter />\n                </Match>\n                <Match when={error.message.startsWith(\"Browser error\")}>\n                    <Title>\n                        {i18n.t(\"error.on_boot.incompatible_browser.title\")}\n                    </Title>\n                    <LargeHeader>\n                        {i18n.t(\"error.on_boot.incompatible_browser.header\")}\n                    </LargeHeader>\n                    <p class=\"rounded-xl bg-white/10 p-4 font-mono\">\n                        <span class=\"font-bold\">{error.name}</span>:{\" \"}\n                        {error.message}\n                    </p>\n                    <NiceP>\n                        {i18n.t(\n                            \"error.on_boot.incompatible_browser.description\"\n                        )}\n                    </NiceP>\n                    <NiceP>\n                        {i18n.t(\n                            \"error.on_boot.incompatible_browser.try_different_browser\"\n                        )}\n                    </NiceP>\n                    <NiceP>\n                        {i18n.t(\n                            \"error.on_boot.incompatible_browser.browser_storage\"\n                        )}\n                    </NiceP>\n                    <ExternalLink href=\"https://github.com/MutinyWallet/mutiny-web/wiki/Browser-Compatibility\">\n                        {i18n.t(\n                            \"error.on_boot.incompatible_browser.browsers_link\"\n                        )}\n                    </ExternalLink>\n\n                    <ErrorFooter />\n                </Match>\n                <Match when={true}>\n                    <Title>\n                        {i18n.t(\"error.on_boot.loading_failed.title\")}\n                    </Title>\n                    <LargeHeader>\n                        {i18n.t(\"error.on_boot.loading_failed.header\")}\n                    </LargeHeader>\n                    <p class=\"rounded-xl bg-white/10 p-4 font-mono\">\n                        <span class=\"font-bold\">{error.name}</span>:{\" \"}\n                        {error.message}\n                    </p>\n                    <NiceP>\n                        {i18n.t(\"error.on_boot.loading_failed.description\")}\n                    </NiceP>\n                    <Button onClick={() => window.location.reload()}>\n                        {i18n.t(\"error.reload\")}\n                    </Button>\n                    <NiceP>\n                        {i18n.t(\"error.on_boot.loading_failed.repair_options\")}\n                    </NiceP>\n                    <NiceP>\n                        {i18n.t(\"error.on_boot.loading_failed.questions\")}{\" \"}\n                        <ExternalLink href=\"https://matrix.to/#/#mutiny-community:lightninghackers.com\">\n                            {i18n.t(\n                                \"error.on_boot.loading_failed.support_link\"\n                            )}\n                        </ExternalLink>\n                    </NiceP>\n                    <NiceP>\n                        {i18n.t(\"error.on_boot.loading_failed.in_the_meantime\")}{\" \"}\n                        <a href=\"/?safe_mode=true\">\n                            {\" \"}\n                            {i18n.t(\"error.on_boot.loading_failed.safe_mode\")}\n                        </a>\n                        .\n                    </NiceP>\n                    <ImportExport emergency />\n                    <Logs />\n                    <div class=\"flex flex-col gap-2 rounded-xl bg-m-red p-4\">\n                        <SmallHeader>\n                            {i18n.t(\"settings.danger_zone\")}\n                        </SmallHeader>\n                        <DeleteEverything emergency />\n                    </div>\n\n                    <ErrorFooter />\n                </Match>\n            </Switch>\n        </DefaultMain>\n    );\n}\n"
  },
  {
    "path": "src/components/ShareCard.tsx",
    "content": "import { Copy, Eye, Share } from \"lucide-solid\";\nimport { createSignal, Show } from \"solid-js\";\n\nimport { Card, JsonModal, VStack } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useCopy } from \"~/utils\";\n\nconst STYLE =\n    \"px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold hover:text-m-blue transition-colors\";\n\nfunction ShareButton(props: { receiveString: string; whiteBg?: boolean }) {\n    const i18n = useI18n();\n    async function share(receiveString: string) {\n        // If the browser doesn't support share we can just copy the address\n        if (!navigator.share) {\n            console.error(\"Share not supported\");\n        }\n        const shareData: ShareData = {\n            title: i18n.t(\"common.title\"),\n            text: receiveString\n        };\n        try {\n            await navigator.share(shareData);\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    return (\n        <button class={STYLE} onClick={(_) => share(props.receiveString)}>\n            <span>{i18n.t(\"modals.share\")}</span>\n            <Share />\n        </button>\n    );\n}\n\nexport function TruncateMiddle(props: {\n    text: string;\n    whiteBg?: boolean;\n    hidden?: boolean;\n}) {\n    return (\n        <div\n            class=\"flex font-mono\"\n            classList={{\n                \"text-black\": props.whiteBg\n            }}\n        >\n            <span class=\"truncate\">{props.text}</span>\n            <span class=\"pr-2\">\n                {props.text.length > 32 ? props.text.slice(-8) : \"\"}\n            </span>\n        </div>\n    );\n}\n\nexport function StringShower(props: { text: string }) {\n    const i18n = useI18n();\n    const [open, setOpen] = createSignal(false);\n    return (\n        <>\n            <JsonModal\n                open={open()}\n                plaintext={props.text}\n                title={i18n.t(\"modals.details\")}\n                setOpen={setOpen}\n            />\n            <div class=\"grid w-full grid-cols-[minmax(0,_1fr)_auto] items-center\">\n                <TruncateMiddle text={props.text} />\n                <button class=\"w-[2rem]\" onClick={() => setOpen(true)}>\n                    <Eye />\n                </button>\n            </div>\n        </>\n    );\n}\n\nexport function CopyButton(props: {\n    text?: string;\n    title?: string;\n    whiteBg?: boolean;\n}) {\n    const i18n = useI18n();\n    const [copy, copied] = useCopy({ copiedTimeout: 1000 });\n\n    function handleCopy() {\n        copy(props.text ?? \"\");\n    }\n\n    return (\n        <button class={STYLE} onClick={handleCopy}>\n            {copied()\n                ? i18n.t(\"common.copied\")\n                : props.title ?? i18n.t(\"common.copy\")}\n            <Copy />\n        </button>\n    );\n}\n\nexport function ShareCard(props: { text?: string }) {\n    return (\n        <Card>\n            <StringShower text={props.text ?? \"\"} />\n            <VStack>\n                <div class=\"flex justify-center gap-4\">\n                    <CopyButton text={props.text ?? \"\"} />\n                    <Show when={navigator.share}>\n                        <ShareButton receiveString={props.text ?? \"\"} />\n                    </Show>\n                </div>\n            </VStack>\n        </Card>\n    );\n}\n"
  },
  {
    "path": "src/components/SharpButton.tsx",
    "content": "import { JSX } from \"solid-js\";\n\nexport function SharpButton(props: {\n    onClick: () => void;\n    children: JSX.Element;\n    disabled?: boolean;\n}) {\n    return (\n        <button\n            onClick={() => props.onClick()}\n            disabled={props.disabled}\n            class=\"text-m-grey-300 flex items-center gap-2 rounded px-2 py-1 text-sm font-light md:text-base\"\n            classList={{\n                \"border-b border-t border-b-white/10 border-t-white/50 bg-m-grey-750 active:mt-[1px] active:-mb-[1px]\":\n                    !props.disabled\n            }}\n        >\n            {props.children}\n        </button>\n    );\n}\n"
  },
  {
    "path": "src/components/ShutdownPopup.tsx",
    "content": "import { createSignal } from \"solid-js\";\n\nimport { ExternalLink, NiceP, SimpleDialog } from \"~/components/layout\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function ShutdownPopup() {\n    const [state, actions, _sw] = useMegaStore();\n    const selfHosted =\n        state.settings?.selfhosted && state.settings?.selfhosted === \"true\";\n    const [showShutdownWarning, setShowShutdownWarning] = createSignal(\n        !selfHosted && !state.shutdown_warning_seen\n    );\n\n    const i18n = useI18n();\n\n    return (\n        <SimpleDialog\n            title={`${i18n.t(\"home.shutdown.title\")}`}\n            open={showShutdownWarning()}\n            setOpen={(open: boolean) => {\n                if (!open) {\n                    setShowShutdownWarning(false);\n                    actions.clearShutdownWarning();\n                }\n            }}\n        >\n            <NiceP>{i18n.t(\"home.shutdown.message\")}</NiceP>\n            <NiceP>\n                <ExternalLink href=\"https://blog.mutinywallet.com/mutiny-timeline/\">\n                    Learn more\n                </ExternalLink>\n            </NiceP>\n        </SimpleDialog>\n    );\n}\n"
  },
  {
    "path": "src/components/SimpleInput.tsx",
    "content": "import { JSX } from \"solid-js\";\n\ntype SimpleInputProps = {\n    type?: \"text\" | \"email\" | \"tel\" | \"password\" | \"url\" | \"date\";\n    placeholder?: string;\n    value: string | undefined;\n    disabled?: boolean;\n    onInput: JSX.EventHandler<\n        HTMLInputElement | HTMLTextAreaElement,\n        InputEvent\n    >;\n};\n\nexport function SimpleInput(props: SimpleInputProps) {\n    return (\n        <input\n            class=\"w-full rounded-lg bg-m-grey-800 p-2 placeholder-m-grey-400 disabled:text-m-grey-400\"\n            type={props.type || \"text\"}\n            value={props.value}\n            onInput={(e) => props.onInput(e)}\n            placeholder={props.placeholder}\n        />\n    );\n}\n"
  },
  {
    "path": "src/components/SocialActionRow.tsx",
    "content": "import { TagItem } from \"@mutinywallet/mutiny-wasm\";\nimport { cache, createAsync, useNavigate } from \"@solidjs/router\";\nimport { Scan, Search } from \"lucide-solid\";\nimport { For, Suspense } from \"solid-js\";\n\nimport { Circle, LabelCircle, showToast } from \"~/components\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function SocialActionRow(props: {\n    onSearch: () => void;\n    onScan: () => void;\n}) {\n    const [_state, actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n\n    const getContacts = cache(async () => {\n        try {\n            const contacts = await sw.get_contacts_sorted(40);\n            const myNpub = (await sw.get_npub()) || \"\";\n\n            // contact must have a npub, ln_address, or lnurl\n            return contacts.filter(\n                (contact) =>\n                    (contact.npub !== undefined ||\n                        contact.ln_address !== undefined ||\n                        contact.lnurl !== undefined) &&\n                    contact.npub !== myNpub\n            );\n        } catch (e) {\n            console.error(e);\n            return [];\n        }\n    }, \"contacts\");\n\n    const contacts = createAsync(() => getContacts(), { initialValue: [] });\n\n    const profileDeleted = createAsync(async () => {\n        const profile = await sw.get_nostr_profile();\n        return profile?.deleted;\n    });\n\n    // TODO this is mostly copy pasted from chat, could be a shared util maybe\n    async function sendToContact(contact?: TagItem) {\n        if (!contact) return;\n        const address = contact.ln_address || contact.lnurl;\n        if (address) {\n            await actions.handleIncomingString(\n                (address || \"\").trim(),\n                (error) => {\n                    showToast(error);\n                },\n                (result) => {\n                    actions.setScanResult({\n                        ...result,\n                        contact_id: contact.id\n                    });\n                    navigate(\"/send\");\n                }\n            );\n        } else {\n            console.error(\"no ln_address or lnurl\");\n        }\n    }\n\n    async function handleClick(contact: TagItem) {\n        if (profileDeleted() || !contact.npub) {\n            sendToContact(contact);\n        } else {\n            navigate(`/chat/${contact.id}`);\n        }\n    }\n\n    return (\n        <div class=\"-ml-4 -mr-4 flex gap-2 overflow-x-scroll py-[1px] pl-4\">\n            <Circle color=\"red\" onClick={props.onSearch}>\n                <Search />\n            </Circle>\n            <Circle onClick={props.onScan}>\n                <Scan />\n            </Circle>\n            <Suspense>\n                <For each={contacts()}>\n                    {(contact) => (\n                        <LabelCircle\n                            contact\n                            label={false}\n                            name={contact.name}\n                            image_url={contact.primal_image_url}\n                            onClick={() => handleClick(contact)}\n                        />\n                    )}\n                </For>\n            </Suspense>\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/components/Toaster.tsx",
    "content": "import { Toast, toaster } from \"@kobalte/core\";\nimport { X } from \"lucide-solid\";\nimport { Portal } from \"solid-js/web\";\n\nimport { SmallHeader } from \"~/components\";\n\nexport function Toaster() {\n    return (\n        <Portal>\n            <Toast.Region class=\"fixed left-0 right-0 top-0 z-[9999] flex w-full justify-center gap-4 safe-top safe-left safe-right safe-bottom\">\n                <Toast.List class=\"mt-8 flex w-[400px] max-w-[100vw] flex-col gap-4\" />\n            </Toast.Region>\n        </Portal>\n    );\n}\n\nexport type ToastArg = { title: string; description: string } | Error;\n\nexport function showToast(arg: ToastArg) {\n    if (arg instanceof Error) {\n        return toaster.show((props) => (\n            <ToastItem\n                title=\"Error\"\n                description={arg.message}\n                isError\n                {...props}\n            />\n        ));\n    } else {\n        return toaster.show((props) => (\n            <ToastItem\n                title={arg.title}\n                description={arg.description}\n                {...props}\n            />\n        ));\n    }\n}\n\nfunction ToastItem(props: {\n    toastId: number;\n    title: string;\n    description: string;\n    isError?: boolean;\n}) {\n    return (\n        <Toast.Root\n            toastId={props.toastId}\n            class={`mx-auto w-[80vw] max-w-[400px] rounded-xl border bg-neutral-900/80 p-4 shadow-xl backdrop-blur-md ${\n                props.isError ? \"border-m-red/50\" : \"border-white/10\"\n            } `}\n        >\n            <div class=\"flex w-full items-start justify-between gap-4\">\n                <div class=\"flex-1\">\n                    <Toast.Title>\n                        <SmallHeader>{props.title}</SmallHeader>\n                    </Toast.Title>\n                    <Toast.Description>\n                        <p>{props.description}</p>\n                    </Toast.Description>\n                </div>\n                <Toast.CloseButton class=\"flex-0 flex h-8 w-8 items-center justify-center rounded-lg hover:bg-white/10 active:bg-m-blue\">\n                    <X />\n                </Toast.CloseButton>\n            </div>\n        </Toast.Root>\n    );\n}\n"
  },
  {
    "path": "src/components/ToggleHodl.tsx",
    "content": "import { Button, InnerCard, NiceP, VStack } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function ToggleHodl() {\n    const i18n = useI18n();\n    const [state, actions] = useMegaStore();\n\n    async function toggle() {\n        try {\n            await actions.toggleHodl();\n            window.location.href = \"/\";\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    return (\n        <InnerCard\n            title={i18n.t(\"settings.admin.kitchen_sink.enable_zaps_to_hodl\")}\n        >\n            <VStack>\n                <NiceP>\n                    {i18n.t(\"settings.admin.kitchen_sink.zaps_to_hodl_desc\")}\n                </NiceP>\n\n                <Button\n                    intent={state.should_zap_hodl ? \"green\" : \"red\"}\n                    onClick={toggle}\n                >\n                    {state.should_zap_hodl\n                        ? i18n.t(\n                              \"settings.admin.kitchen_sink.zaps_to_hodl_disable\"\n                          )\n                        : i18n.t(\n                              \"settings.admin.kitchen_sink.zaps_to_hodl_enable\"\n                          )}\n                </Button>\n            </VStack>\n        </InnerCard>\n    );\n}\n"
  },
  {
    "path": "src/components/index.ts",
    "content": "export * from \"./layout\";\nexport * from \"./successfail\";\n\nexport * from \"./Activity\";\nexport * from \"./ActivityDetailsModal\";\nexport * from \"./Amount\";\nexport * from \"./AmountEditable\";\nexport * from \"./BalanceBox\";\nexport * from \"./ChooseCurrency\";\nexport * from \"./ChooseLanguage\";\nexport * from \"./ContactEditor\";\nexport * from \"./ContactForm\";\nexport * from \"./ContactViewer\";\nexport * from \"./DecryptDialog\";\nexport * from \"./DeleteEverything\";\nexport * from \"./ErrorDisplay\";\nexport * from \"./Fee\";\nexport * from \"./I18nProvider\";\nexport * from \"./ImportExport\";\nexport * from \"./InfoBox\";\nexport * from \"./IntegratedQR\";\nexport * from \"./JsonModal\";\nexport * from \"./KitchenSink\";\nexport * from \"./LoadingIndicator\";\nexport * from \"./Logo\";\nexport * from \"./Logs\";\nexport * from \"./MoreInfoModal\";\nexport * from \"./NavBar\";\nexport * from \"./PendingNwc\";\nexport * from \"./Reader\";\nexport * from \"./Reload\";\nexport * from \"./Restart\";\nexport * from \"./ResyncOnchain\";\nexport * from \"./SeedWords\";\nexport * from \"./SetupErrorDisplay\";\nexport * from \"./Failure\";\nexport * from \"./ShareCard\";\nexport * from \"./Toaster\";\nexport * from \"./NostrActivity\";\nexport * from \"./MutinyPlusCta\";\nexport * from \"./ToggleHodl\";\nexport * from \"./IOSbanner\";\nexport * from \"./HomePrompt\";\nexport * from \"./BigMoney\";\nexport * from \"./FeeDisplay\";\nexport * from \"./ReceiveWarnings\";\nexport * from \"./SimpleInput\";\nexport * from \"./LabelCircle\";\nexport * from \"./SharpButton\";\nexport * from \"./HomeSubnav\";\nexport * from \"./SocialActionRow\";\nexport * from \"./ContactButton\";\nexport * from \"./GenericItem\";\nexport * from \"./HomeBalance\";\nexport * from \"./EditProfileForm\";\nexport * from \"./ImportNsecForm\";\nexport * from \"./LightningAddressShower\";\nexport * from \"./FederationInviteShower\";\nexport * from \"./FederationPopup\";\nexport * from \"./ShutdownPopup\";\n"
  },
  {
    "path": "src/components/layout/BackLink.tsx",
    "content": "import { A } from \"@solidjs/router\";\nimport { ChevronLeft } from \"lucide-solid\";\n\nimport { useI18n } from \"~/i18n/context\";\n\nexport function BackLink(props: {\n    href?: string;\n    title?: string;\n    showOnDesktop?: boolean;\n}) {\n    const i18n = useI18n();\n    return (\n        <A\n            href={props.href ? props.href : \"/\"}\n            class=\"-mx-2 flex items-center text-xl font-semibold text-white no-underline active:-mb-[1px] active:mt-[1px] active:text-white/80 md:hidden\"\n            classList={{ \"md:!flex\": props.showOnDesktop }}\n        >\n            <ChevronLeft class=\"h-7 w-7\" />\n            {props.title ? props.title : i18n.t(\"common.home\")}\n        </A>\n    );\n}\n\nexport function BackButton(props: {\n    onClick: () => void;\n    title?: string;\n    showOnDesktop?: boolean;\n}) {\n    const i18n = useI18n();\n    return (\n        <button\n            onClick={() => props.onClick()}\n            class=\"-mx-2 flex items-center text-xl font-semibold text-white no-underline active:-mb-[1px] active:mt-[1px] active:text-white/80 md:hidden\"\n            classList={{ \"md:!flex\": props.showOnDesktop }}\n        >\n            <ChevronLeft class=\"h-7 w-7\" />\n            {props.title !== undefined ? props.title : i18n.t(\"common.home\")}\n        </button>\n    );\n}\n"
  },
  {
    "path": "src/components/layout/BackPop.tsx",
    "content": "import { useLocation, useNavigate } from \"@solidjs/router\";\nimport { JSXElement } from \"solid-js\";\n\nimport { BackButton } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nexport type StateWithPrevious = {\n    previous?: string;\n};\n\nexport function BackPop(props: { default: string; title?: string }) {\n    const i18n = useI18n();\n    const navigate = useNavigate();\n    const location = useLocation();\n\n    function getBackPath() {\n        const state = location.state as StateWithPrevious;\n\n        // If there's no previous state want to just go back one level, basically ../\n        const newBackPath = props.default\n            ? props.default\n            : location.pathname.split(\"/\").slice(0, -1).join(\"/\");\n\n        const backPath = state?.previous ? state?.previous : newBackPath;\n        return backPath;\n    }\n\n    const backPath = () => getBackPath();\n\n    return (\n        <BackButton\n            title={\n                props.title !== undefined\n                    ? props.title\n                    : backPath() === \"/\"\n                      ? i18n.t(\"common.home\")\n                      : i18n.t(\"common.back\")\n            }\n            onClick={() => navigate(backPath())}\n            showOnDesktop\n        />\n    );\n}\n\nexport function UnstyledBackPop(props: {\n    default: string;\n    children: JSXElement;\n}) {\n    const i18n = useI18n();\n    const navigate = useNavigate();\n    const location = useLocation();\n\n    function getBackPath() {\n        const state = location.state as StateWithPrevious;\n\n        // If there's no previous state want to just go back one level, basically ../\n        const newBackPath = props.default\n            ? props.default\n            : location.pathname.split(\"/\").slice(0, -1).join(\"/\");\n\n        const backPath = state?.previous ? state?.previous : newBackPath;\n        return backPath;\n    }\n\n    const backPath = () => getBackPath();\n\n    return (\n        <button\n            title={\n                backPath() === \"/\"\n                    ? i18n.t(\"common.home\")\n                    : i18n.t(\"common.back\")\n            }\n            onClick={() => {\n                console.log(\"backPath\", backPath());\n                navigate(backPath());\n            }}\n        >\n            {props.children}\n        </button>\n    );\n}\n"
  },
  {
    "path": "src/components/layout/Button.tsx",
    "content": "import { A } from \"@solidjs/router\";\nimport { JSX, ParentComponent, Show, splitProps } from \"solid-js\";\nimport { Dynamic } from \"solid-js/web\";\n\nimport { LoadingSpinner } from \"~/components\";\n\n// Help from https://github.com/arpadgabor/credee/blob/main/packages/www/src/components/ui/button.tsx\n\ntype CommonButtonStyleProps = {\n    intent?: \"active\" | \"inactive\" | \"blue\" | \"red\" | \"green\" | \"text\";\n    layout?: \"flex\" | \"pad\" | \"small\" | \"xs\" | \"full\";\n};\n\ninterface ButtonProps\n    extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n        CommonButtonStyleProps {\n    loading?: boolean;\n    disabled?: boolean;\n}\n\ninterface ButtonLinkProps\n    extends JSX.ButtonHTMLAttributes<HTMLAnchorElement>,\n        CommonButtonStyleProps {\n    href: string;\n    target?: string;\n    rel?: string;\n}\n\nexport const Button: ParentComponent<ButtonProps> = (props) => {\n    const [local, attrs] = splitProps(props, [\"children\", \"intent\", \"layout\"]);\n    return (\n        <button\n            {...attrs}\n            disabled={props.disabled || props.loading}\n            class=\"rounded-xl p-3 font-semibold transition active:-mb-[2px] active:mt-[2px] \"\n            classList={{\n                \"disabled:bg-neutral-400/10 disabled:text-white/20 disabled:shadow-inner-button-disabled\":\n                    local.intent !== \"text\",\n                \"bg-white text-black\": local.intent === \"active\",\n                \"bg-m-grey-800 text-white shadow-inner-button text-shadow-button\":\n                    !local.intent || local.intent === \"inactive\",\n                \"hover:text-m-grey-300 hover:bg-m-grey-900 bg-m-grey-800 text-white shadow-inner-button text-shadow-button\":\n                    !local.intent || !!local.intent.match(/(active|inactive)/),\n                \"bg-m-blue hover:bg-m-blue-dark\": local.intent === \"blue\",\n                \"bg-m-red hover:bg-m-red-dark\": local.intent === \"red\",\n                \"bg-m-green hover:bg-m-green-dark\": local.intent === \"green\",\n                \"text-white shadow-inner-button text-shadow-button\":\n                    local.intent && !!local.intent.match(/(blue|red|green)/),\n                \"\": local.intent === \"text\",\n                \"flex-1 text-xl\": !local.layout || local.layout === \"flex\",\n                \"px-8 text-xl\": local.layout === \"pad\",\n                \"px-4 py-2 w-auto text-lg\": local.layout === \"small\",\n                \"px-4 py-2 w-auto rounded-lg text-base\": local.layout === \"xs\",\n                \"w-full text-xl\": local.layout === \"full\"\n            }}\n        >\n            <Show when={props.loading} fallback={local.children}>\n                <div class=\"flex justify-center\">\n                    {/* TODO: constrain this to the exact height of the button */}\n                    <Show when={local.layout !== \"xs\"}>\n                        <LoadingSpinner wide />\n                    </Show>\n                    <Show when={local.layout === \"xs\"}>\n                        <span>...</span>\n                    </Show>\n                </div>\n            </Show>\n        </button>\n    );\n};\n\nexport const ButtonLink: ParentComponent<ButtonLinkProps> = (props) => {\n    const [local, attrs] = splitProps(props, [\n        \"children\",\n        \"intent\",\n        \"layout\",\n        \"href\",\n        \"target\",\n        \"rel\"\n    ]);\n    return (\n        <Dynamic\n            {...attrs}\n            component={local.href?.includes(\"://\") ? \"a\" : A}\n            href={local.href}\n            target={local.target}\n            rel={local.rel}\n            class=\"flex justify-center rounded-xl p-3 font-semibold no-underline transition disabled:bg-neutral-400/10 disabled:text-white/20 disabled:shadow-inner-button-disabled\"\n            classList={{\n                \"bg-white text-black\": local.intent === \"active\",\n                \"bg-m-grey-800 text-white shadow-inner-button text-shadow-button\":\n                    !local.intent || local.intent === \"inactive\",\n                \"hover:text-m-grey-300 hover:bg-m-grey-900 bg-m-grey-800 text-white shadow-inner-button text-shadow-button\":\n                    !local.intent || !!local.intent.match(/(active|inactive)/),\n                \"bg-m-blue hover:bg-m-blue-dark\": local.intent === \"blue\",\n                \"bg-m-red hover:bg-m-red-dark\": local.intent === \"red\",\n                \"bg-m-green hover:bg-m-green-dark\": local.intent === \"green\",\n                \"text-white shadow-inner-button text-shadow-button\":\n                    local.intent && !!local.intent.match(/(blue|red|green)/),\n                \"\": local.intent === \"text\",\n                \"flex-1 text-xl\": !local.layout || local.layout === \"flex\",\n                \"px-8 text-xl\": local.layout === \"pad\",\n                \"px-4 py-2 w-auto text-lg\": local.layout === \"small\",\n                \"px-4 py-2 w-auto rounded-lg text-base\": local.layout === \"xs\",\n                \"w-full text-xl\": local.layout === \"full\"\n            }}\n        >\n            {local.children}\n        </Dynamic>\n    );\n};\n"
  },
  {
    "path": "src/components/layout/ExternalLink.tsx",
    "content": "import { ParentComponent } from \"solid-js\";\n\nexport const ExternalLink: ParentComponent<{ href: string }> = (props) => {\n    return (\n        <a target=\"_blank\" rel=\"noopener noreferrer\" href={props.href}>\n            {props.children}\n            <svg\n                class=\"inline-block pl-0.5\"\n                width=\"16\"\n                height=\"16\"\n                fill=\"none\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n            >\n                <path\n                    d=\"M6.00002 3.33337v1.33334H10.39L2.66669 12.39l.94333.9434 7.72338-7.72336V10h1.3333V3.33337H6.00002Z\"\n                    fill=\"currentColor\"\n                />\n            </svg>\n        </a>\n    );\n};\n"
  },
  {
    "path": "src/components/layout/LoadingSpinner.tsx",
    "content": "export const LoadingSpinner = (props: {\n    big?: boolean;\n    wide?: boolean;\n    small?: boolean;\n    smallest?: boolean;\n}) => {\n    return (\n        <div\n            role=\"status\"\n            classList={{\n                \"w-24\": !props.small && !props.smallest,\n                \"w-16\": props.small,\n                \"w-8 h-8\": props.smallest,\n                \"flex justify-center\": props.wide,\n                \"h-full grid\": props.big\n            }}\n        >\n            <svg\n                class=\"mr-2 h-8 w-8 animate-spin place-self-center fill-m-red text-gray-200\"\n                viewBox=\"0 0 100 101\"\n                fill=\"none\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n            >\n                <path\n                    d=\"M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z\"\n                    fill=\"currentColor\"\n                />\n                <path\n                    d=\"M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z\"\n                    fill=\"currentFill\"\n                />\n            </svg>\n        </div>\n    );\n};\n"
  },
  {
    "path": "src/components/layout/Misc.tsx",
    "content": "import {\n    Collapsible,\n    Dialog,\n    Checkbox as KCheckbox,\n    Separator\n} from \"@kobalte/core\";\nimport { TagItem, TagKind } from \"@mutinywallet/mutiny-wasm\";\nimport { A } from \"@solidjs/router\";\nimport { Check, ChevronDown, X } from \"lucide-solid\";\nimport {\n    createResource,\n    createSignal,\n    JSX,\n    Match,\n    ParentComponent,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    Button,\n    DecryptDialog,\n    LoadingIndicator,\n    LoadingSpinner\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { generateGradient } from \"~/utils\";\n\nexport const SmallHeader: ParentComponent<{ class?: string }> = (props) => {\n    return (\n        <header class={`text-sm font-semibold uppercase ${props.class}`}>\n            {props.children}\n        </header>\n    );\n};\n\nexport const Card: ParentComponent<{\n    title?: string | null;\n    titleElement?: JSX.Element;\n}> = (props) => {\n    return (\n        <div class=\"flex w-full flex-col gap-2 rounded-xl bg-neutral-900 p-4\">\n            {props.title && <SmallHeader>{props.title}</SmallHeader>}\n            {props.titleElement && props.titleElement}\n            {props.children}\n        </div>\n    );\n};\n\nexport const ButtonCard: ParentComponent<{\n    onClick: () => void;\n    red?: boolean;\n}> = (props) => {\n    return (\n        <button\n            onClick={() => props.onClick()}\n            class=\"flex w-full rounded-xl border border-white/10  p-4 active:-mb-[1px] active:mt-[1px] active:opacity-70\"\n            classList={{\n                \"bg-neutral-900\": !props.red,\n                \"bg-m-red\": props.red\n            }}\n        >\n            {props.children}\n        </button>\n    );\n};\n\nexport const InnerCard: ParentComponent<{ title?: string }> = (props) => {\n    return (\n        <div class=\"flex flex-col gap-2 rounded-xl border border-white/10 bg-[rgba(255,255,255,0.05)] p-4\">\n            {props.title && <SmallHeader>{props.title}</SmallHeader>}\n            {props.children}\n        </div>\n    );\n};\n\nexport const FancyCard: ParentComponent<{\n    title?: string;\n    subtitle?: string;\n    tag?: JSX.Element;\n}> = (props) => {\n    return (\n        <VStack smallgap>\n            <Show when={props.title}>\n                <div class=\"mt-2 pl-4\">\n                    <SmallHeader>{props.title}</SmallHeader>\n                </div>\n            </Show>\n            <div class=\"flex flex-col gap-2 rounded-xl border border-b-4 border-black/50 bg-m-grey-900 p-4 shadow-fancy-card\">\n                {props.children}\n            </div>\n        </VStack>\n    );\n};\n\nexport const SettingsCard: ParentComponent<{\n    title?: string;\n}> = (props) => {\n    return (\n        <VStack smallgap>\n            <div class=\"mt-2 pl-4\">\n                <SmallHeader>{props.title}</SmallHeader>\n            </div>\n            <div class=\"flex w-full flex-col gap-2 rounded-xl bg-m-grey-900 py-4\">\n                {props.children}\n            </div>\n        </VStack>\n    );\n};\n\nexport const Collapser: ParentComponent<{\n    title: string;\n    defaultOpen?: boolean;\n    activityLight?: \"on\" | \"off\";\n}> = (props) => {\n    return (\n        <Collapsible.Root defaultOpen={props.defaultOpen} class=\"collapsible\">\n            <Collapsible.Trigger class=\"flex w-full justify-between px-4 py-2 hover:bg-m-grey-750 active:bg-m-grey-900\">\n                <div class=\"flex items-center gap-2\">\n                    <Switch>\n                        <Match when={props.activityLight === \"on\"}>\n                            <div class=\"h-2 w-2 rounded-full bg-m-green\" />\n                        </Match>\n                        <Match when={props.activityLight === \"off\"}>\n                            <div class=\"h-2 w-2 rounded-full bg-m-red\" />\n                        </Match>\n                    </Switch>\n                    <span>{props.title}</span>\n                </div>\n                <ChevronDown />\n            </Collapsible.Trigger>\n            <Collapsible.Content class=\"bg-m-grey-950 p-4 shadow-inner\">\n                {props.children}\n            </Collapsible.Content>\n        </Collapsible.Root>\n    );\n};\n\nexport const DefaultMain = (props: { children?: JSX.Element }) => {\n    return (\n        <>\n            {/* blur content that goes under the notification bar */}\n            <div class=\"relative\">\n                <div class=\"fixed left-0 right-0 top-0 z-50 bg-m-grey-975/70 backdrop-blur-lg safe-top\" />\n            </div>\n            <main class=\"flex h-full flex-1 flex-col gap-4 px-4 pb-8 pt-4\">\n                {props.children}\n                <div class=\"h-4\" />\n            </main>\n        </>\n    );\n};\n\nconst FullscreenLoader = () => {\n    const i18n = useI18n();\n    const [waitedTooLong, setWaitedTooLong] = createSignal(false);\n\n    setTimeout(() => {\n        setWaitedTooLong(true);\n    }, 10000);\n\n    return (\n        <div class=\"flex w-full flex-col items-center justify-center gap-4 h-device\">\n            <LoadingSpinner wide />\n            <Show when={waitedTooLong()}>\n                <p class=\"max-w-[20rem] text-m-grey-350\">\n                    {i18n.t(\"error.load_time.stuck\")}{\" \"}\n                    <A class=\"text-white\" href=\"/settings/emergencykit\">\n                        {i18n.t(\"error.emergency_link\")}\n                    </A>\n                </p>\n            </Show>\n        </div>\n    );\n};\n\nexport const MutinyWalletGuard: ParentComponent = (props) => {\n    const [state] = useMegaStore();\n\n    return (\n        <Suspense fallback={<FullscreenLoader />}>\n            <Switch>\n                <Match\n                    when={!state.wallet_loading && state.load_stage === \"done\"}\n                >\n                    {props.children}\n                </Match>\n                <Match when={true}>\n                    <DefaultMain>\n                        <LoadingIndicator />\n                    </DefaultMain>\n                </Match>\n            </Switch>\n            <DecryptDialog />\n        </Suspense>\n    );\n};\n\nexport const Hr = () => <Separator.Root class=\"my-4 border-m-grey-750\" />;\n\nexport const KeyValue: ParentComponent<{ key: string }> = (props) => {\n    return (\n        <li class=\"flex items-center justify-between gap-6\">\n            <span class=\"min-w-max text-sm font-semibold uppercase text-m-grey-400\">\n                {props.key}\n            </span>\n            <span class=\"truncate font-light\">{props.children}</span>\n        </li>\n    );\n};\n\nexport const LargeHeader: ParentComponent<{\n    action?: JSX.Element;\n    centered?: boolean;\n}> = (props) => {\n    return (\n        <header\n            class=\"mb-2 mt-2 flex w-full items-center justify-between\"\n            classList={{\n                \"justify-between\": !props.centered,\n                \"justify-center\": props.centered\n            }}\n        >\n            <h1\n                class=\"text-2xl font-semibold\"\n                classList={{\n                    \"text-center\": props.centered\n                }}\n            >\n                {props.children}\n            </h1>\n            <Show when={props.action}>{props.action}</Show>\n        </header>\n    );\n};\n\nexport const MediumHeader: ParentComponent = (props) => {\n    return (\n        <header class=\"mt-2\">\n            <h2 class=\"text-xl font-semibold text-m-grey-350\">\n                {props.children}\n            </h2>\n        </header>\n    );\n};\n\nexport const VStack: ParentComponent<{\n    biggap?: boolean;\n    smallgap?: boolean;\n}> = (props) => {\n    return (\n        <div\n            class=\"flex flex-col\"\n            classList={{\n                \"gap-2\": props.smallgap,\n                \"gap-8\": props.biggap,\n                \"gap-4\": !props.biggap && !props.smallgap\n            }}\n        >\n            {props.children}\n        </div>\n    );\n};\n\nexport const Spacer = () => {\n    return <div class=\"h-4 w-4\" />;\n};\n\nexport const NiceP: ParentComponent = (props) => {\n    return <p class=\"text-xl font-light text-neutral-200\">{props.children}</p>;\n};\n\nexport const TinyText: ParentComponent = (props) => {\n    return <p class=\"text-sm text-m-grey-350\">{props.children}</p>;\n};\n\nexport const TinyButton: ParentComponent<{\n    onClick: () => void;\n    tag?: TagItem;\n}> = (props) => {\n    // TODO: don't need to run this if it's not a contact\n    const [gradient] = createResource(async () => {\n        return generateGradient(props.tag?.name || \"?\");\n    });\n\n    const bg = () =>\n        props.tag?.name && props.tag?.kind === TagKind.Contact\n            ? gradient()\n            : \"rgb(255 255 255 / 0.1)\";\n\n    return (\n        <button\n            class=\"rounded-lg bg-white/10 px-2 py-1\"\n            onClick={() => props.onClick()}\n            style={{ background: bg() }}\n        >\n            {props.children}\n        </button>\n    );\n};\n\nexport const Indicator: ParentComponent = (props) => {\n    return (\n        <div class=\"-my-1 box-border animate-pulse rounded bg-white/70 px-2 py-1 text-xs uppercase text-black\">\n            {props.children}\n        </div>\n    );\n};\n\nexport function Checkbox(props: {\n    label: string;\n    checked: boolean;\n    onChange: (checked: boolean) => void;\n    caption?: string;\n}) {\n    return (\n        <KCheckbox.Root\n            class=\"inline-flex gap-2\"\n            classList={{\n                \"items-center\": !props.caption,\n                \"items-start\": !!props.caption\n            }}\n            checked={props.checked}\n            onChange={props.onChange}\n        >\n            <KCheckbox.Input class=\"\" />\n            <KCheckbox.Control class=\"flex-0 flex h-8 w-8 items-center justify-center rounded-lg border-2 border-white bg-neutral-800 ui-checked:bg-m-red\">\n                <KCheckbox.Indicator>\n                    <Check class=\"h-6 w-6\" />\n                </KCheckbox.Indicator>\n            </KCheckbox.Control>\n            <KCheckbox.Label class=\"flex flex-1 flex-col gap-1 font-semibold\">\n                {props.label}\n                <Show when={props.caption}>\n                    <TinyText>{props.caption}</TinyText>\n                </Show>\n            </KCheckbox.Label>\n        </KCheckbox.Root>\n    );\n}\n\nexport function ModalCloseButton() {\n    return (\n        <button class=\"flex h-8 w-8 items-center justify-center self-center justify-self-center rounded-lg hover:bg-white/10 active:bg-m-blue\">\n            <X class=\"h-6 w-6\" />\n        </button>\n    );\n}\n\nexport const SIMPLE_OVERLAY = \"fixed inset-0 z-50 bg-black/50 backdrop-blur-lg\";\nexport const SIMPLE_DIALOG_POSITIONER =\n    \"fixed inset-0 z-50 flex items-center justify-center\";\nexport const SIMPLE_DIALOG_CONTENT =\n    \"max-w-[500px] w-[90vw] max-h-device overflow-y-scroll disable-scrollbars mx-4 p-4 bg-neutral-800/90 rounded-xl border border-white/10\";\n\nexport const SimpleDialog: ParentComponent<{\n    title: string;\n    open: boolean;\n    setOpen?: (open: boolean) => void;\n}> = (props) => {\n    return (\n        <Dialog.Root\n            open={props.open}\n            onOpenChange={props.setOpen && props.setOpen}\n            modal={true}\n        >\n            <Dialog.Portal>\n                <Dialog.Overlay class={SIMPLE_OVERLAY} />\n                <div class={SIMPLE_DIALOG_POSITIONER}>\n                    <Dialog.Content class={SIMPLE_DIALOG_CONTENT} tabIndex={0}>\n                        <div class=\"mb-2 flex items-center justify-between\">\n                            <Dialog.Title>\n                                <SmallHeader>{props.title}</SmallHeader>\n                            </Dialog.Title>\n                            <Show when={props.setOpen}>\n                                <Dialog.CloseButton>\n                                    <ModalCloseButton />\n                                </Dialog.CloseButton>\n                            </Show>\n                        </div>\n                        <Dialog.Description class=\"flex flex-col gap-4\">\n                            {props.children}\n                        </Dialog.Description>\n                    </Dialog.Content>\n                </div>\n            </Dialog.Portal>\n        </Dialog.Root>\n    );\n};\n\nexport const ConfirmDialog: ParentComponent<{\n    open: boolean;\n    loading: boolean;\n    onCancel: () => void;\n    onConfirm: () => void;\n}> = (props) => {\n    const i18n = useI18n();\n    return (\n        <Dialog.Root open={props.open} onOpenChange={props.onCancel}>\n            <Dialog.Portal>\n                <Dialog.Overlay class={SIMPLE_OVERLAY} />\n                <div class={SIMPLE_DIALOG_POSITIONER}>\n                    <Dialog.Content class={SIMPLE_DIALOG_CONTENT}>\n                        <div class=\"mb-2 flex justify-between\">\n                            <Dialog.Title>\n                                <SmallHeader>\n                                    {i18n.t(\n                                        \"modals.confirm_dialog.are_you_sure\"\n                                    )}\n                                </SmallHeader>\n                            </Dialog.Title>\n                        </div>\n                        <Dialog.Description class=\"flex flex-col gap-4\">\n                            {props.children}\n                            <div class=\"flex w-full justify-end gap-4\">\n                                <Button layout=\"small\" onClick={props.onCancel}>\n                                    {i18n.t(\"modals.confirm_dialog.cancel\")}\n                                </Button>\n                                <Button\n                                    layout=\"small\"\n                                    intent=\"red\"\n                                    onClick={props.onConfirm}\n                                    loading={props.loading}\n                                    disabled={props.loading}\n                                >\n                                    {i18n.t(\"modals.confirm_dialog.confirm\")}\n                                </Button>\n                            </div>\n                        </Dialog.Description>\n                    </Dialog.Content>\n                </div>\n            </Dialog.Portal>\n        </Dialog.Root>\n    );\n};\n"
  },
  {
    "path": "src/components/layout/Radio.tsx",
    "content": "import { RadioGroup } from \"@kobalte/core\";\nimport { createSignal, For, Show } from \"solid-js\";\n\nimport { timeout } from \"~/utils\";\n\ntype Choices = {\n    value: string;\n    label: string;\n    caption: string;\n    disabled?: boolean;\n}[];\n\n// TODO: how could would it be if we could just pass the estimated fees in here?\nexport function StyledRadioGroup(props: {\n    initialValue: string;\n    choices: Choices;\n    onValueChange: (value: string) => void;\n    small?: boolean;\n    accent?: \"red\" | \"white\";\n    vertical?: boolean;\n    delayOnChange?: boolean;\n}) {\n    const [value, setValue] = createSignal(props.initialValue);\n\n    async function onChange(value: string) {\n        setValue(value);\n\n        // This is so if the modal is going to be closed after value change, it will show the choice briefly first\n        if (props.delayOnChange) {\n            await timeout(200);\n            props.onValueChange(value);\n        } else {\n            props.onValueChange(value);\n        }\n    }\n    return (\n        <RadioGroup.Root\n            value={value()}\n            onChange={onChange}\n            class={\"w-full gap-4\"}\n            classList={{\n                \"flex flex-col\": props.vertical,\n                \"grid grid-cols-2\":\n                    props.choices.length === 2 && !props.vertical,\n                \"grid grid-cols-3\":\n                    props.choices.length === 3 && !props.vertical,\n                \"gap-2\": props.small\n            }}\n        >\n            <For each={props.choices}>\n                {(choice) => (\n                    <RadioGroup.Item\n                        value={choice.value}\n                        class={`rounded-lg bg-m-grey-700 ui-checked:outline`}\n                        classList={{\n                            \"ui-checked:outline-m-red\": props.accent === \"red\",\n                            \"ui-checked:outline-white\":\n                                props.accent === \"white\",\n                            \"ui-disabled:opacity-50\": choice.disabled\n                        }}\n                        disabled={choice.disabled}\n                    >\n                        <div class={\"flex items-center\"}>\n                            <RadioGroup.ItemInput />\n                            <RadioGroup.ItemLabel\n                                class=\"w-full p-4\"\n                                classList={{ \"px-2 py-2\": props.small }}\n                            >\n                                <div class=\"flex flex-row gap-2\">\n                                    <RadioGroup.ItemControl\n                                        class=\"my-auto flex aspect-square h-6 w-6 items-center justify-center rounded-full border\"\n                                        classList={{\n                                            hidden: !props.vertical\n                                        }}\n                                    >\n                                        <RadioGroup.ItemIndicator class=\"h-3 w-3 rounded-full bg-white\" />\n                                    </RadioGroup.ItemControl>\n                                    <div class=\"flex flex-col\">\n                                        <div\n                                            class={\"font-semibold\"}\n                                            classList={{\n                                                \"text-base\": props.small,\n                                                \"text-md\": !props.small,\n                                                \"text-sm\": !props.vertical\n                                            }}\n                                        >\n                                            {choice.label}\n                                        </div>\n                                        <Show when={!props.small}>\n                                            <div class=\"text-sm font-light\">\n                                                {choice.caption}\n                                            </div>\n                                        </Show>\n                                    </div>\n                                </div>\n                            </RadioGroup.ItemLabel>\n                        </div>\n                    </RadioGroup.Item>\n                )}\n            </For>\n        </RadioGroup.Root>\n    );\n}\n"
  },
  {
    "path": "src/components/layout/SubtleButton.tsx",
    "content": "import { JSX, Show } from \"solid-js\";\n\nimport { LoadingSpinner } from \"./LoadingSpinner\";\n\nexport function SubtleButton(props: {\n    children: JSX.Element | string;\n    onClick?: () => void;\n    loading?: boolean;\n    disabled?: boolean;\n    intent?: \"red\" | \"green\" | \"blue\";\n    type?: \"button\" | \"submit\";\n}) {\n    return (\n        <button\n            type={props.type || \"button\"}\n            onClick={() => props.onClick && props.onClick()}\n            disabled={props.loading || props.disabled}\n            class=\"flex items-center justify-center gap-2 rounded-xl border border-white/10 bg-neutral-900 p-2  no-underline active:-mb-[1px] active:mt-[1px] active:opacity-70\"\n            classList={{\n                \"text-m-grey-350\": !props.intent,\n                \"text-m-red\": props.intent === \"red\",\n                \"text-m-green\": props.intent === \"green\",\n                \"text-m-blue\": props.intent === \"blue\"\n            }}\n        >\n            <Show when={props.loading}>\n                {/* Loading spinner has a weird padding on it */}\n                <div class=\"-my-1 -mr-2\">\n                    <LoadingSpinner smallest wide />\n                </div>\n            </Show>\n            <Show when={!props.loading}>{props.children}</Show>\n        </button>\n    );\n}\n"
  },
  {
    "path": "src/components/layout/TextField.tsx",
    "content": "import { TextField as KTextField } from \"@kobalte/core\";\nimport { Show, splitProps, type JSX } from \"solid-js\";\n\nimport { TinyText } from \"~/components\";\n\nexport type TextFieldProps = {\n    name: string;\n    type?: \"text\" | \"email\" | \"tel\" | \"password\" | \"url\" | \"date\";\n    label?: string;\n    placeholder?: string;\n    caption?: string;\n    value: string | undefined;\n    error: string;\n    required?: boolean;\n    multiline?: boolean;\n    disabled?: boolean;\n    autoCapitalize?: string;\n    ref: (element: HTMLInputElement | HTMLTextAreaElement) => void;\n    onInput: JSX.EventHandler<\n        HTMLInputElement | HTMLTextAreaElement,\n        InputEvent\n    >;\n    onChange: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, Event>;\n    onBlur: JSX.EventHandler<\n        HTMLInputElement | HTMLTextAreaElement,\n        FocusEvent\n    >;\n};\n\nexport function TextField(props: TextFieldProps) {\n    const [fieldProps] = splitProps(props, [\n        \"placeholder\",\n        \"ref\",\n        \"onInput\",\n        \"onChange\",\n        \"onBlur\",\n        \"autoCapitalize\"\n    ]);\n    return (\n        <KTextField.Root\n            class=\"flex flex-col gap-2\"\n            name={props.name}\n            value={props.value}\n            validationState={props.error ? \"invalid\" : \"valid\"}\n            required={props.required}\n            disabled={props.disabled}\n        >\n            <Show when={props.label}>\n                <KTextField.Label class=\"text-sm font-semibold uppercase\">\n                    {props.label}\n                </KTextField.Label>\n            </Show>\n            <Show\n                when={props.multiline}\n                fallback={\n                    // @ts-expect-error autocapitalize isn't in the props for some reason\n                    <KTextField.Input\n                        {...fieldProps}\n                        type={props.type}\n                        class=\"w-full rounded-lg bg-white/10 p-2 placeholder-m-grey-400 disabled:text-m-grey-400\"\n                    />\n                }\n            >\n                {\n                    // @ts-expect-error autocapitalize isn't in the props for some reason\n                    <KTextField.TextArea\n                        {...fieldProps}\n                        autoResize\n                        class=\"w-full rounded-lg bg-white/10 p-2 placeholder-neutral-400\"\n                    />\n                }\n            </Show>\n            <KTextField.ErrorMessage class=\"text-sm text-m-red\">\n                {props.error}\n            </KTextField.ErrorMessage>\n            <Show when={props.caption}>\n                <TinyText>{props.caption}</TinyText>\n            </Show>\n        </KTextField.Root>\n    );\n}\n"
  },
  {
    "path": "src/components/layout/index.ts",
    "content": "export * from \"./BackLink\";\nexport * from \"./BackPop\";\nexport * from \"./Button\";\nexport * from \"./Misc\";\nexport * from \"./Radio\";\nexport * from \"./TextField\";\nexport * from \"./ExternalLink\";\nexport * from \"./LoadingSpinner\";\nexport * from \"./SubtleButton\";\n"
  },
  {
    "path": "src/components/successfail/MegaCheck.tsx",
    "content": "import megacheck from \"~/assets/icons/megacheck.png\";\n\nexport function MegaCheck() {\n    return (\n        <img\n            src={megacheck}\n            alt=\"success\"\n            class=\"mx-auto w-1/2 max-w-[50vh] flex-shrink\"\n        />\n    );\n}\n"
  },
  {
    "path": "src/components/successfail/MegaClock.tsx",
    "content": "import megaclock from \"~/assets/icons/megaclock.png\";\n\nexport function MegaClock() {\n    return (\n        <img\n            src={megaclock}\n            alt=\"fail\"\n            class=\"mx-auto w-1/2 max-w-[30vh] flex-shrink\"\n        />\n    );\n}\n"
  },
  {
    "path": "src/components/successfail/MegaEx.tsx",
    "content": "import megaex from \"~/assets/icons/megaex.png\";\n\nexport function MegaEx() {\n    return (\n        <img\n            src={megaex}\n            alt=\"fail\"\n            class=\"mx-auto w-1/2 max-w-[30vh] flex-shrink\"\n        />\n    );\n}\n"
  },
  {
    "path": "src/components/successfail/SuccessModal.tsx",
    "content": "import { Dialog } from \"@kobalte/core\";\nimport { JSX } from \"solid-js\";\n\nimport { Button } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { DIALOG_CONTENT, DIALOG_POSITIONER } from \"~/styles/dialogs\";\n\ntype SuccessModalProps = {\n    open: boolean;\n    setOpen: (open: boolean) => void;\n    children?: JSX.Element;\n    onConfirm?: () => void;\n    confirmText?: string;\n};\n\nexport function SuccessModal(props: SuccessModalProps) {\n    const i18n = useI18n();\n    const onNice = () => {\n        props.onConfirm ? props.onConfirm() : props.setOpen(false);\n    };\n\n    return (\n        <Dialog.Root open={props.open} onOpenChange={props.setOpen}>\n            <Dialog.Portal>\n                <div class={DIALOG_POSITIONER}>\n                    <Dialog.Content class={DIALOG_CONTENT}>\n                        <Dialog.Description class=\"mx-auto flex h-full w-full max-w-[400px] flex-col items-center justify-center gap-6\">\n                            {props.children}\n                        </Dialog.Description>\n                        <div class=\"mx-auto flex w-full max-w-[300px]\">\n                            <Button onClick={onNice} intent=\"inactive\">\n                                {props.confirmText ??\n                                    `${i18n.t(\"common.nice\")}`}\n                            </Button>\n                        </div>\n                        <div class=\"py-2\" />\n                    </Dialog.Content>\n                </div>\n            </Dialog.Portal>\n        </Dialog.Root>\n    );\n}\n"
  },
  {
    "path": "src/components/successfail/index.ts",
    "content": "export * from \"./MegaCheck\";\nexport * from \"./MegaEx\";\nexport * from \"./MegaClock\";\nexport * from \"./SuccessModal\";\n"
  },
  {
    "path": "src/i18n/config.ts",
    "content": "import { use } from \"i18next\";\nimport LanguageDetector from \"i18next-browser-languagedetector\";\nimport HttpApi from \"i18next-http-backend\";\n\nexport const defaultNS = \"translations\";\n\nconst i18n = use(HttpApi)\n    .use(LanguageDetector)\n    .init(\n        {\n            returnNull: false,\n            fallbackLng: \"en\",\n            preload: [\"en\"],\n            load: \"languageOnly\",\n            fallbackNS: false,\n            debug: true,\n            detection: {\n                order: [\"localStorage\", \"querystring\", \"navigator\", \"htmlTag\"],\n                lookupQuerystring: \"lang\",\n                lookupLocalStorage: \"i18nextLng\",\n                caches: [\"localStorage\"]\n            },\n            backend: {\n                loadPath: \"/i18n/{{lng}}.json\"\n            }\n        },\n        (err, _t) => {\n            // Do we actually wanna log something in case of an unsupported language?\n            if (err) return console.error(err);\n        }\n    );\nexport default i18n;\n"
  },
  {
    "path": "src/i18n/context.ts",
    "content": "import { i18n } from \"i18next\";\nimport { createContext, useContext } from \"solid-js\";\n\nexport const I18nContext = createContext<i18n>();\n\nexport function useI18n() {\n    const context = useContext(I18nContext);\n\n    if (!context) throw new ReferenceError(\"I18nContext\");\n\n    return context;\n}\n"
  },
  {
    "path": "src/index.tsx",
    "content": "import { render } from \"solid-js/web\";\n\nimport \"./root.css\";\n\nimport { Router } from \"./router\";\n\nconst root = document.getElementById(\"root\");\n\nrender(() => <Router />, root!);\n"
  },
  {
    "path": "src/logic/browserCompatibility.ts",
    "content": "export async function checkBrowserCompatibility(): Promise<boolean> {\n    // Check if we can write to localstorage\n    console.debug(\"Checking localstorage\");\n    try {\n        localStorage.setItem(\"test\", \"test\");\n        localStorage.removeItem(\"test\");\n    } catch (e) {\n        console.error(e);\n        throw new Error(\"Browser error: LocalStorage is not supported.\");\n    }\n\n    // Check if the browser supports WebAssembly\n    console.debug(\"Checking WebAssembly\");\n    if (\n        typeof WebAssembly !== \"object\" ||\n        !WebAssembly.validate(\n            Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)\n        )\n    ) {\n        throw new Error(\"Browser error: WebAssembly is not supported.\");\n    }\n\n    console.debug(\"Checking indexedDB\");\n    // Check if we can write to IndexedDB\n    try {\n        await openDatabase();\n    } catch (e) {\n        console.error(e);\n        throw new Error(\"Browser error: IndexedDB is not supported.\");\n    }\n\n    return true;\n}\n\nfunction openDatabase(): Promise<void> {\n    return new Promise((resolve, reject) => {\n        console.debug(\"Checking IndexedDB is on the window object\");\n        if (!(\"indexedDB\" in window)) {\n            reject(\"IndexedDB not supported.\");\n            return;\n        }\n\n        console.debug(\"Opening IndexedDB\");\n        const dbName = \"compatibility-test-db\";\n        const request = indexedDB.open(dbName, 1);\n\n        request.onsuccess = (_event) => {\n            indexedDB.deleteDatabase(dbName);\n            resolve();\n        };\n\n        request.onerror = (_event) => {\n            reject(\"Error opening database.\");\n        };\n    });\n}\n"
  },
  {
    "path": "src/logic/errorDispatch.ts",
    "content": "// IMPORTANT: this should match 1:1 with the MutinyJsError enum in mutiny-wasm\n// If we can handle all of these, we can handle all the errors that Mutiny can throw\n\n// WARNING: autogenerated code, generated by calling:\n// `node scripts/errorsToTs.cjs /path/to/mutiny-wasm/src/error.rs`\ntype MutinyError =\n    | \"Mutiny is already running.\"\n    | \"Mutiny is not running.\"\n    | \"Incorrect expected network.\"\n    | \"Resource Not found.\"\n    | \"Funding transaction could not be created.\"\n    | \"Network connection closed.\"\n    | \"The invoice or address is on a different network.\"\n    | \"An invoice must not get payed twice.\"\n    | \"Payment timed out.\"\n    | \"The given invoice is invalid.\"\n    | \"The given invoice is expired.\"\n    | \"Failed to create invoice.\"\n    | \"Channel reserve amount is too high.\"\n    | \"We do not have enough balance to pay the given amount.\"\n    | \"Failed to call on the given LNURL.\"\n    | \"Failed to make a request to the LSP.\"\n    | \"Failed to request channel from LSP due to funding error.\"\n    | \"Failed to request channel from LSP due to amount being too high.\"\n    | \"Failed to have a connection to the LSP node.\"\n    | \"Failed to provide an invoice to the LSP.\"\n    | \"Subscription Client Not Configured\"\n    | \"Invalid Parameter\"\n    | \"Called incorrect lnurl function.\"\n    | \"Failed to find route.\"\n    | \"Failed to parse the given peer information.\"\n    | \"Failed to create channel.\"\n    | \"Failed to close channel.\"\n    | \"Failed to persist data.\"\n    | \"Failed to read data from storage.\"\n    | \"Failed to decode lightning data.\"\n    | \"Failed to generate seed\"\n    | \"Invalid mnemonic\"\n    | \"Failed to conduct wallet operation.\"\n    | \"Failed to sign given transaction.\"\n    | \"Failed to conduct chain access operation.\"\n    | \"Failed to to sync on-chain wallet.\"\n    | \"Failed to execute a rapid gossip sync function\"\n    | \"Failed to read or write json from the front end\"\n    | \"The given node pubkey is invalid.\"\n    | \"Failed to get nostr data.\"\n    | \"Error with NIP-07 extension\"\n    | \"Failed to get the bitcoin price.\"\n    | \"Satoshi amount is invalid\"\n    | \"Failed to execute a dlc function\"\n    | \"Failed to execute a wasm_bindgen function\"\n    | \"Invalid Arguments were given\"\n    | \"Incorrect password entered.\"\n    | \"Cannot change password to the same password.\"\n    | \"Failed to create payjoin request.\"\n    | \"Payjoin response error: {0}\"\n    | \"Payjoin configuration failed.\"\n    | \"Error calling Cashu Mint\"\n    | \"Mint URL in token is empty\"\n    | \"Token has been already spent.\"\n    | \"A federation is required\"\n    | \"Failed to connect to a federation.\"\n    | \"Unknown Error\";\n\nexport function matchError(e: unknown): Error {\n    let errorString;\n\n    if (e instanceof Error) {\n        errorString = e.message;\n    } else if (typeof e === \"string\") {\n        errorString = e;\n    } else {\n        errorString = \"Unknown error\";\n    }\n\n    switch (errorString as MutinyError) {\n        case \"Failed to make a request to the LSP.\":\n        case \"Failed to request channel from LSP due to funding error.\":\n        case \"Failed to have a connection to the LSP node.\":\n            return new Error(\"LSP error, only on-chain is available for now.\");\n    }\n\n    return new Error(errorString);\n}\n"
  },
  {
    "path": "src/logic/mutinyWalletSetup.ts",
    "content": "export type Network = \"bitcoin\" | \"testnet\" | \"regtest\" | \"signet\";\n\nexport type MutinyWalletSettingStrings = {\n    network?: string;\n    proxy?: string;\n    esplora?: string;\n    rgs?: string;\n    lsp?: string;\n    lsps_connection_string?: string;\n    lsps_token?: string;\n    auth?: string;\n    subscriptions?: string;\n    storage?: string;\n    scorer?: string;\n    selfhosted?: string;\n    primal_api?: string;\n    blind_auth?: string;\n    hermes?: string;\n};\n\nconst SETTINGS_KEYS = [\n    {\n        name: \"network\",\n        storageKey: \"USER_SETTINGS_network\",\n        default: import.meta.env.VITE_NETWORK\n    },\n    {\n        name: \"proxy\",\n        storageKey: \"USER_SETTINGS_proxy\",\n        default: import.meta.env.VITE_PROXY\n    },\n    {\n        name: \"esplora\",\n        storageKey: \"USER_SETTINGS_esplora\",\n        default: import.meta.env.VITE_ESPLORA\n    },\n    {\n        name: \"rgs\",\n        storageKey: \"USER_SETTINGS_rgs\",\n        default: import.meta.env.VITE_RGS\n    },\n    {\n        name: \"lsp\",\n        storageKey: \"USER_SETTINGS_lsp\",\n        default: import.meta.env.VITE_LSP\n    },\n    {\n        name: \"lsps_connection_string\",\n        storageKey: \"USER_SETTINGS_lsps_connection_string\",\n        default: import.meta.env.VITE_LSPS_CONNECTION_STRING\n    },\n    {\n        name: \"lsps_token\",\n        storageKey: \"USER_SETTINGS_lsps_token\",\n        default: import.meta.env.VITE_LSPS_TOKEN\n    },\n    {\n        name: \"auth\",\n        storageKey: \"USER_SETTINGS_auth\",\n        default: import.meta.env.VITE_AUTH\n    },\n    {\n        name: \"subscriptions\",\n        storageKey: \"USER_SETTINGS_subscriptions\",\n        default: import.meta.env.VITE_SUBSCRIPTIONS\n    },\n    {\n        name: \"storage\",\n        storageKey: \"USER_SETTINGS_storage\",\n        default: import.meta.env.VITE_STORAGE\n    },\n    {\n        name: \"scorer\",\n        storageKey: \"USER_SETTINGS_scorer\",\n        default: import.meta.env.VITE_SCORER\n    },\n    {\n        name: \"selfhosted\",\n        storageKey: \"USER_SETTINGS_selfhosted\",\n        default: import.meta.env.VITE_SELFHOSTED\n    },\n    {\n        name: \"primal_api\",\n        storageKey: \"USER_SETTINGS_primal_api\",\n        default: import.meta.env.VITE_PRIMAL\n    },\n    {\n        name: \"blind_auth\",\n        storageKey: \"USER_SETTINGS_blind_auth\",\n        default: import.meta.env.VITE_BLIND_AUTH\n    },\n    {\n        name: \"hermes\",\n        storageKey: \"USER_SETTINGS_hermes\",\n        default: import.meta.env.VITE_HERMES\n    }\n];\n\nfunction getItemOrDefault(\n    storageKey: string,\n    defaultValue: string\n): string | undefined {\n    const item = localStorage.getItem(storageKey);\n    if (item === \"\") {\n        return undefined;\n    } else if (item === null) {\n        return defaultValue;\n    } else {\n        return item;\n    }\n}\n\nfunction setItemIfNotDefault(\n    key: string,\n    override: string,\n    defaultValue: string\n) {\n    if (override === defaultValue) {\n        localStorage.removeItem(key);\n    } else {\n        localStorage.setItem(key, override);\n    }\n}\n\nexport async function getSettings() {\n    const settings = <MutinyWalletSettingStrings>{};\n\n    SETTINGS_KEYS.forEach(({ name, storageKey, default: defaultValue }) => {\n        const n = name as keyof MutinyWalletSettingStrings;\n        const item = getItemOrDefault(storageKey, defaultValue);\n        settings[n] = item as string;\n    });\n\n    // VITE_PROXY and VITE_STORAGE might be set as relative URLs when self-hosting, so we need to make them absolute\n    const selfhosted = settings.selfhosted === \"true\";\n\n    // Expect urls like /_services/proxy and /_services/storage\n    if (selfhosted) {\n        let base = location.origin;\n        console.log(\"Self-hosted mode enabled, using base URL\", base);\n        const storage = settings.storage;\n        if (storage && storage.startsWith(\"/\")) {\n            settings.storage = base + storage;\n        }\n\n        const proxy = settings.proxy;\n        if (proxy && proxy.startsWith(\"/\")) {\n            if (base.startsWith(\"http://\")) {\n                base = base.replace(\"http://\", \"ws://\");\n            } else if (base.startsWith(\"https://\")) {\n                base = base.replace(\"https://\", \"wss://\");\n            }\n            settings.proxy = base + proxy;\n        }\n    }\n\n    if (!settings.network || !settings.proxy) {\n        throw new Error(\n            \"Missing a default setting for network or proxy. Check your .env file to make sure it looks like .env.sample\"\n        );\n    }\n\n    return settings;\n}\n\nexport async function setSettings(newSettings: MutinyWalletSettingStrings) {\n    SETTINGS_KEYS.forEach(({ name, storageKey, default: defaultValue }) => {\n        const n = name as keyof MutinyWalletSettingStrings;\n        const override = newSettings[n];\n        // If the value is in the newSettings, and it's not the default, set it in localstorage\n        // Also, \"\" is a valid value, so we only want to reject undefined\n        if (override !== undefined) {\n            setItemIfNotDefault(storageKey, override, defaultValue);\n        }\n    });\n}\n\nexport async function doubleInitDefense() {\n    console.log(\"Starting init...\");\n    // Ultimate defense against getting multiple instances of the wallet running.\n    // If we detect that the wallet has already been initialized in this session, we'll reload the page.\n    // A successful stop of the wallet in onCleanup will clear this flag\n    const init = sessionStorage.getItem(\"MUTINY_WALLET_INITIALIZED\");\n    if (init) {\n        const diff = Date.now() - Number(init);\n        console.error(\n            `Mutiny Wallet already initialized at ${init}, ${diff}ms ago. Reloading page.`\n        );\n        sessionStorage.removeItem(\"MUTINY_WALLET_INITIALIZED\");\n        window.location.reload();\n    } else {\n        // Timestamp our initialization for double init defense\n        sessionStorage.setItem(\n            \"MUTINY_WALLET_INITIALIZED\",\n            Date.now().toString()\n        );\n    }\n}\n"
  },
  {
    "path": "src/logic/waila.ts",
    "content": "import { WalletWorker } from \"~/state/megaStore\";\nimport { Result } from \"~/utils\";\n\nexport type ParsedParams = {\n    original: string;\n    address?: string;\n    payjoin_enabled?: boolean;\n    invoice?: string;\n    amount_sats?: bigint;\n    network?: string;\n    memo?: string;\n    privateTag?: string;\n    node_pubkey?: string;\n    lnurl?: string;\n    lightning_address?: string;\n    nostr_wallet_auth?: string;\n    fedimint_invite?: string;\n    is_lnurl_auth?: boolean;\n    contact_id?: string;\n};\n\nexport async function toParsedParams(\n    str: string,\n    ourNetwork: string,\n    sw: WalletWorker\n): Promise<Result<ParsedParams>> {\n    let params;\n    try {\n        params = await sw.parse_params(str || \"\");\n    } catch (e) {\n        return { ok: false, error: new Error(\"Invalid payment request\") };\n    }\n\n    // If WAILA doesn't return a network we should default to our own\n    // If the networks is testnet and we're on signet we should use signet\n    const network = !params.network\n        ? ourNetwork\n        : params.network === \"testnet\" && ourNetwork === \"signet\"\n          ? \"signet\"\n          : params.network;\n\n    if (network !== ourNetwork) {\n        return {\n            ok: false,\n            error: new Error(\n                `Destination is for ${params.network} but you're on ${ourNetwork}`\n            )\n        };\n    }\n\n    return {\n        ok: true,\n        value: {\n            original: str,\n            address: params.address,\n            payjoin_enabled: params.payjoin_supported,\n            invoice: params.invoice,\n            amount_sats: params.amount_sats,\n            network,\n            memo: params.memo,\n            node_pubkey: params.node_pubkey,\n            lnurl: params.lnurl,\n            lightning_address: params.lightning_address,\n            nostr_wallet_auth: params.nostr_wallet_auth,\n            is_lnurl_auth: params.is_lnurl_auth,\n            fedimint_invite: params.fedimint_invite_code\n        }\n    };\n}\n"
  },
  {
    "path": "src/root.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n* {\n    touch-action: manipulation;\n    user-select: none;\n}\n\nhtml {\n    @apply disable-scrollbars;\n    @apply bg-m-grey-975;\n}\n\nbody {\n    -webkit-overflow-scrolling: touch;\n    /* After load we need to remove the bg so the qr scanner can show through */\n    @apply text-white;\n    @apply !bg-transparent;\n    @apply flex w-full flex-1 flex-col safe-top safe-left safe-right safe-bottom min-h-device h-device;\n}\n\n#root {\n    @apply mx-auto flex w-full max-w-[600px] flex-1 flex-col;\n}\n\n@media (prefers-color-scheme: light) {\n    /* we don't support this but I want the browser to know I care */\n}\n\n#mutiny-logo {\n    image-rendering: pixelated;\n}\n\n.bg-gradient {\n    @apply bg-gradient-to-b from-black to-[#0b215b] bg-fixed bg-no-repeat;\n}\n\n.bg-gray {\n    @apply bg-gradient-to-b from-[hsl(224,5%,5%)] to-[hsl(224,5%,20%)] bg-fixed bg-no-repeat;\n}\n\n.react-modal-sheet-container {\n    @apply !bg-[#262626];\n}\n\na {\n    @apply underline decoration-m-grey-400 hover:decoration-white;\n}\n\np {\n    @apply font-light;\n}\n\np:not(:last-child) {\n    @apply mb-2;\n}\n\n#video-container {\n    position: relative;\n    width: max-content;\n    height: max-content;\n    overflow: hidden;\n    @apply bg-transparent;\n}\n\n#video-container .scan-region-highlight {\n    border-radius: 30px;\n    outline: rgba(0, 0, 0, 0.25) solid 50vmax;\n}\n\n#video-container .scan-region-highlight-svg {\n    display: none;\n}\n\nselect {\n    @apply appearance-none;\n    @apply block;\n    @apply border-[2px] ring-offset-black focus:outline-none focus:ring-2 focus:ring-offset-2;\n    @apply text-lg font-light;\n    @apply py-4 pl-4 pr-8;\n    background-image: url(\"data:image/svg+xml,%3Csvg aria-hidden='true' class='w-4 h-4 ml-1' fill='white' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 0 1 1.414 0L10 10.586l3.293-3.293a1 1 0 1 1 1.414 1.414l-4 4a1 1 0 0 1-1.414 0l-4-4a1 1 0 0 1 0-1.414z' clip-rule='evenodd'/%3E%3C/svg%3E\");\n    background-position: right 0.75rem center;\n    background-size: 20px 20px;\n    background-repeat: no-repeat;\n}\n\nstrong {\n    @apply font-semibold;\n}\n\n@layer components {\n    .shiny-button {\n        @apply border-b border-t border-b-white/10 border-t-white/50 active:-mb-[1px] active:mt-[1px] active:opacity-70;\n    }\n}\n\n.crt::before {\n    content: \" \";\n    display: block;\n    position: absolute;\n    top: 0;\n    left: 0;\n    bottom: 0;\n    right: 0;\n    background: linear-gradient(\n            rgba(18, 16, 16, 0) 50%,\n            rgba(0, 0, 0, 0.25) 50%\n        ),\n        linear-gradient(\n            90deg,\n            rgba(255, 0, 0, 0.06),\n            rgba(0, 255, 0, 0.02),\n            rgba(0, 0, 255, 0.06)\n        );\n    z-index: 2;\n    background-size:\n        100% 2px,\n        3px 100%;\n    pointer-events: none;\n    border-radius: 1rem;\n}\n"
  },
  {
    "path": "src/router.tsx",
    "content": "import { App as CapacitorApp } from \"@capacitor/app\";\nimport { Capacitor } from \"@capacitor/core\";\nimport { StatusBar, Style } from \"@capacitor/status-bar\";\nimport { MetaProvider, Title } from \"@solidjs/meta\";\nimport { Route, Router as SolidRouter, useNavigate } from \"@solidjs/router\";\nimport {\n    ErrorBoundary,\n    JSX,\n    Match,\n    onCleanup,\n    onMount,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    ErrorDisplay,\n    I18nProvider,\n    SetupErrorDisplay,\n    Toaster\n} from \"~/components\";\nimport {\n    Chat,\n    EditProfile,\n    Feedback,\n    Main,\n    NotFound,\n    Profile,\n    Receive,\n    Redeem,\n    RequestRoute,\n    Scanner,\n    Search,\n    Send,\n    Swap,\n    SwapLightning,\n    Transfer\n} from \"~/routes\";\nimport {\n    Admin,\n    Backup,\n    Channels,\n    Connections,\n    Currency,\n    EmergencyKit,\n    Encrypt,\n    ImportProfileSettings,\n    Language,\n    LightningAddress,\n    ManageFederations,\n    NostrKeys,\n    Plus,\n    Restore,\n    Servers,\n    Settings\n} from \"~/routes/settings\";\nimport {\n    AddFederation,\n    ImportProfile,\n    NewProfile,\n    Setup,\n    SetupRestore\n} from \"~/routes/setup\";\nimport { Provider as MegaStoreProvider, useMegaStore } from \"~/state/megaStore\";\n\nconst setStatusBarStyleDark = async () => {\n    await StatusBar.setStyle({ style: Style.Dark });\n};\n\nif (Capacitor.isNativePlatform()) {\n    await setStatusBarStyleDark();\n}\n\nfunction ChildrenOrError(props: { children: JSX.Element }) {\n    const [state] = useMegaStore();\n\n    // listeners for native navigation handling\n    // Check if the platform is Android to handle back\n    onMount(async () => {\n        if (Capacitor.getPlatform() === \"android\") {\n            const { remove } = await CapacitorApp.addListener(\n                \"backButton\",\n                ({ canGoBack }) => {\n                    if (!canGoBack) {\n                        CapacitorApp.exitApp();\n                    } else {\n                        window.history.back();\n                    }\n                }\n            );\n\n            // Ensure the listener is cleaned up when the component is destroyed\n            onCleanup(() => {\n                console.debug(\"cleaning up backButton listener\");\n                remove();\n            });\n        }\n\n        // Handle app links on native platforms\n        if (Capacitor.isNativePlatform()) {\n            const navigate = useNavigate();\n            const { remove } = await CapacitorApp.addListener(\n                \"appUrlOpen\",\n                (data) => {\n                    const url = new URL(data.url);\n                    const path = url.pathname;\n                    const urlParams = new URLSearchParams(url.search);\n\n                    if (urlParams.size) {\n                        console.log(\n                            `Navigating to ${path}?${urlParams.toString()}`\n                        );\n                        navigate(`${path}?${urlParams.toString()}`);\n                    } else {\n                        console.log(`Navigating to ${path}`);\n                        navigate(path);\n                    }\n                }\n            );\n\n            onCleanup(() => {\n                console.debug(\"cleaning up appUrlOpen listener\");\n                remove();\n            });\n        }\n    });\n\n    return (\n        <Switch>\n            <Match when={state.setup_error}>\n                <SetupErrorDisplay\n                    initialError={state.setup_error!}\n                    password={state.password}\n                />\n            </Match>\n            <Match when={true}>{props.children}</Match>\n        </Switch>\n    );\n}\n\nexport function Router() {\n    return (\n        <SolidRouter\n            root={(props) => (\n                <MetaProvider>\n                    <Title>Mutiny Wallet</Title>\n                    <ErrorBoundary fallback={(e) => <ErrorDisplay error={e} />}>\n                        <Suspense>\n                            <ErrorBoundary\n                                fallback={(e) => <ErrorDisplay error={e} />}\n                            >\n                                <MegaStoreProvider>\n                                    <I18nProvider>\n                                        <ErrorBoundary\n                                            fallback={(e) => (\n                                                <ErrorDisplay error={e} />\n                                            )}\n                                        >\n                                            <ChildrenOrError>\n                                                {props.children}\n                                            </ChildrenOrError>\n                                            <Toaster />\n                                        </ErrorBoundary>\n                                    </I18nProvider>\n                                </MegaStoreProvider>\n                            </ErrorBoundary>\n                        </Suspense>\n                    </ErrorBoundary>\n                </MetaProvider>\n            )}\n        >\n            <Route path=\"/\" component={Main} />\n            <Route path=\"/setup\" component={Setup} />\n            <Route path=\"/setup/restore\" component={SetupRestore} />\n            <Route path=\"/addfederation\" component={AddFederation} />\n            <Route path=\"/newprofile\" component={NewProfile} />\n            <Route path=\"/editprofile\" component={EditProfile} />\n            <Route path=\"/importprofile\" component={ImportProfile} />\n            <Route path=\"/profile\" component={Profile} />\n            <Route path=\"/chat/:id\" component={Chat} />\n            <Route path=\"/feedback\" component={Feedback} />\n            <Route path=\"/receive\" component={Receive} />\n            <Route path=\"/redeem\" component={Redeem} />\n            <Route path=\"/request/:id\" component={RequestRoute} />\n            <Route path=\"/scanner\" component={Scanner} />\n            <Route path=\"/send\" component={Send} />\n            <Route path=\"/swap\" component={Swap} />\n            <Route path=\"/swaplightning\" component={SwapLightning} />\n            <Route path=\"/transfer\" component={Transfer} />\n            <Route path=\"/search\" component={Search} />\n            <Route path=\"/settings\">\n                <Route path=\"/\" component={Settings} />\n                <Route path=\"/admin\" component={Admin} />\n                <Route path=\"/backup\" component={Backup} />\n                <Route path=\"/channels\" component={Channels} />\n                <Route path=\"/connections\" component={Connections} />\n                <Route path=\"/currency\" component={Currency} />\n                <Route path=\"/language\" component={Language} />\n                <Route path=\"/emergencykit\" component={EmergencyKit} />\n                <Route path=\"/encrypt\" component={Encrypt} />\n                <Route path=\"/plus\" component={Plus} />\n                <Route path=\"/restore\" component={Restore} />\n                <Route path=\"/servers\" component={Servers} />\n                <Route path=\"/nostrkeys\" component={NostrKeys} />\n                <Route\n                    path=\"/importprofile\"\n                    component={ImportProfileSettings}\n                />\n                <Route path=\"/federations\" component={ManageFederations} />\n                <Route path=\"/lightningaddress\" component={LightningAddress} />\n            </Route>\n            <Route path=\"/*all\" component={NotFound} />\n        </SolidRouter>\n    );\n}\n"
  },
  {
    "path": "src/routes/Chat.tsx",
    "content": "import { TagItem } from \"@mutinywallet/mutiny-wasm\";\nimport { createAsync, useNavigate, useParams } from \"@solidjs/router\";\nimport {\n    ArrowDownLeft,\n    ArrowUpRight,\n    Check,\n    MessagesSquare,\n    X,\n    Zap\n} from \"lucide-solid\";\nimport {\n    createEffect,\n    createResource,\n    createSignal,\n    For,\n    Match,\n    onCleanup,\n    onMount,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    ActivityDetailsModal,\n    AmountSats,\n    BackPop,\n    Button,\n    ButtonCard,\n    ContactButton,\n    ContactFormValues,\n    ContactViewer,\n    HackActivityType,\n    IActivityItem,\n    LoadingShimmer,\n    MutinyWalletGuard,\n    NiceP,\n    showToast,\n    SimpleInput,\n    UnifiedActivityItem\n} from \"~/components\";\nimport { MiniFab } from \"~/components/Fab\";\nimport { useI18n } from \"~/i18n/context\";\nimport { ParsedParams, toParsedParams } from \"~/logic/waila\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { createDeepSignal, eify, hexpubFromNpub, timeAgo } from \"~/utils\";\n\ntype CombinedMessagesAndActivity =\n    | { kind: \"message\"; content: FakeDirectMessage }\n    | { kind: \"activity\"; content: IActivityItem };\n\n// TODO: Use the actual type from MutinyWallet\nexport type FakeDirectMessage = {\n    from: string;\n    to: string;\n    message: string;\n    date: number;\n};\n\nfunction isActivityItem(content: unknown): content is IActivityItem {\n    return (content as IActivityItem).last_updated !== undefined;\n}\n\nfunction isDirectMessage(content: unknown): content is FakeDirectMessage {\n    return (content as FakeDirectMessage).date !== undefined;\n}\n\nfunction SingleMessage(props: {\n    dm: FakeDirectMessage;\n    counterPartyNpub: string;\n    counterPartyContactId: string;\n}) {\n    const [state, actions, sw] = useMegaStore();\n    const network = state.network || \"signet\";\n    const navigate = useNavigate();\n\n    const parsed = createAsync(\n        async () => {\n            let result = undefined;\n\n            // Look for a long word that might be an invoice\n            const split_message_by_whitespace = props.dm.message.split(/\\s+/g);\n            for (const word of split_message_by_whitespace) {\n                if (word.length > 15) {\n                    result = await toParsedParams(word, network, sw);\n                    if (result.ok) {\n                        break;\n                    }\n                }\n            }\n\n            if (!result || !result.ok) {\n                return undefined;\n            }\n\n            if (result.value?.invoice) {\n                try {\n                    const alreadyPaid = await sw.get_invoice(\n                        result.value.invoice\n                    );\n                    if (alreadyPaid?.paid) {\n                        return {\n                            type: \"invoice\",\n                            status: \"paid\",\n                            message_without_invoice: props.dm.message.replace(\n                                result.value.original,\n                                \"\"\n                            ),\n                            value: result.value.invoice,\n                            amount: result.value.amount_sats\n                        };\n                    }\n                } catch (e) {\n                    // No invoice found, no worries\n                }\n\n                return {\n                    type: \"invoice\",\n                    status: \"unpaid\",\n                    message_without_invoice: props.dm.message.replace(\n                        result.value.original,\n                        \"\"\n                    ),\n                    from: props.dm.from,\n                    value: result.value.invoice,\n                    amount: result.value.amount_sats\n                };\n            }\n        },\n        {\n            initialValue: undefined\n        }\n    );\n\n    function navWithContactId() {\n        navigate(\"/send\", {\n            state: {\n                previous: \"/chat/\" + props.counterPartyContactId\n            }\n        });\n    }\n\n    function payContact(result: ParsedParams) {\n        actions.setScanResult({\n            ...result,\n            contact_id: props.counterPartyContactId\n        });\n        navWithContactId();\n    }\n\n    async function handlePay() {\n        if (!parsed()) return;\n        await actions.handleIncomingString(\n            parsed()!.value,\n            (error) => {\n                showToast(error);\n            },\n            payContact\n        );\n    }\n\n    return (\n        <div\n            id=\"message\"\n            class=\"flex max-w-[80%] flex-col rounded-lg px-4 py-2\"\n            classList={{\n                \"bg-m-grey-750 self-start\":\n                    props.dm.from === props.counterPartyNpub,\n                \"bg-m-blue self-end\": props.dm.from !== props.counterPartyNpub\n            }}\n        >\n            <Switch>\n                <Match when={parsed()?.type === \"invoice\"}>\n                    <div class=\"flex flex-col gap-2\">\n                        <Show when={parsed()?.message_without_invoice}>\n                            <p class=\"!mb-0 break-words\">\n                                {parsed()?.message_without_invoice}\n                            </p>\n                        </Show>\n                        <div class=\"flex items-center gap-2\">\n                            <Zap class=\"h-4 w-4\" />\n                            <span>Lightning Invoice</span>\n                        </div>\n                        <AmountSats amountSats={parsed()?.amount} />\n                        <Show\n                            when={\n                                parsed()?.status !== \"paid\" &&\n                                parsed()?.from === props.counterPartyNpub\n                            }\n                        >\n                            <Button\n                                intent=\"blue\"\n                                layout=\"xs\"\n                                onClick={handlePay}\n                            >\n                                Pay\n                            </Button>\n                        </Show>\n                        <Show when={parsed()?.status === \"paid\"}>\n                            <p class=\"!mb-0 italic\">Paid</p>\n                        </Show>\n                        <div />\n                    </div>\n                </Match>\n                <Match when={true}>\n                    <p class=\"!mb-0 !select-text break-words\">\n                        {props.dm.message}\n                    </p>\n                </Match>\n            </Switch>\n            <time\n                class=\"text-xs font-light text-white/50\"\n                classList={{\n                    \"self-end\": props.dm.from !== props.counterPartyNpub\n                }}\n            >\n                {timeAgo(props.dm.date)}\n            </time>\n        </div>\n    );\n}\n\nfunction MessageList(props: {\n    convo: CombinedMessagesAndActivity[];\n    contact: TagItem;\n}) {\n    let scrollRef: HTMLDivElement;\n\n    onMount(() => {\n        scrollRef.scrollIntoView();\n    });\n\n    // Details modal stuff\n    const [detailsOpen, setDetailsOpen] = createSignal(false);\n    const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();\n    const [detailsId, setDetailsId] = createSignal(\"\");\n\n    function openDetailsModal(id: string, kind: HackActivityType) {\n        console.log(\"Opening details modal: \", id, kind);\n\n        if (!id) {\n            console.warn(\"No id provided to openDetailsModal\");\n            return;\n        }\n\n        setDetailsId(id);\n        setDetailsKind(kind);\n        setDetailsOpen(true);\n    }\n\n    return (\n        <>\n            <div class=\"flex flex-col-reverse justify-end gap-4 safe-bottom\">\n                <For each={props.convo}>\n                    {(combined, _index) => (\n                        <>\n                            <Show when={combined.kind === \"activity\"}>\n                                <div\n                                    class=\"w-[80%] rounded-lg bg-m-grey-750 px-4 pt-4\"\n                                    classList={{\n                                        \"self-start\": (\n                                            combined.content as IActivityItem\n                                        ).inbound,\n                                        \"self-end\": !(\n                                            combined.content as IActivityItem\n                                        ).inbound\n                                    }}\n                                >\n                                    <UnifiedActivityItem\n                                        item={combined.content as IActivityItem}\n                                        onClick={openDetailsModal}\n                                        // This isn't applicable here\n                                        onNewContactClick={() => {}}\n                                    />\n                                </div>\n                            </Show>\n                            <Show when={combined.kind === \"message\"}>\n                                <SingleMessage\n                                    dm={combined.content as FakeDirectMessage}\n                                    counterPartyNpub={props.contact.npub || \"\"}\n                                    counterPartyContactId={props.contact.id}\n                                />\n                            </Show>\n                        </>\n                    )}\n                </For>\n            </div>\n            <div\n                class=\"h-16\"\n                ref={(el) => (scrollRef = el)}\n                id=\"scroll-to-me\"\n            />\n            <Show when={detailsId() && detailsKind()}>\n                <ActivityDetailsModal\n                    open={detailsOpen()}\n                    kind={detailsKind()}\n                    id={detailsId()}\n                    setOpen={setDetailsOpen}\n                />\n            </Show>\n        </>\n    );\n}\n\nfunction FixedChatHeader(props: {\n    contact: TagItem;\n    refetch: () => void;\n    sendToContact: (contact: TagItem) => void;\n    requestFromContact: (contact: TagItem) => void;\n}) {\n    const [_state, _actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n\n    async function saveContact(id: string, contact: ContactFormValues) {\n        console.log(\"saving contact\", id, contact);\n        const hexpub = await hexpubFromNpub(sw, contact.npub?.trim());\n        try {\n            const existing = await sw.get_tag_item(id);\n            // This shouldn't happen\n            if (!existing) throw new Error(\"No existing contact\");\n            await sw.edit_contact(\n                id,\n                contact.name,\n                hexpub ? hexpub : undefined,\n                contact.ln_address ? contact.ln_address.trim() : undefined,\n                existing.lnurl,\n                existing.image_url\n            );\n        } catch (e) {\n            console.error(e);\n            showToast(eify(e));\n        }\n\n        // TODO: refetch contact\n        props.refetch();\n    }\n\n    async function deleteContact(id: string) {\n        try {\n            await sw.delete_contact(id);\n        } catch (e) {\n            console.error(e);\n            showToast(eify(e));\n        }\n        navigate(\"/search\");\n    }\n\n    const [updatingFollowStatus, setUpdatingFollowStatus] = createSignal(false);\n\n    async function followContact() {\n        setUpdatingFollowStatus(true);\n\n        try {\n            if (!props.contact.npub) throw new Error(\"No npub\");\n            await sw.follow_npub(props.contact.npub);\n            props.refetch();\n        } catch (e) {\n            console.error(e);\n            showToast(eify(e));\n        }\n        setUpdatingFollowStatus(false);\n    }\n\n    async function unfollowContact() {\n        setUpdatingFollowStatus(true);\n\n        try {\n            if (!props.contact.npub) throw new Error(\"No npub\");\n            await sw.unfollow_npub(props.contact.npub);\n            props.refetch();\n        } catch (e) {\n            console.error(e);\n            showToast(eify(e));\n        }\n        setUpdatingFollowStatus(false);\n    }\n\n    return (\n        <div class=\"fixed top-0 z-50 flex w-full max-w-[600px] flex-col gap-2 bg-m-grey-975/70 px-4 py-4 backdrop-blur-lg\">\n            <div class=\"backgrop-blur-lg z-50 bg-m-grey-975/70 safe-top\" />\n            <div class=\"flex w-full flex-col gap-2\">\n                <div class=\"flex items-center gap-4\">\n                    <BackPop default=\"/\" title=\"\" />\n                    <ContactViewer\n                        contact={props.contact}\n                        saveContact={saveContact}\n                        deleteContact={deleteContact}\n                    >\n                        <ContactButton\n                            contact={props.contact}\n                            onClick={() => {}}\n                        />\n                    </ContactViewer>\n                </div>\n                <div class=\"flex w-full justify-around gap-4\">\n                    <button\n                        disabled={\n                            !props.contact ||\n                            !(props.contact?.ln_address || props.contact?.lnurl)\n                        }\n                        class=\"flex gap-2 font-semibold text-m-green disabled:text-m-grey-350 disabled:opacity-50\"\n                        onClick={() => {\n                            props.sendToContact(props.contact);\n                        }}\n                    >\n                        <ArrowUpRight class=\"inline-block\" />\n                        <span>Send</span>\n                    </button>\n                    <Show when={props.contact?.npub}>\n                        <button\n                            class=\"flex gap-2 font-semibold text-m-blue\"\n                            onClick={() =>\n                                props.requestFromContact(props.contact)\n                            }\n                        >\n                            <ArrowDownLeft class=\"inline-block text-m-blue\" />\n                            <span>Request</span>\n                        </button>\n                        <Switch>\n                            <Match when={props.contact?.is_followed}>\n                                <button\n                                    class=\"flex gap-2 font-semibold text-m-red disabled:text-m-grey-350 disabled:opacity-50\"\n                                    onClick={unfollowContact}\n                                    disabled={updatingFollowStatus()}\n                                >\n                                    <X class=\"inline-block text-m-red\" />\n                                    <span>Unfollow</span>\n                                </button>\n                            </Match>\n                            <Match when={!props.contact?.is_followed}>\n                                <button\n                                    class=\"flex gap-2 font-semibold text-white disabled:text-m-grey-350 disabled:opacity-50\"\n                                    onClick={followContact}\n                                    disabled={updatingFollowStatus()}\n                                >\n                                    <Check class=\"inline-block text-white\" />\n                                    <span>Follow</span>\n                                </button>\n                            </Match>\n                        </Switch>\n                    </Show>\n                </div>\n            </div>\n        </div>\n    );\n}\n\nexport function Chat() {\n    const params = useParams();\n    const [_state, actions, sw] = useMegaStore();\n\n    const [messageValue, setMessageValue] = createSignal(\"\");\n    const [sending, setSending] = createSignal(false);\n\n    const i18n = useI18n();\n\n    const [contact, { refetch: refetchContact }] = createResource(async () => {\n        try {\n            return await sw.get_tag_item(params.id);\n        } catch (e) {\n            console.error(\"couldn't find contact\");\n            console.error(e);\n            return undefined;\n        }\n    });\n\n    // TODO: could probably move this to the web worker and make it even snappier\n    const [convo, { refetch }] = createResource(\n        contact,\n        async (contact?: TagItem) => {\n            if (!contact || !contact?.npub) return undefined;\n            if (!contact.npub) return [] as CombinedMessagesAndActivity[];\n            try {\n                let acts = [] as IActivityItem[];\n                let dms = [] as FakeDirectMessage[];\n\n                try {\n                    acts = (await sw.get_label_activity(\n                        params.id\n                    )) as IActivityItem[];\n                } catch (e) {\n                    console.error(\"error getting activity:\", e);\n                }\n\n                try {\n                    dms = (await sw.get_dm_conversation(\n                        contact.npub,\n                        20n,\n                        undefined,\n                        undefined\n                    )) as FakeDirectMessage[];\n                } catch (e) {\n                    console.error(\"error getting dms:\", e);\n                }\n\n                // Combine both arrays into an array of CombinedMessagesAndActivity, then sort by date\n                const combined = [\n                    ...dms.map((dm) => ({\n                        kind: \"message\",\n                        content: dm\n                    })),\n                    ...acts.map((act) => ({\n                        kind: \"activity\",\n                        content: act\n                    }))\n                ];\n\n                combined.sort((a, b) => {\n                    const a_time = isDirectMessage(a.content)\n                        ? a.content.date\n                        : isActivityItem(a.content)\n                          ? a.content.last_updated\n                          : 0;\n                    const b_time = isDirectMessage(b.content)\n                        ? b.content.date\n                        : isActivityItem(b.content)\n                          ? b.content.last_updated\n                          : 0;\n\n                    return b_time - a_time; // Descending order\n                });\n\n                return combined as CombinedMessagesAndActivity[];\n            } catch (e) {\n                console.error(\"error getting convo:\", e);\n                return [] as CombinedMessagesAndActivity[];\n            }\n        },\n        {\n            storage: createDeepSignal\n        }\n    );\n\n    async function sendMessage() {\n        const npub = contact()?.npub;\n        if (!npub) return;\n        setSending(true);\n        const rememberedValue = messageValue();\n        setMessageValue(\"\");\n        try {\n            const dmResult = await sw.send_dm(npub, rememberedValue);\n            console.log(\"dmResult:\", dmResult);\n            refetch();\n        } catch (e) {\n            console.error(\"error sending dm:\", e);\n        }\n        setSending(false);\n    }\n\n    createEffect(() => {\n        const interval = setInterval(() => {\n            refetch();\n        }, 5000); // Poll every 5 seconds\n        onCleanup(() => {\n            clearInterval(interval);\n        });\n    });\n\n    async function sendToContact(contact?: TagItem) {\n        if (!contact) return;\n        const address = contact.ln_address || contact.lnurl;\n        if (address) {\n            await actions.handleIncomingString(\n                (address || \"\").trim(),\n                (error) => {\n                    showToast(error);\n                },\n                (result) => {\n                    actions.setScanResult({\n                        ...result,\n                        contact_id: contact.id\n                    });\n                    navWithContactId();\n                }\n            );\n        } else {\n            console.error(\"no ln_address or lnurl\");\n        }\n    }\n\n    function requestFromContact(contact?: TagItem) {\n        if (!contact) return;\n        navigate(\"/request/\" + contact.id, {\n            state: {\n                previous: \"/chat/\" + params.id\n            }\n        });\n    }\n\n    const navigate = useNavigate();\n\n    function navWithContactId() {\n        navigate(\"/send\", {\n            state: {\n                previous: \"/chat/\" + params.id\n            }\n        });\n    }\n\n    return (\n        <MutinyWalletGuard>\n            <Show when={contact()}>\n                <FixedChatHeader\n                    contact={contact()!}\n                    refetch={refetchContact}\n                    requestFromContact={requestFromContact}\n                    sendToContact={sendToContact}\n                />\n            </Show>\n\n            <div class=\"h-[8rem]\" />\n            {/* <pre class=\"whitespace-pre-wrap break-all\">\n                            {JSON.stringify(convo(), null, 2)}\n                        </pre> */}\n            <div class=\"p-4\">\n                <Suspense>\n                    <Show when={contact()}>\n                        <Suspense fallback={<LoadingShimmer />}>\n                            <Switch>\n                                <Match\n                                    when={\n                                        convo.latest && convo.latest.length > 0\n                                    }\n                                >\n                                    {/* TODO: figure out how not to do typecasting here */}\n                                    <MessageList\n                                        convo={\n                                            convo.latest as CombinedMessagesAndActivity[]\n                                        }\n                                        contact={contact()!}\n                                    />\n                                </Match>\n                                <Match when={contact() && contact()?.npub}>\n                                    <ButtonCard\n                                        onClick={() =>\n                                            requestFromContact(contact())\n                                        }\n                                    >\n                                        <div class=\"flex items-center gap-4 text-left\">\n                                            <div class=\"flex-0\">\n                                                <MessagesSquare class=\"inline-block text-m-red\" />\n                                            </div>\n                                            <NiceP>\n                                                {i18n.t(\"chat.prompt\")}\n                                            </NiceP>\n                                        </div>\n                                    </ButtonCard>\n                                </Match>\n                            </Switch>\n                        </Suspense>\n                    </Show>\n                </Suspense>\n            </div>\n            <Show when={contact() && contact()?.npub}>\n                <div class=\"fixed bottom-0 grid w-full max-w-[600px] grid-cols-[auto_1fr_auto] grid-rows-1 items-center gap-2 bg-m-grey-975/70 px-4 py-2 backdrop-blur-lg\">\n                    <MiniFab\n                        onScan={() => navigate(\"/scanner\")}\n                        onSend={() => {\n                            sendToContact(contact());\n                        }}\n                        sendDisabled={\n                            !contact() ||\n                            !(contact()?.ln_address || contact()?.lnurl)\n                        }\n                        onRequest={() => requestFromContact(contact())}\n                    />\n                    <form\n                        onSubmit={async (e) => {\n                            e.preventDefault();\n                            await sendMessage();\n                        }}\n                    >\n                        <SimpleInput\n                            disabled={sending()}\n                            value={messageValue()}\n                            onInput={(e) =>\n                                setMessageValue(e.currentTarget.value)\n                            }\n                            placeholder={i18n.t(\"chat.placeholder\")}\n                        />\n                    </form>\n                    <div>\n                        <Show when={messageValue() || sending()}>\n                            <Button\n                                layout=\"xs\"\n                                intent=\"blue\"\n                                loading={sending()}\n                                onClick={sendMessage}\n                            >\n                                Send\n                            </Button>\n                        </Show>\n                    </div>\n                    <div class=\"backgrop-blur-lg z-50 bg-m-grey-975/70 safe-bottom\" />\n                </div>\n            </Show>\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/EditProfile.tsx",
    "content": "import { createAsync, useNavigate } from \"@solidjs/router\";\nimport { createSignal, Show } from \"solid-js\";\n\nimport {\n    BackLink,\n    DefaultMain,\n    EditableProfile,\n    EditProfileForm,\n    LargeHeader,\n    MutinyWalletGuard,\n    NavBar\n} from \"~/components\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { DEFAULT_NOSTR_NAME } from \"~/utils\";\n\nexport function EditProfile() {\n    const [_state, _actions, sw] = useMegaStore();\n    // const i18n = useI18n();\n    const navigate = useNavigate();\n\n    const [saving, setSaving] = createSignal(false);\n\n    const originalProfile = createAsync(async () => {\n        const profile = await sw.get_nostr_profile();\n\n        return {\n            name: profile?.display_name || profile?.name || DEFAULT_NOSTR_NAME,\n            picture: profile?.picture || undefined,\n            lud16: profile?.lud16 || undefined,\n            nip05: profile?.nip05 || undefined\n        };\n    });\n\n    async function saveProfile(profile: EditableProfile) {\n        try {\n            setSaving(true);\n\n            console.log(\"new profile\", profile);\n\n            const newProfile = await sw.edit_nostr_profile(\n                profile.nym ? profile.nym : undefined,\n                profile.imageUrl ? profile.imageUrl : undefined,\n                profile.lightningAddress ? profile.lightningAddress : undefined,\n                originalProfile()?.nip05 ? originalProfile()?.nip05 : undefined\n            );\n\n            console.log(\"newProfile\", newProfile);\n\n            setSaving(false);\n            navigate(\"/profile\");\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackLink href=\"/profile\" title=\"Profile\" />\n                <LargeHeader>Edit Profile</LargeHeader>\n                <div class=\"flex-1\" />\n                <Show when={originalProfile()}>\n                    <EditProfileForm\n                        initialProfile={{\n                            nym: originalProfile()?.name,\n                            lightningAddress: originalProfile()?.lud16,\n                            imageUrl: originalProfile()?.picture\n                        }}\n                        onSave={saveProfile}\n                        saving={saving()}\n                        cta=\"Save\"\n                    />\n                </Show>\n                <NavBar activeTab=\"profile\" />\n            </DefaultMain>\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/Feedback.tsx",
    "content": "import { createForm, required, SubmitHandler } from \"@modular-forms/solid\";\nimport { A, useLocation } from \"@solidjs/router\";\nimport { MessageSquareText } from \"lucide-solid\";\nimport { createSignal, Match, Show, Switch } from \"solid-js\";\n\nimport { ExternalLink, InfoBox, MegaCheck, NavBar } from \"~/components\";\nimport {\n    BackPop,\n    Button,\n    ButtonLink,\n    DefaultMain,\n    LargeHeader,\n    NiceP,\n    TextField,\n    VStack\n} from \"~/components/layout\";\nimport { useI18n } from \"~/i18n/context\";\nimport { eify } from \"~/utils\";\n\nconst FEEDBACK_API = import.meta.env.VITE_FEEDBACK;\n\nexport function FeedbackLink(props: { setupError?: boolean }) {\n    const i18n = useI18n();\n    const location = useLocation();\n    return (\n        <A\n            class=\"flex items-center gap-2 font-semibold text-m-grey-350 no-underline\"\n            state={{\n                previous: location.pathname,\n                // If we're coming from an error page we want to know that so we can hide the navbar\n                // TODO: either use actual error info from this or remove and just check for setup error in the navbar component\n                setupError: props.setupError\n            }}\n            href=\"/feedback\"\n        >\n            {i18n.t(\"feedback.link\")}\n            <MessageSquareText class=\"h-5 w-5\" />\n        </A>\n    );\n}\n\ntype FeedbackForm = {\n    include_contact: boolean;\n    user_type: \"nostr\" | \"email\";\n    id: string;\n    feedback: string;\n    include_logs: boolean;\n    images: File[];\n};\n\nasync function formDataFromFeedbackForm(f: FeedbackForm) {\n    const formData = new FormData();\n\n    formData.append(\"description\", JSON.stringify(f.feedback));\n\n    if (f.id) {\n        const contact =\n            f.user_type === \"nostr\" ? { nostr: f.id } : { email: f.id };\n        formData.append(\"contact\", JSON.stringify(contact));\n    }\n\n    if (f.include_contact) {\n        const contact =\n            f.user_type === \"nostr\"\n                ? { nostr: JSON.stringify(f.id) }\n                : { email: JSON.stringify(f.id) };\n        formData.append(\"contact\", JSON.stringify(contact));\n\n        formData.append(\"feedback_type\", JSON.stringify(\"generalcomment\"));\n    }\n\n    return formData;\n}\n\nfunction FeedbackForm(props: { onSubmitted: () => void }) {\n    const i18n = useI18n();\n    const [loading, setLoading] = createSignal(false);\n    const [error, setError] = createSignal<Error>();\n\n    const [feedbackForm, { Form, Field }] = createForm<FeedbackForm>({\n        initialValues: {\n            user_type: \"nostr\",\n            id: \"\",\n            feedback: \"\",\n            include_logs: false,\n            images: []\n        }\n    });\n\n    const handleSubmit: SubmitHandler<FeedbackForm> = async (\n        f: FeedbackForm\n    ) => {\n        try {\n            setLoading(true);\n            const formData = await formDataFromFeedbackForm(f);\n\n            const res = await fetch(`${FEEDBACK_API}/v1/feedback`, {\n                method: \"POST\",\n                body: formData\n            });\n\n            if (!res.ok) {\n                throw new Error(\n                    i18n.t(\"feedback.error\", { error: `: ${res.statusText}` })\n                );\n            }\n\n            const json = await res.json();\n\n            if (json.status === \"OK\") {\n                props.onSubmitted();\n            } else {\n                throw new Error(\n                    i18n.t(\"feedback.error\", {\n                        error: `. ${i18n.t(\"feedback.try_again\")}`\n                    })\n                );\n            }\n        } catch (e) {\n            console.error(e);\n            setError(eify(e));\n        } finally {\n            setLoading(false);\n        }\n    };\n\n    return (\n        <Form onSubmit={handleSubmit}>\n            <VStack>\n                <Field\n                    name=\"feedback\"\n                    validate={[required(i18n.t(\"feedback.invalid_feedback\"))]}\n                >\n                    {(field, props) => (\n                        <TextField\n                            multiline\n                            {...props}\n                            value={field.value}\n                            error={field.error}\n                            placeholder={i18n.t(\n                                \"feedback.feedback_placeholder\"\n                            )}\n                        />\n                    )}\n                </Field>\n                <Show when={error()}>\n                    <InfoBox accent=\"red\">{error()?.message}</InfoBox>\n                </Show>\n                <Button\n                    loading={loading()}\n                    disabled={\n                        !feedbackForm.dirty ||\n                        feedbackForm.submitting ||\n                        feedbackForm.invalid\n                    }\n                    intent=\"blue\"\n                    type=\"submit\"\n                >\n                    {i18n.t(\"feedback.send_feedback\")}\n                </Button>\n            </VStack>\n        </Form>\n    );\n}\n\nexport function Feedback() {\n    const i18n = useI18n();\n    const [submitted, setSubmitted] = createSignal(false);\n    const location = useLocation();\n\n    const state = location.state as { setupError?: boolean };\n\n    const setupError = state?.setupError || undefined;\n\n    return (\n        <DefaultMain>\n            <BackPop default=\"/\" />\n            <Switch>\n                <Match when={submitted()}>\n                    <div class=\"flex h-full flex-col items-center gap-4\">\n                        <MegaCheck />\n                        <LargeHeader centered>\n                            {i18n.t(\"feedback.received\")}\n                        </LargeHeader>\n                        <NiceP>{i18n.t(\"feedback.thanks\")}</NiceP>\n                        <ButtonLink intent=\"blue\" href=\"/\" layout=\"full\">\n                            {i18n.t(\"common.home\")}\n                        </ButtonLink>\n                        <Button\n                            intent=\"text\"\n                            layout=\"full\"\n                            onClick={() => setSubmitted(false)}\n                        >\n                            {i18n.t(\"feedback.more\")}\n                        </Button>\n                    </div>\n                </Match>\n                <Match when={true}>\n                    <LargeHeader>{i18n.t(\"feedback.header\")}</LargeHeader>\n                    <NiceP>{i18n.t(\"feedback.tracking\")}</NiceP>\n                    <NiceP>\n                        {i18n.t(\"feedback.github\")}{\" \"}\n                        <ExternalLink href=\"https://github.com/MutinyWallet/mutiny-web/issues\">\n                            {i18n.t(\"feedback.create_issue\")}\n                        </ExternalLink>\n                    </NiceP>\n                    <FeedbackForm onSubmitted={() => setSubmitted(true)} />\n                </Match>\n            </Switch>\n            <Show when={!setupError}>\n                <NavBar activeTab=\"send\" />\n            </Show>\n        </DefaultMain>\n    );\n}\n"
  },
  {
    "path": "src/routes/Main.tsx",
    "content": "import { createAsync, useNavigate } from \"@solidjs/router\";\nimport { Show, Suspense } from \"solid-js\";\n\nimport {\n    Circle,\n    DecryptDialog,\n    DefaultMain,\n    HomeBalance,\n    HomePrompt,\n    HomeSubnav,\n    LabelCircle,\n    LoadingIndicator,\n    NavBar,\n    ReloadPrompt,\n    SocialActionRow\n} from \"~/components\";\nimport { Fab } from \"~/components/Fab\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function WalletHeader(props: { loading: boolean }) {\n    const navigate = useNavigate();\n    const [state, _actions, sw] = useMegaStore();\n\n    const profile = createAsync(async () => {\n        if (props.loading) {\n            return undefined;\n        }\n        return await sw.get_nostr_profile();\n    });\n\n    return (\n        <header class=\"grid grid-cols-[auto_minmax(0,_1fr)_auto] items-center gap-4\">\n            <Suspense\n                fallback={\n                    <LabelCircle\n                        contact\n                        label={false}\n                        image_url={undefined}\n                        onClick={() => navigate(\"/profile\")}\n                    />\n                }\n            >\n                <LabelCircle\n                    contact\n                    label={false}\n                    image_url={profile()?.picture}\n                    onClick={() => navigate(\"/profile\")}\n                />\n            </Suspense>\n            <HomeBalance />\n            <Circle onClick={() => navigate(\"/settings\")}>\n                <img\n                    src={\n                        state.mutiny_plus\n                            ? \"/m-plus.png\"\n                            : \"/mutiny-pixel-m.png\"\n                    }\n                    alt=\"mutiny\"\n                    width={\"32px\"}\n                    height={\"32px\"}\n                    style={{\n                        \"image-rendering\": \"pixelated\"\n                    }}\n                />\n            </Circle>\n        </header>\n    );\n}\n\nexport function Main() {\n    const [state] = useMegaStore();\n\n    const navigate = useNavigate();\n\n    return (\n        <DefaultMain>\n            <Show when={state.load_stage !== \"done\"}>\n                <WalletHeader loading={true} />\n                <div class=\"flex-1\" />\n\n                <LoadingIndicator />\n                <div class=\"flex-1\" />\n            </Show>\n            <Show when={state.load_stage === \"done\"}>\n                <WalletHeader loading={false} />\n\n                <Show when={!state.wallet_loading && !state.safe_mode}>\n                    <SocialActionRow\n                        onScan={() => navigate(\"/scanner\")}\n                        onSearch={() => navigate(\"/search\")}\n                    />\n                </Show>\n\n                {/* <hr class=\"border-t border-m-grey-700\" /> */}\n                <ReloadPrompt />\n                <HomeSubnav />\n            </Show>\n\n            <Fab\n                onSearch={() => navigate(\"/search\")}\n                onScan={() => navigate(\"/scanner\")}\n            />\n\n            <DecryptDialog />\n            <HomePrompt />\n            <NavBar activeTab=\"home\" />\n        </DefaultMain>\n    );\n}\n"
  },
  {
    "path": "src/routes/Profile.tsx",
    "content": "import { createAsync, useNavigate } from \"@solidjs/router\";\nimport { Edit, Import } from \"lucide-solid\";\nimport { createMemo, Show, Suspense } from \"solid-js\";\n\nimport {\n    BackLink,\n    BalanceBox,\n    ButtonCard,\n    DefaultMain,\n    LabelCircle,\n    LightningAddressShower,\n    LoadingShimmer,\n    MutinyWalletGuard,\n    NavBar,\n    NiceP\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { DEFAULT_NOSTR_NAME } from \"~/utils\";\n\nexport type UserProfile = {\n    name: string;\n    picture?: string;\n    lud16?: string;\n    deleted: boolean | string;\n};\n\nexport function Profile() {\n    const [state, _actions, sw] = useMegaStore();\n    const i18n = useI18n();\n\n    const navigate = useNavigate();\n\n    const profile = createAsync(async () => {\n        const profile = await sw.get_nostr_profile();\n\n        const userProfile: UserProfile = {\n            name: profile?.display_name || profile?.name || DEFAULT_NOSTR_NAME,\n            picture: profile?.picture || undefined,\n            lud16: profile?.lud16 || undefined,\n            deleted: profile?.deleted || false\n        };\n        return userProfile;\n    });\n\n    const profileDeleted = createMemo(() => {\n        return profile()?.deleted === true || profile()?.deleted === \"true\";\n    });\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackLink />\n                <Show when={profile() && !profileDeleted()}>\n                    <div class=\"flex flex-col items-center gap-4\">\n                        <LabelCircle\n                            contact\n                            label={false}\n                            image_url={profile()?.picture}\n                            size=\"xl\"\n                        />\n                        <h1 class=\"text-3xl font-semibold\">\n                            <Show when={profile()?.name}>\n                                {profile()?.name}\n                            </Show>\n                        </h1>\n\n                        <LightningAddressShower profile={profile()} />\n                    </div>\n                    <ButtonCard onClick={() => navigate(\"/editprofile\")}>\n                        <div class=\"flex items-center gap-2\">\n                            {/* <Users class=\"inline-block text-m-red\" /> */}\n                            <Edit class=\"inline-block text-m-red\" />\n                            <NiceP>{i18n.t(\"profile.edit_profile\")}</NiceP>\n                        </div>\n                    </ButtonCard>\n                </Show>\n                <Show when={profile() && profile()?.deleted}>\n                    <ButtonCard\n                        onClick={() => navigate(\"/settings/importprofile\")}\n                    >\n                        <div class=\"flex items-center gap-2\">\n                            <Import class=\"inline-block text-m-red\" />\n                            <NiceP>\n                                {i18n.t(\"settings.nostr_keys.import_profile\")}\n                            </NiceP>\n                        </div>\n                    </ButtonCard>\n                </Show>\n                <Suspense fallback={<LoadingShimmer />}>\n                    <BalanceBox loading={state.wallet_loading} />\n                </Suspense>\n                <NavBar activeTab=\"profile\" />\n            </DefaultMain>\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/Receive.tsx",
    "content": "import { MutinyInvoice } from \"@mutinywallet/mutiny-wasm\";\nimport { useNavigate } from \"@solidjs/router\";\nimport { CircleHelp, Link, Users, Zap } from \"lucide-solid\";\nimport {\n    createEffect,\n    createMemo,\n    createResource,\n    createSignal,\n    Match,\n    onCleanup,\n    Show,\n    Switch\n} from \"solid-js\";\n\nimport {\n    ActivityDetailsModal,\n    AmountEditable,\n    AmountFiat,\n    AmountSats,\n    BackButton,\n    BackLink,\n    Button,\n    DefaultMain,\n    Fee,\n    FeesModal,\n    HackActivityType,\n    Indicator,\n    InfoBox,\n    IntegratedQr,\n    LargeHeader,\n    MegaCheck,\n    MutinyWalletGuard,\n    NavBar,\n    ReceiveWarnings,\n    SharpButton,\n    showToast,\n    SimpleDialog,\n    SimpleInput,\n    StyledRadioGroup,\n    SuccessModal,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify, objectToSearchParams, vibrateSuccess } from \"~/utils\";\n\nexport type OnChainTx = {\n    transaction: {\n        version: number;\n        lock_time: number;\n        input: Array<{\n            previous_output: string;\n            script_sig: string;\n            sequence: number;\n            witness: Array<string>;\n        }>;\n        output: Array<{\n            value: number;\n            script_pubkey: string;\n        }>;\n    };\n    txid: string;\n    internal_id: string;\n    received: number;\n    sent: number;\n    confirmation_time: {\n        height: number;\n        timestamp: number;\n    };\n};\n\nexport type ReceiveFlavor = \"lightning\" | \"onchain\";\ntype ReceiveState = \"edit\" | \"show\" | \"paid\";\ntype PaidState = \"lightning_paid\" | \"onchain_paid\";\n\nfunction FeeWarning(props: { fee: bigint; flavor: ReceiveFlavor }) {\n    const i18n = useI18n();\n    return (\n        <Show when={props.fee > 1000n && props.flavor === \"lightning\"}>\n            <InfoBox accent=\"blue\">\n                {i18n.t(\"receive.lightning_setup_fee\", {\n                    amount: props.fee.toLocaleString()\n                })}\n                <FeesModal />\n            </InfoBox>\n        </Show>\n    );\n}\n\nfunction FlavorChooser(props: {\n    flavor: ReceiveFlavor;\n    setFlavor: (value: string) => void;\n}) {\n    const [methodChooserOpen, setMethodChooserOpen] = createSignal(false);\n    const i18n = useI18n();\n\n    const RECEIVE_FLAVORS = [\n        {\n            value: \"lightning\",\n            label: i18n.t(\"receive.lightning_label\"),\n            caption: i18n.t(\"receive.lightning_caption\")\n        },\n        {\n            value: \"onchain\",\n            label: i18n.t(\"receive.onchain_label\"),\n            caption: i18n.t(\"receive.onchain_caption\")\n        }\n    ];\n    return (\n        <>\n            <SharpButton onClick={() => setMethodChooserOpen(true)}>\n                {props.flavor === \"lightning\" ? (\n                    <Zap class=\"h-4 w-4\" />\n                ) : (\n                    <Link class=\"h-4 w-4\" />\n                )}\n                {props.flavor === \"lightning\" ? \"Lightning\" : \"On-chain\"}\n            </SharpButton>\n            <SimpleDialog\n                title={i18n.t(\"receive.choose_payment_format\")}\n                open={methodChooserOpen()}\n                setOpen={(open) => setMethodChooserOpen(open)}\n            >\n                <StyledRadioGroup\n                    initialValue={props.flavor}\n                    onValueChange={(flavor) => {\n                        props.setFlavor(flavor);\n                        setMethodChooserOpen(false);\n                    }}\n                    choices={RECEIVE_FLAVORS}\n                    accent=\"white\"\n                    vertical\n                    delayOnChange\n                />\n            </SimpleDialog>\n        </>\n    );\n}\n\nfunction ReceiveMethodHelp() {\n    const i18n = useI18n();\n    const [open, setOpen] = createSignal(false);\n    return (\n        <>\n            <button class=\"flex gap-2 self-end\" onClick={() => setOpen(true)}>\n                <Users class=\"w-[18px]\" />\n                <CircleHelp class=\"w-[18px] text-m-grey-350\" />\n            </button>\n            <SimpleDialog\n                open={open()}\n                setOpen={setOpen}\n                title={i18n.t(\"receive.method_help.title\")}\n            >\n                <p>{i18n.t(\"receive.method_help.body\")}</p>\n            </SimpleDialog>\n        </>\n    );\n}\n\nexport function Receive() {\n    const [state, _actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n    const i18n = useI18n();\n\n    const [amount, setAmount] = createSignal<bigint>(0n);\n    const [whatForInput, setWhatForInput] = createSignal(\"\");\n\n    const [receiveState, setReceiveState] = createSignal<ReceiveState>(\"edit\");\n    // We use these for displaying the QR\n    const [receiveStrings, setReceiveStrings] = createSignal<{\n        lightning?: string;\n        onchain?: string;\n    }>();\n    // We use these for checking the payment status\n    const [rawReceiveStrings, setRawReceiveStrings] = createSignal<{\n        bolt11?: string;\n        address?: string;\n    }>();\n\n    const [lspFee, setLspFee] = createSignal(0n);\n\n    // The data we get after a payment\n    const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();\n    const [paymentInvoice, setPaymentInvoice] = createSignal<MutinyInvoice>();\n\n    // The flavor of the receive\n    const [flavor, setFlavor] = createSignal<ReceiveFlavor>(\"lightning\");\n\n    // loading state for the continue button\n    const [loading, setLoading] = createSignal(false);\n    const [error, setError] = createSignal<string>(\"\");\n\n    // Details Modal\n    const [detailsOpen, setDetailsOpen] = createSignal(false);\n    const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();\n    const [detailsId, setDetailsId] = createSignal<string>(\"\");\n\n    function clearAllButAmount() {\n        setReceiveState(\"edit\");\n        setReceiveStrings(undefined);\n        setPaymentTx(undefined);\n        setPaymentInvoice(undefined);\n        setError(\"\");\n    }\n\n    function clearAll() {\n        clearAllButAmount();\n        setAmount(0n);\n        setFlavor(\"lightning\");\n        setWhatForInput(\"\");\n    }\n\n    function openDetailsModal() {\n        const paymentTxId =\n            paidState() === \"onchain_paid\"\n                ? paymentTx()\n                    ? paymentTx()?.internal_id\n                    : undefined\n                : paymentInvoice()\n                  ? paymentInvoice()?.payment_hash\n                  : undefined;\n        const kind = paidState() === \"onchain_paid\" ? \"OnChain\" : \"Lightning\";\n\n        console.log(\"Opening details modal: \", paymentTxId, kind);\n\n        if (!paymentTxId) {\n            console.warn(\"No id provided to openDetailsModal\");\n            return;\n        }\n        if (paymentTxId !== undefined) {\n            setDetailsId(paymentTxId);\n        }\n        setDetailsKind(kind);\n        setDetailsOpen(true);\n    }\n\n    const receiveTags = createMemo(() => {\n        return whatForInput() ? [whatForInput().trim()] : [];\n    });\n\n    async function getLightningReceiveString(amount: bigint) {\n        try {\n            const inv = await sw.create_invoice(amount, receiveTags());\n\n            const bolt11 = inv?.bolt11;\n            setRawReceiveStrings({ bolt11 });\n\n            return `lightning:${bolt11}`;\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    async function getOnchainReceiveString(amount?: bigint) {\n        try {\n            if (amount && amount < 546n) {\n                throw new Error(i18n.t(\"receive.error_under_min_onchain\"));\n            }\n            const raw = await sw.get_new_address(receiveTags());\n            const address = raw?.address;\n\n            if (amount && amount > 0n) {\n                const btc_amount = await sw.convert_sats_to_btc(amount);\n                const params = objectToSearchParams({\n                    amount: btc_amount.toString()\n                });\n                setRawReceiveStrings({ address });\n                return `bitcoin:${address}?${params}`;\n            } else {\n                setRawReceiveStrings({ address });\n                return `bitcoin:${address}`;\n            }\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    async function onSubmit(e: Event) {\n        e.preventDefault();\n\n        await getQr();\n    }\n\n    async function getQr() {\n        setLoading(true);\n        try {\n            if (flavor() === \"lightning\") {\n                const lightning = await getLightningReceiveString(amount());\n                setReceiveStrings({ lightning });\n            }\n\n            if (flavor() === \"onchain\") {\n                const onchain = await getOnchainReceiveString(amount());\n                setReceiveStrings({ onchain });\n            }\n\n            if (!receiveStrings()?.lightning && !receiveStrings()?.onchain) {\n                throw new Error(i18n.t(\"receive.receive_strings_error\"));\n            }\n\n            if (!error()) {\n                setReceiveState(\"show\");\n            }\n        } catch (e) {\n            console.error(e);\n            showToast(eify(e));\n        }\n\n        setLoading(false);\n    }\n\n    const qrString = createMemo(() => {\n        if (receiveState() === \"show\") {\n            if (flavor() === \"lightning\") {\n                return receiveStrings()?.lightning;\n            } else if (flavor() === \"onchain\") {\n                return receiveStrings()?.onchain;\n            }\n        }\n    });\n\n    // Only copy the raw invoice string for lightning because the lightning prefix is not needed\n    // for the onchain address we share the whole bip21 uri because it has more information\n    const copyString = createMemo(() => {\n        if (receiveState() === \"show\") {\n            if (flavor() === \"lightning\") {\n                return rawReceiveStrings()?.bolt11;\n            } else if (flavor() === \"onchain\") {\n                return receiveStrings()?.onchain;\n            }\n        }\n    });\n\n    async function checkIfPaid(receiveStrings?: {\n        bolt11?: string;\n        address?: string;\n    }): Promise<PaidState | undefined> {\n        if (receiveStrings) {\n            const lightning = receiveStrings.bolt11;\n            const address = receiveStrings.address;\n\n            try {\n                // Lightning invoice might be blank\n                if (lightning) {\n                    console.log(\"checking invoice\", lightning);\n                    const invoice = await sw.get_invoice(lightning);\n\n                    // If the invoice has a fees amount that's probably the LSP fee\n                    if (invoice?.fees_paid) {\n                        setLspFee(invoice.fees_paid);\n                    }\n\n                    if (invoice && invoice.paid) {\n                        setReceiveState(\"paid\");\n                        setPaymentInvoice(invoice);\n                        await vibrateSuccess();\n                        return \"lightning_paid\";\n                    }\n                }\n\n                if (address) {\n                    console.log(\"checking address\", address);\n                    const tx = await sw.check_address(address);\n\n                    if (tx) {\n                        setReceiveState(\"paid\");\n                        setPaymentTx(tx);\n                        await vibrateSuccess();\n                        return \"onchain_paid\";\n                    }\n                }\n            } catch (e) {\n                console.error(e);\n            }\n        }\n    }\n\n    const [paidState, { refetch }] = createResource(\n        rawReceiveStrings,\n        checkIfPaid\n    );\n\n    createEffect(() => {\n        const interval = setInterval(() => {\n            if (receiveState() === \"show\") refetch();\n        }, 1000); // Poll every second\n        if (receiveState() !== \"show\") {\n            clearInterval(interval);\n        }\n        onCleanup(() => {\n            clearInterval(interval);\n        });\n    });\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <Show when={receiveState() === \"show\"} fallback={<BackLink />}>\n                    <BackButton\n                        onClick={() => clearAllButAmount()}\n                        title={i18n.t(\"receive.edit\")}\n                        showOnDesktop\n                    />\n                </Show>\n                <LargeHeader\n                    action={\n                        receiveState() === \"show\" && (\n                            <Indicator>{i18n.t(\"receive.checking\")}</Indicator>\n                        )\n                    }\n                >\n                    {i18n.t(\"receive.receive_bitcoin\")}\n                </LargeHeader>\n                <Switch>\n                    <Match\n                        when={!receiveStrings() || receiveState() === \"edit\"}\n                    >\n                        <div class=\"flex-1\" />\n                        <VStack>\n                            <div class=\"mx-auto flex w-full max-w-[400px] flex-col items-center\">\n                                <AmountEditable\n                                    initialAmountSats={amount() || \"0\"}\n                                    setAmountSats={setAmount}\n                                    onSubmit={getQr}\n                                />\n                                <FlavorChooser\n                                    flavor={flavor()}\n                                    setFlavor={setFlavor}\n                                />\n                            </div>\n                            <ReceiveWarnings\n                                amountSats={amount() || 0n}\n                                from_fedi_to_ln={false}\n                                flavor={flavor()}\n                            />\n                        </VStack>\n                        <div class=\"flex-1\" />\n                        <VStack>\n                            <Show\n                                when={\n                                    state.federations &&\n                                    state.federations.length\n                                }\n                            >\n                                <ReceiveMethodHelp />\n                            </Show>\n                            <form onSubmit={onSubmit}>\n                                <SimpleInput\n                                    type=\"text\"\n                                    value={whatForInput()}\n                                    placeholder={i18n.t(\"receive.what_for\")}\n                                    onInput={(e) =>\n                                        setWhatForInput(e.currentTarget.value)\n                                    }\n                                />\n                            </form>\n                            <Button\n                                disabled={\n                                    !amount() && !(flavor() === \"onchain\")\n                                }\n                                intent=\"green\"\n                                onClick={onSubmit}\n                                loading={loading()}\n                            >\n                                {i18n.t(\"common.continue\")}\n                            </Button>\n                        </VStack>\n                    </Match>\n                    <Match when={receiveStrings() && receiveState() === \"show\"}>\n                        <FeeWarning fee={lspFee()} flavor={flavor()} />\n                        <Show when={error()}>\n                            <InfoBox accent=\"red\">\n                                <p>{error()}</p>\n                            </InfoBox>\n                        </Show>\n                        <Show when={flavor() === \"onchain\"}>\n                            <InfoBox accent=\"blue\">\n                                {i18n.t(\"receive.warning_address_reuse\")}\n                            </InfoBox>\n                        </Show>\n                        <IntegratedQr\n                            value={qrString() ?? \"\"}\n                            copyString={copyString()}\n                            amountSats={amount() ? amount().toString() : \"0\"}\n                            kind={flavor()}\n                        />\n                        <Show when={flavor() === \"lightning\"}>\n                            <p class=\"text-center text-m-grey-350\">\n                                {i18n.t(\"receive.keep_mutiny_open\")}\n                            </p>\n                        </Show>\n                    </Match>\n                    <Match when={receiveState() === \"paid\"}>\n                        <SuccessModal\n                            open={!!paidState()}\n                            setOpen={(open: boolean) => {\n                                if (!open) clearAll();\n                            }}\n                            onConfirm={() => {\n                                clearAll();\n                                navigate(\"/\");\n                            }}\n                        >\n                            <Show when={detailsId() && detailsKind()}>\n                                <ActivityDetailsModal\n                                    open={detailsOpen()}\n                                    kind={detailsKind()}\n                                    id={detailsId()}\n                                    setOpen={setDetailsOpen}\n                                />\n                            </Show>\n                            <MegaCheck />\n                            <h1 class=\"mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl\">\n                                {receiveState() === \"paid\" &&\n                                paidState() === \"lightning_paid\"\n                                    ? i18n.t(\"receive.payment_received\")\n                                    : i18n.t(\"receive.payment_initiated\")}\n                            </h1>\n                            <div class=\"flex flex-col items-center gap-1\">\n                                <div class=\"text-xl\">\n                                    <AmountSats\n                                        amountSats={\n                                            receiveState() === \"paid\" &&\n                                            paidState() === \"lightning_paid\"\n                                                ? paymentInvoice()?.amount_sats\n                                                : paymentTx()?.received\n                                        }\n                                        icon=\"plus\"\n                                    />\n                                </div>\n                                <div class=\"text-white/70\">\n                                    <AmountFiat\n                                        amountSats={\n                                            receiveState() === \"paid\" &&\n                                            paidState() === \"lightning_paid\"\n                                                ? paymentInvoice()?.amount_sats\n                                                : paymentTx()?.received\n                                        }\n                                        denominationSize=\"sm\"\n                                    />\n                                </div>\n                            </div>\n                            <hr class=\"w-16 bg-m-grey-400\" />\n                            <Show\n                                when={\n                                    receiveState() === \"paid\" &&\n                                    paidState() === \"lightning_paid\"\n                                }\n                            >\n                                <Fee amountSats={lspFee()} />\n                            </Show>\n                            {/*TODO: Confirmation time estimate still not possible needs to be implemented in mutiny-node first*/}\n                            <Show when={receiveState() === \"paid\"}>\n                                <p\n                                    class=\"cursor-pointer underline\"\n                                    onClick={openDetailsModal}\n                                >\n                                    {i18n.t(\"common.view_payment_details\")}\n                                </p>\n                            </Show>\n                        </SuccessModal>\n                    </Match>\n                </Switch>\n            </DefaultMain>\n            <NavBar activeTab=\"receive\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/Redeem.tsx",
    "content": "import { LnUrlParams } from \"@mutinywallet/mutiny-wasm\";\nimport { useNavigate } from \"@solidjs/router\";\nimport {\n    createEffect,\n    createMemo,\n    createResource,\n    createSignal,\n    Match,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    AmountEditable,\n    AmountFiat,\n    AmountSats,\n    BackLink,\n    Button,\n    DefaultMain,\n    InfoBox,\n    LargeHeader,\n    LoadingShimmer,\n    MegaCheck,\n    MutinyWalletGuard,\n    NavBar,\n    ReceiveWarnings,\n    showToast,\n    SuccessModal,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify, vibrateSuccess } from \"~/utils\";\n\ntype RedeemState = \"edit\" | \"paid\";\n\nexport function Redeem() {\n    const [state, _actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n    const i18n = useI18n();\n\n    const [amount, setAmount] = createSignal<bigint>(0n);\n    // const [whatForInput, setWhatForInput] = createSignal(\"\");\n    const [lnurlData, setLnUrlData] = createSignal<LnUrlParams>();\n    const [lnurlString, setLnUrlString] = createSignal(\"\");\n    const [fixedAmount, setFixedAmount] = createSignal(false);\n\n    const [redeemState, setRedeemState] = createSignal<RedeemState>(\"edit\");\n\n    // loading state for the continue button\n    const [loading, setLoading] = createSignal(false);\n    const [error, setError] = createSignal<string>(\"\");\n\n    function mSatsToSats(mSats: bigint | number) {\n        const bigMsats = BigInt(mSats);\n        return bigMsats / 1000n;\n    }\n\n    function clearAll() {\n        setAmount(0n);\n        setLnUrlData(undefined);\n        setLnUrlString(\"\");\n        setFixedAmount(false);\n        setRedeemState(\"edit\");\n        setLoading(false);\n        setError(\"\");\n    }\n\n    const [decodedLnurl] = createResource(async () => {\n        if (state.scan_result) {\n            if (state.scan_result.lnurl) {\n                const decoded = await sw.decode_lnurl(state.scan_result.lnurl);\n                return decoded;\n            }\n        }\n    });\n\n    createEffect(() => {\n        if (decodedLnurl()) {\n            processLnurl(decodedLnurl()!);\n        }\n    });\n\n    // A ParsedParams with an lnurl in it\n    async function processLnurl(decoded: LnUrlParams) {\n        if (decoded.tag === \"withdrawRequest\") {\n            if (decoded.min === decoded.max) {\n                console.log(\"fixed amount\", decoded.max.toString());\n                setAmount(mSatsToSats(decoded.max));\n                setFixedAmount(true);\n            } else {\n                setAmount(mSatsToSats(decoded.min));\n                setFixedAmount(false);\n            }\n            setLnUrlData(decoded);\n            setLnUrlString(state.scan_result?.lnurl || \"\");\n        }\n    }\n\n    const lnurlAmountText = createMemo(() => {\n        if (lnurlData()) {\n            return i18n.t(\"redeem.lnurl_amount_message\", {\n                min: mSatsToSats(lnurlData()!.min).toLocaleString(),\n                max: mSatsToSats(lnurlData()!.max).toLocaleString()\n            });\n        }\n    });\n\n    const canSend = createMemo(() => {\n        const lnurlParams = lnurlData();\n        if (!lnurlParams) return false;\n        const min = mSatsToSats(lnurlParams.min);\n        const max = mSatsToSats(lnurlParams.max);\n        if (amount() === 0n || amount() < min || amount() > max) return false;\n\n        return true;\n    });\n\n    async function handleLnUrlWithdrawal() {\n        const lnurlParams = lnurlData();\n        if (!lnurlParams) return;\n\n        setError(\"\");\n        setLoading(true);\n\n        try {\n            const success = await sw.lnurl_withdraw(lnurlString(), amount());\n            if (!success) {\n                setError(i18n.t(\"redeem.lnurl_redeem_failed\"));\n            } else {\n                setRedeemState(\"paid\");\n                await vibrateSuccess();\n            }\n        } catch (e) {\n            console.error(\"lnurl_withdraw failed\", e);\n            showToast(eify(e));\n        } finally {\n            setLoading(false);\n        }\n    }\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackLink />\n                <LargeHeader>{i18n.t(\"redeem.redeem_bitcoin\")}</LargeHeader>\n                <Switch>\n                    <Match when={redeemState() === \"edit\"}>\n                        <div class=\"flex-1\" />\n                        <VStack>\n                            <Suspense\n                                fallback={\n                                    <div class=\"self-center\">\n                                        <LoadingShimmer />\n                                    </div>\n                                }\n                            >\n                                <Show when={decodedLnurl() && lnurlData()}>\n                                    <AmountEditable\n                                        initialAmountSats={amount() || \"0\"}\n                                        setAmountSats={setAmount}\n                                        onSubmit={handleLnUrlWithdrawal}\n                                        frozenAmount={fixedAmount()}\n                                    />\n                                </Show>\n                            </Suspense>\n                            <ReceiveWarnings\n                                amountSats={amount() || 0n}\n                                from_fedi_to_ln={false}\n                            />\n                            <Show when={lnurlAmountText() && !fixedAmount()}>\n                                <InfoBox accent=\"white\">\n                                    <p>{lnurlAmountText()}</p>\n                                </InfoBox>\n                            </Show>\n                            <Show when={error()}>\n                                <InfoBox accent=\"red\">\n                                    <p>{error()}</p>\n                                </InfoBox>\n                            </Show>\n                        </VStack>\n                        <div class=\"flex-1\" />\n                        <VStack>\n                            {/* TODO: add tagging to lnurlwithdrawal and all the redeem flows */}\n                            {/* <form onSubmit={handleLnUrlWithdrawal}>\n                                <SimpleInput\n                                    type=\"text\"\n                                    value={whatForInput()}\n                                    placeholder={i18n.t(\"receive.what_for\")}\n                                    onInput={(e) =>\n                                        setWhatForInput(e.currentTarget.value)\n                                    }\n                                />\n                            </form> */}\n                            <Button\n                                disabled={!amount() || !canSend()}\n                                intent=\"green\"\n                                onClick={handleLnUrlWithdrawal}\n                                loading={loading()}\n                            >\n                                {i18n.t(\"common.continue\")}\n                            </Button>\n                        </VStack>\n                    </Match>\n                    <Match when={redeemState() === \"paid\"}>\n                        <SuccessModal\n                            open={true}\n                            setOpen={(open: boolean) => {\n                                if (!open) clearAll();\n                            }}\n                            onConfirm={() => {\n                                clearAll();\n                                navigate(\"/\");\n                            }}\n                        >\n                            <MegaCheck />\n                            <h1 class=\"mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl\">\n                                {i18n.t(\"redeem.lnurl_redeem_success\")}\n                            </h1>\n                            <div class=\"flex flex-col items-center gap-1\">\n                                <div class=\"text-xl\">\n                                    <AmountSats\n                                        amountSats={amount()}\n                                        icon=\"plus\"\n                                    />\n                                </div>\n                                <div class=\"text-white/70\">\n                                    <AmountFiat\n                                        amountSats={amount()}\n                                        denominationSize=\"sm\"\n                                    />\n                                </div>\n                            </div>\n                            {/* TODO: add payment details */}\n                        </SuccessModal>\n                        <pre>NICE</pre>\n                    </Match>\n                </Switch>\n            </DefaultMain>\n            <NavBar activeTab=\"receive\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/Request.tsx",
    "content": "import { useNavigate, useParams } from \"@solidjs/router\";\nimport { createResource, createSignal, Suspense } from \"solid-js\";\n\nimport {\n    AmountEditable,\n    BackPop,\n    Button,\n    DefaultMain,\n    LabelCircle,\n    LargeHeader,\n    MutinyWalletGuard,\n    NavBar,\n    ReceiveWarnings,\n    showToast,\n    SimpleInput,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify } from \"~/utils\";\n\nimport { DestinationItem } from \"./Send\";\n\nexport function RequestRoute() {\n    const [_state, _actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n    const i18n = useI18n();\n\n    const params = useParams();\n\n    const [amount, setAmount] = createSignal<bigint>(0n);\n    const [whatForInput, setWhatForInput] = createSignal(\"\");\n\n    const [loading, setLoading] = createSignal(false);\n\n    async function handleSubmit() {\n        setLoading(true);\n        console.log(\"requesting\", amount(), whatForInput());\n        try {\n            const npub = contact()?.npub;\n            if (!npub) throw new Error(\"Contact doesn't have a npub\");\n\n            const tags = params.id ? [params.id] : [];\n\n            if (whatForInput()) {\n                tags.push(whatForInput().trim());\n            }\n\n            const raw = await sw.create_bip21(amount(), tags);\n\n            if (!raw || !raw.invoice)\n                throw new Error(\"Invoice creation failed\");\n\n            await sw.send_dm(npub, raw.invoice);\n\n            navigate(\"/chat/\" + params.id);\n        } catch (e) {\n            console.error(e);\n            showToast(eify(e));\n        }\n        setLoading(false);\n    }\n\n    async function getContact(id: string) {\n        console.log(\"fetching contact\", id);\n        try {\n            const contact = await sw.get_tag_item(id);\n            console.log(\"fetching contact\", contact);\n            // This shouldn't happen\n            if (!contact) throw new Error(\"Contact not found\");\n            return contact;\n        } catch (e) {\n            console.error(e);\n            showToast(eify(e));\n        }\n    }\n\n    const [contact] = createResource(() => params.id, getContact);\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackPop default={\"/\"} />\n                <LargeHeader>{i18n.t(\"request.request_bitcoin\")}</LargeHeader>\n                <Suspense>\n                    <DestinationItem\n                        title={contact()?.name || \"\"}\n                        value={contact()?.ln_address}\n                        icon={\n                            <LabelCircle\n                                name={contact()?.name || \"\"}\n                                image_url={contact()?.image_url}\n                                contact\n                                label={false}\n                            />\n                        }\n                    />\n                </Suspense>\n                <div class=\"flex-1\" />\n                <VStack>\n                    <AmountEditable\n                        initialAmountSats={amount() || \"0\"}\n                        setAmountSats={setAmount}\n                        onSubmit={handleSubmit}\n                    />\n                    <ReceiveWarnings amountSats={amount() || 0n} />\n                </VStack>\n                <div class=\"flex-1\" />\n                <VStack>\n                    <form\n                        onSubmit={async (e) => {\n                            e.preventDefault();\n                            if (amount() > BigInt(0)) {\n                                await handleSubmit();\n                            }\n                        }}\n                    >\n                        <SimpleInput\n                            type=\"text\"\n                            value={whatForInput()}\n                            placeholder={i18n.t(\"receive.what_for\")}\n                            onInput={(e) =>\n                                setWhatForInput(e.currentTarget.value)\n                            }\n                        />\n                    </form>\n                    <Button\n                        disabled={!amount()}\n                        intent=\"green\"\n                        onClick={handleSubmit}\n                        loading={loading()}\n                    >\n                        {i18n.t(\"request.request\")}\n                    </Button>\n                </VStack>\n            </DefaultMain>\n            <NavBar activeTab=\"receive\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/Scanner.tsx",
    "content": "import { Clipboard } from \"@capacitor/clipboard\";\nimport { Capacitor } from \"@capacitor/core\";\nimport { useNavigate } from \"@solidjs/router\";\nimport { createEffect, createSignal } from \"solid-js\";\n\nimport { Button, Reader, showToast } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function Scanner() {\n    const i18n = useI18n();\n    const [_state, actions] = useMegaStore();\n    const [scanResult, setScanResult] = createSignal<string>();\n    const navigate = useNavigate();\n\n    function onResult(result: string) {\n        setScanResult(result);\n    }\n\n    // TODO: is this correct? we always go back to where we came from when we scan... kind of like scan is a modal tbh\n    function exit() {\n        history.back();\n    }\n\n    async function handlePaste() {\n        try {\n            let text;\n\n            if (Capacitor.isNativePlatform()) {\n                const { value } = await Clipboard.read();\n                text = value;\n            } else {\n                text = await navigator.clipboard.readText();\n            }\n\n            const trimText = text.trim();\n            setScanResult(trimText);\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    // When we have a nice result we can head over to the send screen\n    createEffect(() => {\n        if (scanResult()) {\n            actions.handleIncomingString(\n                scanResult()!,\n                (error) => {\n                    showToast(error);\n                },\n                (result) => {\n                    actions.setScanResult(result);\n                    navigate(\"/send\");\n                }\n            );\n        }\n    });\n\n    return (\n        <div class=\"absolute inset-0\">\n            <Reader onResult={onResult} />\n            <div class=\"fixed bottom-[2rem] flex w-full flex-col items-center gap-8 px-8\">\n                <div class=\"flex w-full max-w-[800px] flex-col gap-2\">\n                    <Button intent=\"blue\" onClick={handlePaste}>\n                        {i18n.t(\"scanner.paste\")}\n                    </Button>\n                    <Button onClick={exit}>{i18n.t(\"scanner.cancel\")}</Button>\n                </div>\n            </div>\n        </div>\n    );\n}\n"
  },
  {
    "path": "src/routes/Search.tsx",
    "content": "import { Clipboard } from \"@capacitor/clipboard\";\nimport { Capacitor } from \"@capacitor/core\";\nimport { TagItem } from \"@mutinywallet/mutiny-wasm\";\nimport {\n    A,\n    cache,\n    createAsync,\n    useNavigate,\n    useSearchParams\n} from \"@solidjs/router\";\nimport { LucideClipboard, Scan, X } from \"lucide-solid\";\nimport {\n    createEffect,\n    createMemo,\n    createResource,\n    createSignal,\n    For,\n    Match,\n    onMount,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    ContactButton,\n    ContactEditor,\n    ContactFormValues,\n    LoadingShimmer,\n    NavBar,\n    showToast,\n    VStack\n} from \"~/components\";\nimport {\n    BackLink,\n    Button,\n    DefaultMain,\n    MutinyWalletGuard\n} from \"~/components/layout\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport {\n    actuallyFetchNostrProfile,\n    debounce,\n    hexpubFromNpub,\n    profileToPseudoContact,\n    PseudoContact,\n    searchProfiles\n} from \"~/utils\";\n\nexport function Search() {\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <div class=\"flex items-center justify-between\">\n                    <BackLink />\n                    <A\n                        class=\"rounded-lg p-2 hover:bg-white/5 active:bg-m-blue md:hidden\"\n                        href=\"/scanner\"\n                    >\n                        <Scan class=\"h-6 w-6\" />\n                    </A>{\" \"}\n                </div>\n                {/* Need to put the search view in a supsense so it loads list on first nav */}\n                <Suspense>\n                    <ActualSearch />\n                </Suspense>\n            </DefaultMain>\n            <NavBar activeTab=\"send\" />\n        </MutinyWalletGuard>\n    );\n}\n\nfunction ActualSearch(props: { initialValue?: string }) {\n    const [searchValue, setSearchValue] = createSignal(\"\");\n    const [debouncedSearchValue, setDebouncedSearchValue] = createSignal(\"\");\n    const [_state, actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n    const i18n = useI18n();\n\n    const trigger = debounce((message: string) => {\n        setDebouncedSearchValue(message);\n    }, 250);\n\n    createEffect(() => {\n        trigger(searchValue());\n    });\n\n    const getContacts = cache(async () => {\n        try {\n            return await sw.get_contacts_sorted(40);\n        } catch (e) {\n            console.error(e);\n            return [] as TagItem[];\n        }\n    }, \"contacts\");\n\n    const contacts = createAsync<TagItem[]>(() => getContacts(), {\n        initialValue: []\n    });\n\n    const filteredContacts = createMemo(() => {\n        const s = debouncedSearchValue().toLowerCase();\n        return (\n            contacts()?.filter((c) => {\n                return (\n                    c.name.toLowerCase().includes(s) ||\n                    c.ln_address?.toLowerCase().includes(s) ||\n                    c.lnurl?.toLowerCase().includes(s) ||\n                    c.npub?.includes(s)\n                );\n            }) || []\n        );\n    });\n\n    const foundNpubs = createMemo(() => {\n        return (\n            filteredContacts()\n                ?.map((c) => c.npub)\n                .filter((n) => !!n) || []\n        );\n    });\n\n    type SearchState = \"notsendable\" | \"sendable\" | \"sendableWithContact\";\n\n    const searchState = createAsync<SearchState>(async () => {\n        if (debouncedSearchValue() === \"\") {\n            return \"notsendable\";\n        }\n        const text = debouncedSearchValue().trim();\n        // Only want to check for something parseable if it's of reasonable length\n        if (text.length < 6) {\n            return \"notsendable\";\n        }\n        let state: SearchState = \"notsendable\";\n        await actions.handleIncomingString(\n            text,\n            (_error) => {\n                // noop\n            },\n            (result) => {\n                if (result.lightning_address || result.lnurl) {\n                    state = \"sendableWithContact\";\n                } else {\n                    state = \"sendable\";\n                }\n            }\n        );\n        console.log(\"params searchState\", state);\n        return state;\n    });\n\n    function navWithSearchValue(to: string) {\n        navigate(to, {\n            state: {\n                previous: searchValue()\n                    ? `/search/?search=${searchValue().trim()}`\n                    : \"/search\"\n            }\n        });\n    }\n\n    async function handleContinue() {\n        await actions.handleIncomingString(\n            debouncedSearchValue().trim(),\n            (error) => {\n                showToast(error);\n            },\n            (result) => {\n                if (result) {\n                    actions.setScanResult(result);\n                    navigate(\"/send\", {\n                        state: {\n                            previous: \"/search\"\n                        }\n                    });\n                } else {\n                    showToast(new Error(i18n.t(\"send.error_address\")));\n                }\n            }\n        );\n    }\n\n    const profileDeleted = createAsync(async () => {\n        const profile = await sw.get_nostr_profile();\n        return profile?.deleted;\n    });\n\n    // TODO this is mostly copy pasted from chat, could be a shared util maybe\n    async function navToSend(contact?: TagItem) {\n        if (!contact) return;\n        const address = contact.ln_address || contact.lnurl;\n        if (address) {\n            await actions.handleIncomingString(\n                (address || \"\").trim(),\n                (error) => {\n                    showToast(error);\n                },\n                (result) => {\n                    actions.setScanResult({\n                        ...result,\n                        contact_id: contact.id\n                    });\n                    navigate(\"/send\");\n                }\n            );\n        } else {\n            console.error(\"no ln_address or lnurl\");\n        }\n    }\n\n    async function sendToContact(contact: TagItem) {\n        if (profileDeleted()) {\n            await navToSend(contact);\n        } else {\n            navWithSearchValue(`/chat/${contact.id}`);\n        }\n    }\n\n    async function createContact(contact: ContactFormValues) {\n        try {\n            // First check if the contact already exists\n            const existingContact = contacts()?.find(\n                (c) => c.npub === contact.npub?.trim().toLowerCase()\n            );\n\n            if (existingContact) {\n                await sendToContact(existingContact);\n                return;\n            }\n\n            const contactId = await sw.create_new_contact(\n                contact.name,\n                contact.npub ? contact.npub.trim() : undefined,\n                contact.ln_address ? contact.ln_address.trim() : undefined,\n                undefined,\n                undefined\n            );\n\n            if (!contactId) {\n                throw new Error(\"no contact id returned\");\n            }\n\n            const tagItem = await sw.get_tag_item(contactId);\n\n            if (!tagItem) {\n                throw new Error(\"no contact returned\");\n            }\n\n            // if the new contact has an npub, send to chat\n            // otherwise, send to send page\n            if (tagItem.npub) {\n                await sendToContact(tagItem);\n            } else if (tagItem.ln_address) {\n                await actions.handleIncomingString(\n                    tagItem.ln_address,\n                    () => {},\n                    () => {\n                        navigate(\"/send\");\n                    }\n                );\n            } else if (tagItem.lnurl) {\n                await actions.handleIncomingString(\n                    tagItem.lnurl,\n                    () => {},\n                    () => {\n                        navigate(\"/send\");\n                    }\n                );\n            } else {\n                console.error(\n                    \"No npub, ln_address, or lnurl found, this should never happen\"\n                );\n            }\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    // Search input stuff\n    async function handlePaste() {\n        try {\n            let text;\n\n            if (Capacitor.isNativePlatform()) {\n                const { value } = await Clipboard.read();\n                text = value;\n            } else {\n                if (!navigator.clipboard.readText) {\n                    return showToast(new Error(i18n.t(\"send.error_clipboard\")));\n                }\n                text = await navigator.clipboard.readText();\n            }\n\n            const trimText = text.trim();\n            setSearchValue(trimText);\n            await parsePaste(trimText);\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    async function parsePaste(text: string) {\n        await actions.handleIncomingString(\n            text,\n            (error) => {\n                showToast(error);\n            },\n            (result) => {\n                actions.setScanResult(result);\n                navigate(\"/send\", { state: { previous: \"/search\" } });\n            }\n        );\n    }\n\n    let searchInputRef!: HTMLInputElement;\n\n    const [_params, setParams] = useSearchParams();\n\n    onMount(() => {\n        setSearchValue(props.initialValue || \"\");\n        setParams({ search: \"\" });\n        searchInputRef.focus();\n    });\n\n    return (\n        <>\n            <div class=\"relative\">\n                <input\n                    class=\"w-full rounded-lg bg-m-grey-750 p-2 placeholder-m-grey-400 disabled:text-m-grey-400\"\n                    type=\"text\"\n                    value={searchValue()}\n                    onInput={(e) => setSearchValue(e.currentTarget.value)}\n                    placeholder={i18n.t(\"send.search.placeholder\") + \" ...\"}\n                    autofocus\n                    autocomplete=\"off\"\n                    autocorrect=\"off\"\n                    ref={(el) => (searchInputRef = el)}\n                />\n                <Show when={!searchValue()}>\n                    <button\n                        class=\"absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1 py-1 pr-4\"\n                        onClick={handlePaste}\n                    >\n                        <LucideClipboard class=\"h-4 w-4\" />\n                        {i18n.t(\"send.search.paste\")}\n                    </button>\n                </Show>\n                <Show when={!!searchValue()}>\n                    <button\n                        class=\"absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-1 rounded-full bg-m-grey-800 px-1 py-1\"\n                        onClick={() => setSearchValue(\"\")}\n                    >\n                        <X class=\"h-4 w-4\" />\n                    </button>\n                </Show>\n            </div>\n            <Suspense>\n                <div class=\"flex-0 flex w-full\">\n                    <Show when={searchState() !== \"notsendable\"}>\n                        <Button intent=\"green\" onClick={handleContinue}>\n                            {i18n.t(\"common.continue\")}\n                        </Button>\n                    </Show>\n                </div>\n                <Show when={searchState() !== \"sendable\"}>\n                    <VStack>\n                        <Suspense>\n                            <h2 class=\"text-xl font-semibold\">\n                                {i18n.t(\"send.search.contacts\")}\n                            </h2>\n                            <Show when={contacts() && contacts().length > 0}>\n                                <For each={filteredContacts()}>\n                                    {(contact) => (\n                                        <ContactButton\n                                            contact={contact}\n                                            onClick={() =>\n                                                sendToContact(contact)\n                                            }\n                                        />\n                                    )}\n                                </For>\n                            </Show>\n                        </Suspense>\n                        <ContactEditor createContact={createContact} />\n\n                        <Suspense fallback={<LoadingShimmer />}>\n                            <Show when={!!debouncedSearchValue()}>\n                                <h2 class=\"py-2 text-xl font-semibold\">\n                                    {i18n.t(\"send.search.global_search\")}\n                                </h2>\n                                <GlobalSearch\n                                    searchValue={debouncedSearchValue()}\n                                    sendToContact={sendToContact}\n                                    foundNpubs={foundNpubs()}\n                                />\n                            </Show>\n                        </Suspense>\n                        <div class=\"h-4\" />\n                    </VStack>\n                </Show>\n            </Suspense>\n        </>\n    );\n}\n\nfunction GlobalSearch(props: {\n    searchValue: string;\n    sendToContact: (contact: TagItem) => void;\n    foundNpubs: (string | undefined)[];\n}) {\n    const [_state, _actions, sw] = useMegaStore();\n    const hexpubs = createMemo(() => {\n        const hexpubs: Set<string> = new Set();\n        for (const npub of props.foundNpubs) {\n            hexpubFromNpub(sw, npub)\n                .then((h) => {\n                    if (h) {\n                        hexpubs.add(h);\n                    }\n                })\n                .catch((e) => {\n                    console.error(e);\n                });\n        }\n        return hexpubs;\n    });\n\n    async function searchFetcher(args: {\n        value?: string;\n        hexpubs?: Set<string>;\n    }) {\n        try {\n            // Handling case when value starts with \"npub\"\n            if (args.value?.toLowerCase().startsWith(\"npub\")) {\n                const hexpub = await hexpubFromNpub(sw, args.value);\n                if (!hexpub) return [];\n\n                const profile = await actuallyFetchNostrProfile(hexpub);\n                if (!profile) return [];\n\n                const contact = profileToPseudoContact(profile);\n                return [contact];\n            }\n\n            // Handling case for other values (name, nip-05, whatever else primal searches)\n            const contacts = await searchProfiles(args.value!.toLowerCase());\n            return contacts.filter((c) => !args.hexpubs?.has(c.hexpub));\n        } catch (e) {\n            console.error(e);\n            return [];\n        }\n    }\n\n    const searchArgs = createMemo(() => {\n        if (props.searchValue) {\n            return {\n                value: props.searchValue,\n                hexpubs: hexpubs()\n            };\n        } else {\n            return {\n                value: \"\",\n                hexpubs: undefined\n            };\n        }\n    });\n\n    const [searchResults] = createResource(searchArgs, searchFetcher);\n\n    return (\n        <Switch>\n            <Match\n                when={\n                    !!props.searchValue &&\n                    searchResults.state === \"ready\" &&\n                    searchResults()?.length === 0\n                }\n            >\n                <p class=\"text-neutral-500\">\n                    No results found for \"{props.searchValue}\"\n                </p>\n            </Match>\n            <Match when={true}>\n                <For each={searchResults()}>\n                    {(contact) => (\n                        <SingleContact\n                            contact={contact}\n                            sendToContact={props.sendToContact}\n                        />\n                    )}\n                </For>\n            </Match>\n        </Switch>\n    );\n}\n\nfunction SingleContact(props: {\n    contact: PseudoContact;\n    sendToContact: (contact: TagItem) => void;\n}) {\n    const [_state, _actions, sw] = useMegaStore();\n\n    async function createContactFromSearchResult(contact: PseudoContact) {\n        try {\n            const contactId = await sw.create_new_contact(\n                contact.name,\n                contact.hexpub ? contact.hexpub : \"\",\n                contact.ln_address ? contact.ln_address : undefined,\n                undefined,\n                contact.image_url ? contact.image_url : undefined\n            );\n\n            console.log(\"contactId\", contactId);\n\n            if (!contactId) {\n                throw new Error(\"no contact id returned\");\n            }\n\n            const tagItem = await sw.get_tag_item(contactId);\n\n            if (!tagItem) {\n                throw new Error(\"no contact returned\");\n            }\n\n            props.sendToContact(tagItem);\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    return (\n        <ContactButton\n            contact={props.contact}\n            onClick={() => createContactFromSearchResult(props.contact)}\n        />\n    );\n}\n"
  },
  {
    "path": "src/routes/Send.tsx",
    "content": "import { MutinyInvoice, TagItem } from \"@mutinywallet/mutiny-wasm\";\nimport { useLocation, useNavigate, useSearchParams } from \"@solidjs/router\";\nimport { Eye, EyeOff, Link, X, Zap } from \"lucide-solid\";\nimport {\n    createEffect,\n    createMemo,\n    createResource,\n    createSignal,\n    JSX,\n    Match,\n    onMount,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    ActivityDetailsModal,\n    AmountEditable,\n    AmountFiat,\n    AmountSats,\n    BackPop,\n    Button,\n    DefaultMain,\n    Failure,\n    Fee,\n    FeeDisplay,\n    HackActivityType,\n    InfoBox,\n    LabelCircle,\n    LoadingShimmer,\n    MegaCheck,\n    MethodChoice,\n    MutinyWalletGuard,\n    NavBar,\n    SharpButton,\n    showToast,\n    SimpleInput,\n    SmallHeader,\n    StringShower,\n    SuccessModal,\n    UnstyledBackPop,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { ParsedParams } from \"~/logic/waila\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify, vibrateSuccess } from \"~/utils\";\n\nexport type SendSource = \"lightning\" | \"onchain\";\nexport type PrivacyLevel = \"Public\" | \"Private\" | \"Anonymous\" | \"Not Available\";\n\n// const TEST_DEST = \"bitcoin:tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe?amount=0.00001&label=heyo&lightning=lntbs10u1pjrwrdedq8dpjhjmcnp4qd60w268ve0jencwzhz048ruprkxefhj0va2uspgj4q42azdg89uupp5gngy2pqte5q5uvnwcxwl2t8fsdlla5s6xl8aar4xcsvxeus2w2pqsp5n5jp3pz3vpu92p3uswttxmw79a5lc566herwh3f2amwz2sp6f9tq9qyysgqcqpcxqrpwugv5m534ww5ukcf6sdw2m75f2ntjfh3gzeqay649256yvtecgnhjyugf74zakaf56sdh66ec9fqep2kvu6xv09gcwkv36rrkm38ylqsgpw3yfjl\"\n// const TEST_DEST_ADDRESS = \"tb1pdh43en28jmhnsrhxkusja46aufdlae5qnfrhucw5jvefw9flce3sdxfcwe\"\n\n// TODO: better success / fail type\ntype SentDetails = {\n    amount?: bigint;\n    destination?: string;\n    txid?: string;\n    payment_hash?: string;\n    failure_reason?: string;\n    fee_estimate?: bigint | number;\n};\n\nfunction DestinationShower(props: {\n    source: SendSource;\n    description?: string;\n    address?: string;\n    invoice?: MutinyInvoice;\n    nodePubkey?: string;\n    lnurl?: string;\n    lightning_address?: string;\n    contact?: TagItem;\n}) {\n    return (\n        <Switch>\n            <Match when={props.contact}>\n                <DestinationItem\n                    title={props.contact?.name || \"\"}\n                    value={props.contact?.ln_address}\n                    icon={\n                        <LabelCircle\n                            name={props.contact?.name || \"\"}\n                            image_url={props.contact?.image_url}\n                            contact\n                            label={false}\n                        />\n                    }\n                />\n            </Match>\n            <Match when={props.address && props.source === \"onchain\"}>\n                <DestinationItem\n                    title=\"On-chain\"\n                    value={<StringShower text={props.address || \"\"} />}\n                    icon={<Link class=\"h-4 w-4\" />}\n                />\n            </Match>\n            <Match when={props.invoice && props.source === \"lightning\"}>\n                <DestinationItem\n                    title=\"Lightning\"\n                    value={<StringShower text={props.invoice?.bolt11 || \"\"} />}\n                    icon={<Zap class=\"h-4 w-4\" />}\n                />\n            </Match>\n            <Match\n                when={props.lightning_address && props.source === \"lightning\"}\n            >\n                <DestinationItem\n                    title=\"Lightning\"\n                    value={props.lightning_address || \"\"}\n                    icon={<Zap class=\"h-4 w-4\" />}\n                />\n            </Match>\n            <Match when={props.nodePubkey && props.source === \"lightning\"}>\n                <DestinationItem\n                    title=\"Lightning\"\n                    value={<StringShower text={props.nodePubkey || \"\"} />}\n                    icon={<Zap class=\"h-4 w-4\" />}\n                />\n            </Match>\n            <Match\n                when={\n                    props.lnurl &&\n                    !props.lightning_address &&\n                    props.source === \"lightning\"\n                }\n            >\n                <DestinationItem\n                    title=\"Lightning\"\n                    value={<StringShower text={props.lnurl || \"\"} />}\n                    icon={<Zap class=\"h-4 w-4\" />}\n                />\n            </Match>\n        </Switch>\n    );\n}\n\nexport function DestinationItem(props: {\n    title: string;\n    value: JSX.Element;\n    icon: JSX.Element;\n}) {\n    return (\n        <div class=\"grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)] items-center gap-2 rounded-xl bg-neutral-800 p-2\">\n            {props.icon}\n            <div class=\"flex flex-col gap-1\">\n                <SmallHeader>{props.title}</SmallHeader>\n                <div class=\"text-sm text-neutral-500\">{props.value}</div>\n            </div>\n            <UnstyledBackPop default=\"/\">\n                <div class=\"h-8 w-8 rounded-full bg-m-grey-800 px-1 py-1\">\n                    <X class=\"h-6 w-6\" />\n                </div>\n            </UnstyledBackPop>\n        </div>\n    );\n}\n\nexport function Send() {\n    const [state, actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n    const [params, setParams] = useSearchParams();\n    const i18n = useI18n();\n\n    const [amountInput, setAmountInput] = createSignal(\"\");\n    const [whatForInput, setWhatForInput] = createSignal(\"\");\n\n    // These can be derived from the destination or set by the user\n    const [amountSats, setAmountSats] = createSignal(0n);\n    const [unparsedAmount, setUnparsedAmount] = createSignal(true);\n\n    // These are derived from the incoming destination\n    const [isAmtEditable, setIsAmtEditable] = createSignal(true);\n    const [source, setSource] = createSignal<SendSource>(\"lightning\");\n    const [invoice, setInvoice] = createSignal<MutinyInvoice>();\n    const [nodePubkey, setNodePubkey] = createSignal<string>();\n    const [lnurlp, setLnurlp] = createSignal<string>();\n    const [lnAddress, setLnAddress] = createSignal<string>();\n    const [originalScan, setOriginalScan] = createSignal<string>();\n    const [address, setAddress] = createSignal<string>();\n    const [payjoinEnabled, setPayjoinEnabled] = createSignal<boolean>();\n    const [description, setDescription] = createSignal<string>();\n    const [contactId, setContactId] = createSignal<string>();\n    const [isHodlInvoice, setIsHodlInvoice] = createSignal<boolean>(false);\n\n    // Is sending / sent\n    const [sending, setSending] = createSignal(false);\n    const [sentDetails, setSentDetails] = createSignal<SentDetails>();\n\n    // Details Modal\n    const [detailsOpen, setDetailsOpen] = createSignal(false);\n    const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();\n    const [detailsId, setDetailsId] = createSignal(\"\");\n\n    // Errors\n    const [error, setError] = createSignal<string>();\n\n    function openDetailsModal() {\n        const paymentTxId = sentDetails()?.txid\n            ? sentDetails()\n                ? sentDetails()?.txid\n                : undefined\n            : sentDetails()\n              ? sentDetails()?.payment_hash\n              : undefined;\n        const kind = sentDetails()?.txid ? \"OnChain\" : \"Lightning\";\n\n        console.log(\"Opening details modal: \", paymentTxId, kind);\n\n        if (!paymentTxId) {\n            console.warn(\"No id provided to openDetailsModal\");\n            return;\n        }\n        if (paymentTxId !== undefined) {\n            setDetailsId(paymentTxId);\n        }\n        setDetailsKind(kind);\n        setDetailsOpen(true);\n    }\n\n    // TODO: can I dedupe this from the search page?\n    async function parsePaste(text: string) {\n        await actions.handleIncomingString(\n            text,\n            (error) => {\n                showToast(error);\n            },\n            (result) => {\n                actions.setScanResult(result);\n                navigate(\"/send\", { state: { previous: \"/search\" } });\n            }\n        );\n    }\n\n    // TODO: do we actually use this anywhere?\n    // send?invoice=... need to check for wallet because we can't parse until we have the wallet\n    createEffect(() => {\n        if (params.invoice && state.load_stage === \"done\") {\n            parsePaste(params.invoice);\n            setParams({ invoice: undefined });\n        }\n    });\n\n    const maxOnchain = createMemo(() => {\n        const conf = state.balance?.confirmed ?? 0n;\n        const unc = state.balance?.unconfirmed ?? 0n;\n        const fed = state.balance?.federation ?? 0n;\n\n        if (fed > conf + unc) {\n            return fed;\n        } else {\n            return conf + unc;\n        }\n    });\n\n    const maxLightning = createMemo(() => {\n        const fed = state.balance?.federation ?? 0n;\n        const ln = state.balance?.lightning ?? 0n;\n        if (fed > ln) {\n            return fed;\n        } else {\n            return ln;\n        }\n    });\n\n    const maxAmountSats = createMemo(() => {\n        return source() === \"onchain\" ? maxOnchain() : maxLightning();\n    });\n\n    const isMax = createMemo(() => {\n        if (source() === \"onchain\") {\n            return amountSats() === maxOnchain();\n        }\n    });\n\n    // Rerun every time the source or amount changes to check for amount errors\n    createEffect(() => {\n        // Don't recompute if sending\n        if (sending()) return;\n        if (source() === \"onchain\" && maxOnchain() < amountSats()) {\n            setError(i18n.t(\"send.error_low_balance\"));\n            return;\n        }\n        if (\n            source() === \"lightning\" &&\n            (state.balance?.lightning ?? 0n) <= amountSats() &&\n            (state.balance?.federation ?? 0n) <= amountSats()\n        ) {\n            setError(i18n.t(\"send.error_low_balance\"));\n            return;\n        }\n        if (\n            source() === \"lightning\" &&\n            !!invoice()?.amount_sats &&\n            amountSats() !== invoice()?.amount_sats\n        ) {\n            setError(\n                i18n.t(\"send.error_invoice_match\", {\n                    amount: invoice()?.amount_sats?.toLocaleString()\n                })\n            );\n            return;\n        }\n        setError(undefined);\n    });\n\n    // Rerun every time the amount changes if we're onchain\n    const [feeEstimate, { refetch }] = createResource(async () => {\n        // If it's under the dust limit don't bother\n        if (amountSats() < 546n) return undefined;\n        if (\n            source() === \"onchain\" &&\n            amountSats() &&\n            amountSats() > 0n &&\n            address()\n        ) {\n            try {\n                // If max we want to use the sweep fee estimator\n                if (isMax()) {\n                    return await sw.estimate_sweep_tx_fee(address()!);\n                }\n\n                const estimate = await sw.estimate_tx_fee(\n                    address()!,\n                    amountSats(),\n                    undefined\n                );\n                console.log(\"estimate\", estimate);\n                return estimate;\n            } catch (e) {\n                // This is usually because the amount is too small or too large so we can ignore\n                console.error(e);\n            }\n        }\n        return undefined;\n    });\n\n    createEffect(() => {\n        if (amountSats() && amountSats() > 0n) {\n            refetch();\n        }\n    });\n\n    const [parsingDestination, setParsingDestination] = createSignal(false);\n\n    const [decodingLnUrl, setDecodingLnUrl] = createSignal(false);\n\n    function handleDestination(source: ParsedParams | undefined) {\n        if (!source) return;\n        setParsingDestination(true);\n        setOriginalScan(source.original);\n        try {\n            if (source.address) setAddress(source.address);\n            if (source.payjoin_enabled)\n                setPayjoinEnabled(source.payjoin_enabled);\n            if (source.memo) setDescription(source.memo);\n            if (source.contact_id) setContactId(source.contact_id);\n\n            if (source.invoice) {\n                processInvoice(source as ParsedParams & { invoice: string });\n            } else if (source.node_pubkey) {\n                processNodePubkey(\n                    source as ParsedParams & { node_pubkey: string }\n                );\n            } else if (source.lnurl) {\n                console.log(\"processing lnurl\");\n                processLnurl(source as ParsedParams & { lnurl: string });\n            } else {\n                setAmountSats(source.amount_sats || 0n);\n                if (source.amount_sats) setIsAmtEditable(false);\n                setSource(\"onchain\");\n            }\n            // Return the source just to trigger `decodedDestination` as not undefined\n            return source;\n        } catch (e) {\n            console.error(\"error\", e);\n        } finally {\n            setParsingDestination(false);\n        }\n    }\n\n    // A ParsedParams with an invoice in it\n    function processInvoice(source: ParsedParams & { invoice: string }) {\n        sw.decode_invoice(source.invoice!)\n            .then((invoice) => {\n                if (!invoice) return;\n                if (invoice.expire <= Date.now() / 1000) {\n                    navigate(\"/search\");\n                    throw new Error(i18n.t(\"send.error_expired\"));\n                }\n\n                if (invoice.amount_sats) {\n                    setAmountSats(invoice.amount_sats);\n                    setIsAmtEditable(false);\n                }\n                setInvoice(invoice);\n                setIsHodlInvoice(invoice.potential_hodl_invoice);\n                setSource(\"lightning\");\n            })\n            .catch((e) => showToast(eify(e)));\n    }\n\n    // A ParsedParams with a node_pubkey in it\n    function processNodePubkey(source: ParsedParams & { node_pubkey: string }) {\n        setAmountSats(source.amount_sats || 0n);\n        setNodePubkey(source.node_pubkey);\n        setSource(\"lightning\");\n    }\n\n    // A ParsedParams with an lnurl in it\n    function processLnurl(source: ParsedParams & { lnurl: string }) {\n        setDecodingLnUrl(true);\n        sw.decode_lnurl(source.lnurl)\n            .then((lnurlParams) => {\n                setDecodingLnUrl(false);\n                if (lnurlParams.tag === \"payRequest\") {\n                    if (lnurlParams.min == lnurlParams.max) {\n                        setAmountSats(lnurlParams.min / 1000n);\n                        setIsAmtEditable(false);\n                    } else {\n                        setAmountSats(source.amount_sats || 0n);\n                    }\n\n                    if (source.lightning_address) {\n                        setLnAddress(source.lightning_address);\n                        setIsHodlInvoice(\n                            source.lightning_address\n                                .toLowerCase()\n                                .includes(\"zeuspay.com\")\n                        );\n                    }\n                    setLnurlp(source.lnurl);\n                    setSource(\"lightning\");\n                }\n                // TODO: this is a bit of a hack, ideally we do more nav from the megastore\n                if (lnurlParams.tag === \"withdrawRequest\") {\n                    actions.setScanResult(source);\n                    navigate(\"/redeem\");\n                }\n            })\n            .catch((e) => showToast(eify(e)));\n    }\n\n    createEffect(() => {\n        if (amountInput() === \"\") {\n            setAmountSats(0n);\n        } else {\n            const parsed = BigInt(amountInput());\n            console.log(\"parsed\", parsed);\n            if (!parsed) {\n                setUnparsedAmount(true);\n            }\n            if (parsed > 0n) {\n                setAmountSats(parsed);\n                setUnparsedAmount(false);\n            } else {\n                setUnparsedAmount(true);\n            }\n        }\n    });\n\n    // If we got here from a scan or search\n    onMount(() => {\n        if (state.scan_result) {\n            handleDestination(state.scan_result);\n            actions.setScanResult(undefined);\n        }\n    });\n\n    async function handleSend() {\n        try {\n            setSending(true);\n            const bolt11 = invoice()?.bolt11;\n            const sentDetails: Partial<SentDetails> = {};\n\n            const tags = contactId() ? [contactId()!] : [];\n\n            if (whatForInput()) {\n                tags.push(whatForInput().trim());\n            }\n\n            if (source() === \"lightning\" && invoice() && bolt11) {\n                sentDetails.destination = bolt11;\n                // If the invoice has sats use that, otherwise we pass the user-defined amount\n                if (invoice()?.amount_sats) {\n                    const payment = await sw.pay_invoice(\n                        bolt11,\n                        undefined,\n                        tags\n                    );\n                    sentDetails.amount = payment?.amount_sats;\n                    sentDetails.payment_hash = payment?.payment_hash;\n                    sentDetails.fee_estimate = payment?.fees_paid || 0;\n                } else {\n                    const payment = await sw.pay_invoice(\n                        bolt11,\n                        amountSats(),\n                        tags\n                    );\n                    sentDetails.amount = payment?.amount_sats;\n                    sentDetails.payment_hash = payment?.payment_hash;\n                    sentDetails.fee_estimate = payment?.fees_paid || 0;\n                }\n            } else if (source() === \"lightning\" && nodePubkey()) {\n                const payment = await sw.keysend(\n                    nodePubkey()!,\n                    amountSats(),\n                    undefined, // todo add optional keysend message\n                    tags\n                );\n\n                // TODO: handle timeouts\n                if (!payment?.paid) {\n                    throw new Error(i18n.t(\"send.error_keysend\"));\n                } else {\n                    sentDetails.amount = payment?.amount_sats;\n                    sentDetails.payment_hash = payment?.payment_hash;\n                    sentDetails.fee_estimate = payment?.fees_paid || 0;\n                }\n            } else if (source() === \"lightning\" && lnurlp()) {\n                const zapNpub =\n                    visibility() !== \"Not Available\" && contact()?.npub\n                        ? contact()?.npub\n                        : undefined;\n                const payment = await sw.lnurl_pay(\n                    lnurlp()!,\n                    amountSats(),\n                    zapNpub, // zap_npub\n                    tags,\n                    whatForInput(), // comment\n                    visibility()\n                );\n                sentDetails.payment_hash = payment?.payment_hash;\n\n                if (!payment?.paid) {\n                    throw new Error(i18n.t(\"send.error_LNURL\"));\n                } else {\n                    sentDetails.amount = payment?.amount_sats;\n                    sentDetails.payment_hash = payment?.payment_hash;\n                    sentDetails.fee_estimate = payment?.fees_paid || 0;\n                }\n            } else if (source() === \"onchain\" && address()) {\n                if (isMax()) {\n                    // If we're trying to send the max amount, use the sweep method instead of regular send\n                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n                    const txid = await sw.sweep_wallet(address()!, tags);\n\n                    sentDetails.amount = amountSats();\n                    sentDetails.destination = address();\n                    sentDetails.txid = txid;\n                    sentDetails.fee_estimate = feeEstimate.latest ?? 0;\n                } else if (payjoinEnabled()) {\n                    const txid = await sw.send_payjoin(\n                        originalScan()!,\n                        amountSats(),\n                        tags\n                    );\n                    sentDetails.amount = amountSats();\n                    sentDetails.destination = address();\n                    sentDetails.txid = txid;\n                    sentDetails.fee_estimate = feeEstimate.latest ?? 0;\n                } else {\n                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n                    const txid = await sw.send_to_address(\n                        address()!,\n                        amountSats(),\n                        tags\n                    );\n                    sentDetails.amount = amountSats();\n                    sentDetails.destination = address();\n                    sentDetails.txid = txid;\n                    sentDetails.fee_estimate = feeEstimate.latest ?? 0;\n                }\n            }\n            if (sentDetails.payment_hash || sentDetails.txid) {\n                setSentDetails(sentDetails as SentDetails);\n                await vibrateSuccess();\n            } else {\n                // TODO: what should we do here? hopefully this never happens?\n                console.error(\"failed to send: no payment hash or txid\");\n            }\n        } catch (e) {\n            const error = eify(e);\n            setSentDetails({ failure_reason: error.message });\n            // TODO: figure out ux of when we want to show toast vs error screen\n            // showToast(eify(e))\n            console.error(e);\n        } finally {\n            setSending(false);\n        }\n    }\n\n    const sendButtonDisabled = createMemo(() => {\n        return (\n            unparsedAmount() ||\n            parsingDestination() ||\n            sending() ||\n            amountSats() == 0n ||\n            amountSats() === undefined ||\n            (source() === \"onchain\" && amountSats() < 546n) ||\n            !!error()\n        );\n    });\n\n    const lightningMethod = createMemo<MethodChoice>(() => {\n        return {\n            method: \"lightning\",\n            maxAmountSats: maxLightning()\n        };\n    });\n\n    const onchainMethod = createMemo<MethodChoice>(() => {\n        return {\n            method: \"onchain\",\n            maxAmountSats: maxOnchain()\n        };\n    });\n\n    const sendMethods = createMemo<MethodChoice[]>(() => {\n        if (lnAddress() || lnurlp() || nodePubkey()) {\n            return [lightningMethod()];\n        }\n\n        if (invoice() && address()) {\n            return [lightningMethod(), onchainMethod()];\n        }\n\n        if (invoice()) {\n            return [lightningMethod()];\n        }\n\n        if (address()) {\n            return [onchainMethod()];\n        }\n\n        // We should never get here\n        console.error(\"No send methods found\");\n\n        return [];\n    });\n\n    function setSourceFromMethod(method: MethodChoice) {\n        if (method.method === \"lightning\") {\n            setSource(\"lightning\");\n        } else if (method.method === \"onchain\") {\n            setSource(\"onchain\");\n        }\n    }\n\n    const activeMethod = createMemo(() => {\n        if (source() === \"lightning\") {\n            return lightningMethod();\n        } else if (source() === \"onchain\") {\n            return onchainMethod();\n        }\n    });\n\n    const [visibility, setVisibility] =\n        createSignal<PrivacyLevel>(\"Not Available\");\n\n    // If the contact has an npub and it's an lnurlp send set the default visibility to private zap\n    createEffect(() => {\n        contact()?.npub && lnurlp() && setVisibility(\"Private\");\n    });\n\n    function toggleVisibility() {\n        if (visibility() === \"Not Available\") {\n            setVisibility(\"Private\");\n        } else if (visibility() === \"Private\") {\n            setVisibility(\"Public\");\n        } else {\n            setVisibility(\"Not Available\");\n        }\n    }\n\n    async function getContact(id: string) {\n        console.log(\"fetching contact\", id);\n        try {\n            const contact = await sw.get_tag_item(id);\n            console.log(\"fetching contact\", contact);\n            // This shouldn't happen\n            if (!contact) throw new Error(\"Contact not found\");\n            return contact;\n        } catch (e) {\n            console.error(e);\n            showToast(eify(e));\n        }\n    }\n\n    const [contact] = createResource(contactId, getContact);\n    const location = useLocation();\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackPop default=\"/\" />\n                <SuccessModal\n                    confirmText={\n                        sentDetails()?.amount\n                            ? i18n.t(\"common.nice\")\n                            : i18n.t(\"common.home\")\n                    }\n                    open={!!sentDetails()}\n                    setOpen={(open: boolean) => {\n                        if (!open) setSentDetails(undefined);\n                    }}\n                    onConfirm={() => {\n                        setSentDetails(undefined);\n                        const state = location.state as { previous?: string };\n                        // If we're coming from a chat, we want to go back to the chat\n                        // Otherwise we want to go home\n                        if (\n                            state?.previous &&\n                            state?.previous.includes(\"chat/\")\n                        ) {\n                            navigate(state?.previous);\n                        } else {\n                            navigate(\"/\");\n                        }\n                    }}\n                >\n                    <Switch>\n                        <Match when={sentDetails()?.failure_reason}>\n                            <Failure\n                                reason={\n                                    sentDetails()?.failure_reason ||\n                                    \"Payment failed for an unknown reason\"\n                                }\n                            />\n                        </Match>\n                        <Match when={true}>\n                            <Show when={detailsId() && detailsKind()}>\n                                <ActivityDetailsModal\n                                    open={detailsOpen()}\n                                    kind={detailsKind()}\n                                    id={detailsId()}\n                                    setOpen={setDetailsOpen}\n                                />\n                            </Show>\n                            <MegaCheck />\n                            <h1 class=\"mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl\">\n                                {sentDetails()?.amount\n                                    ? source() === \"onchain\"\n                                        ? i18n.t(\"send.payment_initiated\")\n                                        : i18n.t(\"send.payment_sent\")\n                                    : sentDetails()?.failure_reason}\n                            </h1>\n                            <div class=\"flex flex-col items-center gap-1\">\n                                <div class=\"text-xl\">\n                                    <AmountSats\n                                        amountSats={sentDetails()?.amount}\n                                        icon=\"minus\"\n                                    />\n                                </div>\n                                <div class=\"text-white/70\">\n                                    <AmountFiat\n                                        amountSats={sentDetails()?.amount}\n                                        denominationSize=\"sm\"\n                                    />\n                                </div>\n                            </div>\n                            <hr class=\"w-16 bg-m-grey-400\" />\n                            <Fee amountSats={sentDetails()?.fee_estimate} />\n                            <p\n                                class=\"cursor-pointer underline\"\n                                onClick={openDetailsModal}\n                            >\n                                {i18n.t(\"common.view_payment_details\")}\n                            </p>\n                        </Match>\n                    </Switch>\n                </SuccessModal>\n                <div class=\"flex flex-1 flex-col justify-between gap-2\">\n                    <Suspense fallback={<LoadingShimmer />}>\n                        <DestinationShower\n                            source={source()}\n                            description={description()}\n                            invoice={invoice()}\n                            address={address()}\n                            nodePubkey={nodePubkey()}\n                            lnurl={lnurlp()}\n                            lightning_address={lnAddress()}\n                            contact={contact()}\n                        />\n                    </Suspense>\n                    <div class=\"flex-1\" />\n                    {/* Need both these versions so that we make sure to get the right initial amount on load */}\n                    <Show when={isAmtEditable()}>\n                        <AmountEditable\n                            initialAmountSats={amountSats()}\n                            setAmountSats={setAmountInput}\n                            onSubmit={() =>\n                                sendButtonDisabled() ? undefined : handleSend()\n                            }\n                            activeMethod={activeMethod()}\n                            methods={sendMethods()}\n                            setChosenMethod={setSourceFromMethod}\n                        />\n                    </Show>\n                    <Show when={payjoinEnabled() && source() === \"onchain\"}>\n                        <InfoBox accent=\"green\">\n                            <p>{i18n.t(\"send.payjoin_send\")}</p>\n                        </InfoBox>\n                    </Show>\n                    <Show when={!isAmtEditable()}>\n                        <AmountEditable\n                            initialAmountSats={amountSats()}\n                            setAmountSats={setAmountInput}\n                            frozenAmount={true}\n                            onSubmit={() =>\n                                sendButtonDisabled() ? undefined : handleSend()\n                            }\n                            activeMethod={activeMethod()}\n                            methods={sendMethods()}\n                            setChosenMethod={setSourceFromMethod}\n                        />\n                    </Show>\n                    <Suspense>\n                        <Show when={feeEstimate.latest}>\n                            <FeeDisplay\n                                amountSats={amountSats().toString()}\n                                fee={feeEstimate.latest!.toString()}\n                                maxAmountSats={maxAmountSats()}\n                            />\n                        </Show>\n                    </Suspense>\n                    <Show when={isHodlInvoice()}>\n                        <InfoBox accent=\"red\">\n                            <p>{i18n.t(\"send.hodl_invoice_warning\")}</p>\n                        </InfoBox>\n                    </Show>\n                    <Show when={error() && !decodingLnUrl()}>\n                        <InfoBox accent=\"red\">\n                            <p>{error()}</p>\n                        </InfoBox>\n                    </Show>\n                    <div class=\"flex-1\" />\n\n                    <VStack>\n                        <Suspense>\n                            <div class=\"flex w-full\">\n                                <SharpButton\n                                    onClick={toggleVisibility}\n                                    // If there's no npub, or if there's an invoice, don't let switch to zap\n                                    disabled={!contact()?.npub || !!invoice()}\n                                >\n                                    <div class=\"flex items-center gap-2\">\n                                        <Switch>\n                                            <Match\n                                                when={\n                                                    visibility() ===\n                                                    \"Not Available\"\n                                                }\n                                            >\n                                                <EyeOff class=\"h-4 w-4\" />\n                                                <span>\n                                                    {i18n.t(\"send.private\")}\n                                                </span>\n                                            </Match>\n                                            <Match\n                                                when={\n                                                    visibility() === \"Private\"\n                                                }\n                                            >\n                                                <Zap class=\"h-4 w-4\" />\n                                                <EyeOff class=\"h-4 w-4\" />\n                                                <span>\n                                                    {i18n.t(\"send.privatezap\")}\n                                                </span>\n                                            </Match>\n                                            <Match\n                                                when={visibility() === \"Public\"}\n                                            >\n                                                <Zap class=\"h-4 w-4\" />\n                                                <Eye class=\"h-4 w-4\" />\n                                                <span>\n                                                    {i18n.t(\"send.publiczap\")}\n                                                </span>\n                                            </Match>\n                                        </Switch>\n                                    </div>\n                                </SharpButton>\n                            </div>\n                        </Suspense>\n                        <form\n                            onSubmit={async (e) => {\n                                e.preventDefault();\n                                if (!sendButtonDisabled()) {\n                                    await handleSend();\n                                }\n                            }}\n                        >\n                            <SimpleInput\n                                type=\"text\"\n                                placeholder={\n                                    visibility() === \"Not Available\"\n                                        ? i18n.t(\"send.what_for\")\n                                        : i18n.t(\"send.zap_note\")\n                                }\n                                onInput={(e) =>\n                                    setWhatForInput(e.currentTarget.value)\n                                }\n                                value={whatForInput()}\n                            />\n                        </form>\n                        <Button\n                            disabled={sendButtonDisabled()}\n                            intent=\"blue\"\n                            onClick={handleSend}\n                            loading={sending()}\n                        >\n                            {sending()\n                                ? i18n.t(\"send.sending\")\n                                : i18n.t(\"send.confirm_send\")}\n                        </Button>\n                    </VStack>\n                </div>\n            </DefaultMain>\n            <NavBar activeTab=\"send\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/Swap.tsx",
    "content": "import { createForm, required } from \"@modular-forms/solid\";\nimport { MutinyChannel } from \"@mutinywallet/mutiny-wasm\";\nimport { useNavigate } from \"@solidjs/router\";\nimport {\n    createEffect,\n    createMemo,\n    createResource,\n    createSignal,\n    For,\n    Match,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    ActivityDetailsModal,\n    AmountEditable,\n    AmountFiat,\n    BackLink,\n    Button,\n    Card,\n    DefaultMain,\n    FeeDisplay,\n    HackActivityType,\n    InfoBox,\n    LargeHeader,\n    MegaCheck,\n    MegaEx,\n    MutinyWalletGuard,\n    NavBar,\n    showToast,\n    SuccessModal,\n    TextField,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify, vibrateSuccess } from \"~/utils\";\n\nconst CHANNEL_FEE_ESTIMATE_ADDRESS =\n    \"bc1qf7546vg73ddsjznzq57z3e8jdn6gtw6au576j07kt6d9j7nz8mzsyn6lgf\";\n\ntype PeerConnectForm = {\n    peer: string;\n};\n\ntype ChannelOpenDetails = {\n    channel?: MutinyChannel;\n    failure_reason?: Error;\n};\n\nexport function Swap() {\n    const [state, _actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n    const i18n = useI18n();\n\n    const [amountSats, setAmountSats] = createSignal(0n);\n    const [isConnecting, setIsConnecting] = createSignal(false);\n\n    const [loading, setLoading] = createSignal(false);\n\n    const [selectedPeer, setSelectedPeer] = createSignal<string>(\"\");\n\n    // Details Modal\n    const [detailsOpen, setDetailsOpen] = createSignal(false);\n    const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();\n    const [detailsId, setDetailsId] = createSignal(\"\");\n\n    const [channelOpenResult, setChannelOpenResult] =\n        createSignal<ChannelOpenDetails>();\n\n    function openDetailsModal() {\n        const paymentTxId =\n            channelOpenResult()?.channel?.outpoint?.split(\":\")[0];\n        const kind: HackActivityType = \"ChannelOpen\";\n\n        console.log(\"Opening details modal: \", paymentTxId, kind);\n\n        if (!paymentTxId) {\n            console.warn(\"No id provided to openDetailsModal\");\n            return;\n        }\n        if (paymentTxId !== undefined) {\n            setDetailsId(paymentTxId);\n        }\n        setDetailsKind(kind);\n        setDetailsOpen(true);\n    }\n\n    function resetState() {\n        setAmountSats(0n);\n        setIsConnecting(false);\n        setLoading(false);\n        setSelectedPeer(\"\");\n        setChannelOpenResult(undefined);\n    }\n\n    const hasLsp = createMemo(() => {\n        return !!state.settings?.lsp;\n    });\n\n    const getPeers = async () => {\n        return await sw?.list_peers();\n    };\n\n    const [peers, { refetch }] = createResource(getPeers);\n\n    const [_peerForm, { Form, Field }] = createForm<PeerConnectForm>();\n\n    const onSubmit = async (values: PeerConnectForm) => {\n        setIsConnecting(true);\n        try {\n            const peerConnectString = values.peer.trim();\n\n            await sw.connect_to_peer(peerConnectString);\n\n            await refetch();\n\n            // If peers list contains the peer we just connected to, select it\n            const peer = peers()?.find(\n                (p) => p.pubkey === peerConnectString.split(\"@\")[0]\n            );\n\n            if (peer) {\n                setSelectedPeer(peer.pubkey);\n            } else {\n                showToast(new Error(i18n.t(\"swap.peer_not_found\")));\n            }\n        } catch (e) {\n            showToast(eify(e));\n        } finally {\n            setIsConnecting(false);\n        }\n    };\n\n    const handlePeerSelect = (\n        e: Event & {\n            currentTarget: HTMLSelectElement;\n            target: HTMLSelectElement;\n        }\n    ) => {\n        setSelectedPeer(e.currentTarget.value);\n    };\n\n    const handleSwap = async () => {\n        if (canSwap()) {\n            try {\n                setLoading(true);\n\n                let peer = undefined;\n\n                if (!hasLsp()) {\n                    peer = selectedPeer();\n                }\n\n                if (isMax()) {\n                    const new_channel = await sw.sweep_all_to_channel(peer);\n\n                    setChannelOpenResult({ channel: new_channel });\n                } else {\n                    const new_channel = await sw.open_channel(\n                        peer,\n                        amountSats()\n                    );\n\n                    setChannelOpenResult({ channel: new_channel });\n                }\n\n                await vibrateSuccess();\n            } catch (e) {\n                setChannelOpenResult({ failure_reason: eify(e) });\n            } finally {\n                setLoading(false);\n            }\n        }\n    };\n\n    const canSwap = createMemo(() => {\n        const balance =\n            (state.balance?.confirmed || 0n) +\n            (state.balance?.unconfirmed || 0n);\n\n        return (\n            (!!selectedPeer() || !!hasLsp()) &&\n            amountSats() >= 100000n &&\n            amountSats() <= balance\n        );\n    });\n\n    const [amountWarning, { refetch: refetchAmountWarning }] = createResource(\n        async () => {\n            if (amountSats() === 0n || !!channelOpenResult()) {\n                return undefined;\n            }\n\n            if (amountSats() < 100000n) {\n                return i18n.t(\"swap.channel_too_small\", { amount: \"100,000\" });\n            }\n\n            if (\n                amountSats() >\n                    (state.balance?.confirmed || 0n) +\n                        (state.balance?.unconfirmed || 0n) ||\n                !feeEstimate()\n            ) {\n                return i18n.t(\"swap.insufficient_funds\");\n            }\n\n            return undefined;\n        }\n    );\n\n    function calculateMaxOnchain() {\n        return (\n            (state.balance?.confirmed ?? 0n) +\n            (state.balance?.unconfirmed ?? 0n)\n        );\n    }\n\n    const maxOnchain = createMemo(() => {\n        return calculateMaxOnchain();\n    });\n\n    const isMax = createMemo(() => {\n        return amountSats() === calculateMaxOnchain();\n    });\n\n    const [feeEstimate, { refetch: refetchFeeEstimate }] = createResource(\n        async () => {\n            // If it's under the dust limit don't bother\n            if (amountSats() < 546n) return undefined;\n            const max = maxOnchain();\n            // If max we want to use the sweep fee estimator\n            if (amountSats() > 0n && amountSats() === max) {\n                try {\n                    return await sw.estimate_sweep_channel_open_fee();\n                } catch (e) {\n                    console.error(e);\n                    return undefined;\n                }\n            }\n\n            if (amountSats() > 0n) {\n                try {\n                    return await sw.estimate_tx_fee(\n                        CHANNEL_FEE_ESTIMATE_ADDRESS,\n                        amountSats(),\n                        undefined\n                    );\n                } catch (e) {\n                    console.error(e);\n                    return undefined;\n                }\n            }\n            return undefined;\n        }\n    );\n\n    createEffect(() => {\n        if (amountSats()) {\n            refetchAmountWarning();\n            refetchFeeEstimate();\n        }\n    });\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackLink />\n                <LargeHeader>{i18n.t(\"swap.header\")}</LargeHeader>\n                <SuccessModal\n                    confirmText={\n                        channelOpenResult()?.channel\n                            ? i18n.t(\"common.nice\")\n                            : i18n.t(\"common.home\")\n                    }\n                    open={!!channelOpenResult()}\n                    setOpen={(open: boolean) => {\n                        if (!open) resetState();\n                    }}\n                    onConfirm={() => {\n                        resetState();\n                        navigate(\"/\");\n                    }}\n                >\n                    <Switch>\n                        <Match when={channelOpenResult()?.failure_reason}>\n                            <MegaEx />\n                            <h1 class=\"mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl\">\n                                {channelOpenResult()?.failure_reason\n                                    ? channelOpenResult()?.failure_reason\n                                          ?.message\n                                    : \"\"}\n                            </h1>\n                            {/*TODO: Error hint needs to be added for possible failure reasons*/}\n                        </Match>\n                        <Match when={channelOpenResult()?.channel}>\n                            <Show when={detailsId() && detailsKind()}>\n                                <ActivityDetailsModal\n                                    open={detailsOpen()}\n                                    kind={detailsKind()}\n                                    id={detailsId()}\n                                    setOpen={setDetailsOpen}\n                                />\n                            </Show>\n                            <MegaCheck />\n                            <div class=\"flex flex-col justify-center\">\n                                <h1 class=\"mb-2 mt-4 w-full justify-center text-center text-2xl font-semibold md:text-3xl\">\n                                    {i18n.t(\"swap.initiated\")}\n                                </h1>\n                                <p class=\"text-center text-xl\">\n                                    {i18n.t(\"swap.sats_added\", {\n                                        amount: (\n                                            Number(\n                                                channelOpenResult()?.channel\n                                                    ?.balance\n                                            ) +\n                                            Number(\n                                                channelOpenResult()?.channel\n                                                    ?.reserve\n                                            )\n                                        ).toLocaleString()\n                                    })}\n                                </p>\n                                <div class=\"text-center text-sm text-white/70\">\n                                    <Suspense>\n                                        <AmountFiat\n                                            amountSats={\n                                                Number(\n                                                    channelOpenResult()?.channel\n                                                        ?.balance\n                                                ) +\n                                                Number(\n                                                    channelOpenResult()?.channel\n                                                        ?.reserve\n                                                )\n                                            }\n                                        />\n                                    </Suspense>\n                                </div>\n                            </div>\n                            <hr class=\"w-16 bg-m-grey-400\" />\n                            <p\n                                class=\"cursor-pointer underline\"\n                                onClick={openDetailsModal}\n                            >\n                                {i18n.t(\"common.view_payment_details\")}\n                            </p>\n                        </Match>\n                    </Switch>\n                </SuccessModal>\n                <div class=\"flex flex-1 flex-col justify-between gap-2\">\n                    <div class=\"flex-1\" />\n                    <VStack biggap>\n                        <Show when={!hasLsp()}>\n                            <Card>\n                                <VStack>\n                                    <div class=\"flex w-full flex-col gap-2\">\n                                        <label\n                                            for=\"peerselect\"\n                                            class=\"text-sm font-semibold uppercase\"\n                                        >\n                                            {i18n.t(\"swap.use_existing\")}\n                                        </label>\n                                        <select\n                                            name=\"peerselect\"\n                                            class=\"w-full truncate rounded bg-black px-4 py-2\"\n                                            onChange={handlePeerSelect}\n                                            value={selectedPeer()}\n                                        >\n                                            <option value=\"\" class=\"\" selected>\n                                                {i18n.t(\"swap.choose_peer\")}\n                                            </option>\n                                            <For each={peers()}>\n                                                {(peer) => (\n                                                    <option value={peer.pubkey}>\n                                                        {peer.alias ??\n                                                            peer.pubkey}\n                                                    </option>\n                                                )}\n                                            </For>\n                                        </select>\n                                    </div>\n                                    <Show when={!selectedPeer()}>\n                                        <Form\n                                            onSubmit={onSubmit}\n                                            class=\"flex flex-col gap-4\"\n                                        >\n                                            <Field\n                                                name=\"peer\"\n                                                validate={[required(\"\")]}\n                                            >\n                                                {(field, props) => (\n                                                    <TextField\n                                                        {...props}\n                                                        value={field.value}\n                                                        error={field.error}\n                                                        label={i18n.t(\n                                                            \"swap.peer_connect_label\"\n                                                        )}\n                                                        placeholder={i18n.t(\n                                                            \"swap.peer_connect_placeholder\"\n                                                        )}\n                                                    />\n                                                )}\n                                            </Field>\n                                            <Button\n                                                layout=\"small\"\n                                                type=\"submit\"\n                                                disabled={isConnecting()}\n                                            >\n                                                {isConnecting()\n                                                    ? i18n.t(\"swap.connecting\")\n                                                    : i18n.t(\"swap.connect\")}\n                                            </Button>\n                                        </Form>\n                                    </Show>\n                                </VStack>\n                            </Card>\n                        </Show>\n                        <AmountEditable\n                            initialAmountSats={amountSats()}\n                            setAmountSats={setAmountSats}\n                            activeMethod={{\n                                method: \"onchain\",\n                                maxAmountSats: maxOnchain()\n                            }}\n                            methods={[\n                                {\n                                    method: \"onchain\",\n                                    maxAmountSats: maxOnchain()\n                                }\n                            ]}\n                        />\n                        <Suspense>\n                            <Show\n                                when={feeEstimate.latest && amountSats() > 0n}\n                            >\n                                <FeeDisplay\n                                    amountSats={amountSats().toString()}\n                                    fee={feeEstimate.latest!.toString()}\n                                    maxAmountSats={maxOnchain()}\n                                />\n                            </Show>\n                        </Suspense>\n                        <Suspense>\n                            <Show\n                                when={amountWarning.latest && amountSats() > 0n}\n                            >\n                                <InfoBox accent={\"red\"}>\n                                    {amountWarning.latest}\n                                </InfoBox>\n                            </Show>\n                        </Suspense>\n                    </VStack>\n                    <div class=\"flex-1\" />\n                    <VStack>\n                        <Button\n                            disabled={!canSwap()}\n                            intent=\"blue\"\n                            onClick={handleSwap}\n                            loading={loading()}\n                        >\n                            {i18n.t(\"swap.confirm_swap\")}\n                        </Button>\n                    </VStack>\n                </div>\n            </DefaultMain>\n            <NavBar activeTab=\"none\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/SwapLightning.tsx",
    "content": "import { FedimintSweepResult } from \"@mutinywallet/mutiny-wasm\";\nimport { useNavigate } from \"@solidjs/router\";\nimport {\n    createMemo,\n    createSignal,\n    Match,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    AmountEditable,\n    AmountFiat,\n    BackButton,\n    BackLink,\n    Button,\n    DefaultMain,\n    Failure,\n    Fee,\n    FeeDisplay,\n    InfoBox,\n    LargeHeader,\n    MegaCheck,\n    MutinyWalletGuard,\n    NavBar,\n    ReceiveWarnings,\n    SuccessModal,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify, vibrateSuccess } from \"~/utils\";\n\ntype SweepResultDetails = {\n    result?: FedimintSweepResult;\n    failure_reason?: string;\n};\n\nexport function SwapLightning() {\n    const [state, _actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n    const i18n = useI18n();\n\n    const [stage, setStage] = createSignal<\"start\" | \"preview\" | \"done\">(\n        \"start\"\n    );\n\n    const [amountSats, setAmountSats] = createSignal(0n);\n    const [invoice, setInvoice] = createSignal(\"\");\n    const [feeSats, setFeeSats] = createSignal(0n);\n    const [maxFederationBalanceBeforeSwap, setMaxFederationBalanceBeforeSwap] =\n        createSignal(0n);\n\n    const [loading, setLoading] = createSignal(false);\n\n    const [sweepResult, setSweepResult] = createSignal<SweepResultDetails>();\n\n    function resetState() {\n        setAmountSats(0n);\n        setFeeSats(0n);\n        setMaxFederationBalanceBeforeSwap(0n);\n        setLoading(false);\n        setSweepResult(undefined);\n        setStage(\"start\");\n    }\n\n    const handleSwap = async () => {\n        try {\n            setLoading(true);\n            setFeeEstimateWarning(undefined);\n\n            const result = await sw.sweep_federation_balance_to_invoice(\n                undefined,\n                invoice()\n            );\n            setSweepResult({ result: result });\n\n            await vibrateSuccess();\n        } catch (e) {\n            const error = eify(e);\n            setSweepResult({ failure_reason: error.message });\n            console.error(e);\n        } finally {\n            setLoading(false);\n        }\n    };\n\n    const canSwap = () => {\n        const balance = state.balance?.federation || 0n;\n        return amountSats() > 0n && amountSats() <= balance;\n    };\n\n    const amountWarning = () => {\n        if (amountSats() === 0n || !!sweepResult() || loading()) {\n            return undefined;\n        }\n\n        if (amountSats() > (state.balance?.federation || 0n)) {\n            return i18n.t(\"swap_lightning.insufficient_funds\");\n        }\n\n        return undefined;\n    };\n\n    function calculateMaxFederation() {\n        return state.balance?.federation ?? 0n;\n    }\n\n    const maxFederationBalance = createMemo(() => {\n        return calculateMaxFederation();\n    });\n\n    const isMax = createMemo(() => {\n        return amountSats() === calculateMaxFederation();\n    });\n\n    const feeIsSet = createMemo(() => {\n        return feeSats() !== 0n;\n    });\n\n    const [feeEstimateWarning, setFeeEstimateWarning] = createSignal<Error>();\n\n    const feeEstimate = async () => {\n        try {\n            setLoading(true);\n            setFeeEstimateWarning(undefined);\n\n            const mutinyInvoice = await sw.create_sweep_federation_invoice(\n                isMax() ? undefined : amountSats()\n            );\n\n            if (mutinyInvoice.bolt11) {\n                setInvoice(mutinyInvoice.bolt11);\n            } else {\n                console.error(\"No invoice created\");\n                // this should never happen\n                throw new Error(\"No invoice created\");\n            }\n\n            if (mutinyInvoice.fees_paid) {\n                setFeeSats(mutinyInvoice.fees_paid);\n            }\n            setMaxFederationBalanceBeforeSwap(calculateMaxFederation());\n\n            setStage(\"preview\");\n        } catch (e) {\n            console.error(e);\n            const err = eify(e);\n            if (err.message === \"Satoshi amount is invalid\") {\n                setFeeEstimateWarning(\n                    new Error(i18n.t(\"swap_lightning.too_small\"))\n                );\n            } else {\n                setFeeEstimateWarning(err);\n            }\n        } finally {\n            setLoading(false);\n        }\n    };\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <Switch>\n                    <Match when={stage() === \"start\"}>\n                        <BackLink />\n                        <LargeHeader>\n                            {i18n.t(\"swap_lightning.header\")}\n                        </LargeHeader>\n                    </Match>\n                    <Match when={stage() === \"preview\"}>\n                        <BackButton\n                            title={i18n.t(\"receive.edit\")}\n                            onClick={() => setStage(\"start\")}\n                            showOnDesktop\n                        />\n                        <LargeHeader>\n                            {i18n.t(\"swap_lightning.header_preview\")}\n                        </LargeHeader>\n                    </Match>\n                </Switch>\n                <SuccessModal\n                    confirmText={\n                        sweepResult()?.result\n                            ? i18n.t(\"common.nice\")\n                            : i18n.t(\"common.home\")\n                    }\n                    open={!!sweepResult()}\n                    setOpen={(open: boolean) => {\n                        if (!open) resetState();\n                    }}\n                    onConfirm={() => {\n                        resetState();\n                        navigate(\"/\");\n                    }}\n                >\n                    <Switch>\n                        <Match when={sweepResult()?.failure_reason}>\n                            <Failure reason={sweepResult()!.failure_reason!} />\n                        </Match>\n                        <Match when={sweepResult()?.result}>\n                            <MegaCheck />\n                            <div class=\"flex flex-col justify-center\">\n                                <h1 class=\"mb-2 mt-4 w-full justify-center text-center text-2xl font-semibold md:text-3xl\">\n                                    {i18n.t(\"swap_lightning.completed\")}\n                                </h1>\n                                <p class=\"text-center text-xl\">\n                                    {i18n.t(\"swap_lightning.sats_added\", {\n                                        amount: Number(\n                                            sweepResult()?.result?.amount\n                                        ).toLocaleString()\n                                    })}\n                                </p>\n                                <div class=\"text-center text-sm text-white/70\">\n                                    <Suspense>\n                                        <AmountFiat\n                                            amountSats={Number(\n                                                sweepResult()?.result?.amount\n                                            )}\n                                        />\n                                    </Suspense>\n                                </div>\n                            </div>\n                            <hr class=\"w-16 bg-m-grey-400\" />\n                            <Fee\n                                amountSats={Number(sweepResult()?.result?.fees)}\n                            />\n                        </Match>\n                    </Switch>\n                </SuccessModal>\n                <div class=\"flex flex-1 flex-col justify-between gap-2\">\n                    <div class=\"flex-1\" />\n                    <Switch>\n                        <Match when={stage() === \"start\"}>\n                            <VStack biggap>\n                                <AmountEditable\n                                    initialAmountSats={amountSats()}\n                                    setAmountSats={setAmountSats}\n                                    activeMethod={{\n                                        method: \"lightning\",\n                                        maxAmountSats: maxFederationBalance()\n                                    }}\n                                    methods={[\n                                        {\n                                            method: \"lightning\",\n                                            maxAmountSats:\n                                                maxFederationBalance()\n                                        }\n                                    ]}\n                                />\n                                <ReceiveWarnings\n                                    amountSats={amountSats() || 0n}\n                                    from_fedi_to_ln={true}\n                                />\n                                <Show\n                                    when={amountWarning() && amountSats() > 0n}\n                                >\n                                    <InfoBox accent={\"red\"}>\n                                        {amountWarning()}\n                                    </InfoBox>\n                                </Show>\n                                <Suspense>\n                                    <Show when={feeEstimateWarning()}>\n                                        <InfoBox accent={\"red\"}>\n                                            {feeEstimateWarning()?.message}\n                                        </InfoBox>\n                                    </Show>\n                                </Suspense>\n                            </VStack>\n                            <div class=\"flex-1\" />\n                            <VStack>\n                                <Button\n                                    disabled={!canSwap()}\n                                    intent=\"blue\"\n                                    onClick={feeEstimate}\n                                    loading={loading()}\n                                >\n                                    {i18n.t(\"swap_lightning.preview_swap\")}\n                                </Button>\n                            </VStack>\n                        </Match>\n                        <Match when={stage() === \"preview\"}>\n                            <VStack biggap>\n                                <AmountEditable\n                                    frozenAmount\n                                    initialAmountSats={amountSats()}\n                                    setAmountSats={setAmountSats}\n                                    activeMethod={{\n                                        method: \"lightning\",\n                                        maxAmountSats: maxFederationBalance()\n                                    }}\n                                    methods={[\n                                        {\n                                            method: \"lightning\",\n                                            maxAmountSats:\n                                                maxFederationBalance()\n                                        }\n                                    ]}\n                                />\n                                <Show when={feeIsSet()}>\n                                    <FeeDisplay\n                                        amountSats={amountSats().toString()}\n                                        fee={feeSats()!.toString()}\n                                        maxAmountSats={\n                                            maxFederationBalanceBeforeSwap()!\n                                        }\n                                    />\n                                </Show>\n                                <Show\n                                    when={amountWarning() && amountSats() > 0n}\n                                >\n                                    <InfoBox accent={\"red\"}>\n                                        {amountWarning()}\n                                    </InfoBox>\n                                </Show>\n                            </VStack>\n                            <div class=\"flex-1\" />\n                            <VStack>\n                                <Button\n                                    disabled={!canSwap()}\n                                    intent=\"blue\"\n                                    onClick={handleSwap}\n                                    loading={loading()}\n                                >\n                                    {i18n.t(\"swap_lightning.confirm_swap\")}\n                                </Button>\n                            </VStack>\n                        </Match>\n                    </Switch>\n                </div>\n            </DefaultMain>\n            <NavBar activeTab=\"none\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/Transfer.tsx",
    "content": "import { FedimintSweepResult } from \"@mutinywallet/mutiny-wasm\";\nimport { createAsync, useNavigate, useSearchParams } from \"@solidjs/router\";\nimport { ArrowDown, Users } from \"lucide-solid\";\nimport { createMemo, createSignal, Match, Suspense, Switch } from \"solid-js\";\n\nimport {\n    AmountEditable,\n    AmountFiat,\n    AmountSats,\n    BackLink,\n    Button,\n    DefaultMain,\n    Failure,\n    Fee,\n    LargeHeader,\n    MegaCheck,\n    MutinyWalletGuard,\n    SharpButton,\n    SuccessModal,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify, vibrateSuccess } from \"~/utils\";\n\ntype TransferResultDetails = {\n    result?: FedimintSweepResult;\n    failure_reason?: string;\n};\n\nexport function Transfer() {\n    const [state, _actions, sw] = useMegaStore();\n    const i18n = useI18n();\n    const navigate = useNavigate();\n    const [amountSats, setAmountSats] = createSignal(0n);\n    const [loading, setLoading] = createSignal(false);\n    const [params] = useSearchParams();\n\n    const [transferResult, setTransferResult] =\n        createSignal<TransferResultDetails>();\n\n    const fromFed = () => {\n        return state.federations?.find((f) => f.federation_id === params.from);\n    };\n\n    const toFed = () => {\n        return state.federations?.find((f) => f.federation_id !== params.from);\n    };\n\n    const federationBalances = createAsync(async () => {\n        try {\n            const balances = await sw.get_federation_balances();\n            return balances?.balances || [];\n        } catch (e) {\n            console.error(e);\n            return [];\n        }\n    });\n\n    const calculateMaxFederation = createAsync(async () => {\n        const balance = federationBalances()?.find(\n            (f) => f.identity_federation_id === fromFed()?.federation_id\n        )?.balance;\n        return balance || 0n;\n    });\n\n    const toBalance = createAsync(async () => {\n        return federationBalances()?.find(\n            (f) => f.identity_federation_id === toFed()?.federation_id\n        )?.balance;\n    });\n\n    const isMax = createMemo(() => {\n        return amountSats() === calculateMaxFederation();\n    });\n\n    const canTransfer = createMemo(() => {\n        if (!calculateMaxFederation()) return false;\n        return amountSats() > 0n && amountSats() <= calculateMaxFederation()!;\n    });\n\n    async function handleTransfer() {\n        try {\n            setLoading(true);\n            if (!fromFed()) throw new Error(\"No from federation\");\n            if (!toFed()) throw new Error(\"No to federation\");\n\n            const mutinyInvoice = await sw.create_sweep_federation_invoice(\n                isMax() ? undefined : amountSats(),\n                fromFed()?.federation_id,\n                toFed()?.federation_id\n            );\n\n            if (!mutinyInvoice.bolt11) {\n                // this should never happen\n                throw new Error(\"No invoice created\");\n            }\n\n            const result = await sw.sweep_federation_balance_to_invoice(\n                fromFed()?.federation_id,\n                mutinyInvoice.bolt11\n            );\n\n            setTransferResult({ result: result });\n\n            await vibrateSuccess();\n        } catch (e) {\n            const error = eify(e);\n            setTransferResult({ failure_reason: error.message });\n            console.error(e);\n        } finally {\n            setLoading(false);\n        }\n    }\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <SuccessModal\n                    confirmText={\n                        transferResult()?.result\n                            ? i18n.t(\"common.nice\")\n                            : i18n.t(\"common.home\")\n                    }\n                    open={!!transferResult()}\n                    setOpen={(open: boolean) => {\n                        if (!open) setTransferResult(undefined);\n                    }}\n                    onConfirm={() => {\n                        setTransferResult(undefined);\n                        navigate(\"/\");\n                    }}\n                >\n                    <Switch>\n                        <Match when={transferResult()?.failure_reason}>\n                            <Failure\n                                reason={transferResult()!.failure_reason!}\n                            />\n                        </Match>\n                        <Match when={transferResult()?.result}>\n                            <MegaCheck />\n                            <div class=\"flex flex-col justify-center\">\n                                <h1 class=\"mb-2 mt-4 w-full justify-center text-center text-2xl font-semibold md:text-3xl\">\n                                    {i18n.t(\"transfer.completed\")}\n                                </h1>\n                                <p class=\"text-center text-xl\">\n                                    {i18n.t(\"transfer.sats_moved\", {\n                                        amount: Number(\n                                            transferResult()?.result?.amount\n                                        ).toLocaleString(),\n                                        federation_name:\n                                            toFed()?.federation_name\n                                    })}\n                                </p>\n                                <div class=\"text-center text-sm text-white/70\">\n                                    <Suspense>\n                                        <AmountFiat\n                                            amountSats={Number(\n                                                transferResult()?.result?.amount\n                                            )}\n                                        />\n                                    </Suspense>\n                                </div>\n                            </div>\n                            <hr class=\"w-16 bg-m-grey-400\" />\n                            <Fee\n                                amountSats={Number(\n                                    transferResult()?.result?.fees\n                                )}\n                            />\n                        </Match>\n                    </Switch>\n                </SuccessModal>\n                <BackLink\n                    title={i18n.t(\"common.back\")}\n                    href=\"/settings/federations\"\n                    showOnDesktop\n                />\n                <LargeHeader>{i18n.t(\"transfer.title\")}</LargeHeader>\n                <div class=\"flex flex-1 flex-col justify-between gap-2\">\n                    <div class=\"flex-1\" />\n                    <div class=\"flex flex-col items-center\">\n                        <AmountEditable\n                            initialAmountSats={amountSats()}\n                            setAmountSats={setAmountSats}\n                        />\n                        <SharpButton disabled onClick={() => {}}>\n                            <Users class=\"w-[18px]\" />\n                            {fromFed()?.federation_name}\n                            <AmountSats\n                                amountSats={calculateMaxFederation()}\n                                denominationSize=\"sm\"\n                            />\n                        </SharpButton>\n                        <ArrowDown class=\"h-4 w-4\" />\n                        <SharpButton disabled onClick={() => {}}>\n                            <Users class=\"w-[18px]\" />\n                            {toFed()?.federation_name}\n                            <AmountSats\n                                amountSats={toBalance()}\n                                denominationSize=\"sm\"\n                            />\n                        </SharpButton>\n                    </div>\n                    <div class=\"flex-1\" />\n                    <VStack>\n                        <Button\n                            disabled={!canTransfer()}\n                            intent=\"blue\"\n                            onClick={handleTransfer}\n                            loading={loading()}\n                        >\n                            {i18n.t(\"transfer.confirm\")}\n                        </Button>\n                    </VStack>\n                </div>\n            </DefaultMain>\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/[...404].tsx",
    "content": "import { Title } from \"@solidjs/meta\";\n\nimport { ButtonLink, DefaultMain, LargeHeader } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nexport function NotFound() {\n    const i18n = useI18n();\n    return (\n        <DefaultMain>\n            <Title>{i18n.t(\"error.not_found.title\")}</Title>\n            <LargeHeader>{i18n.t(\"error.not_found.title\")}</LargeHeader>\n            <p>{i18n.t(\"error.not_found.wtf_paul\")}</p>\n            <div class=\"h-full\" />\n            <ButtonLink href=\"/\" intent=\"red\">\n                {i18n.t(\"common.dangit\")}\n            </ButtonLink>\n        </DefaultMain>\n    );\n}\n"
  },
  {
    "path": "src/routes/index.ts",
    "content": "export * from \"./[...404]\";\nexport * from \"./Feedback\";\nexport * from \"./Main\";\nexport * from \"./Receive\";\nexport * from \"./Scanner\";\nexport * from \"./Send\";\nexport * from \"./Search\";\nexport * from \"./Redeem\";\nexport * from \"./Profile\";\nexport * from \"./Chat\";\nexport * from \"./Request\";\nexport * from \"./EditProfile\";\nexport * from \"./Swap\";\nexport * from \"./SwapLightning\";\nexport * from \"./Transfer\";\n"
  },
  {
    "path": "src/routes/settings/Admin.tsx",
    "content": "import {\n    BackLink,\n    DefaultMain,\n    DeleteEverything,\n    KitchenSink,\n    LargeHeader,\n    MutinyWalletGuard,\n    NavBar,\n    NiceP,\n    SmallHeader,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nexport function Admin() {\n    const i18n = useI18n();\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackLink href=\"/settings\" title={i18n.t(\"settings.header\")} />\n                <LargeHeader>{i18n.t(\"settings.admin.header\")}</LargeHeader>\n                <VStack>\n                    <NiceP>{i18n.t(\"settings.admin.warning_one\")}</NiceP>\n                    <NiceP>{i18n.t(\"settings.admin.warning_two\")}</NiceP>\n                    <KitchenSink />\n                    <div class=\"flex flex-col gap-2 overflow-x-hidden rounded-xl bg-m-red p-4\">\n                        <SmallHeader>\n                            {i18n.t(\"settings.danger_zone\")}\n                        </SmallHeader>\n                        <DeleteEverything />\n                    </div>\n                </VStack>\n            </DefaultMain>\n            <NavBar activeTab=\"settings\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/Backup.tsx",
    "content": "import { createAsync, useNavigate } from \"@solidjs/router\";\nimport { createEffect, createSignal, Show } from \"solid-js\";\n\nimport {\n    BackLink,\n    Button,\n    Checkbox,\n    DefaultMain,\n    LargeHeader,\n    MutinyWalletGuard,\n    NavBar,\n    NiceP,\n    SeedWords,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nfunction Quiz(props: { setHasCheckedAll: (hasChecked: boolean) => void }) {\n    const i18n = useI18n();\n    const [one, setOne] = createSignal(false);\n    const [two, setTwo] = createSignal(false);\n    const [three, setThree] = createSignal(false);\n\n    createEffect(() => {\n        if (one() && two() && three()) {\n            props.setHasCheckedAll(true);\n        } else {\n            props.setHasCheckedAll(false);\n        }\n    });\n\n    return (\n        <VStack>\n            <Checkbox\n                checked={one()}\n                onChange={setOne}\n                label={i18n.t(\"settings.backup.confirm\")}\n            />\n            <Checkbox\n                checked={two()}\n                onChange={setTwo}\n                label={i18n.t(\"settings.backup.responsibility\")}\n            />\n            <Checkbox\n                checked={three()}\n                onChange={setThree}\n                label={i18n.t(\"settings.backup.liar\")}\n            />\n        </VStack>\n    );\n}\n\nexport function Backup() {\n    const i18n = useI18n();\n    const [_store, actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n\n    const [hasSeenBackup, setHasSeenBackup] = createSignal(false);\n    const [hasCheckedAll, setHasCheckedAll] = createSignal(false);\n    const [loading, setLoading] = createSignal(false);\n\n    function wroteDownTheWords() {\n        setLoading(true);\n        actions.setHasBackedUp();\n        navigate(\"/\");\n        setLoading(false);\n    }\n\n    const words = createAsync(async () => await sw.show_seed());\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackLink href=\"/settings\" title={i18n.t(\"settings.header\")} />\n                <LargeHeader>{i18n.t(\"settings.backup.title\")}</LargeHeader>\n\n                <VStack>\n                    <NiceP>{i18n.t(\"settings.backup.secure_funds\")}</NiceP>\n                    <NiceP>{i18n.t(\"settings.backup.twelve_words_tip\")}</NiceP>\n                    <NiceP>{i18n.t(\"settings.backup.warning_one\")}</NiceP>\n                    <NiceP>{i18n.t(\"settings.backup.warning_two\")}</NiceP>\n                    <SeedWords\n                        words={words() || \"\"}\n                        setHasSeen={setHasSeenBackup}\n                    />\n                    <Show when={hasSeenBackup()}>\n                        <Quiz setHasCheckedAll={setHasCheckedAll} />\n                    </Show>\n                    <Button\n                        disabled={!hasSeenBackup() || !hasCheckedAll()}\n                        intent=\"blue\"\n                        onClick={wroteDownTheWords}\n                        loading={loading()}\n                    >\n                        {i18n.t(\"settings.backup.confirm\")}\n                    </Button>\n                </VStack>\n            </DefaultMain>\n            <NavBar activeTab=\"settings\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/Channels.tsx",
    "content": "import { MutinyChannel } from \"@mutinywallet/mutiny-wasm\";\nimport {\n    createEffect,\n    createMemo,\n    createResource,\n    createSignal,\n    For,\n    Match,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    AmountSmall,\n    BackLink,\n    Card,\n    Collapser,\n    ConfirmDialog,\n    DefaultMain,\n    ExternalLink,\n    LargeHeader,\n    MutinyWalletGuard,\n    NavBar,\n    NiceP,\n    SettingsCard,\n    showToast,\n    SmallHeader,\n    TinyText,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { createDeepSignal, eify, mempoolTxUrl } from \"~/utils\";\n\nexport function BalanceBar(props: {\n    inbound: number;\n    reserve: number;\n    outbound: number;\n    hideHeader?: boolean;\n}) {\n    const i18n = useI18n();\n    return (\n        <VStack smallgap>\n            <Show when={!props.hideHeader}>\n                <div class=\"flex justify-between\">\n                    <SmallHeader>\n                        {i18n.t(\"settings.channels.outbound\")}\n                    </SmallHeader>\n                    <SmallHeader>\n                        {i18n.t(\"settings.channels.reserve\")}\n                    </SmallHeader>\n                    <SmallHeader>\n                        {i18n.t(\"settings.channels.inbound\")}\n                    </SmallHeader>\n                </div>\n            </Show>\n            <div class=\"flex w-full gap-1\">\n                <div\n                    class=\"min-w-fit rounded-l-xl bg-m-green p-2\"\n                    style={{\n                        \"flex-grow\": props.outbound || 1\n                    }}\n                >\n                    <AmountSmall amountSats={props.outbound} />\n                </div>\n                <div\n                    class=\"min-w-fit bg-m-grey-400 p-2\"\n                    style={{\n                        \"flex-grow\": props.reserve\n                    }}\n                >\n                    <AmountSmall amountSats={props.reserve} />\n                </div>\n                <div\n                    class=\"min-w-fit rounded-r-xl bg-m-blue p-2\"\n                    style={{\n                        \"flex-grow\": props.inbound || 1\n                    }}\n                >\n                    <AmountSmall amountSats={props.inbound} />\n                </div>\n            </div>\n        </VStack>\n    );\n}\n\nfunction splitChannelNumbers(channel: MutinyChannel): {\n    inbound: number;\n    reserve: number;\n    outbound: number;\n} {\n    return {\n        inbound: Number(channel.inbound) || 0,\n        reserve: Number(channel.reserve),\n        outbound: Number(channel.balance)\n    };\n}\n\nfunction SingleChannelItem(props: { channel: MutinyChannel; online: boolean }) {\n    const i18n = useI18n();\n    const [state, _actions, sw] = useMegaStore();\n    const network = state.network;\n\n    const [confirmOpen, setConfirmOpen] = createSignal(false);\n    const [confirmLoading, setConfirmLoading] = createSignal(false);\n\n    function confirmChannelClose() {\n        setConfirmOpen(true);\n    }\n\n    async function closeChannel() {\n        try {\n            if (!props.channel.outpoint) return;\n            setConfirmLoading(true);\n            const forceClose = !props.online;\n            await sw.close_channel(props.channel.outpoint, forceClose, false);\n        } catch (e) {\n            console.error(e);\n            showToast(eify(e));\n        } finally {\n            setConfirmOpen(false);\n            setConfirmLoading(false);\n        }\n    }\n\n    const channelDetails = createMemo(() => splitChannelNumbers(props.channel));\n\n    return (\n        <Card>\n            <VStack smallgap>\n                <BalanceBar\n                    inbound={channelDetails().inbound}\n                    reserve={channelDetails().reserve}\n                    outbound={channelDetails().outbound}\n                    hideHeader\n                />\n                <div class=\"flex justify-between text-sm\">\n                    <ExternalLink\n                        href={mempoolTxUrl(\n                            props.channel.outpoint?.split(\":\")[0],\n                            network\n                        )}\n                    >\n                        {i18n.t(\"common.view_transaction\")}\n                    </ExternalLink>\n                    <button\n                        onClick={confirmChannelClose}\n                        class=\"self-center font-semibold text-m-red no-underline active:text-m-red/80\"\n                    >\n                        {i18n.t(\"settings.channels.close_channel\")}\n                    </button>\n                </div>\n                <ConfirmDialog\n                    loading={confirmLoading()}\n                    open={confirmOpen()}\n                    onConfirm={closeChannel}\n                    onCancel={() => setConfirmOpen(false)}\n                >\n                    <Switch>\n                        <Match when={!props.online}>\n                            {i18n.t(\n                                \"settings.channels.force_close_channel_confirm\"\n                            )}\n                        </Match>\n                        <Match when={true}>\n                            {i18n.t(\"settings.channels.close_channel_confirm\")}\n                        </Match>\n                    </Switch>\n                </ConfirmDialog>\n            </VStack>\n        </Card>\n    );\n}\n\nfunction LiquidityMonitor() {\n    const i18n = useI18n();\n    const [state, _actions, sw] = useMegaStore();\n\n    async function listChannels() {\n        try {\n            const channels = await sw.list_channels();\n\n            if (!channels)\n                return {\n                    inbound: 0,\n                    reserve: 0,\n                    outbound: 0,\n                    channelCount: 0\n                };\n\n            let outbound = 0n;\n            let inbound = 0n;\n            let reserve = 0n;\n\n            for (const channel of channels) {\n                inbound = inbound + BigInt(channel.inbound);\n                reserve = reserve + BigInt(channel.reserve);\n                outbound = outbound + BigInt(channel.balance);\n            }\n\n            return {\n                inbound,\n                reserve,\n                outbound,\n                channelCount: channels?.length,\n                online: channels?.filter((c) => c.is_usable),\n                offline: channels?.filter((c) => !c.is_usable)\n            };\n        } catch (e) {\n            console.error(e);\n            return { inbound: 0, reserve: 0, outbound: 0, channelCount: 0 };\n        }\n    }\n\n    const [channelInfo, { refetch }] = createResource(listChannels, {\n        storage: createDeepSignal\n    });\n\n    createEffect(() => {\n        // Refetch on the sync interval\n        if (!state.is_syncing) {\n            refetch();\n        }\n    });\n\n    return (\n        <Switch>\n            <Match\n                when={channelInfo.latest && channelInfo.latest?.channelCount}\n            >\n                <VStack>\n                    <Card>\n                        <NiceP>\n                            {i18n.t(\"settings.channels.have_channels\")}{\" \"}\n                            {channelInfo.latest?.channelCount}{\" \"}\n                            {channelInfo.latest?.channelCount === 1\n                                ? i18n.t(\"settings.channels.have_channels_one\")\n                                : i18n.t(\n                                      \"settings.channels.have_channels_many\"\n                                  )}\n                        </NiceP>{\" \"}\n                        <BalanceBar\n                            inbound={Number(channelInfo.latest?.inbound) || 0}\n                            reserve={Number(channelInfo.latest?.reserve) || 0}\n                            outbound={Number(channelInfo.latest?.outbound) || 0}\n                        />\n                        <TinyText>\n                            {i18n.t(\"settings.channels.inbound_outbound_tip\")}\n                        </TinyText>\n                        <TinyText>\n                            {i18n.t(\"settings.channels.reserve_tip\")}\n                        </TinyText>\n                    </Card>\n                    <Show when={channelInfo.latest?.online?.length}>\n                        <SettingsCard>\n                            <Collapser\n                                title={i18n.t(\n                                    \"settings.channels.online_channels\"\n                                )}\n                                activityLight=\"on\"\n                            >\n                                <VStack>\n                                    <For each={channelInfo.latest?.online}>\n                                        {(channel) => (\n                                            <SingleChannelItem\n                                                channel={channel}\n                                                online={true}\n                                            />\n                                        )}\n                                    </For>\n                                </VStack>\n                            </Collapser>\n                        </SettingsCard>\n                    </Show>\n                    <Show when={channelInfo.latest?.offline?.length}>\n                        <SettingsCard>\n                            <Collapser\n                                title={i18n.t(\n                                    \"settings.channels.offline_channels\"\n                                )}\n                                activityLight=\"off\"\n                            >\n                                <VStack>\n                                    <For each={channelInfo.latest?.offline}>\n                                        {(channel) => (\n                                            <SingleChannelItem\n                                                channel={channel}\n                                                online={false}\n                                            />\n                                        )}\n                                    </For>\n                                </VStack>\n                            </Collapser>\n                        </SettingsCard>\n                    </Show>\n                </VStack>\n            </Match>\n            <Match when={true}>\n                <NiceP>{i18n.t(\"settings.channels.no_channels\")}</NiceP>\n            </Match>\n        </Switch>\n    );\n}\n\nexport function Channels() {\n    const i18n = useI18n();\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackLink href=\"/settings\" title={i18n.t(\"settings.header\")} />\n                <LargeHeader>{i18n.t(\"settings.channels.title\")}</LargeHeader>\n                <Suspense>\n                    <LiquidityMonitor />\n                </Suspense>\n            </DefaultMain>\n            <NavBar activeTab=\"settings\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/Connections.tsx",
    "content": "import { NwcProfile } from \"@mutinywallet/mutiny-wasm\";\nimport { A, useSearchParams } from \"@solidjs/router\";\nimport { Scan } from \"lucide-solid\";\nimport {\n    createResource,\n    createSignal,\n    ErrorBoundary,\n    For,\n    Show\n} from \"solid-js\";\nimport { QRCodeSVG } from \"solid-qr-code\";\n\nimport {\n    AmountSats,\n    AmountSmall,\n    BackLink,\n    Button,\n    Collapser,\n    ConfirmDialog,\n    DefaultMain,\n    InfoBox,\n    KeyValue,\n    LargeHeader,\n    MutinyWalletGuard,\n    NavBar,\n    NiceP,\n    SettingsCard,\n    ShareCard,\n    SimpleDialog,\n    SmallHeader,\n    TinyText,\n    VStack\n} from \"~/components\";\nimport { NWCEditor } from \"~/components/NWCEditor\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { createDeepSignal, openLinkProgrammatically } from \"~/utils\";\n\nfunction Spending(props: { spent: number; remaining: number }) {\n    const i18n = useI18n();\n    return (\n        <VStack smallgap>\n            <div class=\"flex justify-between\">\n                <SmallHeader>\n                    {i18n.t(\"settings.connections.spent\")}\n                </SmallHeader>\n                <SmallHeader>\n                    {i18n.t(\"settings.connections.remaining\")}\n                </SmallHeader>\n            </div>\n            <div class=\"flex w-full gap-1\">\n                <div\n                    class=\"min-w-fit rounded-l-xl bg-m-green p-2\"\n                    style={{\n                        \"flex-grow\": props.spent || 1\n                    }}\n                >\n                    <AmountSmall amountSats={props.spent} />\n                </div>\n\n                <div\n                    class=\"min-w-fit rounded-r-xl bg-m-blue p-2\"\n                    style={{\n                        \"flex-grow\": props.remaining || 1\n                    }}\n                >\n                    <AmountSmall amountSats={props.remaining} />\n                </div>\n            </div>\n        </VStack>\n    );\n}\n\nfunction NwcDetails(props: {\n    profile: NwcProfile;\n    refetch: () => void;\n    onEdit?: () => void;\n}) {\n    const i18n = useI18n();\n    const [state, _actions, sw] = useMegaStore();\n\n    const [confirmOpen, setConfirmOpen] = createSignal(false);\n\n    function confirmDelete() {\n        setConfirmOpen(true);\n    }\n\n    async function deleteProfile() {\n        try {\n            await sw.delete_nwc_profile(props.profile.index);\n            setConfirmOpen(false);\n            props.refetch();\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    async function openInNostrClient() {\n        const uri = props.profile.nwc_uri;\n        await openLinkProgrammatically(uri, {\n            title: i18n.t(\"settings.connections.nostr_client_not_found\"),\n            description: i18n.t(\n                \"settings.connections.client_not_found_description\"\n            )\n        });\n    }\n\n    return (\n        <VStack>\n            <Show when={props.profile.index >= 1000 && props.profile.nwc_uri}>\n                <div class=\"w-full rounded-xl bg-white\">\n                    <QRCodeSVG\n                        value={props.profile.nwc_uri!}\n                        class=\"h-full max-h-[320px] w-full p-8\"\n                    />\n                </div>\n                <ShareCard text={props.profile.nwc_uri || \"\"} />\n            </Show>\n\n            <Show when={!props.profile.require_approval}>\n                <Show when={props.profile.nwc_uri}>\n                    <TinyText>\n                        {i18n.t(\"settings.connections.careful\")}\n                    </TinyText>\n                </Show>\n                <Spending\n                    spent={Number(\n                        Number(props.profile.budget_amount || 0) -\n                            Number(props.profile.budget_remaining || 0)\n                    )}\n                    remaining={Number(props.profile.budget_remaining || 0)}\n                />\n                <KeyValue key={i18n.t(\"settings.connections.budget\")}>\n                    <AmountSats amountSats={props.profile.budget_amount} />\n                </KeyValue>\n                {/* No interval for gifts */}\n                <Show when={props.profile.budget_period}>\n                    <KeyValue key={i18n.t(\"settings.connections.resets_every\")}>\n                        {props.profile.budget_period}\n                    </KeyValue>\n                </Show>\n                <Show when={props.profile.index === 0}>\n                    <KeyValue\n                        key={i18n.t(\"settings.connections.resubscribe_date\")}\n                    >\n                        {new Date(\n                            state.subscription_timestamp! * 1000\n                        ).toLocaleDateString()}\n                    </KeyValue>\n                </Show>\n            </Show>\n\n            <Show\n                when={props.profile.enabled}\n                fallback={\n                    <InfoBox accent=\"red\">\n                        {i18n.t(\"settings.connections.disabled\")}\n                    </InfoBox>\n                }\n            >\n                <Button layout=\"small\" intent=\"green\" onClick={props.onEdit}>\n                    {i18n.t(\"settings.connections.edit_budget\")}\n                </Button>\n            </Show>\n\n            <Show\n                when={\n                    props.profile.tag !== \"Gift\" &&\n                    props.profile.tag !== \"Subscription\" &&\n                    props.profile.nwc_uri\n                }\n            >\n                <Button\n                    layout=\"small\"\n                    intent=\"blue\"\n                    onClick={openInNostrClient}\n                >\n                    {i18n.t(\"settings.connections.open_in_nostr_client\")}\n                </Button>\n            </Show>\n\n            <Show when={props.profile.enabled}>\n                <Button layout=\"small\" onClick={confirmDelete}>\n                    {props.profile.tag === \"Subscription\"\n                        ? i18n.t(\"settings.connections.disable_connection\")\n                        : i18n.t(\"settings.connections.delete_connection\")}\n                </Button>\n            </Show>\n            <ConfirmDialog\n                loading={false}\n                open={confirmOpen()}\n                onConfirm={deleteProfile}\n                onCancel={() => setConfirmOpen(false)}\n            >\n                {props.profile.tag === \"Subscription\"\n                    ? i18n.t(\"settings.connections.disable_connection_confirm\")\n                    : i18n.t(\"settings.connections.confirm_delete\")}\n            </ConfirmDialog>\n        </VStack>\n    );\n}\n\nfunction Nwc() {\n    const i18n = useI18n();\n    const [_state, _actions, sw] = useMegaStore();\n\n    async function fetchNwcProfiles() {\n        try {\n            const profiles = await sw.get_nwc_profiles();\n            console.log(\"profiles\", profiles);\n            if (!profiles) return [];\n\n            return profiles;\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    const [nwcProfiles, { refetch }] = createResource(fetchNwcProfiles, {\n        storage: createDeepSignal\n    });\n\n    const [searchParams, setSearchParams] = useSearchParams();\n    const [callbackDialogOpen, setCallbackDialogOpen] = createSignal(false);\n    const [callbackUri, setCallbackUri] = createSignal<string>();\n\n    // Profile creation / editing\n    const [dialogOpen, setDialogOpen] = createSignal(\n        !!searchParams.queryName || !!searchParams.nwa\n    );\n\n    function handleToggleOpen(open: boolean) {\n        setDialogOpen(open);\n        // If they close the dialog clear the search params\n        setSearchParams({ nwa: undefined, name: undefined });\n    }\n\n    const [profileToOpen, setProfileToOpen] = createSignal<number>();\n\n    function editProfile(profile: NwcProfile) {\n        if (profile && typeof profile.index === \"number\") {\n            setProfileToOpen(profile.index);\n            setDialogOpen(true);\n        }\n    }\n\n    function createProfile() {\n        setProfileToOpen(undefined);\n        setDialogOpen(true);\n    }\n\n    const [newConnection, setNewConnection] = createSignal<number>();\n\n    async function handleSave(\n        indexToOpen?: number,\n        nwcUriForCallback?: string\n    ) {\n        setDialogOpen(false);\n        refetch();\n\n        if (indexToOpen) {\n            setNewConnection(indexToOpen);\n        }\n\n        const callbackUriScheme = searchParams.callbackUri;\n        if (callbackUriScheme && nwcUriForCallback) {\n            const fullURI = nwcUriForCallback.replace(\n                \"nostr+walletconnect://\",\n                `${callbackUriScheme}://`\n            );\n            setCallbackUri(fullURI);\n            setCallbackDialogOpen(true);\n        }\n\n        setSearchParams({ nwa: undefined, name: undefined });\n    }\n\n    async function openCallbackUri() {\n        await openLinkProgrammatically(callbackUri());\n        setSearchParams({ callbackUri: \"\" });\n        setCallbackDialogOpen(false);\n    }\n\n    return (\n        <VStack biggap>\n            <Button intent=\"blue\" onClick={createProfile}>\n                {i18n.t(\"settings.connections.add_connection\")}\n            </Button>\n            <Show when={nwcProfiles.latest && nwcProfiles.latest?.length > 0}>\n                <SettingsCard\n                    title={i18n.t(\"settings.connections.manage_connections\")}\n                >\n                    <For each={nwcProfiles()?.filter((p) => p.tag !== \"Gift\")}>\n                        {(profile) => (\n                            <Collapser\n                                title={profile.name}\n                                activityLight={profile.enabled ? \"on\" : \"off\"}\n                                defaultOpen={profile.index === newConnection()}\n                            >\n                                <NwcDetails\n                                    onEdit={() => editProfile(profile)}\n                                    profile={profile}\n                                    refetch={refetch}\n                                />\n                            </Collapser>\n                        )}\n                    </For>\n                </SettingsCard>\n            </Show>\n            <SimpleDialog\n                open={dialogOpen()}\n                setOpen={handleToggleOpen}\n                title={\n                    typeof profileToOpen() === \"number\"\n                        ? i18n.t(\"settings.connections.edit_connection\")\n                        : i18n.t(\"settings.connections.add_connection\")\n                }\n            >\n                <ErrorBoundary\n                    fallback={(e) => (\n                        <InfoBox accent=\"red\">{e.message}</InfoBox>\n                    )}\n                >\n                    <NWCEditor\n                        initialNWA={searchParams.nwa}\n                        initialProfileIndex={profileToOpen()}\n                        onSave={handleSave}\n                    />\n                </ErrorBoundary>\n            </SimpleDialog>\n            <SimpleDialog\n                open={callbackDialogOpen()}\n                setOpen={setCallbackDialogOpen}\n                title={i18n.t(\"settings.connections.open_app\")}\n            >\n                <Button onClick={openCallbackUri}>\n                    {i18n.t(\"settings.connections.open_app\")}\n                </Button>\n            </SimpleDialog>\n        </VStack>\n    );\n}\n\nexport function Connections() {\n    const i18n = useI18n();\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <div class=\"flex items-center justify-between\">\n                    <BackLink\n                        href=\"/?tab=requests\"\n                        title={i18n.t(\"home.subnav.requests\")}\n                    />\n                    <A\n                        class=\"rounded-lg p-2 hover:bg-white/5 active:bg-m-blue md:hidden\"\n                        href=\"/scanner\"\n                    >\n                        <Scan />\n                    </A>{\" \"}\n                </div>\n                <LargeHeader>\n                    {i18n.t(\"settings.connections.title\")}\n                </LargeHeader>\n                <NiceP>{i18n.t(\"settings.connections.authorize\")}</NiceP>\n                <Nwc />\n                <div class=\"h-full\" />\n            </DefaultMain>\n            <NavBar activeTab=\"settings\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/Currency.tsx",
    "content": "import {\n    BackLink,\n    Card,\n    ChooseCurrency,\n    DefaultMain,\n    LargeHeader,\n    MutinyWalletGuard,\n    NavBar\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nexport function Currency() {\n    const i18n = useI18n();\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackLink href=\"/settings\" title={i18n.t(\"settings.header\")} />\n                <LargeHeader>{i18n.t(\"settings.currency.title\")}</LargeHeader>\n                <Card title={i18n.t(\"settings.currency.select_currency\")}>\n                    <ChooseCurrency />\n                </Card>\n                <div class=\"h-full\" />\n            </DefaultMain>\n            <NavBar activeTab=\"settings\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/EmergencyKit.tsx",
    "content": "import {\n    BackLink,\n    Button,\n    DefaultMain,\n    DeleteEverything,\n    ExternalLink,\n    ImportExport,\n    InnerCard,\n    LargeHeader,\n    LoadingIndicator,\n    Logs,\n    NavBar,\n    NiceP,\n    SmallHeader,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nfunction EmergencyStack() {\n    const i18n = useI18n();\n    return (\n        <VStack>\n            <ImportExport emergency />\n            <Logs />\n            <InnerCard title={\"Safe Mode\"}>\n                <VStack>\n                    <NiceP>\n                        Disable certain wallet functionality to help with\n                        debugging.\n                    </NiceP>\n                    <Button\n                        onClick={() =>\n                            (window.location.href = \"/?safe_mode=true\")\n                        }\n                    >\n                        Enable Safe Mode\n                    </Button>\n                </VStack>\n            </InnerCard>\n            <div class=\"flex flex-col gap-2 overflow-x-hidden rounded-xl bg-m-red p-4\">\n                <SmallHeader>{i18n.t(\"settings.danger_zone\")}</SmallHeader>\n                <DeleteEverything emergency />\n            </div>\n        </VStack>\n    );\n}\n\nexport function EmergencyKit() {\n    const i18n = useI18n();\n    return (\n        <DefaultMain>\n            <BackLink href=\"/settings\" title={i18n.t(\"settings.header\")} />\n            <LargeHeader>{i18n.t(\"settings.emergency_kit.title\")}</LargeHeader>\n            <VStack>\n                <LoadingIndicator />\n                <NiceP>{i18n.t(\"settings.emergency_kit.emergency_tip\")}</NiceP>\n                <NiceP>\n                    {i18n.t(\"settings.emergency_kit.questions\")}{\" \"}\n                    <ExternalLink href=\"https://discord.gg/x3njeHUjVd\">\n                        {i18n.t(\"settings.emergency_kit.link\")}\n                    </ExternalLink>\n                </NiceP>\n                <EmergencyStack />\n            </VStack>\n            <NavBar activeTab=\"settings\" />\n        </DefaultMain>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/Encrypt.tsx",
    "content": "import { createForm } from \"@modular-forms/solid\";\nimport { createMemo, createSignal, Show } from \"solid-js\";\n\nimport {\n    BackLink,\n    Button,\n    DefaultMain,\n    InfoBox,\n    LargeHeader,\n    MutinyWalletGuard,\n    NavBar,\n    NiceP,\n    TextField,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify } from \"~/utils\";\n\ntype EncryptPasswordForm = {\n    existingPassword: string;\n    password: string;\n    confirmPassword: string;\n};\n\nexport function Encrypt() {\n    const i18n = useI18n();\n    const [_state, _actions, sw] = useMegaStore();\n    const [error, setError] = createSignal<Error>();\n    const [loading, setLoading] = createSignal(false);\n\n    const [encryptPasswordForm, { Form, Field }] =\n        createForm<EncryptPasswordForm>({\n            initialValues: {\n                existingPassword: \"\",\n                password: \"\",\n                confirmPassword: \"\"\n            },\n            validate: (values) => {\n                setError(undefined);\n                const errors: Record<string, string> = {};\n                if (values.password !== values.confirmPassword) {\n                    errors.confirmPassword = i18n.t(\n                        \"settings.encrypt.error_match\"\n                    );\n                } else if (values.password === values.existingPassword) {\n                    errors.password = i18n.t(\n                        \"settings.encrypt.error_same_as_existingpassword\"\n                    );\n                }\n                return errors;\n            }\n        });\n\n    const handleFormSubmit = async (f: EncryptPasswordForm) => {\n        setLoading(true);\n        try {\n            await sw.change_password(\n                f.existingPassword === \"\" ? undefined : f.existingPassword,\n                f.password === \"\" ? undefined : f.password\n            );\n\n            // await timeout(1000);\n            window.location.href = \"/\";\n        } catch (e) {\n            console.error(e);\n            setError(eify(e));\n            setLoading(false);\n        }\n    };\n\n    const encryptButtonDisabled = createMemo(() => {\n        return (\n            !encryptPasswordForm.dirty ||\n            encryptPasswordForm.invalid ||\n            !!error()\n        );\n    });\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackLink href=\"/settings\" title={i18n.t(\"settings.header\")} />\n                <LargeHeader>\n                    {`${i18n.t(\"settings.encrypt.header\")} ${i18n.t(\n                        \"settings.encrypt.optional\"\n                    )}`}\n                </LargeHeader>\n                <VStack>\n                    <NiceP>\n                        {i18n.t(\"settings.encrypt.hot_wallet_warning\")}\n                    </NiceP>\n                    <NiceP>{i18n.t(\"settings.encrypt.password_tip\")}</NiceP>\n                    <Form onSubmit={handleFormSubmit}>\n                        <VStack>\n                            <Field name=\"existingPassword\">\n                                {(field, props) => (\n                                    <TextField\n                                        {...props}\n                                        {...field}\n                                        type=\"password\"\n                                        label={`${i18n.t(\n                                            \"settings.encrypt.existing_password\"\n                                        )} ${i18n.t(\n                                            \"settings.encrypt.optional\"\n                                        )}`}\n                                        placeholder={i18n.t(\n                                            \"settings.encrypt.existing_password\"\n                                        )}\n                                        caption={i18n.t(\n                                            \"settings.encrypt.existing_password_caption\"\n                                        )}\n                                    />\n                                )}\n                            </Field>\n                            <Field name=\"password\">\n                                {(field, props) => (\n                                    <TextField\n                                        {...props}\n                                        {...field}\n                                        type=\"password\"\n                                        label={i18n.t(\n                                            \"settings.encrypt.new_password_label\"\n                                        )}\n                                        placeholder={i18n.t(\n                                            \"settings.encrypt.new_password_placeholder\"\n                                        )}\n                                        caption={i18n.t(\n                                            \"settings.encrypt.new_password_caption\"\n                                        )}\n                                    />\n                                )}\n                            </Field>\n                            <Field name=\"confirmPassword\">\n                                {(field, props) => (\n                                    <TextField\n                                        {...props}\n                                        {...field}\n                                        type=\"password\"\n                                        label={i18n.t(\n                                            \"settings.encrypt.confirm_password_label\"\n                                        )}\n                                        placeholder={i18n.t(\n                                            \"settings.encrypt.confirm_password_placeholder\"\n                                        )}\n                                    />\n                                )}\n                            </Field>\n                            <Show when={error()}>\n                                <InfoBox accent=\"red\">\n                                    {error()?.message}\n                                </InfoBox>\n                            </Show>\n                            <div />\n                            <Button\n                                intent=\"blue\"\n                                disabled={encryptButtonDisabled()}\n                                loading={loading()}\n                            >\n                                {i18n.t(\"settings.encrypt.encrypt\")}\n                            </Button>\n                        </VStack>\n                    </Form>\n                </VStack>\n            </DefaultMain>\n            <NavBar activeTab=\"settings\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/ImportProfile.tsx",
    "content": "import {\n    BackLink,\n    DefaultMain,\n    ImportNsecForm,\n    MutinyWalletGuard\n} from \"~/components\";\n\nexport function ImportProfileSettings() {\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackLink title=\"Back\" href=\"/settings/nostrkeys\" />\n                <div class=\"mx-auto flex max-w-[20rem] flex-1 flex-col items-center gap-4\">\n                    <div class=\"flex-1\" />\n                    <h1 class=\"text-3xl font-semibold\">Import nostr profile</h1>\n                    <p class=\"text-center text-xl font-light text-neutral-200\">\n                        Login with an existing nostr account.\n                        <br />\n                    </p>\n                    <div class=\"flex-1\" />\n                    <ImportNsecForm />\n                    <div class=\"flex-1\" />\n                </div>\n            </DefaultMain>\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/Language.tsx",
    "content": "import {\n    BackLink,\n    Card,\n    ChooseLanguage,\n    DefaultMain,\n    LargeHeader,\n    MutinyWalletGuard,\n    NavBar\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nexport function Language() {\n    const i18n = useI18n();\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackLink href=\"/settings\" title={i18n.t(\"settings.header\")} />\n                <LargeHeader>{i18n.t(\"settings.language.title\")}</LargeHeader>\n                <Card title={i18n.t(\"settings.language.select_language\")}>\n                    <ChooseLanguage />\n                </Card>\n                <div class=\"h-full\" />\n            </DefaultMain>\n            <NavBar activeTab=\"settings\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/LightningAddress.tsx",
    "content": "import { Capacitor } from \"@capacitor/core\";\nimport {\n    createForm,\n    custom,\n    required,\n    reset,\n    SubmitHandler\n} from \"@modular-forms/solid\";\nimport { createAsync, useNavigate } from \"@solidjs/router\";\nimport { Users } from \"lucide-solid\";\nimport {\n    createMemo,\n    createResource,\n    createSignal,\n    Match,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    BackPop,\n    Button,\n    ButtonCard,\n    DefaultMain,\n    InfoBox,\n    LargeHeader,\n    LightningAddressShower,\n    MutinyPlusCta,\n    MutinyWalletGuard,\n    NavBar,\n    NiceP,\n    TextField,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify } from \"~/utils\";\n\ntype HermesForm = {\n    name: string;\n};\n\nconst validateLowerCase = (value?: string) => {\n    if (!value) return false;\n    const valid = /^[a-z0-9-_.]+$/;\n    return valid.test(value);\n};\n\n// todo(paul) put this somewhere else\nfunction HermesForm(props: { onSubmit: (name: string) => void }) {\n    const [_state, _actions, sw] = useMegaStore();\n    const [error, setError] = createSignal<Error>();\n    const [success, setSuccess] = createSignal(\"\");\n\n    const [nameForm, { Form, Field }] = createForm<HermesForm>({\n        initialValues: {\n            name: \"\"\n        }\n    });\n\n    const hermes = import.meta.env.VITE_HERMES;\n    if (!hermes) {\n        throw new Error(\"Hermes not configured\");\n    }\n    const hermesDomain = new URL(hermes).hostname;\n\n    const handleSubmit: SubmitHandler<HermesForm> = async (f: HermesForm) => {\n        setSuccess(\"\");\n        setError(undefined);\n        try {\n            const name = f.name.trim().toLowerCase();\n            const available = await sw.check_available_lnurl_name(name);\n            if (!available) {\n                throw new Error(\"Name already taken\");\n            }\n            await sw.reserve_lnurl_name(name);\n            console.log(\"lnurl name reserved:\", name);\n\n            const formattedName = `${name}@${hermesDomain}`;\n\n            const _ = await sw.edit_nostr_profile(\n                undefined,\n                undefined,\n                // lnurl\n                formattedName,\n                undefined\n            );\n            reset(nameForm);\n            props.onSubmit(name);\n        } catch (e) {\n            console.error(\"Error reserving name:\", e);\n            setError(eify(e));\n        }\n    };\n\n    return (\n        <Form onSubmit={handleSubmit}>\n            <VStack>\n                <Field\n                    name=\"name\"\n                    validate={[\n                        required(\"Must not be empty\"),\n                        custom(validateLowerCase, \"Address must be lowercase\")\n                    ]}\n                >\n                    {(field, props) => (\n                        <div class=\"flex w-full flex-1 gap-2\">\n                            <div class=\"flex-1\">\n                                <TextField\n                                    {...props}\n                                    {...field}\n                                    autoCapitalize=\"none\"\n                                    error={field.error}\n                                    label={\"Nym\"}\n                                    required\n                                />\n                            </div>\n                            <div class=\"flex-0 self-start pt-8 text-2xl text-m-grey-350\">\n                                @{hermesDomain}\n                            </div>\n                        </div>\n                    )}\n                </Field>\n                <Button\n                    loading={nameForm.submitting}\n                    disabled={nameForm.invalid}\n                    intent=\"blue\"\n                    type=\"submit\"\n                >\n                    Submit\n                </Button>\n                <Show when={error()}>\n                    <InfoBox accent=\"red\">{error()?.message}</InfoBox>\n                </Show>\n                <Show when={success()}>\n                    <InfoBox accent=\"green\">{success()}</InfoBox>\n                </Show>\n            </VStack>\n        </Form>\n    );\n}\n\nexport function LightningAddress() {\n    const i18n = useI18n();\n    const [state, _actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n    const [error, setError] = createSignal<Error>();\n    const [settingLnAddress, setSettingLnAddress] = createSignal(false);\n\n    const [lnurlName] = createResource(async () => {\n        try {\n            const name = await sw.check_lnurl_name();\n            return name;\n        } catch (e) {\n            setError(eify(e));\n        }\n    });\n    const ios = Capacitor.getPlatform() === \"ios\";\n\n    const hermes = import.meta.env.VITE_HERMES;\n    if (!hermes) {\n        throw new Error(\"Hermes not configured\");\n    }\n    const hermesDomain = new URL(hermes).hostname;\n\n    const formattedLnAddress = createMemo(() => {\n        const name = lnurlName();\n        if (name) {\n            return `${lnurlName()}@${hermesDomain}`;\n        }\n    });\n\n    const profileLnAddress = createAsync(async () => {\n        if (lnurlName()) {\n            const profile = await sw.get_nostr_profile();\n            if (profile && profile.lud16) {\n                return profile.lud16;\n            }\n        }\n    });\n\n    async function setLnAddress(newAddress: string) {\n        if (!newAddress) return;\n        try {\n            setSettingLnAddress(true);\n            setError(undefined);\n\n            const _ = await sw.edit_nostr_profile(\n                undefined,\n                undefined,\n                newAddress,\n                undefined\n            );\n            navigate(\"/profile\");\n        } catch (e) {\n            console.error(\"Error setting ln address:\", e);\n            setError(eify(e));\n        }\n\n        setSettingLnAddress(false);\n    }\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackPop default=\"/profile\" />\n                <LargeHeader>\n                    {i18n.t(\"settings.lightning_address.title\")}\n                </LargeHeader>\n                <NiceP>\n                    {i18n.t(\"settings.lightning_address.description\")}\n                </NiceP>\n                <Show when={error()}>\n                    <InfoBox accent=\"red\">{error()?.message}</InfoBox>\n                </Show>\n                <Suspense>\n                    <Switch>\n                        <Match when={!state.mutiny_plus && ios}>\n                            <InfoBox accent=\"white\">\n                                {i18n.t(\n                                    \"settings.lightning_address.ios_warning\"\n                                )}\n                            </InfoBox>\n                        </Match>\n                        <Match when={!state.mutiny_plus}>\n                            <NiceP>\n                                Join <strong>Mutiny+</strong> to get a lightning\n                                address.\n                            </NiceP>\n                            <MutinyPlusCta />\n                        </Match>\n                        <Match when={!lnurlName() && state.federations?.length}>\n                            <HermesForm\n                                onSubmit={() => {\n                                    navigate(\"/profile\");\n                                }}\n                            />\n                        </Match>\n                        <Match\n                            when={\n                                formattedLnAddress() &&\n                                state.federations?.length\n                            }\n                        >\n                            <VStack>\n                                <LightningAddressShower\n                                    lud16={formattedLnAddress()!}\n                                />\n                                <Show\n                                    when={\n                                        profileLnAddress() !==\n                                        formattedLnAddress()\n                                    }\n                                >\n                                    <Button\n                                        loading={settingLnAddress()}\n                                        onClick={() =>\n                                            setLnAddress(formattedLnAddress()!)\n                                        }\n                                    >\n                                        Use this address\n                                    </Button>\n                                </Show>\n                            </VStack>\n                        </Match>\n                        <Match when={true}>\n                            <NiceP>\n                                {i18n.t(\n                                    \"settings.lightning_address.add_a_federation\"\n                                )}\n                            </NiceP>\n                            <ButtonCard\n                                onClick={() =>\n                                    navigate(\"/settings/federations\")\n                                }\n                            >\n                                <div class=\"flex items-center gap-2\">\n                                    <Users class=\"inline-block text-m-red\" />\n                                    <NiceP>\n                                        {i18n.t(\"profile.join_federation\")}\n                                    </NiceP>\n                                </div>\n                            </ButtonCard>\n                        </Match>\n                    </Switch>\n                </Suspense>\n            </DefaultMain>\n            <NavBar activeTab=\"settings\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/ManageFederations.tsx",
    "content": "import { Progress } from \"@kobalte/core\";\nimport {\n    createForm,\n    required,\n    reset,\n    setValue,\n    SubmitHandler\n} from \"@modular-forms/solid\";\nimport { FederationBalance, TagItem } from \"@mutinywallet/mutiny-wasm\";\nimport { A, useNavigate, useSearchParams } from \"@solidjs/router\";\nimport {\n    ArrowLeftRight,\n    BadgeCheck,\n    LogOut,\n    RefreshCw,\n    Scan,\n    Trash\n} from \"lucide-solid\";\nimport {\n    createEffect,\n    createResource,\n    createSignal,\n    For,\n    Match,\n    onCleanup,\n    onMount,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport {\n    AmountSats,\n    BackLink,\n    Button,\n    ConfirmDialog,\n    DefaultMain,\n    ExternalLink,\n    FancyCard,\n    FederationInviteShower,\n    FederationPopup,\n    InfoBox,\n    KeyValue,\n    LargeHeader,\n    MediumHeader,\n    MiniStringShower,\n    MutinyWalletGuard,\n    NavBar,\n    NiceP,\n    showToast,\n    SimpleDialog,\n    SubtleButton,\n    TextField,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify, timeAgo, timeout } from \"~/utils\";\n\ntype FederationForm = {\n    federation_code: string;\n};\n\nexport type MutinyFederationIdentity = {\n    federation_id: string;\n    federation_name: string;\n    welcome_message: string;\n    federation_expiry_timestamp: number;\n    invite_code: string;\n    meta_external_url?: string;\n    popup_end_timestamp?: number;\n    popup_countdown_message?: string;\n};\n\nexport type Metadata = {\n    name: string;\n    picture?: string;\n    about?: string;\n};\n\nexport type ResyncProgress = {\n    total: number;\n    complete: number;\n    done: boolean;\n};\n\nexport type DiscoveredFederation = {\n    id: string;\n    invite_codes: string[];\n    pubkey: string;\n    created_at: number;\n    event_id: string;\n    metadata: Metadata | undefined;\n    recommendations: TagItem[]; // fixme, not the best type to use here\n};\n\ntype RefetchType = (\n    info?: unknown\n) =>\n    | FederationBalance[]\n    | Promise<FederationBalance[] | undefined>\n    | null\n    | undefined;\n\nexport function AddFederationForm(props: {\n    refetch?: RefetchType;\n    setup?: boolean;\n    browseOnly?: boolean;\n}) {\n    const i18n = useI18n();\n    const [state, actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n    const [error, setError] = createSignal<Error>();\n    const [success, setSuccess] = createSignal(\"\");\n\n    const [params, setParams] = useSearchParams();\n\n    onMount(() => {\n        if (params.fedimint_invite) {\n            setValue(feedbackForm, \"federation_code\", params.fedimint_invite);\n\n            // Clear the search params\n            setParams({ fedimint_invite: undefined });\n        }\n    });\n\n    const [feedbackForm, { Form, Field }] = createForm<FederationForm>({\n        initialValues: {\n            federation_code: \"\"\n        }\n    });\n\n    const handleSubmit: SubmitHandler<FederationForm> = async (\n        f: FederationForm\n    ) => {\n        const federation_code = f.federation_code.trim();\n        await onSelect([federation_code]);\n    };\n\n    const onSelect = async (inviteCodes: string[]) => {\n        setSuccess(\"\");\n        setError(undefined);\n        try {\n            for (const inviteCode of inviteCodes) {\n                try {\n                    console.log(\"Adding federation:\", inviteCode);\n                    const newFederation = await sw.new_federation(inviteCode);\n                    console.log(\"New federation added:\", newFederation);\n                    break;\n                } catch (e) {\n                    const error = eify(e);\n                    console.log(\"Error adding federation:\", error.message);\n                    // if we can't connect to the guardian, try to others,\n                    // otherwise throw the error\n                    if (\n                        error.message ===\n                            \"Failed to connect to a federation.\" ||\n                        error.message === \"Invalid Arguments were given\"\n                    ) {\n                        console.error(\n                            \"Failed to connect to guardian, trying another one\"\n                        );\n                    } else {\n                        throw e;\n                    }\n                }\n            }\n            setSuccess(\n                i18n.t(\"settings.manage_federations.federation_added_success\")\n            );\n            await actions.refreshFederations();\n            if (props.refetch) {\n                await props.refetch();\n            }\n            reset(feedbackForm);\n            if (props.setup) {\n                navigate(\"/\");\n            }\n        } catch (e) {\n            console.error(\"Error submitting federation:\", e);\n            setError(eify(e));\n        }\n    };\n\n    return (\n        <div class=\"flex w-full flex-col gap-4\">\n            <Show when={state.expiration_warning}>\n                <FederationPopup />\n            </Show>\n            <Show when={!props.setup && !props.browseOnly}>\n                <MediumHeader>\n                    {i18n.t(\"settings.manage_federations.manual\")}\n                </MediumHeader>\n                <Form onSubmit={handleSubmit}>\n                    <VStack>\n                        <Field\n                            name=\"federation_code\"\n                            validate={[\n                                required(\n                                    i18n.t(\n                                        \"settings.manage_federations.federation_code_required\"\n                                    )\n                                )\n                            ]}\n                        >\n                            {(field, props) => (\n                                <TextField\n                                    {...props}\n                                    {...field}\n                                    error={field.error}\n                                    placeholder=\"fed11...\"\n                                    required\n                                />\n                            )}\n                        </Field>\n                        <Button\n                            loading={feedbackForm.submitting}\n                            disabled={\n                                feedbackForm.invalid || !feedbackForm.dirty\n                            }\n                            intent=\"blue\"\n                            type=\"submit\"\n                        >\n                            {i18n.t(\"settings.manage_federations.add\")}\n                        </Button>\n                        <Show when={error()}>\n                            <InfoBox accent=\"red\">{error()?.message}</InfoBox>\n                        </Show>\n                        <Show when={success()}>\n                            <InfoBox accent=\"green\">{success()}</InfoBox>\n                        </Show>\n                    </VStack>\n                </Form>\n            </Show>\n        </div>\n    );\n}\n\nfunction RecommendButton(props: { fed: MutinyFederationIdentity }) {\n    const [_state, _actions, sw] = useMegaStore();\n    const i18n = useI18n();\n    const [recommendLoading, setRecommendLoading] = createSignal(false);\n    // This is just some local state that makes it feel like they've recommended it\n    // even if they aren't a \"real person\"\n    const [recommended, setRecommended] = createSignal(false);\n\n    const [recommendedByMe, { refetch }] = createResource(async () => {\n        try {\n            const hasRecommended = await sw.has_recommended_federation(\n                props.fed.federation_id\n            );\n            return hasRecommended;\n        } catch (e) {\n            console.error(e);\n            return false;\n        }\n    });\n\n    async function recommendFederation() {\n        setRecommendLoading(true);\n        try {\n            const event_id = await sw.recommend_federation(\n                props.fed.invite_code\n            );\n            console.log(\"Recommended federation: \", event_id);\n            setRecommended(true);\n            refetch();\n        } catch (e) {\n            console.error(\"Error recommending federation: \", e);\n        }\n        setRecommendLoading(false);\n    }\n\n    async function deleteRecommendation() {\n        setRecommendLoading(true);\n        try {\n            await sw.delete_federation_recommendation(props.fed.federation_id);\n            setRecommended(false);\n            refetch();\n        } catch (e) {\n            console.error(\"Error deleting federation recommendation: \", e);\n        }\n        setRecommendLoading(false);\n    }\n\n    return (\n        <Switch>\n            <Match when={recommendedByMe() || recommended()}>\n                <div class=\"flex items-center justify-between gap-2\">\n                    <div class=\"flex items-center gap-2\">\n                        <BadgeCheck class=\"h-4 w-4\" />\n                        {i18n.t(\n                            \"settings.manage_federations.recommended_by_you\"\n                        )}\n                    </div>\n                    <SubtleButton\n                        onClick={deleteRecommendation}\n                        loading={recommendLoading()}\n                    >\n                        <Trash class=\"h-4 w-4\" />\n                    </SubtleButton>\n                </div>\n            </Match>\n            <Match when={true}>\n                <SubtleButton\n                    onClick={recommendFederation}\n                    loading={recommendLoading()}\n                >\n                    <BadgeCheck class=\"h-4 w-4\" />\n                    {i18n.t(\"settings.manage_federations.recommend\")}\n                </SubtleButton>\n            </Match>\n        </Switch>\n    );\n}\n\nfunction ResyncLoadingBar(props: { value: number; max: number }) {\n    return (\n        <Progress.Root\n            value={props.value}\n            minValue={0}\n            maxValue={props.max}\n            getValueLabel={({ value, max }) => {\n                const percent = Math.round((value / max) * 100);\n                return `${percent}% - ${value.toLocaleString()} / ${max.toLocaleString()}`;\n            }}\n            class=\"flex w-full flex-col gap-2\"\n        >\n            <Progress.ValueLabel class=\"text-sm text-m-grey-400\" />\n            <Progress.Track class=\"h-6  rounded bg-white/10\">\n                <Progress.Fill class=\"h-full w-[var(--kb-progress-fill-width)] rounded bg-m-blue transition-[width]\" />\n            </Progress.Track>\n        </Progress.Root>\n    );\n}\n\nfunction FederationListItem(props: {\n    fed: MutinyFederationIdentity;\n    balance?: bigint;\n}) {\n    const i18n = useI18n();\n    const [state, actions, sw] = useMegaStore();\n    const navigate = useNavigate();\n\n    async function removeFederation() {\n        setConfirmLoading(true);\n        try {\n            await sw.remove_federation(props.fed.federation_id);\n            await actions.refreshFederations();\n        } catch (e) {\n            console.error(e);\n        }\n        setConfirmLoading(false);\n    }\n\n    // Warn when leaving the page if there's a resync in progress\n    createEffect(() => {\n        if (resyncLoading()) {\n            window.onbeforeunload = function () {\n                return true;\n            };\n        } else {\n            window.onbeforeunload = null;\n        }\n    });\n\n    const [shouldPollProgress, setShouldPollProgress] = createSignal(false);\n\n    async function resyncFederation() {\n        setResyncLoading(true);\n        try {\n            await sw.resync_federation(props.fed.federation_id);\n\n            console.log(\"RESYNC STARTED\");\n\n            // This loop is so we can try enough times until the resync actually starts\n            for (let i = 0; i < 60; i++) {\n                await timeout(1000);\n                const progress = await sw.get_federation_resync_progress(\n                    props.fed.federation_id\n                );\n\n                console.log(\"progress\", progress);\n                if (progress?.total !== 0) {\n                    setResyncProgress(progress);\n                    setResyncOpen(false);\n                    setShouldPollProgress(true);\n                    break;\n                }\n            }\n        } catch (e) {\n            console.error(e);\n            setResyncLoading(false);\n        }\n    }\n\n    function refetch() {\n        sw.get_federation_resync_progress(props.fed.federation_id).then(\n            (progress) => {\n                // if the progress is undefined, it also means we're done\n                if (progress === undefined) {\n                    setResyncProgress({\n                        total: 100,\n                        complete: 100,\n                        done: true\n                    });\n                    setShouldPollProgress(false);\n                    setResyncLoading(false);\n                    return;\n                } else if (progress?.done) {\n                    setShouldPollProgress(false);\n                    setResyncLoading(false);\n                }\n\n                setResyncProgress(progress);\n            }\n        );\n    }\n\n    createEffect(() => {\n        let interval = undefined;\n\n        if (shouldPollProgress()) {\n            interval = setInterval(() => {\n                refetch();\n            }, 1000); // Poll every second\n        }\n\n        if (!shouldPollProgress()) {\n            clearInterval(interval);\n        }\n\n        onCleanup(() => {\n            clearInterval(interval);\n        });\n    });\n\n    async function confirmRemove() {\n        setConfirmOpen(true);\n    }\n\n    async function confirmResync() {\n        setResyncOpen(true);\n    }\n\n    const [transferDialogOpen, setTransferDialogOpen] = createSignal(false);\n\n    async function transferFunds() {\n        // If there's only one federation we need to let them know to add another\n        if (state.federations?.length && state.federations.length < 2) {\n            setTransferDialogOpen(true);\n        } else {\n            navigate(\"/transfer?from=\" + props.fed.federation_id);\n        }\n    }\n\n    const [confirmOpen, setConfirmOpen] = createSignal(false);\n    const [confirmLoading, setConfirmLoading] = createSignal(false);\n\n    const [resyncOpen, setResyncOpen] = createSignal(false);\n    const [resyncLoading, setResyncLoading] = createSignal(false);\n\n    const [resyncProgress, setResyncProgress] = createSignal<\n        ResyncProgress | undefined\n    >(undefined);\n\n    return (\n        <>\n            <FancyCard>\n                <VStack>\n                    <Show when={props.fed.federation_name}>\n                        <header class={`font-semibold`}>\n                            {props.fed.federation_name}\n                        </header>\n                    </Show>\n                    <Show when={props.fed.welcome_message}>\n                        <p>{props.fed.welcome_message}</p>\n                    </Show>\n                    <SimpleDialog\n                        title={i18n.t(\n                            \"settings.manage_federations.transfer_funds\"\n                        )}\n                        open={transferDialogOpen()}\n                        setOpen={setTransferDialogOpen}\n                    >\n                        <NiceP>\n                            {i18n.t(\n                                \"settings.manage_federations.transfer_funds_message\"\n                            )}\n                        </NiceP>\n                    </SimpleDialog>\n                    <Show when={props.balance !== undefined}>\n                        <KeyValue\n                            key={i18n.t(\"activity.transaction_details.balance\")}\n                        >\n                            <AmountSats\n                                amountSats={props.balance}\n                                denominationSize={\"sm\"}\n                                isFederation\n                            />\n                        </KeyValue>\n                    </Show>\n                    <Show when={props.fed.federation_expiry_timestamp}>\n                        <KeyValue\n                            key={i18n.t(\"settings.manage_federations.expires\")}\n                        >\n                            <time>\n                                {timeAgo(props.fed.federation_expiry_timestamp)}\n                            </time>\n                        </KeyValue>\n                    </Show>\n                    <KeyValue\n                        key={i18n.t(\n                            \"settings.manage_federations.federation_id\"\n                        )}\n                    >\n                        <MiniStringShower text={props.fed.federation_id} />\n                    </KeyValue>\n                    <KeyValue key={\"Invite code\"}>\n                        <FederationInviteShower\n                            name={props.fed.federation_name}\n                            inviteCode={props.fed.invite_code}\n                        />\n                    </KeyValue>\n                    <SubtleButton onClick={transferFunds}>\n                        <ArrowLeftRight class=\"h-4 w-4\" />\n                        {i18n.t(\"settings.manage_federations.transfer_funds\")}\n                    </SubtleButton>\n                    <Suspense>\n                        <RecommendButton fed={props.fed} />\n                    </Suspense>\n                    <SubtleButton intent=\"red\" onClick={confirmRemove}>\n                        <LogOut class=\"h-4 w-4\" />\n                        {i18n.t(\"settings.manage_federations.remove\")}\n                    </SubtleButton>\n                    <Show when={state.safe_mode}>\n                        <SubtleButton intent=\"red\" onClick={confirmResync}>\n                            <RefreshCw class=\"h-4 w-4\" />\n                            Resync\n                        </SubtleButton>\n                    </Show>\n                </VStack>\n            </FancyCard>\n            <ConfirmDialog\n                loading={confirmLoading()}\n                open={confirmOpen()}\n                onConfirm={removeFederation}\n                onCancel={() => setConfirmOpen(false)}\n            >\n                {i18n.t(\n                    \"settings.manage_federations.federation_remove_confirm\"\n                )}\n            </ConfirmDialog>\n            <ConfirmDialog\n                loading={resyncLoading()}\n                open={resyncOpen()}\n                onConfirm={resyncFederation}\n                onCancel={() => setResyncOpen(false)}\n            >\n                Are you sure you want to resync this federation? This will\n                rescan the federation for your ecash, this can take multiple\n                hours in some cases. If you stop the rescan it can cause your\n                wallet to be bricked. Please be sure you can run the rescan\n                before you start it.\n            </ConfirmDialog>\n            <Show when={resyncProgress()}>\n                <SimpleDialog\n                    title={\"Resyncing...\"}\n                    open={!resyncProgress()!.done}\n                >\n                    <Show when={!resyncProgress()!.done}>\n                        <NiceP>This could take a couple of hours.</NiceP>\n                        <NiceP>\n                            DO NOT CLOSE THIS WINDOW UNTIL RESYNC IS DONE.\n                        </NiceP>\n                        <ResyncLoadingBar\n                            value={resyncProgress()!.complete}\n                            max={resyncProgress()!.total}\n                        />\n                    </Show>\n                    <Show when={resyncProgress()!.done}>\n                        <NiceP>Resync complete!</NiceP>\n                        <Button onClick={() => (window.location.href = \"/\")}>\n                            Nice\n                        </Button>\n                    </Show>\n                </SimpleDialog>\n            </Show>\n        </>\n    );\n}\n\nexport function ManageFederations() {\n    const i18n = useI18n();\n    const [state, _actions, sw] = useMegaStore();\n\n    const [balances, { refetch }] = createResource(async () => {\n        try {\n            const balances = await sw.get_federation_balances();\n            return balances?.balances || [];\n        } catch (e) {\n            console.error(e);\n            return [];\n        }\n    });\n\n    const [params, setParams] = useSearchParams();\n\n    onMount(() => {\n        if (params.fedimint_invite && state.federations?.length) {\n            showToast(\n                new Error(i18n.t(\"settings.manage_federations.already_in_fed\"))\n            );\n\n            // Clear the search params\n            setParams({ fedimint_invite: undefined });\n        }\n    });\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <div class=\"flex items-center justify-between\">\n                    <BackLink\n                        href=\"/profile\"\n                        title={i18n.t(\"profile.profile\")}\n                        showOnDesktop\n                    />\n                    <Show when={!state.federations?.length}>\n                        <A\n                            class=\"rounded-lg p-2 hover:bg-white/5 active:bg-m-blue\"\n                            href=\"/scanner\"\n                        >\n                            <Scan />\n                        </A>{\" \"}\n                    </Show>\n                </div>\n                <LargeHeader>\n                    {i18n.t(\"settings.manage_federations.title\")}\n                </LargeHeader>\n                <NiceP>\n                    {i18n.t(\"settings.manage_federations.description\")}{\" \"}\n                </NiceP>\n                <NiceP>\n                    {i18n.t(\"settings.manage_federations.descriptionpart2\")}{\" \"}\n                </NiceP>\n                <NiceP>\n                    <ExternalLink href=\"https://fedimint.org/docs/intro\">\n                        {i18n.t(\"settings.manage_federations.learn_more\")}\n                    </ExternalLink>\n                </NiceP>\n                <Suspense>\n                    <Show when={!state.federations?.length}>\n                        <AddFederationForm refetch={refetch} />\n                    </Show>\n                </Suspense>\n                <VStack>\n                    <Suspense>\n                        <Switch>\n                            <Match when={balances()}>\n                                <For each={state.federations ?? []}>\n                                    {(fed) => (\n                                        <FederationListItem\n                                            fed={fed}\n                                            balance={\n                                                balances()?.find(\n                                                    (b) =>\n                                                        b.identity_federation_id ===\n                                                        fed.federation_id\n                                                )?.balance\n                                            }\n                                        />\n                                    )}\n                                </For>\n                            </Match>\n                            <Match when={true}>\n                                <For each={state.federations ?? []}>\n                                    {(fed) => <FederationListItem fed={fed} />}\n                                </For>\n                            </Match>\n                        </Switch>\n                    </Suspense>\n                </VStack>\n                <Suspense>\n                    <Show when={state.federations?.length}>\n                        <AddFederationForm refetch={refetch} />\n                    </Show>\n                </Suspense>\n            </DefaultMain>\n            <NavBar activeTab=\"settings\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/NostrKeys.tsx",
    "content": "import { A, createAsync } from \"@solidjs/router\";\nimport { SecureStoragePlugin } from \"capacitor-secure-storage-plugin\";\nimport { Import, Trash, Unlink } from \"lucide-solid\";\nimport { createResource, createSignal, Match, Show, Switch } from \"solid-js\";\nimport { QRCodeSVG } from \"solid-qr-code\";\n\nimport {\n    BackPop,\n    ConfirmDialog,\n    DefaultMain,\n    ExternalLink,\n    FancyCard,\n    KeyValue,\n    LargeHeader,\n    MiniStringShower,\n    MutinyWalletGuard,\n    NavBar,\n    NiceP,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nfunction DeleteAccount() {\n    const i18n = useI18n();\n    const [_state, _actions, sw] = useMegaStore();\n\n    async function confirmDelete() {\n        setConfirmOpen(true);\n    }\n\n    const [confirmOpen, setConfirmOpen] = createSignal(false);\n    const [confirmLoading, setConfirmLoading] = createSignal(false);\n\n    async function deleteNostrAccount() {\n        setConfirmLoading(true);\n        try {\n            await sw.delete_profile();\n            // Remove the nsec from secure storage if it exists\n            await SecureStoragePlugin.clear();\n            window.location.href = \"/\";\n        } catch (e) {\n            console.error(e);\n        }\n        setConfirmLoading(false);\n    }\n\n    return (\n        <>\n            <button\n                class=\"flex w-full items-center justify-center gap-2 rounded-xl border border-white/10 bg-neutral-900 p-2 text-m-red  no-underline active:-mb-[1px] active:mt-[1px] active:opacity-70\"\n                onClick={confirmDelete}\n            >\n                <Trash class=\"w-4\" />\n                {i18n.t(\"settings.nostr_keys.delete_account\")}\n            </button>\n            <ConfirmDialog\n                loading={confirmLoading()}\n                open={confirmOpen()}\n                onConfirm={deleteNostrAccount}\n                onCancel={() => setConfirmOpen(false)}\n            >\n                <p>{i18n.t(\"settings.nostr_keys.delete_account_confirm\")}</p>\n                <p class=\"font-semibold\">\n                    {i18n.t(\"settings.nostr_keys.delete_account_confirm_scary\")}\n                </p>\n            </ConfirmDialog>\n        </>\n    );\n}\n\nfunction UnlinkAccount() {\n    const i18n = useI18n();\n\n    async function confirmUnlink() {\n        setConfirmOpen(true);\n    }\n\n    const [confirmOpen, setConfirmOpen] = createSignal(false);\n    const [confirmLoading, setConfirmLoading] = createSignal(false);\n\n    async function unlinkNostrAccount() {\n        setConfirmLoading(true);\n        try {\n            await SecureStoragePlugin.clear();\n            window.location.href = \"/\";\n        } catch (e) {\n            console.error(e);\n        }\n        setConfirmLoading(false);\n    }\n\n    return (\n        <>\n            <button\n                class=\"flex w-full items-center justify-center gap-2 rounded-xl border border-white/10 bg-neutral-900 p-2 text-m-grey-350  no-underline active:-mb-[1px] active:mt-[1px] active:opacity-70\"\n                onClick={confirmUnlink}\n            >\n                <Unlink class=\"w-4\" />\n                {i18n.t(\"settings.nostr_keys.unlink_account\")}\n            </button>\n            <ConfirmDialog\n                loading={confirmLoading()}\n                open={confirmOpen()}\n                onConfirm={unlinkNostrAccount}\n                onCancel={() => setConfirmOpen(false)}\n            >\n                {i18n.t(\"settings.nostr_keys.unlink_account_confirm\")}\n            </ConfirmDialog>\n        </>\n    );\n}\n\nexport function NostrKeys() {\n    const i18n = useI18n();\n    const [_state, _actions, sw] = useMegaStore();\n\n    const npub = createAsync(async () => await sw.get_npub());\n    const nsec = createAsync(async () => await sw.export_nsec());\n    const profile = createAsync(async () => await sw.get_nostr_profile());\n\n    const [nsecInSecureStorage] = createResource(async () => {\n        try {\n            const value = await SecureStoragePlugin.get({ key: \"nsec\" });\n            if (value) return true;\n        } catch (e) {\n            console.log(\"No nsec stored\");\n            return false;\n        }\n    });\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackPop default=\"/settings\" />\n                <LargeHeader>{i18n.t(\"settings.nostr_keys.title\")}</LargeHeader>\n                <NiceP>{i18n.t(\"settings.nostr_keys.description\")}</NiceP>\n                <NiceP>\n                    <ExternalLink href=\"https://nostr.com/\">\n                        {i18n.t(\"settings.nostr_keys.learn_more\")}\n                    </ExternalLink>\n                </NiceP>\n                <Switch>\n                    <Match when={profile() && !profile()?.deleted}>\n                        <FancyCard>\n                            <VStack>\n                                <div class=\"w-[10rem] self-center rounded bg-white p-[1rem]\">\n                                    <QRCodeSVG\n                                        value={npub() || \"\"}\n                                        class=\"h-full max-h-[256px] w-full\"\n                                    />\n                                </div>\n                                <KeyValue key=\"Public Key\">\n                                    <MiniStringShower text={npub() || \"\"} />\n                                </KeyValue>\n                                <Show when={nsec()}>\n                                    <KeyValue key=\"Private Key\">\n                                        <MiniStringShower\n                                            text={nsec() || \"\"}\n                                            hide\n                                        />\n                                    </KeyValue>\n                                    <p class=\"text-base italic text-m-grey-350\">\n                                        {i18n.t(\"settings.nostr_keys.warning\")}\n                                    </p>\n                                </Show>\n                            </VStack>\n                        </FancyCard>\n                        <A\n                            href=\"/settings/importprofile\"\n                            class=\"flex w-full items-center justify-center gap-2 rounded-xl border border-white/10 bg-neutral-900 p-2 text-m-grey-350 no-underline active:-mb-[1px] active:mt-[1px] active:opacity-70\"\n                        >\n                            <Import class=\"w-4\" />\n                            {i18n.t(\"settings.nostr_keys.import_profile\")}\n                        </A>\n                        <Switch>\n                            <Match when={nsecInSecureStorage()}>\n                                <UnlinkAccount />\n                            </Match>\n                            <Match when={!nsecInSecureStorage()}>\n                                <DeleteAccount />\n                            </Match>\n                        </Switch>\n                    </Match>\n                    <Match when={profile() && profile()?.deleted}>\n                        <A\n                            href=\"/settings/importprofile\"\n                            class=\"flex w-full items-center justify-center gap-2 rounded-xl border border-white/10 bg-neutral-900 p-2 text-m-grey-350 no-underline active:-mb-[1px] active:mt-[1px] active:opacity-70\"\n                        >\n                            <Import class=\"w-4\" />\n                            {i18n.t(\"settings.nostr_keys.import_profile\")}\n                        </A>\n                    </Match>\n                </Switch>\n            </DefaultMain>\n            <NavBar activeTab=\"settings\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/Plus.tsx",
    "content": "import { A } from \"@solidjs/router\";\nimport {\n    createResource,\n    createSignal,\n    Match,\n    Show,\n    Suspense,\n    Switch\n} from \"solid-js\";\n\nimport party from \"~/assets/party.gif\";\nimport {\n    BackLink,\n    Button,\n    ConfirmDialog,\n    DefaultMain,\n    ExternalLink,\n    FancyCard,\n    InfoBox,\n    LargeHeader,\n    LoadingShimmer,\n    MutinyWalletGuard,\n    NavBar,\n    NiceP,\n    TinyText,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify, vibrateSuccess } from \"~/utils\";\n\nfunction Perks(props: { alreadySubbed?: boolean }) {\n    const i18n = useI18n();\n    return (\n        <ul class=\"ml-8 list-disc text-lg font-light\">\n            <Show when={props.alreadySubbed}>\n                <li>{i18n.t(\"settings.plus.satisfaction\")}</li>\n            </Show>\n            <li>\n                <Show\n                    when={props.alreadySubbed}\n                    fallback={i18n.t(\"settings.plus.ios_testflight\")}\n                >\n                    <ExternalLink href=\"https://testflight.apple.com/join/9g23f0Mc\">\n                        {i18n.t(\"settings.plus.ios_testflight\")}\n                    </ExternalLink>\n                </Show>\n            </li>\n            <li>{i18n.t(\"settings.plus.lightning_address\")}</li>\n            <li>{i18n.t(\"settings.plus.more\")}</li>\n        </ul>\n    );\n}\n\nfunction PlusCTA() {\n    const i18n = useI18n();\n    const [state, actions, sw] = useMegaStore();\n\n    const [subbing, setSubbing] = createSignal(false);\n    const [confirmOpen, setConfirmOpen] = createSignal(false);\n\n    const [error, setError] = createSignal<Error>();\n\n    const [planDetails] = createResource(async () => {\n        try {\n            const plans = await sw.get_subscription_plans();\n            console.log(\"plans:\", plans);\n            if (!plans) return undefined;\n            return plans[0];\n        } catch (e) {\n            console.error(e);\n        }\n    });\n\n    async function handleConfirm() {\n        try {\n            setSubbing(true);\n            setError(undefined);\n\n            if (planDetails()?.id === undefined || planDetails()?.id === null)\n                throw new Error(i18n.t(\"settings.plus.error_no_plan\"));\n\n            const invoice = await sw.subscribe_to_plan(planDetails()!.id);\n\n            if (!invoice?.bolt11)\n                throw new Error(i18n.t(\"settings.plus.error_failure\"));\n\n            await sw.pay_subscription_invoice(invoice?.bolt11, true);\n\n            await vibrateSuccess();\n\n            // \"true\" flag gives this a fallback to set a timestamp in case the subscription server is down\n            await actions.checkForSubscription(true);\n        } catch (e) {\n            console.error(e);\n            setError(eify(e));\n        } finally {\n            setConfirmOpen(false);\n            setSubbing(false);\n        }\n    }\n\n    const hasEnough = () => {\n        if (!planDetails()) return false;\n        return (\n            (state.balance?.lightning || 0n) +\n                (state.balance?.federation || 0n) >\n            planDetails()!.amount_sat\n        );\n    };\n\n    return (\n        <Show when={planDetails()}>\n            <VStack>\n                <NiceP>\n                    {i18n.t(\"settings.plus.join\")}{\" \"}\n                    <strong class=\"text-white\">\n                        {i18n.t(\"settings.plus.title\")}\n                    </strong>{\" \"}\n                    {i18n.t(\"settings.plus.sats_per_month\", {\n                        amount: Number(\n                            planDetails()!.amount_sat\n                        ).toLocaleString()\n                    })}\n                </NiceP>\n                <Show when={error()}>\n                    <InfoBox accent=\"red\">{error()!.message}</InfoBox>\n                </Show>\n                <Show when={!hasEnough()}>\n                    <TinyText>\n                        {i18n.t(\"settings.plus.lightning_balance\", {\n                            amount: Number(\n                                planDetails()!.amount_sat\n                            ).toLocaleString()\n                        })}\n                    </TinyText>\n                </Show>\n                <div class=\"flex gap-2\">\n                    <Button\n                        intent=\"red\"\n                        layout=\"flex\"\n                        onClick={() => setConfirmOpen(true)}\n                        disabled={!hasEnough()}\n                    >\n                        {i18n.t(\"settings.plus.join\")}\n                    </Button>\n                </div>\n            </VStack>\n            <ConfirmDialog\n                loading={subbing()}\n                open={confirmOpen()}\n                onConfirm={handleConfirm}\n                onCancel={() => setConfirmOpen(false)}\n            >\n                <p>\n                    {i18n.t(\"settings.plus.ready_to_join\")}{\" \"}\n                    <strong class=\"text-white\">\n                        {i18n.t(\"settings.plus.title\")}\n                    </strong>\n                    ? {i18n.t(\"settings.plus.click_confirm\")}\n                </p>\n            </ConfirmDialog>\n        </Show>\n    );\n}\n\nexport function Plus() {\n    const i18n = useI18n();\n    const [state, _actions] = useMegaStore();\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackLink href=\"/settings\" title={i18n.t(\"settings.header\")} />\n                <LargeHeader>{i18n.t(\"settings.plus.title\")}</LargeHeader>\n                <Switch>\n                    <Match when={state.mutiny_plus}>\n                        <img src={party} class=\"mx-auto w-1/2\" />\n                        <NiceP>{i18n.t(\"settings.plus.thanks\")}</NiceP>\n\n                        <Perks alreadySubbed />\n                        <NiceP>\n                            {i18n.t(\"settings.plus.renewal_time\")}{\" \"}\n                            <strong class=\"text-white\">\n                                {new Date(\n                                    state.subscription_timestamp! * 1000\n                                ).toLocaleDateString()}\n                            </strong>\n                            .\n                        </NiceP>\n                        <NiceP>\n                            {i18n.t(\"settings.plus.cancel\")}{\" \"}\n                            <A href=\"/settings/connections\">\n                                {i18n.t(\"settings.plus.wallet_connection\")}\n                            </A>\n                        </NiceP>\n                    </Match>\n                    <Match when={!state.mutiny_plus}>\n                        <NiceP>\n                            {i18n.t(\"settings.plus.open_source\")}{\" \"}\n                            <strong>\n                                {i18n.t(\"settings.plus.optional_pay\")}\n                            </strong>\n                        </NiceP>\n                        <NiceP>\n                            {i18n.t(\"settings.plus.paying_for\")}{\" \"}\n                            <strong class=\"text-white\">\n                                {i18n.t(\"settings.plus.title\")}\n                            </strong>{\" \"}\n                            {i18n.t(\"settings.plus.supports_dev\")}\n                        </NiceP>\n                        <Perks />\n                        <FancyCard title={i18n.t(\"settings.plus.subscribe\")}>\n                            <Suspense fallback={<LoadingShimmer />}>\n                                <PlusCTA />\n                            </Suspense>\n                        </FancyCard>\n                    </Match>\n                </Switch>\n            </DefaultMain>\n            <NavBar activeTab=\"settings\" />\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/Restore.tsx",
    "content": "import { Clipboard } from \"@capacitor/clipboard\";\nimport { Capacitor } from \"@capacitor/core\";\nimport { TextField as KTextField } from \"@kobalte/core\";\nimport {\n    createForm,\n    custom,\n    required,\n    setValues,\n    SubmitHandler,\n    validate\n} from \"@modular-forms/solid\";\nimport { LucideClipboard } from \"lucide-solid\";\nimport { createSignal, For, Show, splitProps } from \"solid-js\";\n\nimport {\n    BackLink,\n    Button,\n    ConfirmDialog,\n    DefaultMain,\n    InfoBox,\n    LargeHeader,\n    NavBar,\n    NiceP,\n    showToast,\n    TextFieldProps,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify, WORDS_EN } from \"~/utils\";\n\ntype SeedWordsForm = {\n    words: string[];\n};\n\n// create an array of 12 empty strings\nconst initialValues: SeedWordsForm = {\n    words: Array.from({ length: 12 }, () => \"\")\n};\n\nfunction validateWord(word?: string): boolean {\n    const trimmed = word?.trim();\n    return trimmed ? WORDS_EN.has(trimmed) : false;\n}\n\nfunction SeedTextField(props: TextFieldProps) {\n    const [fieldProps] = splitProps(props, [\n        \"placeholder\",\n        \"ref\",\n        \"onInput\",\n        \"onChange\",\n        \"onBlur\"\n    ]);\n    return (\n        <KTextField.Root\n            class=\"flex flex-col gap-2\"\n            name={props.name}\n            value={props.value}\n            validationState={props.error ? \"invalid\" : \"valid\"}\n            required={props.required}\n        >\n            <KTextField.Input\n                {...fieldProps}\n                autoCapitalize=\"none\"\n                autocorrect=\"off\"\n                autocomplete=\"off\"\n                type={props.type}\n                class=\"w-full rounded-lg border bg-m-grey-750 p-2 placeholder-neutral-400\"\n                classList={{\n                    \"border-m-grey-750\": !props.error && !props.value,\n                    \"border-m-red\": !!props.error,\n                    \"border-m-green\": !props.error && !!props.value\n                }}\n            />\n        </KTextField.Root>\n    );\n}\n\nexport function TwelveWordsEntry() {\n    const i18n = useI18n();\n    const [state, actions, sw] = useMegaStore();\n\n    const [error, setError] = createSignal<Error>();\n    const [mnemnoic, setMnemonic] = createSignal<string>();\n\n    const [confirmOpen, setConfirmOpen] = createSignal(false);\n    const [confirmLoading, setConfirmLoading] = createSignal(false);\n\n    const [seedWordsForm, { Form, Field, FieldArray }] =\n        createForm<SeedWordsForm>({\n            initialValues,\n            validateOn: \"blur\"\n        });\n\n    async function handlePaste() {\n        try {\n            let text: string;\n\n            if (Capacitor.isNativePlatform()) {\n                const { value } = await Clipboard.read();\n                text = value;\n            } else {\n                if (!navigator.clipboard.readText) {\n                    return showToast(\n                        new Error(i18n.t(\"settings.restore.error_clipboard\"))\n                    );\n                }\n                text = await navigator.clipboard.readText();\n            }\n\n            // split words on space or newline\n            const words = text.trim().split(/[\\s\\n]+/);\n\n            if (words.length !== 12) {\n                return showToast(\n                    new Error(i18n.t(\"settings.restore.error_word_number\"))\n                );\n            }\n\n            setValues(seedWordsForm, \"words\", words);\n            validate(seedWordsForm);\n        } catch (e) {\n            console.error(e);\n        }\n    }\n\n    async function restore() {\n        try {\n            setConfirmLoading(true);\n\n            if (state.load_stage === \"done\") {\n                console.log(\"Mutiny wallet loaded, stopping\");\n                try {\n                    await sw.stop();\n                } catch (e) {\n                    console.error(e);\n                }\n            }\n\n            await sw.restore_mnemonic(mnemnoic() || \"\", \"\");\n\n            actions.setHasBackedUp();\n\n            setTimeout(() => {\n                window.location.href = \"/\";\n            }, 1000);\n        } catch (e) {\n            setError(eify(e));\n        } finally {\n            setConfirmLoading(false);\n        }\n    }\n\n    const onSubmit: SubmitHandler<SeedWordsForm> = async (values) => {\n        setError(undefined);\n        const valid = values.words?.every(validateWord);\n\n        if (!valid) {\n            setError(new Error(i18n.t(\"settings.restore.error_invalid_seed\")));\n            return;\n        }\n\n        const seed = values.words?.join(\" \");\n        setMnemonic(seed);\n        setConfirmOpen(true);\n    };\n\n    return (\n        <>\n            <Form onSubmit={onSubmit} class=\"flex flex-col gap-4\">\n                <div class=\"flex flex-col gap-4 overflow-hidden rounded-xl bg-m-grey-800 p-4\">\n                    <Show when={error()}>\n                        <InfoBox accent=\"red\">{error()?.message}</InfoBox>\n                    </Show>\n                    <ul class=\"w-full list-none columns-2 overflow-hidden pr-2 pt-2\">\n                        <FieldArray name=\"words\">\n                            {(fieldArray) => (\n                                <For each={fieldArray.items}>\n                                    {(_, index) => (\n                                        <div class=\"mb-2 flex items-center gap-1\">\n                                            <pre class=\"w-[2rem] text-right\">\n                                                {index() + 1}\n                                                {\".\"}\n                                            </pre>\n                                            <Field\n                                                name={`words.${index()}`}\n                                                validate={[\n                                                    required(\n                                                        i18n.t(\n                                                            \"settings.restore.all_twelve\"\n                                                        )\n                                                    ),\n                                                    custom(\n                                                        validateWord,\n                                                        i18n.t(\n                                                            \"settings.restore.wrong_word\"\n                                                        )\n                                                    )\n                                                ]}\n                                            >\n                                                {(field, props) => (\n                                                    <SeedTextField\n                                                        {...props}\n                                                        value={field.value}\n                                                        error={field.error}\n                                                    />\n                                                )}\n                                            </Field>\n                                        </div>\n                                    )}\n                                </For>\n                            )}\n                        </FieldArray>\n                    </ul>\n                    <div class=\"flex w-full justify-center\">\n                        <button\n                            onClick={handlePaste}\n                            class=\"rounded-lg bg-white/10 px-4 py-2 hover:bg-white/20\"\n                            type=\"button\"\n                        >\n                            <div class=\"flex items-center gap-2\">\n                                <span>{i18n.t(\"settings.restore.paste\")}</span>\n                                <LucideClipboard class=\"h-4 w-4\" />\n                            </div>\n                        </button>\n                    </div>\n                </div>\n                <Button\n                    type=\"submit\"\n                    intent=\"red\"\n                    disabled={seedWordsForm.invalid || !seedWordsForm.dirty}\n                >\n                    {i18n.t(\"settings.restore.title\")}\n                </Button>\n            </Form>\n            <ConfirmDialog\n                open={confirmOpen()}\n                onConfirm={restore}\n                onCancel={() => setConfirmOpen(false)}\n                loading={confirmLoading()}\n            >\n                <p>{i18n.t(\"settings.restore.confirm_text\")}</p>\n            </ConfirmDialog>\n        </>\n    );\n}\n\nexport function Restore() {\n    const i18n = useI18n();\n    return (\n        <DefaultMain>\n            <BackLink title={i18n.t(\"settings.header\")} href=\"/settings\" />\n            <LargeHeader>{i18n.t(\"settings.restore.title\")}</LargeHeader>\n            <VStack>\n                <NiceP>\n                    <p>{i18n.t(\"settings.restore.restore_tip\")}</p>\n                    <p>{i18n.t(\"settings.restore.multi_browser_warning\")}</p>\n                </NiceP>\n                <TwelveWordsEntry />\n            </VStack>\n            <NavBar activeTab=\"settings\" />\n        </DefaultMain>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/Root.tsx",
    "content": "import { A } from \"@solidjs/router\";\nimport { ChevronRight } from \"lucide-solid\";\nimport { For, Show } from \"solid-js\";\n\nimport {\n    BackLink,\n    DefaultMain,\n    ExternalLink,\n    LargeHeader,\n    NavBar,\n    SettingsCard,\n    TinyText,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { FeedbackLink } from \"~/routes/Feedback\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nfunction SettingsLinkList(props: {\n    header: string;\n    links: {\n        href: string;\n        text: string;\n        caption?: string;\n        accent?: \"red\" | \"green\";\n        disabled?: boolean;\n    }[];\n}) {\n    return (\n        <SettingsCard title={props.header}>\n            <For each={props.links}>\n                {(link) => (\n                    <A\n                        href={link.href}\n                        class=\"flex w-full flex-col gap-1 px-4 py-2 no-underline hover:bg-m-grey-750 active:bg-m-grey-900\"\n                        classList={{\n                            \"opacity-50 cursor pointer-events-none grayscale\":\n                                link.disabled\n                        }}\n                    >\n                        <div class=\"flex justify-between\">\n                            <span\n                                classList={{\n                                    \"text-m-red\": link.accent === \"red\",\n                                    \"text-m-green\": link.accent === \"green\"\n                                }}\n                            >\n                                {link.text}\n                            </span>\n                            <ChevronRight />\n                        </div>\n                        <Show when={link.caption}>\n                            <div class=\"text-sm text-m-grey-400\">\n                                {link.caption}\n                            </div>\n                        </Show>\n                    </A>\n                )}\n            </For>\n        </SettingsCard>\n    );\n}\n\nexport function Settings() {\n    const i18n = useI18n();\n    const [state, _actions] = useMegaStore();\n    // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n    // @ts-ignore\n    const RELEASE_VERSION = import.meta.env.__RELEASE_VERSION__;\n    // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n    // @ts-ignore\n    const COMMIT_HASH = import.meta.env.__COMMIT_HASH__;\n\n    return (\n        <DefaultMain>\n            <BackLink />\n            <LargeHeader>{i18n.t(\"settings.header\")}</LargeHeader>\n            <VStack biggap>\n                <SettingsLinkList\n                    header={i18n.t(\"settings.general\")}\n                    links={[\n                        {\n                            href: \"/settings/channels\",\n                            text: i18n.t(\"settings.channels.title\")\n                        },\n                        {\n                            href: \"/settings/backup\",\n                            text: i18n.t(\"settings.backup.title\"),\n                            accent: \"green\"\n                        },\n                        {\n                            href: \"/settings/restore\",\n                            text: i18n.t(\"settings.restore.title\"),\n                            accent: \"red\"\n                        },\n                        {\n                            href: \"/settings/encrypt\",\n                            text: i18n.t(\"settings.encrypt.title\"),\n                            disabled: !state.has_backed_up,\n                            caption: !state.has_backed_up\n                                ? i18n.t(\"settings.encrypt.caption\")\n                                : undefined\n                        },\n\n                        {\n                            href: \"/settings/servers\",\n                            text: i18n.t(\"settings.servers.title\"),\n                            caption: i18n.t(\"settings.servers.caption\")\n                        }\n                    ]}\n                />\n                <SettingsLinkList\n                    header={i18n.t(\"settings.appearance\")}\n                    links={[\n                        {\n                            href: \"/settings/currency\",\n                            text: i18n.t(\"settings.currency.title\"),\n                            caption: i18n.t(\"settings.currency.caption\")\n                        },\n                        {\n                            href: \"/settings/language\",\n                            text: i18n.t(\"settings.language.title\"),\n                            caption: i18n.t(\"settings.language.caption\")\n                        }\n                    ]}\n                />\n                <SettingsLinkList\n                    header={i18n.t(\"settings.social\")}\n                    links={[\n                        {\n                            href: \"/settings/nostrkeys\",\n                            text: i18n.t(\"settings.nostr_keys.title\"),\n                            caption: i18n.t(\"settings.nostr_keys.caption\")\n                        }\n                    ]}\n                />\n                <SettingsLinkList\n                    header={i18n.t(\"settings.debug_tools\")}\n                    links={[\n                        {\n                            href: \"/settings/emergencykit\",\n                            text: i18n.t(\"settings.emergency_kit.title\"),\n                            caption: i18n.t(\"settings.emergency_kit.caption\")\n                        },\n                        {\n                            href: \"/settings/admin\",\n                            text: i18n.t(\"settings.admin.title\"),\n                            caption: i18n.t(\"settings.admin.caption\"),\n                            accent: \"red\"\n                        }\n                    ]}\n                />\n                <div class=\"flex justify-center\">\n                    <FeedbackLink />\n                </div>\n                <div class=\"flex justify-center pb-8\">\n                    <TinyText>\n                        {i18n.t(\"settings.version\")} {RELEASE_VERSION}{\" \"}\n                        <ExternalLink\n                            href={`https://github.com/MutinyWallet/mutiny-web/commits/${COMMIT_HASH}`}\n                        >\n                            {COMMIT_HASH}\n                        </ExternalLink>\n                    </TinyText>\n                </div>\n            </VStack>\n            <NavBar activeTab=\"settings\" />\n        </DefaultMain>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/Servers.tsx",
    "content": "import { createForm, custom, url } from \"@modular-forms/solid\";\nimport { createResource, For, Match, Suspense, Switch } from \"solid-js\";\n\nimport {\n    BackLink,\n    Button,\n    Card,\n    DefaultMain,\n    ExternalLink,\n    LargeHeader,\n    LoadingShimmer,\n    MutinyWalletGuard,\n    NavBar,\n    NiceP,\n    showToast,\n    SimpleErrorDisplay,\n    TextField,\n    TinyText\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport {\n    getSettings,\n    MutinyWalletSettingStrings,\n    setSettings\n} from \"~/logic/mutinyWalletSetup\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { eify } from \"~/utils\";\n\nconst validateNotTorUrl = async (value?: string) => {\n    if (!value) {\n        return false;\n    }\n\n    // if it is one of the default URLs, it's safe\n    // need this handling for self-hosting deployments\n    if (\n        value === import.meta.env.VITE_PROXY ||\n        value === import.meta.env.VITE_ESPLORA ||\n        value === import.meta.env.VITE_RGS ||\n        value === import.meta.env.VITE_LSP ||\n        value === import.meta.env.VITE_STORAGE\n    ) {\n        return true;\n    }\n\n    return !value.includes(\".onion\");\n};\n\nfunction SettingsStringsEditor(props: {\n    initialSettings: MutinyWalletSettingStrings;\n}) {\n    const [_state, _actions, sw] = useMegaStore();\n    const i18n = useI18n();\n    const [settingsForm, { Form, Field }] =\n        createForm<MutinyWalletSettingStrings>({\n            initialValues: props.initialSettings\n        });\n\n    async function handleSubmit(values: MutinyWalletSettingStrings) {\n        try {\n            if (values.lsp !== props.initialSettings.lsp) {\n                await sw.change_lsp(\n                    values.lsp ? values.lsp : undefined,\n                    undefined,\n                    undefined\n                );\n            }\n\n            await setSettings(values);\n            window.location.reload();\n        } catch (e) {\n            console.error(e);\n            const err = eify(e);\n            if (err.message === \"Failed to make a request to the LSP.\") {\n                showToast(\n                    new Error(\n                        i18n.t(\"settings.servers.error_lsp_change_failed\")\n                    )\n                );\n            } else {\n                showToast(eify(e));\n            }\n        }\n    }\n\n    const MAINNET_LSP_OPTIONS = [\n        {\n            value: \"https://0conf.lnolymp.us\",\n            label: \"Olympus by Zeus\"\n        },\n        {\n            value: \"https://lsp.voltageapi.com\",\n            label: \"Flow 2.0 by Voltage\"\n        },\n        {\n            value: \"\",\n            label: \"None\"\n        }\n    ];\n\n    const SIGNET_LSP_OPTIONS = [\n        {\n            value: \"https://mutinynet-flow.lnolymp.us\",\n            label: \"Olympus by Zeus\"\n        },\n        {\n            value: \"https://signet-lsp.mutinywallet.com\",\n            label: \"Flow 2.0 by Voltage\"\n        },\n        {\n            value: \"\",\n            label: \"None\"\n        }\n    ];\n\n    const LSP_OPTIONS_WITH_VOLTAGE =\n        props.initialSettings.network === \"signet\"\n            ? SIGNET_LSP_OPTIONS\n            : MAINNET_LSP_OPTIONS;\n\n    // If the user already has a non-voltage LSP, remove Voltage from the list\n    function filterOutVoltage(lspOptions: { value: string; label: string }[]) {\n        if (\n            props.initialSettings.network === \"signet\" &&\n            props.initialSettings.lsp &&\n            props.initialSettings.lsp !== \"https://signet-lsp.mutinywallet.com\"\n        ) {\n            return lspOptions.filter(\n                (option) =>\n                    option.value !== \"https://signet-lsp.mutinywallet.com\"\n            );\n        }\n\n        if (\n            props.initialSettings.lsp &&\n            props.initialSettings.lsp !== \"https://lsp.voltageapi.com\"\n        ) {\n            return lspOptions.filter(\n                (option) => option.value !== \"https://lsp.voltageapi.com\"\n            );\n        }\n    }\n\n    const LSP_OPTIONS = filterOutVoltage(LSP_OPTIONS_WITH_VOLTAGE);\n\n    return (\n        <Card title={i18n.t(\"settings.servers.title\")}>\n            <Form onSubmit={handleSubmit} class=\"flex flex-col gap-4\">\n                <NiceP>{i18n.t(\"settings.servers.caption\")}</NiceP>\n                <ExternalLink href=\"https://github.com/MutinyWallet/mutiny-web/wiki/Self-hosting\">\n                    {i18n.t(\"settings.servers.link\")}\n                </ExternalLink>\n                <div />\n\n                <Field\n                    name=\"lsp\"\n                    validate={[url(i18n.t(\"settings.servers.error_lsp\"))]}\n                >\n                    {(field, props) => (\n                        <div class=\"flex flex-col gap-2\">\n                            <label\n                                class=\"text-sm font-semibold uppercase\"\n                                for=\"lsp\"\n                                id=\"lsp\"\n                            >\n                                {i18n.t(\"settings.servers.lsp_label\")}\n                            </label>\n                            <select\n                                {...props}\n                                value={field.value}\n                                class=\"w-full rounded-lg bg-m-grey-750 py-2 pl-4 pr-12 text-base font-normal text-white\"\n                            >\n                                <For each={LSP_OPTIONS}>\n                                    {({ value, label }) => (\n                                        <option\n                                            selected={field.value === value}\n                                            value={value}\n                                        >\n                                            {label}\n                                        </option>\n                                    )}\n                                </For>\n                            </select>\n                            <TinyText>\n                                {i18n.t(\"settings.servers.lsp_caption\")}\n                            </TinyText>\n                        </div>\n                    )}\n                </Field>\n                <Field\n                    name=\"proxy\"\n                    validate={[\n                        url(i18n.t(\"settings.servers.error_proxy\")),\n                        custom(\n                            validateNotTorUrl,\n                            i18n.t(\"settings.servers.error_tor\")\n                        )\n                    ]}\n                >\n                    {(field, props) => (\n                        <TextField\n                            {...props}\n                            value={field.value}\n                            error={field.error}\n                            label={i18n.t(\"settings.servers.proxy_label\")}\n                            caption={i18n.t(\"settings.servers.proxy_caption\")}\n                        />\n                    )}\n                </Field>\n                <Field\n                    name=\"esplora\"\n                    validate={[\n                        url(i18n.t(\"settings.servers.error_esplora\")),\n                        custom(\n                            validateNotTorUrl,\n                            i18n.t(\"settings.servers.error_tor\")\n                        )\n                    ]}\n                >\n                    {(field, props) => (\n                        <TextField\n                            {...props}\n                            value={field.value}\n                            error={field.error}\n                            label={i18n.t(\"settings.servers.esplora_label\")}\n                            caption={i18n.t(\"settings.servers.esplora_caption\")}\n                        />\n                    )}\n                </Field>\n                <Field\n                    name=\"rgs\"\n                    validate={[\n                        url(i18n.t(\"settings.servers.error_rgs\")),\n                        custom(\n                            validateNotTorUrl,\n                            i18n.t(\"settings.servers.error_tor\")\n                        )\n                    ]}\n                >\n                    {(field, props) => (\n                        <TextField\n                            {...props}\n                            value={field.value}\n                            error={field.error}\n                            label={i18n.t(\"settings.servers.rgs_label\")}\n                            caption={i18n.t(\"settings.servers.rgs_caption\")}\n                        />\n                    )}\n                </Field>\n                <Field\n                    name=\"storage\"\n                    validate={[\n                        url(i18n.t(\"settings.servers.error_lsp\")),\n                        custom(\n                            validateNotTorUrl,\n                            i18n.t(\"settings.servers.error_tor\")\n                        )\n                    ]}\n                >\n                    {(field, props) => (\n                        <TextField\n                            {...props}\n                            value={field.value}\n                            error={field.error}\n                            label={i18n.t(\"settings.servers.storage_label\")}\n                            caption={i18n.t(\"settings.servers.storage_caption\")}\n                        />\n                    )}\n                </Field>\n                <div />\n                <Button\n                    type=\"submit\"\n                    disabled={!settingsForm.dirty}\n                    intent=\"blue\"\n                >\n                    {i18n.t(\"settings.servers.save\")}\n                </Button>\n            </Form>\n        </Card>\n    );\n}\n\nfunction AsyncSettingsEditor() {\n    const [_state, _actions, sw] = useMegaStore();\n\n    const [settings] = createResource<MutinyWalletSettingStrings>(async () => {\n        const settings = await getSettings();\n\n        // set the lsp to what the node manager is using\n        const lsp = await sw.get_configured_lsp();\n        settings.lsp = lsp.url;\n        settings.lsps_connection_string = lsp.connection_string;\n        settings.lsps_token = lsp.token;\n\n        return settings;\n    });\n\n    return (\n        <Switch>\n            <Match when={settings.error}>\n                <SimpleErrorDisplay error={settings.error} />\n            </Match>\n            <Match when={settings.latest}>\n                <SettingsStringsEditor initialSettings={settings()!} />\n            </Match>\n        </Switch>\n    );\n}\n\nexport function Servers() {\n    const i18n = useI18n();\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <BackLink href=\"/settings\" title={i18n.t(\"settings.header\")} />\n                <LargeHeader>{i18n.t(\"settings.servers.title\")}</LargeHeader>\n                <Suspense fallback={<LoadingShimmer />}>\n                    <AsyncSettingsEditor />\n                </Suspense>\n                <NavBar activeTab=\"settings\" />\n            </DefaultMain>\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/settings/index.ts",
    "content": "export * from \"./Root\";\nexport * from \"./Admin\";\nexport * from \"./Backup\";\nexport * from \"./Channels\";\nexport * from \"./Connections\";\nexport * from \"./Currency\";\nexport * from \"./Language\";\nexport * from \"./EmergencyKit\";\nexport * from \"./Encrypt\";\nexport * from \"./Plus\";\nexport * from \"./Restore\";\nexport * from \"./Servers\";\nexport * from \"./ManageFederations\";\nexport * from \"./NostrKeys\";\nexport * from \"./ImportProfile\";\nexport * from \"./LightningAddress\";\n"
  },
  {
    "path": "src/routes/setup/AddFederation.tsx",
    "content": "import { useNavigate } from \"@solidjs/router\";\nimport { createSignal, Suspense } from \"solid-js\";\n\nimport {\n    Button,\n    ConfirmDialog,\n    DefaultMain,\n    ExternalLink,\n    MutinyWalletGuard\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nimport { AddFederationForm } from \"../settings\";\n\nexport function AddFederation() {\n    const i18n = useI18n();\n    const navigate = useNavigate();\n\n    const [confirmOpen, setConfirmOpen] = createSignal(false);\n\n    async function handleSkip() {\n        navigate(\"/\");\n    }\n\n    return (\n        <MutinyWalletGuard>\n            <DefaultMain>\n                <div class=\"mx-auto flex max-w-[20rem] flex-col items-center gap-4 py-4\">\n                    <h1 class=\"text-3xl font-semibold\">\n                        {i18n.t(\"setup.federation.pick\")}\n                    </h1>\n                    <p class=\"text-pretty text-center text-xl font-light text-neutral-200\">\n                        {i18n.t(\"settings.manage_federations.description\")}\n                    </p>\n                    <p class=\"text-pretty text-center text-xl font-light text-neutral-200\">\n                        {i18n.t(\"settings.manage_federations.descriptionpart2\")}\n                    </p>\n\n                    <Button onClick={() => setConfirmOpen(true)} intent=\"text\">\n                        {i18n.t(\"setup.skip\")}\n                    </Button>\n                    <ConfirmDialog\n                        open={confirmOpen()}\n                        loading={false}\n                        onConfirm={handleSkip}\n                        onCancel={() => setConfirmOpen(false)}\n                    >\n                        {i18n.t(\"setup.federation.skip_confirm\")}\n                    </ConfirmDialog>\n                    <Suspense>\n                        <AddFederationForm setup />\n                    </Suspense>\n                    <p class=\"text-pretty text-center text-xl font-light text-neutral-200\">\n                        <ExternalLink href=\"https://fedimint.org/docs/intro\">\n                            {i18n.t(\"settings.manage_federations.learn_more\")}\n                        </ExternalLink>\n                    </p>\n                </div>\n            </DefaultMain>\n        </MutinyWalletGuard>\n    );\n}\n"
  },
  {
    "path": "src/routes/setup/ImportProfile.tsx",
    "content": "import { BackLink, DefaultMain, ImportNsecForm } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\n\nexport function ImportProfile() {\n    const i18n = useI18n();\n    return (\n        <DefaultMain>\n            <BackLink title=\"Back\" showOnDesktop href=\"/newprofile\" />\n            <div class=\"mx-auto flex max-w-[20rem] flex-1 flex-col items-center gap-4\">\n                <div class=\"flex-1\" />\n                <h1 class=\"text-3xl font-semibold\">\n                    {i18n.t(\"setup.import.title\")}\n                </h1>\n                <p class=\"text-center text-xl font-light text-neutral-200\">\n                    {i18n.t(\"setup.import.description\")}\n                    <br />\n                </p>\n                <div class=\"flex-1\" />\n                <ImportNsecForm />\n                <div class=\"flex-1\" />\n            </div>\n        </DefaultMain>\n    );\n}\n"
  },
  {
    "path": "src/routes/setup/NewProfile.tsx",
    "content": "import { A, useNavigate } from \"@solidjs/router\";\nimport { createSignal } from \"solid-js\";\n\nimport {\n    Button,\n    DefaultMain,\n    EditableProfile,\n    EditProfileForm\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\nimport { DEFAULT_NOSTR_NAME } from \"~/utils\";\n\nexport function NewProfile() {\n    const [_state, _actions, sw] = useMegaStore();\n    const i18n = useI18n();\n\n    const [creating, setCreating] = createSignal(false);\n    const [skipping, setSkipping] = createSignal(false);\n\n    const navigate = useNavigate();\n\n    async function handleSkip() {\n        setSkipping(true);\n        // set up an empty profile so we at least have some kind0 event\n        const profile = await sw.setup_new_profile(\n            DEFAULT_NOSTR_NAME,\n            undefined,\n            undefined,\n            undefined\n        );\n        console.log(\"profile\", profile);\n        localStorage.setItem(\"profile_setup_stage\", \"skipped\");\n        // navigate(\"/addfederation\");\n        navigate(\"/\");\n        setSkipping(false);\n    }\n\n    async function createProfile(p: EditableProfile) {\n        setCreating(true);\n        try {\n            const profile = await sw.setup_new_profile(\n                p.nym ? p.nym : DEFAULT_NOSTR_NAME,\n                p.imageUrl ? p.imageUrl : undefined,\n                undefined,\n                undefined\n            );\n            console.log(\"profile\", profile);\n            localStorage.setItem(\"profile_setup_stage\", \"saved\");\n            // navigate(\"/addfederation\");\n            navigate(\"/\");\n        } catch (e) {\n            console.error(e);\n        }\n        setCreating(false);\n    }\n\n    return (\n        <DefaultMain>\n            <div class=\"mx-auto flex max-w-[20rem] flex-1 flex-col items-center gap-4\">\n                <div class=\"flex-1\" />\n                <h1 class=\"text-3xl font-semibold\">\n                    {i18n.t(\"setup.new_profile.title\")}\n                </h1>\n                <p class=\"text-center text-xl font-light text-neutral-200\">\n                    {i18n.t(\"setup.new_profile.description\")}\n                </p>\n                <div class=\"flex-1\" />\n                <EditProfileForm\n                    onSave={createProfile}\n                    saving={creating()}\n                    cta=\"Create\"\n                />\n                <Button onClick={handleSkip} intent=\"text\" loading={skipping()}>\n                    {i18n.t(\"setup.skip\")}\n                </Button>\n                <A\n                    class=\"text-base font-normal text-m-grey-400\"\n                    href=\"/importprofile\"\n                >\n                    {i18n.t(\"setup.import_profile\")}\n                </A>\n                <div class=\"flex-1\" />\n            </div>\n        </DefaultMain>\n    );\n}\n"
  },
  {
    "path": "src/routes/setup/Restore.tsx",
    "content": "import {\n    BackLink,\n    DefaultMain,\n    LargeHeader,\n    NiceP,\n    VStack\n} from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { TwelveWordsEntry } from \"~/routes/settings/Restore\";\n\nexport function SetupRestore() {\n    const i18n = useI18n();\n    return (\n        <DefaultMain>\n            <BackLink\n                showOnDesktop\n                title={i18n.t(\"modals.onboarding.setup\")}\n                href=\"/setup\"\n            />\n            <LargeHeader>{i18n.t(\"settings.restore.title\")}</LargeHeader>\n            <VStack>\n                <NiceP>\n                    <p>{i18n.t(\"settings.restore.restore_tip\")}</p>\n                    <p>{i18n.t(\"settings.restore.multi_browser_warning\")}</p>\n                </NiceP>\n                <TwelveWordsEntry />\n            </VStack>\n        </DefaultMain>\n    );\n}\n"
  },
  {
    "path": "src/routes/setup/Root.tsx",
    "content": "import { useNavigate } from \"@solidjs/router\";\nimport { createSignal } from \"solid-js\";\n\nimport logo from \"~/assets/mutiny-pixel-logo.png\";\nimport { Button, DefaultMain, NiceP } from \"~/components\";\nimport { useI18n } from \"~/i18n/context\";\nimport { useMegaStore } from \"~/state/megaStore\";\n\nexport function Setup() {\n    const [_state, actions] = useMegaStore();\n    const i18n = useI18n();\n\n    const [isCreatingNewWallet, setIsCreatingNewWallet] = createSignal(false);\n\n    const navigate = useNavigate();\n\n    async function handleNewWallet() {\n        try {\n            setIsCreatingNewWallet(true);\n            const profileSetupStage = localStorage.getItem(\n                \"profile_setup_stage\"\n            );\n\n            // Check for nip07 browser extension. If it exists, we can skip the profile setup\n            const hasNip07 = Object.prototype.hasOwnProperty.call(\n                window,\n                \"nostr\"\n            );\n\n            await actions.setup(undefined);\n\n            if (!profileSetupStage && !hasNip07) {\n                navigate(\"/newprofile\");\n            } else {\n                navigate(\"/\");\n            }\n        } catch (e) {\n            console.error(e);\n            throw e;\n        }\n    }\n\n    return (\n        <DefaultMain>\n            <div class=\"flex flex-1 flex-col items-center justify-between gap-4\">\n                <div class=\"flex-1\" />\n                <div class=\"flex flex-col items-center gap-4\">\n                    <img\n                        id=\"mutiny-logo\"\n                        src={logo}\n                        class=\"h-[50px] w-[172px]\"\n                        alt=\"Mutiny Plus logo\"\n                    />\n                    <NiceP>{i18n.t(\"setup.initial.welcome\")}</NiceP>\n                    <div class=\"h-4\" />\n                    <Button\n                        layout=\"full\"\n                        onClick={handleNewWallet}\n                        loading={isCreatingNewWallet()}\n                    >\n                        {i18n.t(\"setup.initial.new_wallet\")}\n                    </Button>\n                    <Button\n                        intent=\"text\"\n                        layout=\"full\"\n                        disabled={isCreatingNewWallet()}\n                        onClick={() => navigate(\"/setup/restore\")}\n                    >\n                        {i18n.t(\"setup.initial.import_existing\")}\n                    </Button>\n                </div>\n                <div class=\"flex-1\" />\n            </div>\n        </DefaultMain>\n    );\n}\n"
  },
  {
    "path": "src/routes/setup/index.ts",
    "content": "export * from \"./Restore\";\nexport * from \"./ImportProfile\";\nexport * from \"./NewProfile\";\nexport * from \"./Root\";\nexport * from \"./AddFederation\";\n"
  },
  {
    "path": "src/state/megaStore.tsx",
    "content": "// Inspired by https://github.com/solidjs/solid-realworld/blob/main/src/store/index.js\nimport { MutinyBalance, TagItem } from \"@mutinywallet/mutiny-wasm\";\nimport { useNavigate, useSearchParams } from \"@solidjs/router\";\nimport { SecureStoragePlugin } from \"capacitor-secure-storage-plugin\";\nimport { Remote } from \"comlink\";\nimport {\n    createContext,\n    onCleanup,\n    onMount,\n    ParentComponent,\n    useContext\n} from \"solid-js\";\nimport { createStore } from \"solid-js/store\";\n\nimport { checkBrowserCompatibility } from \"~/logic/browserCompatibility\";\nimport {\n    doubleInitDefense,\n    getSettings,\n    MutinyWalletSettingStrings,\n    Network,\n    setSettings\n} from \"~/logic/mutinyWalletSetup\";\nimport { ParsedParams, toParsedParams } from \"~/logic/waila\";\nimport { MutinyFederationIdentity } from \"~/routes/settings\";\nimport {\n    BTC_OPTION,\n    Currency,\n    eify,\n    subscriptionValid,\n    USD_OPTION\n} from \"~/utils\";\n\ntype LoadStage =\n    | \"fresh\"\n    | \"checking_double_init\"\n    | \"downloading\"\n    | \"checking_for_existing_wallet\"\n    | \"setup\"\n    | \"done\";\n\nexport type WalletWorker = Remote<typeof import(\"../workers/walletWorker\")>;\n\nexport const makeMegaStoreContext = () => {\n    const [searchParams] = useSearchParams();\n    const navigate = useNavigate();\n\n    // Not actually a shared worker, but it's the same code\n    const sw = new ComlinkWorker<typeof import(\"../workers/walletWorker\")>(\n        new URL(\"../workers/walletWorker\", import.meta.url),\n        {\n            type: \"module\"\n        }\n    );\n\n    const [state, setState] = createStore({\n        network: undefined as Network | undefined,\n        deleting: false,\n        scan_result: undefined as ParsedParams | undefined,\n        price: 0,\n        fiat: localStorage.getItem(\"fiat_currency\")\n            ? (JSON.parse(localStorage.getItem(\"fiat_currency\")!) as Currency)\n            : USD_OPTION,\n        has_backed_up: localStorage.getItem(\"has_backed_up\") === \"true\",\n        balance: undefined as Partial<MutinyBalance> | undefined,\n        last_sync: undefined as number | undefined,\n        price_sync_backoff_multiple: 1,\n        is_syncing: false,\n        wallet_loading: true,\n        setup_error: undefined as Error | undefined,\n        is_pwa: window.matchMedia(\"(display-mode: standalone)\").matches,\n        existing_tab_detected: false,\n        subscription_timestamp: undefined as number | undefined,\n        get mutiny_plus(): boolean {\n            // Make sure the subscription hasn't expired\n            return subscriptionValid(state.subscription_timestamp);\n        },\n        needs_password: false,\n        // If setup fails we can remember the password for checking the device lock\n        password: undefined as string | undefined,\n        load_stage: \"fresh\" as LoadStage,\n        settings: undefined as MutinyWalletSettingStrings | undefined,\n        safe_mode: searchParams.safe_mode === \"true\",\n        lang: localStorage.getItem(\"i18nexLng\") || undefined,\n        preferredInvoiceType: \"unified\" as \"unified\" | \"lightning\" | \"onchain\",\n        should_zap_hodl: localStorage.getItem(\"should_zap_hodl\") === \"true\",\n        testflightPromptDismissed:\n            localStorage.getItem(\"testflightPromptDismissed\") === \"true\",\n        federations: undefined as MutinyFederationIdentity[] | undefined,\n        balanceView: localStorage.getItem(\"balanceView\") || \"sats\",\n        expiration_warning: undefined as\n            | {\n                  expiresTimestamp: number;\n                  expiresMessage: string;\n                  federationName: string;\n              }\n            | undefined,\n        expiration_warning_seen: false,\n        shutdown_warning_seen: false\n    });\n\n    const actions = {\n        async checkForSubscription(justPaid?: boolean): Promise<void> {\n            try {\n                const timestamp = await sw.check_subscribed();\n\n                // Check that timestamp is a number\n                if (timestamp && !isNaN(Number(timestamp))) {\n                    setState({ subscription_timestamp: Number(timestamp) });\n                } else if (justPaid) {\n                    // we make a fake timestamp for 24 hours from now, in case the server is down\n                    const timestamp = Math.ceil(Date.now() / 1000) + 86400;\n                    setState({ subscription_timestamp: timestamp });\n                }\n            } catch (e) {\n                console.error(e);\n            }\n        },\n        async preSetup(): Promise<boolean> {\n            try {\n                // If we're already in an error state there should be no reason to continue\n                if (state.setup_error) {\n                    throw state.setup_error;\n                }\n\n                setState({ wallet_loading: true });\n\n                await this.checkForExistingTab();\n\n                if (state.existing_tab_detected) {\n                    return false;\n                }\n\n                console.log(\"checking for browser compatibility\");\n                try {\n                    await checkBrowserCompatibility();\n                } catch (e) {\n                    setState({ setup_error: eify(e) });\n                    return false;\n                }\n\n                setState({\n                    wallet_loading: true,\n                    load_stage: \"checking_double_init\"\n                });\n\n                await doubleInitDefense();\n\n                setState({ load_stage: \"downloading\" });\n                await sw.initializeWasm();\n\n                setState({ load_stage: \"checking_for_existing_wallet\" });\n                const existing = await sw.has_node_manager();\n\n                if (!existing && !searchParams.skip_setup) {\n                    navigate(\"/setup\");\n                    return false;\n                }\n                return true;\n            } catch (e) {\n                console.error(e);\n                setState({ setup_error: eify(e) });\n                return false;\n            }\n        },\n        async setup(password?: string): Promise<void> {\n            let interval: NodeJS.Timeout | undefined;\n            try {\n                const settings = await getSettings();\n                setState({ load_stage: \"setup\" });\n\n                // handle lsp settings\n                if (searchParams.lsps) {\n                    settings.lsp = \"\";\n                    settings.lsps_connection_string = searchParams.lsps;\n                    settings.lsps_token = searchParams.token;\n\n                    await setSettings(settings);\n                }\n\n                // 90 seconds to load or we bail\n                const start = Date.now();\n                const MAX_LOAD_TIME = 90000;\n                interval = setInterval(() => {\n                    console.log(\"Running setup\", Date.now() - start);\n                    if (Date.now() - start > MAX_LOAD_TIME) {\n                        clearInterval(interval);\n                        // Only want to do this if we're really not done loading\n                        if (state.load_stage !== \"done\") {\n                            // Have to set state error here because throwing doesn't work if WASM panics\n                            setState({\n                                setup_error: new Error(\n                                    \"Load timed out, please try again\"\n                                )\n                            });\n                            return;\n                        }\n                    }\n                }, 1000);\n\n                let nsec;\n                // get nsec from secure storage\n                try {\n                    const value = await SecureStoragePlugin.get({\n                        key: \"nsec\"\n                    });\n                    nsec = value.value;\n                } catch (e) {\n                    console.log(\"No nsec stored\");\n                }\n\n                // https://developer.mozilla.org/en-US/docs/Web/API/Storage_API\n                // Ask the browser to not clear storage\n                if (navigator.storage && navigator.storage.persist) {\n                    navigator.storage.persist().then((persistent) => {\n                        if (persistent) {\n                            console.log(\n                                \"Storage will not be cleared except by explicit user action\"\n                            );\n                        } else {\n                            console.log(\n                                \"Storage may be cleared by the UA under storage pressure.\"\n                            );\n                        }\n                    });\n                } else {\n                    console.warn(\n                        \"Persistent storage not supported, storage may be cleared by the UA under storage pressure.\"\n                    );\n                }\n\n                const success = await sw.setupMutinyWallet(\n                    settings,\n                    password,\n                    state.safe_mode,\n                    state.should_zap_hodl,\n                    nsec\n                );\n\n                if (!success) {\n                    throw new Error(\"Failed to initialize mutiny wallet\");\n                }\n\n                // Give other components access to settings via the store\n                setState({ settings: settings });\n\n                // If we get this far then we don't need the password anymore\n                setState({ needs_password: false });\n\n                // Get network\n                const network = await sw.get_network();\n\n                // Get balance\n                const balance = await sw.get_balance();\n\n                // Get federations\n                const federations = await sw.list_federations();\n\n                let expiration_warning:\n                    | {\n                          expiresTimestamp: number;\n                          expiresMessage: string;\n                          federationName: string;\n                      }\n                    | undefined = undefined;\n\n                federations.forEach((f) => {\n                    if (f.popup_countdown_message && f.popup_end_timestamp) {\n                        expiration_warning = {\n                            expiresTimestamp: f.popup_end_timestamp,\n                            expiresMessage: f.popup_countdown_message,\n                            federationName: f.federation_name\n                        };\n                    }\n                });\n\n                setState({\n                    wallet_loading: false,\n                    load_stage: \"done\",\n                    balance,\n                    federations,\n                    network: network as Network,\n                    expiration_warning\n                });\n\n                console.log(\"Wallet initialized\");\n\n                clearInterval(interval);\n\n                await actions.postSetup();\n            } catch (e) {\n                // clear the interval if it exists\n                if (interval) {\n                    clearInterval(interval);\n                }\n\n                console.error(e);\n                if (eify(e).message === \"Incorrect password entered.\") {\n                    setState({ needs_password: true });\n                } else {\n                    // We only save the password for checking the timelock, will be blown away by the reload\n                    setState({ setup_error: eify(e), password: password });\n                }\n            }\n        },\n        async postSetup(): Promise<void> {\n            if (!sw) {\n                console.error(\n                    \"Unable to run post setup, no mutiny_wallet is set\"\n                );\n                return;\n            }\n\n            // Check if we're subscribed and update the timestamp\n            try {\n                const timestamp = await sw.check_subscribed();\n                // Check that timestamp is a number\n                if (timestamp && !isNaN(Number(timestamp))) {\n                    setState({ subscription_timestamp: Number(timestamp) });\n                }\n            } catch (e) {\n                console.error(\"error checking subscription\", e);\n            }\n\n            // Set up syncing\n            setInterval(async () => {\n                await actions.sync();\n            }, 3 * 1000); // Poll every 3 seconds\n\n            // Run our first price check\n            console.log(\"running first price check\");\n            await actions.priceCheck();\n\n            // Set up price checking every minute\n            setInterval(\n                async () => {\n                    await actions.priceCheck();\n                },\n                60 * 1000 * state.price_sync_backoff_multiple\n            ); // Poll every minute * backoff multiple\n        },\n        async deleteMutinyWallet(): Promise<void> {\n            try {\n                setState((prevState) => ({\n                    ...prevState,\n                    deleting: true\n                }));\n                if (sw) {\n                    await sw.stop();\n                    await sw.delete_all();\n                }\n            } catch (e) {\n                console.error(e);\n            }\n        },\n        async priceCheck(): Promise<void> {\n            try {\n                const price = await actions.fetchPrice(state.fiat);\n                setState({\n                    price: price || 0,\n                    fiat: state.fiat,\n                    price_sync_backoff_multiple: 1\n                });\n            } catch (e) {\n                setState({\n                    price: 1,\n                    fiat: BTC_OPTION,\n                    price_sync_backoff_multiple:\n                        state.price_sync_backoff_multiple * 2\n                });\n            }\n        },\n        async sync(): Promise<void> {\n            try {\n                if (sw && !state.is_syncing) {\n                    setState({ is_syncing: true });\n                    const newBalance = await sw.get_balance();\n                    try {\n                        setState({\n                            balance: newBalance,\n                            last_sync: Date.now(),\n                            fiat: state.fiat\n                        });\n                    } catch (e) {\n                        setState({\n                            balance: newBalance,\n                            last_sync: Date.now(),\n                            fiat: BTC_OPTION\n                        });\n                    }\n                }\n            } catch (e) {\n                console.error(e);\n            } finally {\n                setState({ is_syncing: false });\n            }\n        },\n        async fetchPrice(fiat: Currency): Promise<number | undefined> {\n            let price;\n            if (fiat.value === \"BTC\") {\n                price = 1;\n                return price;\n            } else {\n                try {\n                    price = await sw.get_bitcoin_price(\n                        fiat.value.toLowerCase() || \"usd\"\n                    );\n                    return price;\n                } catch (e) {\n                    console.error(e);\n                    throw e;\n                }\n            }\n        },\n        setScanResult(scan_result: ParsedParams | undefined) {\n            setState({ scan_result });\n        },\n        setHasBackedUp() {\n            localStorage.setItem(\"has_backed_up\", \"true\");\n            setState({ has_backed_up: true });\n        },\n        async listTags(): Promise<TagItem[] | undefined> {\n            try {\n                return sw.get_tag_items();\n            } catch (e) {\n                console.error(e);\n                return [];\n            }\n        },\n        async saveFiat(fiat: Currency) {\n            localStorage.setItem(\"fiat_currency\", JSON.stringify(fiat));\n            const price = await actions.fetchPrice(fiat);\n            setState({\n                price: price,\n                fiat: fiat\n            });\n        },\n        saveLanguage(lang: string) {\n            localStorage.setItem(\"i18nextLng\", lang);\n            setState({ lang });\n        },\n        setPreferredInvoiceType(type: \"unified\" | \"lightning\" | \"onchain\") {\n            setState({ preferredInvoiceType: type });\n        },\n        async handleIncomingString(\n            str: string,\n            onError: (e: Error) => void,\n            onSuccess: (value: ParsedParams) => void\n        ): Promise<void> {\n            try {\n                const url = new URL(str);\n                if (url && url.pathname.startsWith(\"/settings/plus\")) {\n                    navigate(url.pathname + url.search);\n                    return;\n                }\n            } catch (e) {\n                // If it's not a URL, we'll just continue with normal parsing\n            }\n\n            const network = state.network || \"signet\";\n            const result = await toParsedParams(str || \"\", network, sw);\n\n            if (!result || !result.ok) {\n                if (onError) {\n                    onError(result.error);\n                }\n                return;\n            } else {\n                if (\n                    result.value?.address ||\n                    result.value?.payjoin_enabled ||\n                    result.value?.invoice ||\n                    result.value?.node_pubkey ||\n                    (result.value?.lnurl && !result.value.is_lnurl_auth)\n                ) {\n                    if (onSuccess) {\n                        onSuccess(result.value);\n                    }\n                }\n                if (result.value?.lnurl && result.value?.is_lnurl_auth) {\n                    navigate(\n                        \"/?lnurlauth=\" + encodeURIComponent(result.value?.lnurl)\n                    );\n                    actions.setScanResult(undefined);\n                }\n                if (result.value?.fedimint_invite) {\n                    navigate(\n                        \"/settings/federations?fedimint_invite=\" +\n                            encodeURIComponent(result.value?.fedimint_invite)\n                    );\n                    actions.setScanResult(undefined);\n                }\n                if (result.value?.nostr_wallet_auth) {\n                    console.log(\n                        \"nostr_wallet_auth\",\n                        result.value?.nostr_wallet_auth\n                    );\n                    navigate(\n                        \"/settings/connections/?nwa=\" +\n                            encodeURIComponent(result.value?.nostr_wallet_auth)\n                    );\n                }\n            }\n        },\n        setTestFlightPromptDismissed() {\n            localStorage.setItem(\"testflightPromptDismissed\", \"true\");\n            setState({ testflightPromptDismissed: true });\n        },\n        toggleHodl() {\n            const should_zap_hodl = !state.should_zap_hodl;\n            localStorage.setItem(\"should_zap_hodl\", should_zap_hodl.toString());\n            setState({ should_zap_hodl });\n        },\n        async refreshFederations() {\n            const federations = await sw.list_federations();\n\n            let expiration_warning:\n                | {\n                      expiresTimestamp: number;\n                      expiresMessage: string;\n                      federationName: string;\n                  }\n                | undefined = undefined;\n\n            federations.forEach((f) => {\n                if (f.popup_countdown_message && f.popup_end_timestamp) {\n                    expiration_warning = {\n                        expiresTimestamp: f.popup_end_timestamp,\n                        expiresMessage: f.popup_countdown_message,\n                        federationName: f.federation_name\n                    };\n                }\n            });\n\n            setState({ federations, expiration_warning });\n        },\n        cycleBalanceView() {\n            if (state.balanceView === \"sats\") {\n                localStorage.setItem(\"balanceView\", \"fiat\");\n                setState({ balanceView: \"fiat\" });\n            } else if (state.balanceView === \"fiat\") {\n                localStorage.setItem(\"balanceView\", \"hidden\");\n                setState({ balanceView: \"hidden\" });\n            } else {\n                localStorage.setItem(\"balanceView\", \"sats\");\n                setState({ balanceView: \"sats\" });\n            }\n        },\n        async checkForExistingTab() {\n            // Set up existing tab detector\n            const channel = new BroadcastChannel(\"tab-detector\");\n\n            // First we let everyone know we exist\n            channel.postMessage({ type: \"NEW_TAB\" });\n\n            channel.onmessage = (e) => {\n                // If any tabs reply, we know there's an existing tab so abort setup\n                if (e.data.type === \"EXISTING_TAB\") {\n                    console.debug(\"there's an existing tab\");\n                    setState({\n                        existing_tab_detected: true,\n                        setup_error: new Error(\n                            \"Existing tab detected, aborting setup\"\n                        )\n                    });\n                    return;\n                }\n\n                // If we get notified of a new tab, we let it know we exist\n                if (e.data.type === \"NEW_TAB\") {\n                    console.debug(\"a new tab just came online\");\n                    channel.postMessage({ type: \"EXISTING_TAB\" });\n                }\n            };\n        },\n        // Only show the expiration warning once per session\n        clearExpirationWarning() {\n            setState({ expiration_warning_seen: true });\n        },\n        // Only show the shutdown warning once per session\n        clearShutdownWarning() {\n            setState({ shutdown_warning_seen: true });\n        }\n    };\n\n    return [state, actions, sw] as const;\n};\n\ntype MegaStoreContextType = ReturnType<typeof makeMegaStoreContext>;\n\nexport const MegaStoreContext = createContext<MegaStoreContextType>();\nexport const useMegaStore = () => useContext(MegaStoreContext)!;\n\nexport const Provider: ParentComponent = (props) => {\n    const [state, actions, sw] = makeMegaStoreContext();\n\n    onMount(async () => {\n        const shouldSetup = await actions.preSetup();\n        console.log(\"Should run setup?\", shouldSetup);\n        if (\n            shouldSetup &&\n            sw &&\n            !state.existing_tab_detected &&\n            !state.deleting &&\n            !state.setup_error\n        ) {\n            await actions.setup();\n        }\n    });\n\n    onCleanup(async () => {\n        console.warn(\"Parent Component is being unmounted!!!\");\n        await sw.stop();\n        console.warn(\"Successfully stopped mutiny wallet\");\n        sessionStorage.removeItem(\"MUTINY_WALLET_INITIALIZED\");\n    });\n\n    return (\n        <MegaStoreContext.Provider value={[state, actions, sw]}>\n            {props.children}\n        </MegaStoreContext.Provider>\n    );\n};\n"
  },
  {
    "path": "src/styles/dialogs.ts",
    "content": "export const DIALOG_POSITIONER =\n    \"fixed inset-0 h-[100dvh] z-50 bg-m-grey-900 safe-top safe-bottom\";\nexport const DIALOG_CONTENT =\n    \"h-full flex flex-col justify-between px-4 pt-4 pb-8 bg-m-grey-900 touch-manipulation select-none overflow-y-scroll disable-scrollbars\";\n"
  },
  {
    "path": "src/utils/baseUrl.ts",
    "content": "import { Capacitor } from \"@capacitor/core\";\n\n// On mobile the origin URL is localhost, so we hardcode the base URL\nexport function baseUrlAccountingForNative(network?: string) {\n    if (Capacitor.isNativePlatform()) {\n        return network === \"bitcoin\"\n            ? \"https://app.mutinywallet.com\"\n            : \"https://signet-app.mutinywallet.com\";\n    } else {\n        return window.location.origin;\n    }\n}\n"
  },
  {
    "path": "src/utils/bech32.ts",
    "content": "// pasted from https://github.com/bitcoinjs/bech32/\nconst ALPHABET = \"qpzry9x8gf2tvdw0s3jn54khce6mua7l\";\n\nconst ALPHABET_MAP: { [key: string]: number } = {};\nfor (let z = 0; z < ALPHABET.length; z++) {\n    const x = ALPHABET.charAt(z);\n    ALPHABET_MAP[x] = z;\n}\n\nfunction polymodStep(pre: number): number {\n    const b = pre >> 25;\n    return (\n        ((pre & 0x1ffffff) << 5) ^\n        (-((b >> 0) & 1) & 0x3b6a57b2) ^\n        (-((b >> 1) & 1) & 0x26508e6d) ^\n        (-((b >> 2) & 1) & 0x1ea119fa) ^\n        (-((b >> 3) & 1) & 0x3d4233dd) ^\n        (-((b >> 4) & 1) & 0x2a1462b3)\n    );\n}\n\nfunction prefixChk(prefix: string): number | string {\n    let chk = 1;\n    for (let i = 0; i < prefix.length; ++i) {\n        const c = prefix.charCodeAt(i);\n        if (c < 33 || c > 126) return \"Invalid prefix (\" + prefix + \")\";\n\n        chk = polymodStep(chk) ^ (c >> 5);\n    }\n    chk = polymodStep(chk);\n\n    for (let i = 0; i < prefix.length; ++i) {\n        const v = prefix.charCodeAt(i);\n        chk = polymodStep(chk) ^ (v & 0x1f);\n    }\n    return chk;\n}\n\nfunction convert(\n    data: ArrayLike<number>,\n    inBits: number,\n    outBits: number,\n    pad: true\n): number[];\nfunction convert(\n    data: ArrayLike<number>,\n    inBits: number,\n    outBits: number,\n    pad: false\n): number[] | string;\nfunction convert(\n    data: ArrayLike<number>,\n    inBits: number,\n    outBits: number,\n    pad: boolean\n): number[] | string {\n    let value = 0;\n    let bits = 0;\n    const maxV = (1 << outBits) - 1;\n\n    const result: number[] = [];\n    for (let i = 0; i < data.length; ++i) {\n        value = (value << inBits) | data[i];\n        bits += inBits;\n\n        while (bits >= outBits) {\n            bits -= outBits;\n            result.push((value >> bits) & maxV);\n        }\n    }\n\n    if (pad) {\n        if (bits > 0) {\n            result.push((value << (outBits - bits)) & maxV);\n        }\n    } else {\n        if (bits >= inBits) return \"Excess padding\";\n        if ((value << (outBits - bits)) & maxV) return \"Non-zero padding\";\n    }\n\n    return result;\n}\n\nfunction toWords(bytes: ArrayLike<number>): number[] {\n    return convert(bytes, 8, 5, true);\n}\n\nfunction fromWordsUnsafe(words: ArrayLike<number>): number[] | undefined {\n    const res = convert(words, 5, 8, false);\n    if (Array.isArray(res)) return res;\n}\n\nfunction fromWords(words: ArrayLike<number>): number[] {\n    const res = convert(words, 5, 8, false);\n    if (Array.isArray(res)) return res;\n\n    throw new Error(res);\n}\n\nfunction getLibraryFromEncoding(encoding: \"bech32\" | \"bech32m\"): BechLib {\n    let ENCODING_CONST: number;\n    if (encoding === \"bech32\") {\n        ENCODING_CONST = 1;\n    } else {\n        ENCODING_CONST = 0x2bc830a3;\n    }\n\n    function encode(\n        prefix: string,\n        words: ArrayLike<number>,\n        LIMIT?: number\n    ): string {\n        LIMIT = LIMIT || 90;\n        if (prefix.length + 7 + words.length > LIMIT)\n            throw new TypeError(\"Exceeds length limit\");\n\n        prefix = prefix.toLowerCase();\n\n        // determine chk mod\n        let chk = prefixChk(prefix);\n        if (typeof chk === \"string\") throw new Error(chk);\n\n        let result = prefix + \"1\";\n        for (let i = 0; i < words.length; ++i) {\n            const x = words[i];\n            if (x >> 5 !== 0) throw new Error(\"Non 5-bit word\");\n\n            chk = polymodStep(chk) ^ x;\n            result += ALPHABET.charAt(x);\n        }\n\n        for (let i = 0; i < 6; ++i) {\n            chk = polymodStep(chk);\n        }\n        chk ^= ENCODING_CONST;\n\n        for (let i = 0; i < 6; ++i) {\n            const v = (chk >> ((5 - i) * 5)) & 0x1f;\n            result += ALPHABET.charAt(v);\n        }\n\n        return result;\n    }\n\n    function __decode(str: string, LIMIT?: number): Decoded | string {\n        LIMIT = LIMIT || 90;\n        if (str.length < 8) return str + \" too short\";\n        if (str.length > LIMIT) return \"Exceeds length limit\";\n\n        // don't allow mixed case\n        const lowered = str.toLowerCase();\n        const uppered = str.toUpperCase();\n        if (str !== lowered && str !== uppered)\n            return \"Mixed-case string \" + str;\n        str = lowered;\n\n        const split = str.lastIndexOf(\"1\");\n        if (split === -1) return \"No separator character for \" + str;\n        if (split === 0) return \"Missing prefix for \" + str;\n\n        const prefix = str.slice(0, split);\n        const wordChars = str.slice(split + 1);\n        if (wordChars.length < 6) return \"Data too short\";\n\n        let chk = prefixChk(prefix);\n        if (typeof chk === \"string\") return chk;\n\n        const words = [];\n        for (let i = 0; i < wordChars.length; ++i) {\n            const c = wordChars.charAt(i);\n            const v = ALPHABET_MAP[c];\n            if (v === undefined) return \"Unknown character \" + c;\n            chk = polymodStep(chk) ^ v;\n\n            // not in the checksum?\n            if (i + 6 >= wordChars.length) continue;\n            words.push(v);\n        }\n\n        if (chk !== ENCODING_CONST) return \"Invalid checksum for \" + str;\n        return { prefix, words };\n    }\n\n    function decodeUnsafe(str: string, LIMIT?: number): Decoded | undefined {\n        const res = __decode(str, LIMIT);\n        if (typeof res === \"object\") return res;\n    }\n\n    function decode(str: string, LIMIT?: number): Decoded {\n        const res = __decode(str, LIMIT);\n        if (typeof res === \"object\") return res;\n\n        throw new Error(res);\n    }\n\n    return {\n        decodeUnsafe,\n        decode,\n        encode,\n        toWords,\n        fromWordsUnsafe,\n        fromWords\n    };\n}\n\nexport function bech32WordsToUrl(words: number[]) {\n    // Step 1: Convert words to bytes\n    const bits = words\n        .map((word) => word.toString(2).padStart(5, \"0\"))\n        .join(\"\");\n    const bytes = [];\n    for (let i = 0; i < bits.length; i += 8) {\n        bytes.push(parseInt(bits.substring(i, i + 8), 2));\n    }\n\n    // Step 2: Convert bytes to characters\n    const urlChars = bytes.map((byte) => String.fromCharCode(byte));\n\n    // Step 3: Concatenate characters to form URL string\n    return urlChars.join(\"\");\n}\n\nexport const bech32 = getLibraryFromEncoding(\"bech32\");\ninterface Decoded {\n    prefix: string;\n    words: number[];\n}\ninterface BechLib {\n    decodeUnsafe: (\n        str: string,\n        LIMIT?: number | undefined\n    ) => Decoded | undefined;\n    decode: (str: string, LIMIT?: number | undefined) => Decoded;\n    encode: (\n        prefix: string,\n        words: ArrayLike<number>,\n        LIMIT?: number | undefined\n    ) => string;\n    toWords: typeof toWords;\n    fromWordsUnsafe: typeof fromWordsUnsafe;\n    fromWords: typeof fromWords;\n}\n"
  },
  {
    "path": "src/utils/blobToBase64.ts",
    "content": "export async function blobToBase64(file: File): Promise<string> {\n    const dataUrl = await new Promise<string>((resolve, reject) => {\n        const fileReader = new FileReader();\n        fileReader.onload = (e) => {\n            const result = e.target?.result?.toString();\n            if (result) {\n                resolve(result);\n            } else {\n                reject(new Error(\"Error reading file\"));\n            }\n        };\n        fileReader.onerror = (_e) => reject(new Error(\"Error reading file\"));\n        fileReader.readAsDataURL(file);\n    });\n\n    // remove the data url prefix\n    const base64 = dataUrl.split(\",\")[1];\n\n    return base64;\n}\n"
  },
  {
    "path": "src/utils/conversions.ts",
    "content": "import { WalletWorker } from \"~/state/megaStore\";\n\nimport { Currency } from \"./currencies\";\n\n/** satsToFiat\n *  returns a toLocaleString() based on the bitcoin price in the chosen currency\n *  @param {number} amount - Takes a number as a string to parse the formatted value\n *  @param {number} price - Finds the Price from the megaStore state\n *  @param {Currency} fiat - Takes {@link Currency} object options to determine how to format the amount input\n */\n\nexport async function satsToFiat(\n    amount: number | undefined,\n    price: number,\n    fiat: Currency,\n    sw: WalletWorker\n): Promise<string> {\n    if (typeof amount !== \"number\" || isNaN(amount)) {\n        return \"\";\n    }\n    try {\n        const btc = await sw.convert_sats_to_btc(BigInt(Math.floor(amount)));\n        const fiatPrice = btc * price;\n        const roundedFiat = Math.round(fiatPrice);\n        if (\n            (fiat.value !== \"BTC\" &&\n                roundedFiat * 100 === Math.round(fiatPrice * 100)) ||\n            fiatPrice === 0\n        ) {\n            return fiatPrice.toFixed(0);\n        } else {\n            return fiatPrice.toFixed(fiat.maxFractionalDigits);\n        }\n    } catch (e) {\n        console.error(e);\n        return \"\";\n    }\n}\n\n/** satsToFormattedFiat\n *  returns a toLocaleString() based on the bitcoin price in the chosen currency with its appropriate currency prefix\n *  @param {number} amount - Takes a number as a string to parse the formatted value\n *  @param {number} price - Finds the Price from the megaStore state\n *  @param {Currency} fiat - Takes {@link Currency} object options to determine how to format the amount input\n */\n\nexport async function satsToFormattedFiat(\n    amount: number | undefined,\n    price: number,\n    fiat: Currency,\n    sw: WalletWorker\n): Promise<string> {\n    if (typeof amount !== \"number\" || isNaN(amount)) {\n        return \"\";\n    }\n    try {\n        const btc = await sw.convert_sats_to_btc(BigInt(Math.floor(amount)));\n        const fiatPrice = btc * price;\n        //Handles currencies not supported by .toLocaleString() like BTC\n        //Returns a string with a currency symbol and a number with decimals equal to the maxFractionalDigits\n        if (fiat.hasSymbol) {\n            return (\n                fiat.hasSymbol +\n                fiatPrice.toLocaleString(navigator.languages[0] || \"en-US\", {\n                    minimumFractionDigits:\n                        fiatPrice === 0 ? 0 : fiat.maxFractionalDigits,\n                    maximumFractionDigits: fiat.maxFractionalDigits\n                })\n            );\n            //Handles currencies with no symbol only an ISO code\n        } else {\n            return fiatPrice.toLocaleString(navigator.languages[0], {\n                minimumFractionDigits:\n                    fiatPrice === 0 ? 0 : fiat.maxFractionalDigits\n            });\n        }\n    } catch (e) {\n        console.error(e);\n        return \"\";\n    }\n}\n\nexport async function fiatToSats(\n    amount: number | undefined,\n    price: number,\n    formatted: boolean,\n    sw: WalletWorker\n): Promise<string> {\n    if (typeof amount !== \"number\" || isNaN(amount)) {\n        return \"\";\n    }\n    try {\n        const btc = price / amount;\n        const sats = await sw.convert_btc_to_sats(btc);\n        if (formatted) {\n            return parseInt(sats.toString()).toLocaleString();\n        } else {\n            return sats.toString();\n        }\n    } catch (e) {\n        console.error(e);\n        return \"\";\n    }\n}\n"
  },
  {
    "path": "src/utils/currencies.ts",
    "content": "export interface Currency {\n    value: string;\n    label: string;\n    hasSymbol?: string;\n    maxFractionalDigits: number;\n}\n\n// When populating with new currencies it is recommended to get data from: https://www.xe.com/currency/usd-us-dollar/\n\n/**\n *  BTC_USD_OPTIONS is an array of BTC and USD currencies\n *  These are separated from the rest of the list for ease of access by the user and necessary access in the megaStore\n */\n\nexport const BTC_OPTION: Currency = {\n    label: \"Bitcoin BTC\",\n    value: \"BTC\",\n    hasSymbol: \"₿\",\n    maxFractionalDigits: 8\n};\n\nexport const USD_OPTION: Currency = {\n    label: \"United States Dollar USD\",\n    value: \"USD\",\n    hasSymbol: \"$\",\n    maxFractionalDigits: 2\n};\n\n/**\n *  FIAT_OPTIONS is an array of all available currencies on the coingecko api https://api.coingecko.com/api/v3/simple/supported_vs_currencies\n *  @Currency\n *  @param {string} label - should be in the format {Name} {ISO code}\n *  @param {string} values - are uppercase ISO 4217 currency code\n *  @param {string?} hasSymbol - if the currency has a symbol it should be represented as a string\n *  @param {number} maxFractionalDigits - the standard fractional units used by the currency should be set with maxFractionalDigits\n *\n *  Bitcoin is represented as:\n *  {\n *      label: \"bitcoin BTC\",\n *      value: \"BTC\",\n *      hasSymbol: \"₿\",\n *      maxFractionalDigits: 8\n *  }\n */\n\nexport const FIAT_OPTIONS: Currency[] = [\n    {\n        label: \"United Arab Emirates Dirham AED\",\n        value: \"AED\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Albanian Lek ALL\",\n        value: \"ALL\",\n        hasSymbol: \"Lek\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Dutch Guilder ANG\",\n        value: \"ANG\",\n        hasSymbol: \"ƒ\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Angolan Kwanza AOA\",\n        value: \"AOA\",\n        hasSymbol: \"Kz\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Argentine Peso ARS\",\n        value: \"ARS\",\n        hasSymbol: \"$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Australian Dollar AUD\",\n        value: \"AUD\",\n        hasSymbol: \"$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Azerbaijan Manat AZN\",\n        value: \"AZN\",\n        hasSymbol: \"₼\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Bangladeshi Taka BDT\",\n        value: \"BDT\",\n        hasSymbol: \"৳\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Bulgarian Lev BGN\",\n        value: \"BGN\",\n        hasSymbol: \"лв\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Bahraini Dinar BHD\",\n        value: \"BHD\",\n        hasSymbol: \"BD\",\n        maxFractionalDigits: 3\n    },\n    {\n        label: \"Burundian Franc BIF\",\n        value: \"BIF\",\n        hasSymbol: \"Franc\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Bermuda Dollar BMD\",\n        value: \"BMD\",\n        hasSymbol: \"$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Bolivian Bolíviano BOB\",\n        value: \"BOB\",\n        hasSymbol: \"$b\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Brazilian Real BRL\",\n        value: \"BRL\",\n        hasSymbol: \"R$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Botswana Pula BWP\",\n        value: \"BWP\",\n        hasSymbol: \"P\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Belarusian Ruble BYN\",\n        value: \"BYN\",\n        hasSymbol: \"p.\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Belizean Dollar BZD\",\n        value: \"BZD\",\n        hasSymbol: \"BZ$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Canadian Dollar CAD\",\n        value: \"CAD\",\n        hasSymbol: \"$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Congolese Franc CDF\",\n        value: \"CDF\",\n        hasSymbol: \"FC\",\n        maxFractionalDigits: 0\n    },\n    { label: \"Swiss Franc CHF\", value: \"CHF\", maxFractionalDigits: 2 },\n    {\n        label: \"Chilean Peso CLP\",\n        value: \"CLP\",\n        hasSymbol: \"$\",\n        maxFractionalDigits: 0\n    },\n    {\n        label: \"Chinese Yuan CNY\",\n        value: \"CNY\",\n        hasSymbol: \"¥\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Colombian Peso COP\",\n        value: \"COP\",\n        hasSymbol: \"$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Costa Rican Colon CRC\",\n        value: \"CRC\",\n        hasSymbol: \"₡\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Cuban Peso CUP\",\n        value: \"CUP\",\n        hasSymbol: \"₱\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Czech Koruna CZK\",\n        value: \"CZK\",\n        hasSymbol: \"Kč\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Djiboutian Franc DJF\",\n        value: \"DJF\",\n        hasSymbol: \"Franc\",\n        maxFractionalDigits: 0\n    },\n    {\n        label: \"Danish Krone DKK\",\n        value: \"DKK\",\n        hasSymbol: \"kr\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Dominican Peso DOP\",\n        value: \"DOP\",\n        hasSymbol: \"RD$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Algerian Dinar DZD\",\n        value: \"DZD\",\n        hasSymbol: \"DA\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Egyptian Pound EGP\",\n        value: \"EGP\",\n        hasSymbol: \"£\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Ethiopian Birr ETP\",\n        value: \"ETP\",\n        hasSymbol: \"Br\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Euro EUR\",\n        value: \"EUR\",\n        hasSymbol: \"€\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"British Pound GBP\",\n        value: \"GBP\",\n        hasSymbol: \"₤\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Georgian Lari GEL\",\n        value: \"GEL\",\n        hasSymbol: \"Lari\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Ghanaian Cedi GHS\",\n        value: \"GHS\",\n        hasSymbol: \"₵\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Guinean Franc GNF\",\n        value: \"GNF\",\n        hasSymbol: \"Franc\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Guatemalan Quetzal GTQ\",\n        value: \"GTQ\",\n        hasSymbol: \"Q\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Hong Kong Dollar HKD\",\n        value: \"HKD\",\n        hasSymbol: \"$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Honduran Lempira HNL\",\n        value: \"HNL\",\n        hasSymbol: \"L\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Hungarian Forint HUF\",\n        value: \"HUF\",\n        hasSymbol: \"Ft\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Indonesian Rupiah IDR\",\n        value: \"IDR\",\n        hasSymbol: \"Rp\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Israeli New Shekel ILS\",\n        value: \"ILS\",\n        hasSymbol: \"₪\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Indian Rupee INR\",\n        value: \"INR\",\n        hasSymbol: \"₹\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Iranian Rial IRR\",\n        value: \"IRR\",\n        hasSymbol: \"﷼\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Iranian Toman IRT\",\n        value: \"IRT\",\n        hasSymbol: \"﷼\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Icelandic Krona ISK\",\n        value: \"ISK\",\n        hasSymbol: \"kr\",\n        maxFractionalDigits: 0\n    },\n    {\n        label: \"Jamaican Dollar JMD\",\n        value: \"JMD\",\n        hasSymbol: \"J$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Jordanian Dinar JOD\",\n        value: \"JOD\",\n        hasSymbol: \"Dinar\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Japanese Yen JPY\",\n        value: \"JPY\",\n        hasSymbol: \"¥\",\n        maxFractionalDigits: 0\n    },\n    {\n        label: \"Kenyan Shilling KES\",\n        value: \"KES\",\n        hasSymbol: \"KSh\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Kyrgyzstani Som KGS\",\n        value: \"KGS\",\n        hasSymbol: \"лв\",\n        maxFractionalDigits: 0\n    },\n    {\n        label: \"Korean Won KRW\",\n        value: \"KRW\",\n        hasSymbol: \"₩\",\n        maxFractionalDigits: 0\n    },\n    {\n        label: \"Kazakhstani Tenge KZT\",\n        value: \"KZT\",\n        hasSymbol: \"₸\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Lebanese Pound LBP\",\n        value: \"LBP\",\n        hasSymbol: \"ل.ل\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Sri Lankan Rupee LKR\",\n        value: \"LKR\",\n        hasSymbol: \"Rs\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Moroccan Dirham MAD\",\n        value: \"MAD\",\n        hasSymbol: \"Dirham\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Malagasy Ariary MGA\",\n        value: \"MGA\",\n        hasSymbol: \"Ar\",\n        maxFractionalDigits: 1\n    },\n    {\n        label: \"Cuban Peso MLC\",\n        value: \"MLC\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Mauritanian Ouguiya MRU\",\n        value: \"MRU\",\n        hasSymbol: \"UM\",\n        maxFractionalDigits: 1\n    },\n    {\n        label: \"Mexican Peso MXN\",\n        value: \"MXN\",\n        hasSymbol: \"$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Malaysian Ringgit MYR\",\n        value: \"MYR\",\n        hasSymbol: \"RM\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Namibian Dollar NAD\",\n        value: \"NAD\",\n        hasSymbol: \"$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Nigerian Naira NGN\",\n        value: \"NGN\",\n        hasSymbol: \"₦\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Nicaraguan Cordoba NIO\",\n        value: \"NIO\",\n        hasSymbol: \"C$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Norwegian Krone NOK\",\n        value: \"NOK\",\n        hasSymbol: \"kr\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Nepalese Rupee NPR\",\n        value: \"NPR\",\n        hasSymbol: \"₨\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"New Zealand Dollar NZD\",\n        value: \"NZD\",\n        hasSymbol: \"$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Panamanian Balboa PAB\",\n        value: \"PAB\",\n        hasSymbol: \"B/.\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Peruvian Sol PEN\",\n        value: \"PEN\",\n        hasSymbol: \"S/.\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Philippine Peso PHP\",\n        value: \"PHP\",\n        hasSymbol: \"₱\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Pakistani Rupee PKR\",\n        value: \"PKR\",\n        hasSymbol: \"₨\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Polish Złoty PLN\",\n        value: \"PLN\",\n        hasSymbol: \"zł\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Paraguayan Guarani PYG\",\n        value: \"PYG\",\n        hasSymbol: \"Gs\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Qatari Riyal QAR\",\n        value: \"QAR\",\n        hasSymbol: \"﷼\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Romanian Leu RON\",\n        value: \"RON\",\n        hasSymbol: \"lei\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Serbian Dinar RSD\",\n        value: \"RSD\",\n        hasSymbol: \"РСД\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Russian Ruble RUB\",\n        value: \"RUB\",\n        hasSymbol: \"₽\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Rwandan Franc RWF\",\n        value: \"RWF\",\n        hasSymbol: \"Franc\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Saudi Riyal SAR\",\n        value: \"SAR\",\n        hasSymbol: \"SR\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Swedish Krona SEK\",\n        value: \"SEK\",\n        hasSymbol: \"kr\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Singapore Dollar SGD\",\n        value: \"SGD\",\n        hasSymbol: \"$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Thai Baht THB\",\n        value: \"THB\",\n        hasSymbol: \"฿\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Tunisian Dinar TND\",\n        value: \"TND\",\n        hasSymbol: \"Dinar\",\n        maxFractionalDigits: 0\n    },\n    {\n        label: \"Turkish Lira TRY\",\n        value: \"TRY\",\n        hasSymbol: \"₺\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"New Taiwan Dollar TWD\",\n        value: \"TWD\",\n        hasSymbol: \"NT$\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Tanzanian Shilling TZS\",\n        value: \"TZS\",\n        hasSymbol: \"Shilling\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Ukrainian Hryvnia UAH\",\n        value: \"UAH\",\n        hasSymbol: \"₴\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Ugandan Shilling UGX\",\n        value: \"UGX\",\n        maxFractionalDigits: 0\n    },\n    {\n        label: \"Uruguayan Peso UYU\",\n        value: \"UYU\",\n        hasSymbol: \"$U\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Uzbekistani Som UZS\",\n        value: \"UZS\",\n        hasSymbol: \"лв\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Venezuelan Bolívar VES\",\n        value: \"VES\",\n        hasSymbol: \"Bs\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Vietnamese Dong VND\",\n        value: \"VND\",\n        hasSymbol: \"₫\",\n        maxFractionalDigits: 0\n    },\n    {\n        label: \"Central African CFA Franc BEAC XAF\",\n        value: \"XAF\",\n        hasSymbol: \"FCFA\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Silver Ounce XAG\",\n        value: \"XAG\",\n        hasSymbol: \"Oz\",\n        maxFractionalDigits: 6\n    },\n    {\n        label: \"Gold Ounce XAU\",\n        value: \"XAU\",\n        hasSymbol: \"Oz\",\n        maxFractionalDigits: 6\n    },\n    {\n        label: \"CFA Franc XOF\",\n        value: \"XOF\",\n        hasSymbol: \"FCFA\",\n        maxFractionalDigits: 2\n    },\n    {\n        label: \"Platinum Ounce XPT\",\n        value: \"XPT\",\n        hasSymbol: \"Oz\",\n        maxFractionalDigits: 6\n    },\n    {\n        label: \"South African Rand ZAR\",\n        value: \"ZAR\",\n        hasSymbol: \"R\",\n        maxFractionalDigits: 2\n    }\n];\n"
  },
  {
    "path": "src/utils/debounce.ts",
    "content": "// copied from https://github.com/solidjs-community/solid-primitives/tree/main/packages/scheduled#readme\n\nimport { getOwner, onCleanup } from \"solid-js\";\n\nexport type ScheduleCallback = <Args extends unknown[]>(\n    callback: (...args: Args) => void,\n    wait?: number\n) => Scheduled<Args>;\n\nexport interface Scheduled<Args extends unknown[]> {\n    (...args: Args): void;\n    clear: VoidFunction;\n}\n\n/**\n * Creates a callback that is debounced and cancellable. The debounced callback is called on **trailing** edge.\n *\n * The timeout will be automatically cleared on root dispose.\n *\n * @param callback The callback to debounce\n * @param wait The duration to debounce in milliseconds\n * @returns The debounced function\n *\n * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/scheduled#debounce\n *\n * @example\n * ```ts\n * const fn = debounce((message: string) => console.log(message), 250);\n * fn('Hello!');\n * fn.clear() // clears a timeout in progress\n * ```\n */\nexport const debounce: ScheduleCallback = (callback, wait) => {\n    let timeoutId: ReturnType<typeof setTimeout> | undefined;\n    const clear = () => clearTimeout(timeoutId);\n    if (getOwner()) onCleanup(clear);\n    const debounced: typeof callback = (...args) => {\n        if (timeoutId !== undefined) clear();\n        timeoutId = setTimeout(() => callback(...args), wait);\n    };\n    return Object.assign(debounced, { clear });\n};\n"
  },
  {
    "path": "src/utils/deepSignal.ts",
    "content": "import { Signal } from \"solid-js\";\nimport { createStore, reconcile, unwrap } from \"solid-js/store\";\n\n// All these shenanigans are so we don't get a flicker after every refresh\n// API may change in the future: https://docs.solidjs.com/references/api-reference/basic-reactivity/createResource\nexport function createDeepSignal<T>(value: T): Signal<T> {\n    const [store, setStore] = createStore({\n        value\n    });\n    return [\n        // eslint-disable-next-line\n        () => store.value,\n        // eslint-disable-next-line\n        (v: T) => {\n            const unwrapped = unwrap(store.value);\n            typeof v === \"function\" && (v = v(unwrapped));\n            setStore(\"value\", reconcile(v, { merge: true }));\n            return store.value;\n        }\n    ] as Signal<T>;\n}\n"
  },
  {
    "path": "src/utils/download.ts",
    "content": "import { Capacitor } from \"@capacitor/core\";\nimport { Directory, Encoding, Filesystem } from \"@capacitor/filesystem\";\nimport { Share } from \"@capacitor/share\";\nimport { Toast } from \"@capacitor/toast\";\n\nimport { eify } from \"./eify\";\n\nexport async function downloadTextFile(\n    content: string,\n    fileName: string,\n    type?: string\n) {\n    const contentType = type ? type : \"application/json\";\n\n    if (Capacitor.isNativePlatform()) {\n        try {\n            const fileConfig = {\n                path: `Exports/${fileName}`,\n                data: content,\n                directory: Directory.Cache,\n                encoding: Encoding.UTF8,\n                recursive: true\n            };\n            const writeFileResult = await Filesystem.writeFile(fileConfig);\n\n            // Share the file\n            // This is because we save to the cache to get around reinstall issues\n            // with file storage permissions.\n            await Share.share({ url: writeFileResult.uri });\n\n            await Toast.show({\n                text: `File shared successfully`,\n                duration: \"long\"\n            });\n        } catch (error) {\n            console.error(\n                \"Error creating or sharing the file: \",\n                JSON.stringify(error)\n            );\n\n            const err = eify(error);\n\n            if (err.message.includes(\"Share canceled\")) {\n                // Do nothing\n                return;\n            } else {\n                await Toast.show({\n                    text: `Error while saving or sharing file`,\n                    duration: \"long\"\n                });\n            }\n        }\n    } else {\n        // https://stackoverflow.com/questions/34156282/how-do-i-save-json-to-local-text-file\n        const a = document.createElement(\"a\");\n        const file = new Blob([content], { type: contentType });\n        a.href = URL.createObjectURL(file);\n        a.download = fileName;\n        a.click();\n    }\n}\n"
  },
  {
    "path": "src/utils/eify.ts",
    "content": "/// Sometimes we catch an error as `unknown` so this turns it into an Error.\nexport function eify(e: unknown): Error {\n    if (e instanceof Error) {\n        return e;\n    } else if (typeof e === \"string\") {\n        return new Error(e);\n    } else {\n        return new Error(\"Unknown error\");\n    }\n}\n"
  },
  {
    "path": "src/utils/fetchZaps.ts",
    "content": "import { ResourceFetcher } from \"solid-js\";\n\nimport { useMegaStore, WalletWorker } from \"~/state/megaStore\";\nimport {\n    getPrimalImageUrl,\n    hexpubFromNpub,\n    NostrKind,\n    NostrTag\n} from \"~/utils/nostr\";\n\nexport const ZAPPLE_PAY_NPUB =\n    \"npub1wxl6njlcgygduct7jkgzrvyvd9fylj4pqvll6p32h59wyetm5fxqjchcan\";\n\nexport const ZAPPLE_PAY_HEXPUB =\n    \"71bfa9cbf84110de617e959021b08c69524fcaa1033ffd062abd0ae2657ba24c\";\n\ntype NostrEvent = {\n    created_at: number;\n    content: string;\n    tags: NostrTag[];\n    kind?: NostrKind | number;\n    pubkey: string;\n    id?: string;\n    sig?: string;\n};\n\ntype SimpleZapItem = {\n    kind: \"public\" | \"private\" | \"anonymous\";\n    from_hexpub: string;\n    to_hexpub: string;\n    timestamp: bigint;\n    amount_sats: bigint;\n    note?: string;\n    event_id?: string;\n    event: NostrEvent;\n    content?: string;\n    needsProfile?: boolean;\n};\n\nexport type NostrProfile = {\n    id: string;\n    pubkey: string;\n    created_at: number;\n    kind: number;\n    tags: string[][];\n    content: string;\n    sig: string;\n};\n\nfunction findByTag(tags: string[][], tag: string): string | undefined {\n    if (!tags || !Array.isArray(tags)) return;\n    const found = tags.find((t) => {\n        if (t[0] === tag) {\n            return true;\n        }\n    });\n\n    if (found) {\n        return found[1];\n    }\n}\n\nfunction getZapKind(event: NostrEvent): \"public\" | \"private\" | \"anonymous\" {\n    const anonTag = event.tags.find((t) => {\n        if (t[0] === \"anon\") {\n            return true;\n        }\n    });\n\n    // If the anon field is empty it's anon, if it has other elements its private, otherwise it's public\n    if (anonTag) {\n        return anonTag.length < 2 ? \"anonymous\" : \"private\";\n    }\n\n    return \"public\";\n}\n\nasync function simpleZapFromEvent(\n    event: NostrEvent,\n    sw: WalletWorker\n): Promise<SimpleZapItem | undefined> {\n    if (event.kind === 9735 && event.tags?.length > 0) {\n        const to = findByTag(event.tags, \"p\") || \"\";\n        const request = JSON.parse(\n            findByTag(event.tags, \"description\") || \"{}\"\n        );\n\n        // Special case for zapple pay\n        const isZapplePay =\n            request.pubkey == ZAPPLE_PAY_HEXPUB &&\n            request.content.startsWith(\"From: nostr:npub\");\n\n        const from = isZapplePay\n            ? await sw.npub_to_hexpub(request.content.split(\"From: nostr:\")[1])\n            : request.pubkey;\n        const content = isZapplePay ? \"\" : request.content;\n\n        const bolt11 = findByTag(event.tags, \"bolt11\") || \"\";\n\n        if (!bolt11) {\n            // not a zap!\n            return undefined;\n        }\n\n        let amount = 0n;\n\n        // who is the asshole putting \"lnbc9m\" in all these tags?\n        if (bolt11) {\n            try {\n                // We hardcode the \"bitcoin\" network because we don't have a good source of mutinynet zaps\n                const decoded = await sw.decode_invoice(bolt11, \"bitcoin\");\n                if (decoded?.amount_sats) {\n                    amount = decoded.amount_sats;\n                } else {\n                    console.log(\"no amount in decoded invoice\");\n                    return undefined;\n                }\n            } catch (e) {\n                console.error(e);\n                return undefined;\n            }\n        }\n\n        // If we can't get the amount from the invoice we'll fallback to the event tags\n        if (amount === 0n && request.tags?.length > 0) {\n            amount = BigInt(findByTag(request.tags, \"amount\") || \"0\") / 1000n;\n        }\n\n        return {\n            kind: getZapKind(request),\n            from_hexpub: from,\n            to_hexpub: to,\n            timestamp: BigInt(event.created_at),\n            amount_sats: amount,\n            note: request.id,\n            event_id: findByTag(request.tags, \"e\"),\n            event,\n            content: content,\n            needsProfile: isZapplePay\n        };\n    }\n}\n\n// todo remove\nconst PRIMAL_API = import.meta.env.VITE_PRIMAL;\n\ntype PrimalResponse = NostrEvent | NostrProfile;\n\nasync function fetchZapsFromPrimal(\n    follows: string[],\n    primal_url?: string,\n    until?: number\n): Promise<PrimalResponse[]> {\n    if (!primal_url) throw new Error(\"Missing PRIMAL_API environment variable\");\n\n    const query = {\n        kinds: [9735, 0, 10000113],\n        limit: 100,\n        pubkeys: follows\n    };\n\n    const restPayload = JSON.stringify([\n        \"zaps_feed\",\n        // If we have a until value, use it, otherwise don't include it\n        until ? { ...query, since: until } : query\n    ]);\n\n    const response = await fetch(primal_url, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json\"\n        },\n        body: restPayload\n    });\n\n    if (!response.ok) {\n        throw new Error(`Failed to load zaps`);\n    }\n\n    const data = await response.json();\n\n    // A primal response could be an error ({error: \"error\"}, or an array of events\n    if (data.error || !Array.isArray(data)) {\n        throw new Error(\"Zap response was not an array\");\n    }\n\n    return data;\n}\n\nexport const fetchZaps: ResourceFetcher<\n    string,\n    {\n        follows: string[];\n        zaps: SimpleZapItem[];\n        profiles: Record<string, NostrProfile>;\n        until?: number;\n    }\n> = async (npub, info) => {\n    const [state, _actions, sw] = useMegaStore();\n\n    try {\n        console.log(\"fetching zaps for:\", npub);\n        let follows: string[] = info?.value ? info.value.follows : [];\n        const zaps: SimpleZapItem[] = [];\n        const profiles: Record<string, NostrProfile> =\n            info.value?.profiles || {};\n        let newUntil = undefined;\n\n        const primal_url = state.settings?.primal_api;\n        if (!primal_url)\n            throw new Error(\"Missing PRIMAL_API environment variable\");\n\n        // Only have to ask the relays for follows one time\n        if (follows.length === 0) {\n            const contacts = await sw.get_contacts_sorted();\n            const hexpubs = [];\n            if (contacts) {\n                for (const contact of contacts) {\n                    if (contact.npub) {\n                        const hexpub = await hexpubFromNpub(sw, contact.npub);\n                        if (hexpub) {\n                            hexpubs.push(hexpub);\n                        }\n                    }\n                }\n            }\n            follows = hexpubs;\n        }\n\n        // Ask primal for all the zaps for these follow pubkeys\n        const data = await fetchZapsFromPrimal(\n            follows,\n            primal_url,\n            info?.value?.until\n        );\n\n        // Parse the primal response\n        for (const object of data) {\n            if (object.kind === 10000113) {\n                // console.log(\"got a 10000113 object\", object);\n                try {\n                    const content = JSON.parse(object.content);\n                    if (content?.until) {\n                        newUntil = content?.until + 1;\n                    }\n                } catch (e) {\n                    console.error(\"Failed to parse content: \", object.content);\n                }\n            }\n\n            if (object.kind === 0) {\n                // console.log(\"got a 0 object\", object);\n                profiles[object.pubkey] = object as NostrProfile;\n            }\n\n            if (object.kind === 9735) {\n                // console.log(\"got a 9735 object\", object);\n                try {\n                    const event = await simpleZapFromEvent(object, sw);\n\n                    // Only add it if it's a valid zap (not undefined)\n                    if (event) {\n                        if (\n                            event.needsProfile &&\n                            profiles[event.from_hexpub] === undefined\n                        ) {\n                            console.log(\n                                \"fetching profile for\",\n                                event.from_hexpub\n                            );\n                            const profile = await actuallyFetchNostrProfile(\n                                event.from_hexpub\n                            );\n                            if (profile) {\n                                profiles[profile.pubkey] = profile;\n                            }\n                        }\n\n                        zaps.push(event);\n                    }\n                } catch (e) {\n                    console.error(\"Failed to parse zap event: \", object, e);\n                }\n            }\n        }\n\n        return {\n            follows,\n            zaps: [...zaps, ...(info?.value?.zaps || [])],\n            profiles,\n            until: newUntil ? newUntil : info?.value?.until\n        };\n    } catch (e) {\n        console.error(\"Failed to load zaps: \", e);\n        throw new Error(\"Failed to load zaps\");\n    }\n};\n\nexport const fetchNostrProfile: ResourceFetcher<\n    string,\n    NostrProfile | undefined\n> = async (hexpub, _info) => {\n    return await actuallyFetchNostrProfile(hexpub);\n};\n\nexport async function actuallyFetchNostrProfile(hexpub: string) {\n    try {\n        if (!PRIMAL_API)\n            throw new Error(\"Missing PRIMAL_API environment variable\");\n\n        const response = await fetch(PRIMAL_API, {\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\"\n            },\n            body: JSON.stringify([\"user_profile\", { pubkey: hexpub }])\n        });\n\n        if (!response.ok) {\n            throw new Error(`Failed to load profile`);\n        }\n\n        const data = await response.json();\n\n        for (const object of data) {\n            if (object.kind === 0) {\n                return object as NostrProfile;\n            }\n        }\n    } catch (e) {\n        console.error(\"Failed to load profile: \", e);\n        throw new Error(\"Failed to load profile\");\n    }\n}\n\n// Search results from primal have some of the stuff we want for a TagItem contact\nexport type PseudoContact = {\n    name: string;\n    hexpub: string;\n    ln_address?: string;\n    lnurl?: string;\n    image_url?: string;\n    primal_image_url?: string;\n};\n\nexport async function searchProfiles(query: string): Promise<PseudoContact[]> {\n    console.log(\"searching profiles...\");\n    const response = await fetch(PRIMAL_API, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json\"\n        },\n        body: JSON.stringify([\n            \"user_search\",\n            { query: query.trim(), limit: 10 }\n        ])\n    });\n\n    if (!response.ok) {\n        throw new Error(`Failed to search`);\n    }\n\n    const data = await response.json();\n\n    const users: PseudoContact[] = [];\n\n    for (const object of data) {\n        if (object.kind === 0) {\n            try {\n                const profile = object as NostrProfile;\n                const contact = profileToPseudoContact(profile);\n                users.push(contact);\n            } catch (e) {\n                console.error(\"Failed to parse content: \", object.content);\n            }\n        }\n    }\n\n    return users;\n}\n\nexport function profileToPseudoContact(profile: NostrProfile): PseudoContact {\n    const content = JSON.parse(profile.content);\n    const contact: Partial<PseudoContact> = {\n        hexpub: profile.pubkey\n    };\n    contact.name = content.display_name || content.name || profile.pubkey;\n    contact.ln_address = content.lud16 || undefined;\n    contact.lnurl = content.lud06 || undefined;\n    contact.image_url = content.image || content.picture || undefined;\n    contact.primal_image_url = getPrimalImageUrl(contact.image_url);\n\n    return contact as PseudoContact;\n}\n"
  },
  {
    "path": "src/utils/gradientHash.ts",
    "content": "import { TagItem } from \"@mutinywallet/mutiny-wasm\";\n\nexport async function generateGradient(str: string) {\n    const encoder = new TextEncoder();\n    const data = encoder.encode(str);\n    const digestBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n    const digestArray = new Uint8Array(digestBuffer);\n    const h1 = digestArray[0] % 360;\n    const h2 = (h1 + 180) % 360;\n\n    const gradient = `linear-gradient(135deg, hsl(${h1}, 50%, 50%) 0%, hsl(${h2}, 50%, 50%) 100%)`;\n\n    return gradient;\n}\n\nexport async function gradientsPerContact(contacts: TagItem[]) {\n    const gradients = new Map();\n    for (const contact of contacts) {\n        const gradient = await generateGradient(contact.name);\n        gradients.set(contact.name, gradient);\n    }\n\n    return gradients;\n}\n"
  },
  {
    "path": "src/utils/index.ts",
    "content": "export * from \"./conversions\";\nexport * from \"./deepSignal\";\nexport * from \"./download\";\nexport * from \"./eify\";\nexport * from \"./gradientHash\";\nexport * from \"./mempoolTxUrl\";\nexport * from \"./objectToSearchParams\";\nexport * from \"./prettyPrintTime\";\nexport * from \"./subscriptions\";\nexport * from \"./timeout\";\nexport * from \"./typescript\";\nexport * from \"./useCopy\";\nexport * from \"./words\";\nexport * from \"./fetchZaps\";\nexport * from \"./vibrate\";\nexport * from \"./openLinkProgrammatically\";\nexport * from \"./nostr\";\nexport * from \"./currencies\";\nexport * from \"./languages\";\nexport * from \"./bech32\";\nexport * from \"./keypad\";\nexport * from \"./debounce\";\nexport * from \"./blobToBase64\";\n"
  },
  {
    "path": "src/utils/keypad.ts",
    "content": "import { Currency } from \"./currencies\";\n\n// Checks the users locale to determine if decimals should be a \".\" or a \",\"\nconst decimalDigitDivider = Number(1.0)\n    .toLocaleString(navigator.languages[0], { minimumFractionDigits: 1 })\n    .substring(1, 2);\n\nexport function toDisplayHandleNaN(\n    input?: string | bigint,\n    fiat?: Currency\n): string {\n    if (!input) {\n        return \"0\";\n    }\n\n    if (typeof input === \"bigint\") {\n        console.error(\"toDisplayHandleNaN: input is a bigint\", input);\n    }\n\n    const inputStr = input.toString();\n\n    const parsed = Number(input);\n\n    //handle decimals so the user can always see the accurate amount\n    if (isNaN(parsed)) {\n        return \"0\";\n    } else if (parsed === Math.trunc(parsed) && inputStr.endsWith(\".\")) {\n        return (\n            parsed.toLocaleString(navigator.languages[0]) + decimalDigitDivider\n        );\n        /* To avoid having logic to handle every number up to 8 decimals \n        any custom currency pair that has more than 3 decimals will always show all decimals*/\n    } else if (fiat?.maxFractionalDigits && fiat.maxFractionalDigits > 3) {\n        return parsed.toLocaleString(navigator.languages[0], {\n            minimumFractionDigits: parsed === 0 ? 0 : fiat.maxFractionalDigits,\n            maximumFractionDigits: fiat.maxFractionalDigits\n        });\n    } else if (parsed === Math.trunc(parsed) && inputStr.endsWith(\".0\")) {\n        return parsed.toLocaleString(navigator.languages[0], {\n            minimumFractionDigits: 1\n        });\n    } else if (parsed === Math.trunc(parsed) && inputStr.endsWith(\".00\")) {\n        return parsed.toLocaleString(navigator.languages[0], {\n            minimumFractionDigits: 2\n        });\n    } else if (parsed === Math.trunc(parsed) && inputStr.endsWith(\".000\")) {\n        return parsed.toLocaleString(navigator.languages[0], {\n            minimumFractionDigits: 3\n        });\n    } else if (\n        parsed !== Math.trunc(parsed) &&\n        // matches strings that have 3 total digits after the decimal and ends with 0\n        inputStr.match(/\\.\\d{2}0$/) &&\n        inputStr.includes(\".\", inputStr.length - 4)\n    ) {\n        return parsed.toLocaleString(navigator.languages[0], {\n            minimumFractionDigits: 3\n        });\n    } else if (\n        parsed !== Math.trunc(parsed) &&\n        // matches strings that have 2 total digits after the decimal and ends with 0\n        inputStr.match(/\\.\\d{1}0$/) &&\n        inputStr.includes(\".\", inputStr.length - 3)\n    ) {\n        return parsed.toLocaleString(navigator.languages[0], {\n            minimumFractionDigits: 2\n        });\n    } else {\n        return parsed.toLocaleString(navigator.languages[0], {\n            maximumFractionDigits: 3\n        });\n    }\n}\n\nexport function fiatInputSanitizer(input: string, maxDecimals: number): string {\n    // Make sure only numbers and a single decimal point are allowed if decimals are allowed\n    let allowDecimalRegex;\n    if (maxDecimals !== 0) {\n        allowDecimalRegex = new RegExp(\"[^0-9.]\", \"g\");\n    } else {\n        allowDecimalRegex = new RegExp(\"[^0-9]\", \"g\");\n    }\n    const numeric = input\n        .replace(allowDecimalRegex, \"\")\n        .replace(/(\\..*)\\./g, \"$1\");\n\n    // Remove leading zeros if not a decimal, add 0 if starts with a decimal\n    const cleaned = numeric.replace(/^0([^.]|$)/g, \"$1\").replace(/^\\./g, \"0.\");\n\n    // If there are more characters after the decimal than allowed, shift the decimal\n    const shiftRegex = new RegExp(\n        \"(\\\\.[0-9]{\" + (maxDecimals + 1) + \"}).*\",\n        \"g\"\n    );\n    const shifted = cleaned.match(shiftRegex)\n        ? (parseFloat(cleaned) * 10).toFixed(maxDecimals)\n        : cleaned;\n\n    // Truncate any numbers past the maxDecimal for the currency\n    const decimalRegex = new RegExp(\"(\\\\.[0-9]{\" + maxDecimals + \"}).*\", \"g\");\n    const decimals = shifted.replace(decimalRegex, \"$1\");\n\n    return decimals;\n}\n\nexport function satsInputSanitizer(input: string): string {\n    // Make sure only numbers are allowed\n    const numeric = input.replace(/[^0-9]/g, \"\");\n    // If it starts with a 0, remove the 0\n    const noLeadingZero = numeric.replace(/^0([^.]|$)/g, \"$1\");\n\n    return noLeadingZero;\n}\n\nexport function btcFloatRounding(localValue: string): string {\n    return (\n        (parseFloat(localValue) -\n            parseFloat(localValue.charAt(localValue.length - 1)) / 100000000) /\n        10\n    ).toFixed(8);\n}\n"
  },
  {
    "path": "src/utils/languages.ts",
    "content": "export interface Language {\n    value: string;\n    shortName: string;\n}\n\nexport const EN_OPTION: Language = {\n    value: \"English\",\n    shortName: \"en\"\n};\n\n// Sorted alphabetically-ish\nexport const LANGUAGE_OPTIONS: Language[] = [\n    {\n        value: \"Deutsch\",\n        shortName: \"de\"\n    },\n    {\n        value: \"Español\",\n        shortName: \"es\"\n    },\n    {\n        value: \"Français\",\n        shortName: \"fr\"\n    },\n    {\n        value: \"简体中文\",\n        shortName: \"hans\"\n    },\n    {\n        value: \"繁體中文\",\n        shortName: \"hant\"\n    },\n    {\n        value: \"Italian\",\n        shortName: \"it\"\n    },\n    {\n        value: \"한국어\",\n        shortName: \"ko\"\n    },\n    {\n        value: \"Português\",\n        shortName: \"pt\"\n    }\n];\n"
  },
  {
    "path": "src/utils/mempoolTxUrl.ts",
    "content": "import { Network } from \"~/logic/mutinyWalletSetup\";\n\nexport function mempoolTxUrl(txid?: string, network?: Network) {\n    if (!txid || !network) {\n        console.error(\"Problem creating the mempool url\");\n        return \"#\";\n    }\n\n    if (network) {\n        switch (network) {\n            case \"bitcoin\":\n                return `https://mempool.space/tx/${txid}`;\n            case \"testnet\":\n                return `https://mempool.space/testnet/tx/${txid}`;\n            case \"signet\":\n                return `https://mutinynet.com/tx/${txid}`;\n            default:\n                return `https://mempool.space/tx/${txid}`;\n        }\n    }\n\n    return `https://mempool.space/tx/${txid}`;\n}\n"
  },
  {
    "path": "src/utils/nostr.ts",
    "content": "import { WalletWorker } from \"~/state/megaStore\";\n\nexport type NostrTag = string[];\nexport declare enum NostrKind {\n    Metadata = 0,\n    Text = 1,\n    RecommendRelay = 2,\n    Contacts = 3,\n    EncryptedDirectMessage = 4,\n    EventDeletion = 5,\n    Repost = 6,\n    Reaction = 7,\n    BadgeAward = 8,\n    GenericRepost = 16,\n    ChannelCreation = 40,\n    ChannelMetadata = 41,\n    ChannelMessage = 42,\n    ChannelHideMessage = 43,\n    ChannelMuteUser = 44,\n    Report = 1984,\n    ZapRequest = 9734,\n    Zap = 9735,\n    Highlight = 9802,\n    MuteList = 10000,\n    PinList = 10001,\n    RelayList = 10002,\n    ClientAuth = 22242,\n    NostrConnect = 24133,\n    CategorizedPeopleList = 30000,\n    CategorizedBookmarkList = 30001,\n    CategorizedRelayList = 30022,\n    ProfileBadge = 30008,\n    BadgeDefinition = 30009,\n    MarketStall = 30017,\n    MarketProduct = 30018,\n    Article = 30023,\n    AppSpecificData = 30078,\n    Classified = 30402,\n    AppRecommendation = 31989,\n    AppHandler = 31990,\n    CategorizedHighlightList = 39802,\n    DVMJobFeedback = 65000,\n    DVMJobResult = 65001,\n    DVMJobRequestTranscription = 65002\n}\n\nexport async function hexpubFromNpub(\n    sw: WalletWorker,\n    npub?: string\n): Promise<string | undefined> {\n    if (!npub) {\n        return undefined;\n    }\n    if (!npub.toLowerCase().startsWith(\"npub\")) {\n        return undefined;\n    }\n\n    try {\n        const hexpub = await sw?.npub_to_hexpub(npub);\n        return hexpub;\n    } catch (err) {\n        console.error(err);\n        return undefined;\n    }\n}\n\nexport function getPrimalImageUrl(image_url?: string): string | undefined {\n    if (!image_url) {\n        return undefined;\n    }\n    return `https://primal.b-cdn.net/media-cache?s=s&a=1&u=${encodeURIComponent(\n        image_url\n    )}`;\n}\n\nexport const DEFAULT_NOSTR_NAME = \"Anon\";\n"
  },
  {
    "path": "src/utils/objectToSearchParams.ts",
    "content": "export function objectToSearchParams<\n    T extends Record<string, string | undefined>\n>(obj: T): string {\n    return (\n        Object.entries(obj)\n            .filter(([_, value]) => value !== undefined)\n            // Value shouldn't be null we just filtered it out but typescript is dumb\n            .map(([key, value]) =>\n                value\n                    ? `${encodeURIComponent(key)}=${encodeURIComponent(value)}`\n                    : \"\"\n            )\n            .join(\"&\")\n    );\n}\n"
  },
  {
    "path": "src/utils/openLinkProgrammatically.ts",
    "content": "import { AppLauncher } from \"@capacitor/app-launcher\";\nimport { Capacitor } from \"@capacitor/core\";\n\nimport { showToast, ToastArg } from \"~/components\";\n\n// Have to pass in the failure text because i18n doesn't work in utils\nexport async function openLinkProgrammatically(\n    url?: string,\n    failureText?: ToastArg\n) {\n    if (!url) return;\n    if (Capacitor.isNativePlatform()) {\n        const { value } = await AppLauncher.canOpenUrl({ url });\n\n        if (!value) {\n            showToast(\n                failureText || {\n                    title: \"Client not found\",\n                    description:\n                        \"Please install a compatible client to open this link.\"\n                }\n            );\n            // Try to open in browser just in case that works glhf\n            window.open(url || \"\", \"_blank\");\n            return;\n        } else {\n            await AppLauncher.openUrl({ url });\n        }\n    } else {\n        window.open(url || \"\", \"_blank\");\n    }\n}\n"
  },
  {
    "path": "src/utils/platform.ts",
    "content": "// https://stackoverflow.com/questions/9038625/detect-if-device-is-ios\n\nimport { Capacitor } from \"@capacitor/core\";\n\nexport function iosNotNative() {\n    if (Capacitor.isNativePlatform() || Capacitor.getPlatform() === \"ios\") {\n        return false;\n    }\n    return (\n        [\n            \"iPad Simulator\",\n            \"iPhone Simulator\",\n            \"iPod Simulator\",\n            \"iPad\",\n            \"iPhone\",\n            \"iPod\"\n        ].includes(navigator.platform) ||\n        // iPad on iOS 13 detection\n        (navigator.userAgent.includes(\"Mac\") && \"ontouchend\" in document)\n    );\n}\n"
  },
  {
    "path": "src/utils/prettyPrintTime.ts",
    "content": "import { useI18n } from \"~/i18n/context\";\n\nexport function prettyPrintTime(ts: number) {\n    const options: Intl.DateTimeFormatOptions = {\n        year: \"numeric\",\n        month: \"short\",\n        day: \"numeric\",\n        hour: \"numeric\",\n        minute: \"numeric\"\n    };\n\n    return new Date(ts * 1000).toLocaleString(\"en-US\", options);\n}\n\n// Rerender signal is a silly way to force timeAgo to recalculate even though the timestamp is static\nexport function timeAgo(\n    ts?: number | bigint,\n    _rerenderSignal?: number\n): string {\n    const i18n = useI18n();\n\n    if (!ts || ts === 0) return i18n.t(\"common.pending\");\n    const timestamp = Number(ts) * 1000;\n    const now = Date.now();\n    const tense = now - timestamp < 0 ? \"future\" : \"past\";\n    const elapsedMilliseconds = Math.abs(now - timestamp);\n    const elapsedSeconds = Math.floor(elapsedMilliseconds / 1000);\n    const elapsedMinutes = Math.floor(elapsedSeconds / 60);\n    const elapsedHours = Math.floor(elapsedMinutes / 60);\n    const elapsedDays = Math.floor(elapsedHours / 24);\n\n    if (elapsedSeconds < 60) {\n        return i18n.t(`utils.seconds_${tense}`);\n    } else if (elapsedMinutes < 60) {\n        return i18n.t(`utils.minutes_${tense}`, { count: elapsedMinutes });\n    } else if (elapsedHours < 24) {\n        return i18n.t(`utils.hours_${tense}`, { count: elapsedHours });\n    } else if (elapsedDays < 7) {\n        return i18n.t(`utils.days_${tense}`, { count: elapsedDays });\n    } else {\n        const date = new Date(timestamp);\n        const day = String(date.getDate()).padStart(2, \"0\");\n        const month = String(date.getMonth() + 1).padStart(2, \"0\");\n        const year = date.getFullYear();\n        return `${month}/${day}/${year}`;\n    }\n}\n\nexport function veryShortTimeStamp(ts?: number | bigint) {\n    const i18n = useI18n();\n\n    if (!ts || ts === 0) return i18n.t(\"common.pending\");\n    const timestamp = Number(ts) * 1000;\n    const now = Date.now();\n    const elapsedMilliseconds = Math.abs(now - timestamp);\n    const elapsedSeconds = Math.floor(elapsedMilliseconds / 1000);\n    const elapsedMinutes = Math.floor(elapsedSeconds / 60);\n    const elapsedHours = Math.floor(elapsedMinutes / 60);\n    const elapsedDays = Math.floor(elapsedHours / 24);\n\n    if (elapsedSeconds < 60) {\n        return i18n.t(\"utils.nowish\");\n    } else if (elapsedMinutes < 60) {\n        return i18n.t(\"utils.minutes_short\", { count: elapsedMinutes });\n    } else if (elapsedHours < 24) {\n        return i18n.t(\"utils.hours_short\", { count: elapsedHours });\n    } else if (elapsedDays < 7) {\n        return i18n.t(\"utils.days_short\", { count: elapsedDays });\n    } else {\n        const date = new Date(timestamp);\n        const day = String(date.getDate()).padStart(2, \"0\");\n        const month = String(date.getMonth() + 1).padStart(2, \"0\");\n        const year = date.getFullYear();\n        return `${month}/${day}/${year}`;\n    }\n}\n"
  },
  {
    "path": "src/utils/subscriptions.ts",
    "content": "const GRACE = 60 * 60 * 24 * 3; // 3 days\n\nexport function subscriptionValid(subscriptionExpiresTimestamp?: number) {\n    if (!subscriptionExpiresTimestamp) return false;\n\n    return subscriptionExpiresTimestamp + GRACE > Math.ceil(Date.now() / 1000);\n}\n\nexport function isFreeGiftingDay() {\n    const today = new Date();\n    // days are 1 indexed, months are 0 indexed\n    const isChristmas = today.getDate() === 25 && today.getMonth() === 11;\n\n    return isChristmas || isThanksgiving(today);\n}\n\nfunction isThanksgiving(today: Date) {\n    if (today.getMonth() !== 10) return false;\n\n    const year = today.getFullYear();\n    const thanksgivingDay = new Date(year, 10, 1);\n\n    // Find out what day of the week Nov 1 is\n    const dayOfWeek = thanksgivingDay.getDay();\n\n    // Calculate the date of the fourth Thursday in November\n    thanksgivingDay.setDate(1 + ((4 - dayOfWeek) % 7) + 3 * 7);\n\n    // Compare the current date with Thanksgiving date\n    return today.getDate() === thanksgivingDay.getDate();\n}\n"
  },
  {
    "path": "src/utils/timeout.ts",
    "content": "// A promise version of timeout\nexport const timeout = (ms: number) =>\n    new Promise((resolve) => setTimeout(resolve, ms));\n"
  },
  {
    "path": "src/utils/typescript.ts",
    "content": "export type Result<T, E = Error> =\n    | { ok: true; value: T }\n    | { ok: false; error: E };\n"
  },
  {
    "path": "src/utils/useCopy.ts",
    "content": "// Thanks you https://soorria.com/snippets/use-copy-solidjs\nimport { Clipboard } from \"@capacitor/clipboard\";\nimport { Capacitor } from \"@capacitor/core\";\nimport type { Accessor } from \"solid-js\";\nimport { createSignal } from \"solid-js\";\n\ntype UseCopyProps = {\n    copiedTimeout?: number;\n};\ntype CopyFn = (text: string) => Promise<void>;\nexport const useCopy = ({ copiedTimeout = 2000 }: UseCopyProps = {}): [\n    copy: CopyFn,\n    copied: Accessor<boolean>\n] => {\n    const [copied, setCopied] = createSignal(false);\n    let timeout: ReturnType<typeof setTimeout> | undefined;\n    const copy: CopyFn = async (text) => {\n        if (Capacitor.isNativePlatform()) {\n            await Clipboard.write({\n                string: text\n            });\n        } else {\n            await navigator.clipboard.writeText(text);\n        }\n        setCopied(true);\n        if (timeout) clearTimeout(timeout);\n        timeout = setTimeout(() => setCopied(false), copiedTimeout);\n    };\n    return [copy, copied];\n};\n"
  },
  {
    "path": "src/utils/vibrate.ts",
    "content": "import { Haptics } from \"@capacitor/haptics\";\nimport { NotificationType } from \"@capacitor/haptics/dist/esm/definitions\";\n\nexport const vibrateSuccess = async () => {\n    try {\n        await Haptics.notification({ type: NotificationType.Success });\n    } catch (error) {\n        console.warn(error);\n    }\n};\n"
  },
  {
    "path": "src/utils/words.ts",
    "content": "export const WORDS_EN: Set<string> = new Set([\n    \"abandon\",\n    \"ability\",\n    \"able\",\n    \"about\",\n    \"above\",\n    \"absent\",\n    \"absorb\",\n    \"abstract\",\n    \"absurd\",\n    \"abuse\",\n    \"access\",\n    \"accident\",\n    \"account\",\n    \"accuse\",\n    \"achieve\",\n    \"acid\",\n    \"acoustic\",\n    \"acquire\",\n    \"across\",\n    \"act\",\n    \"action\",\n    \"actor\",\n    \"actress\",\n    \"actual\",\n    \"adapt\",\n    \"add\",\n    \"addict\",\n    \"address\",\n    \"adjust\",\n    \"admit\",\n    \"adult\",\n    \"advance\",\n    \"advice\",\n    \"aerobic\",\n    \"affair\",\n    \"afford\",\n    \"afraid\",\n    \"again\",\n    \"age\",\n    \"agent\",\n    \"agree\",\n    \"ahead\",\n    \"aim\",\n    \"air\",\n    \"airport\",\n    \"aisle\",\n    \"alarm\",\n    \"album\",\n    \"alcohol\",\n    \"alert\",\n    \"alien\",\n    \"all\",\n    \"alley\",\n    \"allow\",\n    \"almost\",\n    \"alone\",\n    \"alpha\",\n    \"already\",\n    \"also\",\n    \"alter\",\n    \"always\",\n    \"amateur\",\n    \"amazing\",\n    \"among\",\n    \"amount\",\n    \"amused\",\n    \"analyst\",\n    \"anchor\",\n    \"ancient\",\n    \"anger\",\n    \"angle\",\n    \"angry\",\n    \"animal\",\n    \"ankle\",\n    \"announce\",\n    \"annual\",\n    \"another\",\n    \"answer\",\n    \"antenna\",\n    \"antique\",\n    \"anxiety\",\n    \"any\",\n    \"apart\",\n    \"apology\",\n    \"appear\",\n    \"apple\",\n    \"approve\",\n    \"april\",\n    \"arch\",\n    \"arctic\",\n    \"area\",\n    \"arena\",\n    \"argue\",\n    \"arm\",\n    \"armed\",\n    \"armor\",\n    \"army\",\n    \"around\",\n    \"arrange\",\n    \"arrest\",\n    \"arrive\",\n    \"arrow\",\n    \"art\",\n    \"artefact\",\n    \"artist\",\n    \"artwork\",\n    \"ask\",\n    \"aspect\",\n    \"assault\",\n    \"asset\",\n    \"assist\",\n    \"assume\",\n    \"asthma\",\n    \"athlete\",\n    \"atom\",\n    \"attack\",\n    \"attend\",\n    \"attitude\",\n    \"attract\",\n    \"auction\",\n    \"audit\",\n    \"august\",\n    \"aunt\",\n    \"author\",\n    \"auto\",\n    \"autumn\",\n    \"average\",\n    \"avocado\",\n    \"avoid\",\n    \"awake\",\n    \"aware\",\n    \"away\",\n    \"awesome\",\n    \"awful\",\n    \"awkward\",\n    \"axis\",\n    \"baby\",\n    \"bachelor\",\n    \"bacon\",\n    \"badge\",\n    \"bag\",\n    \"balance\",\n    \"balcony\",\n    \"ball\",\n    \"bamboo\",\n    \"banana\",\n    \"banner\",\n    \"bar\",\n    \"barely\",\n    \"bargain\",\n    \"barrel\",\n    \"base\",\n    \"basic\",\n    \"basket\",\n    \"battle\",\n    \"beach\",\n    \"bean\",\n    \"beauty\",\n    \"because\",\n    \"become\",\n    \"beef\",\n    \"before\",\n    \"begin\",\n    \"behave\",\n    \"behind\",\n    \"believe\",\n    \"below\",\n    \"belt\",\n    \"bench\",\n    \"benefit\",\n    \"best\",\n    \"betray\",\n    \"better\",\n    \"between\",\n    \"beyond\",\n    \"bicycle\",\n    \"bid\",\n    \"bike\",\n    \"bind\",\n    \"biology\",\n    \"bird\",\n    \"birth\",\n    \"bitter\",\n    \"black\",\n    \"blade\",\n    \"blame\",\n    \"blanket\",\n    \"blast\",\n    \"bleak\",\n    \"bless\",\n    \"blind\",\n    \"blood\",\n    \"blossom\",\n    \"blouse\",\n    \"blue\",\n    \"blur\",\n    \"blush\",\n    \"board\",\n    \"boat\",\n    \"body\",\n    \"boil\",\n    \"bomb\",\n    \"bone\",\n    \"bonus\",\n    \"book\",\n    \"boost\",\n    \"border\",\n    \"boring\",\n    \"borrow\",\n    \"boss\",\n    \"bottom\",\n    \"bounce\",\n    \"box\",\n    \"boy\",\n    \"bracket\",\n    \"brain\",\n    \"brand\",\n    \"brass\",\n    \"brave\",\n    \"bread\",\n    \"breeze\",\n    \"brick\",\n    \"bridge\",\n    \"brief\",\n    \"bright\",\n    \"bring\",\n    \"brisk\",\n    \"broccoli\",\n    \"broken\",\n    \"bronze\",\n    \"broom\",\n    \"brother\",\n    \"brown\",\n    \"brush\",\n    \"bubble\",\n    \"buddy\",\n    \"budget\",\n    \"buffalo\",\n    \"build\",\n    \"bulb\",\n    \"bulk\",\n    \"bullet\",\n    \"bundle\",\n    \"bunker\",\n    \"burden\",\n    \"burger\",\n    \"burst\",\n    \"bus\",\n    \"business\",\n    \"busy\",\n    \"butter\",\n    \"buyer\",\n    \"buzz\",\n    \"cabbage\",\n    \"cabin\",\n    \"cable\",\n    \"cactus\",\n    \"cage\",\n    \"cake\",\n    \"call\",\n    \"calm\",\n    \"camera\",\n    \"camp\",\n    \"can\",\n    \"canal\",\n    \"cancel\",\n    \"candy\",\n    \"cannon\",\n    \"canoe\",\n    \"canvas\",\n    \"canyon\",\n    \"capable\",\n    \"capital\",\n    \"captain\",\n    \"car\",\n    \"carbon\",\n    \"card\",\n    \"cargo\",\n    \"carpet\",\n    \"carry\",\n    \"cart\",\n    \"case\",\n    \"cash\",\n    \"casino\",\n    \"castle\",\n    \"casual\",\n    \"cat\",\n    \"catalog\",\n    \"catch\",\n    \"category\",\n    \"cattle\",\n    \"caught\",\n    \"cause\",\n    \"caution\",\n    \"cave\",\n    \"ceiling\",\n    \"celery\",\n    \"cement\",\n    \"census\",\n    \"century\",\n    \"cereal\",\n    \"certain\",\n    \"chair\",\n    \"chalk\",\n    \"champion\",\n    \"change\",\n    \"chaos\",\n    \"chapter\",\n    \"charge\",\n    \"chase\",\n    \"chat\",\n    \"cheap\",\n    \"check\",\n    \"cheese\",\n    \"chef\",\n    \"cherry\",\n    \"chest\",\n    \"chicken\",\n    \"chief\",\n    \"child\",\n    \"chimney\",\n    \"choice\",\n    \"choose\",\n    \"chronic\",\n    \"chuckle\",\n    \"chunk\",\n    \"churn\",\n    \"cigar\",\n    \"cinnamon\",\n    \"circle\",\n    \"citizen\",\n    \"city\",\n    \"civil\",\n    \"claim\",\n    \"clap\",\n    \"clarify\",\n    \"claw\",\n    \"clay\",\n    \"clean\",\n    \"clerk\",\n    \"clever\",\n    \"click\",\n    \"client\",\n    \"cliff\",\n    \"climb\",\n    \"clinic\",\n    \"clip\",\n    \"clock\",\n    \"clog\",\n    \"close\",\n    \"cloth\",\n    \"cloud\",\n    \"clown\",\n    \"club\",\n    \"clump\",\n    \"cluster\",\n    \"clutch\",\n    \"coach\",\n    \"coast\",\n    \"coconut\",\n    \"code\",\n    \"coffee\",\n    \"coil\",\n    \"coin\",\n    \"collect\",\n    \"color\",\n    \"column\",\n    \"combine\",\n    \"come\",\n    \"comfort\",\n    \"comic\",\n    \"common\",\n    \"company\",\n    \"concert\",\n    \"conduct\",\n    \"confirm\",\n    \"congress\",\n    \"connect\",\n    \"consider\",\n    \"control\",\n    \"convince\",\n    \"cook\",\n    \"cool\",\n    \"copper\",\n    \"copy\",\n    \"coral\",\n    \"core\",\n    \"corn\",\n    \"correct\",\n    \"cost\",\n    \"cotton\",\n    \"couch\",\n    \"country\",\n    \"couple\",\n    \"course\",\n    \"cousin\",\n    \"cover\",\n    \"coyote\",\n    \"crack\",\n    \"cradle\",\n    \"craft\",\n    \"cram\",\n    \"crane\",\n    \"crash\",\n    \"crater\",\n    \"crawl\",\n    \"crazy\",\n    \"cream\",\n    \"credit\",\n    \"creek\",\n    \"crew\",\n    \"cricket\",\n    \"crime\",\n    \"crisp\",\n    \"critic\",\n    \"crop\",\n    \"cross\",\n    \"crouch\",\n    \"crowd\",\n    \"crucial\",\n    \"cruel\",\n    \"cruise\",\n    \"crumble\",\n    \"crunch\",\n    \"crush\",\n    \"cry\",\n    \"crystal\",\n    \"cube\",\n    \"culture\",\n    \"cup\",\n    \"cupboard\",\n    \"curious\",\n    \"current\",\n    \"curtain\",\n    \"curve\",\n    \"cushion\",\n    \"custom\",\n    \"cute\",\n    \"cycle\",\n    \"dad\",\n    \"damage\",\n    \"damp\",\n    \"dance\",\n    \"danger\",\n    \"daring\",\n    \"dash\",\n    \"daughter\",\n    \"dawn\",\n    \"day\",\n    \"deal\",\n    \"debate\",\n    \"debris\",\n    \"decade\",\n    \"december\",\n    \"decide\",\n    \"decline\",\n    \"decorate\",\n    \"decrease\",\n    \"deer\",\n    \"defense\",\n    \"define\",\n    \"defy\",\n    \"degree\",\n    \"delay\",\n    \"deliver\",\n    \"demand\",\n    \"demise\",\n    \"denial\",\n    \"dentist\",\n    \"deny\",\n    \"depart\",\n    \"depend\",\n    \"deposit\",\n    \"depth\",\n    \"deputy\",\n    \"derive\",\n    \"describe\",\n    \"desert\",\n    \"design\",\n    \"desk\",\n    \"despair\",\n    \"destroy\",\n    \"detail\",\n    \"detect\",\n    \"develop\",\n    \"device\",\n    \"devote\",\n    \"diagram\",\n    \"dial\",\n    \"diamond\",\n    \"diary\",\n    \"dice\",\n    \"diesel\",\n    \"diet\",\n    \"differ\",\n    \"digital\",\n    \"dignity\",\n    \"dilemma\",\n    \"dinner\",\n    \"dinosaur\",\n    \"direct\",\n    \"dirt\",\n    \"disagree\",\n    \"discover\",\n    \"disease\",\n    \"dish\",\n    \"dismiss\",\n    \"disorder\",\n    \"display\",\n    \"distance\",\n    \"divert\",\n    \"divide\",\n    \"divorce\",\n    \"dizzy\",\n    \"doctor\",\n    \"document\",\n    \"dog\",\n    \"doll\",\n    \"dolphin\",\n    \"domain\",\n    \"donate\",\n    \"donkey\",\n    \"donor\",\n    \"door\",\n    \"dose\",\n    \"double\",\n    \"dove\",\n    \"draft\",\n    \"dragon\",\n    \"drama\",\n    \"drastic\",\n    \"draw\",\n    \"dream\",\n    \"dress\",\n    \"drift\",\n    \"drill\",\n    \"drink\",\n    \"drip\",\n    \"drive\",\n    \"drop\",\n    \"drum\",\n    \"dry\",\n    \"duck\",\n    \"dumb\",\n    \"dune\",\n    \"during\",\n    \"dust\",\n    \"dutch\",\n    \"duty\",\n    \"dwarf\",\n    \"dynamic\",\n    \"eager\",\n    \"eagle\",\n    \"early\",\n    \"earn\",\n    \"earth\",\n    \"easily\",\n    \"east\",\n    \"easy\",\n    \"echo\",\n    \"ecology\",\n    \"economy\",\n    \"edge\",\n    \"edit\",\n    \"educate\",\n    \"effort\",\n    \"egg\",\n    \"eight\",\n    \"either\",\n    \"elbow\",\n    \"elder\",\n    \"electric\",\n    \"elegant\",\n    \"element\",\n    \"elephant\",\n    \"elevator\",\n    \"elite\",\n    \"else\",\n    \"embark\",\n    \"embody\",\n    \"embrace\",\n    \"emerge\",\n    \"emotion\",\n    \"employ\",\n    \"empower\",\n    \"empty\",\n    \"enable\",\n    \"enact\",\n    \"end\",\n    \"endless\",\n    \"endorse\",\n    \"enemy\",\n    \"energy\",\n    \"enforce\",\n    \"engage\",\n    \"engine\",\n    \"enhance\",\n    \"enjoy\",\n    \"enlist\",\n    \"enough\",\n    \"enrich\",\n    \"enroll\",\n    \"ensure\",\n    \"enter\",\n    \"entire\",\n    \"entry\",\n    \"envelope\",\n    \"episode\",\n    \"equal\",\n    \"equip\",\n    \"era\",\n    \"erase\",\n    \"erode\",\n    \"erosion\",\n    \"error\",\n    \"erupt\",\n    \"escape\",\n    \"essay\",\n    \"essence\",\n    \"estate\",\n    \"eternal\",\n    \"ethics\",\n    \"evidence\",\n    \"evil\",\n    \"evoke\",\n    \"evolve\",\n    \"exact\",\n    \"example\",\n    \"excess\",\n    \"exchange\",\n    \"excite\",\n    \"exclude\",\n    \"excuse\",\n    \"execute\",\n    \"exercise\",\n    \"exhaust\",\n    \"exhibit\",\n    \"exile\",\n    \"exist\",\n    \"exit\",\n    \"exotic\",\n    \"expand\",\n    \"expect\",\n    \"expire\",\n    \"explain\",\n    \"expose\",\n    \"express\",\n    \"extend\",\n    \"extra\",\n    \"eye\",\n    \"eyebrow\",\n    \"fabric\",\n    \"face\",\n    \"faculty\",\n    \"fade\",\n    \"faint\",\n    \"faith\",\n    \"fall\",\n    \"false\",\n    \"fame\",\n    \"family\",\n    \"famous\",\n    \"fan\",\n    \"fancy\",\n    \"fantasy\",\n    \"farm\",\n    \"fashion\",\n    \"fat\",\n    \"fatal\",\n    \"father\",\n    \"fatigue\",\n    \"fault\",\n    \"favorite\",\n    \"feature\",\n    \"february\",\n    \"federal\",\n    \"fee\",\n    \"feed\",\n    \"feel\",\n    \"female\",\n    \"fence\",\n    \"festival\",\n    \"fetch\",\n    \"fever\",\n    \"few\",\n    \"fiber\",\n    \"fiction\",\n    \"field\",\n    \"figure\",\n    \"file\",\n    \"film\",\n    \"filter\",\n    \"final\",\n    \"find\",\n    \"fine\",\n    \"finger\",\n    \"finish\",\n    \"fire\",\n    \"firm\",\n    \"first\",\n    \"fiscal\",\n    \"fish\",\n    \"fit\",\n    \"fitness\",\n    \"fix\",\n    \"flag\",\n    \"flame\",\n    \"flash\",\n    \"flat\",\n    \"flavor\",\n    \"flee\",\n    \"flight\",\n    \"flip\",\n    \"float\",\n    \"flock\",\n    \"floor\",\n    \"flower\",\n    \"fluid\",\n    \"flush\",\n    \"fly\",\n    \"foam\",\n    \"focus\",\n    \"fog\",\n    \"foil\",\n    \"fold\",\n    \"follow\",\n    \"food\",\n    \"foot\",\n    \"force\",\n    \"forest\",\n    \"forget\",\n    \"fork\",\n    \"fortune\",\n    \"forum\",\n    \"forward\",\n    \"fossil\",\n    \"foster\",\n    \"found\",\n    \"fox\",\n    \"fragile\",\n    \"frame\",\n    \"frequent\",\n    \"fresh\",\n    \"friend\",\n    \"fringe\",\n    \"frog\",\n    \"front\",\n    \"frost\",\n    \"frown\",\n    \"frozen\",\n    \"fruit\",\n    \"fuel\",\n    \"fun\",\n    \"funny\",\n    \"furnace\",\n    \"fury\",\n    \"future\",\n    \"gadget\",\n    \"gain\",\n    \"galaxy\",\n    \"gallery\",\n    \"game\",\n    \"gap\",\n    \"garage\",\n    \"garbage\",\n    \"garden\",\n    \"garlic\",\n    \"garment\",\n    \"gas\",\n    \"gasp\",\n    \"gate\",\n    \"gather\",\n    \"gauge\",\n    \"gaze\",\n    \"general\",\n    \"genius\",\n    \"genre\",\n    \"gentle\",\n    \"genuine\",\n    \"gesture\",\n    \"ghost\",\n    \"giant\",\n    \"gift\",\n    \"giggle\",\n    \"ginger\",\n    \"giraffe\",\n    \"girl\",\n    \"give\",\n    \"glad\",\n    \"glance\",\n    \"glare\",\n    \"glass\",\n    \"glide\",\n    \"glimpse\",\n    \"globe\",\n    \"gloom\",\n    \"glory\",\n    \"glove\",\n    \"glow\",\n    \"glue\",\n    \"goat\",\n    \"goddess\",\n    \"gold\",\n    \"good\",\n    \"goose\",\n    \"gorilla\",\n    \"gospel\",\n    \"gossip\",\n    \"govern\",\n    \"gown\",\n    \"grab\",\n    \"grace\",\n    \"grain\",\n    \"grant\",\n    \"grape\",\n    \"grass\",\n    \"gravity\",\n    \"great\",\n    \"green\",\n    \"grid\",\n    \"grief\",\n    \"grit\",\n    \"grocery\",\n    \"group\",\n    \"grow\",\n    \"grunt\",\n    \"guard\",\n    \"guess\",\n    \"guide\",\n    \"guilt\",\n    \"guitar\",\n    \"gun\",\n    \"gym\",\n    \"habit\",\n    \"hair\",\n    \"half\",\n    \"hammer\",\n    \"hamster\",\n    \"hand\",\n    \"happy\",\n    \"harbor\",\n    \"hard\",\n    \"harsh\",\n    \"harvest\",\n    \"hat\",\n    \"have\",\n    \"hawk\",\n    \"hazard\",\n    \"head\",\n    \"health\",\n    \"heart\",\n    \"heavy\",\n    \"hedgehog\",\n    \"height\",\n    \"hello\",\n    \"helmet\",\n    \"help\",\n    \"hen\",\n    \"hero\",\n    \"hidden\",\n    \"high\",\n    \"hill\",\n    \"hint\",\n    \"hip\",\n    \"hire\",\n    \"history\",\n    \"hobby\",\n    \"hockey\",\n    \"hold\",\n    \"hole\",\n    \"holiday\",\n    \"hollow\",\n    \"home\",\n    \"honey\",\n    \"hood\",\n    \"hope\",\n    \"horn\",\n    \"horror\",\n    \"horse\",\n    \"hospital\",\n    \"host\",\n    \"hotel\",\n    \"hour\",\n    \"hover\",\n    \"hub\",\n    \"huge\",\n    \"human\",\n    \"humble\",\n    \"humor\",\n    \"hundred\",\n    \"hungry\",\n    \"hunt\",\n    \"hurdle\",\n    \"hurry\",\n    \"hurt\",\n    \"husband\",\n    \"hybrid\",\n    \"ice\",\n    \"icon\",\n    \"idea\",\n    \"identify\",\n    \"idle\",\n    \"ignore\",\n    \"ill\",\n    \"illegal\",\n    \"illness\",\n    \"image\",\n    \"imitate\",\n    \"immense\",\n    \"immune\",\n    \"impact\",\n    \"impose\",\n    \"improve\",\n    \"impulse\",\n    \"inch\",\n    \"include\",\n    \"income\",\n    \"increase\",\n    \"index\",\n    \"indicate\",\n    \"indoor\",\n    \"industry\",\n    \"infant\",\n    \"inflict\",\n    \"inform\",\n    \"inhale\",\n    \"inherit\",\n    \"initial\",\n    \"inject\",\n    \"injury\",\n    \"inmate\",\n    \"inner\",\n    \"innocent\",\n    \"input\",\n    \"inquiry\",\n    \"insane\",\n    \"insect\",\n    \"inside\",\n    \"inspire\",\n    \"install\",\n    \"intact\",\n    \"interest\",\n    \"into\",\n    \"invest\",\n    \"invite\",\n    \"involve\",\n    \"iron\",\n    \"island\",\n    \"isolate\",\n    \"issue\",\n    \"item\",\n    \"ivory\",\n    \"jacket\",\n    \"jaguar\",\n    \"jar\",\n    \"jazz\",\n    \"jealous\",\n    \"jeans\",\n    \"jelly\",\n    \"jewel\",\n    \"job\",\n    \"join\",\n    \"joke\",\n    \"journey\",\n    \"joy\",\n    \"judge\",\n    \"juice\",\n    \"jump\",\n    \"jungle\",\n    \"junior\",\n    \"junk\",\n    \"just\",\n    \"kangaroo\",\n    \"keen\",\n    \"keep\",\n    \"ketchup\",\n    \"key\",\n    \"kick\",\n    \"kid\",\n    \"kidney\",\n    \"kind\",\n    \"kingdom\",\n    \"kiss\",\n    \"kit\",\n    \"kitchen\",\n    \"kite\",\n    \"kitten\",\n    \"kiwi\",\n    \"knee\",\n    \"knife\",\n    \"knock\",\n    \"know\",\n    \"lab\",\n    \"label\",\n    \"labor\",\n    \"ladder\",\n    \"lady\",\n    \"lake\",\n    \"lamp\",\n    \"language\",\n    \"laptop\",\n    \"large\",\n    \"later\",\n    \"latin\",\n    \"laugh\",\n    \"laundry\",\n    \"lava\",\n    \"law\",\n    \"lawn\",\n    \"lawsuit\",\n    \"layer\",\n    \"lazy\",\n    \"leader\",\n    \"leaf\",\n    \"learn\",\n    \"leave\",\n    \"lecture\",\n    \"left\",\n    \"leg\",\n    \"legal\",\n    \"legend\",\n    \"leisure\",\n    \"lemon\",\n    \"lend\",\n    \"length\",\n    \"lens\",\n    \"leopard\",\n    \"lesson\",\n    \"letter\",\n    \"level\",\n    \"liar\",\n    \"liberty\",\n    \"library\",\n    \"license\",\n    \"life\",\n    \"lift\",\n    \"light\",\n    \"like\",\n    \"limb\",\n    \"limit\",\n    \"link\",\n    \"lion\",\n    \"liquid\",\n    \"list\",\n    \"little\",\n    \"live\",\n    \"lizard\",\n    \"load\",\n    \"loan\",\n    \"lobster\",\n    \"local\",\n    \"lock\",\n    \"logic\",\n    \"lonely\",\n    \"long\",\n    \"loop\",\n    \"lottery\",\n    \"loud\",\n    \"lounge\",\n    \"love\",\n    \"loyal\",\n    \"lucky\",\n    \"luggage\",\n    \"lumber\",\n    \"lunar\",\n    \"lunch\",\n    \"luxury\",\n    \"lyrics\",\n    \"machine\",\n    \"mad\",\n    \"magic\",\n    \"magnet\",\n    \"maid\",\n    \"mail\",\n    \"main\",\n    \"major\",\n    \"make\",\n    \"mammal\",\n    \"man\",\n    \"manage\",\n    \"mandate\",\n    \"mango\",\n    \"mansion\",\n    \"manual\",\n    \"maple\",\n    \"marble\",\n    \"march\",\n    \"margin\",\n    \"marine\",\n    \"market\",\n    \"marriage\",\n    \"mask\",\n    \"mass\",\n    \"master\",\n    \"match\",\n    \"material\",\n    \"math\",\n    \"matrix\",\n    \"matter\",\n    \"maximum\",\n    \"maze\",\n    \"meadow\",\n    \"mean\",\n    \"measure\",\n    \"meat\",\n    \"mechanic\",\n    \"medal\",\n    \"media\",\n    \"melody\",\n    \"melt\",\n    \"member\",\n    \"memory\",\n    \"mention\",\n    \"menu\",\n    \"mercy\",\n    \"merge\",\n    \"merit\",\n    \"merry\",\n    \"mesh\",\n    \"message\",\n    \"metal\",\n    \"method\",\n    \"middle\",\n    \"midnight\",\n    \"milk\",\n    \"million\",\n    \"mimic\",\n    \"mind\",\n    \"minimum\",\n    \"minor\",\n    \"minute\",\n    \"miracle\",\n    \"mirror\",\n    \"misery\",\n    \"miss\",\n    \"mistake\",\n    \"mix\",\n    \"mixed\",\n    \"mixture\",\n    \"mobile\",\n    \"model\",\n    \"modify\",\n    \"mom\",\n    \"moment\",\n    \"monitor\",\n    \"monkey\",\n    \"monster\",\n    \"month\",\n    \"moon\",\n    \"moral\",\n    \"more\",\n    \"morning\",\n    \"mosquito\",\n    \"mother\",\n    \"motion\",\n    \"motor\",\n    \"mountain\",\n    \"mouse\",\n    \"move\",\n    \"movie\",\n    \"much\",\n    \"muffin\",\n    \"mule\",\n    \"multiply\",\n    \"muscle\",\n    \"museum\",\n    \"mushroom\",\n    \"music\",\n    \"must\",\n    \"mutual\",\n    \"myself\",\n    \"mystery\",\n    \"myth\",\n    \"naive\",\n    \"name\",\n    \"napkin\",\n    \"narrow\",\n    \"nasty\",\n    \"nation\",\n    \"nature\",\n    \"near\",\n    \"neck\",\n    \"need\",\n    \"negative\",\n    \"neglect\",\n    \"neither\",\n    \"nephew\",\n    \"nerve\",\n    \"nest\",\n    \"net\",\n    \"network\",\n    \"neutral\",\n    \"never\",\n    \"news\",\n    \"next\",\n    \"nice\",\n    \"night\",\n    \"noble\",\n    \"noise\",\n    \"nominee\",\n    \"noodle\",\n    \"normal\",\n    \"north\",\n    \"nose\",\n    \"notable\",\n    \"note\",\n    \"nothing\",\n    \"notice\",\n    \"novel\",\n    \"now\",\n    \"nuclear\",\n    \"number\",\n    \"nurse\",\n    \"nut\",\n    \"oak\",\n    \"obey\",\n    \"object\",\n    \"oblige\",\n    \"obscure\",\n    \"observe\",\n    \"obtain\",\n    \"obvious\",\n    \"occur\",\n    \"ocean\",\n    \"october\",\n    \"odor\",\n    \"off\",\n    \"offer\",\n    \"office\",\n    \"often\",\n    \"oil\",\n    \"okay\",\n    \"old\",\n    \"olive\",\n    \"olympic\",\n    \"omit\",\n    \"once\",\n    \"one\",\n    \"onion\",\n    \"online\",\n    \"only\",\n    \"open\",\n    \"opera\",\n    \"opinion\",\n    \"oppose\",\n    \"option\",\n    \"orange\",\n    \"orbit\",\n    \"orchard\",\n    \"order\",\n    \"ordinary\",\n    \"organ\",\n    \"orient\",\n    \"original\",\n    \"orphan\",\n    \"ostrich\",\n    \"other\",\n    \"outdoor\",\n    \"outer\",\n    \"output\",\n    \"outside\",\n    \"oval\",\n    \"oven\",\n    \"over\",\n    \"own\",\n    \"owner\",\n    \"oxygen\",\n    \"oyster\",\n    \"ozone\",\n    \"pact\",\n    \"paddle\",\n    \"page\",\n    \"pair\",\n    \"palace\",\n    \"palm\",\n    \"panda\",\n    \"panel\",\n    \"panic\",\n    \"panther\",\n    \"paper\",\n    \"parade\",\n    \"parent\",\n    \"park\",\n    \"parrot\",\n    \"party\",\n    \"pass\",\n    \"patch\",\n    \"path\",\n    \"patient\",\n    \"patrol\",\n    \"pattern\",\n    \"pause\",\n    \"pave\",\n    \"payment\",\n    \"peace\",\n    \"peanut\",\n    \"pear\",\n    \"peasant\",\n    \"pelican\",\n    \"pen\",\n    \"penalty\",\n    \"pencil\",\n    \"people\",\n    \"pepper\",\n    \"perfect\",\n    \"permit\",\n    \"person\",\n    \"pet\",\n    \"phone\",\n    \"photo\",\n    \"phrase\",\n    \"physical\",\n    \"piano\",\n    \"picnic\",\n    \"picture\",\n    \"piece\",\n    \"pig\",\n    \"pigeon\",\n    \"pill\",\n    \"pilot\",\n    \"pink\",\n    \"pioneer\",\n    \"pipe\",\n    \"pistol\",\n    \"pitch\",\n    \"pizza\",\n    \"place\",\n    \"planet\",\n    \"plastic\",\n    \"plate\",\n    \"play\",\n    \"please\",\n    \"pledge\",\n    \"pluck\",\n    \"plug\",\n    \"plunge\",\n    \"poem\",\n    \"poet\",\n    \"point\",\n    \"polar\",\n    \"pole\",\n    \"police\",\n    \"pond\",\n    \"pony\",\n    \"pool\",\n    \"popular\",\n    \"portion\",\n    \"position\",\n    \"possible\",\n    \"post\",\n    \"potato\",\n    \"pottery\",\n    \"poverty\",\n    \"powder\",\n    \"power\",\n    \"practice\",\n    \"praise\",\n    \"predict\",\n    \"prefer\",\n    \"prepare\",\n    \"present\",\n    \"pretty\",\n    \"prevent\",\n    \"price\",\n    \"pride\",\n    \"primary\",\n    \"print\",\n    \"priority\",\n    \"prison\",\n    \"private\",\n    \"prize\",\n    \"problem\",\n    \"process\",\n    \"produce\",\n    \"profit\",\n    \"program\",\n    \"project\",\n    \"promote\",\n    \"proof\",\n    \"property\",\n    \"prosper\",\n    \"protect\",\n    \"proud\",\n    \"provide\",\n    \"public\",\n    \"pudding\",\n    \"pull\",\n    \"pulp\",\n    \"pulse\",\n    \"pumpkin\",\n    \"punch\",\n    \"pupil\",\n    \"puppy\",\n    \"purchase\",\n    \"purity\",\n    \"purpose\",\n    \"purse\",\n    \"push\",\n    \"put\",\n    \"puzzle\",\n    \"pyramid\",\n    \"quality\",\n    \"quantum\",\n    \"quarter\",\n    \"question\",\n    \"quick\",\n    \"quit\",\n    \"quiz\",\n    \"quote\",\n    \"rabbit\",\n    \"raccoon\",\n    \"race\",\n    \"rack\",\n    \"radar\",\n    \"radio\",\n    \"rail\",\n    \"rain\",\n    \"raise\",\n    \"rally\",\n    \"ramp\",\n    \"ranch\",\n    \"random\",\n    \"range\",\n    \"rapid\",\n    \"rare\",\n    \"rate\",\n    \"rather\",\n    \"raven\",\n    \"raw\",\n    \"razor\",\n    \"ready\",\n    \"real\",\n    \"reason\",\n    \"rebel\",\n    \"rebuild\",\n    \"recall\",\n    \"receive\",\n    \"recipe\",\n    \"record\",\n    \"recycle\",\n    \"reduce\",\n    \"reflect\",\n    \"reform\",\n    \"refuse\",\n    \"region\",\n    \"regret\",\n    \"regular\",\n    \"reject\",\n    \"relax\",\n    \"release\",\n    \"relief\",\n    \"rely\",\n    \"remain\",\n    \"remember\",\n    \"remind\",\n    \"remove\",\n    \"render\",\n    \"renew\",\n    \"rent\",\n    \"reopen\",\n    \"repair\",\n    \"repeat\",\n    \"replace\",\n    \"report\",\n    \"require\",\n    \"rescue\",\n    \"resemble\",\n    \"resist\",\n    \"resource\",\n    \"response\",\n    \"result\",\n    \"retire\",\n    \"retreat\",\n    \"return\",\n    \"reunion\",\n    \"reveal\",\n    \"review\",\n    \"reward\",\n    \"rhythm\",\n    \"rib\",\n    \"ribbon\",\n    \"rice\",\n    \"rich\",\n    \"ride\",\n    \"ridge\",\n    \"rifle\",\n    \"right\",\n    \"rigid\",\n    \"ring\",\n    \"riot\",\n    \"ripple\",\n    \"risk\",\n    \"ritual\",\n    \"rival\",\n    \"river\",\n    \"road\",\n    \"roast\",\n    \"robot\",\n    \"robust\",\n    \"rocket\",\n    \"romance\",\n    \"roof\",\n    \"rookie\",\n    \"room\",\n    \"rose\",\n    \"rotate\",\n    \"rough\",\n    \"round\",\n    \"route\",\n    \"royal\",\n    \"rubber\",\n    \"rude\",\n    \"rug\",\n    \"rule\",\n    \"run\",\n    \"runway\",\n    \"rural\",\n    \"sad\",\n    \"saddle\",\n    \"sadness\",\n    \"safe\",\n    \"sail\",\n    \"salad\",\n    \"salmon\",\n    \"salon\",\n    \"salt\",\n    \"salute\",\n    \"same\",\n    \"sample\",\n    \"sand\",\n    \"satisfy\",\n    \"satoshi\",\n    \"sauce\",\n    \"sausage\",\n    \"save\",\n    \"say\",\n    \"scale\",\n    \"scan\",\n    \"scare\",\n    \"scatter\",\n    \"scene\",\n    \"scheme\",\n    \"school\",\n    \"science\",\n    \"scissors\",\n    \"scorpion\",\n    \"scout\",\n    \"scrap\",\n    \"screen\",\n    \"script\",\n    \"scrub\",\n    \"sea\",\n    \"search\",\n    \"season\",\n    \"seat\",\n    \"second\",\n    \"secret\",\n    \"section\",\n    \"security\",\n    \"seed\",\n    \"seek\",\n    \"segment\",\n    \"select\",\n    \"sell\",\n    \"seminar\",\n    \"senior\",\n    \"sense\",\n    \"sentence\",\n    \"series\",\n    \"service\",\n    \"session\",\n    \"settle\",\n    \"setup\",\n    \"seven\",\n    \"shadow\",\n    \"shaft\",\n    \"shallow\",\n    \"share\",\n    \"shed\",\n    \"shell\",\n    \"sheriff\",\n    \"shield\",\n    \"shift\",\n    \"shine\",\n    \"ship\",\n    \"shiver\",\n    \"shock\",\n    \"shoe\",\n    \"shoot\",\n    \"shop\",\n    \"short\",\n    \"shoulder\",\n    \"shove\",\n    \"shrimp\",\n    \"shrug\",\n    \"shuffle\",\n    \"shy\",\n    \"sibling\",\n    \"sick\",\n    \"side\",\n    \"siege\",\n    \"sight\",\n    \"sign\",\n    \"silent\",\n    \"silk\",\n    \"silly\",\n    \"silver\",\n    \"similar\",\n    \"simple\",\n    \"since\",\n    \"sing\",\n    \"siren\",\n    \"sister\",\n    \"situate\",\n    \"six\",\n    \"size\",\n    \"skate\",\n    \"sketch\",\n    \"ski\",\n    \"skill\",\n    \"skin\",\n    \"skirt\",\n    \"skull\",\n    \"slab\",\n    \"slam\",\n    \"sleep\",\n    \"slender\",\n    \"slice\",\n    \"slide\",\n    \"slight\",\n    \"slim\",\n    \"slogan\",\n    \"slot\",\n    \"slow\",\n    \"slush\",\n    \"small\",\n    \"smart\",\n    \"smile\",\n    \"smoke\",\n    \"smooth\",\n    \"snack\",\n    \"snake\",\n    \"snap\",\n    \"sniff\",\n    \"snow\",\n    \"soap\",\n    \"soccer\",\n    \"social\",\n    \"sock\",\n    \"soda\",\n    \"soft\",\n    \"solar\",\n    \"soldier\",\n    \"solid\",\n    \"solution\",\n    \"solve\",\n    \"someone\",\n    \"song\",\n    \"soon\",\n    \"sorry\",\n    \"sort\",\n    \"soul\",\n    \"sound\",\n    \"soup\",\n    \"source\",\n    \"south\",\n    \"space\",\n    \"spare\",\n    \"spatial\",\n    \"spawn\",\n    \"speak\",\n    \"special\",\n    \"speed\",\n    \"spell\",\n    \"spend\",\n    \"sphere\",\n    \"spice\",\n    \"spider\",\n    \"spike\",\n    \"spin\",\n    \"spirit\",\n    \"split\",\n    \"spoil\",\n    \"sponsor\",\n    \"spoon\",\n    \"sport\",\n    \"spot\",\n    \"spray\",\n    \"spread\",\n    \"spring\",\n    \"spy\",\n    \"square\",\n    \"squeeze\",\n    \"squirrel\",\n    \"stable\",\n    \"stadium\",\n    \"staff\",\n    \"stage\",\n    \"stairs\",\n    \"stamp\",\n    \"stand\",\n    \"start\",\n    \"state\",\n    \"stay\",\n    \"steak\",\n    \"steel\",\n    \"stem\",\n    \"step\",\n    \"stereo\",\n    \"stick\",\n    \"still\",\n    \"sting\",\n    \"stock\",\n    \"stomach\",\n    \"stone\",\n    \"stool\",\n    \"story\",\n    \"stove\",\n    \"strategy\",\n    \"street\",\n    \"strike\",\n    \"strong\",\n    \"struggle\",\n    \"student\",\n    \"stuff\",\n    \"stumble\",\n    \"style\",\n    \"subject\",\n    \"submit\",\n    \"subway\",\n    \"success\",\n    \"such\",\n    \"sudden\",\n    \"suffer\",\n    \"sugar\",\n    \"suggest\",\n    \"suit\",\n    \"summer\",\n    \"sun\",\n    \"sunny\",\n    \"sunset\",\n    \"super\",\n    \"supply\",\n    \"supreme\",\n    \"sure\",\n    \"surface\",\n    \"surge\",\n    \"surprise\",\n    \"surround\",\n    \"survey\",\n    \"suspect\",\n    \"sustain\",\n    \"swallow\",\n    \"swamp\",\n    \"swap\",\n    \"swarm\",\n    \"swear\",\n    \"sweet\",\n    \"swift\",\n    \"swim\",\n    \"swing\",\n    \"switch\",\n    \"sword\",\n    \"symbol\",\n    \"symptom\",\n    \"syrup\",\n    \"system\",\n    \"table\",\n    \"tackle\",\n    \"tag\",\n    \"tail\",\n    \"talent\",\n    \"talk\",\n    \"tank\",\n    \"tape\",\n    \"target\",\n    \"task\",\n    \"taste\",\n    \"tattoo\",\n    \"taxi\",\n    \"teach\",\n    \"team\",\n    \"tell\",\n    \"ten\",\n    \"tenant\",\n    \"tennis\",\n    \"tent\",\n    \"term\",\n    \"test\",\n    \"text\",\n    \"thank\",\n    \"that\",\n    \"theme\",\n    \"then\",\n    \"theory\",\n    \"there\",\n    \"they\",\n    \"thing\",\n    \"this\",\n    \"thought\",\n    \"three\",\n    \"thrive\",\n    \"throw\",\n    \"thumb\",\n    \"thunder\",\n    \"ticket\",\n    \"tide\",\n    \"tiger\",\n    \"tilt\",\n    \"timber\",\n    \"time\",\n    \"tiny\",\n    \"tip\",\n    \"tired\",\n    \"tissue\",\n    \"title\",\n    \"toast\",\n    \"tobacco\",\n    \"today\",\n    \"toddler\",\n    \"toe\",\n    \"together\",\n    \"toilet\",\n    \"token\",\n    \"tomato\",\n    \"tomorrow\",\n    \"tone\",\n    \"tongue\",\n    \"tonight\",\n    \"tool\",\n    \"tooth\",\n    \"top\",\n    \"topic\",\n    \"topple\",\n    \"torch\",\n    \"tornado\",\n    \"tortoise\",\n    \"toss\",\n    \"total\",\n    \"tourist\",\n    \"toward\",\n    \"tower\",\n    \"town\",\n    \"toy\",\n    \"track\",\n    \"trade\",\n    \"traffic\",\n    \"tragic\",\n    \"train\",\n    \"transfer\",\n    \"trap\",\n    \"trash\",\n    \"travel\",\n    \"tray\",\n    \"treat\",\n    \"tree\",\n    \"trend\",\n    \"trial\",\n    \"tribe\",\n    \"trick\",\n    \"trigger\",\n    \"trim\",\n    \"trip\",\n    \"trophy\",\n    \"trouble\",\n    \"truck\",\n    \"true\",\n    \"truly\",\n    \"trumpet\",\n    \"trust\",\n    \"truth\",\n    \"try\",\n    \"tube\",\n    \"tuition\",\n    \"tumble\",\n    \"tuna\",\n    \"tunnel\",\n    \"turkey\",\n    \"turn\",\n    \"turtle\",\n    \"twelve\",\n    \"twenty\",\n    \"twice\",\n    \"twin\",\n    \"twist\",\n    \"two\",\n    \"type\",\n    \"typical\",\n    \"ugly\",\n    \"umbrella\",\n    \"unable\",\n    \"unaware\",\n    \"uncle\",\n    \"uncover\",\n    \"under\",\n    \"undo\",\n    \"unfair\",\n    \"unfold\",\n    \"unhappy\",\n    \"uniform\",\n    \"unique\",\n    \"unit\",\n    \"universe\",\n    \"unknown\",\n    \"unlock\",\n    \"until\",\n    \"unusual\",\n    \"unveil\",\n    \"update\",\n    \"upgrade\",\n    \"uphold\",\n    \"upon\",\n    \"upper\",\n    \"upset\",\n    \"urban\",\n    \"urge\",\n    \"usage\",\n    \"use\",\n    \"used\",\n    \"useful\",\n    \"useless\",\n    \"usual\",\n    \"utility\",\n    \"vacant\",\n    \"vacuum\",\n    \"vague\",\n    \"valid\",\n    \"valley\",\n    \"valve\",\n    \"van\",\n    \"vanish\",\n    \"vapor\",\n    \"various\",\n    \"vast\",\n    \"vault\",\n    \"vehicle\",\n    \"velvet\",\n    \"vendor\",\n    \"venture\",\n    \"venue\",\n    \"verb\",\n    \"verify\",\n    \"version\",\n    \"very\",\n    \"vessel\",\n    \"veteran\",\n    \"viable\",\n    \"vibrant\",\n    \"vicious\",\n    \"victory\",\n    \"video\",\n    \"view\",\n    \"village\",\n    \"vintage\",\n    \"violin\",\n    \"virtual\",\n    \"virus\",\n    \"visa\",\n    \"visit\",\n    \"visual\",\n    \"vital\",\n    \"vivid\",\n    \"vocal\",\n    \"voice\",\n    \"void\",\n    \"volcano\",\n    \"volume\",\n    \"vote\",\n    \"voyage\",\n    \"wage\",\n    \"wagon\",\n    \"wait\",\n    \"walk\",\n    \"wall\",\n    \"walnut\",\n    \"want\",\n    \"warfare\",\n    \"warm\",\n    \"warrior\",\n    \"wash\",\n    \"wasp\",\n    \"waste\",\n    \"water\",\n    \"wave\",\n    \"way\",\n    \"wealth\",\n    \"weapon\",\n    \"wear\",\n    \"weasel\",\n    \"weather\",\n    \"web\",\n    \"wedding\",\n    \"weekend\",\n    \"weird\",\n    \"welcome\",\n    \"west\",\n    \"wet\",\n    \"whale\",\n    \"what\",\n    \"wheat\",\n    \"wheel\",\n    \"when\",\n    \"where\",\n    \"whip\",\n    \"whisper\",\n    \"wide\",\n    \"width\",\n    \"wife\",\n    \"wild\",\n    \"will\",\n    \"win\",\n    \"window\",\n    \"wine\",\n    \"wing\",\n    \"wink\",\n    \"winner\",\n    \"winter\",\n    \"wire\",\n    \"wisdom\",\n    \"wise\",\n    \"wish\",\n    \"witness\",\n    \"wolf\",\n    \"woman\",\n    \"wonder\",\n    \"wood\",\n    \"wool\",\n    \"word\",\n    \"work\",\n    \"world\",\n    \"worry\",\n    \"worth\",\n    \"wrap\",\n    \"wreck\",\n    \"wrestle\",\n    \"wrist\",\n    \"write\",\n    \"wrong\",\n    \"yard\",\n    \"year\",\n    \"yellow\",\n    \"you\",\n    \"young\",\n    \"youth\",\n    \"zebra\",\n    \"zero\",\n    \"zone\",\n    \"zoo\"\n]);\n"
  },
  {
    "path": "src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n/// <reference types=\"vite-plugin-comlink/client\" />\n"
  },
  {
    "path": "src/workers/walletWorker.ts",
    "content": "import initMutinyWallet, {\n    ActivityItem,\n    BudgetPeriod,\n    ChannelClosure,\n    FederationBalance,\n    FederationBalances,\n    FedimintSweepResult,\n    LnUrlParams,\n    MutinyBalance,\n    MutinyBip21RawMaterials,\n    MutinyChannel,\n    MutinyInvoice,\n    MutinyPeer,\n    MutinyWallet,\n    NwcProfile,\n    PaymentParams,\n    PendingNwcInvoice,\n    TagItem\n} from \"@mutinywallet/mutiny-wasm\";\n\nimport { IActivityItem } from \"~/components\";\nimport { MutinyWalletSettingStrings } from \"~/logic/mutinyWalletSetup\";\nimport { FakeDirectMessage, OnChainTx } from \"~/routes\";\nimport {\n    DiscoveredFederation,\n    MutinyFederationIdentity,\n    ResyncProgress\n} from \"~/routes/settings\";\n\n// For some reason {...invoice } doesn't bring across the paid field\nfunction destructureInvoice(invoice: MutinyInvoice): MutinyInvoice {\n    return {\n        amount_sats: invoice.amount_sats,\n        bolt11: invoice.bolt11,\n        description: invoice.description,\n        expire: invoice.expire,\n        expired: invoice.expired,\n        fees_paid: invoice.fees_paid,\n        inbound: invoice.inbound,\n        labels: invoice.labels,\n        last_updated: invoice.last_updated,\n        paid: invoice.paid,\n        payee_pubkey: invoice.payee_pubkey,\n        payment_hash: invoice.payment_hash,\n        potential_hodl_invoice: invoice.potential_hodl_invoice,\n        preimage: invoice.preimage,\n        privacy_level: invoice.privacy_level,\n        status: invoice.status\n    } as MutinyInvoice;\n}\n\nlet wallet: MutinyWallet | undefined;\nexport let wasm_initialized = false;\nexport let wallet_initialized = false;\n\nexport async function checkForWasm() {\n    try {\n        if (\n            typeof WebAssembly === \"object\" &&\n            typeof WebAssembly.instantiate === \"function\"\n        ) {\n            const module = new WebAssembly.Module(\n                Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)\n            );\n            if (!(module instanceof WebAssembly.Module)) {\n                throw new Error(\"Couldn't instantiate WASM Module\");\n            }\n        } else {\n            throw new Error(\"No WebAssembly global object found\");\n        }\n    } catch (e) {\n        console.error(e);\n    }\n}\n\nexport async function initializeWasm() {\n    // Actually intialize the WASM, this should be the first thing that requires the WASM blob to be downloaded\n\n    // If WASM is already initialized, don't init twice\n    try {\n        const _sats_the_standard = MutinyWallet.convert_btc_to_sats(1);\n        console.debug(\"MutinyWallet WASM already initialized, skipping init\");\n        wasm_initialized = true;\n        return;\n    } catch (e) {\n        console.debug(\"MutinyWallet WASM about to be initialized\");\n        await initMutinyWallet();\n        console.debug(\"MutinyWallet WASM initialized\");\n    }\n}\n\nexport async function setupMutinyWallet(\n    settings: MutinyWalletSettingStrings,\n    password?: string,\n    safeMode?: boolean,\n    shouldZapHodl?: boolean,\n    nsec?: string\n): Promise<boolean> {\n    console.log(\"Starting setup...\");\n\n    const {\n        network,\n        proxy,\n        esplora,\n        rgs,\n        lsp,\n        lsps_connection_string,\n        lsps_token,\n        auth,\n        subscriptions,\n        storage,\n        scorer,\n        primal_api,\n        blind_auth,\n        hermes\n    } = settings;\n\n    console.log(\"Initializing Mutiny Manager\");\n    console.log(\"Using network\", network);\n    console.log(\"Using proxy\", proxy);\n    console.log(\"Using esplora address\", esplora);\n    console.log(\"Using rgs address\", rgs);\n    console.log(\"Using lsp address\", lsp);\n    console.log(\"Using lsp connection string\", lsps_connection_string);\n    console.log(\"Using lsp token\", lsps_token);\n    console.log(\"Using auth address\", auth);\n    console.log(\"Using subscriptions address\", subscriptions);\n    console.log(\"Using storage address\", storage);\n    console.log(\"Using scorer address\", scorer);\n    console.log(\"Using primal api\", primal_api);\n    console.log(\"Using blind auth\", blind_auth);\n    console.log(\"Using hermes\", hermes);\n    console.log(safeMode ? \"Safe mode enabled\" : \"Safe mode disabled\");\n    console.log(shouldZapHodl ? \"Hodl zaps enabled\" : \"Hodl zaps disabled\");\n\n    // Only use lsps if there's no lsp set\n    const shouldUseLSPS = !lsp && lsps_connection_string && lsps_token;\n\n    const mutinyWallet = await MutinyWallet.new(\n        // Password\n        password ? password : undefined,\n        // Mnemonic\n        undefined,\n        proxy,\n        network,\n        esplora,\n        rgs,\n        shouldUseLSPS ? undefined : lsp,\n        shouldUseLSPS ? lsps_connection_string : undefined,\n        shouldUseLSPS ? lsps_token : undefined,\n        auth,\n        subscriptions,\n        storage,\n        scorer,\n        // Do not connect peers\n        undefined,\n        // Do not skip device lock\n        undefined,\n        // Safe mode\n        safeMode || undefined,\n        // Skip hodl invoices? (defaults to true, so if shouldZapHodl is true that's when we pass false)\n        shouldZapHodl ? false : undefined,\n        // Nsec override\n        nsec,\n        // Nip7 (not supported in web worker)\n        undefined,\n        // primal URL\n        primal_api || \"https://primal-cache.mutinywallet.com/api\",\n        /// blind auth url\n        blind_auth,\n        /// hermes url\n        hermes\n    );\n\n    wallet = mutinyWallet;\n    wallet_initialized = true;\n\n    return true;\n}\n\n/**\n * Gets the current balance of the wallet.\n * This includes both on-chain and lightning funds.\n *\n * This will not include any funds in an unconfirmed lightning channel.\n * @returns {Promise<MutinyBalance>}\n */\nexport async function get_balance(): Promise<MutinyBalance> {\n    const balance = await wallet!.get_balance();\n    return {\n        federation: balance.federation,\n        lightning: balance.lightning,\n        confirmed: balance.confirmed,\n        unconfirmed: balance.unconfirmed,\n        force_close: balance.force_close\n    } as MutinyBalance;\n}\n\n/**\n * Lists the federation id's of the federation clients in the manager.\n * @returns {Promise<any>}\n */\nexport async function list_federations(): Promise<MutinyFederationIdentity[]> {\n    const federations = await wallet!.list_federations();\n    return federations as MutinyFederationIdentity[];\n}\n\n/**\n * Checks whether or not the user is subscribed to Mutiny+.\n * Submits a NWC string to keep the subscription active if not expired.\n *\n * Returns None if there's no subscription at all.\n * Returns Some(u64) for their unix expiration timestamp, which may be in the\n * past or in the future, depending on whether or not it is currently active.\n * @returns {Promise<bigint | undefined>}\n */\nexport async function check_subscribed(): Promise<bigint | undefined> {\n    const subscribed = await wallet!.check_subscribed();\n    return subscribed;\n}\n\n/**\n * Stops all of the nodes and background processes.\n * Returns after node has been stopped.\n * @returns {Promise<void>}\n */\nexport async function stop(): Promise<void> {\n    await wallet!.stop();\n}\n\n/**\n * Clears storage and deletes all data.\n *\n * All data in VSS persists but the device lock is cleared.\n * @returns {Promise<void>}\n */\nexport async function delete_all(): Promise<void> {\n    await wallet!.delete_all();\n}\n\n/**\n * Gets the current bitcoin price in chosen Fiat.\n * @param {string | undefined} [fiat]\n * @returns {Promise<number>}\n */\nexport async function get_bitcoin_price(fiat: string): Promise<number> {\n    const price = await wallet!.get_bitcoin_price(fiat);\n    return price;\n}\n\nexport async function get_tag_items(): Promise<TagItem[]> {\n    const tagItems = await wallet!.get_tag_items();\n    return tagItems;\n}\n\n/**\n * Returns the network of the wallet.\n * @returns {string}\n */\nexport async function get_network(): Promise<string> {\n    const network = await wallet!.get_network();\n    return network || \"signet\";\n}\n\n/**\n * Returns the user's nostr profile data\n * @returns {any}\n */\nexport async function get_nostr_profile(): Promise<NostrMetadata | undefined> {\n    if (wallet) {\n        const profile = wallet.get_nostr_profile();\n        return { ...profile };\n    } else {\n        return undefined;\n    }\n}\n\n/**\n * Returns all the on-chain and lightning activity from the wallet.\n * @param {number | undefined} [limit]\n * @param {number | undefined} [offset]\n */\nexport async function get_activity(\n    limit: number,\n    offset?: number\n): Promise<IActivityItem[]> {\n    const activity = await wallet!.get_activity(limit, offset);\n    return activity;\n}\n\nexport async function get_contact_for_npub(\n    npub: string\n): Promise<TagItem | undefined> {\n    const contact = await wallet!.get_contact_for_npub(npub);\n    if (!contact) return undefined;\n    return { ...contact?.value };\n}\n\nexport async function create_new_contact(\n    name: string,\n    npub?: string,\n    ln_address?: string,\n    lnurl?: string,\n    image_url?: string\n): Promise<string> {\n    const contactId = await wallet!.create_new_contact(\n        name,\n        npub,\n        ln_address,\n        lnurl,\n        image_url\n    );\n    return contactId;\n}\n\nexport async function get_tag_item(id: string): Promise<TagItem | undefined> {\n    const tagItem = await wallet!.get_tag_item(id);\n    if (!tagItem) return undefined;\n    return { ...tagItem?.value };\n}\n\n/**\n * Returns all the on-chain and lightning activity for a given label\n * @param {string} label\n * @returns {Promise<any>}\n */\nexport async function get_label_activity(\n    label: string\n): Promise<IActivityItem[]> {\n    const activity = await wallet!.get_label_activity(label);\n    return activity;\n}\n\n/**\n * Get dm conversation between us and given npub\n * Returns a vector of messages sorted by newest first\n * @param {string} npub\n * @param {bigint} limit\n * @param {bigint | undefined} [until]\n * @param {bigint | undefined} [since]\n * @returns {Promise<any>}\n */\nexport async function get_dm_conversation(\n    npub: string,\n    limit: bigint,\n    until?: bigint,\n    since?: bigint\n): Promise<FakeDirectMessage[] | undefined> {\n    const dms = await wallet!.get_dm_conversation(npub, limit, until, since);\n    return [...dms];\n}\n\n/**\n * Gets an invoice from the node manager.\n * This includes sent and received invoices.\n * @param {string} invoice\n * @returns {Promise<MutinyInvoice>}\n */\nexport async function get_invoice(\n    bolt11: string\n): Promise<MutinyInvoice | undefined> {\n    const invoice = await wallet!.get_invoice(bolt11);\n    if (!invoice) return undefined;\n    // For some reason {...invoice } doesn't bring across the paid field\n    return destructureInvoice(invoice);\n}\n\n/**\n * Gets all contacts sorted by last used\n * @returns {Promise<any>}\n */\nexport async function get_contacts_sorted(limit?: number): Promise<TagItem[]> {\n    const contacts = await wallet!.get_contacts_sorted();\n    if (!contacts) return [];\n    if (contacts.length && limit) {\n        return contacts.slice(0, limit);\n    } else {\n        return contacts;\n    }\n}\n\nexport async function edit_contact(\n    id: string,\n    name: string,\n    npub?: string,\n    ln_address?: string,\n    lnurl?: string,\n    image_url?: string\n): Promise<void> {\n    await wallet!.edit_contact(id, name, npub, ln_address, lnurl, image_url);\n}\n\nexport async function delete_contact(id: string): Promise<void> {\n    await wallet!.delete_contact(id);\n}\n\n/**\n * Follows the npub on nostr if we're not already following\n * @param {string} npub\n * @returns {Promise<void>}\n */\nexport async function follow_npub(npub: string): Promise<void> {\n    await wallet!.follow_npub(npub);\n}\n\n/**\n * Unfollows the npub on nostr if we're following them\n *\n * Returns true if we were following them before\n * @param {string} npub\n * @returns {Promise<void>}\n */\nexport async function unfollow_npub(npub: string): Promise<void> {\n    await wallet!.unfollow_npub(npub);\n}\n\n/**\n * Sends a DM to the given npub\n * @param {string} npub\n * @param {string} message\n * @returns {Promise<string>}\n */\nexport async function send_dm(\n    npub: string,\n    message: string\n): Promise<string | undefined> {\n    const result = await wallet!.send_dm(npub, message);\n    return result;\n}\n\n/**\n * Returns the user's npub\n * @returns {Promise<string>}\n */\nexport async function get_npub(): Promise<string | undefined> {\n    const npub = await wallet!.get_npub();\n    return npub;\n}\n\n/**\n * Decodes a lightning invoice into useful information.\n * Will return an error if the invoice is for a different network.\n * @param {string} invoice\n * @param {string | undefined} [network]\n * @returns {Promise<MutinyInvoice>}\n */\nexport async function decode_invoice(\n    invoice: string,\n    network?: string\n): Promise<MutinyInvoice | undefined> {\n    const decoded = await wallet!.decode_invoice(invoice, network);\n    if (!decoded) return undefined;\n    return destructureInvoice(decoded);\n}\n\n/**\n * Creates a BIP 21 invoice. This creates a new address and a lightning invoice.\n * The lightning invoice may return errors related to the LSP. Check the error and\n * fallback to `get_new_address` and warn the user that Lightning is not available.\n *\n *\n * Errors that might be returned include:\n *\n * - [`MutinyJsError::LspGenericError`]: This is returned for various reasons, including if a\n *   request to the LSP server fails for any reason, or if the server returns\n *   a status other than 500 that can't be parsed into a `ProposalResponse`.\n *\n * - [`MutinyJsError::LspFundingError`]: Returned if the LSP server returns an error with\n *   a status of 500, indicating an \"Internal Server Error\", and a message\n *   stating \"Cannot fund new channel at this time\". This means that the LSP cannot support\n *   a new channel at this time.\n *\n * - [`MutinyJsError::LspAmountTooHighError`]: Returned if the LSP server returns an error with\n *   a status of 500, indicating an \"Internal Server Error\", and a message stating \"Invoice\n *   amount is too high\". This means that the LSP cannot support the amount that the user\n *   requested. The user should request a smaller amount from the LSP.\n *\n * - [`MutinyJsError::LspConnectionError`]: Returned if the LSP server returns an error with\n *   a status of 500, indicating an \"Internal Server Error\", and a message that starts with\n *   \"Failed to connect to peer\". This means that the LSP is not connected to our node.\n *\n * If the server returns a status of 500 with a different error message,\n * a [`MutinyJsError::LspGenericError`] is returned.\n * @param {bigint | undefined} amount\n * @param {(string)[]} labels\n * @returns {Promise<MutinyBip21RawMaterials>}\n */\nexport async function create_bip21(\n    amount: bigint | undefined,\n    labels: string[]\n): Promise<MutinyBip21RawMaterials> {\n    const mbrw = await wallet!.create_bip21(amount, labels);\n    return {\n        ...mbrw?.value\n    } as MutinyBip21RawMaterials;\n}\n\n/**\n * Creates a lightning invoice. The amount should be in satoshis.\n * If no amount is provided, the invoice will be created with no amount.\n * If no description is provided, the invoice will be created with no description.\n *\n * If the manager has more than one node it will create a phantom invoice.\n * If there is only one node it will create an invoice just for that node.\n * @param {bigint} amount\n * @param {(string)[]} labels\n * @returns {Promise<MutinyInvoice>}\n */\nexport async function create_invoice(\n    amount: bigint,\n    labels: string[]\n): Promise<MutinyInvoice | undefined> {\n    const invoice = await wallet!.create_invoice(amount, labels);\n    if (!invoice) return undefined;\n    return destructureInvoice(invoice);\n}\n\n/**\n * Estimates the onchain fee for a transaction sweep our on-chain balance\n * to the given address.\n *\n * The fee rate is in sat/vbyte.\n * @param {string} destination_address\n * @param {number | undefined} [fee_rate]\n * @returns {bigint}\n */\nexport async function estimate_sweep_tx_fee(\n    address: string\n): Promise<bigint | undefined> {\n    const fee = await wallet!.estimate_sweep_tx_fee(address);\n    return fee;\n}\n\n/**\n * Estimates the onchain fee for a transaction sending to the given address.\n * The amount is in satoshis and the fee rate is in sat/vbyte.\n * @param {string} destination_address\n * @param {bigint} amount\n * @param {number | undefined} [fee_rate]\n * @returns {bigint}\n */\nexport async function estimate_tx_fee(\n    address: string,\n    amount: bigint,\n    feeRate?: number\n): Promise<bigint | undefined> {\n    const fee = await wallet!.estimate_tx_fee(address, amount, feeRate);\n    return fee;\n}\n\n/**\n * Calls upon a LNURL to get the parameters for it.\n * This contains what kind of LNURL it is (pay, withdrawal, auth, etc).\n * @param {string} lnurl\n * @returns {Promise<LnUrlParams>}\n */\nexport async function decode_lnurl(lnurl: string): Promise<LnUrlParams> {\n    const lnurlParams = await wallet!.decode_lnurl(lnurl);\n    // PAIN: this is supposed to be returning bigints, but it returns numbers instead\n    return {\n        ...lnurlParams?.value\n    } as LnUrlParams;\n}\n\n/**\n * Pays a lightning invoice from the selected node.\n * An amount should only be provided if the invoice does not have an amount.\n * The amount should be in satoshis.\n * @param {string} invoice_str\n * @param {bigint | undefined} amt_sats\n * @param {(string)[]} labels\n * @returns {Promise<MutinyInvoice>}\n */\nexport async function pay_invoice(\n    invoice_str: string,\n    amt_sats: bigint | undefined,\n    labels: string[]\n): Promise<MutinyInvoice | undefined> {\n    const invoice = await wallet!.pay_invoice(invoice_str, amt_sats, labels);\n    if (!invoice) return undefined;\n    return destructureInvoice(invoice);\n}\n\n/**\n * Calls upon a LNURL and pays it.\n * This will fail if the LNURL is not a LNURL pay.\n * @param {string} lnurl\n * @param {bigint} amount_sats\n * @param {string | undefined} zap_npub\n * @param {(string)[]} labels\n * @param {string | undefined} [comment]\n * @param {string | undefined} [privacy_level]\n * @returns {Promise<MutinyInvoice>}\n */\nexport async function lnurl_pay(\n    lnurl: string,\n    amount_sats: bigint,\n    zap_npub: string | undefined,\n    labels: string[],\n    comment?: string,\n    privacy_level?: string\n): Promise<MutinyInvoice | undefined> {\n    const invoice = await wallet!.lnurl_pay(\n        lnurl,\n        amount_sats,\n        zap_npub,\n        labels,\n        comment,\n        privacy_level\n    );\n    if (!invoice) return undefined;\n    return destructureInvoice(invoice);\n}\n\n/**\n * Sweeps all the funds from the wallet to the given address.\n * The fee rate is in sat/vbyte.\n *\n * If a fee rate is not provided, one will be used from the fee estimator.\n * @param {string} destination_address\n * @param {(string)[]} labels\n * @param {number | undefined} [fee_rate]\n * @returns {Promise<string>}\n */\nexport async function sweep_wallet(\n    destination_address: string,\n    labels: string[],\n    fee_rate?: number\n): Promise<string | undefined> {\n    const payment = await wallet!.sweep_wallet(\n        destination_address,\n        labels,\n        fee_rate\n    );\n    return payment;\n}\n\nexport async function send_payjoin(\n    payjoin_uri: string,\n    amount: bigint,\n    labels: string[],\n    fee_rate?: number\n): Promise<string | undefined> {\n    const payment = await wallet!.send_payjoin(\n        payjoin_uri,\n        amount,\n        labels,\n        fee_rate\n    );\n    return payment;\n}\n\n/**\n * Sends an on-chain transaction to the given address.\n * The amount is in satoshis and the fee rate is in sat/vbyte.\n *\n * If a fee rate is not provided, one will be used from the fee estimator.\n * @param {string} destination_address\n * @param {bigint} amount\n * @param {(string)[]} labels\n * @param {number | undefined} [fee_rate]\n * @returns {Promise<string>}\n */\nexport async function send_to_address(\n    destination_address: string,\n    amount: bigint,\n    labels: string[],\n    fee_rate?: number\n): Promise<string | undefined> {\n    const payment = await wallet!.send_to_address(\n        destination_address,\n        amount,\n        labels,\n        fee_rate\n    );\n    return payment;\n}\n\n/**\n * Sends a spontaneous payment to a node from the selected node.\n * The amount should be in satoshis.\n * @param {string} to_node\n * @param {bigint} amt_sats\n * @param {string | undefined} message\n * @param {(string)[]} labels\n * @returns {Promise<MutinyInvoice>}\n */\nexport async function keysend(\n    to_node: string,\n    amt_sats: bigint,\n    message: string | undefined,\n    labels: string[]\n): Promise<MutinyInvoice | undefined> {\n    const invoice = await wallet!.keysend(to_node, amt_sats, message, labels);\n    if (!invoice) return undefined;\n    return destructureInvoice(invoice);\n}\n\n/**\n * Gets an invoice from the node manager.\n * This includes sent and received invoices.\n * @param {string} hash\n * @returns {Promise<MutinyInvoice>}\n */\nexport async function get_invoice_by_hash(\n    hash: string\n): Promise<MutinyInvoice> {\n    const invoice = await wallet!.get_invoice_by_hash(hash);\n    return destructureInvoice(invoice);\n}\n\n/**\n * Gets an channel closure from the node manager.\n * @param {string} user_channel_id\n * @returns {Promise<ChannelClosure>}\n */\nexport async function get_channel_closure(\n    user_channel_id: string\n): Promise<ChannelClosure> {\n    const channel_closure = await wallet!.get_channel_closure(user_channel_id);\n    return {\n        channel_id: channel_closure.channel_id,\n        node_id: channel_closure.node_id,\n        reason: channel_closure.reason,\n        timestamp: channel_closure.timestamp\n    } as ChannelClosure;\n}\n\n/**\n * Gets the details of a specific on-chain transaction.\n * @param {string} txid\n * @returns {any}\n */\nexport async function get_transaction(txid: string): Promise<ActivityItem> {\n    // TODO: this is an ActivityItem right?\n    const transaction = await wallet!.get_transaction(txid);\n    return transaction as ActivityItem;\n}\n\n/**\n * Gets a new bitcoin address from the wallet.\n * Will generate a new address on every call.\n *\n * It is recommended to create a new address for every transaction.\n * @param {(string)[]} labels\n * @returns {MutinyBip21RawMaterials}\n */\nexport async function get_new_address(\n    labels: string[]\n): Promise<MutinyBip21RawMaterials> {\n    const mbrw = await wallet!.get_new_address(labels);\n    return {\n        ...mbrw?.value\n    } as MutinyBip21RawMaterials;\n}\n\n/**\n * Checks if the given address has any transactions.\n * If it does, it returns the details of the first transaction.\n *\n * This should be used to check if a payment has been made to an address.\n * @param {string} address\n * @returns {Promise<any>}\n */\nexport async function check_address(address: string): Promise<OnChainTx> {\n    const tx = await wallet!.check_address(address);\n    return tx as OnChainTx;\n}\n\n/**\n * Lists all the channels for all the nodes in the node manager.\n * @returns {Promise<any>}\n */\nexport async function list_channels(): Promise<MutinyChannel[]> {\n    const channels = await wallet!.list_channels();\n    return channels;\n}\n\n/**\n * This should only be called when the user is setting up a new profile\n * never for an existing profile\n * @param {string | undefined} [name]\n * @param {string | undefined} [img_url]\n * @param {string | undefined} [lnurl]\n * @param {string | undefined} [nip05]\n * @returns {Promise<any>}\n */\nexport async function setup_new_profile(\n    name?: string,\n    img_url?: string,\n    lnurl?: string,\n    nip05?: string\n): Promise<unknown> {\n    const profile = await wallet!.setup_new_profile(\n        name,\n        img_url,\n        lnurl,\n        nip05\n    );\n    return profile;\n}\n\n/**\n * Queries our relays for federation announcements\n * @returns {Promise<any>}\n */\nexport async function discover_federations(): Promise<\n    DiscoveredFederation[] | undefined\n> {\n    const federations = await wallet!.discover_federations();\n    return federations;\n}\n\n/**\n * Checks if we have recommended the given federation\n * @param {string} federation_id\n * @returns {Promise<boolean>}\n */\nexport async function has_recommended_federation(\n    federation_id: string\n): Promise<boolean> {\n    const hasRecommended =\n        await wallet!.has_recommended_federation(federation_id);\n    return hasRecommended;\n}\n\n/**\n * Adds a new federation based on its federation code\n * @param {string} federation_code\n * @returns {Promise<FederationIdentity>}\n */\nexport async function new_federation(inviteCode: string): Promise<unknown> {\n    const newFederation = await wallet!.new_federation(inviteCode);\n    return newFederation;\n}\n\nexport type NostrMetadata = {\n    name?: string;\n    display_name?: string;\n    picture?: string;\n    lud16?: string;\n    nip05?: string;\n    deleted?: boolean;\n};\n\n/**\n * Sets the user's nostr profile data\n * @param {string | undefined} [name]\n * @param {string | undefined} [img_url]\n * @param {string | undefined} [lnurl]\n * @param {string | undefined} [nip05]\n * @returns {Promise<any>}\n */\nexport async function edit_nostr_profile(\n    name?: string,\n    img_url?: string,\n    lnurl?: string,\n    nip05?: string\n): Promise<NostrMetadata> {\n    const profile = await wallet!.edit_nostr_profile(\n        name,\n        img_url,\n        lnurl,\n        nip05\n    );\n    return {\n        ...profile\n    };\n}\n\n/**\n * Uploads a profile pic to nostr.build and returns the uploaded file's URL\n * @param {string} img_base64\n * @returns {Promise<string>}\n */\nexport async function upload_profile_pic(data: string): Promise<string> {\n    const url = await wallet!.upload_profile_pic(data);\n    return url;\n}\n\n/**\n * Lists all pending NWC invoices\n * @returns {(PendingNwcInvoice)[]}\n */\nexport async function get_pending_nwc_invoices(): Promise<\n    PendingNwcInvoice[] | undefined\n> {\n    const pending = await wallet!.get_pending_nwc_invoices();\n\n    // PAIN\n    // Have to rebuild the array because it's an array of pointers\n    const newPending: PendingNwcInvoice[] = [];\n    for (const pendingItem of pending) {\n        newPending.push({\n            amount_sats: pendingItem.amount_sats,\n            expiry: pendingItem.expiry,\n            id: pendingItem.id,\n            index: pendingItem.index,\n            invoice: pendingItem.invoice,\n            invoice_description: pendingItem.invoice_description,\n            npub: pendingItem.npub,\n            profile_name: pendingItem.profile_name\n        } as PendingNwcInvoice);\n    }\n    return newPending;\n}\n\n/**\n * Deletes a nostr wallet connect profile\n * @param {number} profile_index\n * @returns {Promise<void>}\n */\nexport async function delete_nwc_profile(index: number): Promise<void> {\n    await wallet!.delete_nwc_profile(index);\n}\n\n/**\n * Get nostr wallet connect profiles\n * @returns {(NwcProfile)[]}\n */\nexport async function get_nwc_profiles(): Promise<NwcProfile[]> {\n    const profiles = await wallet!.get_nwc_profiles();\n\n    // PAIN\n    // Have to rebuild the array because it's an array of pointers\n    const newProfiles: NwcProfile[] = [];\n    for (const profile of profiles) {\n        newProfiles.push({\n            ...profile.value\n        } as NwcProfile);\n    }\n    return newProfiles;\n}\n\n/**\n * Approves a nostr wallet auth request.\n * Creates a new NWC profile and saves to storage.\n * This will also broadcast the info event to the relay.\n * @param {string} name\n * @param {string} uri\n * @param {bigint} budget\n * @param {BudgetPeriod} period\n * @returns {Promise<NwcProfile>}\n */\nexport async function approve_nostr_wallet_auth(\n    name: string,\n    uri: string\n): Promise<NwcProfile> {\n    const profile = await wallet!.approve_nostr_wallet_auth(name, uri);\n    return profile;\n}\n\n/**\n * Finds a nostr wallet connect profile by index\n * @param {number} index\n * @returns {Promise<NwcProfile>}\n */\nexport async function get_nwc_profile(index: number): Promise<NwcProfile> {\n    const profile = await wallet!.get_nwc_profile(index);\n    return {\n        ...profile.value\n    } as NwcProfile;\n}\n\n/**\n * Approves an invoice and sends the payment\n * @param {string} hash\n * @returns {Promise<void>}\n */\nexport async function approve_invoice(hash: string): Promise<void> {\n    await wallet!.approve_invoice(hash);\n}\n\n/**\n * Removes an invoice from the pending list, will also remove expired invoices\n * @param {string} hash\n * @returns {Promise<void>}\n */\nexport async function deny_invoice(hash: string): Promise<void> {\n    await wallet!.deny_invoice(hash);\n}\n\n/**\n * Removes all invoices from the pending list\n * @returns {Promise<void>}\n */\nexport async function deny_all_pending_nwc(): Promise<void> {\n    await wallet!.deny_all_pending_nwc();\n}\n\n/**\n * Set budget for a NWC Profile\n * @param {number} profile_index\n * @param {bigint} budget_sats\n * @param {BudgetPeriod} period\n * @param {bigint | undefined} [single_max_sats]\n * @returns {Promise<NwcProfile>}\n */\nexport async function set_nwc_profile_budget(\n    profile_index: number,\n    budget_sats: bigint,\n    period: BudgetPeriod,\n    single_max_sats?: bigint\n): Promise<NwcProfile> {\n    const profile = await wallet!.set_nwc_profile_budget(\n        profile_index,\n        budget_sats,\n        period,\n        single_max_sats\n    );\n    return profile;\n}\n\n/**\n * Require approval for a NWC Profile\n * @param {number} profile_index\n * @returns {Promise<NwcProfile>}\n */\nexport async function set_nwc_profile_require_approval(\n    profile_index: number\n): Promise<NwcProfile> {\n    const profile =\n        await wallet!.set_nwc_profile_require_approval(profile_index);\n    return profile;\n}\n\n/**\n * Create a nostr wallet connect profile\n * @param {string} name\n * @param {(string)[] | undefined} [commands]\n * @returns {Promise<NwcProfile>}\n */\nexport async function create_nwc_profile(\n    name: string,\n    commands?: string[]\n): Promise<NwcProfile> {\n    const profile = await wallet!.create_nwc_profile(name, commands);\n    return profile;\n}\n\n/**\n * Create a budgeted nostr wallet connect profile\n * @param {string} name\n * @param {bigint} budget\n * @param {BudgetPeriod} period\n * @param {bigint | undefined} [single_max]\n * @param {(string)[] | undefined} [commands]\n * @returns {Promise<NwcProfile>}\n */\nexport async function create_budget_nwc_profile(\n    name: string,\n    budget: bigint,\n    period: BudgetPeriod,\n    single_max?: bigint,\n    commands?: string[]\n): Promise<NwcProfile> {\n    const profile = await wallet!.create_budget_nwc_profile(\n        name,\n        budget,\n        period,\n        single_max,\n        commands\n    );\n    return profile;\n}\n\n/**\n * Disconnects from a peer from the selected node.\n * @param {string} peer\n * @returns {Promise<void>}\n */\nexport async function disconnect_peer(pubkey: string): Promise<void> {\n    await wallet!.disconnect_peer(pubkey);\n}\n\n/**\n * Deletes a peer from the selected node.\n * This will make it so that the node will not attempt to\n * reconnect to the peer.\n * @param {string} peer\n * @returns {Promise<void>}\n */\nexport async function delete_peer(pubkey: string): Promise<void> {\n    await wallet!.delete_peer(pubkey);\n}\n\n/**\n * Lists all the peers for all the nodes in the node manager.\n * @returns {Promise<any>}\n */\nexport async function list_peers(): Promise<MutinyPeer[]> {\n    return wallet!.list_peers();\n}\n\n/**\n * Attempts to connect to a peer from the selected node.\n * @param {string} connection_string\n * @param {string | undefined} [label]\n * @returns {Promise<void>}\n */\nexport async function connect_to_peer(\n    connection_string: string\n): Promise<void> {\n    await wallet!.connect_to_peer(connection_string);\n}\n\n/**\n * Closes a channel with the given outpoint.\n *\n * If force is true, the channel will be force closed.\n *\n * If abandon is true, the channel will be abandoned.\n * This will force close without broadcasting the latest transaction.\n * This should only be used if the channel will never actually be opened.\n *\n * If both force and abandon are true, an error will be returned.\n * @param {string} outpoint\n * @param {boolean} force\n * @param {boolean} abandon\n * @returns {Promise<void>}\n */\nexport async function close_channel(\n    outpoint: string,\n    force: boolean,\n    abandon: boolean\n): Promise<void> {\n    await wallet!.close_channel(outpoint, force, abandon);\n}\n\n/**\n * Removes a federation by setting its archived status to true, based on the FederationId.\n * @param {string} federation_id\n * @returns {Promise<void>}\n */\nexport async function remove_federation(federation_id: string): Promise<void> {\n    await wallet!.remove_federation(federation_id);\n}\n\n/**\n * Resyncs a federation\n * @param {string} federation_id\n * @returns {Promise<void>}\n */\nexport async function resync_federation(federation_id: string): Promise<void> {\n    await wallet!.resync_federation(federation_id);\n}\n\n/**\n * Gets the resync progress for a federation\n * @param {string} federation_id\n * @returns {Promise<ResyncProgress>}\n */\nexport async function get_federation_resync_progress(\n    federation_id: string\n): Promise<ResyncProgress | undefined> {\n    return wallet!.get_federation_resync_progress(federation_id);\n}\n\n/**\n * Opens a channel from our selected node to the given pubkey.\n * The amount is in satoshis.\n *\n * The node must be online and have a connection to the peer.\n * The wallet much have enough funds to open the channel.\n * @param {string | undefined} to_pubkey\n * @param {bigint} amount\n * @param {number | undefined} [fee_rate]\n * @returns {Promise<MutinyChannel>}\n */\nexport async function open_channel(\n    to_pubkey: string | undefined,\n    amount: bigint\n): Promise<MutinyChannel> {\n    const channel = await wallet!.open_channel(to_pubkey, amount);\n    return { ...channel.value } as MutinyChannel;\n}\n\n/**\n * Lists the pubkeys of the lightning node in the manager.\n * @returns {Promise<any>}\n */\nexport async function list_nodes(): Promise<string[]> {\n    return await wallet!.list_nodes();\n}\n\n/**\n * Changes all the node's LSPs to the given config. If any of the nodes have an active channel with the\n * current LSP, it will fail to change the LSP.\n *\n * Requires a restart of the node manager to take effect.\n * @param {string | undefined} [lsp_url]\n * @param {string | undefined} [lsp_connection_string]\n * @param {string | undefined} [lsp_token]\n * @returns {Promise<void>}\n */\nexport async function change_lsp(\n    lsp_url?: string,\n    lsp_connection_string?: string,\n    lsps_token?: string\n): Promise<void> {\n    await wallet!.change_lsp(lsp_url, lsp_connection_string, lsps_token);\n}\n\ntype LspConfig = {\n    url?: string;\n    connection_string?: string;\n    token?: string;\n};\n\n/**\n * Returns the configured LSP for the node manager.\n *\n * @returns {Promise<LspConfig>}\n */\nexport async function get_configured_lsp(): Promise<LspConfig> {\n    return await wallet!.get_configured_lsp();\n}\n\n/**\n * Resets BDK's keychain tracker. This will require a re-sync of the blockchain.\n *\n * This can be useful if you get stuck in a bad state.\n * @returns {Promise<void>}\n */\nexport async function reset_onchain_tracker(): Promise<void> {\n    await wallet!.reset_onchain_tracker();\n}\n\n/**\n * Starts up all the nodes again.\n * Not needed after [NodeManager]'s `new()` function.\n * @returns {Promise<void>}\n */\nexport async function start(): Promise<void> {\n    await wallet!.start();\n}\n\n/**\n * Authenticates with a LNURL-auth for the given profile.\n * @param {string} lnurl\n * @returns {Promise<void>}\n */\nexport async function lnurl_auth(lnurl: string): Promise<void> {\n    // TODO: test auth\n    await wallet!.lnurl_auth(lnurl);\n}\n\ntype PlanDetails = {\n    id: number;\n    amount_sat: bigint;\n};\n\n/**\n * Gets the subscription plans for Mutiny+ subscriptions\n * @returns {Promise<any>}\n */\nexport async function get_subscription_plans(): Promise<PlanDetails[]> {\n    return await wallet!.get_subscription_plans();\n}\n\n/**\n * Subscribes to a Mutiny+ plan with a specific plan id.\n *\n * Returns a lightning invoice so that the plan can be paid for to start it.\n * @param {number} id\n * @returns {Promise<MutinyInvoice>}\n */\nexport async function subscribe_to_plan(id: number): Promise<MutinyInvoice> {\n    const invoice = await wallet!.subscribe_to_plan(id);\n    return destructureInvoice(invoice);\n}\n\n/**\n * Pay the subscription invoice. This will post a NWC automatically afterwards.\n * @param {string} invoice_str\n * @param {boolean} autopay\n * @returns {Promise<void>}\n */\nexport async function pay_subscription_invoice(\n    invoice_str: string,\n    autopay: boolean\n): Promise<void> {\n    await wallet!.pay_subscription_invoice(invoice_str, autopay);\n}\n\n/**\n * Change our active nostr keys to the given nsec\n * @param {string | undefined} [nsec]\n * @param {string | undefined} [extension_pk]\n * @returns {Promise<string>}\n */\nexport async function change_nostr_keys(\n    nsec?: string,\n    extension_pk?: string\n): Promise<string> {\n    return await wallet!.change_nostr_keys(nsec, extension_pk);\n}\n\n/**\n * Sets the user's nostr profile data to a \"deleted\" state\n * @returns {Promise<any>}\n */\nexport async function delete_profile(): Promise<void> {\n    await wallet!.delete_profile();\n}\n\n/**\n * Export the user's nostr secret key if available\n * @returns {Promise<string | undefined>}\n */\nexport async function export_nsec(): Promise<string | undefined> {\n    // TODO: is this the right nsec?\n    return await wallet!.export_nsec();\n}\n\n/**\n * Returns the mnemonic seed phrase for the wallet.\n * @returns {string}\n */\nexport async function show_seed(): Promise<string> {\n    return await wallet!.show_seed();\n}\n\n/**\n * Create a single use nostr wallet connect profile\n * @param {bigint} amount_sats\n * @param {string} nwc_uri\n * @returns {Promise<string | undefined>}\n */\nexport async function claim_single_use_nwc(\n    amount_sats: bigint,\n    nwc_uri: string\n): Promise<string | undefined> {\n    return await wallet!.claim_single_use_nwc(amount_sats, nwc_uri);\n}\n\n/**\n * Calls upon a LNURL and withdraws from it.\n * This will fail if the LNURL is not a LNURL withdrawal.\n * @param {string} lnurl\n * @param {bigint} amount_sats\n * @returns {Promise<boolean>}\n */\nexport async function lnurl_withdraw(\n    lnurl: string,\n    amount_sats: bigint\n): Promise<boolean> {\n    return await wallet!.lnurl_withdraw(lnurl, amount_sats);\n}\n\n/**\n * Checks the registered username for the user\n * @returns {Promise<string | undefined>}\n */\nexport async function check_lnurl_name(): Promise<string | undefined> {\n    return await wallet!.check_lnurl_name();\n}\n\n/**\n * Checks if a given LNURL name is available\n * @param {string} name\n * @returns {Promise<boolean>}\n */\nexport async function check_available_lnurl_name(\n    name: string\n): Promise<boolean> {\n    return await wallet!.check_available_lnurl_name(name);\n}\n\n/**\n * Reserves a given LNURL name for the user\n * @param {string} name\n * @returns {Promise<void>}\n */\nexport async function reserve_lnurl_name(name: string): Promise<void> {\n    return await wallet!.reserve_lnurl_name(name);\n}\n\n/**\n * Creates a recommendation event for a federation\n * @param {string} invite_code\n * @param {string | undefined} [review]\n * @returns {Promise<string>}\n */\nexport async function recommend_federation(\n    invite_code: string,\n    review?: string\n): Promise<string> {\n    return await wallet!.recommend_federation(invite_code, review);\n}\n\n/**\n * Creates a delete event for a federation recommendation\n * @param {string} federation_id\n * @returns {Promise<void>}\n */\nexport async function delete_federation_recommendation(\n    federation_id: string\n): Promise<void> {\n    await wallet!.delete_federation_recommendation(federation_id);\n}\n\n/**\n * Gets the current balances of each federation.\n * @returns {Promise<FederationBalances>}\n */\nexport async function get_federation_balances(): Promise<FederationBalances> {\n    const balances = await wallet!.get_federation_balances();\n    if (!balances) return { balances: [] } as unknown as FederationBalances;\n    // PAIN\n    // Have to rebuild the balances from the raw data, which is a bit of a pain\n    const newBalances: FederationBalance[] = [];\n    for (const balance of balances.balances) {\n        const newBalance: FederationBalance = {\n            balance: balance.balance,\n            identity_federation_id: balance.identity_federation_id,\n            identity_uuid: balance.identity_uuid\n        } as FederationBalance;\n        newBalances.push(newBalance);\n    }\n    return {\n        balances: newBalances\n    } as FederationBalances;\n}\n\nexport async function change_password(\n    old_password?: string,\n    new_password?: string\n): Promise<void> {\n    await wallet!.change_password(old_password, new_password);\n}\n\n/**\n * Converts a satoshi amount to BTC.\n * @param {bigint} sats\n * @returns {number}\n */\nexport async function convert_sats_to_btc(sats: bigint): Promise<number> {\n    return await MutinyWallet.convert_sats_to_btc(sats);\n}\n\n/**\n * Converts a bitcoin amount in BTC to satoshis.\n * @param {number} btc\n * @returns {bigint}\n */\nexport async function convert_btc_to_sats(btc: number): Promise<bigint> {\n    return await MutinyWallet.convert_btc_to_sats(btc);\n}\n\n/**\n * Returns if there is a saved wallet in storage.\n * This is checked by seeing if a mnemonic seed exists in storage.\n * @returns {Promise<boolean>}\n */\nexport async function has_node_manager(): Promise<boolean> {\n    return await MutinyWallet.has_node_manager();\n}\n\n/**\n * Convert an npub string to a hex string\n * @param {string} npub\n * @returns {Promise<string>}\n */\nexport async function npub_to_hexpub(npub: string): Promise<string> {\n    return await MutinyWallet.npub_to_hexpub(npub);\n}\n\n/**\n * Convert an npub string to a hex string\n * @param {string} nsec\n * @returns {Promise<string>}\n */\nexport async function nsec_to_npub(nsec: string): Promise<string> {\n    return await MutinyWallet.nsec_to_npub(nsec);\n}\n\n/**\n * Convert an hex string to a npub string\n * @param {string} npub\n * @returns {Promise<string>}\n */\nexport async function hexpub_to_npub(hexpub: string): Promise<string> {\n    // TODO: the argument is called \"npub\" but it's actually a hexpub?\n    return await MutinyWallet.hexpub_to_npub(hexpub);\n}\n\n/**\n * Restore's the mnemonic after deleting the previous state.\n *\n * Backup the state beforehand. Does not restore lightning data.\n * Should refresh or restart afterwards. Wallet should be stopped.\n * @param {string} m\n * @param {string | undefined} [password]\n * @returns {Promise<void>}\n */\nexport async function restore_mnemonic(\n    mnemonic: string,\n    password?: string\n): Promise<void> {\n    await MutinyWallet.restore_mnemonic(mnemonic, password);\n}\n\n/**\n * Restore a node manager from a json object.\n * @param {string} json\n * @returns {Promise<void>}\n */\nexport async function import_json(json: string): Promise<void> {\n    await MutinyWallet.import_json(json);\n}\n\n/**\n * Exports the current state of the node manager to a json object.\n * @param {string | undefined} [password]\n * @returns {Promise<string>}\n */\nexport async function export_json(password?: string): Promise<string> {\n    return await MutinyWallet.export_json(password);\n}\n\n/**\n * Exports the current state of the node manager to a json object.\n * @returns {Promise<any>}\n */\nexport async function get_logs(): Promise<string[]> {\n    return await MutinyWallet.get_logs();\n}\n\n/**\n * Returns the number of remaining seconds until the device lock expires.\n */\nexport async function get_device_lock_remaining_secs(\n    password?: string,\n    auth_url?: string,\n    storage_url?: string\n): Promise<bigint | undefined> {\n    return await MutinyWallet.get_device_lock_remaining_secs(\n        password,\n        auth_url,\n        storage_url\n    );\n}\n\n/**\n * Opens a channel from our selected node to the given pubkey.\n * It will spend the all the on-chain utxo in full to fund the channel.\n *\n * The node must be online and have a connection to the peer.\n * @param {string | undefined} [to_pubkey]\n * @returns {Promise<MutinyChannel>}\n */\nexport async function sweep_all_to_channel(\n    to_pubkey?: string\n): Promise<MutinyChannel> {\n    return await wallet!.sweep_all_to_channel(to_pubkey);\n}\n\n/**\n * Estimates the onchain fee for sweeping our on-chain balance to open a lightning channel.\n * The fee rate is in sat/vbyte.\n * @param {number | undefined} [fee_rate]\n * @returns {bigint}\n */\nexport async function estimate_sweep_channel_open_fee(\n    fee_rate?: number | undefined\n): Promise<bigint> {\n    return await wallet!.estimate_sweep_channel_open_fee(fee_rate);\n}\n\n/**\n * Sweep the federation balance into a lightning channel\n * @param {string | undefined} [from_federation_id]\n * @param {string} [invoice]\n * @returns {Promise<FedimintSweepResult>}\n */\nexport async function sweep_federation_balance_to_invoice(\n    from_federation_id: string | undefined,\n    invoice: string\n): Promise<FedimintSweepResult> {\n    const result = await wallet!.sweep_federation_balance_to_invoice(\n        from_federation_id,\n        invoice\n    );\n    return { ...result.value } as FedimintSweepResult;\n}\n\n/**\n * Estimate the fee before trying to sweep from federation\n * @param {bigint | undefined} [amount]\n * @param {string | undefined} [from_federation_id]\n * @param {string | undefined} [to_federation_id]\n * @returns {Promise<MutinyInvoice>}\n */\nexport async function create_sweep_federation_invoice(\n    amount?: bigint,\n    from_federation_id?: string,\n    to_federation_id?: string\n): Promise<MutinyInvoice> {\n    const invoice = await wallet!.create_sweep_federation_invoice(\n        amount,\n        from_federation_id,\n        to_federation_id\n    );\n    return destructureInvoice(invoice);\n}\n\nexport async function parse_params(params: string): Promise<PaymentParams> {\n    const paramsResult = await new PaymentParams(params);\n    // PAIN just another object rebuild\n    return {\n        address: paramsResult.address,\n        amount_msats: paramsResult.amount_msats,\n        amount_sats: paramsResult.amount_sats,\n        cashu_token: paramsResult.cashu_token,\n        disable_output_substitution: paramsResult.disable_output_substitution,\n        fedimint_invite_code: paramsResult.fedimint_invite_code,\n        fedimint_oob_notes: paramsResult.fedimint_oob_notes,\n        invoice: paramsResult.invoice,\n        is_lnurl_auth: paramsResult.is_lnurl_auth,\n        lightning_address: paramsResult.lightning_address,\n        lnurl: paramsResult.lnurl,\n        memo: paramsResult.memo,\n        network: paramsResult.network,\n        node_pubkey: paramsResult.node_pubkey,\n        nostr_pubkey: paramsResult.nostr_pubkey,\n        nostr_wallet_auth: paramsResult.nostr_wallet_auth,\n        offer: paramsResult.offer,\n        payjoin_endpoint: paramsResult.payjoin_endpoint,\n        payjoin_supported: paramsResult.payjoin_supported,\n        refund: paramsResult.refund,\n        string: paramsResult.string\n    } as PaymentParams;\n}\n"
  },
  {
    "path": "tailwind.config.cjs",
    "content": "const plugin = require(\"tailwindcss/plugin\");\n\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n    content: [\"./index.html\", \"./src/**/*.{js,ts,jsx,tsx}\"],\n    safelist: [\n        \"grid-cols-1\",\n        \"grid-cols-2\",\n        \"gap-2\",\n        \"gap-4\",\n        \"outline-m-red\",\n        \"outline-m-blue\"\n    ],\n    variants: {\n        extend: {\n            borderWidth: [\"responsive\", \"last\", \"hover\", \"focus\"]\n        }\n    },\n    theme: {\n        extend: {\n            colors: {\n                \"half-black\": \"rgba(0, 0, 0, 0.5)\",\n                \"faint-white\": \"rgba(255, 255, 255, 0.1)\",\n                \"light-text\": \"rgba(250, 245, 234, 0.5)\",\n                \"m-green\": \"hsla(163, 70%, 38%, 1)\",\n                \"m-green-dark\": \"hsla(163, 70%, 28%, 1)\",\n                \"m-blue\": \"hsla(220, 59%, 52%, 1)\",\n                \"m-blue-400\": \"hsla(219, 57%, 52%, 1)\",\n                \"m-blue-dark\": \"hsla(220, 59%, 42%, 1)\",\n                \"m-red\": \"hsla(343, 92%, 54%, 1)\",\n                \"m-red-dark\": \"hsla(343, 92%, 44%, 1)\",\n                \"m-yellow\": \"#E7D538\",\n                \"sidebar-gray\": \"hsla(222, 15%, 7%, 1)\",\n                \"m-grey-350\": \"hsla(0, 0%, 73%, 1)\",\n                \"m-grey-400\": \"hsla(0, 0%, 64%, 1)\",\n                \"m-grey-700\": \"hsla(0, 0%, 25%, 1)\",\n                \"m-grey-750\": \"hsla(0, 0%, 17%, 1)\",\n                \"m-grey-800\": \"hsla(0, 0%, 12%, 1)\",\n                \"m-grey-900\": \"hsla(0, 0%, 9%, 1)\",\n                \"m-grey-950\": \"hsla(0, 0%, 8%, 1)\",\n                \"m-grey-975\": \"hsla(0, 0%, 5%, 1)\"\n            },\n            backgroundImage: {\n                \"fade-to-blue\":\n                    \"linear-gradient(1.63deg, #0B215B 32.05%, rgba(11, 33, 91, 0) 84.78%)\",\n                \"subtle-fade\":\n                    \"linear-gradient(180deg, #060A13 0%, #131E39 100%)\",\n                \"richer-fade\":\n                    \"linear-gradient(180deg, hsla(224, 20%, 8%, 1) 0%, hsla(224, 20%, 15%, 1) 100%)\"\n            },\n            dropShadow: {\n                \"blue-glow\": \"0px 0px 32px rgba(11, 33, 91, 0.5)\"\n            },\n            boxShadow: {\n                \"inner-button\":\n                    \"1px 1px 2px rgba(0, 0, 0, 0.1), inset 1px 1px 2px rgba(255, 255, 255, 0.1), inset -1px -1px 2px rgba(0, 0, 0, 0.2)\",\n                \"inner-button-disabled\":\n                    \"1px 1px 2px rgba(0, 0, 0, 0.05), inset 1px 1px 2px rgba(255, 255, 255, 0.05), inset -1px -1px 2px rgba(0, 0, 0, 0.1)\",\n                \"fancy-card\": \"0px 4px 4px rgba(0, 0, 0, 0.1)\",\n                above: \"0px -4px 10px rgba(0, 0, 0, 0.25)\",\n                keycap: \"15px 15px 20px -5px rgba(0, 0, 0, 0.3)\"\n            },\n            fontFamily: {\n                \"system-mono\": [\"ui-monospace\", \"Menlo\", \"Monaco\", \"monospace\"]\n            },\n            textShadow: {\n                button: \"1px 1px 0px rgba(0, 0, 0, 0.4)\"\n            },\n            animation: {\n                throb: \"throb 5s ease-in-out infinite\"\n            },\n            keyframes: {\n                throb: {\n                    \"0%, 100%\": { transform: \"scale(1)\", opacity: 0.9 },\n                    \"50%\": { transform: \"scale(1.1)\", opacity: 1 }\n                }\n            }\n        }\n    },\n    plugins: [\n        // default prefix is \"ui\"\n        require(\"@kobalte/tailwindcss\"),\n        plugin(function ({ addUtilities }) {\n            const newUtilities = {\n                \".safe-top\": {\n                    paddingTop: \"env(safe-area-inset-top)\"\n                },\n                \".safe-left\": {\n                    paddingLeft: \"env(safe-area-inset-left)\"\n                },\n                \".safe-right\": {\n                    paddingRight: \"env(safe-area-inset-right)\"\n                },\n                \".safe-bottom\": {\n                    paddingBottom: \"env(safe-area-inset-bottom)\"\n                },\n                \".min-h-device\": {\n                    minHeight:\n                        \"calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom))\"\n                },\n                \".h-device\": {\n                    height: \"calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom))\"\n                },\n                \"max-h-device\": {\n                    maxHeight:\n                        \"calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom))\"\n                },\n                \".h-svh\": {\n                    height: \"calc(100svh - env(safe-area-inset-top) - env(safe-area-inset-bottom))\"\n                },\n                \".disable-scrollbars\": {\n                    scrollbarWidth: \"none\",\n                    \"-ms-overflow-style\": \"none\",\n                    \"&::-webkit-scrollbar\": {\n                        width: \"0px\",\n                        background: \"transparent\",\n                        display: \"none\"\n                    },\n                    \"& *::-webkit-scrollbar\": {\n                        width: \"0px\",\n                        background: \"transparent\",\n                        display: \"none\"\n                    },\n                    \"& *\": {\n                        scrollbarWidth: \"none\",\n                        \"-ms-overflow-style\": \"none\"\n                    }\n                }\n            };\n            addUtilities(newUtilities);\n        }),\n        // Text shadow!\n        plugin(function ({ matchUtilities, theme }) {\n            matchUtilities(\n                {\n                    \"text-shadow\": (value) => ({\n                        textShadow: value\n                    })\n                },\n                { values: theme(\"textShadow\") }\n            );\n        })\n    ]\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"declaration\": true,\n        \"declarationDir\": \"./types\",\n        \"allowSyntheticDefaultImports\": true,\n        \"esModuleInterop\": true,\n        \"target\": \"ESNext\",\n        \"module\": \"ESNext\",\n        \"moduleResolution\": \"node\",\n        \"jsx\": \"preserve\",\n        \"jsxImportSource\": \"solid-js\",\n        \"noEmit\": true,\n        \"strict\": true,\n        \"types\": [\"vite/client\", \"vite-plugin-pwa/client\"],\n        \"baseUrl\": \"./\",\n        \"paths\": {\n            \"~/*\": [\"./src/*\"]\n        },\n        \"skipLibCheck\": true\n    },\n    \"include\": [\n        \"global.d.ts\",\n        \"src/**/*\",\n        \"src/routes/**/*\",\n        \"tailwind.config.cjs\",\n        \"playwright.config.ts\",\n        \"vite.config.ts\",\n        \".eslintrc.cjs\",\n        \"e2e/**/*\",\n        \"capacitor.config.ts\",\n        \"prettier.config.mjs\"\n    ],\n    \"exclude\": [\n        \"node_modules\",\n        \"./node_modules\",\n        \"./node_modules/*\",\n        \"./node_modules/**/*\",\n        \"./node_modules/@types/node/index.d.ts\",\n        \"node_modules/.pnpm/solid-js@1.7.7/node_modules/solid-js/types/*\",\n        \"build\",\n        \"dist\",\n        \"public\"\n    ]\n}\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import child from \"node:child_process\";\nimport path from \"node:path\";\nimport { defineConfig } from \"vite\";\nimport { comlink } from \"vite-plugin-comlink\";\nimport { VitePWA, VitePWAOptions } from \"vite-plugin-pwa\";\nimport solid from \"vite-plugin-solid\";\nimport wasm from \"vite-plugin-wasm\";\n\nimport manifest from \"./manifest\";\n\nconst commitHash =\n    process.env.VITE_COMMIT_HASH ??\n    child.execSync(\"git rev-parse --short HEAD\").toString().trim();\n\nconst pwaOptions: Partial<VitePWAOptions> = {\n    base: \"/\",\n    registerType: \"prompt\",\n    devOptions: {\n        enabled: false\n    },\n    workbox: {\n        navigateFallback: \"/index.html\",\n        globPatterns: [\"**/*.{js,css,html,svg,png,gif,wasm}\"],\n        // mutiny_wasm is 10mb, so we'll do 25mb to be safe\n        maximumFileSizeToCacheInBytes: 25 * 1024 * 1024\n    },\n    includeAssets: [\"favicon.ico\", \"robots.txt\"],\n    manifest: manifest\n};\n\nexport default defineConfig({\n    build: {\n        target: \"esnext\",\n        outDir: \"dist/public\",\n        emptyOutDir: true,\n        sourcemap: true\n    },\n    server: {\n        port: 3420,\n        fs: {\n            // Allow serving files from one level up (so that if mutiny-node is a sibling folder we can use it locally)\n            allow: [\"..\"]\n        }\n    },\n    plugins: [comlink(), wasm(), solid(), VitePWA(pwaOptions)],\n    worker: {\n        plugins: () => [comlink(), wasm()],\n        format: \"es\"\n    },\n    define: {\n        \"import.meta.env.__COMMIT_HASH__\": JSON.stringify(commitHash),\n        \"import.meta.env.__RELEASE_VERSION__\": JSON.stringify(\n            process.env.npm_package_version\n        )\n    },\n    resolve: {\n        alias: [{ find: \"~\", replacement: path.resolve(__dirname, \"./src\") }]\n    },\n    optimizeDeps: {\n        // Don't want vite to bundle these late during dev causing reload\n        include: [\n            \"qr-scanner\",\n            \"@solid-primitives/upload\",\n            \"i18next\",\n            \"i18next-browser-languagedetector\",\n            \"@capacitor-mlkit/barcode-scanning\",\n            \"@capacitor/app\",\n            \"@capacitor/app-launcher\",\n            \"@capacitor/clipboard\",\n            \"@capacitor/core\",\n            \"@capacitor/filesystem\",\n            \"@capacitor/haptics\",\n            \"@capacitor/share\",\n            \"@capacitor/status-bar\",\n            \"@capacitor/toast\"\n        ],\n        // This is necessary because otherwise `vite dev` can't find the wasm\n        exclude: [\"@mutinywallet/mutiny-wasm\"],\n        esbuildOptions: {\n            target: \"esnext\"\n        }\n    }\n});\n"
  }
]