diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 403e2bc..5623342 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main, "**"] + branches: [main] pull_request: branches: [main] @@ -62,3 +62,11 @@ jobs: - name: Run tests run: | dart test + + precompile-binaries: + needs: build-and-test + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + permissions: + contents: write + uses: ./.github/workflows/precompile_binaries.yml + secrets: inherit diff --git a/.github/workflows/precompile_binaries.yml b/.github/workflows/precompile_binaries.yml new file mode 100644 index 0000000..e093564 --- /dev/null +++ b/.github/workflows/precompile_binaries.yml @@ -0,0 +1,88 @@ +name: Precompile Binaries + +on: + workflow_call: + secrets: + PRECOMPILED_PRIVATE_KEY: + required: true + workflow_dispatch: + +permissions: + contents: write + +env: + CRATE_DIR: "native" + CRATE_PACKAGE: "bdk_dart_ffi" + +jobs: + precompile_macos_ios: + runs-on: macos-latest + env: + PRIVATE_KEY: ${{ secrets.PRECOMPILED_PRIVATE_KEY }} + GH_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.85.1 + override: true + - uses: dart-lang/setup-dart@v1 + - uses: subosito/flutter-action@v2 + with: + channel: "stable" + - name: Pub get + run: dart pub get + - name: Precompile (macOS + iOS) + run: | + set -euo pipefail + dart run bin/build_tool.dart precompile-binaries \ + -v \ + --os=macos \ + --manifest-dir="${CRATE_DIR}" \ + --crate-package="${CRATE_PACKAGE}" \ + --repository="${GITHUB_REPOSITORY}" + + precompile_android: + runs-on: ubuntu-latest + env: + PRIVATE_KEY: ${{ secrets.PRECOMPILED_PRIVATE_KEY }} + GH_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ github.token }} + ANDROID_NDK_VERSION: "26.3.11579264" + ANDROID_MIN_SDK: "23" + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.85.1 + override: true + - uses: dart-lang/setup-dart@v1 + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + - name: Install NDK + run: | + set -euo pipefail + sdkmanager --install "ndk;${ANDROID_NDK_VERSION}" + - name: Install cargo-ndk + run: cargo install cargo-ndk --locked + - name: Pub get + run: dart pub get + - name: Precompile (Android) + env: + ANDROID_SDK_ROOT: ${{ env.ANDROID_SDK_ROOT }} + run: | + set -euo pipefail + ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}" + dart run bin/build_tool.dart precompile-binaries \ + -v \ + --os=android \ + --manifest-dir="${CRATE_DIR}" \ + --crate-package="${CRATE_PACKAGE}" \ + --repository="${GITHUB_REPOSITORY}" \ + --android-sdk-location="${ANDROID_SDK_ROOT}" \ + --android-ndk-version="${ANDROID_NDK_VERSION}" \ + --android-min-sdk-version="${ANDROID_MIN_SDK}" + diff --git a/.gitignore b/.gitignore index 3adc1b6..8b1f506 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ /target native/target/ **/*.rs.bk -Cargo.lock # Flutter/Dart **/build/ diff --git a/README.md b/README.md index f0793af..a910c1c 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,28 @@ If you have the Rust toolchain installed, the native library will be automatical As a user of the package, you don't need to worry about building the native library or bindings yourself. Only if you want to contribute to the bindings or modify the native code yourself, you can follow the instructions in [development](#development) below. +## Precompiled binaries + +This plugin adds a precompiled-binary layer on top of the standard Native Assets approach. +Depending on the mode configuration, the build hook may download signed precompiled binaries or build locally. +If precompiled binaries are attempted but unavailable or verification fails, it falls back to building from scratch via the Flutter/Dart build hook. +This gives consumers a choice between using published binaries or building locally. + +### pubspec.yaml configuration + +In your app's `pubspec.yaml`, add the `bdk_dart` section at the top level (next to `dependencies`), like: + +```yaml +bdk_dart: + precompiled_binaries: + mode: auto # auto | always | never +``` + +`mode` controls when the precompiled path is used: +- `auto` prefers local builds if Rust toolchain is detected (for development), otherwise uses precompiled binaries +- `always` requires precompiled binaries and skips local builds +- `never` always builds from source via the build hook + ## Development ### Generating bindings @@ -80,6 +102,10 @@ Dart test suite, which covers wallet creation, persistence, offline behavior, an dart test ``` +### Precompiled binaries (maintainers) + +See `docs/precompiled_binaries.md` for CI details, manual release steps, and configuration. + ## License The Rust crate and generated bindings are dual-licensed under MIT or Apache 2.0 per the diff --git a/bin/build_tool.dart b/bin/build_tool.dart new file mode 100644 index 0000000..17a3b86 --- /dev/null +++ b/bin/build_tool.dart @@ -0,0 +1,6 @@ +import 'package:bdk_dart/src/precompiled/cli/cli.dart'; + +Future main(List args) async { + await runCli(args); +} + diff --git a/docs/precompiled_binaries.md b/docs/precompiled_binaries.md new file mode 100644 index 0000000..3fff858 --- /dev/null +++ b/docs/precompiled_binaries.md @@ -0,0 +1,91 @@ +# Precompiled binaries (maintainers) + +This document describes how precompiled binaries are built, signed, and published for the plugin. + +## Overview + +- CI builds and uploads precompiled binaries via `.github/workflows/precompile_binaries.yml`. +- Artifacts are tagged by the crate hash and uploaded to a GitHub release. +- Each binary is signed with an Ed25519 key; the public key is embedded in `pubspec.yaml`. +- The build hook downloads verified binaries when appropriate (depending on mode configuration) and falls back to local builds if needed. + +## Mode behavior + +The `mode` configuration in `pubspec.yaml` controls fallback behavior: + +- `auto`: Uses a heuristic to prefer local builds for development. If the Rust toolchain (`rustup`) is detected, it disables precompiled binaries and builds locally. If no Rust toolchain is found, it uses precompiled binaries. This provides optimal developer experience while keeping end-user builds fast. +- `always`: Throws an exception if download/verification fails; does not fall back. +- `never`: Always builds locally via the standard build hook, ignoring precompiled binaries. + +## CI workflow + +The workflow runs on `push` to `main` and on manual dispatch. It invokes: + +``` +dart run bin/build_tool.dart precompile-binaries ... +``` + +It currently builds macOS/iOS and Android targets. + +## Release expectations + +- The workflow creates/releases a GitHub release named `precompiled_` where `` comes from the verified crate sources and config. +- If the release already exists, the workflow uploads missing assets without rebuilding anything already present. +- If `gh release view precompiled_` fails locally, rerun `dart run bin/build_tool.dart precompile-binaries ...` with the same crate hash to recreate or update the release. + +## How the download works + +- The crate hash is computed from the Rust crate sources plus the plugin's `precompiled_binaries` config. +- The release tag is `precompiled_`. +- Assets are named `_` with a matching `.sig` file. +- Each binary is paired with the `.sig` file that the hook uses to verify the download before applying it. +- The hook chooses the correct `lib$cratePackage` (or `lib$cratePackage.so`) artifact by matching the target triple and link mode from the Dart build config. +- On build, the hook downloads the signature and binary, verifies it, then places it in the build output. +- If any step fails (missing asset, bad signature), the hook builds locally via the standard build hook. + +## Manual release (local) + +Use this when debugging CI or producing artifacts manually. + +Required environment variables: + +- `PRIVATE_KEY` (Ed25519 private key, hex-encoded, 64 bytes) +- `GH_TOKEN` or `GITHUB_TOKEN` (GitHub token with release upload permissions) + +Example: + +``` +dart run bin/build_tool.dart precompile-binaries \ + --manifest-dir="native" \ + --crate-package="bdk_dart_ffi" \ + --repository="owner/repo" \ + --os=macos +``` + +## Troubleshooting & ops tips + +- If `gh release view precompiled_` shows a release without the expected `_` assets, rerun the build locally to regenerate them. +- A stale crate hash (because sources or `precompiled_binaries` config changed) will point to a release that either doesn’t exist yet or lacks current binaries; re-run `dart run bin/build_tool.dart hash --manifest-dir=native` to confirm the hash and rebuild with the same inputs. +- Use `gh release view precompiled_ --json assets --jq '.assets[].name'` to inspect what’s uploaded and verify `.sig` coverage. +- When debugging download failures, set `BDK_DART_PRECOMPILED_VERBOSE=1` to see why the hook skipped an asset. + +## Configuration knobs + +- `rust-toolchain.toml` controls the Rust channel and target list. +- `pubspec.yaml` under `bdk_dart.precompiled_binaries` must include: + - `artifact_host` (owner/repo) + - `public_key` (Ed25519 public key, hex-encoded, 32 bytes) + +## Environment, keys, and secrets + +- `PRIVATE_KEY`: 64-byte hex string (Ed25519 private key). This must be set locally or as a GitHub Actions secret before running `precompile-binaries`. Keep it out of source control. +- `PUBLIC_KEY`: Add the matching 32-byte hex public key to `pubspec.yaml` so consumers can verify downloads. +- `GH_TOKEN` / `GITHUB_TOKEN`: release upload permissions (already used in the CI workflow). +- `BDK_DART_PRECOMPILED_VERBOSE=1`: optional; shows download and verification details when debugging consumer builds. + +Generate a keypair with `dart run bin/build_tool.dart gen-key` and copy the printed `PRIVATE_KEY`/`PUBLIC_KEY` values. Rotate the pair if you ever suspect the signing key was exposed, and update every release’s config accordingly. + +## Security reminder + +- Treat the `PRIVATE_KEY` used for signing as highly sensitive; do not commit it to version control and rotate it immediately if you suspect compromise. +- Update the public key in `pubspec.yaml` if the private key is rotated so consumers can still verify downloads. diff --git a/hook/build.dart b/hook/build.dart index 7e7790e..46d6192 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -1,10 +1,25 @@ +import 'package:bdk_dart/src/precompiled/precompiled_builder.dart'; import 'package:hooks/hooks.dart'; -import 'package:native_toolchain_rust/native_toolchain_rust.dart'; +import 'package:native_toolchain_rust/native_toolchain_rust.dart' as ntr; void main(List args) async { await build(args, (input, output) async { - await const RustBuilder( + final builder = PrecompiledBuilder( assetName: 'uniffi:bdk_dart_ffi', - ).run(input: input, output: output); + buildModeName: ntr.BuildMode.release.name, + fallback: (input, output, assetRouting, logger) async { + final rustBuilder = ntr.RustBuilder( + assetName: 'uniffi:bdk_dart_ffi', + buildMode: ntr.BuildMode.release, + ); + await rustBuilder.run( + input: input, + output: output, + assetRouting: assetRouting, + logger: logger, + ); + }, + ); + await builder.run(input: input, output: output); }); } diff --git a/lib/src/precompiled/artifacts_provider.dart b/lib/src/precompiled/artifacts_provider.dart new file mode 100644 index 0000000..b916bdc --- /dev/null +++ b/lib/src/precompiled/artifacts_provider.dart @@ -0,0 +1,201 @@ +import 'dart:io'; + +import 'package:code_assets/code_assets.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:hooks/hooks.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'cargo.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'target.dart'; +import 'util.dart'; + +// Handles locating, downloading, and verifying precompiled artifacts. +final _log = Logger('bdk_dart.artifacts_provider'); + +// Infer the invoking package root from the build output directory. +String? _invokerRootFromOutputDirectory(Uri outputDirectory) { + final parts = path.split(path.fromUri(outputDirectory)); + final dartToolIndex = parts.lastIndexOf('.dart_tool'); + if (dartToolIndex <= 0) { + return null; + } + return path.joinAll(parts.take(dartToolIndex)); +} + +// Result of a successful download + verification. +class DownloadedArtifact { + DownloadedArtifact({required this.filePath, required this.dependencies}); + + final String filePath; + final List dependencies; +} + +class PrecompiledBinaryRequiredException implements Exception { + PrecompiledBinaryRequiredException(this.message); + + final String message; + + @override + String toString() => 'PrecompiledBinaryRequiredException: $message'; +} + +// Resolves config, downloads binary + signature, and verifies them. +class PrecompiledArtifactProvider { + PrecompiledArtifactProvider({ + required this.input, + required this.buildModeName, + required this.crateDir, + }); + + final BuildInput input; + final String buildModeName; + final Directory crateDir; + + Future tryGetPrecompiledArtifact() async { + // Read precompiled_binaries configuration from pubspec.yaml. + final pubspecOptions = PubspecOptions.load( + packageRoot: path.fromUri(input.packageRoot), + pluginConfigKey: 'bdk_dart', + ); + final baseConfig = pubspecOptions.precompiledBinaries; + if (baseConfig == null) { + return null; + } + + // Allow invoker package to override mode for this build. + final invokerRoot = _invokerRootFromOutputDirectory(input.outputDirectory); + final invokerMode = invokerRoot == null + ? null + : PubspecOptions.loadModeOverride( + packageRoot: invokerRoot, + packageName: input.packageName, + ); + + final precompiled = invokerMode == null + ? baseConfig + : PrecompiledBinariesConfig( + artifactHost: baseConfig.artifactHost, + mode: invokerMode, + publicKey: baseConfig.publicKey, + urlPrefix: baseConfig.urlPrefix, + ); + + if (invokerMode != null) { + _log.info( + 'Using invoker override precompiled_binaries.mode=${invokerMode.name} from $invokerRoot', + ); + } + + if (precompiled.mode == PrecompiledBinaryMode.never) { + _log.info('Precompiled binaries disabled by mode=never'); + return null; + } + + if (precompiled.mode == PrecompiledBinaryMode.auto) { + final userOptions = UserOptions.load(hasConfig: true); + if (!userOptions.usePrecompiledBinaries) { + _log.info( + 'Precompiled binaries disabled (auto mode + local build preferred)', + ); + return null; + } + } + + if (!input.config.buildCodeAssets) { + return null; + } + + final codeConfig = input.config.code; + // Derive crate name from Cargo.toml for file naming. + final crateInfo = CrateInfo.load(crateDir.path); + final targetTriple = codeConfig.targetTriple; + final linkMode = codeConfig.linkMode; + + // Write into the build output target directory. + final outDir = path.join(path.fromUri(input.outputDirectory), 'target'); + final libFileName = codeConfig.targetOS + .libraryFileName(crateInfo.packageName, linkMode) + .replaceAll('-', '_'); + final finalLibPath = path.join( + outDir, + targetTriple, + buildModeName, + libFileName, + ); + + Directory(path.dirname(finalLibPath)).createSync(recursive: true); + + // Crate hash keys the release tag and artifact path. + final crateHash = CrateHash.compute(crateDir.path, tempStorage: outDir); + + final remoteFileName = '${targetTriple}_$libFileName'; + final remoteSignatureName = '$remoteFileName.sig'; + + final binaryUrl = precompiled.fileUrl( + crateHash: crateHash, + fileName: remoteFileName, + ); + final signatureUrl = precompiled.fileUrl( + crateHash: crateHash, + fileName: remoteSignatureName, + ); + + final bool requirePrecompiled = + precompiled.mode == PrecompiledBinaryMode.always; + + DownloadedArtifact? handleFailure(String message) { + if (requirePrecompiled) { + throw PrecompiledBinaryRequiredException(message); + } + return null; + } + + _log.info('Downloading signature from $signatureUrl'); + final signatureRes = await httpGetWithRetry(signatureUrl); + if (signatureRes.statusCode == 404) { + _log.info('No precompiled binaries for crate hash $crateHash'); + return handleFailure('No precompiled binaries for crate hash $crateHash'); + } + if (signatureRes.statusCode != 200) { + _log.warning( + 'Failed to download signature: status ${signatureRes.statusCode}', + ); + return handleFailure( + 'Failed to download signature: status ${signatureRes.statusCode}', + ); + } + + _log.info('Downloading binary from $binaryUrl'); + final binaryRes = await httpGetWithRetry(binaryUrl); + if (binaryRes.statusCode != 200) { + _log.warning('Failed to download binary: status ${binaryRes.statusCode}'); + return handleFailure( + 'Failed to download binary: status ${binaryRes.statusCode}', + ); + } + + // Verify binary integrity before writing it to disk. + final ok = verify( + precompiled.publicKey, + binaryRes.bodyBytes, + signatureRes.bodyBytes, + ); + if (!ok) { + _log.warning('Signature verification failed; ignoring binary'); + return handleFailure('Signature verification failed; ignoring binary'); + } + + await writeBytesAtomically(File(finalLibPath), binaryRes.bodyBytes); + _log.info('Verified and wrote precompiled binary to $finalLibPath'); + + // Dependencies track local crate inputs for rebuild invalidation. + final deps = CrateHash.collectFiles( + crateDir.path, + ).map((f) => f.absolute.uri).toList(growable: false); + + return DownloadedArtifact(filePath: finalLibPath, dependencies: deps); + } +} diff --git a/lib/src/precompiled/cargo.dart b/lib/src/precompiled/cargo.dart new file mode 100644 index 0000000..1db3f6f --- /dev/null +++ b/lib/src/precompiled/cargo.dart @@ -0,0 +1,36 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; + +class CrateInfo { + CrateInfo({required this.packageName}); + + final String packageName; + + // Extract crate package name from Cargo.toml. + static CrateInfo load(String manifestDir) { + final manifestFile = File(path.join(manifestDir, 'Cargo.toml')); + final manifest = manifestFile.readAsStringSync(); + final lines = manifest.split('\n'); + var inPackage = false; + for (final raw in lines) { + final line = raw.trim(); + if (line.startsWith('[') && line.endsWith(']')) { + inPackage = line == '[package]'; + continue; + } + if (!inPackage) continue; + if (line.startsWith('name')) { + final parts = line.split('='); + if (parts.length >= 2) { + final value = parts.sublist(1).join('=').trim(); + final m = RegExp(r'^"(.+)"$').firstMatch(value); + if (m != null) { + return CrateInfo(packageName: m.group(1)!); + } + } + } + } + throw StateError('Failed to determine crate name from Cargo.toml'); + } +} diff --git a/lib/src/precompiled/cli/cli.dart b/lib/src/precompiled/cli/cli.dart new file mode 100644 index 0000000..630080e --- /dev/null +++ b/lib/src/precompiled/cli/cli.dart @@ -0,0 +1,56 @@ +import 'dart:io'; + +import 'commands/gen_key.dart' as gen_key; +import 'commands/hash.dart' as hash; +import 'commands/precompile_binaries.dart' as precompile_binaries; +import 'commands/sign.dart' as sign_cmd; +import 'commands/targets.dart' as targets; + +// Entry point for the precompiled tooling CLI. +Future runCli(List args) async { + if (args.isEmpty) { + _printUsage(); + exitCode = 2; + return; + } + + final command = args[0]; + final commandArgs = args.sublist(1); + + switch (command) { + case 'precompile-binaries': + await precompile_binaries.run(commandArgs); + return; + case 'hash': + await hash.run(commandArgs); + return; + case 'targets': + await targets.run(commandArgs); + return; + case 'sign': + await sign_cmd.run(commandArgs); + return; + case 'gen-key': + await gen_key.run(commandArgs); + return; + default: + stderr.writeln('Unknown command: $command'); + _printUsage(); + exitCode = 2; + } +} + +void _printUsage() { + stdout.writeln(''' +Usage: dart run bdk_dart [options] + +Commands: + precompile-binaries Precompile binaries for all targets + hash Compute crate hash + targets Resolve toolchain targets + sign Sign a file + gen-key Generate Ed25519 keypair + +Run 'dart run bdk_dart --help' for command-specific help. +'''); +} diff --git a/lib/src/precompiled/cli/commands/gen_key.dart b/lib/src/precompiled/cli/commands/gen_key.dart new file mode 100644 index 0000000..ce9912a --- /dev/null +++ b/lib/src/precompiled/cli/commands/gen_key.dart @@ -0,0 +1,16 @@ +import 'package:bdk_dart/src/precompiled/util.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; + +// Generate an Ed25519 keypair for signing. +Future run(List args) async { + if (args.contains('--help') || args.contains('-h')) { + return; + } + final kp = generateKey(); + final privateHex = hexEncode(kp.privateKey.bytes); + final publicHex = hexEncode(kp.publicKey.bytes); + // ignore: avoid_print + print('PRIVATE_KEY=$privateHex'); + // ignore: avoid_print + print('PUBLIC_KEY=$publicHex'); +} diff --git a/lib/src/precompiled/cli/commands/hash.dart b/lib/src/precompiled/cli/commands/hash.dart new file mode 100644 index 0000000..a94ff0a --- /dev/null +++ b/lib/src/precompiled/cli/commands/hash.dart @@ -0,0 +1,56 @@ +import 'dart:io'; + +import 'package:bdk_dart/src/precompiled/crate_hash.dart'; + +// Compute and print the crate hash for release tagging. +Future run(List args) async { + String? manifestDir; + var debug = false; + var list = false; + + for (final arg in args) { + if (arg == '--debug') { + debug = true; + continue; + } + if (arg == '--list') { + list = true; + continue; + } + if (arg.startsWith('--manifest-dir=')) { + manifestDir = arg.substring('--manifest-dir='.length).trim(); + continue; + } + if (arg == '--help' || arg == '-h') { + stdout.writeln('Usage: hash --manifest-dir= [--debug] [--list]'); + return; + } + if (arg.trim().isEmpty) continue; + stderr.writeln('Unknown argument: $arg'); + exitCode = 2; + return; + } + + if (manifestDir == null || manifestDir.trim().isEmpty) { + stderr.writeln('Missing --manifest-dir'); + exitCode = 2; + return; + } + if (!Directory(manifestDir).existsSync()) { + stderr.writeln('Manifest directory not found: $manifestDir'); + exitCode = 1; + return; + } + + if (debug || list) { + final files = CrateHash.collectFiles(manifestDir); + for (final file in files) { + if (list) { + stderr.writeln(file.path); + } else { + stderr.writeln('${file.path}'); + } + } + } + stdout.write(CrateHash.compute(manifestDir)); +} diff --git a/lib/src/precompiled/cli/commands/precompile_binaries.dart b/lib/src/precompiled/cli/commands/precompile_binaries.dart new file mode 100644 index 0000000..af03a2f --- /dev/null +++ b/lib/src/precompiled/cli/commands/precompile_binaries.dart @@ -0,0 +1,457 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:bdk_dart/src/precompiled/crate_hash.dart'; +import 'package:bdk_dart/src/precompiled/rust_toolchain.dart'; +import 'package:bdk_dart/src/precompiled/util.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:path/path.dart' as p; + +import '../support/os.dart'; +import '../support/process.dart'; + +// Build, sign, and upload precompiled artifacts to a GitHub release. +Future run(List args) async { + String? manifestDir; + String? cratePackage; + String? repository; + String? os; + String? androidSdkLocation; + String? androidNdkVersion; + String? androidMinSdkVersion; + var verbose = false; + + // Basic argument parsing. + for (final arg in args) { + if (arg == '--help' || arg == '-h') { + stdout.writeln(''' +Usage: precompile-binaries [options] + +Options: + --manifest-dir= Rust crate directory (required) + --crate-package= Cargo package name (required) + --repository= GitHub repository for releases (required) + --os= macos | linux | windows | android | ios | all (default: current) + --android-sdk-location= Android SDK root (required when --os=android) + --android-ndk-version= NDK version folder name (required when --os=android) + --android-min-sdk-version= Android minSdkVersion (required when --os=android) + -v, --verbose Verbose output +'''); + return; + } + if (arg == '-v' || arg == '--verbose') { + verbose = true; + continue; + } + if (arg.startsWith('--manifest-dir=')) { + manifestDir = arg.substring('--manifest-dir='.length).trim(); + continue; + } + if (arg.startsWith('--crate-package=')) { + cratePackage = arg.substring('--crate-package='.length).trim(); + continue; + } + if (arg.startsWith('--repository=')) { + repository = arg.substring('--repository='.length).trim(); + continue; + } + if (arg.startsWith('--os=')) { + os = arg.substring('--os='.length).trim(); + continue; + } + if (arg.startsWith('--android-sdk-location=')) { + androidSdkLocation = arg + .substring('--android-sdk-location='.length) + .trim(); + continue; + } + if (arg.startsWith('--android-ndk-version=')) { + androidNdkVersion = arg.substring('--android-ndk-version='.length).trim(); + continue; + } + if (arg.startsWith('--android-min-sdk-version=')) { + androidMinSdkVersion = arg + .substring('--android-min-sdk-version='.length) + .trim(); + continue; + } + if (arg.trim().isEmpty) continue; + stderr.writeln('Unknown argument: $arg'); + exitCode = 2; + return; + } + + if (manifestDir == null || manifestDir.trim().isEmpty) { + stderr.writeln('Missing --manifest-dir'); + exitCode = 2; + return; + } + final manifestDirNormalized = manifestDir.trim(); + final manifestPath = Directory(manifestDirNormalized).absolute.path; + if (cratePackage == null || cratePackage.trim().isEmpty) { + stderr.writeln('Missing --crate-package'); + exitCode = 2; + return; + } + if (repository == null || repository.trim().isEmpty) { + stderr.writeln('Missing --repository'); + exitCode = 2; + return; + } + os ??= Platform.operatingSystem; + + final normalizedOs = normalizeOs(os); + if (normalizedOs == null && os.trim().toLowerCase() != 'all') { + stderr.writeln('Unsupported --os=$os'); + exitCode = 2; + return; + } + + if (verbose) { + stderr.writeln( + 'Precompiling binaries for manifest: $manifestDirNormalized', + ); + stderr.writeln('Crate package: $cratePackage'); + stderr.writeln('Repository: $repository'); + stderr.writeln('OS filter: ${normalizedOs ?? 'all'}'); + } + + // Load toolchain + derive crate hash for the release tag. + final toolchain = RustToolchain.load(manifestPath); + final crateHash = CrateHash.compute(manifestPath); + final tag = 'precompiled_$crateHash'; + + // Signing key used for artifact verification. + final privateKeyHex = Platform.environment['PRIVATE_KEY']; + if (privateKeyHex == null) { + stderr.writeln('Missing PRIVATE_KEY environment variable'); + exitCode = 2; + return; + } + final privateKeyBytes = decodeHex(privateKeyHex); + if (privateKeyBytes.length != 64) { + stderr.writeln('PRIVATE_KEY must be 64 bytes (hex-encoded)'); + exitCode = 2; + return; + } + final privateKey = PrivateKey(privateKeyBytes); + + // Required for GitHub release creation/upload. + final ghToken = + Platform.environment['GH_TOKEN'] ?? + Platform.environment['GITHUB_TOKEN'] ?? + Platform.environment['GITHUB_TOKEN'.toUpperCase()]; + if (ghToken == null || ghToken.trim().isEmpty) { + stderr.writeln('Missing GH_TOKEN/GITHUB_TOKEN for GitHub release upload'); + exitCode = 2; + return; + } + + final targets = normalizedOs == null + ? toolchain.targets + : toolchain.targetsForOs(normalizedOs); + if (targets.isEmpty) { + stderr.writeln( + 'No toolchain targets found for os=${normalizedOs ?? 'all'}', + ); + exitCode = 1; + return; + } + + String? abiForTarget(String t) => switch (t) { + 'armv7-linux-androideabi' => 'armeabi-v7a', + 'aarch64-linux-android' => 'arm64-v8a', + 'x86_64-linux-android' => 'x86_64', + _ => null, + }; + + final buildableTargets = normalizedOs == 'android' + ? targets.where((t) => abiForTarget(t) != null).toList(growable: false) + : List.from(targets); + + // If release already contains assets, avoid duplicate builds. + final releaseHasAssets = buildableTargets.isNotEmpty + ? await _releaseHasAllAssets( + tag: tag, + targets: buildableTargets, + repository: repository, + ) + : false; + + // Local build and upload staging folders. + final buildDir = Directory('precompiled_build'); + final uploadDir = Directory('precompiled_upload'); + final buildDirAbs = Directory( + p.join(Directory.current.absolute.path, buildDir.path), + ); + final uploadDirAbs = Directory( + p.join(Directory.current.absolute.path, uploadDir.path), + ); + + await _ensureReleaseExists( + tag: tag, + crateHash: crateHash, + repository: repository, + ); + if (releaseHasAssets) { + if (verbose) { + stderr.writeln( + 'Release $tag already contains assets for ${buildableTargets.join(', ')}; skipping build.', + ); + } + return; + } + + if (buildDirAbs.existsSync()) buildDirAbs.deleteSync(recursive: true); + if (uploadDirAbs.existsSync()) uploadDirAbs.deleteSync(recursive: true); + buildDirAbs.createSync(recursive: true); + uploadDirAbs.createSync(recursive: true); + + // Android builds use cargo-ndk with ABI mapping. + if (normalizedOs == 'android') { + if (androidSdkLocation == null || androidSdkLocation.trim().isEmpty) { + stderr.writeln( + 'Missing --android-sdk-location (required for --os=android)', + ); + exitCode = 2; + return; + } + if (androidNdkVersion == null || androidNdkVersion.trim().isEmpty) { + stderr.writeln( + 'Missing --android-ndk-version (required for --os=android)', + ); + exitCode = 2; + return; + } + if (androidMinSdkVersion == null || androidMinSdkVersion.trim().isEmpty) { + stderr.writeln( + 'Missing --android-min-sdk-version (required for --os=android)', + ); + exitCode = 2; + return; + } + + final ndkHome = '$androidSdkLocation/ndk/$androidNdkVersion'; + final env = { + ...Platform.environment, + 'ANDROID_SDK_ROOT': androidSdkLocation, + 'ANDROID_NDK_HOME': ndkHome, + 'ANDROID_NDK_ROOT': ndkHome, + }; + + final buildOut = Directory(p.join(buildDirAbs.path, 'android')); + buildOut.createSync(recursive: true); + + String? abiForTarget(String t) => switch (t) { + 'armv7-linux-androideabi' => 'armeabi-v7a', + 'aarch64-linux-android' => 'arm64-v8a', + 'x86_64-linux-android' => 'x86_64', + _ => null, + }; + + for (final target in buildableTargets) { + final abi = abiForTarget(target)!; + if (verbose) + stderr.writeln('Building Android target: $target (abi=$abi)'); + + await runOrThrow( + 'rustup', + ['target', 'add', target, '--toolchain', toolchain.channel], + verbose: verbose, + environment: env, + ); + + await runOrThrow( + 'rustup', + [ + 'run', + toolchain.channel, + 'cargo', + 'ndk', + '--platform', + androidMinSdkVersion, + '-t', + abi, + '-o', + buildOut.absolute.path, + 'build', + '--manifest-path', + '$manifestPath/Cargo.toml', + '--package', + cratePackage, + '--release', + '--locked', + ], + verbose: verbose, + environment: env, + workingDirectory: manifestPath, + ); + + final soPath = p.join(buildOut.path, abi, 'lib$cratePackage.so'); + final soFile = File(soPath); + if (!soFile.existsSync()) { + stderr.writeln('Expected Android artifact not found: $soPath'); + exitCode = 1; + return; + } + + final outPath = p.join( + uploadDirAbs.path, + '${target}_lib$cratePackage.so', + ); + final outFile = File(outPath)..writeAsBytesSync(soFile.readAsBytesSync()); + final sig = sign(privateKey, outFile.readAsBytesSync()); + File('$outPath.sig').writeAsBytesSync(sig); + if (verbose) stderr.writeln('Prepared: $outPath (+ .sig)'); + } + } else { + for (final target in buildableTargets) { + if (verbose) stderr.writeln('Building target: $target'); + await runOrThrow('rustup', [ + 'target', + 'add', + target, + '--toolchain', + toolchain.channel, + ], verbose: verbose); + await runOrThrow( + 'rustup', + [ + 'run', + toolchain.channel, + 'cargo', + 'build', + '--manifest-path', + '$manifestPath/Cargo.toml', + '--package', + cratePackage, + '--release', + '--locked', + '--target', + target, + '--target-dir', + buildDirAbs.path, + ], + verbose: verbose, + workingDirectory: manifestPath, + ); + + final artDir = Directory(p.join(buildDirAbs.path, target, 'release')); + if (!artDir.existsSync()) { + stderr.writeln('Missing artifact directory: ${artDir.path}'); + exitCode = 1; + return; + } + + final artifacts = []; + for (final ent in artDir.listSync(followLinks: false)) { + if (ent is! File) continue; + final name = ent.uri.pathSegments.last; + final ok = + name.startsWith('lib$cratePackage.') || + name.startsWith('$cratePackage.') || + name == 'lib$cratePackage.a'; + if (ok) artifacts.add(ent); + } + + if (artifacts.isEmpty) { + stderr.writeln( + 'No artifacts found in ${artDir.path} for $cratePackage', + ); + exitCode = 1; + return; + } + + for (final file in artifacts) { + final base = file.uri.pathSegments.last; + final outPath = p.join(uploadDirAbs.path, '${target}_$base'); + final outFile = File(outPath); + outFile.writeAsBytesSync(file.readAsBytesSync()); + final sig = sign(privateKey, outFile.readAsBytesSync()); + File('$outPath.sig').writeAsBytesSync(sig); + if (verbose) stderr.writeln('Prepared: $outPath (+ .sig)'); + } + } + } + + // Upload all artifacts for this hash. + await runOrThrow('gh', [ + 'release', + 'upload', + '--repo', + repository, + tag, + '${uploadDirAbs.path}/*', + '--clobber', + ], verbose: verbose); +} + +Future _ensureReleaseExists({ + required String tag, + required String crateHash, + required String repository, +}) async { + final view = await Process.run('gh', ['release', 'view', tag]); + if (view.exitCode == 0) return; + final create = await Process.run('gh', [ + 'release', + 'create', + tag, + '--title', + 'Precompiled binaries $crateHash', + '--repo', + repository, + '--notes', + 'Precompiled binaries for crate hash $crateHash.', + ]); + stdout.write(create.stdout); + stderr.write(create.stderr); + if (create.exitCode != 0) { + exitCode = create.exitCode; + throw StateError('Failed to create release $tag'); + } +} + +Future _releaseHasAllAssets({ + required String tag, + required List targets, + required String repository, +}) async { + final view = await Process.run('gh', [ + 'release', + 'view', + tag, + '--repo', + repository, + '--json', + 'assets', + '--jq', + '.assets[].name', + ]); + if (view.exitCode != 0) { + return false; + } + final output = view.stdout.toString(); + final assets = LineSplitter.split( + output, + ).map((name) => name.trim()).where((name) => name.isNotEmpty).toSet(); + if (assets.isEmpty) { + return false; + } + + for (final target in targets) { + final prefix = '${target}_'; + final binaryNames = assets + .where((name) => name.startsWith(prefix) && !name.endsWith('.sig')) + .toList(); + if (binaryNames.isEmpty) { + return false; + } + final hasPair = binaryNames.any((name) => assets.contains('$name.sig')); + if (!hasPair) { + return false; + } + } + + return true; +} diff --git a/lib/src/precompiled/cli/commands/sign.dart b/lib/src/precompiled/cli/commands/sign.dart new file mode 100644 index 0000000..ebfffd7 --- /dev/null +++ b/lib/src/precompiled/cli/commands/sign.dart @@ -0,0 +1,41 @@ +import 'dart:io'; + +import 'package:bdk_dart/src/precompiled/util.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; + +// Sign a binary file using the PRIVATE_KEY env var. +Future run(List args) async { + if (args.length != 2 || args.contains('--help') || args.contains('-h')) { + stderr.writeln('Usage: sign '); + if (args.contains('--help') || args.contains('-h')) return; + exitCode = 2; + return; + } + + final privateKeyHex = Platform.environment['PRIVATE_KEY']; + if (privateKeyHex == null) { + stderr.writeln('Missing PRIVATE_KEY environment variable'); + exitCode = 2; + return; + } + final privateKeyBytes = decodeHex(privateKeyHex); + if (privateKeyBytes.length != 64) { + stderr.writeln('PRIVATE_KEY must be 64 bytes (hex-encoded)'); + exitCode = 2; + return; + } + + final inputFile = File(args[0]); + if (!inputFile.existsSync()) { + stderr.writeln('Input file does not exist: ${inputFile.path}'); + exitCode = 1; + return; + } + + final outFile = File(args[1]); + outFile.parent.createSync(recursive: true); + + final data = inputFile.readAsBytesSync(); + final signature = sign(PrivateKey(privateKeyBytes), data); + outFile.writeAsBytesSync(signature); +} diff --git a/lib/src/precompiled/cli/commands/targets.dart b/lib/src/precompiled/cli/commands/targets.dart new file mode 100644 index 0000000..4cdbb2d --- /dev/null +++ b/lib/src/precompiled/cli/commands/targets.dart @@ -0,0 +1,69 @@ +import 'dart:io'; + +import 'package:bdk_dart/src/precompiled/rust_toolchain.dart'; + +import '../support/os.dart'; + +Future run(List args) async { + String? os; + String? manifestDir; + var printChannel = false; + + for (final arg in args) { + if (arg.startsWith('--os=')) { + os = arg.substring('--os='.length).trim(); + continue; + } + if (arg.startsWith('--manifest-dir=')) { + manifestDir = arg.substring('--manifest-dir='.length).trim(); + continue; + } + if (arg == '--channel') { + printChannel = true; + continue; + } + if (arg == '--help' || arg == '-h') { + stdout.writeln( + 'Usage: targets --manifest-dir= [--os=macos] [--channel]', + ); + return; + } + if (arg.trim().isEmpty) continue; + stderr.writeln('Unknown argument: $arg'); + exitCode = 2; + return; + } + + if (manifestDir == null || manifestDir.trim().isEmpty) { + stderr.writeln('Missing --manifest-dir'); + exitCode = 2; + return; + } + + try { + final toolchain = RustToolchain.load(manifestDir); + if (printChannel) { + stdout.write(toolchain.channel); + return; + } + final targets = toolchain.targets; + if (targets.isEmpty) { + stderr.writeln('No targets found in rust-toolchain.toml'); + exitCode = 1; + return; + } + final normalized = normalizeOs(os ?? Platform.operatingSystem); + final filtered = normalized == null + ? targets + : toolchain.targetsForOs(normalized); + if (filtered.isEmpty) { + stderr.writeln('No targets match os=${normalized ?? 'all'}'); + exitCode = 1; + return; + } + stdout.write(filtered.join(' ')); + } catch (e) { + stderr.writeln('Error: $e'); + exitCode = 1; + } +} diff --git a/lib/src/precompiled/cli/support/os.dart b/lib/src/precompiled/cli/support/os.dart new file mode 100644 index 0000000..87e68be --- /dev/null +++ b/lib/src/precompiled/cli/support/os.dart @@ -0,0 +1,13 @@ +// Normalize OS inputs for CLI filtering. +String? normalizeOs(String raw) { + final v = raw.trim().toLowerCase(); + return switch (v) { + 'linux' || 'ubuntu-latest' => 'linux', + 'macos' || 'darwin' || 'macos-latest' => 'macos', + 'windows' || 'windows-latest' => 'windows', + 'android' => 'android', + 'ios' => 'ios', + 'all' => null, + _ => v.isEmpty ? null : v, + }; +} diff --git a/lib/src/precompiled/cli/support/process.dart b/lib/src/precompiled/cli/support/process.dart new file mode 100644 index 0000000..c590774 --- /dev/null +++ b/lib/src/precompiled/cli/support/process.dart @@ -0,0 +1,24 @@ +import 'dart:io'; + +// Run a process, echoing output, and throw on non-zero exit. +Future runOrThrow( + String exe, + List args, { + required bool verbose, + Map? environment, + String? workingDirectory, +}) async { + if (verbose) stderr.writeln('> $exe ${args.join(' ')}'); + final res = await Process.run( + exe, + args, + environment: environment, + workingDirectory: workingDirectory, + ); + stdout.write(res.stdout); + stderr.write(res.stderr); + if (res.exitCode != 0) { + exitCode = res.exitCode; + throw StateError('Command failed: $exe ${args.join(' ')}'); + } +} diff --git a/lib/src/precompiled/crate_hash.dart b/lib/src/precompiled/crate_hash.dart new file mode 100644 index 0000000..fb30ec9 --- /dev/null +++ b/lib/src/precompiled/crate_hash.dart @@ -0,0 +1,173 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; +import 'package:path/path.dart' as path; +import 'package:yaml/yaml.dart'; + +// Computes a stable hash for crate inputs used in precompiled releases. +class CrateHash { + // Compute (and optionally cache) the crate hash. + static String compute(String manifestDir, {String? tempStorage}) { + return CrateHash._( + manifestDir: manifestDir, + tempStorage: tempStorage, + )._compute(); + } + + CrateHash._({required this.manifestDir, required this.tempStorage}); + + final String manifestDir; + final String? tempStorage; + + // Collect all files that participate in the crate hash. + static List collectFiles(String manifestDir) { + return CrateHash._(manifestDir: manifestDir, tempStorage: null)._getFiles(); + } + + String _compute() { + final files = _getFiles(); + final tempStorage = this.tempStorage; + if (tempStorage != null) { + // Quick hash keyed by file path + size + mtime to reuse cached full hash. + final quickHash = _computeQuickHash(files); + final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash')); + quickHashFolder.createSync(recursive: true); + final quickHashFile = File(path.join(quickHashFolder.path, quickHash)); + if (quickHashFile.existsSync()) { + return quickHashFile.readAsStringSync(); + } + final hash = _computeHash(files); + quickHashFile.writeAsStringSync(hash); + return hash; + } + return _computeHash(files); + } + + // Fast hash over file metadata to key the cache. + String _computeQuickHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + final data = ByteData(8); + for (final file in files) { + input.add(utf8.encode(file.path)); + final stat = file.statSync(); + data.setUint64(0, stat.size); + input.add(data.buffer.asUint8List()); + data.setUint64(0, stat.modified.millisecondsSinceEpoch); + input.add(data.buffer.asUint8List()); + } + + input.close(); + return base64Url.encode(output.events.single.bytes); + } + + // Deterministic content hash; normalizes pubspec precompiled config. + String _computeHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + void addTextFile(File file) { + final splitter = const LineSplitter(); + if (file.existsSync()) { + final data = file.readAsStringSync(); + final lines = splitter.convert(data); + for (final line in lines) { + input.add(utf8.encode(line)); + } + } + } + + // Include precompiled_binaries config so it invalidates releases. + void addPrecompiledBinariesFromPubspec(File file) { + if (!file.existsSync()) { + return; + } + final yamlContent = file.readAsStringSync(); + final doc = loadYaml(yamlContent); + final extensionSection = doc is YamlMap ? doc['bdk_dart'] : null; + final precompiled = extensionSection is YamlMap + ? extensionSection['precompiled_binaries'] + : null; + final normalized = _normalizeYaml(precompiled ?? {}); + input.add(utf8.encode('pubspec.yaml:bdk_dart.precompiled_binaries:')); + input.add(utf8.encode(jsonEncode(normalized))); + } + + // Find pubspec.yaml at package root (one level up from crate). + final rootDir = path.normalize(path.join(manifestDir, '../')); + final pubspecFile = File(path.join(rootDir, 'pubspec.yaml')); + addPrecompiledBinariesFromPubspec(pubspecFile); + + for (final file in files) { + addTextFile(file); + } + + input.close(); + final res = output.events.single; + final hash = res.bytes.sublist(0, 16); + return _hexEncode(hash); + } + + String _hexEncode(List bytes) { + final b = StringBuffer(); + for (final v in bytes) { + b.write(v.toRadixString(16).padLeft(2, '0')); + } + return b.toString(); + } + + // Normalize maps/lists for deterministic serialization. + Object? _normalizeYaml(Object? value) { + if (value is YamlMap) { + final keys = value.keys.map((key) => key.toString()).toList()..sort(); + final result = {}; + for (final key in keys) { + result[key] = _normalizeYaml(value[key]); + } + return result; + } + if (value is YamlList) { + return value.map(_normalizeYaml).toList(); + } + if (value is Map) { + final keys = value.keys.map((key) => key.toString()).toList()..sort(); + final result = {}; + for (final key in keys) { + result[key] = _normalizeYaml(value[key]); + } + return result; + } + if (value is List) { + return value.map(_normalizeYaml).toList(); + } + return value; + } + + // Collect all source and manifest files that affect the build. + List _getFiles() { + final src = Directory(path.join(manifestDir, 'src')); + final files = src.existsSync() + ? src + .listSync(recursive: true, followLinks: false) + .whereType() + .toList() + : []; + files.sort((a, b) => a.path.compareTo(b.path)); + + void addFileInCrate(String relative) { + final file = File(path.join(manifestDir, relative)); + if (file.existsSync()) { + files.add(file); + } + } + + addFileInCrate('Cargo.toml'); + addFileInCrate('Cargo.lock'); + addFileInCrate('build.rs'); + return files; + } +} diff --git a/lib/src/precompiled/options.dart b/lib/src/precompiled/options.dart new file mode 100644 index 0000000..62a231d --- /dev/null +++ b/lib/src/precompiled/options.dart @@ -0,0 +1,264 @@ +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:path/path.dart' as path; +import 'package:yaml/yaml.dart'; + +import 'util.dart'; + +enum PrecompiledBinaryMode { auto, always, never } + +class PrecompiledBinariesConfig { + PrecompiledBinariesConfig({ + required this.artifactHost, + required this.mode, + required this.publicKey, + this.urlPrefix, + }); + + final String artifactHost; + final PrecompiledBinaryMode mode; + final String? urlPrefix; + final PublicKey publicKey; + + Uri fileUrl({required String crateHash, required String fileName}) { + final prefix = urlPrefix; + if (prefix != null && prefix.isNotEmpty) { + return Uri.parse('$prefix$crateHash/$fileName'); + } + final tag = 'precompiled_$crateHash'; + return Uri.parse( + 'https://github.com/$artifactHost/releases/download/$tag/$fileName', + ); + } + + static PrecompiledBinariesConfig parse(YamlNode node) { + if (node is! YamlMap) { + throw FormatException('precompiled_binaries must be a map'); + } + + String? urlPrefix; + final urlPrefixNode = node.nodes['url_prefix']; + if (urlPrefixNode != null) { + if (urlPrefixNode is! YamlScalar || urlPrefixNode.value is! String) { + throw FormatException( + 'precompiled_binaries.url_prefix must be a string', + ); + } + urlPrefix = urlPrefixNode.value as String; + } + + PrecompiledBinaryMode mode = PrecompiledBinaryMode.auto; + final modeNode = node.nodes['mode']; + if (modeNode != null) { + if (modeNode is! YamlScalar || modeNode.value is! String) { + throw FormatException('precompiled_binaries.mode must be a string'); + } + final m = (modeNode.value as String).trim(); + final parsed = _parsePrecompiledBinaryMode(m); + if (parsed == null) { + throw FormatException( + 'precompiled_binaries.mode must be one of: auto, always, never (aliases: download->always, build->never)', + ); + } + mode = parsed; + } + + final artifactHostNode = node.nodes['artifact_host']; + final releaseRepoNode = node.nodes['release_repo']; + + final publicKeyNode = node.nodes['public_key']; + + String? artifactHost; + if (artifactHostNode != null) { + if (artifactHostNode is! YamlScalar || + artifactHostNode.value is! String) { + throw FormatException( + 'precompiled_binaries.artifact_host must be a string', + ); + } + artifactHost = (artifactHostNode.value as String).trim(); + } + if (artifactHost == null && releaseRepoNode != null) { + if (releaseRepoNode is! YamlScalar || releaseRepoNode.value is! String) { + throw FormatException( + 'precompiled_binaries.release_repo must be a string', + ); + } + artifactHost = (releaseRepoNode.value as String).trim(); + } + + if ((urlPrefix == null || urlPrefix.isEmpty) && + (artifactHost == null || artifactHost.isEmpty)) { + throw FormatException( + 'precompiled_binaries must specify either url_prefix or artifact_host', + ); + } + + artifactHost ??= ''; + final normalizedArtifactHost = artifactHost.isEmpty + ? '' + : _normalizeOwnerRepo(artifactHost); + if (artifactHost.isNotEmpty && normalizedArtifactHost == null) { + throw FormatException( + 'precompiled_binaries.artifact_host must be in owner/repo format (or github.com/owner/repo)', + ); + } + + if (publicKeyNode is! YamlScalar || publicKeyNode.value is! String) { + throw FormatException('precompiled_binaries.public_key must be a string'); + } + final keyBytes = decodeHex(publicKeyNode.value as String); + if (keyBytes.length != 32) { + throw FormatException('public_key must be 32 bytes'); + } + return PrecompiledBinariesConfig( + artifactHost: normalizedArtifactHost ?? '', + mode: mode, + publicKey: PublicKey(keyBytes), + urlPrefix: urlPrefix, + ); + } +} + +String? _normalizeOwnerRepo(String raw) { + var v = raw.trim(); + v = v.replaceFirst(RegExp(r'^https?://'), ''); + v = v.replaceFirst(RegExp(r'^github\.com/'), ''); + v = v.replaceAll(RegExp(r'/+$'), ''); + final parts = v.split('/'); + if (parts.length != 2) { + return null; + } + if (parts[0].isEmpty || parts[1].isEmpty) { + return null; + } + return '${parts[0]}/${parts[1]}'; +} + +class PubspecOptions { + PubspecOptions({required this.precompiledBinaries}); + + final PrecompiledBinariesConfig? precompiledBinaries; + + static YamlNode? _findPrecompiledBinariesNode( + YamlMap root, { + required String configKey, + }) { + final key = configKey.trim(); + if (key.isEmpty) { + return null; + } + final packageNode = root.nodes[key]; + if (packageNode is! YamlMap) { + return null; + } + return packageNode.nodes['precompiled_binaries']; + } + + static PrecompiledBinaryMode? loadModeOverride({ + required String packageRoot, + required String packageName, + }) { + final file = File(path.join(packageRoot, 'pubspec.yaml')); + if (!file.existsSync()) { + return null; + } + final root = loadYamlNode(file.readAsStringSync(), sourceUrl: file.uri); + if (root is! YamlMap) { + throw FormatException('pubspec.yaml must be a map'); + } + + final node = _findPrecompiledBinariesNode(root, configKey: packageName); + if (node == null) { + return null; + } + if (node is! YamlMap) { + throw FormatException('precompiled_binaries must be a map'); + } + + final modeNode = node.nodes['mode']; + if (modeNode == null) { + return null; + } + if (modeNode is! YamlScalar || modeNode.value is! String) { + throw FormatException('precompiled_binaries.mode must be a string'); + } + + final m = (modeNode.value as String).trim(); + final parsed = _parsePrecompiledBinaryMode(m); + if (parsed == null) { + throw FormatException( + 'precompiled_binaries.mode must be one of: auto, always, never (aliases: download->always, build->never)', + ); + } + return parsed; + } + + static PubspecOptions load({ + required String packageRoot, + required String pluginConfigKey, + }) { + final file = File(path.join(packageRoot, 'pubspec.yaml')); + if (!file.existsSync()) { + return PubspecOptions(precompiledBinaries: null); + } + final root = loadYamlNode(file.readAsStringSync(), sourceUrl: file.uri); + if (root is! YamlMap) { + throw FormatException('pubspec.yaml must be a map'); + } + final node = _findPrecompiledBinariesNode(root, configKey: pluginConfigKey); + + if (node == null) { + return PubspecOptions(precompiledBinaries: null); + } + + return PubspecOptions( + precompiledBinaries: PrecompiledBinariesConfig.parse(node), + ); + } +} + +class UserOptions { + UserOptions({required this.usePrecompiledBinaries}); + + final bool usePrecompiledBinaries; + + static bool _rustupExists() { + final envPath = Platform.environment['PATH']; + final envPathSeparator = Platform.isWindows ? ';' : ':'; + final home = Platform.isWindows + ? Platform.environment['USERPROFILE'] + : Platform.environment['HOME']; + final paths = [ + if (home != null) path.join(home, '.cargo', 'bin'), + if (envPath != null) ...envPath.split(envPathSeparator), + ]; + for (final p in paths) { + final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup'; + if (File(path.join(p, rustup)).existsSync()) { + return true; + } + } + return false; + } + + static bool defaultUsePrecompiledBinaries() => !_rustupExists(); + + static UserOptions load({required bool hasConfig}) { + if (!hasConfig) { + return UserOptions(usePrecompiledBinaries: false); + } + return UserOptions(usePrecompiledBinaries: defaultUsePrecompiledBinaries()); + } +} + +PrecompiledBinaryMode? _parsePrecompiledBinaryMode(String raw) { + final v = raw.trim().toLowerCase(); + return switch (v) { + 'auto' => PrecompiledBinaryMode.auto, + 'always' || 'download' => PrecompiledBinaryMode.always, + 'never' || 'build' || 'off' || 'disabled' => PrecompiledBinaryMode.never, + _ => null, + }; +} diff --git a/lib/src/precompiled/precompiled_builder.dart b/lib/src/precompiled/precompiled_builder.dart new file mode 100644 index 0000000..2520602 --- /dev/null +++ b/lib/src/precompiled/precompiled_builder.dart @@ -0,0 +1,133 @@ +import 'dart:io'; + +import 'package:code_assets/code_assets.dart'; +import 'package:hooks/hooks.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'target.dart'; + +// Build hook that prefers signed precompiled binaries, with local fallback. +final _log = Logger('bdk_dart.precompiled_builder'); + +// Callback used when no precompiled artifact is available. +typedef FallbackBuilder = + Future Function( + BuildInput input, + BuildOutputBuilder output, + List assetRouting, + Logger? logger, + ); + +// Builder that downloads verified artifacts when available. +final class PrecompiledBuilder implements Builder { + const PrecompiledBuilder({ + required this.assetName, + required this.fallback, + this.cratePath, + this.buildModeName = 'release', + }); + + final String assetName; + final FallbackBuilder fallback; + final String? cratePath; + final String buildModeName; + + @override + Future run({ + required BuildInput input, + required BuildOutputBuilder output, + List assetRouting = const [ToAppBundle()], + Logger? logger, + }) async { + // Configure logging once for the build process. + _initLogging(); + if (!input.config.buildCodeAssets) { + return; + } + + logger ??= Logger('bdk_dart.PrecompiledBuilder'); + + // Resolve crate directory from the package root. + final crateDirectory = _resolveCrateDirectory( + rootPath: path.fromUri(input.packageRoot), + cratePathOptions: cratePath != null ? [cratePath!] : ['native', 'rust'], + ); + + // Provider handles download + signature verification. + final provider = PrecompiledArtifactProvider( + input: input, + buildModeName: buildModeName, + crateDir: crateDirectory, + ); + + final downloaded = await provider.tryGetPrecompiledArtifact(); + if (downloaded != null) { + for (final dep in downloaded.dependencies) { + output.dependencies.add(dep); + } + + final codeConfig = input.config.code; + final linkMode = codeConfig.linkMode; + + // Register the verified asset under each routing. + for (final routing in assetRouting) { + output.assets.code.add( + CodeAsset( + package: input.packageName, + name: assetName, + linkMode: linkMode, + file: File(downloaded.filePath).absolute.uri, + ), + routing: routing, + ); + } + _log.info('Using precompiled binary for ${codeConfig.targetTriple}'); + return; + } + + _log.info( + 'Falling back to local build for ${input.config.code.targetTriple}', + ); + await fallback(input, output, assetRouting, logger); + } + + // Locate the Rust crate folder based on common paths or override. + Directory _resolveCrateDirectory({ + required String rootPath, + required List cratePathOptions, + }) { + for (final option in cratePathOptions) { + final dir = Directory(path.join(rootPath, option)); + if (dir.existsSync()) { + return dir; + } + } + throw StateError( + 'Could not find crate directory. Checked: $cratePathOptions at $rootPath', + ); + } +} + +// Prevent duplicate log handlers across multiple invocations. +bool _loggingInitialized = false; + +void _initLogging() { + if (_loggingInitialized) return; + _loggingInitialized = true; + + // Verbose mode is opt-in via env var to avoid noisy builds. + final verbose = Platform.environment['BDK_DART_PRECOMPILED_VERBOSE'] == '1'; + Logger.root.level = verbose ? Level.ALL : Level.INFO; + Logger.root.onRecord.listen((rec) { + final out = rec.level >= Level.WARNING ? stderr : stdout; + out.writeln('${rec.level.name}: ${rec.message}'); + if (rec.error != null) { + out.writeln(rec.error); + } + if (rec.stackTrace != null && verbose) { + out.writeln(rec.stackTrace); + } + }); +} diff --git a/lib/src/precompiled/rust_toolchain.dart b/lib/src/precompiled/rust_toolchain.dart new file mode 100644 index 0000000..328b1ab --- /dev/null +++ b/lib/src/precompiled/rust_toolchain.dart @@ -0,0 +1,76 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:toml/toml.dart'; + +// Parses rust-toolchain.toml and exposes channel/targets. +class RustToolchain { + RustToolchain._({required this.channel, required this.targets}); + + final String channel; + final List targets; + + // Load rust-toolchain.toml from a crate directory. + static RustToolchain load(String manifestDir) { + final file = File(path.join(manifestDir, 'rust-toolchain.toml')); + if (!file.existsSync()) { + throw StateError('rust-toolchain.toml not found: ${file.path}'); + } + return _parseToolchain(file.path); + } + + // Parse the toolchain TOML into a RustToolchain instance. + static RustToolchain _parseToolchain(String toolchainTomlPath) { + final doc = TomlDocument.loadSync(toolchainTomlPath).toMap(); + final toolchain = doc['toolchain']; + if (toolchain is! Map) { + throw FormatException('Missing [toolchain] table in $toolchainTomlPath'); + } + final channel = toolchain['channel']; + if (channel is! String || channel.trim().isEmpty) { + throw FormatException('Missing toolchain.channel in $toolchainTomlPath'); + } + final targetsRaw = toolchain['targets']; + final targets = targetsRaw is List + ? targetsRaw.whereType().toList(growable: false) + : const []; + return RustToolchain._(channel: channel.trim(), targets: targets); + } + + // Filter targets for a given OS label. + List targetsForOs(String os) { + final normalized = _normalizeOs(os); + if (normalized == null) { + return targets; + } + return _filterTargets(targets, normalized); + } + + // Normalize OS aliases used by CLI/workflows. + static String? _normalizeOs(String raw) { + final v = raw.trim().toLowerCase(); + return switch (v) { + 'linux' || 'ubuntu-latest' => 'linux', + 'macos' || 'darwin' || 'macos-latest' => 'macos', + 'windows' || 'windows-latest' => 'windows', + 'android' => 'android', + 'ios' => 'ios', + 'all' => null, + _ => v.isEmpty ? null : v, + }; + } + + // Apply OS-specific target filtering. + static List _filterTargets(List targets, String os) { + bool include(String t) => switch (os) { + 'macos' => t.endsWith('apple-darwin') || t.contains('apple-ios'), + 'ios' => t.contains('apple-ios'), + 'linux' => t.endsWith('unknown-linux-gnu'), + 'windows' => t.endsWith('pc-windows-msvc'), + 'android' => + t.endsWith('linux-android') || t.endsWith('linux-androideabi'), + _ => false, + }; + return targets.where(include).toList(growable: false); + } +} diff --git a/lib/src/precompiled/target.dart b/lib/src/precompiled/target.dart new file mode 100644 index 0000000..a38a968 --- /dev/null +++ b/lib/src/precompiled/target.dart @@ -0,0 +1,41 @@ +import 'package:code_assets/code_assets.dart'; + +// Maps Dart build config to Rust target triples and link modes. +extension CodeConfigTargetTriple on CodeConfig { + // Derive Rust target triple from OS/arch. + String get targetTriple { + return switch ((targetOS, targetArchitecture)) { + (OS.android, Architecture.arm64) => 'aarch64-linux-android', + (OS.android, Architecture.arm) => 'armv7-linux-androideabi', + (OS.android, Architecture.x64) => 'x86_64-linux-android', + (OS.iOS, Architecture.arm64) + when iOS.targetSdk == IOSSdk.iPhoneSimulator => + 'aarch64-apple-ios-sim', + (OS.iOS, Architecture.arm64) when iOS.targetSdk == IOSSdk.iPhoneOS => + 'aarch64-apple-ios', + (OS.iOS, Architecture.x64) => 'x86_64-apple-ios', + (OS.windows, Architecture.arm64) => 'aarch64-pc-windows-msvc', + (OS.windows, Architecture.x64) => 'x86_64-pc-windows-msvc', + (OS.linux, Architecture.arm64) => 'aarch64-unknown-linux-gnu', + (OS.linux, Architecture.x64) => 'x86_64-unknown-linux-gnu', + (OS.macOS, Architecture.arm64) => 'aarch64-apple-darwin', + (OS.macOS, Architecture.x64) => 'x86_64-apple-darwin', + (_, _) => throw UnsupportedError( + 'Unsupported target: $targetOS on $targetArchitecture', + ), + }; + } + + // Resolve link mode from code asset preferences. + LinkMode get linkMode { + return switch (linkModePreference) { + LinkModePreference.dynamic || + LinkModePreference.preferDynamic => DynamicLoadingBundled(), + LinkModePreference.static || + LinkModePreference.preferStatic => StaticLinking(), + _ => throw UnsupportedError( + 'Unsupported LinkModePreference: $linkModePreference', + ), + }; + } +} diff --git a/lib/src/precompiled/util.dart b/lib/src/precompiled/util.dart new file mode 100644 index 0000000..7548ef7 --- /dev/null +++ b/lib/src/precompiled/util.dart @@ -0,0 +1,79 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:http/http.dart' as http; + +// Parse hex-encoded bytes from configuration/secrets. +List decodeHex(String hex) { + final normalized = hex.trim().toLowerCase(); + if (normalized.length.isOdd) { + throw FormatException('Invalid hex length'); + } + final out = []; + for (var i = 0; i < normalized.length; i += 2) { + final byte = int.parse(normalized.substring(i, i + 2), radix: 16); + out.add(byte); + } + return out; +} + +// Encode raw bytes as lowercase hex. +String hexEncode(List bytes) { + final b = StringBuffer(); + for (final v in bytes) { + b.write(v.toRadixString(16).padLeft(2, '0')); + } + return b.toString(); +} + +// Retry GET requests on transient connection resets. +Future httpGetWithRetry( + Uri url, { + Map? headers, + int maxAttempts = 4, + Duration timeout = const Duration(seconds: 30), + Duration retryDelay = const Duration(seconds: 1), +}) async { + final client = http.Client(); + var attempt = 0; + try { + while (true) { + attempt++; + try { + return await client.get(url, headers: headers).timeout(timeout); + } on TimeoutException { + if (attempt >= maxAttempts) rethrow; + } on SocketException { + if (attempt >= maxAttempts) rethrow; + } on http.ClientException { + if (attempt >= maxAttempts) rethrow; + } on HttpException { + if (attempt >= maxAttempts) rethrow; + } + await Future.delayed(retryDelay); + } + } finally { + client.close(); + } +} + +Future writeBytesAtomically(File file, List bytes) async { + final directory = file.parent; + if (!directory.existsSync()) { + directory.createSync(recursive: true); + } + + final tempPath = '${file.path}.${DateTime.now().microsecondsSinceEpoch}.tmp'; + final tempFile = File(tempPath); + + await tempFile.writeAsBytes(bytes, flush: true); + + try { + await tempFile.rename(file.path); + } on FileSystemException { + if (file.existsSync()) { + await file.delete(); + } + await tempFile.rename(file.path); + } +} diff --git a/native/Cargo.lock b/native/Cargo.lock new file mode 100644 index 0000000..71053a5 --- /dev/null +++ b/native/Cargo.lock @@ -0,0 +1,1859 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "askama" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn 2.0.114", +] + +[[package]] +name = "askama_parser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals 0.3.0", + "bitcoin_hashes 0.14.1", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bdk-ffi" +version = "2.3.0-alpha.0" +source = "git+https://github.com/bitcoindevkit/bdk-ffi.git?branch=master#af2475e9eff9dd422c3346aa284a87e24a2bfba0" +dependencies = [ + "bdk_electrum", + "bdk_esplora", + "bdk_kyoto", + "bdk_wallet", + "thiserror", + "uniffi", +] + +[[package]] +name = "bdk_chain" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5d691fd092aacec7e05046b7d04897d58d6d65ed3152cb6cf65dababcfabed" +dependencies = [ + "bdk_core", + "bitcoin", + "miniscript", + "rusqlite", + "serde", +] + +[[package]] +name = "bdk_core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dbbe4aad0c898bfeb5253c222be3ea3dccfb380a07e72c87e3e4ed6664a6753" +dependencies = [ + "bitcoin", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "bdk_dart_ffi" +version = "2.3.0-alpha.0" +dependencies = [ + "bdk-ffi", + "camino", + "uniffi", + "uniffi-dart", +] + +[[package]] +name = "bdk_electrum" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b59a3f7fbe678874fa34354097644a171276e02a49934c13b3d61c54610ddf39" +dependencies = [ + "bdk_core", + "electrum-client", +] + +[[package]] +name = "bdk_esplora" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9f5961444b5f51b9c3937e729a212363d0e4cde6390ded6e01e16292078df4" +dependencies = [ + "bdk_core", + "esplora-client", +] + +[[package]] +name = "bdk_kyoto" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da8010d2587aba368afd0be83ad87563f465744eb124cda431905cf06f8bd1c" +dependencies = [ + "bdk_wallet", + "bip157", +] + +[[package]] +name = "bdk_wallet" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03f1e31ccc562f600981f747d2262b84428cbff52c9c9cdf14d15fb15bd2286" +dependencies = [ + "bdk_chain", + "bip39", + "bitcoin", + "miniscript", + "rand_core", + "serde", + "serde_json", +] + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bip157" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88df5c18baaea9be4219679afbd4fc26491606f89f6ecdaffcdcabd67635b07b" +dependencies = [ + "bip324", + "bitcoin", + "bitcoin-address-book", + "tokio", +] + +[[package]] +name = "bip324" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53157fcb2d6ec2851c7602d0690536d0b79209e393972cb2b36bd5d72dbd1879" +dependencies = [ + "bitcoin", + "bitcoin_hashes 0.15.0", + "chacha20-poly1305", + "rand", + "tokio", +] + +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes 0.14.1", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin" +version = "0.32.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" +dependencies = [ + "base58ck", + "base64 0.21.7", + "bech32", + "bitcoin-internals 0.3.0", + "bitcoin-io 0.1.4", + "bitcoin-units", + "bitcoin_hashes 0.14.1", + "hex-conservative 0.2.2", + "hex_lit", + "secp256k1", + "serde", +] + +[[package]] +name = "bitcoin-address-book" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060c05780195789a7b89bbfe7f57a1a8cd6ae0bb3daa9b96eeca4fbe0ba8014f" +dependencies = [ + "bitcoin", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90bbbfa552b49101a230fb2668f3f9ef968c81e6f83cf577e1d4b80f689e1aa" + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin-io" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26792cd2bf245069a1c5acb06aa7ad7abe1de69b507c90b490bca81e0665d0ee" +dependencies = [ + "bitcoin-internals 0.4.2", +] + +[[package]] +name = "bitcoin-units" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +dependencies = [ + "bitcoin-internals 0.3.0", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io 0.1.4", + "hex-conservative 0.2.2", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0982261c82a50d89d1a411602afee0498b3e0debe3d36693f0c661352809639" +dependencies = [ + "bitcoin-io 0.2.0", + "hex-conservative 0.3.1", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cc" +version = "1.2.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20-poly1305" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4b0fc281743d80256607bd65e8beedc42cb0787ea119c85b81b4c0eab85e5f" + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "electrum-client" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5059f13888a90486e7268bbce59b175f5f76b1c55e5b9c568ceaa42d2b8507c" +dependencies = [ + "bitcoin", + "byteorder", + "libc", + "log", + "rustls 0.23.36", + "serde", + "serde_json", + "webpki-roots", + "winapi", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "esplora-client" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0af349d96a5d9ad77ba59f1437aa6f348b03c5865d4f7d6e7a662d60aedce39" +dependencies = [ + "bitcoin", + "hex-conservative 0.2.2", + "log", + "minreq", + "serde", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "genco" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35958104272e516c2a5f66a9d82fba4784d2b585fc1e2358b8f96e15d342995" +dependencies = [ + "genco-macros", + "relative-path", + "smallvec", +] + +[[package]] +name = "genco-macros" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eaff6bbc0b3a878361aced5ec6a2818ee7c541c5b33b5880dfa9a86c23e9e7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b9348ee0d8d4e3a894946c1ab104d08a2e44ca13656613afada8905ea609b6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniscript" +version = "12.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c" +dependencies = [ + "bech32", + "bitcoin", + "serde", +] + +[[package]] +name = "minreq" +version = "2.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" +dependencies = [ + "base64 0.22.1", + "rustls 0.21.12", + "rustls-webpki 0.101.7", + "serde", + "serde_json", + "webpki-roots", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes 0.14.1", + "rand", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringcase" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04028eeb851ed08af6aba5caa29f2d59a13ed168cee4d6bd753aeefcf1d636b0" + +[[package]] +name = "stringcase" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72abeda133c49d7bddece6c154728f83eec8172380c80ab7096da9487e20d27c" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "uniffi" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c866f627c3f04c3df068b68bb2d725492caaa539dd313e2a9d26bb85b1a32f4e" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", + "uniffi_bindgen", + "uniffi_build", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi-dart" +version = "0.1.0" +source = "git+https://github.com/Uniffi-Dart/uniffi-dart#5bdcc790c3fc99845e7b4a61d7a4f6e1460896e9" +dependencies = [ + "anyhow", + "camino", + "genco", + "heck", + "lazy_static", + "paste", + "proc-macro2", + "serde", + "stringcase 0.4.0", + "toml", + "uniffi", + "uniffi_bindgen", + "uniffi_build", + "uniffi_dart_macro", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c8ca600167641ebe7c8ba9254af40492dda3397c528cc3b2f511bd23e8541a5" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "indexmap", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_build" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e55c05228f4858bb258f651d21d743fcc1fe5a2ec20d3c0f9daefddb105ee4d" +dependencies = [ + "anyhow", + "camino", + "uniffi_bindgen", +] + +[[package]] +name = "uniffi_core" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7a5a038ebffe8f4cf91416b154ef3c2468b18e828b7009e01b1b99938089f9" +dependencies = [ + "anyhow", + "async-compat", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_dart_macro" +version = "0.1.0" +source = "git+https://github.com/Uniffi-Dart/uniffi-dart#5bdcc790c3fc99845e7b4a61d7a4f6e1460896e9" +dependencies = [ + "futures", + "proc-macro2", + "quote", + "stringcase 0.3.0", + "syn 1.0.109", + "uniffi", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c2a6f93e7b73726e2015696ece25ca0ac5a5f1cf8d6a7ab5214dd0a01d2edf" +dependencies = [ + "anyhow", + "indexmap", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "uniffi_macros" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c6309fc36c7992afc03bc0c5b059c656bccbef3f2a4bc362980017f8936141" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.114", + "toml", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a138823392dba19b0aa494872689f97d0ee157de5852e2bec157ce6de9cdc22" +dependencies = [ + "anyhow", + "siphasher", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c27c4b515d25f8e53cc918e238c39a79c3144a40eaf2e51c4a7958973422c29" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0adacdd848aeed7af4f5af7d2f621d5e82531325d405e29463482becfdeafca" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" diff --git a/pubspec.yaml b/pubspec.yaml index 1499649..028581c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,10 +7,27 @@ environment: dependencies: ffi: ^2.1.4 + code_assets: ^1.0.0 + convert: ^3.1.2 + crypto: ^3.0.7 + ed25519_edwards: ^0.3.1 + http: ^1.2.2 hooks: ^1.0.0 + logging: ^1.0.0 native_toolchain_rust: ^1.0.0 + path: ^1.9.0 + toml: ^0.17.0 + yaml: ^3.1.2 dev_dependencies: test: ^1.26.2 hooks: build: hook/build.dart + +executables: + build_tool: build_tool + +bdk_dart: + precompiled_binaries: + artifact_host: bitcoindevkit/bdk-dart + public_key: 4a13d74312b96c0b8a98f9cc48540c8bf083a47a34bd3ff691a940bef44ee4dc diff --git a/test/precompiled/cargo_test.dart b/test/precompiled/cargo_test.dart new file mode 100644 index 0000000..52549f3 --- /dev/null +++ b/test/precompiled/cargo_test.dart @@ -0,0 +1,22 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import 'package:bdk_dart/src/precompiled/cargo.dart'; + +void main() { + test('CrateInfo.load extracts the package name', () { + final dir = Directory.systemTemp.createTempSync('cargo-test-'); + addTearDown(() => dir.deleteSync(recursive: true)); + + final manifest = File(path.join(dir.path, 'Cargo.toml')); + manifest.writeAsStringSync(''' +[package] +name = "bdk_dart_ffi" +'''); + + final info = CrateInfo.load(dir.path); + expect(info.packageName, 'bdk_dart_ffi'); + }); +} diff --git a/test/precompiled/crate_hash_test.dart b/test/precompiled/crate_hash_test.dart new file mode 100644 index 0000000..05d28fa --- /dev/null +++ b/test/precompiled/crate_hash_test.dart @@ -0,0 +1,92 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; + +import 'package:bdk_dart/src/precompiled/crate_hash.dart'; + +void main() { + group('CrateHash', () { + test('produces stable hash when pubspec config order changes', () { + final root = Directory.systemTemp.createTempSync('crate-hash-'); + addTearDown(() => root.deleteSync(recursive: true)); + + final crateDir = _prepareCrate(root); + final firstHash = CrateHash.compute(crateDir.path); + + final pubspec = File(path.join(root.path, 'pubspec.yaml')); + pubspec.writeAsStringSync(''' +name: sample +bdk_dart: + precompiled_binaries: + public_key: ${_hexKey(64)} + artifact_host: owner/repo +'''); + + final secondHash = CrateHash.compute(crateDir.path); + expect(secondHash, equals(firstHash)); + }); + + test('hash changes when the precompiled configuration changes', () { + final root = Directory.systemTemp.createTempSync('crate-hash-'); + addTearDown(() => root.deleteSync(recursive: true)); + + final crateDir = _prepareCrate(root); + final original = CrateHash.compute(crateDir.path); + + final pubspec = File(path.join(root.path, 'pubspec.yaml')); + pubspec.writeAsStringSync(''' +name: sample +bdk_dart: + precompiled_binaries: + artifact_host: other/repo + public_key: ${_hexKey(64)} +'''); + + final updated = CrateHash.compute(crateDir.path); + expect(updated, isNot(equals(original))); + }); + + test('collectFiles sees cargo manifest and src code', () { + final root = Directory.systemTemp.createTempSync('crate-hash-'); + addTearDown(() => root.deleteSync(recursive: true)); + + final crateDir = _prepareCrate(root); + final files = CrateHash.collectFiles(crateDir.path); + final basenames = files.map((file) => path.basename(file.path)).toSet(); + + expect(basenames, contains('Cargo.toml')); + expect( + files.any((file) => file.path.contains(path.join('src', 'lib.rs'))), + isTrue, + ); + }); + }); +} + +Directory _prepareCrate(Directory root) { + final crateDir = Directory(path.join(root.path, 'native')); + crateDir.createSync(recursive: true); + + File( + path.join(crateDir.path, 'Cargo.toml'), + ).writeAsStringSync('[package]\nname = "bdk_test"\nversion = "0.1.0"\n'); + File(path.join(crateDir.path, 'Cargo.lock')).writeAsStringSync('# empty\n'); + File(path.join(crateDir.path, 'build.rs')).writeAsStringSync(''); + + final srcFile = File(path.join(crateDir.path, 'src', 'lib.rs')); + srcFile.createSync(recursive: true); + srcFile.writeAsStringSync('pub fn hello() {}\n'); + + File(path.join(root.path, 'pubspec.yaml')).writeAsStringSync(''' +name: sample +bdk_dart: + precompiled_binaries: + artifact_host: owner/repo + public_key: ${_hexKey(64)} +'''); + + return crateDir; +} + +String _hexKey(int length) => ''.padLeft(length, 'a'); diff --git a/test/precompiled/options_test.dart b/test/precompiled/options_test.dart new file mode 100644 index 0000000..8727b05 --- /dev/null +++ b/test/precompiled/options_test.dart @@ -0,0 +1,103 @@ +import 'dart:io'; + +import 'package:bdk_dart/src/precompiled/options.dart'; +import 'package:path/path.dart' as path; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +void main() { + group('PrecompiledBinariesConfig', () { + test( + 'normalizes artifact_host into owner/repo and respects mode aliases', + () { + final node = + loadYamlNode(''' +artifact_host: https://github.com/bitcoindevkit/bdk-dart/ +public_key: ${_hexKey(64)} +mode: download +''') + as YamlMap; + + final config = PrecompiledBinariesConfig.parse(node); + expect(config.mode, PrecompiledBinaryMode.always); + expect(config.artifactHost, 'bitcoindevkit/bdk-dart'); + + final fileUrl = config.fileUrl(crateHash: 'abc123', fileName: 'asset'); + expect( + fileUrl.toString(), + 'https://github.com/bitcoindevkit/bdk-dart/releases/download/precompiled_abc123/asset', + ); + }, + ); + + test('uses url_prefix when configured and leaves artifact host empty', () { + final node = + loadYamlNode(''' +url_prefix: http://example.com/exports/ +public_key: ${_hexKey(64)} +''') + as YamlMap; + + final config = PrecompiledBinariesConfig.parse(node); + expect(config.artifactHost, isEmpty); + + final fileUrl = config.fileUrl(crateHash: 'hash', fileName: 'binary'); + expect(fileUrl.toString(), 'http://example.com/exports/hash/binary'); + }); + }); + + group('PubspecOptions', () { + test('returns null when pubspec is missing', () { + final root = Directory.systemTemp.createTempSync('precompiled-options-'); + addTearDown(() => root.deleteSync(recursive: true)); + + final options = PubspecOptions.load( + packageRoot: root.path, + pluginConfigKey: 'bdk_dart', + ); + expect(options.precompiledBinaries, isNull); + }); + + test('loads precompiled configuration from pubspec', () { + final root = Directory.systemTemp.createTempSync('precompiled-options-'); + addTearDown(() => root.deleteSync(recursive: true)); + + final pubspecFile = File(path.join(root.path, 'pubspec.yaml')); + pubspecFile.writeAsStringSync(''' +name: sample +bdk_dart: + precompiled_binaries: + artifact_host: bitcoindevkit/bdk-dart + public_key: ${_hexKey(64)} +'''); + + final options = PubspecOptions.load( + packageRoot: root.path, + pluginConfigKey: 'bdk_dart', + ); + expect(options.precompiledBinaries, isNotNull); + expect(options.precompiledBinaries!.artifactHost, 'bitcoindevkit/bdk-dart'); + }); + + test('loadModeOverride honors invoker pubspec mode overrides', () { + final root = Directory.systemTemp.createTempSync('precompiled-mode-'); + addTearDown(() => root.deleteSync(recursive: true)); + + final pubspecFile = File(path.join(root.path, 'pubspec.yaml')); + pubspecFile.writeAsStringSync(''' +name: invoker +bdk_dart: + precompiled_binaries: + mode: download +'''); + + final mode = PubspecOptions.loadModeOverride( + packageRoot: root.path, + packageName: 'bdk_dart', + ); + expect(mode, PrecompiledBinaryMode.always); + }); + }); +} + +String _hexKey(int length) => ''.padLeft(length, 'a');